diff --git a/app/Desktop/Main/Controls/MessageFilterPanelModel.cs b/app/Desktop/Main/Controls/MessageFilterPanelModel.cs index 06bfc7c..2e03ba7 100644 --- a/app/Desktop/Main/Controls/MessageFilterPanelModel.cs +++ b/app/Desktop/Main/Controls/MessageFilterPanelModel.cs @@ -153,7 +153,7 @@ namespace DHT.Desktop.Main.Controls { else { exportedMessageCount = null; UpdateFilterStatisticsText(); - exportedMessageCountComputer.Compute(_ => db.CountMessages(filter)); + exportedMessageCountComputer.Compute(() => db.CountMessages(filter)); } } diff --git a/app/Server/Database/Sqlite/SqliteDatabaseFile.cs b/app/Server/Database/Sqlite/SqliteDatabaseFile.cs index 247ea5d..2f52c79 100644 --- a/app/Server/Database/Sqlite/SqliteDatabaseFile.cs +++ b/app/Server/Database/Sqlite/SqliteDatabaseFile.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Text; -using System.Threading; using System.Threading.Tasks; using DHT.Server.Data; using DHT.Server.Data.Filters; @@ -458,7 +457,7 @@ LEFT JOIN replied_to rt ON m.message_id = rt.message_id" + filter.GenerateWhereC Statistics.TotalUsers = conn.SelectScalar("SELECT COUNT(*) FROM users") as long? ?? 0; } - private long ComputeMessageStatistics(CancellationToken token) { + private long ComputeMessageStatistics() { using var conn = pool.Take(); return conn.SelectScalar("SELECT COUNT(*) FROM messages") as long? ?? 0L; } diff --git a/app/Utils/Tasks/AsyncValueComputer.cs b/app/Utils/Tasks/AsyncValueComputer.cs index 8bb94cd..f8fec46 100644 --- a/app/Utils/Tasks/AsyncValueComputer.cs +++ b/app/Utils/Tasks/AsyncValueComputer.cs @@ -11,8 +11,10 @@ namespace DHT.Utils.Tasks { private readonly object stateLock = new (); - private CancellationTokenSource? currentCancellationTokenSource; - private Func? currentComputeFunction; + private SoftHardCancellationToken? currentCancellationTokenSource; + private bool wasHardCancelled = false; + + private Func? currentComputeFunction; private bool hasComputeFunctionChanged = false; private AsyncValueComputer(Action resultProcessor, TaskScheduler resultTaskScheduler, bool processOutdatedResults) { @@ -21,12 +23,21 @@ namespace DHT.Utils.Tasks { this.processOutdatedResults = processOutdatedResults; } - public void Compute(Func func) { + public void Cancel() { lock (stateLock) { + wasHardCancelled = true; + currentCancellationTokenSource?.RequestHardCancellation(); + } + } + + public void Compute(Func func) { + lock (stateLock) { + wasHardCancelled = false; + if (currentComputeFunction != null) { currentComputeFunction = func; hasComputeFunctionChanged = true; - currentCancellationTokenSource?.Cancel(); + currentCancellationTokenSource?.RequestSoftCancellation(); } else { EnqueueComputation(func); @@ -35,23 +46,22 @@ namespace DHT.Utils.Tasks { } [SuppressMessage("ReSharper", "MethodSupportsCancellation")] - private void EnqueueComputation(Func func) { - var cancellationTokenSource = new CancellationTokenSource(); - var cancellationToken = cancellationTokenSource.Token; + private void EnqueueComputation(Func func) { + var cancellationTokenSource = new SoftHardCancellationToken(); - currentCancellationTokenSource?.Cancel(); + currentCancellationTokenSource?.RequestSoftCancellation(); currentCancellationTokenSource = cancellationTokenSource; currentComputeFunction = func; hasComputeFunctionChanged = false; - var task = Task.Run(() => func(cancellationToken)); - + var task = Task.Run(func); + task.ContinueWith(t => { - if (processOutdatedResults || !cancellationToken.IsCancellationRequested) { + if (!cancellationTokenSource.IsCancelled(processOutdatedResults)) { resultProcessor(t.Result); } }, CancellationToken.None, TaskContinuationOptions.NotOnFaulted, resultTaskScheduler); - + task.ContinueWith(_ => { lock (stateLock) { cancellationTokenSource.Dispose(); @@ -60,11 +70,12 @@ namespace DHT.Utils.Tasks { currentCancellationTokenSource = null; } - if (hasComputeFunctionChanged) { + if (hasComputeFunctionChanged && !wasHardCancelled) { EnqueueComputation(currentComputeFunction); } else { currentComputeFunction = null; + hasComputeFunctionChanged = false; } } }); @@ -72,9 +83,9 @@ namespace DHT.Utils.Tasks { public sealed class Single { private readonly AsyncValueComputer baseComputer; - private readonly Func resultComputer; + private readonly Func resultComputer; - internal Single(AsyncValueComputer baseComputer, Func resultComputer) { + internal Single(AsyncValueComputer baseComputer, Func resultComputer) { this.baseComputer = baseComputer; this.resultComputer = resultComputer; } @@ -107,7 +118,7 @@ namespace DHT.Utils.Tasks { return new AsyncValueComputer(resultProcessor, resultTaskScheduler, processOutdatedResults); } - public Single BuildWithComputer(Func resultComputer) { + public Single BuildWithComputer(Func resultComputer) { return new Single(Build(), resultComputer); } } diff --git a/app/Utils/Tasks/SoftHardCancellationToken.cs b/app/Utils/Tasks/SoftHardCancellationToken.cs new file mode 100644 index 0000000..866d54e --- /dev/null +++ b/app/Utils/Tasks/SoftHardCancellationToken.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading; + +namespace DHT.Utils.Tasks { + /// + /// Manages a pair of cancellation tokens that follow these rules: + /// + /// If the soft token is cancelled, the hard token remains uncancelled. + /// If the hard token is cancelled, the soft token is also cancelled. + /// + /// + sealed class SoftHardCancellationToken : IDisposable { + private readonly CancellationTokenSource soft; + private readonly CancellationTokenSource hard; + + public SoftHardCancellationToken() { + this.soft = new CancellationTokenSource(); + this.hard = new CancellationTokenSource(); + } + + public bool IsCancelled(bool onlyHardCancellation) { + return (onlyHardCancellation ? hard : soft).IsCancellationRequested; + } + + public void RequestSoftCancellation() { + soft.Cancel(); + } + + public void RequestHardCancellation() { + soft.Cancel(); + hard.Cancel(); + } + + public void Dispose() { + soft.Dispose(); + hard.Dispose(); + } + } +}