mirror of https://github.com/Squidex/squidex.git
19 changed files with 467 additions and 275 deletions
@ -1,154 +0,0 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Channels; |
|||
using Squidex.Infrastructure.Tasks; |
|||
|
|||
#pragma warning disable RECS0082 // Parameter has the same name as a member and hides it
|
|||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
|||
|
|||
namespace Squidex.Infrastructure.EventSourcing.Consume |
|||
{ |
|||
internal sealed class BatchSubscriber : IEventSubscriber, IEventSubscription |
|||
{ |
|||
private readonly IEventSubscription eventSubscription; |
|||
private readonly Channel<object> taskQueue; |
|||
private readonly Channel<EventSource> parseQueue; |
|||
private readonly Task handleTask; |
|||
private readonly CancellationTokenSource completed = new CancellationTokenSource(); |
|||
|
|||
private sealed record EventSource(StoredEvent StoredEvent); |
|||
private sealed record BatchItem(Envelope<IEvent>? Event, string Position); |
|||
private sealed record BatchJob(BatchItem[] Items); |
|||
private sealed record ErrorJob(Exception Exception); |
|||
|
|||
public BatchSubscriber( |
|||
EventConsumerProcessor processor, |
|||
IEventFormatter eventFormatter, |
|||
IEventConsumer eventConsumer, |
|||
Func<IEventSubscriber, IEventSubscription> factory) |
|||
{ |
|||
eventSubscription = factory(this); |
|||
|
|||
var batchSize = Math.Max(1, eventConsumer.BatchSize); |
|||
var batchDelay = Math.Max(100, eventConsumer.BatchDelay); |
|||
|
|||
parseQueue = Channel.CreateBounded<EventSource>(new BoundedChannelOptions(batchSize) |
|||
{ |
|||
AllowSynchronousContinuations = true, |
|||
SingleReader = true, |
|||
SingleWriter = true |
|||
}); |
|||
|
|||
taskQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(2) |
|||
{ |
|||
SingleReader = true, |
|||
SingleWriter = true |
|||
}); |
|||
|
|||
var batchQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(batchSize) |
|||
{ |
|||
AllowSynchronousContinuations = true, |
|||
SingleReader = true, |
|||
SingleWriter = true |
|||
}); |
|||
|
|||
#pragma warning disable MA0040 // Flow the cancellation token
|
|||
batchQueue.Batch<BatchItem, object>(taskQueue, x => new BatchJob(x.ToArray()), batchSize, batchDelay); |
|||
|
|||
Task.Run(async () => |
|||
{ |
|||
await foreach (var source in parseQueue.Reader.ReadAllAsync(completed.Token)) |
|||
{ |
|||
var storedEvent = source.StoredEvent; |
|||
try |
|||
{ |
|||
Envelope<IEvent>? @event = null; |
|||
|
|||
if (eventConsumer.Handles(storedEvent)) |
|||
{ |
|||
@event = eventFormatter.ParseIfKnown(storedEvent); |
|||
} |
|||
|
|||
await batchQueue.Writer.WriteAsync(new BatchItem(@event, storedEvent.EventPosition), completed.Token); |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
await taskQueue.Writer.WriteAsync(new ErrorJob(ex), completed.Token); |
|||
} |
|||
} |
|||
}).ContinueWith(x => batchQueue.Writer.TryComplete(x.Exception)); |
|||
#pragma warning restore MA0040 // Flow the cancellation token
|
|||
|
|||
handleTask = Run(processor); |
|||
} |
|||
|
|||
private async Task Run(EventConsumerProcessor processor) |
|||
{ |
|||
try |
|||
{ |
|||
await foreach (var task in taskQueue.Reader.ReadAllAsync(completed.Token)) |
|||
{ |
|||
switch (task) |
|||
{ |
|||
case ErrorJob { Exception: not OperationCanceledException } error: |
|||
{ |
|||
await processor.OnErrorAsync(this, error.Exception); |
|||
break; |
|||
} |
|||
|
|||
case BatchJob batch: |
|||
{ |
|||
var eventsPosition = batch.Items[^1].Position; |
|||
var eventsCollection = batch.Items.Select(x => x.Event).NotNull().ToList(); |
|||
|
|||
await processor.OnEventsAsync(this, eventsCollection, eventsPosition); |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
catch (OperationCanceledException) |
|||
{ |
|||
return; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
await processor.OnErrorAsync(this, ex); |
|||
} |
|||
} |
|||
|
|||
public Task CompleteAsync() |
|||
{ |
|||
parseQueue.Writer.TryComplete(); |
|||
|
|||
return handleTask; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
completed.Cancel(); |
|||
|
|||
eventSubscription.Dispose(); |
|||
} |
|||
|
|||
public void WakeUp() |
|||
{ |
|||
eventSubscription.WakeUp(); |
|||
} |
|||
|
|||
ValueTask IEventSubscriber.OnEventAsync(IEventSubscription subscription, StoredEvent storedEvent) |
|||
{ |
|||
return parseQueue.Writer.WriteAsync(new EventSource(storedEvent), completed.Token); |
|||
} |
|||
|
|||
ValueTask IEventSubscriber.OnErrorAsync(IEventSubscription subscription, Exception exception) |
|||
{ |
|||
return taskQueue.Writer.WriteAsync(new ErrorJob(exception), completed.Token); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,160 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Channels; |
|||
using Squidex.Infrastructure.Tasks; |
|||
|
|||
namespace Squidex.Infrastructure.EventSourcing.Consume |
|||
{ |
|||
internal sealed class BatchSubscription : IEventSubscriber<ParsedEvent>, IEventSubscription |
|||
{ |
|||
private readonly IEventSubscription eventSubscription; |
|||
private readonly Channel<object> taskQueue; |
|||
private readonly Channel<object> batchQueue; |
|||
private readonly Task handleTask; |
|||
private readonly CancellationTokenSource completed = new CancellationTokenSource(); |
|||
|
|||
public BatchSubscription( |
|||
IEventConsumer eventConsumer, |
|||
IEventSubscriber<ParsedEvents> eventSubscriber, |
|||
EventSubscriptionSource<ParsedEvent> eventSource) |
|||
{ |
|||
eventSubscription = eventSource(this); |
|||
|
|||
var batchSize = Math.Max(1, eventConsumer.BatchSize); |
|||
var batchDelay = Math.Max(100, eventConsumer.BatchDelay); |
|||
|
|||
taskQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(2) |
|||
{ |
|||
SingleReader = true, |
|||
SingleWriter = true |
|||
}); |
|||
|
|||
batchQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(batchSize) |
|||
{ |
|||
AllowSynchronousContinuations = true, |
|||
SingleReader = true, |
|||
SingleWriter = true |
|||
}); |
|||
|
|||
batchQueue.Batch<ParsedEvent>(taskQueue, batchSize, batchDelay, completed.Token); |
|||
|
|||
handleTask = Run(eventSubscriber); |
|||
} |
|||
|
|||
private async Task Run(IEventSubscriber<ParsedEvents> eventSink) |
|||
{ |
|||
try |
|||
{ |
|||
var isStopped = false; |
|||
|
|||
await foreach (var task in taskQueue.Reader.ReadAllAsync(completed.Token)) |
|||
{ |
|||
switch (task) |
|||
{ |
|||
case Exception exception when exception is not OperationCanceledException: |
|||
{ |
|||
if (!completed.IsCancellationRequested) |
|||
{ |
|||
await eventSink.OnErrorAsync(this, exception); |
|||
} |
|||
|
|||
isStopped = true; |
|||
break; |
|||
} |
|||
|
|||
case List<ParsedEvent> batch: |
|||
{ |
|||
if (!completed.IsCancellationRequested) |
|||
{ |
|||
// Events can be null if the event consumer is not interested in the stored event.
|
|||
var eventList = batch.Select(x => x.Event).NotNull().ToList(); |
|||
var eventPosition = batch[^1].Position; |
|||
|
|||
// Use a struct here to save a few allocations.
|
|||
await eventSink.OnNextAsync(this, new ParsedEvents(eventList, eventPosition)); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (isStopped) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
catch (OperationCanceledException) |
|||
{ |
|||
return; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
if (!completed.IsCancellationRequested) |
|||
{ |
|||
await eventSink.OnErrorAsync(this, ex); |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (completed.IsCancellationRequested) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// It is not necessary to dispose the cancellation token source.
|
|||
completed.Cancel(); |
|||
|
|||
// We do not lock here, it is the responsibility of the source subscription to be thread safe.
|
|||
eventSubscription.Dispose(); |
|||
} |
|||
|
|||
public async ValueTask CompleteAsync() |
|||
{ |
|||
await eventSubscription.CompleteAsync(); |
|||
|
|||
batchQueue.Writer.TryComplete(); |
|||
|
|||
await handleTask; |
|||
} |
|||
|
|||
public void WakeUp() |
|||
{ |
|||
eventSubscription.WakeUp(); |
|||
} |
|||
|
|||
async ValueTask IEventSubscriber<ParsedEvent>.OnErrorAsync(IEventSubscription subscription, Exception exception) |
|||
{ |
|||
try |
|||
{ |
|||
// Forward the exception from one task only, but bypass the batch.
|
|||
await taskQueue.Writer.WriteAsync(exception, completed.Token); |
|||
} |
|||
catch (ChannelClosedException) |
|||
{ |
|||
// This exception is acceptable and happens when an exception has been thrown before.
|
|||
return; |
|||
} |
|||
} |
|||
|
|||
async ValueTask IEventSubscriber<ParsedEvent>.OnNextAsync(IEventSubscription subscription, ParsedEvent @event) |
|||
{ |
|||
try |
|||
{ |
|||
await batchQueue.Writer.WriteAsync(@event, completed.Token); |
|||
} |
|||
catch (ChannelClosedException) |
|||
{ |
|||
// This exception is acceptable and happens when an exception has been thrown before.
|
|||
return; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,149 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Threading.Channels; |
|||
|
|||
namespace Squidex.Infrastructure.EventSourcing.Consume |
|||
{ |
|||
internal sealed class ParseSubscription : IEventSubscriber<StoredEvent>, IEventSubscription |
|||
{ |
|||
private readonly Channel<object> deserializeQueue; |
|||
private readonly CancellationTokenSource completed = new CancellationTokenSource(); |
|||
private readonly Task deserializeTask; |
|||
private readonly IEventSubscription eventSubscription; |
|||
|
|||
public ParseSubscription( |
|||
IEventConsumer eventConsumer, |
|||
IEventFormatter eventFormatter, |
|||
IEventSubscriber<ParsedEvent> eventSubscriber, |
|||
EventSubscriptionSource<StoredEvent> eventSource) |
|||
{ |
|||
eventSubscription = eventSource(this); |
|||
|
|||
deserializeQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(2) |
|||
{ |
|||
AllowSynchronousContinuations = true, |
|||
SingleReader = true, |
|||
SingleWriter = true |
|||
}); |
|||
|
|||
#pragma warning disable MA0040 // Flow the cancellation token
|
|||
deserializeTask = Task.Run(async () => |
|||
{ |
|||
try |
|||
{ |
|||
var isFailed = false; |
|||
|
|||
await foreach (var input in deserializeQueue.Reader.ReadAllAsync(completed.Token)) |
|||
{ |
|||
switch (input) |
|||
{ |
|||
case Exception exception: |
|||
{ |
|||
// Not very likely that the task is cancelled.
|
|||
await eventSubscriber.OnErrorAsync(this, exception); |
|||
|
|||
isFailed = true; |
|||
break; |
|||
} |
|||
|
|||
case StoredEvent storedEvent: |
|||
{ |
|||
Envelope<IEvent>? @event = null; |
|||
|
|||
if (eventConsumer.Handles(storedEvent)) |
|||
{ |
|||
@event = eventFormatter.ParseIfKnown(storedEvent); |
|||
} |
|||
|
|||
// Parsing takes a little bit of time, so the task might have been cancelled.
|
|||
if (!completed.IsCancellationRequested) |
|||
{ |
|||
// Also invoke the subscriber if the event is null to update the position.
|
|||
await eventSubscriber.OnNextAsync(this, new ParsedEvent(@event, storedEvent.EventPosition)); |
|||
} |
|||
|
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (isFailed) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
catch (OperationCanceledException) |
|||
{ |
|||
return; |
|||
} |
|||
catch (Exception ex) |
|||
{ |
|||
if (!completed.IsCancellationRequested) |
|||
{ |
|||
await eventSubscriber.OnErrorAsync(this, ex); |
|||
} |
|||
} |
|||
}).ContinueWith(x => deserializeQueue.Writer.TryComplete(x.Exception)); |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (completed.IsCancellationRequested) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
// It is not necessary to dispose the cancellation token source.
|
|||
completed.Cancel(); |
|||
|
|||
// We do not lock here, it is the responsibility of the source subscription to be thread safe.
|
|||
eventSubscription.Dispose(); |
|||
} |
|||
|
|||
public async ValueTask CompleteAsync() |
|||
{ |
|||
await eventSubscription.CompleteAsync(); |
|||
|
|||
deserializeQueue.Writer.TryComplete(); |
|||
|
|||
await deserializeTask; |
|||
} |
|||
|
|||
public void WakeUp() |
|||
{ |
|||
eventSubscription.WakeUp(); |
|||
} |
|||
|
|||
async ValueTask IEventSubscriber<StoredEvent>.OnErrorAsync(IEventSubscription subscription, Exception exception) |
|||
{ |
|||
try |
|||
{ |
|||
// Forward the exception from one task only.
|
|||
await deserializeQueue.Writer.WriteAsync(exception, completed.Token); |
|||
} |
|||
catch (ChannelClosedException) |
|||
{ |
|||
// This exception is acceptable and happens when an exception has been thrown before.
|
|||
return; |
|||
} |
|||
} |
|||
|
|||
async ValueTask IEventSubscriber<StoredEvent>.OnNextAsync(IEventSubscription subscription, StoredEvent @event) |
|||
{ |
|||
try |
|||
{ |
|||
await deserializeQueue.Writer.WriteAsync(@event, completed.Token); |
|||
} |
|||
catch (ChannelClosedException) |
|||
{ |
|||
// This exception is acceptable and happens when an exception has been thrown before.
|
|||
return; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
namespace Squidex.Infrastructure.EventSourcing.Consume |
|||
{ |
|||
public record struct ParsedEvent(Envelope<IEvent>? Event, string Position); |
|||
|
|||
public record struct ParsedEvents(List<Envelope<IEvent>> Events, string Position); |
|||
} |
|||
Loading…
Reference in new issue