public async Task A_command_is_not_marked_as_applied_if_no_handler_is_registered() { // arrange var order = CommandSchedulingTests.CreateOrder(); order.Apply( new ChargeCreditCardOn { Amount = 10, ChargeDate = Clock.Now().AddDays(10) }); await orderRepository.Save(order); // act var schedulerWithNoHandlers = new SqlCommandScheduler( new Configuration().UseSqlEventStore()); await schedulerWithNoHandlers.AdvanceClock(clockName, @by : TimeSpan.FromDays(20)); // assert using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands.Single(c => c.AggregateId == order.Id) .AppliedTime .Should() .BeNull(); } }
public override async Task Specific_scheduled_commands_can_be_triggered_directly_by_target_id() { // arrange var target = new CommandTarget(Any.CamelCaseName()); await store.Put(target); // act await scheduler.Schedule(target.Id, new TestCommand(), Clock.Now().AddDays(2)); using (var db = new CommandSchedulerDbContext()) { var aggregateId = target.Id.ToGuidV3(); var command = db.ScheduledCommands .Single(c => c.AggregateId == aggregateId); await Configuration.Current .SchedulerClockTrigger() .Trigger(command, new SchedulerAdvancedResult(), db); } //assert target = await store.Get(target.Id); target.CommandsEnacted.Should().HaveCount(1); }
private void InitializeCommandScheduler() { using (var context = new CommandSchedulerDbContext(CommandSchedulerConnectionString)) { new EventStoreDatabaseInitializer <CommandSchedulerDbContext>().InitializeDatabase(context); } }
public override async Task When_an_immediately_scheduled_command_depends_on_a_precondition_that_has_not_been_met_yet_then_there_is_not_initially_an_attempt_recorded() { // arrange var targetId = Any.CamelCaseName(); var precondition = new EventHasBeenRecordedPrecondition( Guid.NewGuid().ToString().ToETag(), Guid.NewGuid()); // act await scheduler.Schedule(targetId, new CreateCommandTarget(targetId), deliveryDependsOn : precondition); // assert using (var db = new CommandSchedulerDbContext()) { var aggregateId = targetId.ToGuidV3(); var command = db.ScheduledCommands.Single(c => c.AggregateId == aggregateId); command.AppliedTime .Should() .NotHaveValue(); command.Attempts .Should() .Be(0); } }
/// <summary> /// Allows awaiting delivery of all commands that are currently due on the command scheduler. /// </summary> /// <param name="scheduler">The command scheduler.</param> /// <param name="clockName">The name of the clock on which the commands are scheduled.</param> /// <returns></returns> public static async Task Done( this ISchedulerClockTrigger scheduler, string clockName = null) { clockName = clockName ?? SqlCommandScheduler.DefaultClockName; for (var i = 0; i < 10; i++) { using (var db = new CommandSchedulerDbContext()) { var due = db.ScheduledCommands .Due() .Where(c => c.Clock.Name == clockName); if (!await due.AnyAsync()) { return; } var commands = await due.ToArrayAsync(); foreach (var scheduledCommand in commands) { await scheduler.Trigger( scheduledCommand, new SchedulerAdvancedResult(), db); } await Task.Delay(400); } } }
public async Task Command_scheduler_database_contains_a_default_clock() { using (var db = new CommandSchedulerDbContext(CommandSchedulerConnectionString)) { db.Clocks.Should().ContainSingle(c => c.Name == "default"); } }
public override async Task When_triggering_specific_commands_then_the_result_can_be_used_to_evaluate_failures() { // arrange var target = new CommandTarget(Any.CamelCaseName()); await store.Put(target); var schedulerAdvancedResult = new SchedulerAdvancedResult(); // act await scheduler.Schedule(target.Id, new TestCommand(isValid : false), Clock.Now().AddDays(2)); using (var db = new CommandSchedulerDbContext()) { var aggregateId = target.Id.ToGuidV3(); var command = db.ScheduledCommands .Single(c => c.AggregateId == aggregateId); await Configuration.Current .SchedulerClockTrigger() .Trigger(command, schedulerAdvancedResult, db); } //assert schedulerAdvancedResult .FailedCommands .Should() .HaveCount(1); }
/// <summary> /// Deserializes a scheduled command from SQL storage and delivers it to the target. /// </summary> /// <param name="configuration">The configuration.</param> /// <param name="serializedCommand">The serialized command.</param> /// <param name="db">The command scheduler database context.</param> public static async Task DeserializeAndDeliver( this Configuration configuration, ScheduledCommand serializedCommand, CommandSchedulerDbContext db) => await DeserializeAndDeliver( configuration.Container.Resolve<CommandSchedulerResolver>(), serializedCommand, db);
/// <summary> /// Deserializes a scheduled command from SQL storage and delivers it to the target. /// </summary> /// <param name="configuration">The configuration.</param> /// <param name="serializedCommand">The serialized command.</param> /// <param name="db">The command scheduler database context.</param> public static async Task DeserializeAndDeliver( this Configuration configuration, ScheduledCommand serializedCommand, CommandSchedulerDbContext db) => await DeserializeAndDeliver( configuration.Container.Resolve <CommandDelivererResolver>(), serializedCommand, db);
public EventStoreDbTest() { Logging.Configure(); SetConnectionStrings(); Command <Order> .AuthorizeDefault = (order, command) => true; Command <CustomerAccount> .AuthorizeDefault = (order, command) => true; lock (lockObj) { if (databasesInitialized) { return; } // TODO: (EventStoreDbTest) figure out a db cleanup story //#if !DEBUG // new EventStoreDbContext().Database.Delete(); // new OtherEventStoreDbContext().Database.Delete(); // new ReadModelDbContext().Database.Delete(); // new ReadModels1DbContext().Database.Delete(); // new ReadModels2DbContext().Database.Delete(); // new CommandSchedulerDbContext().Database.Delete(); //#endif using (var eventStore = new EventStoreDbContext()) { new EventStoreDatabaseInitializer <EventStoreDbContext>().InitializeDatabase(eventStore); } using (var db = new CommandSchedulerDbContext()) { new CommandSchedulerDatabaseInitializer().InitializeDatabase(db); } using (var eventStore = new OtherEventStoreDbContext()) { new EventStoreDatabaseInitializer <OtherEventStoreDbContext>().InitializeDatabase(eventStore); } using (var db = new ReadModelDbContext()) { new ReadModelDatabaseInitializer <ReadModelDbContext>().InitializeDatabase(db); } using (var db = new ReadModels1DbContext()) { new ReadModelDatabaseInitializer <ReadModels1DbContext>().InitializeDatabase(db); } using (var db = new ReadModels2DbContext()) { new ReadModelDatabaseInitializer <ReadModels2DbContext>().InitializeDatabase(db); } databasesInitialized = true; } }
public static async Task Done( this ISchedulerClockTrigger scheduler, string clockName = null) { clockName = clockName ?? Sql.CommandScheduler.Clock.DefaultClockName; using (var db = new CommandSchedulerDbContext()) { var now = Clock.Latest(Clock.Current, db.Clocks.Single(c => c.Name == clockName)).Now(); await scheduler.AdvanceClock(clockName, now); } }
internal static async Task DeserializeAndDeliver( CommandSchedulerResolver schedulerResolver, ScheduledCommand serializedCommand, CommandSchedulerDbContext db) { dynamic scheduler = schedulerResolver.ResolveSchedulerForAggregateTypeNamed(serializedCommand.AggregateType); await Storage.DeserializeAndDeliverScheduledCommand( serializedCommand, scheduler); serializedCommand.Attempts++; await db.SaveChangesAsync(); }
private static async Task DeserializeAndDeliver( CommandSchedulerResolver schedulerResolver, ScheduledCommand serializedCommand, CommandSchedulerDbContext db) { dynamic scheduler = schedulerResolver.ResolveSchedulerForAggregateTypeNamed(serializedCommand.AggregateType); await Storage.DeserializeAndDeliverScheduledCommand( serializedCommand, scheduler); serializedCommand.Attempts++; await db.SaveChangesAsync(); }
private MigrationResult Migrate(CommandSchedulerDbContext ctx) { var sql = @" delete from Scheduler.ScheduledCommand where AppliedTime is not null and AppliedTime < {0}"; var numberOfRecords = ctx.Database.ExecuteSqlCommand( sql, cutoffDate); return new MigrationResult { Log = $"Deleted {numberOfRecords} records\n{string.Format(sql, cutoffDate)}", MigrationWasApplied = true }; }
public override async Task When_a_command_is_scheduled_but_an_exception_is_thrown_in_a_handler_then_an_error_is_recorded() { // arrange var target = new CommandTarget(Any.CamelCaseName()); await store.Put(target); // act await scheduler.Schedule(target.Id, new TestCommand(isValid : false)); //assert using (var db = new CommandSchedulerDbContext()) { var aggregateId = target.Id.ToGuidV3(); var error = db.Errors.Single(e => e.ScheduledCommand.AggregateId == aggregateId); error.Error.Should().Contain("CommandValidationException"); } }
public static void ClassInitialize(TestContext context) { CommandSchedulerDbContext.NameOrConnectionString = @"Data Source=(localdb)\v11.0; Integrated Security=True; MultipleActiveResultSets=False; Initial Catalog=ItsCqrsTestsCommandScheduler"; Settings.Sources = new ISettingsSource[] { new ConfigDirectorySettings(@"c:\dev\.config") }.Concat(Settings.Sources); settings = Settings.Get <ServiceBusSettings>(); settings.ConfigureQueue = queue => queue.AutoDeleteOnIdle = TimeSpan.FromHours(24); #if !DEBUG new CommandSchedulerDbContext().Database.Delete(); #endif using (var readModels = new CommandSchedulerDbContext()) { new CommandSchedulerDatabaseInitializer().InitializeDatabase(readModels); } Console.WriteLine(Environment.MachineName); Console.WriteLine(new DirectoryInfo(@"c:\dev\.config").GetFiles().Select(f => f.FullName).ToLogString()); Settings.Sources = new ISettingsSource[] { new ConfigDirectorySettings(@"c:\dev\.config") }.Concat(Settings.Sources); }
public override async Task When_a_clock_is_advanced_and_a_command_fails_to_be_deserialized_then_other_commands_are_still_applied() { var commandScheduler = Configuration.Current.CommandScheduler <CommandTarget>(); var failedTargetId = Any.CamelCaseName(); var successfulTargetId = Any.CamelCaseName(); await commandScheduler.Schedule(failedTargetId, new CreateCommandTarget(failedTargetId), Clock.Now().AddHours(1)); await commandScheduler.Schedule(successfulTargetId, new CreateCommandTarget(successfulTargetId), Clock.Now().AddHours(1.5)); using (var db = new CommandSchedulerDbContext()) { var aggregateId = failedTargetId.ToGuidV3(); var command = db.ScheduledCommands.Single(c => c.AggregateId == aggregateId); var commandBody = command.SerializedCommand.FromJsonTo <dynamic>(); commandBody.Command.CommandName = "not a command name"; command.SerializedCommand = commandBody.ToString(); db.SaveChanges(); } // act Action advanceClock = () => Configuration.Current .SchedulerClockTrigger() .AdvanceClock(clockName: clockName, @by: TimeSpan.FromHours(2)).Wait(); // assert advanceClock.ShouldNotThrow(); var successfulAggregate = await store.Get(successfulTargetId); successfulAggregate.Should().NotBeNull(); }
public async Task When_a_scheduled_command_depends_on_an_event_that_never_arrives_it_is_eventually_abandoned() { VirtualClock.Start(); var container = Configuration.Current.Container; var commandScheduler = container.Resolve<ICommandScheduler<Order>>(); var orderId = Any.Guid(); await orderRepository.Save(new Order(new CreateOrder(Any.FullName()) { AggregateId = orderId })); var prerequisiteEvent = new CustomerAccount.Created { AggregateId = Any.Guid(), ETag = Any.Guid().ToString() }; await commandScheduler.Schedule(orderId, new AddItem { ProductName = Any.Paragraph(3), Price = 10m }, deliveryDependsOn: prerequisiteEvent); for (var i = 0; i < 7; i++) { VirtualClock.Current.AdvanceBy(TimeSpan.FromMinutes(90)); Console.WriteLine( (await clockTrigger.Trigger(commands => commands.Due().Where(c => c.AggregateId == orderId))).ToLogString()); } using (var db = new CommandSchedulerDbContext()) { var command = db.ScheduledCommands.Single(c => c.AggregateId == orderId); command.AppliedTime .Should() .BeNull(); command.Attempts .Should() .BeGreaterOrEqualTo(5); command.FinalAttemptTime .Should() .NotBeNull(); } }
public override async Task When_a_clock_is_set_on_a_command_then_it_takes_precedence_over_GetClockName() { // arrange var clockName = Any.CamelCaseName(); var targetId = Any.CamelCaseName(); var command = new CreateCommandTarget(targetId); var clock = new CommandScheduler.Clock { Name = clockName, UtcNow = DateTimeOffset.Parse("2016-03-01 02:00:00 AM") }; using (var commandScheduler = new CommandSchedulerDbContext()) { commandScheduler.Clocks.Add(clock); commandScheduler.SaveChanges(); } var scheduledCommand = new ScheduledCommand<CommandTarget>( targetId: targetId, command: command, dueTime: DateTimeOffset.Parse("2016-03-20 09:00:00 AM")) { Clock = clock }; // act await scheduler.Schedule(scheduledCommand); await Configuration.Current.SchedulerClockTrigger() .AdvanceClock(clockName, by: 30.Days()); //assert var target = await store.Get(targetId); target.Should().NotBeNull(); }
public async Task When_a_scheduler_assigned_negative_SequenceNumber_collides_then_it_retries_scheduling_with_a_new_SequenceNumber() { // arrange var container = Configuration.Current.Container; var commandScheduler = container.Resolve<ICommandScheduler<Order>>(); var initialSequenceNumber = -Any.PositiveInt(); var orderId = Any.Guid(); await commandScheduler.Schedule(new CommandScheduled<Order> { AggregateId = orderId, Command = new Cancel(), SequenceNumber = initialSequenceNumber, DueTime = Clock.Now().AddYears(1) }); var secondCommand = new CommandScheduled<Order> { AggregateId = orderId, Command = new Cancel(), SequenceNumber = initialSequenceNumber, DueTime = Clock.Now().AddYears(1) }; await commandScheduler.Schedule(secondCommand); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Count(c => c.AggregateId == orderId) .Should() .Be(2); } }
public async Task When_a_command_is_scheduled_but_the_event_that_triggered_it_was_not_successfully_written_then_the_command_is_not_applied() { VirtualClock.Start(); // create a customer account var customer = new CustomerAccount(Any.Guid()) .Apply(new ChangeEmailAddress(Any.Email())); await accountRepository.Save(customer); var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = customer.Id }); // act // cancel the order but don't save it order.Apply(new Cancel()); // assert // verify that the customer did not receive the scheduled NotifyOrderCanceled command customer = await accountRepository.GetLatest(customer.Id); customer.Events().Last().Should().BeOfType<CustomerAccount.EmailAddressChanged>(); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == customer.Id) .Should() .ContainSingle(c => c.AppliedTime == null && c.DueTime == null); } }
public async Task When_a_command_is_delivered_a_second_time_with_the_same_ETag_it_is_not_retried_afterward() { // arrange var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = Any.Guid() }); await orderRepository.Save(order); var command = new AddItem { ProductName = Any.Word(), Price = 10m, ETag = Any.Guid().ToString() }; var commandScheduler = Configuration.Current.Container.Resolve<ICommandScheduler<Order>>(); // act await commandScheduler.Schedule(order.Id, command, Clock.Now().AddDays(1)); Thread.Sleep(1); // the sequence number is set from the current tick count, which every now and then produces a duplicate here await commandScheduler.Schedule(order.Id, command, Clock.Now().AddDays(1)); await clockTrigger.Trigger(cmd => cmd.Where(c => c.AggregateId == order.Id)); // assert order = await orderRepository.GetLatest(order.Id); order.Balance.Should().Be(10); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == order.Id) .Should() .OnlyContain(c => c.AppliedTime != null); db.Errors .Where(c => c.ScheduledCommand.AggregateId == order.Id) .Should() .BeEmpty(); } }
public async Task When_a_scheduled_command_fails_then_the_error_is_recorded() { // arrange var order = CommandSchedulingTests.CreateOrder(); var innerRepository = new SqlEventSourcedRepository<Order>(); var saveCount = 0; Configuration.Current .Container .Register<IEventSourcedRepository<Order>>(c => new FakeEventSourcedRepository<Order>(innerRepository) { OnSave = async o => { if (saveCount > 0) { throw new Exception("oops!"); } await innerRepository.Save(o); saveCount++; } }); // act order.Apply(new ShipOn(Clock.Now().AddDays(30))); await Configuration.Current.Repository<Order>().Save(order); await clockTrigger.AdvanceClock(clockName, TimeSpan.FromDays(31)); //assert using (var db = new CommandSchedulerDbContext()) { var error = db.Errors.Single(c => c.ScheduledCommand.AggregateId == order.Id).Error; error.Should().Contain("oops!"); } }
protected int GetScheduledCommandNumberOfAttempts(Guid aggregateId) { using (var db = new CommandSchedulerDbContext()) { var scheduledCommand = db.ScheduledCommands.SingleOrDefault(c => c.AggregateId == aggregateId); return scheduledCommand.IfNotNull() .Then(c => c.Attempts) .ElseDefault(); } }
public override async Task When_a_clock_is_set_on_a_command_then_it_takes_precedence_over_GetClockName() { // arrange var clockName = Any.CamelCaseName(); var create = new CreateOrder(Any.FullName()) { AggregateId = Any.Guid() }; var clock = new CommandScheduler.Clock { Name = clockName, UtcNow = DateTimeOffset.Parse("2016-03-01 02:00:00 AM") }; using (var commandScheduler = new CommandSchedulerDbContext()) { commandScheduler.Clocks.Add(clock); commandScheduler.SaveChanges(); } var scheduledCommand = new ScheduledCommand<Order>( aggregateId: create.AggregateId, command: create, dueTime: DateTimeOffset.Parse("2016-03-20 09:00:00 AM")) { Clock = clock }; // act var configuration = Configuration.Current; await configuration.CommandScheduler<Order>().Schedule(scheduledCommand); await configuration.SchedulerClockTrigger() .AdvanceClock(clockName, by: 30.Days()); //assert var target = await configuration.Repository<Order>().GetLatest(create.AggregateId); target.Should().NotBeNull(); }
public override async Task When_triggering_specific_commands_then_the_result_can_be_used_to_evaluate_failures() { // arrange var target = new CommandTarget(Any.CamelCaseName()); await store.Put(target); var schedulerAdvancedResult = new SchedulerAdvancedResult(); // act await scheduler.Schedule(target.Id, new TestCommand(isValid: false), Clock.Now().AddDays(2)); using (var db = new CommandSchedulerDbContext()) { var aggregateId = target.Id.ToGuidV3(); var command = db.ScheduledCommands .Single(c => c.AggregateId == aggregateId); await Configuration.Current .SchedulerClockTrigger() .Trigger(command, schedulerAdvancedResult, db); } //assert schedulerAdvancedResult .FailedCommands .Should() .HaveCount(1); }
public async Task Trigger( ScheduledCommand scheduled, SchedulerAdvancedResult result, CommandSchedulerDbContext db) => await deliver(scheduled, result, db);
public override async Task When_a_command_is_scheduled_but_an_exception_is_thrown_in_a_handler_then_an_error_is_recorded() { // arrange var target = new CommandTarget(Any.CamelCaseName()); await store.Put(target); // act await scheduler.Schedule(target.Id, new TestCommand(isValid: false)); //assert using (var db = new CommandSchedulerDbContext()) { var aggregateId = target.Id.ToGuidV3(); var error = db.Errors.Single(e => e.ScheduledCommand.AggregateId == aggregateId); error.Error.Should().Contain("CommandValidationException"); } }
public override async Task When_an_immediately_scheduled_command_depends_on_a_precondition_that_has_not_been_met_yet_then_there_is_not_initially_an_attempt_recorded() { // arrange var targetId = Any.CamelCaseName(); var precondition = new EventHasBeenRecordedPrecondition( Guid.NewGuid().ToString().ToETag(), Guid.NewGuid()); // act await scheduler.Schedule(targetId, new CreateCommandTarget(targetId), deliveryDependsOn: precondition); // assert using (var db = new CommandSchedulerDbContext()) { var aggregateId = targetId.ToGuidV3(); var command = db.ScheduledCommands.Single(c => c.AggregateId == aggregateId); command.AppliedTime .Should() .NotHaveValue(); command.Attempts .Should() .Be(0); } }
public async Task UseSqlCommandScheduling_can_include_using_a_catchup_for_durability() { var configuration = new Configuration() .UseSqlEventStore() .UseSqlCommandScheduling(catchup => catchup.StartAtEventId = eventStoreDbTest.HighestEventId); configuration.SqlCommandScheduler().GetClockName = e => clockName; disposables.Add(configuration); var aggregateIds = Enumerable.Range(1, 5).Select(_ => Any.Guid()).ToArray(); Events.Write(5, i => new CommandScheduled<Order> { Command = new CreateOrder(Any.FullName()), AggregateId = aggregateIds[i - 1], SequenceNumber = -DateTimeOffset.UtcNow.Ticks }); await configuration.Container .Resolve<ReadModelCatchup<CommandSchedulerDbContext>>() .SingleBatchAsync(); using (var db = new CommandSchedulerDbContext()) { var scheduledAggregateIds = db.ScheduledCommands .Where(c => aggregateIds.Any(id => id == c.AggregateId)) .Select(c => c.AggregateId) .ToArray(); scheduledAggregateIds.Should() .BeEquivalentTo(aggregateIds); } }
public async Task When_a_command_is_non_durable_then_immediate_scheduling_does_not_result_in_a_command_scheduler_db_entry() { var commandScheduler = Configuration.Current.Container.Resolve<ICommandScheduler<CustomerAccount>>(); var reserverationService = new Mock<IReservationService>(); reserverationService.Setup(m => m.Reserve(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(), It.IsAny<TimeSpan?>())) .Returns(() => Task.FromResult(true)); Configuration.Current.ReservationService = reserverationService.Object; var aggregateId = Any.Guid(); await accountRepository.Save(new CustomerAccount(aggregateId).Apply(new RequestNoSpam())); await commandScheduler.Schedule(aggregateId, new RequestUserName { UserName = Any.Email() }); using (var db = new CommandSchedulerDbContext()) { var scheduledAggregateIds = db.ScheduledCommands .Where(c => aggregateId == c.AggregateId) .ToArray(); scheduledAggregateIds.Should() .BeEmpty(); } }
public override async Task When_a_clock_is_advanced_and_a_command_fails_to_be_deserialized_then_other_commands_are_still_applied() { var commandScheduler = Configuration.Current.CommandScheduler<Order>(); var failedAggregateId = Any.Guid(); var successfulAggregateId = Any.Guid(); await commandScheduler.Schedule(failedAggregateId, new CreateOrder(Any.FullName()), Clock.Now().AddHours(1)); await commandScheduler.Schedule(successfulAggregateId, new CreateOrder(Any.FullName()) { AggregateId = successfulAggregateId }, Clock.Now().AddHours(1.5)); using (var db = new CommandSchedulerDbContext()) { var command = db.ScheduledCommands.Single(c => c.AggregateId == failedAggregateId); var commandBody = command.SerializedCommand.FromJsonTo<dynamic>(); commandBody.Command.CustomerId = "not a guid"; command.SerializedCommand = commandBody.ToString(); db.SaveChanges(); } // act Action advanceClock = () => clockTrigger.AdvanceClock(clockName: clockName, @by: TimeSpan.FromHours(2)).Wait(); // assert advanceClock.ShouldNotThrow(); var repository = Configuration.Current.Repository<Order>(); var successfulAggregate = await repository.GetLatest(successfulAggregateId); successfulAggregate.Should().NotBeNull(); }
public async Task When_a_scheduled_command_fails_due_to_a_concurrency_exception_then_it_is_not_marked_as_applied() { // arrange var order = CommandSchedulingTests.CreateOrder(); var ship = new Ship(); order.Apply(ship); order.Apply(new ChargeCreditCardOn { Amount = 10, ChargeDate = Clock.Now().AddDays(10) }); await orderRepository.Save(order); TriggerConcurrencyExceptionOnOrderCommands(order.Id); // act await clockTrigger.AdvanceClock(clockName, @by: TimeSpan.FromDays(20)); // assert using (var db = new CommandSchedulerDbContext()) { // make sure we actually triggered a concurrency exception db.Errors .Where(e => e.ScheduledCommand.AggregateId == order.Id) .Should() .Contain(e => e.Error.Contains("ConcurrencyException")); var scheduledCommand = db.ScheduledCommands.Single(c => c.AggregateId == order.Id); scheduledCommand.AppliedTime.Should().BeNull(); scheduledCommand.Attempts.Should().Be(1); } }
public async Task Trigger(ScheduledCommand scheduled, SchedulerAdvancedResult result, CommandSchedulerDbContext db) { await ClockTrigger.Trigger(scheduled, result, db); }
public async Task Specific_scheduled_commands_can_be_triggered_directly_by_aggregate_id() { // arrange var shipmentId = Any.AlphanumericString(8, 8); var order = CommandSchedulingTests.CreateOrder(); order.Apply(new ShipOn(shipDate: Clock.Now().AddMonths(1).Date) { ShipmentId = shipmentId }); await orderRepository.Save(order); // act await clockTrigger.Trigger(commands => commands.Where(cmd => cmd.AggregateId == order.Id)); //assert order = await orderRepository.GetLatest(order.Id); var lastEvent = order.Events().Last(); lastEvent.Should().BeOfType<Order.Shipped>(); order.ShipmentId .Should() .Be(shipmentId); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == order.Id) .Should() .OnlyContain(c => c.AppliedTime != null); } }
public async Task When_a_non_scheduler_assigned_negative_SequenceNumber_collides_then_it_is_ignores() { // arrange var container = Configuration.Current.Container; var commandScheduler = container.Resolve<ICommandScheduler<Order>>(); var db = new CommandSchedulerDbContext(); disposables.Add(db); var initialSequenceNumber = Any.PositiveInt(); var orderId = Any.Guid(); await commandScheduler.Schedule(new CommandScheduled<Order> { AggregateId = orderId, Command = new Cancel(), SequenceNumber = initialSequenceNumber, DueTime = Clock.Now().AddYears(1) }); var secondCommand = new CommandScheduled<Order> { AggregateId = orderId, Command = new Cancel(), SequenceNumber = initialSequenceNumber, DueTime = Clock.Now().AddYears(1) }; Action again = () => commandScheduler.Schedule(secondCommand).Wait(); again.ShouldNotThrow<DbUpdateException>(); }
public async Task When_a_command_is_scheduled_but_an_exception_is_thrown_in_a_handler_then_an_error_is_recorded() { Configuration.Current.UseDependency(_ => new CustomerAccount.OrderEmailConfirmer { SendOrderConfirmationEmail = x => { throw new Exception("drat!"); } }); // create a customer account var customer = new CustomerAccount(Any.Guid()) .Apply(new ChangeEmailAddress(Any.Email())); await accountRepository.Save(customer); var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = customer.Id }); await orderRepository.Save(order); // act order.Apply(new Cancel()); await orderRepository.Save(order); await clockTrigger.AdvanceClock(clockName, TimeSpan.FromMinutes(1.2)); using (var db = new CommandSchedulerDbContext()) { db.Errors .Where(e => e.ScheduledCommand.AggregateId == customer.Id) .Should() .Contain(e => e.Error.Contains("drat!")); } }
public async Task When_a_constructor_commands_fails_with_a_ConcurrencyException_it_is_not_retried() { var commandScheduler = Configuration.Current.Container.Resolve<ICommandScheduler<Order>>(); var orderId = Any.Guid(); var customerName = Any.FullName(); await commandScheduler.Schedule(orderId, new CreateOrder(customerName) { AggregateId = orderId, ETag = Any.Guid().ToString() }); await commandScheduler.Schedule(orderId, new CreateOrder(customerName) { AggregateId = orderId }); using (var db = new CommandSchedulerDbContext()) { var commands = db.ScheduledCommands .Where(c => c.AggregateId == orderId) .OrderBy(c => c.CreatedTime) .ToArray(); commands.Count().Should().Be(2); commands.Last().FinalAttemptTime.Should().NotBeNull(); } }
public async Task When_a_command_is_scheduled_but_the_aggregate_it_applies_to_is_not_found_then_the_command_is_retried() { // create and cancel an order for a nonexistent customer account var customerId = Any.Guid(); Console.WriteLine(new { customerId }); var order = new Order(new CreateOrder(Any.FullName()) { CustomerId = customerId }); order.Apply(new Cancel()); await orderRepository.Save(order); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == customerId) .Should() .ContainSingle(c => c.AppliedTime == null); } // act // now save the customer and advance the clock await accountRepository.Save(new CustomerAccount(customerId).Apply(new ChangeEmailAddress(Any.Email()))); await clockTrigger.AdvanceClock(clockName, TimeSpan.FromMinutes(2)); using (var db = new CommandSchedulerDbContext()) { db.ScheduledCommands .Where(c => c.AggregateId == customerId) .Should() .ContainSingle(c => c.AppliedTime != null); var customer = await accountRepository.GetLatest(customerId); customer.Events() .Last() .Should().BeOfType<CustomerAccount.OrderCancelationConfirmationEmailSent>(); } }
public async Task When_an_immediately_scheduled_command_depends_on_an_event_that_has_not_been_saved_yet_then_there_is_not_initially_a_concurrency_exception() { var container = Configuration.Current.Container; var commandScheduler = container.Resolve<ICommandScheduler<Order>>(); var orderId = Any.Guid(); var customerName = Any.FullName(); var prerequisiteEvent = new CustomerAccount.Created { AggregateId = Any.Guid(), ETag = Any.Guid().ToString() }; var scheduledCommand = await commandScheduler.Schedule(orderId, new CreateOrder(customerName) { AggregateId = orderId }, deliveryDependsOn: prerequisiteEvent); var order = await orderRepository.GetLatest(orderId); order.Should().BeNull(); scheduledCommand.Result.Should().BeOfType<CommandScheduled>(); using (var db = new CommandSchedulerDbContext()) { var command = db.ScheduledCommands.Single(c => c.AggregateId == orderId); command.AppliedTime .Should() .BeNull(); command.Attempts .Should() .Be(0); } }
public override async Task When_a_constructor_command_fails_with_a_ConcurrencyException_it_is_not_retried() { var commandScheduler = Configuration.Current.CommandScheduler<Order>(); var orderId = Any.Guid(); var customerName = Any.FullName(); await commandScheduler.Schedule(orderId, new CreateOrder(customerName) { AggregateId = orderId }); await commandScheduler.Schedule(orderId, new CreateOrder(customerName) { AggregateId = orderId }); using (var db = new CommandSchedulerDbContext()) { var commands = db.ScheduledCommands .Where(c => c.AggregateId == orderId) .OrderBy(c => c.CreatedTime) .ToArray(); Console.WriteLine(commands.ToDiagnosticJson()); commands.Length.Should().Be(2); commands .Should() .ContainSingle(c => c.FinalAttemptTime == null); } }
public override async Task When_a_clock_is_advanced_and_a_command_fails_to_be_deserialized_then_other_commands_are_still_applied() { var commandScheduler = Configuration.Current.CommandScheduler<CommandTarget>(); var failedTargetId = Any.CamelCaseName(); var successfulTargetId = Any.CamelCaseName(); await commandScheduler.Schedule(failedTargetId, new CreateCommandTarget(failedTargetId), Clock.Now().AddHours(1)); await commandScheduler.Schedule(successfulTargetId, new CreateCommandTarget(successfulTargetId), Clock.Now().AddHours(1.5)); using (var db = new CommandSchedulerDbContext()) { var aggregateId = failedTargetId.ToGuidV3(); var command = db.ScheduledCommands.Single(c => c.AggregateId == aggregateId); var commandBody = command.SerializedCommand.FromJsonTo<dynamic>(); commandBody.Command.CommandName = "not a command name"; command.SerializedCommand = commandBody.ToString(); db.SaveChanges(); } // act Action advanceClock = () => Configuration.Current .SchedulerClockTrigger() .AdvanceClock(clockName: clockName, @by: TimeSpan.FromHours(2)).Wait(); // assert advanceClock.ShouldNotThrow(); var successfulAggregate = await store.Get(successfulTargetId); successfulAggregate.Should().NotBeNull(); }
public async Task Recursive_scheduling_is_supported_when_the_virtual_clock_is_advanced() { // arrange using (VirtualClock.Start(DateTimeOffset.Parse("2014-10-08 06:52:10 AM -07:00"))) { var scenario = CreateScenarioBuilder() .AddEvents(new CustomerAccount.EmailAddressChanged { NewEmailAddress = Any.Email() }) .Prepare(); // act var account = (await scenario.GetLatestAsync <CustomerAccount>()) .Apply(new RequestSpam()) .Apply(new SendMarketingEmail()); await scenario.SaveAsync(account); VirtualClock.Current.AdvanceBy(TimeSpan.FromDays((7 * 4) + 2)); await scenario.CommandSchedulerDone(); account = await scenario.GetLatestAsync <CustomerAccount>(); // assert account.Events() .OfType <CommandScheduled <CustomerAccount> >() .Select(e => e.Timestamp.Date) .Should() .BeEquivalentTo(new[] { DateTime.Parse("2014-10-08"), DateTime.Parse("2014-10-15"), DateTime.Parse("2014-10-22"), DateTime.Parse("2014-10-29"), DateTime.Parse("2014-11-05") }); account.Events() .OfType <CustomerAccount.MarketingEmailSent>() .Select(e => e.Timestamp.Date) .Should() .BeEquivalentTo(new[] { DateTime.Parse("2014-10-08"), DateTime.Parse("2014-10-15"), DateTime.Parse("2014-10-22"), DateTime.Parse("2014-10-29"), DateTime.Parse("2014-11-05") }); account.Events() .Last() .Should() .BeOfType <CommandScheduled <CustomerAccount> >(); if (this is ScenarioBuilderWithSqlEventStoreTests) { using (var db = new CommandSchedulerDbContext()) { var scheduledCommands = db.ScheduledCommands .Where(c => c.AggregateId == account.Id) .ToArray(); // all but the last command (which didn't come due yet) should have been marked as applied scheduledCommands .Reverse() .Skip(1) .Should() .OnlyContain(c => c.AppliedTime != null); } } } }
public async Task Immediately_scheduled_commands_triggered_by_a_scheduled_command_have_their_due_time_set_to_the_causative_command_clock() { VirtualClock.Start(); var aggregate = new CommandSchedulerTestAggregate(); var repository = Configuration.Current .Repository<CommandSchedulerTestAggregate>(); await repository.Save(aggregate); var scheduler = Configuration.Current.CommandScheduler<CommandSchedulerTestAggregate>(); var dueTime = Clock.Now().AddMinutes(5); await scheduler.Schedule( aggregate.Id, dueTime: dueTime, command: new CommandSchedulerTestAggregate.CommandThatSchedulesAnotherCommandImmediately { NextCommandAggregateId = aggregate.Id, NextCommand = new CommandSchedulerTestAggregate.Command { CommandId = Any.CamelCaseName() } }); VirtualClock.Current.AdvanceBy(TimeSpan.FromDays(1)); using (var db = new CommandSchedulerDbContext()) { foreach (var command in db.ScheduledCommands.Where(c => c.AggregateId == aggregate.Id)) { command.AppliedTime .IfNotNull() .ThenDo(v => v.Should().BeCloseTo(dueTime, 10)); } } }