Browse Source

Fix event consumer and restore state. (#898)

pull/899/head
Sebastian Stehle 3 years ago
committed by GitHub
parent
commit
a750d4cd03
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs
  2. 5
      backend/src/Squidex.Infrastructure/EventSourcing/Consume/BatchSubscription.cs
  3. 7
      backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs
  4. 6
      backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerWorker.cs
  5. 5
      backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs
  6. 43
      backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs
  7. 1
      backend/src/Squidex.Infrastructure/States/NameReservationState.cs
  8. 20
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs
  9. 8
      backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerWorkerTests.cs

10
backend/src/Squidex.Domain.Apps.Entities/Backup/BackupService.cs

@ -15,19 +15,19 @@ namespace Squidex.Domain.Apps.Entities.Backup
{ {
public sealed class BackupService : IBackupService, IDeleter public sealed class BackupService : IBackupService, IDeleter
{ {
private readonly SimpleState<RestoreJob> restoreState; private readonly SimpleState<BackupRestoreState> restoreState;
private readonly IPersistenceFactory<BackupState> persistenceFactoryBackup; private readonly IPersistenceFactory<BackupState> persistenceFactoryBackup;
private readonly IMessageBus messaging; private readonly IMessageBus messaging;
public BackupService( public BackupService(
IPersistenceFactory<RestoreJob> persistenceFactoryRestore, IPersistenceFactory<BackupRestoreState> persistenceFactoryRestore,
IPersistenceFactory<BackupState> persistenceFactoryBackup, IPersistenceFactory<BackupState> persistenceFactoryBackup,
IMessageBus messaging) IMessageBus messaging)
{ {
this.persistenceFactoryBackup = persistenceFactoryBackup; this.persistenceFactoryBackup = persistenceFactoryBackup;
this.messaging = messaging; this.messaging = messaging;
restoreState = new SimpleState<RestoreJob>(persistenceFactoryRestore, GetType(), "Default"); restoreState = new SimpleState<BackupRestoreState>(persistenceFactoryRestore, GetType(), "Default");
} }
Task IDeleter.DeleteAppAsync(IAppEntity app, Task IDeleter.DeleteAppAsync(IAppEntity app,
@ -51,7 +51,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
{ {
await restoreState.LoadAsync(ct); await restoreState.LoadAsync(ct);
restoreState.Value.EnsureCanStart(); restoreState.Value.Job?.EnsureCanStart();
await messaging.PublishAsync(new BackupRestore(actor, url, newAppName), ct: ct); await messaging.PublishAsync(new BackupRestore(actor, url, newAppName), ct: ct);
} }
@ -67,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
{ {
await restoreState.LoadAsync(ct); await restoreState.LoadAsync(ct);
return restoreState.Value; return restoreState.Value.Job ?? new RestoreJob();
} }
public async Task<List<IBackupJob>> GetBackupsAsync(DomainId appId, public async Task<List<IBackupJob>> GetBackupsAsync(DomainId appId,

5
backend/src/Squidex.Infrastructure/EventSourcing/Consume/BatchSubscription.cs

@ -23,8 +23,6 @@ namespace Squidex.Infrastructure.EventSourcing.Consume
IEventSubscriber<ParsedEvents> eventSubscriber, IEventSubscriber<ParsedEvents> eventSubscriber,
EventSubscriptionSource<ParsedEvent> eventSource) EventSubscriptionSource<ParsedEvent> eventSource)
{ {
eventSubscription = eventSource(this);
var batchSize = Math.Max(1, eventConsumer.BatchSize); var batchSize = Math.Max(1, eventConsumer.BatchSize);
var batchDelay = Math.Max(100, eventConsumer.BatchDelay); var batchDelay = Math.Max(100, eventConsumer.BatchDelay);
@ -44,6 +42,9 @@ namespace Squidex.Infrastructure.EventSourcing.Consume
batchQueue.Batch<ParsedEvent>(taskQueue, batchSize, batchDelay, completed.Token); batchQueue.Batch<ParsedEvent>(taskQueue, batchSize, batchDelay, completed.Token);
handleTask = Run(eventSubscriber); handleTask = Run(eventSubscriber);
// Run last to subscribe after everything is configured.
eventSubscription = eventSource(this);
} }
private async Task Run(IEventSubscriber<ParsedEvents> eventSink) private async Task Run(IEventSubscriber<ParsedEvents> eventSink)

7
backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerProcessor.cs

@ -51,11 +51,12 @@ namespace Squidex.Infrastructure.EventSourcing.Consume
public virtual async Task CompleteAsync() public virtual async Task CompleteAsync()
{ {
if (currentSubscription is BatchSubscription batchSubscriber) // This is only needed for tests to wait for all asynchronous tasks inside the subscriptions.
if (currentSubscription != null)
{ {
try try
{ {
await batchSubscriber.CompleteAsync(); await currentSubscription.CompleteAsync();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -250,12 +251,14 @@ namespace Squidex.Infrastructure.EventSourcing.Consume
protected IEventSubscription CreatePipeline(IEventSubscriber<ParsedEvents> subscriber) protected IEventSubscription CreatePipeline(IEventSubscriber<ParsedEvents> subscriber)
{ {
// Create a pipline of subscription inside a retry.
return new BatchSubscription(eventConsumer, subscriber, return new BatchSubscription(eventConsumer, subscriber,
x => new ParseSubscription(eventConsumer, eventFormatter, x, CreateSubscription)); x => new ParseSubscription(eventConsumer, eventFormatter, x, CreateSubscription));
} }
protected virtual IEventSubscription CreateRetrySubscription(IEventSubscriber<ParsedEvents> subscriber) protected virtual IEventSubscription CreateRetrySubscription(IEventSubscriber<ParsedEvents> subscriber)
{ {
// It is very important to have the retry subscription as outer subscription, because we also need to cancel the batching in case of errors.
return new RetrySubscription<ParsedEvents>(subscriber, CreatePipeline); return new RetrySubscription<ParsedEvents>(subscriber, CreatePipeline);
} }

6
backend/src/Squidex.Infrastructure/EventSourcing/Consume/EventConsumerWorker.cs

@ -15,7 +15,7 @@ namespace Squidex.Infrastructure.EventSourcing.Consume
IMessageHandler<EventConsumerStart>, IMessageHandler<EventConsumerStart>,
IMessageHandler<EventConsumerStop>, IMessageHandler<EventConsumerStop>,
IMessageHandler<EventConsumerReset>, IMessageHandler<EventConsumerReset>,
IInitializable IBackgroundProcess
{ {
private readonly Dictionary<string, EventConsumerProcessor> processors = new Dictionary<string, EventConsumerProcessor>(); private readonly Dictionary<string, EventConsumerProcessor> processors = new Dictionary<string, EventConsumerProcessor>();
private CompletionTimer? timer; private CompletionTimer? timer;
@ -29,7 +29,7 @@ namespace Squidex.Infrastructure.EventSourcing.Consume
} }
} }
public async Task InitializeAsync( public async Task StartAsync(
CancellationToken ct) CancellationToken ct)
{ {
foreach (var (_, processor) in processors) foreach (var (_, processor) in processors)
@ -47,7 +47,7 @@ namespace Squidex.Infrastructure.EventSourcing.Consume
}); });
} }
public Task ReleaseAsync( public Task StopAsync(
CancellationToken ct) CancellationToken ct)
{ {
return timer?.StopAsync() ?? Task.CompletedTask; return timer?.StopAsync() ?? Task.CompletedTask;

5
backend/src/Squidex.Infrastructure/EventSourcing/Consume/ParseSubscription.cs

@ -22,8 +22,6 @@ namespace Squidex.Infrastructure.EventSourcing.Consume
IEventSubscriber<ParsedEvent> eventSubscriber, IEventSubscriber<ParsedEvent> eventSubscriber,
EventSubscriptionSource<StoredEvent> eventSource) EventSubscriptionSource<StoredEvent> eventSource)
{ {
eventSubscription = eventSource(this);
deserializeQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(2) deserializeQueue = Channel.CreateBounded<object>(new BoundedChannelOptions(2)
{ {
AllowSynchronousContinuations = true, AllowSynchronousContinuations = true,
@ -89,6 +87,9 @@ namespace Squidex.Infrastructure.EventSourcing.Consume
} }
} }
}).ContinueWith(x => deserializeQueue.Writer.TryComplete(x.Exception)); }).ContinueWith(x => deserializeQueue.Writer.TryComplete(x.Exception));
// Run last to subscribe after everything is configured.
eventSubscription = eventSource(this);
} }
public void Dispose() public void Dispose()

43
backend/src/Squidex.Infrastructure/EventSourcing/RetrySubscription.cs

@ -15,11 +15,30 @@ namespace Squidex.Infrastructure.EventSourcing
private readonly AsyncLock lockObject = new AsyncLock(); private readonly AsyncLock lockObject = new AsyncLock();
private readonly IEventSubscriber<T> eventSubscriber; private readonly IEventSubscriber<T> eventSubscriber;
private readonly EventSubscriptionSource<T> eventSource; private readonly EventSubscriptionSource<T> eventSource;
private CancellationTokenSource timerCancellation = new CancellationTokenSource(); private SubscriptionHolder? currentSubscription;
private IEventSubscription? currentSubscription;
public int ReconnectWaitMs { get; set; } = 5000; public int ReconnectWaitMs { get; set; } = 5000;
// Holds all information for a current subscription. Therefore we only have to maintain one reference.
private sealed class SubscriptionHolder : IDisposable
{
public CancellationTokenSource Cancellation { get; } = new CancellationTokenSource();
public IEventSubscription Subscription { get; }
public SubscriptionHolder(IEventSubscription subscription)
{
Subscription = subscription;
}
public void Dispose()
{
Cancellation.Cancel();
Subscription.Dispose();
}
}
public RetrySubscription(IEventSubscriber<T> eventSubscriber, public RetrySubscription(IEventSubscriber<T> eventSubscriber,
EventSubscriptionSource<T> eventSource) EventSubscriptionSource<T> eventSource)
{ {
@ -49,7 +68,7 @@ namespace Squidex.Infrastructure.EventSourcing
return; return;
} }
currentSubscription = eventSource(this); currentSubscription = new SubscriptionHolder(eventSource(this));
} }
private void Unsubscribe() private void Unsubscribe()
@ -59,30 +78,26 @@ namespace Squidex.Infrastructure.EventSourcing
return; return;
} }
timerCancellation.Cancel();
timerCancellation.Dispose();
currentSubscription.Dispose(); currentSubscription.Dispose();
currentSubscription = null; currentSubscription = null;
timerCancellation = new CancellationTokenSource();
} }
public void WakeUp() public void WakeUp()
{ {
currentSubscription?.WakeUp(); currentSubscription?.Subscription.WakeUp();
} }
public ValueTask CompleteAsync() public ValueTask CompleteAsync()
{ {
return currentSubscription?.CompleteAsync() ?? default; return currentSubscription?.Subscription.CompleteAsync() ?? default;
} }
async ValueTask IEventSubscriber<T>.OnNextAsync(IEventSubscription subscription, T @event) async ValueTask IEventSubscriber<T>.OnNextAsync(IEventSubscription subscription, T @event)
{ {
// It is not entirely sure, if the lock is needed, but it seems to work so far.
using (await lockObject.EnterAsync(default)) using (await lockObject.EnterAsync(default))
{ {
if (!ReferenceEquals(subscription, currentSubscription)) if (!ReferenceEquals(subscription, currentSubscription?.Subscription))
{ {
return; return;
} }
@ -100,11 +115,12 @@ namespace Squidex.Infrastructure.EventSourcing
using (await lockObject.EnterAsync(default)) using (await lockObject.EnterAsync(default))
{ {
if (!ReferenceEquals(subscription, currentSubscription)) if (!ReferenceEquals(subscription, currentSubscription?.Subscription))
{ {
return; return;
} }
// Unsubscribing is not an atomar operation, therefore the lock.
Unsubscribe(); Unsubscribe();
if (!retryWindow.CanRetryAfterFailure()) if (!retryWindow.CanRetryAfterFailure())
@ -116,7 +132,7 @@ namespace Squidex.Infrastructure.EventSourcing
try try
{ {
await Task.Delay(ReconnectWaitMs, timerCancellation.Token); await Task.Delay(ReconnectWaitMs, currentSubscription?.Cancellation?.Token ?? default);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@ -125,6 +141,7 @@ namespace Squidex.Infrastructure.EventSourcing
using (await lockObject.EnterAsync(default)) using (await lockObject.EnterAsync(default))
{ {
// Subscribing is not an atomar operation, therefore the lock.
Subscribe(); Subscribe();
} }
} }

1
backend/src/Squidex.Infrastructure/States/NameReservationState.cs

@ -9,6 +9,7 @@ namespace Squidex.Infrastructure.States
{ {
public sealed class NameReservationState : SimpleState<NameReservationState.State> public sealed class NameReservationState : SimpleState<NameReservationState.State>
{ {
[CollectionName("Names")]
public sealed class State public sealed class State
{ {
public List<NameReservation> Reservations { get; set; } = new List<NameReservation>(); public List<NameReservation> Reservations { get; set; } = new List<NameReservation>();

20
backend/tests/Squidex.Domain.Apps.Entities.Tests/Backup/BackupServiceTests.cs

@ -20,7 +20,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
public class BackupServiceTests public class BackupServiceTests
{ {
private readonly TestState<BackupState> stateBackup; private readonly TestState<BackupState> stateBackup;
private readonly TestState<RestoreJob> stateRestore; private readonly TestState<BackupRestoreState> stateRestore;
private readonly IMessageBus messaging = A.Fake<IMessageBus>(); private readonly IMessageBus messaging = A.Fake<IMessageBus>();
private readonly DomainId appId = DomainId.NewGuid(); private readonly DomainId appId = DomainId.NewGuid();
private readonly DomainId backupId = DomainId.NewGuid(); private readonly DomainId backupId = DomainId.NewGuid();
@ -29,7 +29,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
public BackupServiceTests() public BackupServiceTests()
{ {
stateRestore = new TestState<RestoreJob>("Default"); stateRestore = new TestState<BackupRestoreState>("Default");
stateBackup = new TestState<BackupState>(appId); stateBackup = new TestState<BackupState>(appId);
sut = new BackupService( sut = new BackupService(
@ -79,9 +79,12 @@ namespace Squidex.Domain.Apps.Entities.Backup
[Fact] [Fact]
public async Task Should_throw_exception_when_restore_already_running() public async Task Should_throw_exception_when_restore_already_running()
{ {
stateRestore.Snapshot = new RestoreJob stateRestore.Snapshot = new BackupRestoreState
{ {
Status = JobStatus.Started Job = new RestoreJob
{
Status = JobStatus.Started
}
}; };
var restoreUrl = new Uri("http://squidex.io"); var restoreUrl = new Uri("http://squidex.io");
@ -114,14 +117,17 @@ namespace Squidex.Domain.Apps.Entities.Backup
[Fact] [Fact]
public async Task Should_get_restore_state_from_store() public async Task Should_get_restore_state_from_store()
{ {
stateRestore.Snapshot = new RestoreJob stateRestore.Snapshot = new BackupRestoreState
{ {
Stopped = SystemClock.Instance.GetCurrentInstant() Job = new RestoreJob
{
Stopped = SystemClock.Instance.GetCurrentInstant()
}
}; };
var result = await sut.GetRestoreAsync(); var result = await sut.GetRestoreAsync();
result.Should().BeEquivalentTo(stateRestore.Snapshot); result.Should().BeEquivalentTo(stateRestore.Snapshot.Job);
} }
[Fact] [Fact]

8
backend/tests/Squidex.Infrastructure.Tests/EventSourcing/Consume/EventConsumerWorkerTests.cs

@ -32,15 +32,15 @@ namespace Squidex.Infrastructure.EventSourcing.Consume
} }
[Fact] [Fact]
public async Task Should_release_without_initialize() public async Task Should_stop_without_start()
{ {
await sut.ReleaseAsync(default); await sut.StopAsync(default);
} }
[Fact] [Fact]
public async Task Should_initialize_all_processors_on_initialize() public async Task Should_initialize_all_processors_on_initialize()
{ {
await sut.InitializeAsync(default); await sut.StartAsync(default);
A.CallTo(() => processor1.InitializeAsync(default)) A.CallTo(() => processor1.InitializeAsync(default))
.MustHaveHappenedOnceExactly(); .MustHaveHappenedOnceExactly();
@ -52,7 +52,7 @@ namespace Squidex.Infrastructure.EventSourcing.Consume
[Fact] [Fact]
public async Task Should_activate_all_processors_on_initialize() public async Task Should_activate_all_processors_on_initialize()
{ {
await sut.InitializeAsync(default); await sut.StartAsync(default);
A.CallTo(() => processor1.ActivateAsync()) A.CallTo(() => processor1.ActivateAsync())
.MustHaveHappened(); .MustHaveHappened();

Loading…
Cancel
Save