-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6338b6c
commit e24368f
Showing
14 changed files
with
501 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<PackageTags>EntityDb EventSourcing EventStreaming DDD CQRS</PackageTags> | ||
<Description>A partial set of implementations of the EntityDb common layer.</Description> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\EntityDb.Common\EntityDb.Common.csproj"/> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="AWSSDK.SQS" Version="3.7.300.39" /> | ||
</ItemGroup> | ||
|
||
</Project> |
43 changes: 43 additions & 0 deletions
43
src/EntityDb.Aws/Extensions/ServiceCollectionExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
using EntityDb.Aws.Sources.Processors.Queues; | ||
using EntityDb.Common.Sources.Processors.Queues; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace EntityDb.Aws.Extensions; | ||
|
||
/// <summary> | ||
/// Extensions for service collections. | ||
/// </summary> | ||
public static class ServiceCollectionExtensions | ||
{ | ||
/// <summary> | ||
/// Registers a queue for processing sources as they are committed. | ||
/// For test mode, the queue is not actually a queue and will immediately process the source. | ||
/// For non-test mode, the implementation of ISourceProcessorQueue uses a buffer | ||
/// block to receive messages, enqueue them to sqs, and then background-only service | ||
/// will dequeue them from sqs and process them as normal. | ||
/// </summary> | ||
/// <param name="serviceCollection">The service collection.</param> | ||
/// <param name="testMode">Whether or not to run in test mode.</param> | ||
[ExcludeFromCodeCoverage(Justification = "Tests are only meant to run in test mode.")] | ||
public static void AddSqsSourceProcessorQueue(this IServiceCollection serviceCollection, | ||
bool testMode) | ||
{ | ||
if (testMode) | ||
{ | ||
serviceCollection.AddSingleton<ISourceProcessorQueue, TestModeSourceProcessorQueue>(); | ||
} | ||
else | ||
{ | ||
serviceCollection.AddSingleton<SqsOutboxSourceProcessorQueue>(); | ||
|
||
serviceCollection.AddSingleton<ISourceProcessorQueue>(serviceProvider => | ||
serviceProvider.GetRequiredService<SqsOutboxSourceProcessorQueue>()); | ||
|
||
serviceCollection.AddHostedService(serviceProvider => | ||
serviceProvider.GetRequiredService<SqsOutboxSourceProcessorQueue>()); | ||
|
||
serviceCollection.AddHostedService<SqsInboxSourceProcessorQueue>(); | ||
} | ||
} | ||
} |
130 changes: 130 additions & 0 deletions
130
src/EntityDb.Aws/Sources/Processors/Queues/SqsInboxSourceProcessorQueue.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
using Amazon.SQS.Model; | ||
using EntityDb.Abstractions; | ||
using EntityDb.Abstractions.Sources; | ||
using EntityDb.Common.Envelopes; | ||
using EntityDb.Common.Sources; | ||
using EntityDb.Common.Sources.Processors; | ||
using EntityDb.Common.TypeResolvers; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Hosting; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using System.Diagnostics; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Text.Json; | ||
|
||
namespace EntityDb.Aws.Sources.Processors.Queues; | ||
|
||
[ExcludeFromCodeCoverage(Justification = "Not used in tests.")] | ||
internal sealed class SqsInboxSourceProcessorQueue : BackgroundService | ||
{ | ||
private readonly ILogger<SqsInboxSourceProcessorQueue> _logger; | ||
private readonly IServiceScopeFactory _serviceScopeFactory; | ||
private readonly ITypeResolver _typeResolver; | ||
private readonly SqsSourceProcessorQueueOptions _options; | ||
|
||
public SqsInboxSourceProcessorQueue | ||
( | ||
ILogger<SqsInboxSourceProcessorQueue> logger, | ||
IServiceScopeFactory serviceScopeFactory, | ||
ITypeResolver typeResolver, | ||
IOptionsFactory<SqsSourceProcessorQueueOptions> optionsFactory, | ||
string sqsOptionsName | ||
) | ||
{ | ||
_logger = logger; | ||
_serviceScopeFactory = serviceScopeFactory; | ||
_typeResolver = typeResolver; | ||
_options = optionsFactory.Create(sqsOptionsName); | ||
} | ||
|
||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||
{ | ||
const int maxNumberOfMessages = 1; | ||
|
||
var active = true; | ||
|
||
while (!stoppingToken.IsCancellationRequested) | ||
{ | ||
await Task.Delay(active ? _options.ActiveDequeueDelay : _options.IdleDequeueDelay, stoppingToken); | ||
|
||
await using var serviceScope = _serviceScopeFactory.CreateAsyncScope(); | ||
|
||
var (amazonSqs, queueUrl) = await _options.AmazonSqsFactory.Invoke(serviceScope.ServiceProvider); | ||
|
||
var receiveMessageResponse = await amazonSqs.ReceiveMessageAsync | ||
( | ||
new ReceiveMessageRequest | ||
{ | ||
QueueUrl = queueUrl, | ||
MaxNumberOfMessages = maxNumberOfMessages, | ||
}, | ||
stoppingToken | ||
); | ||
|
||
if (receiveMessageResponse.Messages.Count != maxNumberOfMessages) | ||
{ | ||
active = false; | ||
continue; | ||
} | ||
|
||
active = true; | ||
|
||
var receivedMessage = receiveMessageResponse.Messages[0]; | ||
|
||
var deleteRequest = new DeleteMessageRequest | ||
{ | ||
ReceiptHandle = receivedMessage.ReceiptHandle, | ||
}; | ||
|
||
var sqsSourceProcessorMessage = | ||
JsonSerializer.Deserialize<SqsSourceProcessorMessage>(receivedMessage.Body) ?? | ||
throw new UnreachableException(); | ||
|
||
var sourceId = new Id(sqsSourceProcessorMessage.SourceId); | ||
var stateId = new Id(sqsSourceProcessorMessage.StateId); | ||
|
||
await using var sourceRepositoryFactory = | ||
serviceScope.ServiceProvider.GetRequiredService<ISourceRepositoryFactory>(); | ||
|
||
await using var sourceRepository = await sourceRepositoryFactory.Create(sqsSourceProcessorMessage.SourceRepositoryOptionsName, | ||
stoppingToken); | ||
|
||
var sourceProcessorTypeName = sqsSourceProcessorMessage.SourceProcessorEnvelopeHeaders.Value[EnvelopeHelper.Type]; | ||
|
||
using var logScope = _logger.BeginScope(new KeyValuePair<string, object>[] | ||
{ | ||
new("QueueUrl", queueUrl), | ||
new("MessageId", receivedMessage.MessageId), | ||
new("ReceiptHandle", receivedMessage.ReceiptHandle), | ||
new("SourceProcessorType", sourceProcessorTypeName), | ||
new("SourceId", sqsSourceProcessorMessage.SourceId), | ||
new("StateId", sqsSourceProcessorMessage.StateId), | ||
}); | ||
|
||
try | ||
{ | ||
var source = await sourceRepository.GetSource(sourceId, stateId, stoppingToken); | ||
|
||
var sourceProcessorType = _typeResolver.ResolveType(sqsSourceProcessorMessage.SourceProcessorEnvelopeHeaders); | ||
|
||
var sourceProcessor = | ||
(ISourceProcessor)serviceScope.ServiceProvider.GetRequiredService(sourceProcessorType); | ||
|
||
_logger.Log(_options.DebugLogLevel, "Started processing source"); | ||
|
||
await sourceProcessor.Process(source, stoppingToken); | ||
|
||
_logger.Log(_options.DebugLogLevel, "Finished processing source"); | ||
|
||
await amazonSqs.DeleteMessageAsync(deleteRequest, stoppingToken); | ||
|
||
_logger.Log(LogLevel.Debug, "Message deleted from queue"); | ||
} | ||
catch (Exception exception) | ||
{ | ||
_logger.LogError(exception, "Error occurred while processing source"); | ||
} | ||
} | ||
} | ||
} |
125 changes: 125 additions & 0 deletions
125
src/EntityDb.Aws/Sources/Processors/Queues/SqsOutboxSourceProcessorQueue.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
using Amazon.SQS.Model; | ||
using EntityDb.Common.Envelopes; | ||
using EntityDb.Common.Sources.Processors.Queues; | ||
using Microsoft.Extensions.DependencyInjection; | ||
using Microsoft.Extensions.Hosting; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using System.Diagnostics.CodeAnalysis; | ||
using System.Text.Json; | ||
using System.Threading.Tasks.Dataflow; | ||
|
||
namespace EntityDb.Aws.Sources.Processors.Queues; | ||
|
||
[ExcludeFromCodeCoverage(Justification = "Not used in tests.")] | ||
internal sealed class SqsOutboxSourceProcessorQueue : BackgroundService, ISourceProcessorQueue | ||
{ | ||
private readonly BufferBlock<ISourceProcessorQueueItem> _bufferBlock = new(); | ||
private readonly ILogger<SqsOutboxSourceProcessorQueue> _logger; | ||
private readonly IServiceScopeFactory _serviceScopeFactory; | ||
private readonly SqsSourceProcessorQueueOptions _options; | ||
private CancellationTokenSource? _linkedStoppingTokenSource; | ||
|
||
public SqsOutboxSourceProcessorQueue | ||
( | ||
ILogger<SqsOutboxSourceProcessorQueue> logger, | ||
IServiceScopeFactory serviceScopeFactory, | ||
IOptionsFactory<SqsSourceProcessorQueueOptions> optionsFactory, | ||
string optionsName | ||
) | ||
{ | ||
_logger = logger; | ||
_serviceScopeFactory = serviceScopeFactory; | ||
_options = optionsFactory.Create(optionsName); | ||
} | ||
|
||
public void Enqueue(ISourceProcessorQueueItem item) | ||
{ | ||
if (_linkedStoppingTokenSource is { IsCancellationRequested: true }) | ||
{ | ||
_logger.LogWarning("Application is shutting down when messages are still being received"); | ||
} | ||
|
||
_bufferBlock.Post(item); | ||
} | ||
|
||
protected override async Task ExecuteAsync(CancellationToken stoppingToken) | ||
{ | ||
using var linkedStoppingTokenSource = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken); | ||
|
||
_linkedStoppingTokenSource = linkedStoppingTokenSource; | ||
|
||
CancellationToken cancellationToken = default; | ||
|
||
while (await _bufferBlock.OutputAvailableAsync(cancellationToken)) | ||
{ | ||
if (_linkedStoppingTokenSource.IsCancellationRequested) | ||
{ | ||
_logger.LogWarning("Application is shutting down when messages are still being enqueued"); | ||
} | ||
|
||
await Task.Delay(_options.EnqueueDelay, cancellationToken); | ||
|
||
var item = await _bufferBlock.ReceiveAsync(cancellationToken); | ||
|
||
await using var serviceScope = _serviceScopeFactory.CreateAsyncScope(); | ||
|
||
var (amazonSqs, queueUrl) = await _options.AmazonSqsFactory.Invoke(serviceScope.ServiceProvider); | ||
|
||
using var logScope = _logger.BeginScope(new KeyValuePair<string, object>[] | ||
{ | ||
new("QueueUrl", queueUrl), | ||
new("SourceProcessorType", item.SourceProcessorType.Name), | ||
new("SourceId", item.Source.Id.Value), | ||
}); | ||
|
||
try | ||
{ | ||
_logger.Log(_options.DebugLogLevel, "Started enqueueing source"); | ||
|
||
var sourceProcessorEnvelopeHeaders = EnvelopeHelper.GetEnvelopeHeaders(item.SourceProcessorType); | ||
var sourceProcessorTypeName = sourceProcessorEnvelopeHeaders.Value[EnvelopeHelper.Type]; | ||
|
||
var sourceId = item.Source.Id.Value; | ||
|
||
foreach (var message in item.Source.Messages.DistinctBy(message => message.StatePointer.Id)) | ||
{ | ||
var stateId = message.StatePointer.Id.Value; | ||
|
||
var sendMessageResponse = await amazonSqs.SendMessageAsync | ||
( | ||
new SendMessageRequest | ||
{ | ||
QueueUrl = queueUrl, | ||
MessageBody = JsonSerializer.Serialize(new SqsSourceProcessorMessage | ||
{ | ||
StateId = stateId, | ||
SourceId = sourceId, | ||
SourceRepositoryOptionsName = | ||
_options.GetSourceRepositoryOptionsNameFromDelta(message.Delta), | ||
SourceProcessorEnvelopeHeaders = sourceProcessorEnvelopeHeaders, | ||
}), | ||
MessageGroupId = $"{sourceProcessorTypeName}:{stateId}", | ||
MessageDeduplicationId = $"{sourceId}", | ||
}, | ||
cancellationToken | ||
); | ||
|
||
_logger.Log(_options.DebugLogLevel, | ||
"Message {MessageId} enqueued for {SourceId} and {StateId} using {SourceProcessorTypeName}", | ||
sendMessageResponse.MessageId, sourceId, stateId, sourceProcessorTypeName); | ||
} | ||
|
||
_logger.Log(_options.DebugLogLevel, "Finished enqueueing source"); | ||
} | ||
catch (Exception exception) | ||
{ | ||
_logger.LogError(exception, "Error occurred while enqueueing source"); | ||
} | ||
} | ||
|
||
while (await _bufferBlock.OutputAvailableAsync(cancellationToken)) | ||
{ | ||
} | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/EntityDb.Aws/Sources/Processors/Queues/SqsSourceProcessorMessage.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
using EntityDb.Common.Envelopes; | ||
using System.Diagnostics.CodeAnalysis; | ||
|
||
namespace EntityDb.Aws.Sources.Processors.Queues; | ||
|
||
[ExcludeFromCodeCoverage(Justification = "Not used in tests.")] | ||
internal record SqsSourceProcessorMessage | ||
{ | ||
public required Guid StateId { get; init; } | ||
public required Guid SourceId { get; init; } | ||
public required string SourceRepositoryOptionsName { get; init; } | ||
public required EnvelopeHeaders SourceProcessorEnvelopeHeaders { get; init; } | ||
} |
Oops, something went wrong.