elsa-core/test/unit/Elsa.Workflows.Runtime.UnitTests/Services/BookmarkQueueDeadLetterTests.cs
Sipke Schoorstra e77da02d52
Fix workflow commit atomicity
Wrap SQL Server EF Core workflow commits in a provider-safe transaction and buffer commit notifications until persistence succeeds. Also preserve dirty activity context state until commit success and make bookmark queue retries resilient to processed-row deletion and transient distributed lock misses.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-21 19:58:09 +02:00

818 lines
40 KiB
C#

using Elsa.Common;
using Elsa.Common.Models;
using Elsa.Common.Services;
using Elsa.Extensions;
using Elsa.Workflows.Runtime.Entities;
using Elsa.Workflows.Runtime.Filters;
using Elsa.Workflows.Runtime.Messages;
using Elsa.Workflows.Runtime.Options;
using Elsa.Workflows.Runtime.OrderDefinitions;
using Elsa.Workflows.Runtime.Requests;
using Elsa.Workflows.Runtime.Stores;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using NSubstitute;
namespace Elsa.Workflows.Runtime.UnitTests.Services;
public class BookmarkQueueDeadLetterTests
{
private readonly DateTimeOffset _now = new(2026, 5, 20, 10, 0, 0, TimeSpan.Zero);
private readonly TestClock _clock;
private readonly IIdentityGenerator _identityGenerator = Substitute.For<IIdentityGenerator>();
private readonly IBookmarkQueueSignaler _signaler = Substitute.For<IBookmarkQueueSignaler>();
private readonly MemoryBookmarkQueueStore _queueStore;
private readonly MemoryBookmarkQueueDeadLetterStore _deadLetterStore;
public BookmarkQueueDeadLetterTests()
{
_clock = new(_now);
_identityGenerator.GenerateId().Returns("generated-1", "generated-2", "generated-3");
_queueStore = new(new MemoryStore<BookmarkQueueItem>());
_deadLetterStore = new(new MemoryStore<BookmarkQueueDeadLetterItem>());
}
[Fact]
public async Task PurgeAsync_MovesExpiredQueueItemsToDeadLetterBeforeDeleting()
{
var expired = NewQueueItem("expired", _now.AddMinutes(-2));
var current = NewQueueItem("current", _now);
await _queueStore.AddAsync(expired);
await _queueStore.AddAsync(current);
var purger = CreatePurger(new BookmarkQueuePurgeOptions { Ttl = TimeSpan.FromMinutes(1), DeadLetterTtl = TimeSpan.FromDays(7) });
await purger.PurgeAsync();
var remainingQueueItems = (await _queueStore.FindManyAsync(new BookmarkQueueFilter())).ToList();
var deadLetters = (await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter())).ToList();
Assert.Single(remainingQueueItems);
Assert.Equal("current", remainingQueueItems[0].Id);
Assert.Single(deadLetters);
Assert.Equal("expired", deadLetters[0].OriginalQueueItemId);
Assert.Equal("Expired", deadLetters[0].Reason);
Assert.Equal(_now, deadLetters[0].DeadLetteredAt);
Assert.True(deadLetters[0].CanReplay);
}
[Fact]
public async Task PurgeAsync_DeletesExpiredQueueItemsInBatch()
{
var expired1 = NewQueueItem("expired-1", _now.AddMinutes(-3));
var expired2 = NewQueueItem("expired-2", _now.AddMinutes(-2));
await _queueStore.AddAsync(expired1);
await _queueStore.AddAsync(expired2);
var queueStore = new RecordingDeleteQueueStore(_queueStore);
var purger = new DefaultBookmarkQueuePurger(
queueStore,
_deadLetterStore,
CreateManager(),
_clock,
Microsoft.Extensions.Options.Options.Create(new BookmarkQueuePurgeOptions { Ttl = TimeSpan.FromMinutes(1), DeadLetterTtl = TimeSpan.FromDays(7), BatchSize = 10 }),
NullLogger<DefaultBookmarkQueuePurger>.Instance);
await purger.PurgeAsync();
var deleteFilter = Assert.Single(queueStore.DeleteFilters);
Assert.Null(deleteFilter.Id);
Assert.Equal(["expired-1", "expired-2"], deleteFilter.Ids);
Assert.Empty(await _queueStore.FindManyAsync(new BookmarkQueueFilter()));
Assert.Equal(2, (await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter())).Count());
}
[Fact]
public async Task PurgeAsync_WhenExpiredItemWasAlreadyRemoved_LeavesDeadLetterForAuditOnly()
{
var expired = NewQueueItem("expired", _now.AddMinutes(-2));
await _queueStore.AddAsync(expired);
var queueStore = new DeletingAfterPageQueueStore(_queueStore);
var purger = new DefaultBookmarkQueuePurger(
queueStore,
_deadLetterStore,
CreateManager(),
_clock,
Microsoft.Extensions.Options.Options.Create(new BookmarkQueuePurgeOptions { Ttl = TimeSpan.FromMinutes(1), DeadLetterTtl = TimeSpan.FromDays(7) }),
NullLogger<DefaultBookmarkQueuePurger>.Instance);
await purger.PurgeAsync();
var deadLetter = Assert.Single((await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter())).ToList());
Assert.Equal("expired", deadLetter.OriginalQueueItemId);
Assert.False(deadLetter.CanReplay);
Assert.Null(deadLetter.ReplayedAt);
Assert.Null(deadLetter.ReplayedQueueItemId);
var replayResult = await CreateManager().ReplayAsync(deadLetter.Id);
Assert.False(replayResult.Succeeded);
Assert.Equal(ReplayBookmarkQueueDeadLetterResult.ReasonNotReplayable, replayResult.Reason);
Assert.Empty(await _queueStore.FindManyAsync(new BookmarkQueueFilter()));
await _signaler.DidNotReceive().TriggerAsync(Arg.Any<CancellationToken>());
}
[Fact]
public async Task PurgeAsync_WhenOneExpiredItemWasAlreadyRemoved_KeepsReplayForDeletedItemsOnly()
{
var removed = NewQueueItem("removed", _now.AddMinutes(-3));
var retained = NewQueueItem("retained", _now.AddMinutes(-2));
await _queueStore.AddAsync(removed);
await _queueStore.AddAsync(retained);
var queueStore = new DeletingFirstAfterPageQueueStore(_queueStore);
var purger = new DefaultBookmarkQueuePurger(
queueStore,
_deadLetterStore,
CreateManager(),
_clock,
Microsoft.Extensions.Options.Options.Create(new BookmarkQueuePurgeOptions { Ttl = TimeSpan.FromMinutes(1), DeadLetterTtl = TimeSpan.FromDays(7), BatchSize = 10 }),
NullLogger<DefaultBookmarkQueuePurger>.Instance);
await purger.PurgeAsync();
var deadLetters = (await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter())).OrderBy(x => x.OriginalQueueItemId).ToList();
Assert.Equal(["removed", "retained"], deadLetters.Select(x => x.OriginalQueueItemId));
Assert.False(deadLetters[0].CanReplay);
Assert.True(deadLetters[1].CanReplay);
Assert.Empty(await _queueStore.FindManyAsync(new BookmarkQueueFilter()));
}
[Fact]
public async Task PurgeAsync_WhenExpiredItemAlreadyHasFailedDeadLetter_PreservesReplay()
{
var expired = NewQueueItem("expired", _now.AddMinutes(-2));
await _queueStore.AddAsync(expired);
await _deadLetterStore.AddAsync(new BookmarkQueueDeadLetterItem
{
Id = "failed-dead-letter",
OriginalQueueItemId = expired.Id,
WorkflowInstanceId = expired.WorkflowInstanceId,
BookmarkId = expired.BookmarkId,
StimulusHash = expired.StimulusHash,
ActivityTypeName = expired.ActivityTypeName,
OriginalCreatedAt = expired.CreatedAt,
DeadLetteredAt = _now.AddMinutes(-1),
Reason = "Failed",
DeliveryAttempts = 2,
CanReplay = true
});
var queueStore = new DeletingAfterPageQueueStore(_queueStore);
var purger = new DefaultBookmarkQueuePurger(
queueStore,
_deadLetterStore,
CreateManager(),
_clock,
Microsoft.Extensions.Options.Options.Create(new BookmarkQueuePurgeOptions { Ttl = TimeSpan.FromMinutes(1), DeadLetterTtl = TimeSpan.FromDays(7) }),
NullLogger<DefaultBookmarkQueuePurger>.Instance);
await purger.PurgeAsync();
var deadLetter = Assert.Single((await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter())).ToList());
Assert.Equal("Failed", deadLetter.Reason);
Assert.True(deadLetter.CanReplay);
Assert.Null(deadLetter.ReplayedAt);
Assert.Null(deadLetter.ReplayedQueueItemId);
var replayResult = await CreateManager().ReplayAsync(deadLetter.Id);
Assert.True(replayResult.Succeeded);
Assert.Equal("generated-1", replayResult.QueueItemId);
var replayedItem = await _queueStore.FindAsync(new BookmarkQueueFilter { Id = "generated-1" });
Assert.NotNull(replayedItem);
await _signaler.Received(1).TriggerAsync(Arg.Any<CancellationToken>());
}
[Fact]
public async Task PurgeAsync_WhenDeadLetterWriteFails_DoesNotDeleteQueueItem()
{
var expired = NewQueueItem("expired", _now.AddMinutes(-2));
await _queueStore.AddAsync(expired);
var deadLetterStore = new ThrowingAddDeadLetterStore(_deadLetterStore);
var purger = new DefaultBookmarkQueuePurger(
_queueStore,
deadLetterStore,
CreateManager(deadLetterStore),
_clock,
Microsoft.Extensions.Options.Options.Create(new BookmarkQueuePurgeOptions { Ttl = TimeSpan.FromMinutes(1), DeadLetterTtl = TimeSpan.FromDays(7) }),
NullLogger<DefaultBookmarkQueuePurger>.Instance);
await Assert.ThrowsAsync<InvalidOperationException>(() => purger.PurgeAsync());
var retained = await _queueStore.FindAsync(new BookmarkQueueFilter { Id = "expired" });
Assert.NotNull(retained);
Assert.Empty(await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter()));
}
[Fact]
public async Task PurgeAsync_RemovesDeadLettersAfterRetentionTtl()
{
await _deadLetterStore.AddAsync(new BookmarkQueueDeadLetterItem
{
Id = "old",
OriginalQueueItemId = "queue-old",
DeadLetteredAt = _now.AddDays(-8),
OriginalCreatedAt = _now.AddDays(-9),
Reason = "Expired"
});
var purger = CreatePurger(new BookmarkQueuePurgeOptions { DeadLetterTtl = TimeSpan.FromDays(7) });
await purger.PurgeAsync();
var deadLetters = await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter());
Assert.Empty(deadLetters);
}
[Fact]
public async Task ProcessAsync_DeadLettersQueueItemAfterMaxDeliveryAttempts()
{
var item = NewQueueItem("failed", _now);
await _queueStore.AddAsync(item);
var workflowResumer = new ThrowingWorkflowResumer();
var processor = new BookmarkQueueProcessor(
_queueStore,
CreateManager(),
workflowResumer,
_clock,
Microsoft.Extensions.Options.Options.Create(new BookmarkQueuePurgeOptions { MaxDeliveryAttempts = 2 }),
NullLogger<BookmarkQueueProcessor>.Instance);
await processor.ProcessAsync();
var retained = await _queueStore.FindAsync(new BookmarkQueueFilter { Id = "failed" });
Assert.NotNull(retained);
Assert.Equal(1, retained.DeliveryAttempts);
await processor.ProcessAsync();
Assert.Null(await _queueStore.FindAsync(new BookmarkQueueFilter { Id = "failed" }));
var deadLetter = Assert.Single((await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter())).ToList());
Assert.Equal("failed", deadLetter.OriginalQueueItemId);
Assert.Equal("Failed", deadLetter.Reason);
Assert.Equal(2, deadLetter.DeliveryAttempts);
Assert.Equal(typeof(ApplicationException).FullName, deadLetter.LastErrorType);
Assert.Equal("resume failed", deadLetter.LastErrorMessage);
}
[Fact]
public async Task ProcessAsync_WhenResumeIsCanceled_PropagatesCancellationWithoutIncrementingDeliveryAttempts()
{
var item = NewQueueItem("canceled", _now);
await _queueStore.AddAsync(item);
var processor = new BookmarkQueueProcessor(
_queueStore,
CreateManager(),
new CancelingWorkflowResumer(),
_clock,
Microsoft.Extensions.Options.Options.Create(new BookmarkQueuePurgeOptions { MaxDeliveryAttempts = 2 }),
NullLogger<BookmarkQueueProcessor>.Instance);
await Assert.ThrowsAsync<OperationCanceledException>(() => processor.ProcessAsync());
var retained = await _queueStore.FindAsync(new BookmarkQueueFilter { Id = "canceled" });
Assert.NotNull(retained);
Assert.Equal(0, retained.DeliveryAttempts);
Assert.Empty(await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter()));
}
[Fact]
public async Task ProcessAsync_WhenResumeThrowsBuiltInException_DeadLettersQueueItem()
{
var item = NewQueueItem("built-in-exception", _now);
await _queueStore.AddAsync(item);
var processor = new BookmarkQueueProcessor(
_queueStore,
CreateManager(),
new ThrowingWorkflowResumer(new InvalidOperationException("transient failure")),
_clock,
Microsoft.Extensions.Options.Options.Create(new BookmarkQueuePurgeOptions { MaxDeliveryAttempts = 1 }),
NullLogger<BookmarkQueueProcessor>.Instance);
await processor.ProcessAsync();
Assert.Null(await _queueStore.FindAsync(new BookmarkQueueFilter { Id = "built-in-exception" }));
var deadLetter = Assert.Single((await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter())).ToList());
Assert.Equal("built-in-exception", deadLetter.OriginalQueueItemId);
Assert.Equal("Failed", deadLetter.Reason);
Assert.Equal(1, deadLetter.DeliveryAttempts);
Assert.Equal(typeof(InvalidOperationException).FullName, deadLetter.LastErrorType);
Assert.Equal("transient failure", deadLetter.LastErrorMessage);
}
[Fact]
public async Task ProcessAsync_WhenProcessedItemsAreDeleted_DoesNotSkipNextPage()
{
for (var i = 0; i < 75; i++)
await _queueStore.AddAsync(NewQueueItem($"queued-{i:00}", _now.AddSeconds(i)));
var processor = new BookmarkQueueProcessor(
_queueStore,
CreateManager(),
new SuccessfulWorkflowResumer(),
_clock,
Microsoft.Extensions.Options.Options.Create(new BookmarkQueuePurgeOptions()),
NullLogger<BookmarkQueueProcessor>.Instance);
await processor.ProcessAsync();
Assert.Empty(await _queueStore.FindManyAsync(new BookmarkQueueFilter()));
}
[Fact]
public async Task ReplayAsync_EnqueuesItemAndPreventsSecondReplay()
{
await _deadLetterStore.AddAsync(new BookmarkQueueDeadLetterItem
{
Id = "dead-letter",
OriginalQueueItemId = "original",
WorkflowInstanceId = "workflow-instance",
BookmarkId = "bookmark",
StimulusHash = "hash",
ActivityTypeName = "activity",
OriginalCreatedAt = _now.AddMinutes(-2),
DeadLetteredAt = _now.AddMinutes(-1),
Reason = "Expired",
CanReplay = true
});
var manager = CreateManager();
var result = await manager.ReplayAsync("dead-letter");
Assert.True(result.Succeeded);
Assert.Equal("generated-1", result.QueueItemId);
var queueItem = await _queueStore.FindAsync(new BookmarkQueueFilter { Id = "generated-1" });
Assert.NotNull(queueItem);
Assert.Equal("workflow-instance", queueItem.WorkflowInstanceId);
await _signaler.Received(1).TriggerAsync(Arg.Any<CancellationToken>());
var secondResult = await manager.ReplayAsync("dead-letter");
Assert.False(secondResult.Succeeded);
Assert.Equal("NotReplayable", secondResult.Reason);
await _signaler.Received(1).TriggerAsync(Arg.Any<CancellationToken>());
}
[Fact]
public async Task ReplayAsync_WhenCalledConcurrently_EnqueuesSingleItem()
{
await _deadLetterStore.AddAsync(new BookmarkQueueDeadLetterItem
{
Id = "dead-letter",
OriginalQueueItemId = "original",
WorkflowInstanceId = "workflow-instance",
BookmarkId = "bookmark",
StimulusHash = "hash",
ActivityTypeName = "activity",
OriginalCreatedAt = _now.AddMinutes(-2),
DeadLetteredAt = _now.AddMinutes(-1),
Reason = "Expired",
CanReplay = true
});
var manager = CreateManager();
var results = await Task.WhenAll(
manager.ReplayAsync("dead-letter"),
manager.ReplayAsync("dead-letter"));
Assert.Single(results, x => x.Succeeded);
Assert.Single(results, x => !x.Succeeded && x.Reason == ReplayBookmarkQueueDeadLetterResult.ReasonNotReplayable);
var queueItems = await _queueStore.FindManyAsync(new BookmarkQueueFilter());
Assert.Single(queueItems);
await _signaler.Received(1).TriggerAsync(Arg.Any<CancellationToken>());
}
[Fact]
public async Task ReplayAsync_WhenQueueEnqueueFails_RestoresDeadLetterReplayState()
{
await _deadLetterStore.AddAsync(new BookmarkQueueDeadLetterItem
{
Id = "dead-letter",
OriginalQueueItemId = "original",
WorkflowInstanceId = "workflow-instance",
BookmarkId = "bookmark",
StimulusHash = "hash",
ActivityTypeName = "activity",
OriginalCreatedAt = _now.AddMinutes(-2),
DeadLetteredAt = _now.AddMinutes(-1),
Reason = "Expired",
CanReplay = true
});
var inspectingDeadLetterStore = new InspectingSaveDeadLetterStore(_deadLetterStore, async () =>
{
var marked = await _deadLetterStore.FindAsync(new BookmarkQueueDeadLetterFilter { Id = "dead-letter" });
Assert.NotNull(marked);
Assert.False(marked.CanReplay);
Assert.Equal(_now, marked.ReplayedAt);
Assert.Equal("generated-1", marked.ReplayedQueueItemId);
});
var manager = new BookmarkQueueDeadLetterManager(
inspectingDeadLetterStore,
new ThrowingAddQueueStore(_queueStore),
_signaler,
_clock,
_identityGenerator,
NullLogger<BookmarkQueueDeadLetterManager>.Instance);
await Assert.ThrowsAsync<InvalidOperationException>(() => manager.ReplayAsync("dead-letter"));
var deadLetter = await _deadLetterStore.FindAsync(new BookmarkQueueDeadLetterFilter { Id = "dead-letter" });
Assert.NotNull(deadLetter);
Assert.True(deadLetter.CanReplay);
Assert.Null(deadLetter.ReplayedAt);
Assert.Null(deadLetter.ReplayedQueueItemId);
Assert.Empty(await _queueStore.FindManyAsync(new BookmarkQueueFilter()));
await _signaler.DidNotReceive().TriggerAsync(Arg.Any<CancellationToken>());
var retryResult = await CreateManager().ReplayAsync("dead-letter");
Assert.True(retryResult.Succeeded);
Assert.Equal("generated-2", retryResult.QueueItemId);
}
[Fact]
public async Task DeadLetterAsync_WhenOriginalQueueItemAlreadyDeadLettered_ReturnsExistingItem()
{
var item = NewQueueItem("original", _now.AddMinutes(-2));
var manager = CreateManager();
var first = await manager.DeadLetterAsync(item, "Expired");
var second = await manager.DeadLetterAsync(item, "Failed", new InvalidOperationException("resume failed"));
Assert.Equal(first.Id, second.Id);
var deadLetters = await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter());
Assert.Single(deadLetters);
Assert.Equal("Expired", first.Reason);
}
[Fact]
public async Task DeadLetterAsync_WhenCalledConcurrentlyForSameQueueItem_StoresSingleDeadLetterItem()
{
var item = NewQueueItem("original", _now.AddMinutes(-2));
var deadLetterStore = new CoordinatedFindDeadLetterStore(_deadLetterStore, 2);
var manager = CreateManager(deadLetterStore);
var results = await Task.WhenAll(
manager.DeadLetterAsync(item, "Expired"),
manager.DeadLetterAsync(item, "Failed", new InvalidOperationException("resume failed")));
Assert.Equal(results[0].Id, results[1].Id);
var deadLetters = (await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter())).ToList();
var deadLetter = Assert.Single(deadLetters);
Assert.Equal("original", deadLetter.OriginalQueueItemId);
Assert.Equal(deadLetter.Id, results[0].Id);
}
[Fact]
public async Task MemoryStore_AddAsync_WhenCalledConcurrentlyForSameOriginalQueueItem_StoresSingleDeadLetterItem()
{
var records = Enumerable.Range(1, 20)
.Select(i => new BookmarkQueueDeadLetterItem
{
Id = $"dead-letter-{i}",
OriginalQueueItemId = "original",
OriginalCreatedAt = _now.AddMinutes(-2),
DeadLetteredAt = _now,
Reason = "Expired",
CanReplay = true
})
.ToList();
await Task.WhenAll(records.Select(record => Task.Run(() => _deadLetterStore.AddAsync(record))));
var deadLetter = Assert.Single((await _deadLetterStore.FindManyAsync(new BookmarkQueueDeadLetterFilter())).ToList());
Assert.Equal("original", deadLetter.OriginalQueueItemId);
Assert.Contains(deadLetter.Id, records.Select(x => x.Id));
}
private DefaultBookmarkQueuePurger CreatePurger(BookmarkQueuePurgeOptions options)
{
return new(
_queueStore,
_deadLetterStore,
CreateManager(),
_clock,
Microsoft.Extensions.Options.Options.Create(options),
NullLogger<DefaultBookmarkQueuePurger>.Instance);
}
private BookmarkQueueDeadLetterManager CreateManager(IBookmarkQueueDeadLetterStore? deadLetterStore = null)
{
return new(
deadLetterStore ?? _deadLetterStore,
_queueStore,
_signaler,
_clock,
_identityGenerator,
NullLogger<BookmarkQueueDeadLetterManager>.Instance);
}
private static BookmarkQueueItem NewQueueItem(string id, DateTimeOffset createdAt)
{
return new()
{
Id = id,
WorkflowInstanceId = "workflow-instance",
BookmarkId = "bookmark",
StimulusHash = "hash",
ActivityTypeName = "activity",
CreatedAt = createdAt
};
}
private sealed class TestClock(DateTimeOffset utcNow) : ISystemClock
{
public DateTimeOffset UtcNow { get; } = utcNow;
}
private sealed class ThrowingWorkflowResumer(Exception? exception = null) : IWorkflowResumer
{
private readonly Exception _exception = exception ?? new ApplicationException("resume failed");
public Task<IEnumerable<RunWorkflowInstanceResponse>> ResumeAsync<TActivity>(object stimulus, ResumeBookmarkOptions? options = null, CancellationToken cancellationToken = default) where TActivity : IActivity
{
throw new NotSupportedException();
}
public Task<RunWorkflowInstanceResponse?> ResumeAsync(string bookmarkId, IDictionary<string, object> input, CancellationToken cancellationToken = default)
{
throw new NotSupportedException();
}
public Task<IEnumerable<RunWorkflowInstanceResponse>> ResumeAsync<TActivity>(object stimulus, string? workflowInstanceId = null, ResumeBookmarkOptions? options = null, CancellationToken cancellationToken = default) where TActivity : IActivity
{
throw new NotSupportedException();
}
public Task<RunWorkflowInstanceResponse?> ResumeAsync<TActivity>(string bookmarkId, ResumeBookmarkOptions? options = null, CancellationToken cancellationToken = default) where TActivity : IActivity
{
throw new NotSupportedException();
}
public Task<IEnumerable<RunWorkflowInstanceResponse>> ResumeAsync(ResumeBookmarkRequest request, CancellationToken cancellationToken = default)
{
throw new NotSupportedException();
}
public Task<IEnumerable<RunWorkflowInstanceResponse>> ResumeAsync(BookmarkFilter filter, ResumeBookmarkOptions? options = null, CancellationToken cancellationToken = default)
{
throw _exception;
}
}
private sealed class CancelingWorkflowResumer : IWorkflowResumer
{
public Task<IEnumerable<RunWorkflowInstanceResponse>> ResumeAsync<TActivity>(object stimulus, ResumeBookmarkOptions? options = null, CancellationToken cancellationToken = default) where TActivity : IActivity
{
throw new NotSupportedException();
}
public Task<RunWorkflowInstanceResponse?> ResumeAsync(string bookmarkId, IDictionary<string, object> input, CancellationToken cancellationToken = default)
{
throw new NotSupportedException();
}
public Task<IEnumerable<RunWorkflowInstanceResponse>> ResumeAsync<TActivity>(object stimulus, string? workflowInstanceId = null, ResumeBookmarkOptions? options = null, CancellationToken cancellationToken = default) where TActivity : IActivity
{
throw new NotSupportedException();
}
public Task<RunWorkflowInstanceResponse?> ResumeAsync<TActivity>(string bookmarkId, ResumeBookmarkOptions? options = null, CancellationToken cancellationToken = default) where TActivity : IActivity
{
throw new NotSupportedException();
}
public Task<IEnumerable<RunWorkflowInstanceResponse>> ResumeAsync(ResumeBookmarkRequest request, CancellationToken cancellationToken = default)
{
throw new NotSupportedException();
}
public Task<IEnumerable<RunWorkflowInstanceResponse>> ResumeAsync(BookmarkFilter filter, ResumeBookmarkOptions? options = null, CancellationToken cancellationToken = default)
{
throw new OperationCanceledException(cancellationToken);
}
}
private sealed class SuccessfulWorkflowResumer : IWorkflowResumer
{
public Task<IEnumerable<RunWorkflowInstanceResponse>> ResumeAsync<TActivity>(object stimulus, ResumeBookmarkOptions? options = null, CancellationToken cancellationToken = default) where TActivity : IActivity
{
throw new NotSupportedException();
}
public Task<RunWorkflowInstanceResponse?> ResumeAsync(string bookmarkId, IDictionary<string, object> input, CancellationToken cancellationToken = default)
{
throw new NotSupportedException();
}
public Task<IEnumerable<RunWorkflowInstanceResponse>> ResumeAsync<TActivity>(object stimulus, string? workflowInstanceId = null, ResumeBookmarkOptions? options = null, CancellationToken cancellationToken = default) where TActivity : IActivity
{
throw new NotSupportedException();
}
public Task<RunWorkflowInstanceResponse?> ResumeAsync<TActivity>(string bookmarkId, ResumeBookmarkOptions? options = null, CancellationToken cancellationToken = default) where TActivity : IActivity
{
throw new NotSupportedException();
}
public Task<IEnumerable<RunWorkflowInstanceResponse>> ResumeAsync(ResumeBookmarkRequest request, CancellationToken cancellationToken = default)
{
throw new NotSupportedException();
}
public Task<IEnumerable<RunWorkflowInstanceResponse>> ResumeAsync(BookmarkFilter filter, ResumeBookmarkOptions? options = null, CancellationToken cancellationToken = default)
{
return Task.FromResult<IEnumerable<RunWorkflowInstanceResponse>>([
new()
{
WorkflowInstanceId = filter.WorkflowInstanceId!
}
]);
}
}
private sealed class DeletingAfterPageQueueStore(IBookmarkQueueStore inner) : IBookmarkQueueStore
{
public Task SaveAsync(BookmarkQueueItem record, CancellationToken cancellationToken = default) => inner.SaveAsync(record, cancellationToken);
public Task AddAsync(BookmarkQueueItem record, CancellationToken cancellationToken = default) => inner.AddAsync(record, cancellationToken);
public Task<BookmarkQueueItem?> FindAsync(BookmarkQueueFilter filter, CancellationToken cancellationToken = default) => inner.FindAsync(filter, cancellationToken);
public Task<IEnumerable<BookmarkQueueItem>> FindManyAsync(BookmarkQueueFilter filter, CancellationToken cancellationToken = default) => inner.FindManyAsync(filter, cancellationToken);
public Task<Page<BookmarkQueueItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default) => inner.PageAsync(pageArgs, orderBy, cancellationToken);
public async Task<Page<BookmarkQueueItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueFilter filter, BookmarkQueueItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default)
{
var page = await inner.PageAsync(pageArgs, filter, orderBy, cancellationToken);
foreach (var item in page.Items)
await inner.DeleteAsync(new BookmarkQueueFilter { Id = item.Id }, cancellationToken);
return page;
}
public Task<long> DeleteAsync(BookmarkQueueFilter filter, CancellationToken cancellationToken = default) => inner.DeleteAsync(filter, cancellationToken);
}
private sealed class DeletingFirstAfterPageQueueStore(IBookmarkQueueStore inner) : IBookmarkQueueStore
{
private bool _deleted;
public Task SaveAsync(BookmarkQueueItem record, CancellationToken cancellationToken = default) => inner.SaveAsync(record, cancellationToken);
public Task AddAsync(BookmarkQueueItem record, CancellationToken cancellationToken = default) => inner.AddAsync(record, cancellationToken);
public Task<BookmarkQueueItem?> FindAsync(BookmarkQueueFilter filter, CancellationToken cancellationToken = default) => inner.FindAsync(filter, cancellationToken);
public Task<IEnumerable<BookmarkQueueItem>> FindManyAsync(BookmarkQueueFilter filter, CancellationToken cancellationToken = default) => inner.FindManyAsync(filter, cancellationToken);
public Task<Page<BookmarkQueueItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default) => inner.PageAsync(pageArgs, orderBy, cancellationToken);
public async Task<Page<BookmarkQueueItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueFilter filter, BookmarkQueueItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default)
{
var page = await inner.PageAsync(pageArgs, filter, orderBy, cancellationToken);
if (!_deleted && page.Items.Count > 0)
{
_deleted = true;
await inner.DeleteAsync(new BookmarkQueueFilter { Id = page.Items.First().Id }, cancellationToken);
}
return page;
}
public Task<long> DeleteAsync(BookmarkQueueFilter filter, CancellationToken cancellationToken = default) => inner.DeleteAsync(filter, cancellationToken);
}
private sealed class RecordingDeleteQueueStore(IBookmarkQueueStore inner) : IBookmarkQueueStore
{
public List<BookmarkQueueFilter> DeleteFilters { get; } = [];
public Task SaveAsync(BookmarkQueueItem record, CancellationToken cancellationToken = default) => inner.SaveAsync(record, cancellationToken);
public Task AddAsync(BookmarkQueueItem record, CancellationToken cancellationToken = default) => inner.AddAsync(record, cancellationToken);
public Task<BookmarkQueueItem?> FindAsync(BookmarkQueueFilter filter, CancellationToken cancellationToken = default) => inner.FindAsync(filter, cancellationToken);
public Task<IEnumerable<BookmarkQueueItem>> FindManyAsync(BookmarkQueueFilter filter, CancellationToken cancellationToken = default) => inner.FindManyAsync(filter, cancellationToken);
public Task<Page<BookmarkQueueItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default) => inner.PageAsync(pageArgs, orderBy, cancellationToken);
public Task<Page<BookmarkQueueItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueFilter filter, BookmarkQueueItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default) =>
inner.PageAsync(pageArgs, filter, orderBy, cancellationToken);
public Task<long> DeleteAsync(BookmarkQueueFilter filter, CancellationToken cancellationToken = default)
{
DeleteFilters.Add(filter);
return inner.DeleteAsync(filter, cancellationToken);
}
}
private sealed class CoordinatedFindDeadLetterStore(IBookmarkQueueDeadLetterStore inner, int participantCount) : IBookmarkQueueDeadLetterStore
{
private readonly TaskCompletionSource _allFindsCompleted = new(TaskCreationOptions.RunContinuationsAsynchronously);
private int _findCount;
public Task SaveAsync(BookmarkQueueDeadLetterItem record, CancellationToken cancellationToken = default) => inner.SaveAsync(record, cancellationToken);
public Task AddAsync(BookmarkQueueDeadLetterItem record, CancellationToken cancellationToken = default) => inner.AddAsync(record, cancellationToken);
public Task<BookmarkQueueDeadLetterItem> AddOrGetExistingAsync(BookmarkQueueDeadLetterItem record, CancellationToken cancellationToken = default) => inner.AddOrGetExistingAsync(record, cancellationToken);
public Task<BookmarkQueueDeadLetterItem?> TryMarkReplayedAsync(string id, string queueItemId, DateTimeOffset replayedAt, CancellationToken cancellationToken = default) =>
inner.TryMarkReplayedAsync(id, queueItemId, replayedAt, cancellationToken);
public async Task<BookmarkQueueDeadLetterItem?> FindAsync(BookmarkQueueDeadLetterFilter filter, CancellationToken cancellationToken = default)
{
var result = await inner.FindAsync(filter, cancellationToken);
if (filter.OriginalQueueItemId == null)
return result;
if (Interlocked.Increment(ref _findCount) == participantCount)
_allFindsCompleted.SetResult();
await _allFindsCompleted.Task.WaitAsync(cancellationToken);
return result;
}
public Task<IEnumerable<BookmarkQueueDeadLetterItem>> FindManyAsync(BookmarkQueueDeadLetterFilter filter, CancellationToken cancellationToken = default) =>
inner.FindManyAsync(filter, cancellationToken);
public Task<Page<BookmarkQueueDeadLetterItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueDeadLetterItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default) =>
inner.PageAsync(pageArgs, orderBy, cancellationToken);
public Task<Page<BookmarkQueueDeadLetterItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueDeadLetterFilter filter, BookmarkQueueDeadLetterItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default) =>
inner.PageAsync(pageArgs, filter, orderBy, cancellationToken);
public Task<long> DeleteAsync(BookmarkQueueDeadLetterFilter filter, CancellationToken cancellationToken = default) => inner.DeleteAsync(filter, cancellationToken);
}
private sealed class ThrowingAddDeadLetterStore(IBookmarkQueueDeadLetterStore inner) : IBookmarkQueueDeadLetterStore
{
public Task SaveAsync(BookmarkQueueDeadLetterItem record, CancellationToken cancellationToken = default) => inner.SaveAsync(record, cancellationToken);
public Task AddAsync(BookmarkQueueDeadLetterItem record, CancellationToken cancellationToken = default) => throw new InvalidOperationException("dead-letter write failed");
public Task<BookmarkQueueDeadLetterItem> AddOrGetExistingAsync(BookmarkQueueDeadLetterItem record, CancellationToken cancellationToken = default) => throw new InvalidOperationException("dead-letter write failed");
public Task<BookmarkQueueDeadLetterItem?> TryMarkReplayedAsync(string id, string queueItemId, DateTimeOffset replayedAt, CancellationToken cancellationToken = default) =>
inner.TryMarkReplayedAsync(id, queueItemId, replayedAt, cancellationToken);
public Task<BookmarkQueueDeadLetterItem?> FindAsync(BookmarkQueueDeadLetterFilter filter, CancellationToken cancellationToken = default) => inner.FindAsync(filter, cancellationToken);
public Task<IEnumerable<BookmarkQueueDeadLetterItem>> FindManyAsync(BookmarkQueueDeadLetterFilter filter, CancellationToken cancellationToken = default) =>
inner.FindManyAsync(filter, cancellationToken);
public Task<Page<BookmarkQueueDeadLetterItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueDeadLetterItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default) =>
inner.PageAsync(pageArgs, orderBy, cancellationToken);
public Task<Page<BookmarkQueueDeadLetterItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueDeadLetterFilter filter, BookmarkQueueDeadLetterItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default) =>
inner.PageAsync(pageArgs, filter, orderBy, cancellationToken);
public Task<long> DeleteAsync(BookmarkQueueDeadLetterFilter filter, CancellationToken cancellationToken = default) => inner.DeleteAsync(filter, cancellationToken);
}
private sealed class InspectingSaveDeadLetterStore(IBookmarkQueueDeadLetterStore inner, Func<Task> beforeSaveAsync) : IBookmarkQueueDeadLetterStore
{
public async Task SaveAsync(BookmarkQueueDeadLetterItem record, CancellationToken cancellationToken = default)
{
await beforeSaveAsync();
await inner.SaveAsync(record, cancellationToken);
}
public Task AddAsync(BookmarkQueueDeadLetterItem record, CancellationToken cancellationToken = default) => inner.AddAsync(record, cancellationToken);
public Task<BookmarkQueueDeadLetterItem> AddOrGetExistingAsync(BookmarkQueueDeadLetterItem record, CancellationToken cancellationToken = default) => inner.AddOrGetExistingAsync(record, cancellationToken);
public Task<BookmarkQueueDeadLetterItem?> TryMarkReplayedAsync(string id, string queueItemId, DateTimeOffset replayedAt, CancellationToken cancellationToken = default) =>
inner.TryMarkReplayedAsync(id, queueItemId, replayedAt, cancellationToken);
public Task<BookmarkQueueDeadLetterItem?> FindAsync(BookmarkQueueDeadLetterFilter filter, CancellationToken cancellationToken = default) => inner.FindAsync(filter, cancellationToken);
public Task<IEnumerable<BookmarkQueueDeadLetterItem>> FindManyAsync(BookmarkQueueDeadLetterFilter filter, CancellationToken cancellationToken = default) =>
inner.FindManyAsync(filter, cancellationToken);
public Task<Page<BookmarkQueueDeadLetterItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueDeadLetterItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default) =>
inner.PageAsync(pageArgs, orderBy, cancellationToken);
public Task<Page<BookmarkQueueDeadLetterItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueDeadLetterFilter filter, BookmarkQueueDeadLetterItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default) =>
inner.PageAsync(pageArgs, filter, orderBy, cancellationToken);
public Task<long> DeleteAsync(BookmarkQueueDeadLetterFilter filter, CancellationToken cancellationToken = default) => inner.DeleteAsync(filter, cancellationToken);
}
private sealed class ThrowingAddQueueStore(IBookmarkQueueStore inner) : IBookmarkQueueStore
{
public Task SaveAsync(BookmarkQueueItem record, CancellationToken cancellationToken = default) => inner.SaveAsync(record, cancellationToken);
public Task AddAsync(BookmarkQueueItem record, CancellationToken cancellationToken = default)
{
throw new InvalidOperationException("enqueue failed");
}
public Task<BookmarkQueueItem?> FindAsync(BookmarkQueueFilter filter, CancellationToken cancellationToken = default) => inner.FindAsync(filter, cancellationToken);
public Task<IEnumerable<BookmarkQueueItem>> FindManyAsync(BookmarkQueueFilter filter, CancellationToken cancellationToken = default) => inner.FindManyAsync(filter, cancellationToken);
public Task<Page<BookmarkQueueItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default) => inner.PageAsync(pageArgs, orderBy, cancellationToken);
public Task<Page<BookmarkQueueItem>> PageAsync<TOrderBy>(PageArgs pageArgs, BookmarkQueueFilter filter, BookmarkQueueItemOrder<TOrderBy> orderBy, CancellationToken cancellationToken = default) => inner.PageAsync(pageArgs, filter, orderBy, cancellationToken);
public Task<long> DeleteAsync(BookmarkQueueFilter filter, CancellationToken cancellationToken = default) => inner.DeleteAsync(filter, cancellationToken);
}
}