Add feature: cancel scheduling
MODIFY
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/CoinVerifierTests.cs
WalletWasabi.Tests/UnitTests/WabiSabi/Backend/CoinVerifierTests.cs
@@ -83,7 +83,9 @@ public class CoinVerifierTests
List<Coin> removedCoins = new();
List<Coin> checkedCoins = new();
- ScheduleVerifications(coinVerifier, generatedCoins);
+ ScheduleVerifications(coinVerifier, generatedCoins, TimeSpan.FromSeconds(2));
+ coinVerifier.CancelSchedule(generatedCoins[9]);
+
foreach (var item in await coinVerifier.VerifyCoinsAsync(generatedCoins, CancellationToken.None).ConfigureAwait(false))
{
checkedCoins.Add(item.Coin);
@@ -98,7 +100,7 @@ public class CoinVerifierTests
}
Assert.Equal(10, checkedCoins.Count);
- Assert.Single(removedCoins);
+ Assert.Equal(2, removedCoins.Count);
Assert.Single(naughtyCoins);
}
@@ -224,11 +226,11 @@ public class CoinVerifierTests
return coins;
}
- private void ScheduleVerifications(CoinVerifier coinVerifier, IEnumerable<Coin> coins)
+ private void ScheduleVerifications(CoinVerifier coinVerifier, IEnumerable<Coin> coins, TimeSpan? delay = null)
{
foreach (Coin coin in coins)
{
- coinVerifier.ScheduleVerification(coin, CancellationToken.None, confirmations: _wabisabiTestConfig.CoinVerifierRequiredConfirmation);
+ coinVerifier.ScheduleVerification(coin, CancellationToken.None, delay, confirmations: _wabisabiTestConfig.CoinVerifierRequiredConfirmation);
}
}
}

MODIFY
WalletWasabi/WabiSabi/Backend/Banning/CoinVerifier.cs
WalletWasabi/WabiSabi/Backend/Banning/CoinVerifier.cs
@@ -27,7 +27,7 @@ public class CoinVerifier
private WabiSabiConfig WabiSabiConfig { get; }
private CoinJoinIdStore CoinJoinIdStore { get; }
private CoinVerifierApiClient CoinVerifierApiClient { get; }
- private ConcurrentDictionary<Coin, (DateTimeOffset ScheduleTime, TaskCompletionSource<CoinVerifyResult> TaskCompletionSource)> CoinVerifyItems { get; } = new();
+ private ConcurrentDictionary<Coin, (DateTimeOffset ScheduleTime, TaskCompletionSource<CoinVerifyResult> TaskCompletionSource, CancellationTokenSource AbortCts)> CoinVerifyItems { get; } = new();
public event EventHandler<Coin>? CoinBlackListed;
@@ -70,7 +70,8 @@ public class CoinVerifier
var result = await completedTask.WaitAsync(linkedCts.Token).ConfigureAwait(false);
// The verification task fulfilled its purpose - clean up.
- CoinVerifyItems.TryRemove(result.Coin, out var _);
+ CoinVerifyItems.TryRemove(result.Coin, out var item);
+ item.AbortCts.Dispose();
// Update the default value with the real result.
coinVerifyItems[result.Coin] = result;
@@ -110,6 +111,8 @@ public class CoinVerifier
{
Logger.LogError($"Unfinished task was removed for coin: '{coin.Outpoint}'.");
}
+
+ item.AbortCts.Dispose();
}
}
}
@@ -146,10 +149,12 @@ public class CoinVerifier
public void ScheduleVerification(Coin coin, CancellationToken cancellationToken, TimeSpan? delayedStart = null, bool oneHop = false, int? confirmations = null)
{
TaskCompletionSource<CoinVerifyResult> taskCompletionSource = new();
+ var abortCts = new CancellationTokenSource();
- if (!CoinVerifyItems.TryAdd(coin, (DateTimeOffset.UtcNow, taskCompletionSource)))
+ if (!CoinVerifyItems.TryAdd(coin, (DateTimeOffset.UtcNow, taskCompletionSource, abortCts)))
{
Logger.LogWarning("Coin was already scheduled for verification.");
+ abortCts.Dispose();
return;
}
@@ -183,7 +188,7 @@ public class CoinVerifier
_ = Task.Run(async () =>
{
using CancellationTokenSource absoluteTimeoutCts = new(AbsoluteScheduleSanityTimeout);
- using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, absoluteTimeoutCts.Token);
+ using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, absoluteTimeoutCts.Token, abortCts.Token);
try
{
@@ -198,9 +203,14 @@ public class CoinVerifier
if (delay > TimeSpan.Zero)
{
- await Task.Delay(delay, linkedCts.Token).ConfigureAwait(false);
+ // We only abort and throw from the delay. If the API request already started, we will go with it.
+ using CancellationTokenSource delayCts = CancellationTokenSource.CreateLinkedTokenSource(linkedCts.Token, abortCts.Token);
+ await Task.Delay(delay, delayCts.Token).ConfigureAwait(false);
}
+ // This is the last chance to abort with abortCts.
+ abortCts.Token.ThrowIfCancellationRequested();
+
var apiResponseItem = await CoinVerifierApiClient.SendRequestAsync(coin.ScriptPubKey, linkedCts.Token).ConfigureAwait(false);
var shouldBan = CheckForFlags(apiResponseItem);
@@ -225,4 +235,12 @@ public class CoinVerifier
}
}, cancellationToken);
}
+
+ public void CancelSchedule(Coin coin)
+ {
+ if (CoinVerifyItems.TryGetValue(coin, out var item) && !item.AbortCts.IsCancellationRequested)
+ {
+ item.AbortCts.Cancel();
+ }
+ }
}

MODIFY
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
WalletWasabi/WabiSabi/Backend/Rounds/Arena.cs
@@ -542,7 +542,14 @@ public partial class Arena : PeriodicRunner
{
foreach (var round in Rounds.Where(x => !x.IsInputRegistrationEnded(Config.MaxInputCountByRound)).ToArray())
{
- var removedAliceCount = round.Alices.RemoveAll(x => x.Deadline < DateTimeOffset.UtcNow);
+ var alicesToRemove = round.Alices.Where(x => x.Deadline < DateTimeOffset.UtcNow).ToArray();
+ foreach (var alice in alicesToRemove)
+ {
+ round.Alices.Remove(alice);
+ CoinVerifier?.CancelSchedule(alice.Coin);
+ }
+
+ var removedAliceCount = alicesToRemove.Length;
if (removedAliceCount > 0)
{
round.LogInfo($"{removedAliceCount} alices timed out and removed.");

Add feature: cancel scheduling