public void ProjectAsyncTokenMessagesCancellationAfterSecondHandlerCausesExpectedResult() { var tcs = new CancellationTokenSource(); Func <object, object, CancellationToken, Task> handler1 = (connection, message, token) => Task.FromResult <object>(null); Func <object, object, CancellationToken, Task> handler2 = (connection, message, token) => { tcs.Cancel(); return(Task.FromResult <object>(null)); }; Func <object, object, CancellationToken, Task> handler3 = (connection, message, token) => { throw new InvalidOperationException("Should not happen!"); }; ConnectedProjectionHandlerResolver <object> resolver = Resolve.WhenEqualToHandlerMessageType(new[] { new ConnectedProjectionHandler <object>(typeof(object), handler1), new ConnectedProjectionHandler <object>(typeof(object), handler2), new ConnectedProjectionHandler <object>(typeof(object), handler3) }); var sut = SutFactory(resolver); Assert.That(async() => await sut.ProjectAsync(new object(), new[] { new object(), new object(), new object() }, tcs.Token), Throws.Nothing); }
public void SetUp() { _resolver = Resolve. WhenEqualToHandlerMessageType(new ConnectedProjectionHandler <object> [0]); _messages = new[] { new object(), new object() }; _sut = new ConnectedProjectionScenario <object>(_resolver).Given(_messages); }
public static IEnumerable <TestCaseData> ProjectMessageWithoutTokenCases() { var task = TaskFactory(); //Match var message1 = new object(); var handler1 = new ConnectedProjectionHandler <CallRecordingConnection>( typeof(object), (connection, message, token) => { connection.RecordCall(1, message, token); return(task); }); var resolver1 = new ConnectedProjectionHandlerResolver <CallRecordingConnection>(message => new[] { handler1 }); yield return(new TestCaseData( resolver1, message1, new[] { new Tuple <int, object, CancellationToken>(1, message1, CancellationToken.None), })); //Mismatch var message2 = new object(); var resolver2 = new ConnectedProjectionHandlerResolver <CallRecordingConnection>(message => new ConnectedProjectionHandler <CallRecordingConnection> [0]); yield return(new TestCaseData( resolver2, message2, new Tuple <int, object, CancellationToken> [0])); //Multimatch var message3 = new object(); var handler3 = new ConnectedProjectionHandler <CallRecordingConnection>( typeof(object), (connection, message, token) => { connection.RecordCall(3, message, token); return(task); }); var handler4 = new ConnectedProjectionHandler <CallRecordingConnection>( typeof(object), (connection, message, token) => { connection.RecordCall(4, message, token); return(task); }); var resolver3 = new ConnectedProjectionHandlerResolver <CallRecordingConnection>(message => new[] { handler3, handler4 }); yield return(new TestCaseData( resolver3, message3, new[] { new Tuple <int, object, CancellationToken>(3, message3, CancellationToken.None), new Tuple <int, object, CancellationToken>(4, message3, CancellationToken.None) })); }
/// <summary> /// Initializes a new instance of the <see cref="ConnectedProjectionScenario{TConnection}"/> class. /// </summary> /// <param name="resolver">The projection handler resolver.</param> /// <exception cref="System.ArgumentNullException">Thrown when <paramref name="resolver"/> is <c>null</c>.</exception> public ConnectedProjectionScenario(ConnectedProjectionHandlerResolver <TConnection> resolver) { if (resolver == null) { throw new ArgumentNullException("resolver"); } _resolver = resolver; _messages = new object[0]; }
/// <summary> /// Initializes a new instance of the <see cref="ConnectedProjector{TConnection}"/> class. /// </summary> /// <param name="resolver">The handler resolver.</param> /// <exception cref="System.ArgumentNullException">Thrown when <paramref name="resolver"/> is <c>null</c>.</exception> public ConnectedProjector(ConnectedProjectionHandlerResolver <TConnection> resolver) { if (resolver == null) { throw new ArgumentNullException("resolver"); } _resolver = resolver; }
public void ProjectAsyncTokenMessagesResolverFailureCausesExpectedResult() { ConnectedProjectionHandlerResolver <object> resolver = m => throw new Exception("message"); var sut = SutFactory(resolver); Assert.That(async() => await sut.ProjectAsync(new object(), new[] { new object(), new object() }, new CancellationToken()), Throws.TypeOf <Exception>().And.Message.EqualTo("message")); }
public async Task ProjectAsyncMessageCausesExpectedCalls( ConnectedProjectionHandlerResolver <CallRecordingConnection> resolver, object message, Tuple <int, object, CancellationToken>[] expectedCalls) { var connection = new CallRecordingConnection(); var sut = SutFactory(resolver); await sut.ProjectAsync(connection, message); Assert.That(connection.RecordedCalls, Is.EquivalentTo(expectedCalls)); }
public void ProjectAsyncTokenMessagesFirstHandlerFailureCausesExpectedResult() { Func <object, object, CancellationToken, Task> handler = (connection, message, token) => { var source = new TaskCompletionSource <object>(); source.SetException(new Exception("message")); return(source.Task); }; ConnectedProjectionHandlerResolver <object> resolver = m => new[] { new ConnectedProjectionHandler <object>(typeof(object), handler) }; var sut = SutFactory(resolver); Assert.That(async() => await sut.ProjectAsync(new object(), new[] { new object(), new object() }, new CancellationToken()), Throws.TypeOf <Exception>().And.Message.EqualTo("message")); }
/// <summary> /// Initializes a new instance of the <see cref="ConnectedProjectionTestSpecification{TConnection}"/> class. /// </summary> /// <param name="resolver">The projection handler resolver.</param> /// <param name="messages">The messages to project.</param> /// <param name="verification">The verification method.</param> /// <exception cref="System.ArgumentNullException">Thrown when /// <paramref name="resolver"/> /// or /// <paramref name="messages"/> /// or /// <paramref name="verification"/> is null. /// </exception> public ConnectedProjectionTestSpecification(ConnectedProjectionHandlerResolver <TConnection> resolver, object[] messages, Func <TConnection, CancellationToken, Task <VerificationResult> > verification) { if (resolver == null) { throw new ArgumentNullException("resolver"); } if (messages == null) { throw new ArgumentNullException("messages"); } if (verification == null) { throw new ArgumentNullException("verification"); } _resolver = resolver; _messages = messages; _verification = verification; }
public void ProjectAsyncTokenMessagesSecondHandlerCancellationCausesExpectedResult() { Func <object, object, CancellationToken, Task> handler1 = (connection, message, token) => Task.FromResult <object>(null); Func <object, object, CancellationToken, Task> handler2 = (connection, message, token) => { var source = new TaskCompletionSource <object>(); source.SetCanceled(); return(source.Task); }; ConnectedProjectionHandlerResolver <object> resolver = Resolve.WhenEqualToHandlerMessageType(new[] { new ConnectedProjectionHandler <object>(typeof(object), handler1), new ConnectedProjectionHandler <object>(typeof(int), handler2) }); var sut = SutFactory(resolver); Assert.That(async() => await sut.ProjectAsync(new object(), new[] { new object(), new int() }, new CancellationToken()), Throws.TypeOf <TaskCanceledException>()); }
public void ProjectAsyncMessageSecondHandlerFailureCausesExpectedResult() { Func <object, object, CancellationToken, Task> handler1 = (connection, message, token) => Task.FromResult <object>(null); Func <object, object, CancellationToken, Task> handler2 = (connection, message, token) => { var source = new TaskCompletionSource <object>(); source.SetException(new Exception("message")); return(source.Task); }; ConnectedProjectionHandlerResolver <object> resolver = Resolve.WhenEqualToHandlerMessageType(new[] { new ConnectedProjectionHandler <object>(typeof(object), handler1), new ConnectedProjectionHandler <object>(typeof(int), handler2) }); var sut = SutFactory(resolver); Assert.That(async() => await sut.ProjectAsync(new object(), new int()), Throws.TypeOf <AggregateException>().And.InnerException.TypeOf <Exception>().And.InnerException.Message.EqualTo("message")); }
public void ProjectAsyncTokenMessagesCancellationAfterSecondHandlerInContinuationCausesExpectedResult() { var tcs = new CancellationTokenSource(); tcs.Cancel(); Func <object, object, CancellationToken, Task> handler1 = async(connection, message, token) => { await Task.Yield(); await Task.Delay(TimeSpan.FromDays(1), token); }; ConnectedProjectionHandlerResolver <object> resolver = Resolve.WhenEqualToHandlerMessageType(new[] { new ConnectedProjectionHandler <object>(typeof(object), handler1), }); var sut = SutFactory(resolver); Assert.That(async() => await sut.ProjectAsync(new object(), new[] { new object() }, tcs.Token), Throws.TypeOf <TaskCanceledException>()); }
public void ProjectAsyncTokenMessagesHandlerObservingCancelledTokenCausesExpectedResult() { var tcs = new CancellationTokenSource(); Func <object, object, CancellationToken, Task> handler1 = (connection, message, token) => { var source = new TaskCompletionSource <object>(); token.Register(() => source.SetCanceled()); tcs.Cancel(); return(source.Task); }; ConnectedProjectionHandlerResolver <object> resolver = Resolve.WhenEqualToHandlerMessageType(new[] { new ConnectedProjectionHandler <object>(typeof(object), handler1), }); var sut = SutFactory(resolver); Assert.That(async() => await sut.ProjectAsync(new object(), new[] { new object() }, tcs.Token), Throws.TypeOf <TaskCanceledException>()); }
/// <summary> /// Initializes a new instance of the <see cref="ConnectedProjector{TConnection}"/> class. /// </summary> /// <param name="resolver">The handler resolver.</param> /// <exception cref="System.ArgumentNullException">Thrown when <paramref name="resolver"/> is <c>null</c>.</exception> public ConnectedProjector(ConnectedProjectionHandlerResolver <TConnection> resolver) => _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
/// <summary> /// Initializes a new instance of the <see cref="ConnectedProjectionTestSpecification{TConnection}"/> class. /// </summary> /// <param name="resolver">The projection handler resolver.</param> /// <param name="messages">The messages to project.</param> /// <param name="verification">The verification method.</param> /// <exception cref="System.ArgumentNullException">Thrown when /// <paramref name="resolver"/> /// or /// <paramref name="messages"/> /// or /// <paramref name="verification"/> is null. /// </exception> public ConnectedProjectionTestSpecification(ConnectedProjectionHandlerResolver <TConnection> resolver, object[] messages, Func <TConnection, CancellationToken, Task <VerificationResult> > verification) { Resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); Messages = messages ?? throw new ArgumentNullException(nameof(messages)); Verification = verification ?? throw new ArgumentNullException(nameof(verification)); }
private static ConnectedProjector <TConnection> SutFactory <TConnection>(ConnectedProjectionHandlerResolver <TConnection> resolver) { return(new ConnectedProjector <TConnection>(resolver)); }
/// <summary> /// Initializes a new instance of the <see cref="ConnectedProjectionScenario{TConnection}"/> class. /// </summary> /// <param name="resolver">The projection handler resolver.</param> /// <exception cref="System.ArgumentNullException">Thrown when <paramref name="resolver"/> is <c>null</c>.</exception> public ConnectedProjectionScenario(ConnectedProjectionHandlerResolver <TConnection> resolver) { _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); _messages = new object[0]; }
public static ConnectedProjectionScenario <TConnection> Create <TConnection>(ConnectedProjectionHandlerResolver <TConnection> resolver) { return(new ConnectedProjectionScenario <TConnection>(resolver)); }
public EventProcessor( IStreamStore streamStore, AcceptStreamMessageFilter filter, EnvelopeFactory envelopeFactory, ConnectedProjectionHandlerResolver <EditorContext> resolver, Func <EditorContext> dbContextFactory, Scheduler scheduler, ILogger <EventProcessor> logger) { if (streamStore == null) { throw new ArgumentNullException(nameof(streamStore)); } if (filter == null) { throw new ArgumentNullException(nameof(filter)); } if (envelopeFactory == null) { throw new ArgumentNullException(nameof(envelopeFactory)); } if (resolver == null) { throw new ArgumentNullException(nameof(resolver)); } if (dbContextFactory == null) { throw new ArgumentNullException(nameof(dbContextFactory)); } _scheduler = scheduler ?? throw new ArgumentNullException(nameof(scheduler)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _messagePumpCancellation = new CancellationTokenSource(); _messageChannel = Channel.CreateUnbounded <object>(new UnboundedChannelOptions { SingleReader = true, SingleWriter = false, AllowSynchronousContinuations = false }); _messagePump = Task.Factory.StartNew(async() => { IAllStreamSubscription subscription = null; try { logger.LogInformation("EventProcessor message pump entered ..."); while (await _messageChannel.Reader.WaitToReadAsync(_messagePumpCancellation.Token).ConfigureAwait(false)) { while (_messageChannel.Reader.TryRead(out var message)) { switch (message) { case Resume _: logger.LogInformation("Resuming ..."); await using (var resumeContext = dbContextFactory()) { var projection = await resumeContext.ProjectionStates .SingleOrDefaultAsync( item => item.Name == RoadRegistryEditorProjectionHost, _messagePumpCancellation.Token) .ConfigureAwait(false); var after = projection?.Position; var head = await streamStore.ReadHeadPosition(); if (head == Position.Start || (after.HasValue ? head - after.Value <= CatchUpThreshold : head - CatchUpThreshold <= 0)) { await _messageChannel.Writer .WriteAsync(new Subscribe(after), _messagePumpCancellation.Token) .ConfigureAwait(false); } else { await _messageChannel.Writer .WriteAsync(new CatchUp(after, CatchUpBatchSize), _messagePumpCancellation.Token) .ConfigureAwait(false); } } break; case CatchUp catchUp: logger.LogInformation("Catching up as of {Position}", catchUp.AfterPosition ?? -1L); var observedMessageCount = 0; var catchUpPosition = catchUp.AfterPosition ?? Position.Start; var context = dbContextFactory(); var page = await streamStore .ReadAllForwards( catchUpPosition, catchUp.BatchSize, true, _messagePumpCancellation.Token) .ConfigureAwait(false); while (!page.IsEnd) { foreach (var streamMessage in page.Messages) { if (catchUp.AfterPosition.HasValue && streamMessage.Position == catchUp.AfterPosition.Value) { continue; // skip already processed message } if (filter(streamMessage)) { logger.LogInformation("Catching up on {MessageType} at {Position}", streamMessage.Type, streamMessage.Position); var envelope = envelopeFactory.Create(streamMessage); var handlers = resolver(envelope); foreach (var handler in handlers) { await handler .Handler(context, envelope, _messagePumpCancellation.Token) .ConfigureAwait(false); } } observedMessageCount++; catchUpPosition = streamMessage.Position; if (observedMessageCount % CatchUpBatchSize == 0) { logger.LogInformation( "Flushing catch up position of {0} and persisting changes ...", catchUpPosition); await context .UpdateProjectionState( RoadRegistryEditorProjectionHost, catchUpPosition, _messagePumpCancellation.Token) .ConfigureAwait(false); await context.SaveChangesAsync(_messagePumpCancellation.Token).ConfigureAwait(false); await context.DisposeAsync().ConfigureAwait(false); context = dbContextFactory(); observedMessageCount = 0; } } page = await page.ReadNext(_messagePumpCancellation.Token).ConfigureAwait(false); } if (observedMessageCount > 0) // case where we just read the last page and pending work in memory needs to be flushed { logger.LogInformation( "Flushing catch up position of {Position} and persisting changes ...", catchUpPosition); await context .UpdateProjectionState( RoadRegistryEditorProjectionHost, catchUpPosition, _messagePumpCancellation.Token) .ConfigureAwait(false); await context.SaveChangesAsync(_messagePumpCancellation.Token).ConfigureAwait(false); } await context.DisposeAsync().ConfigureAwait(false); //switch to subscription as of the last page await _messageChannel.Writer .WriteAsync( new Subscribe(catchUpPosition), _messagePumpCancellation.Token) .ConfigureAwait(false); break; case Subscribe subscribe: logger.LogInformation("Subscribing as of {0}", subscribe.AfterPosition ?? -1L); subscription?.Dispose(); subscription = streamStore.SubscribeToAll( subscribe.AfterPosition, async(_, streamMessage, token) => { if (filter(streamMessage)) { logger.LogInformation("Observing {0} at {1}", streamMessage.Type, streamMessage.Position); var command = new ProcessStreamMessage(streamMessage); await _messageChannel.Writer.WriteAsync(command, token).ConfigureAwait(false); await command.Completion.ConfigureAwait(false); } else if (streamMessage.Position % RecordPositionThreshold == 0 && !_messagePumpCancellation.IsCancellationRequested) { await _messageChannel.Writer .WriteAsync(new RecordPosition(streamMessage), token) .ConfigureAwait(false); } }, async(_, reason, exception) => { if (!_messagePumpCancellation.IsCancellationRequested) { await _messageChannel.Writer .WriteAsync( new SubscriptionDropped(reason, exception), _messagePumpCancellation.Token) .ConfigureAwait(false); } }, prefetchJsonData: false, name: "RoadRegistry.Editor.ProjectionHost.EventProcessor"); break; case RecordPosition record: try { logger.LogInformation("Recording position of {MessageType} at {Position}.", record.Message.Type, record.Message.Position); await using (var recordContext = dbContextFactory()) { await recordContext .UpdateProjectionState( RoadRegistryEditorProjectionHost, record.Message.Position, _messagePumpCancellation.Token) .ConfigureAwait(false); await recordContext.SaveChangesAsync(_messagePumpCancellation.Token).ConfigureAwait(false); } } catch (Exception exception) { logger.LogError(exception, exception.Message); } break; case ProcessStreamMessage process: try { logger.LogInformation("Processing {MessageType} at {Position}", process.Message.Type, process.Message.Position); var envelope = envelopeFactory.Create(process.Message); var handlers = resolver(envelope); await using (var processContext = dbContextFactory()) { foreach (var handler in handlers) { await handler .Handler(processContext, envelope, _messagePumpCancellation.Token) .ConfigureAwait(false); } await processContext.UpdateProjectionState( RoadRegistryEditorProjectionHost, process.Message.Position, _messagePumpCancellation.Token).ConfigureAwait(false); await processContext.SaveChangesAsync(_messagePumpCancellation.Token).ConfigureAwait(false); } process.Complete(); } catch (Exception exception) { logger.LogError(exception, exception.Message); // how are we going to recover from this? do we even need to recover from this? // prediction: it's going to be a serialization error, a data quality error, or a bug process.Fault(exception); } break; case SubscriptionDropped dropped: if (dropped.Reason == SubscriptionDroppedReason.StreamStoreError) { logger.LogError(dropped.Exception, "Subscription was dropped because of a stream store error"); await scheduler.Schedule(async token => { if (!_messagePumpCancellation.IsCancellationRequested) { await _messageChannel.Writer.WriteAsync(new Resume(), token).ConfigureAwait(false); } }, ResubscribeAfter).ConfigureAwait(false); } else if (dropped.Reason == SubscriptionDroppedReason.SubscriberError) { logger.LogError(dropped.Exception, "Subscription was dropped because of a subscriber error"); if (dropped.Exception != null && dropped.Exception is SqlException sqlException && sqlException.Number == -2 /* timeout */) { await scheduler.Schedule(async token => { if (!_messagePumpCancellation.IsCancellationRequested) { await _messageChannel.Writer.WriteAsync(new Resume(), token).ConfigureAwait(false); } }, ResubscribeAfter).ConfigureAwait(false); } } break; } } } } catch (TaskCanceledException) { logger.LogInformation("EventProcessor message pump is exiting due to cancellation"); } catch (OperationCanceledException) { logger.LogInformation("EventProcessor message pump is exiting due to cancellation"); } catch (Exception exception) { logger.LogError(exception, "EventProcessor message pump is exiting due to a bug"); } finally { subscription?.Dispose(); } }, _messagePumpCancellation.Token, TaskCreationOptions.LongRunning | TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); }
private ConnectedProjectionScenario(ConnectedProjectionHandlerResolver <TConnection> resolver, object[] messages) { _resolver = resolver; _messages = messages; }
public static IEnumerable<TestCaseData> ProjectMessageWithTokenCases() { var task = TaskFactory(); //Match var token1 = new CancellationToken(); var message1 = new object(); var handler1 = new ConnectedProjectionHandler<CallRecordingConnection>( typeof(object), (connection, message, token) => { connection.RecordCall(1, message, token); return task; }); var resolver1 = new ConnectedProjectionHandlerResolver<CallRecordingConnection>(message => new[] { handler1 }); yield return new TestCaseData( resolver1, message1, token1, new[] { new Tuple<int, object, CancellationToken>(1, message1, token1) }); //Mismatch var token2 = new CancellationToken(); var message2 = new object(); var resolver2 = new ConnectedProjectionHandlerResolver<CallRecordingConnection>(message => new ConnectedProjectionHandler<CallRecordingConnection>[0]); yield return new TestCaseData( resolver2, message2, token2, new Tuple<int, object, CancellationToken>[0]); //Multimatch var token3 = new CancellationToken(); var message3 = new object(); var handler3 = new ConnectedProjectionHandler<CallRecordingConnection>( typeof(object), (connection, message, token) => { connection.RecordCall(3, message, token); return task; }); var handler4 = new ConnectedProjectionHandler<CallRecordingConnection>( typeof(object), (connection, message, token) => { connection.RecordCall(4, message, token); return task; }); var resolver3 = new ConnectedProjectionHandlerResolver<CallRecordingConnection>(message => new[] { handler3, handler4 }); yield return new TestCaseData( resolver3, message3, token3, new[] { new Tuple<int, object, CancellationToken>(3, message3, token3), new Tuple<int, object, CancellationToken>(4, message3, token3) }); }