Compare commits

...

5 Commits

Author SHA1 Message Date
Robert
71552f2417 incr ver 2025-10-23 09:45:55 -04:00
rmcrackan
5b96f96a80 Merge pull request #1390 from Mbucari/master
Bug Fixes and attempts to solve random crashes
2025-10-23 09:43:23 -04:00
Michael Bucari-Tovo
afffeb953c Enforce sequential access to DbContext. 2025-10-22 09:22:05 -06:00
Mbucari
f4d8685058 Merge branch 'rmcrackan:master' into master 2025-10-22 09:16:49 -06:00
Michael Bucari-Tovo
39e9f675d2 Fix possible race error on startup with autoscan 2025-08-26 10:00:21 -06:00
6 changed files with 130 additions and 4 deletions

View File

@@ -2,7 +2,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Version>12.5.5.1</Version>
<Version>12.5.6.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Octokit" Version="14.0.0" />

View File

@@ -9,7 +9,7 @@ namespace ApplicationServices
{
/// <summary>Use for fully functional context, incl. SaveChanges(). For query-only, use the other method</summary>
public static LibationContext GetContext()
=> LibationContext.Create(SqliteStorage.ConnectionString);
=> InstanceQueue<LibationContext>.WaitToCreateInstance(() => LibationContext.Create(SqliteStorage.ConnectionString));
/// <summary>Use for full library querying. No lazy loading</summary>
public static List<LibraryBook> GetLibrary_Flat_NoTracking(bool includeParents = false)

View File

@@ -0,0 +1,103 @@
using System;
using System.Diagnostics;
using System.Threading;
#nullable enable
namespace DataLayer;
/// <summary> Notifies clients that the object is being disposed. </summary>
public interface INotifyDisposed : IDisposable
{
/// <summary> Event raised when the object is disposed. </summary>
event EventHandler? ObjectDisposed;
}
/// <summary> Creates a single instance of <typeparamref name="TDisposable"/> at a time, blocking subsequent creations until the previous creations are disposed. </summary>
public static class InstanceQueue<TDisposable> where TDisposable : INotifyDisposed
{
/// <summary> Synchronization object for access to <see cref="LastInLine"/>"/> </summary>
private static Lock Locker { get; } = new();
/// <summary> Ticket holder for the last instance creator in line. </summary>
private static Ticket? LastInLine { get; set; }
/// <summary> Waits for all previously created instances of <typeparamref name="TDisposable"/> to be disposed, then creates and returns a new instance of <typeparamref name="TDisposable"/> using the provided <paramref name="creator"/> factory. </summary>
public static TDisposable WaitToCreateInstance(Func<TDisposable> creator)
{
Ticket ticket;
lock (Locker)
{
ticket = LastInLine = new Ticket(creator, LastInLine);
}
return ticket.Fulfill();
}
/// <summary> A queue ticket for an instance creator. </summary>
/// <param name="creator">Factory to create a new instance of <typeparamref name="TDisposable"/></param>
/// <param name="inFront">The ticket immediately in preceding this new ticket. This new ticket must wait for <paramref name="inFront"/> to signal its instance has been disposed before creating a new instance of <typeparamref name="TDisposable"/></param>
private class Ticket(Func<TDisposable> creator, Ticket? inFront) : IDisposable
{
/// <summary> Factory to create a new instance of <typeparamref name="TDisposable"/> </summary>
private Func<TDisposable> Creator { get; } = creator;
/// <summary> Ticket immediately in front of this one. This instance must wait for <see cref="InFront"/> to signal its instance has been disposed before creating a new instance of <typeparamref name="TDisposable"/></summary>
private Ticket? InFront { get; } = inFront;
/// <summary> Wait handle to signal when this ticket's created instance is disposed </summary>
private EventWaitHandle WaitHandle { get; } = new(false, EventResetMode.ManualReset);
/// <summary> This ticket's created instance of <typeparamref name="TDisposable"/> </summary>
private TDisposable? CreatedInstance { get; set; }
/// <summary> Disposes of this ticket and every ticket queued in front of it. </summary>
public void Dispose()
{
WaitHandle.Dispose();
InFront?.Dispose();
}
/// <summary>
/// Waits for the <see cref="InFront"/> ticket's instance to be disposed, then creates and returns a new instance of <typeparamref name="TDisposable"/> using the <see cref="Creator"/> factory.
/// </summary>
public TDisposable Fulfill()
{
#if DEBUG
var sw = Stopwatch.StartNew();
#endif
//Wait for the previous ticket's instance to be disposed, then dispose of the previous ticket.
InFront?.WaitHandle.WaitOne(Timeout.Infinite);
InFront?.Dispose();
#if DEBUG
sw.Stop();
Debug.WriteLine($"Waited {sw.ElapsedMilliseconds}ms to create instance of {typeof(TDisposable).Name}");
#endif
CreatedInstance = Creator();
CreatedInstance.ObjectDisposed += CreatedInstance_ObjectDisposed;
return CreatedInstance;
}
private void CreatedInstance_ObjectDisposed(object? sender, EventArgs e)
{
Debug.WriteLine($"{typeof(TDisposable).Name} Disposed");
if (CreatedInstance is not null)
{
CreatedInstance.ObjectDisposed -= CreatedInstance_ObjectDisposed;
CreatedInstance = default;
}
lock (Locker)
{
if (this == LastInLine)
{
//There are no ticket holders waiting after this one.
//This ticket is fulfilled and will never be waited on.
LastInLine = null;
Dispose();
}
else
{
//Signal the that this ticket has been fulfilled so that
//the next ticket in line may proceed.
WaitHandle.Set();
}
}
}
}
}

View File

@@ -1,9 +1,11 @@
using DataLayer.Configurations;
using Microsoft.EntityFrameworkCore;
using System;
using System.Threading.Tasks;
namespace DataLayer
{
public class LibationContext : DbContext
public class LibationContext : DbContext, INotifyDisposed
{
// IMPORTANT: USING DbSet<>
// ========================
@@ -25,6 +27,18 @@ namespace DataLayer
public DbSet<Category> Categories { get; private set; }
public DbSet<CategoryLadder> CategoryLadders { get; private set; }
public event EventHandler ObjectDisposed;
public override void Dispose()
{
base.Dispose();
ObjectDisposed?.Invoke(this, EventArgs.Empty);
}
public override async ValueTask DisposeAsync()
{
await base.DisposeAsync();
ObjectDisposed?.Invoke(this, EventArgs.Empty);
}
public static LibationContext Create(string connectionString)
{
var factory = new LibationContextFactory();

View File

@@ -12,6 +12,7 @@ namespace LibationAvalonia.ViewModels
{
public partial class MainVM : ViewModelBase
{
public Task? BindToGridTask { get; set; }
public ProcessQueueViewModel ProcessQueue { get; } = new ProcessQueueViewModel();
public ProductsDisplayViewModel ProductsDisplay { get; } = new ProductsDisplayViewModel();
@@ -43,6 +44,13 @@ namespace LibationAvalonia.ViewModels
{
try
{
//Prevent race condition which can occur if an auto-scan
//completes before the initial grid binding completes.
if (BindToGridTask is null)
return;
else if (BindToGridTask.IsCompleted is false)
await BindToGridTask;
await Task.WhenAll(
SetBackupCountsAsync(fullLibrary),
Task.Run(() => ProductsDisplay.UpdateGridAsync(fullLibrary)));

View File

@@ -165,9 +165,10 @@ namespace LibationAvalonia.Views
if (QuickFilters.UseDefault)
await vm.PerformFilter(QuickFilters.Filters.FirstOrDefault());
await Task.WhenAll(
ViewModel.BindToGridTask = Task.WhenAll(
vm.SetBackupCountsAsync(initialLibrary),
Task.Run(() => vm.ProductsDisplay.BindToGridAsync(initialLibrary)));
await ViewModel.BindToGridTask;
}
public void ProductsDisplay_LiberateClicked(object _, LibraryBook[] libraryBook) => ViewModel.LiberateClicked(libraryBook);