public async Task When_two_different_callers_advance_the_same_clock_at_the_same_time_then_commands_are_only_run_once() { // arrange var order = CommandSchedulingTests_EventSourced.CreateOrder(); var barrier = new Barrier(2); // act order.Apply(new ShipOn(Clock.Now().AddDays(5))); await Save(order); var eventCount = order.Events().Count(); var orderScheduler = Configuration.Current .CommandDeliverer <Order>() .InterceptDeliver(async(c, next) => { barrier.SignalAndWait(TimeSpan.FromSeconds(10)); await next(c); }); Configuration.Current.UseDependency(_ => orderScheduler); var caller1 = Task.Run(() => AdvanceClock(TimeSpan.FromDays(10))); var caller2 = Task.Run(() => AdvanceClock(TimeSpan.FromDays(10))); Task.WaitAll(caller1, caller2); (await Get <Order>(order.Id)) .Events() .Count() .Should() .Be(eventCount + 1, "the scheduled command should only be applied once"); }
public async Task When_a_clock_is_advanced_then_resulting_SuccessfulCommands_are_included_in_the_result() { // arrange var shipmentId = Any.AlphanumericString(8, 8); var customerAccountId = Any.Guid(); await Save(new CustomerAccount(customerAccountId) .Apply(new ChangeEmailAddress(Any.Email()))); var order = CommandSchedulingTests_EventSourced.CreateOrder(customerAccountId: customerAccountId); order.Apply(new ShipOn(shipDate: Clock.Now().AddMonths(1).Date) { ShipmentId = shipmentId }); await Save(order); // act var result = await AdvanceClock(to : Clock.Now().AddMonths(2)); //assert result.SuccessfulCommands .Should() .ContainSingle(_ => _.ScheduledCommand .IfTypeIs <IScheduledCommand <Order> >() .Then(c => c.TargetId == order.Id.ToString()) .ElseDefault()); }
public async Task When_a_scheduled_command_fails_due_to_a_concurrency_exception_then_commands_that_its_handler_scheduled_are_not_duplicated() { var order = CommandSchedulingTests_EventSourced.CreateOrder(); await Save(new CustomerAccount(order.CustomerId).Apply(new ChangeEmailAddress(Any.Email()))); await Save(order); TriggerConcurrencyExceptionOnOrderCommands(order.Id); await Schedule(order.Id, new Cancel()); for (var i = 1; i < 3; i++) { await AdvanceClock(by : TimeSpan.FromDays(1)); } StopTriggeringConcurrencyExceptions(); await AdvanceClock(by : TimeSpan.FromDays(1)); await SchedulerWorkComplete(); var customer = await Get <CustomerAccount>(order.CustomerId); customer.Events() .OfType <CustomerAccount.OrderCancelationConfirmationEmailSent>() .Count() .Should() .Be(1); }
public async Task When_ServiceBusCommandQueueSender_is_subscribed_to_the_service_bus_then_messages_are_scheduled_to_trigger_event_based_scheduled_commands() { VirtualClock.Start(DateTimeOffset.Now.AddHours(-13)); using (var queueReceiver = CreateQueueReceiver()) { var aggregateIds = Enumerable.Range(1, 5) .Select(_ => Guid.NewGuid()) .ToArray(); aggregateIds.ForEach(async id => { var order = CommandSchedulingTests_EventSourced.CreateOrder(orderId: id); // due enough in the future that the scheduler won't apply the commands immediately var due = Clock.Now().AddSeconds(5); order.Apply(new ShipOn(due)); Console.WriteLine(new { ShipOrderId = order.Id, due }); await Configuration.Current.Repository <Order>().Save(order); }); // reset the clock so that when the messages are delivered, the target commands are now due Clock.Reset(); await queueReceiver.StartReceivingMessages(); schedulerActivity .Select(a => Guid.Parse(a.TargetId)) .ShouldBeEquivalentTo(aggregateIds); } }
public override async Task When_a_clock_is_advanced_its_associated_commands_are_triggered() { // arrange var shipmentId = Any.AlphanumericString(8, 8); var order = CommandSchedulingTests_EventSourced.CreateOrder(); order.Apply(new ShipOn(shipDate: Clock.Now().AddMonths(1).Date) { ShipmentId = shipmentId }); await Save(order); // act await AdvanceClock(by : TimeSpan.FromDays(32)); //assert order = await Get <Order>(order.Id); var lastEvent = order.Events().Last(); lastEvent.Should().BeOfType <Order.Shipped>(); order.ShipmentId .Should() .Be(shipmentId, "Properties should be transferred correctly from the serialized command"); }
public async Task When_a_command_has_been_completed_and_a_message_for_it_arrives_the_message_is_also_Completed() { var aggregateId = Any.Guid(); // due in the past so that it's scheduled immediately var order = CommandSchedulingTests_EventSourced.CreateOrder(orderId: aggregateId) .Apply(new ShipOn(Clock.Now().AddSeconds(-5))); queueSender.MessageDeliveryOffsetFromCommandDueTime = TimeSpan.FromSeconds(0); await Configuration.Current.Repository <Order>().Save(order); using (var receiver = CreateQueueReceiver()) { var receivedMessages = new List <IScheduledCommand>(); receiver.Messages .Where(m => m.IfTypeIs <IScheduledCommand <Order> >() .Then(c => c.TargetId == aggregateId.ToString()) .ElseDefault()) .Subscribe(receivedMessages.Add); await receiver.StartReceivingMessages(); await Task.Delay(TimeSpan.FromSeconds(5)); receivedMessages.Should() .ContainSingle(m => m.IfTypeIs <IScheduledCommand <Order> >() .Then(c => c.TargetId == aggregateId.ToString()) .ElseDefault()); } using (var receiver = CreateQueueReceiver()) { var receivedMessages = new List <IScheduledCommand>(); receiver.Messages .Where(m => m.IfTypeIs <IScheduledCommand <Order> >() .Then(c => c.TargetId == aggregateId.ToString()) .ElseDefault()) .Subscribe(receivedMessages.Add); await receiver.StartReceivingMessages(); await Task.Delay(TimeSpan.FromSeconds(10)); receivedMessages.Count.Should().Be(0); } }
public override async Task Scheduled_commands_are_delivered_immediately_if_past_due_per_the_domain_clock() { // arrange var order = CommandSchedulingTests_EventSourced.CreateOrder(); // act order.Apply(new ShipOn(Clock.Now().Subtract(TimeSpan.FromDays(2)))); await Save(order); await SchedulerWorkComplete(); //assert order = await Get <Order>(order.Id); var lastEvent = order.Events().Last(); lastEvent.Should().BeOfType <Order.Shipped>(); }
public override async Task A_command_handler_can_request_retry_of_a_failed_command_as_soon_as_possible() { // arrange var order = CommandSchedulingTests_EventSourced.CreateOrder(); order.Apply( new ChargeCreditCardOn { Amount = 10, ChargeDate = Clock.Now().AddDays(30), ChargeRetryPeriod = TimeSpan.Zero }); await Save(order); // act // initial attempt fails await AdvanceClock(TimeSpan.FromDays(30.1)); order = await Get <Order>(order.Id); order.Events().Last().Should().BeOfType <CommandScheduled <Order> >(); // two more attempts, advancing the clock only a tick at a time await AdvanceClock(TimeSpan.FromTicks(1)); order = await Get <Order>(order.Id); order.Events().Last().Should().BeOfType <CommandScheduled <Order> >(); await AdvanceClock(TimeSpan.FromTicks(1)); order = await Get <Order>(order.Id); order.Events().Last().Should().BeOfType <CommandScheduled <Order> >(); // final attempt results in giving up await AdvanceClock(TimeSpan.FromTicks(1)); order = await Get <Order>(order.Id); var last = order.Events().Last(); last.Should().BeOfType <Order.Cancelled>(); last.As <Order.Cancelled>().Reason.Should().Be("Final credit card charge attempt failed."); }
public async Task A_command_handler_can_control_retries_of_a_failed_command() { // arrange var order = CommandSchedulingTests_EventSourced.CreateOrder(); order.Apply( new ChargeCreditCardOn { Amount = 10, ChargeDate = Clock.Now().AddDays(30) }); await Save(order); // act // initial attempt fails await AdvanceClock(TimeSpan.FromDays(31)); order = await Get <Order>(order.Id); order.Events().Last().Should().BeOfType <CommandScheduled <Order> >(); // two more attempts await AdvanceClock(TimeSpan.FromDays(1)); order = await Get <Order>(order.Id); order.Events().Last().Should().BeOfType <CommandScheduled <Order> >(); await AdvanceClock(TimeSpan.FromDays(1)); order = await Get <Order>(order.Id); order.Events().Last().Should().BeOfType <CommandScheduled <Order> >(); // final attempt results in giving up await AdvanceClock(TimeSpan.FromDays(1)); order = await Get <Order>(order.Id); var last = order.Events().Last(); last.Should().BeOfType <Order.Cancelled>(); last.As <Order.Cancelled>().Reason.Should().Be("Final credit card charge attempt failed."); }
public async Task When_a_scheduled_command_fails_due_to_a_concurrency_exception_then_it_is_retried_by_default() { var order = CommandSchedulingTests_EventSourced.CreateOrder(); await Save(order); TriggerConcurrencyExceptionOnOrderCommands(order.Id); await Schedule(order.Id, new Cancel()); for (var i = 1; i < 6; i++) { Console.WriteLine("Advancing clock"); GetScheduledCommandNumberOfAttempts(order.Id) .Should() .Be(i); await AdvanceClock(by : TimeSpan.FromDays(20)); } }
public override async Task When_a_clock_is_advanced_then_commands_are_not_triggered_that_have_not_become_due() { // arrange var shipmentId = Any.AlphanumericString(8, 8); var order = CommandSchedulingTests_EventSourced.CreateOrder(); order.Apply(new ShipOn(shipDate: Clock.Now().AddMonths(2).Date) { ShipmentId = shipmentId }); await Save(order); // act await AdvanceClock(by : TimeSpan.FromDays(30)); //assert order = await Get <Order>(order.Id); var lastEvent = order.Events().Last(); lastEvent.Should().BeOfType <CommandScheduled <Order> >(); }
public override async Task A_command_handler_can_request_retry_of_a_failed_command_as_late_as_it_wants() { var order = CommandSchedulingTests_EventSourced.CreateOrder(); order.Apply( new ChargeCreditCardOn { Amount = 10, ChargeDate = Clock.Now().AddDays(1), ChargeRetryPeriod = TimeSpan.FromDays(7) }); await Save(order); // act // initial attempt fails await AdvanceClock(TimeSpan.FromDays(4)); order = await Get <Order>(order.Id); order.Events().Last().Should().BeOfType <CommandScheduled <Order> >(); // ship the order so the next retry will succeed order.Apply(new Ship()); await Save(order); // advance the clock a couple of days, which doesn't trigger a retry yet await AdvanceClock(TimeSpan.FromDays(2)); order = await Get <Order>(order.Id); order.Events().Should().NotContain(e => e is Order.CreditCardCharged); // advance the clock enough to trigger a retry await AdvanceClock(TimeSpan.FromDays(5)); order = await Get <Order>(order.Id); order.Events().Should().Contain(e => e is Order.CreditCardCharged); }
public async Task When_a_command_trigger_message_arrives_early_it_is_not_Completed() { VirtualClock.Start(Clock.Now().AddHours(-1)); var aggregateId = Any.Guid(); var appliedCommands = new List <ICommandSchedulerActivity>(); using (var receiver = CreateQueueReceiver()) { await receiver.StartReceivingMessages(); // due enough in the future that the scheduler won't apply the commands immediately var order = await CommandSchedulingTests_EventSourced.CreateOrder(orderId : aggregateId) .ApplyAsync(new ShipOn(Clock.Now().AddMinutes(2))); await Configuration.Current.Repository <Order>().Save(order); await receiver.Messages .OfType <IScheduledCommand <Order> >() .FirstAsync(c => c.TargetId == aggregateId.ToString()) .Timeout(TimeSpan.FromMinutes(1)); await Task.Delay(1000); appliedCommands.Should().BeEmpty(); } Clock.Reset(); using (var receiver = CreateQueueReceiver()) { await receiver.StartReceivingMessages(); await Task.Delay(1000); // FIX: (When_a_command_trigger_message_arrives_early_it_is_not_Completed) how was this even passing? // appliedCommands.Should().Contain(c => c.ScheduledCommand.AggregateId == aggregateId); } }
public async Task When_a_scheduled_command_fails_due_to_a_concurrency_exception_then_it_is_not_marked_as_applied() { // arrange var order = CommandSchedulingTests_EventSourced.CreateOrder(); var ship = new Ship(); order.Apply(ship); order.Apply(new ChargeCreditCardOn { Amount = 10, ChargeDate = Clock.Now().AddDays(10) }); await Save(order); TriggerConcurrencyExceptionOnOrderCommands(order.Id); // act await AdvanceClock(by : TimeSpan.FromDays(20)); // assert using (var db = CommandSchedulerDbContext()) { // make sure we actually triggered a concurrency exception db.Errors .Where(e => e.ScheduledCommand.AggregateId == order.Id) .ToArray() .Should() .Contain(e => e.Error.Contains("ConcurrencyException")); var scheduledCommand = db.ScheduledCommands.Single(c => c.AggregateId == order.Id); scheduledCommand.AppliedTime.Should().NotHaveValue(); scheduledCommand.Attempts.Should().Be(1); } }
public async Task When_Save_fails_then_a_scheduled_command_error_is_recorded() { // arrange var order = CommandSchedulingTests_EventSourced.CreateOrder(); var innerRepository = new SqlEventSourcedRepository <Order>(); var saveCount = 0; Configuration.Current .Container .Register <IEventSourcedRepository <Order> >(c => new FakeEventSourcedRepository <Order>(innerRepository) { OnSave = async o => { saveCount++; // throw on the second save attempt, which is when the clock is advanced delivering the scheduled command if (saveCount == 2) { throw new Exception("oops!"); } await innerRepository.Save(o); } }); // act order.Apply(new ShipOn(Clock.Now().AddDays(30))); await Save(order); await AdvanceClock(TimeSpan.FromDays(31)); //assert using (var db = CommandSchedulerDbContext()) { var error = db.Errors.Single(c => c.ScheduledCommand.AggregateId == order.Id).Error; error.Should().Contain("oops!"); } }