예제 #1
0
        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");
        }
예제 #2
0
        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());
        }
예제 #3
0
        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);
        }
예제 #4
0
        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);
            }
        }
예제 #5
0
        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");
        }
예제 #6
0
        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);
            }
        }
예제 #7
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>();
        }
예제 #8
0
        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.");
        }
예제 #9
0
        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.");
        }
예제 #10
0
        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));
            }
        }
예제 #11
0
        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> >();
        }
예제 #12
0
        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);
        }
예제 #13
0
        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);
            }
        }
예제 #14
0
        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);
            }
        }
예제 #15
0
        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!");
            }
        }