mirror of https://github.com/Squidex/squidex.git
11 changed files with 488 additions and 201 deletions
@ -0,0 +1,70 @@ |
|||||
|
// ==========================================================================
|
||||
|
// DispatchEventBlock.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Internal |
||||
|
{ |
||||
|
internal sealed class DispatchEventBlock : EventReceiverBlock<Envelope<IEvent>, Envelope<IEvent>> |
||||
|
{ |
||||
|
public DispatchEventBlock(IEventConsumer eventConsumer, IEventConsumerInfoRepository eventConsumerInfoRepository, ISemanticLog log) |
||||
|
: base(true, eventConsumer, eventConsumerInfoRepository, log) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override async Task<Envelope<IEvent>> On(Envelope<IEvent> input) |
||||
|
{ |
||||
|
var consumerName = EventConsumer.Name; |
||||
|
|
||||
|
var eventId = input.Headers.EventId().ToString(); |
||||
|
var eventType = input.Payload.GetType().Name; |
||||
|
try |
||||
|
{ |
||||
|
Log.LogInformation(w => w |
||||
|
.WriteProperty("action", "HandleEvent") |
||||
|
.WriteProperty("actionId", eventId) |
||||
|
.WriteProperty("state", "Started") |
||||
|
.WriteProperty("eventId", eventId) |
||||
|
.WriteProperty("eventType", eventType) |
||||
|
.WriteProperty("eventConsumer", consumerName)); |
||||
|
|
||||
|
await EventConsumer.On(input); |
||||
|
|
||||
|
Log.LogInformation(w => w |
||||
|
.WriteProperty("action", "HandleEvent") |
||||
|
.WriteProperty("actionId", eventId) |
||||
|
.WriteProperty("state", "Completed") |
||||
|
.WriteProperty("eventId", eventId) |
||||
|
.WriteProperty("eventType", eventType) |
||||
|
.WriteProperty("eventConsumer", consumerName)); |
||||
|
|
||||
|
return input; |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Log.LogError(ex, w => w |
||||
|
.WriteProperty("action", "HandleEvent") |
||||
|
.WriteProperty("actionId", eventId) |
||||
|
.WriteProperty("state", "Started") |
||||
|
.WriteProperty("eventId", eventId) |
||||
|
.WriteProperty("eventType", eventType) |
||||
|
.WriteProperty("eventConsumer", consumerName)); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
protected override long GetEventNumber(Envelope<IEvent> input) |
||||
|
{ |
||||
|
return input.Headers.EventNumber(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,118 @@ |
|||||
|
// ==========================================================================
|
||||
|
// EventReceiverBlock.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
using System.Threading.Tasks.Dataflow; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Internal |
||||
|
{ |
||||
|
public abstract class EventReceiverBlock<TInput, TOutput> |
||||
|
{ |
||||
|
private long lastEventNumber = -1; |
||||
|
|
||||
|
protected ISemanticLog Log { get; } |
||||
|
|
||||
|
protected IEventConsumer EventConsumer { get; } |
||||
|
|
||||
|
protected IEventConsumerInfoRepository EventConsumerInfoRepository { get; } |
||||
|
|
||||
|
public ITargetBlock<TInput> Target { get; } |
||||
|
|
||||
|
public Task Completion |
||||
|
{ |
||||
|
get { return Target.Completion; } |
||||
|
} |
||||
|
|
||||
|
protected EventReceiverBlock(bool transform, IEventConsumer eventConsumer, IEventConsumerInfoRepository eventConsumerInfoRepository, ISemanticLog log) |
||||
|
{ |
||||
|
EventConsumer = eventConsumer; |
||||
|
EventConsumerInfoRepository = eventConsumerInfoRepository; |
||||
|
|
||||
|
Log = log; |
||||
|
|
||||
|
if (transform) |
||||
|
{ |
||||
|
Target = |
||||
|
new TransformBlock<TInput, TOutput>(new Func<TInput, Task<TOutput>>(HandleAsync), |
||||
|
new ExecutionDataflowBlockOptions { BoundedCapacity = 1 }); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
Target = |
||||
|
new ActionBlock<TInput>(HandleAsync, |
||||
|
new ExecutionDataflowBlockOptions { BoundedCapacity = 1 }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public Task NextAsync(TInput input) |
||||
|
{ |
||||
|
return Target.SendAsync(input); |
||||
|
} |
||||
|
|
||||
|
public void NextOrThrowAway(TInput input) |
||||
|
{ |
||||
|
Target.Post(input); |
||||
|
} |
||||
|
|
||||
|
public void Complete() |
||||
|
{ |
||||
|
Target.Complete(); |
||||
|
} |
||||
|
|
||||
|
public void Reset() |
||||
|
{ |
||||
|
lastEventNumber = -1; |
||||
|
} |
||||
|
|
||||
|
public void LinkTo(ITargetBlock<TOutput> other) |
||||
|
{ |
||||
|
if (Target is TransformBlock<TInput, TOutput> transformBlock) |
||||
|
{ |
||||
|
transformBlock.LinkTo(other, e => e != null); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected abstract Task<TOutput> On(TInput input); |
||||
|
|
||||
|
protected abstract long GetEventNumber(TInput input); |
||||
|
|
||||
|
private async Task<TOutput> HandleAsync(TInput input) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var eventNumber = GetEventNumber(input); |
||||
|
|
||||
|
if (eventNumber > lastEventNumber) |
||||
|
{ |
||||
|
var envelope = await On(input); |
||||
|
|
||||
|
lastEventNumber = eventNumber; |
||||
|
|
||||
|
return envelope; |
||||
|
} |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Log.LogFatal(ex, w => w.WriteProperty("action", "EventHandlingFailed")); |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
await EventConsumerInfoRepository.StopAsync(EventConsumer.Name, ex.ToString()); |
||||
|
} |
||||
|
catch (Exception ex2) |
||||
|
{ |
||||
|
Log.LogFatal(ex2, w => w.WriteProperty("action", "EventHandlingFailed")); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return default(TOutput); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
// ==========================================================================
|
||||
|
// ParseEventBlock.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Internal |
||||
|
{ |
||||
|
internal sealed class ParseEventBlock : EventReceiverBlock<StoredEvent, Envelope<IEvent>> |
||||
|
{ |
||||
|
private readonly EventDataFormatter formatter; |
||||
|
|
||||
|
public ParseEventBlock(IEventConsumer eventConsumer, IEventConsumerInfoRepository eventConsumerInfoRepository, ISemanticLog log, EventDataFormatter formatter) |
||||
|
: base(true, eventConsumer, eventConsumerInfoRepository, log) |
||||
|
{ |
||||
|
this.formatter = formatter; |
||||
|
} |
||||
|
|
||||
|
protected override Task<Envelope<IEvent>> On(StoredEvent input) |
||||
|
{ |
||||
|
Envelope<IEvent> result = null; |
||||
|
try |
||||
|
{ |
||||
|
result = formatter.Parse(input.Data); |
||||
|
|
||||
|
result.SetEventNumber(input.EventNumber); |
||||
|
result.SetEventStreamNumber(input.EventStreamNumber); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Log.LogFatal(ex, w => w |
||||
|
.WriteProperty("action", "ParseEvent") |
||||
|
.WriteProperty("state", "Failed") |
||||
|
.WriteProperty("eventId", input.Data.EventId.ToString()) |
||||
|
.WriteProperty("eventNumber", input.EventNumber)); |
||||
|
} |
||||
|
|
||||
|
return Task.FromResult(result); |
||||
|
} |
||||
|
|
||||
|
protected override long GetEventNumber(StoredEvent input) |
||||
|
{ |
||||
|
return input.EventNumber; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,102 @@ |
|||||
|
// ==========================================================================
|
||||
|
// QueryEventsBlock.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using System.Threading.Tasks.Dataflow; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
|
||||
|
// ReSharper disable InvertIf
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Internal |
||||
|
{ |
||||
|
internal sealed class QueryEventsBlock : EventReceiverBlock<object, object> |
||||
|
{ |
||||
|
private readonly IEventStore eventStore; |
||||
|
private bool isStarted; |
||||
|
private long handled; |
||||
|
|
||||
|
public Func<StoredEvent, Task> OnEvent { get; set; } |
||||
|
|
||||
|
public Action OnReset { get; set; } |
||||
|
|
||||
|
public QueryEventsBlock(IEventConsumer eventConsumer, IEventConsumerInfoRepository eventConsumerInfoRepository, ISemanticLog log, IEventStore eventStore) |
||||
|
: base(false, eventConsumer, eventConsumerInfoRepository, log) |
||||
|
{ |
||||
|
this.eventStore = eventStore; |
||||
|
} |
||||
|
|
||||
|
protected override async Task<object> On(object input) |
||||
|
{ |
||||
|
if (!isStarted) |
||||
|
{ |
||||
|
await EventConsumerInfoRepository.CreateAsync(EventConsumer.Name); |
||||
|
|
||||
|
isStarted = true; |
||||
|
} |
||||
|
|
||||
|
var status = await EventConsumerInfoRepository.FindAsync(EventConsumer.Name); |
||||
|
|
||||
|
var lastReceivedEventNumber = status.LastHandledEventNumber; |
||||
|
|
||||
|
if (status.IsResetting) |
||||
|
{ |
||||
|
await ResetAsync(); |
||||
|
} |
||||
|
|
||||
|
if (!status.IsStopped) |
||||
|
{ |
||||
|
var ct = CancellationToken.None; |
||||
|
|
||||
|
await eventStore.GetEventsAsync(storedEvent => OnEvent?.Invoke(storedEvent), ct, null, lastReceivedEventNumber); |
||||
|
} |
||||
|
|
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
private async Task ResetAsync() |
||||
|
{ |
||||
|
var consumerName = EventConsumer.Name; |
||||
|
|
||||
|
var actionId = Guid.NewGuid().ToString(); |
||||
|
try |
||||
|
{ |
||||
|
Log.LogInformation(w => w |
||||
|
.WriteProperty("action", "EventConsumerReset") |
||||
|
.WriteProperty("actionId", actionId) |
||||
|
.WriteProperty("state", "Started") |
||||
|
.WriteProperty("eventConsumer", consumerName)); |
||||
|
|
||||
|
await EventConsumer.ClearAsync(); |
||||
|
await EventConsumerInfoRepository.SetLastHandledEventNumberAsync(consumerName, -1); |
||||
|
|
||||
|
Log.LogInformation(w => w |
||||
|
.WriteProperty("action", "EventConsumerReset") |
||||
|
.WriteProperty("actionId", actionId) |
||||
|
.WriteProperty("state", "Completed") |
||||
|
.WriteProperty("eventConsumer", consumerName)); |
||||
|
|
||||
|
OnReset?.Invoke(); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
Log.LogFatal(ex, w => w |
||||
|
.WriteProperty("action", "EventConsumerReset") |
||||
|
.WriteProperty("actionId", actionId) |
||||
|
.WriteProperty("state", "Completed") |
||||
|
.WriteProperty("eventConsumer", consumerName)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override long GetEventNumber(object input) |
||||
|
{ |
||||
|
return handled++; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
// ==========================================================================
|
||||
|
// UpdateStateBlock.cs
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex Group
|
||||
|
// All rights reserved.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.Log; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.CQRS.Events.Internal |
||||
|
{ |
||||
|
public sealed class UpdateStateBlock : EventReceiverBlock<Envelope<IEvent>, Envelope<IEvent>> |
||||
|
{ |
||||
|
public UpdateStateBlock(IEventConsumer eventConsumer, IEventConsumerInfoRepository eventConsumerInfoRepository, ISemanticLog log) |
||||
|
: base(false, eventConsumer, eventConsumerInfoRepository, log) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected override async Task<Envelope<IEvent>> On(Envelope<IEvent> input) |
||||
|
{ |
||||
|
await EventConsumerInfoRepository.SetLastHandledEventNumberAsync(EventConsumer.Name, input.Headers.EventNumber()); |
||||
|
|
||||
|
return input; |
||||
|
} |
||||
|
|
||||
|
protected override long GetEventNumber(Envelope<IEvent> input) |
||||
|
{ |
||||
|
return input.Headers.EventNumber(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue