private async Task <string> ClockNameForEvent( IScheduledCommand <TAggregate> scheduledCommandEvent, CommandSchedulerDbContext db) { // TODO: (ClockNameForEvent) clean this up var clockName = scheduledCommandEvent.IfTypeIs <IHaveExtensibleMetada>() .Then(e => ((object)e.Metadata) .IfTypeIs <IDictionary <string, object> >() .Then(m => m.IfContains("ClockName") .Then(v => v.ToString()))) .ElseDefault(); if (clockName == null) { clockName = GetClockName(scheduledCommandEvent); if (clockName == null) { var lookupValue = GetClockLookupKey(scheduledCommandEvent); clockName = (await db.ClockMappings .Include(m => m.Clock) .SingleOrDefaultAsync(c => c.Value == lookupValue)) .IfNotNull() .Then(c => c.Clock.Name) .Else(() => SqlCommandScheduler.DefaultClockName); } } return(clockName); }
public async Task Schedule(IScheduledCommand <TAggregate> scheduledCommand) { var storedScheduledCommand = await Storage.StoreScheduledCommand( scheduledCommand, createCommandSchedulerDbContext, (scheduledCommandEvent1, db) => ClockNameForEvent(this, scheduledCommandEvent1, db)); Activity.OnNext(new CommandScheduled(scheduledCommand) { ClockName = storedScheduledCommand.Clock.Name }); // deliver the command immediately if appropriate if (scheduledCommand.IsDue(storedScheduledCommand.Clock)) { // sometimes the command depends on a precondition event that hasn't been saved if (!await commandPreconditionVerifier.IsPreconditionSatisfied(scheduledCommand)) { this.DeliverIfPreconditionIsSatisfiedWithin( TimeSpan.FromSeconds(10), scheduledCommand, eventBus); } else { var scheduler = Configuration.Current.CommandScheduler <TAggregate>(); await scheduler.Deliver(scheduledCommand); } } }
private async Task Deliver <TAggregate>( IScheduledCommand <TAggregate> cmd, Func <IScheduledCommand <TAggregate>, Task> next) where TAggregate : class { IClock clock; if (cmd.DueTime != null) { clock = Domain.Clock.Create(() => cmd.DueTime.Value); } else { clock = cmd.Clock; } using (CommandContext.Establish(cmd.Command, clock)) { await next(cmd); if (!cmd.Command.RequiresDurableScheduling()) { return; } await Storage.UpdateScheduledCommand( cmd, createDbContext); } }
private async Task Deliver(IScheduledCommand <TAggregate> scheduledCommand, bool durable) { IClock clock = null; if (scheduledCommand.DueTime != null) { clock = Domain.Clock.Create(() => scheduledCommand.DueTime.Value); } using (CommandContext.Establish(scheduledCommand.Command, clock)) { var repository = getRepository(); await repository.ApplyScheduledCommand(scheduledCommand, commandPreconditionVerifier); Activity.OnNext(scheduledCommand.Result); if (!durable) { return; } await Storage.UpdateScheduledCommand( scheduledCommand, createCommandSchedulerDbContext); } }
/// <summary> /// Schedules the specified command. /// </summary> /// <param name="scheduledCommand">The scheduled command.</param> /// <returns> /// A task that is complete when the command has been successfully scheduled. /// </returns> /// <exception cref="System.NotSupportedException">Non-immediate scheduling is not supported.</exception> public virtual async Task Schedule(IScheduledCommand <TAggregate> scheduledCommand) { if (scheduledCommand.Result is CommandDeduplicated) { return; } if (scheduledCommand.Command.CanBeDeliveredDuringScheduling() && scheduledCommand.IsDue()) { if (!await PreconditionHasBeenMet(scheduledCommand)) { CommandScheduler.DeliverIfPreconditionIsMetSoon( scheduledCommand, Configuration.Current); } else { // resolve the command scheduler so that delivery goes through the whole pipeline await Configuration.Current.CommandScheduler <TAggregate>().Deliver(scheduledCommand); return; } } if (scheduledCommand.Result == null) { throw new NotSupportedException("Deferred scheduling is not supported by the current command scheduler pipeline configuration."); } }
public async Task Deliver(IScheduledCommand <TAggregate> scheduledCommand) { using (CommandContext.Establish(scheduledCommand.Command)) { await repository.ApplyScheduledCommand(scheduledCommand); } }
/// <summary> /// Initializes a new instance of the <see cref="CommandFailed{TCommand}"/> class. /// </summary> /// <param name="command">The command.</param> /// <param name="scheduledCommand">The scheduled command.</param> /// <param name="exception">The exception.</param> internal CommandFailed( TCommand command, IScheduledCommand scheduledCommand, Exception exception) : base(scheduledCommand, exception) { Command = command; }
internal static void DeliverIfPreconditionIsMetSoon <TAggregate>( IScheduledCommand <TAggregate> scheduledCommand, Configuration configuration, int timeoutInMilliseconds = 10000) where TAggregate : class { Guid aggregateId; if (Guid.TryParse(scheduledCommand.DeliveryPrecondition.Scope, out aggregateId)) { var eventBus = configuration.EventBus; var timeout = TimeSpan.FromMilliseconds(timeoutInMilliseconds); eventBus.Events <IEvent>() .Where( e => e.AggregateId == aggregateId && e.ETag == scheduledCommand.DeliveryPrecondition.ETag) .Take(1) .Timeout(timeout) .Subscribe( e => Task.Run(() => DeliverImmediatelyOnConfiguredScheduler(scheduledCommand, configuration)).Wait(), onError: ex => eventBus.PublishErrorAsync(new EventHandlingError(ex))); } }
public async Task Schedule(IScheduledCommand <TAggregate> scheduledCommandEvent) { Debug.WriteLine("SqlCommandScheduler.Schedule: " + Description(scheduledCommandEvent)); var storedScheduledCommand = await StoreScheduledCommand(scheduledCommandEvent); var scheduledCommand = storedScheduledCommand.ToScheduledCommand <TAggregate>(); Activity.OnNext(new CommandScheduled(scheduledCommand) { ClockName = storedScheduledCommand.Clock.Name }); // deliver the command immediately if appropriate if (storedScheduledCommand.ShouldBeDeliveredImmediately()) { // sometimes the command depends on a precondition event that hasn't been saved if (!await commandPreconditionVerifier.VerifyPrecondition(scheduledCommand)) { this.DeliverIfPreconditionIsSatisfiedWithin( TimeSpan.FromSeconds(10), scheduledCommand, eventBus); } else { await Deliver(scheduledCommand, durable : !storedScheduledCommand.NonDurable); } } }
/// <summary> /// Schedules the specified command. /// </summary> /// <param name="scheduledCommand">The scheduled command.</param> /// <returns>A task that is complete when the command has been successfully scheduled.</returns> public async Task Schedule(IScheduledCommand <TAggregate> scheduledCommand) { var dueTime = scheduledCommand.DueTime; var domainNow = Clock.Current.Now(); if (dueTime == null || dueTime <= domainNow) { Debug.WriteLine(string.Format("Schedule (applying {1} immediately): @ {0}", domainNow, scheduledCommand.Command.CommandName)); // schedule immediately await Deliver(scheduledCommand); return; } // schedule for later VirtualClock.Schedule(scheduledCommand, dueTime.Value, (s, command) => { try { Deliver(command).Wait(); } catch (Exception exception) { Console.WriteLine("InMemoryCommandScheduler caught:\n" + exception); } return(Disposable.Empty); }); }
private static Clock TryToGetSqlClockToScheduleOn <TAggregate>( IScheduledCommand <TAggregate> scheduledCommand) where TAggregate : class => scheduledCommand.Clock as Clock ?? scheduledCommand.Clock .IfTypeIs <AnonymousClock>() .Then(c => c.ParentClock as Clock) .ElseDefault();
private static async Task SaveScheduledCommandToDatabase <TAggregate>( CommandSchedulerDbContext db, ScheduledCommand storedScheduledCommand, IScheduledCommand <TAggregate> scheduledCommand) { var etag = new ETag { Scope = scheduledCommand.TargetId, ETagValue = scheduledCommand.Command.ETag, CreatedDomainTime = Domain.Clock.Now(), CreatedRealTime = DateTimeOffset.UtcNow }; while (true) { try { await InsertScheduledCommandAndETag(db, storedScheduledCommand, etag); break; } catch (Exception exception) { if (!exception.IsConcurrencyException()) { throw; } if (exception.ToString().Contains(@"object 'Scheduler.ScheduledCommand'")) { if (storedScheduledCommand.SequenceNumber < 0) { // for scheduler-assigned sequence numbers, decrement and retry storedScheduledCommand.SequenceNumber--; } else { // this is not a scheduler-assigned sequence number break; } } else if (exception.ToString().Contains(@"object 'Scheduler.ETag'")) { scheduledCommand.Result = new CommandDeduplicated( scheduledCommand, "Schedule"); return; } else { throw; } } } scheduledCommand.Result = new CommandScheduled( scheduledCommand, storedScheduledCommand.Clock); }
private static async Task FailScheduledCommand <TAggregate>( IEventSourcedRepository <TAggregate> repository, IScheduledCommand <TAggregate> scheduled, Exception exception = null, TAggregate aggregate = null) where TAggregate : class, IEventSourced { var failure = (CommandFailed)createMethod .MakeGenericMethod(scheduled.Command.GetType()) .Invoke(null, new object[] { scheduled.Command, scheduled, exception }); var previousAttempts = scheduled.IfHas <int>(s => s.Metadata.NumberOfPreviousAttempts) .ElseDefault(); failure.NumberOfPreviousAttempts = previousAttempts; if (aggregate != null) { // TODO: (FailScheduledCommand) refactor so that getting hold of the handler is simpler var scheduledCommandOfT = scheduled.Command as Command <TAggregate>; if (scheduledCommandOfT != null) { if (scheduledCommandOfT.Handler != null) { await scheduledCommandOfT.Handler .HandleScheduledCommandException((dynamic)aggregate, (dynamic)failure); } } if (!(exception is ConcurrencyException)) { try { await repository.Save(aggregate); } catch (Exception ex) { // TODO: (FailScheduledCommand) surface this more clearly Trace.Write(ex); } } else if (scheduled.Command is ConstructorCommand <TAggregate> ) { failure.Cancel(); scheduled.Result = failure; return; } } if (!failure.IsCanceled && failure.RetryAfter == null && failure.NumberOfPreviousAttempts < DefaultNumberOfRetriesOnException) { failure.Retry(TimeSpan.FromMinutes(Math.Pow(failure.NumberOfPreviousAttempts + 1, 2))); } scheduled.Result = failure; }
public CommandScheduled(IScheduledCommand scheduledCommand) { if (scheduledCommand == null) { throw new ArgumentNullException("scheduledCommand"); } this.scheduledCommand = scheduledCommand; }
protected ScheduledCommandResult(IScheduledCommand command) { if (command == null) { throw new ArgumentNullException(nameof(command)); } this.command = command; }
internal static async Task DeliverImmediatelyOnConfiguredScheduler <TAggregate>( IScheduledCommand <TAggregate> command, Configuration configuration) where TAggregate : class, IEventSourced { var scheduler = configuration.CommandScheduler <TAggregate>(); await scheduler.Deliver(command); }
internal static CommandFailed Create <TCommand>( TCommand command, IScheduledCommand scheduledCommand, Exception exception) where TCommand : class, ICommand { return(new CommandFailed <TCommand>(command, scheduledCommand, exception)); }
/// <summary> /// Initializes a new instance of the <see cref="ScheduledCommandResult"/> class. /// </summary> /// <param name="command">The command.</param> /// <exception cref="System.ArgumentNullException"></exception> protected ScheduledCommandResult(IScheduledCommand command) { if (command == null) { throw new ArgumentNullException(nameof(command)); } this.command = command; }
public void Add(IScheduledCommand command) { var now = Clock.Now(); commands.AddOrUpdate( command, now, (c, t) => now); }
public async Task <bool> IsPreconditionSatisfied(IScheduledCommand scheduledCommand) { if (scheduledCommand.DeliveryPrecondition == null) { return(true); } throw new InvalidOperationException("No ICommandPreconditionVerifier has been configured."); }
public async Task<bool> IsPreconditionSatisfied(IScheduledCommand scheduledCommand) { if (scheduledCommand.DeliveryPrecondition == null) { return true; } throw new InvalidOperationException("No ICommandPreconditionVerifier has been configured."); }
/// <summary> /// Delivers the specified scheduled command to the target. /// </summary> /// <param name="scheduledCommand">The scheduled command to be applied to the target.</param> /// <returns> /// A task that is complete when the command has been applied. /// </returns> /// <remarks> /// The scheduler will apply the command and save it, potentially triggering additional consequences. /// </remarks> public virtual async Task Deliver(IScheduledCommand <TAggregate> scheduledCommand) { if (scheduledCommand.Result is CommandDelivered) { return; } await store.ApplyScheduledCommand(scheduledCommand, etagChecker); }
internal static async Task UpdateScheduledCommand <TAggregate>( IScheduledCommand <TAggregate> scheduledCommand, Func <CommandSchedulerDbContext> createDbContext) where TAggregate : class, IEventSourced { using (var db = createDbContext()) { var storedCommand = await db.ScheduledCommands .SingleOrDefaultAsync(c => c.AggregateId == scheduledCommand.AggregateId && c.SequenceNumber == scheduledCommand.SequenceNumber); if (storedCommand == null) { if (!scheduledCommand.Command.RequiresDurableScheduling) { return; } throw new InvalidOperationException("Scheduled command not found"); } storedCommand.Attempts++; var result = scheduledCommand.Result; if (result is CommandSucceeded) { storedCommand.AppliedTime = Domain.Clock.Now(); } else { var failure = (CommandFailed)result; // reschedule as appropriate var now = Domain.Clock.Now(); if (failure.IsCanceled || failure.RetryAfter == null) { // no further retries storedCommand.FinalAttemptTime = now; } else { storedCommand.DueTime = now + failure.RetryAfter; } db.Errors.Add(new CommandExecutionError { ScheduledCommand = storedCommand, Error = result.IfTypeIs <CommandFailed>() .Then(f => f.Exception.ToJson()) .ElseDefault() }); } await db.SaveChangesAsync(); } }
private static ScheduledCommandResult FailScheduledCommand <TAggregate>( IEventSourcedRepository <TAggregate> repository, IScheduledCommand <TAggregate> scheduled, Exception exception = null, TAggregate aggregate = null) where TAggregate : class, IEventSourced { var failure = (CommandFailed)createMethod .MakeGenericMethod(scheduled.Command.GetType()) .Invoke(null, new object[] { scheduled.Command, scheduled, exception }); var previousAttempts = scheduled.IfHas <int>(s => s.Metadata.NumberOfPreviousAttempts) .ElseDefault(); failure.NumberOfPreviousAttempts = previousAttempts; if (aggregate != null) { // TODO: (FailScheduledCommand) refactor so that getting hold of the handler is simpler scheduled.Command .IfTypeIs <Command <TAggregate> >() .ThenDo(c => { if (c.Handler != null) { Task task = c.Handler .HandleScheduledCommandException((dynamic)aggregate, (dynamic)failure); task.Wait(); } }); if (!(exception is ConcurrencyException)) { try { repository.Save(aggregate); } catch (Exception ex) { // TODO: (FailScheduledCommand) surface this more clearly Trace.Write(ex); } } } else { if (failure.NumberOfPreviousAttempts < 5) { failure.Retry(TimeSpan.FromMinutes(failure.NumberOfPreviousAttempts + 1)); } } return(failure); }
/// <summary> /// Delivers the specified scheduled command to the target. /// </summary> /// <param name="scheduledCommand">The scheduled command to be applied to the target.</param> /// <returns> /// A task that is complete when the command has been applied. /// </returns> /// <remarks> /// The scheduler will apply the command and save it, potentially triggering additional consequences. /// </remarks> public async Task Deliver(IScheduledCommand <TAggregate> scheduledCommand) { if (scheduledCommand.Result is CommandDelivered) { return; } var store = getStore(); await ApplyScheduledCommand(store, scheduledCommand); }
private async Task Schedule <TAggregate>( IScheduledCommand <TAggregate> cmd, Func <IScheduledCommand <TAggregate>, Task> next) where TAggregate : class { await Storage.StoreScheduledCommand( cmd, createDbContext, GetClockName); await next(cmd); }
internal static IDisposable Schedule <TAggregate>( IScheduledCommand <TAggregate> scheduledCommand, DateTimeOffset dueTime, Func <IScheduler, IScheduledCommand <TAggregate>, IDisposable> func) { var scheduler = Clock.Current .IfTypeIs <VirtualClock>() .Then(c => (IScheduler)c.Scheduler) .Else(() => CurrentThreadScheduler.Instance); return(scheduler.Schedule(scheduledCommand, dueTime, func)); }
internal static IDisposable Schedule <TAggregate>( IScheduledCommand <TAggregate> scheduledCommand, DateTimeOffset dueTime, Func <IScheduler, IScheduledCommand <TAggregate>, IDisposable> func) where TAggregate : IEventSourced { var scheduler = Clock.Current.IfTypeIs <VirtualClock>() .Then(c => (IScheduler)c.Scheduler) .Else(() => TaskPoolScheduler.Default); return(scheduler.Schedule(scheduledCommand, dueTime, func)); }
public static async Task <ScheduledCommandResult> ApplyScheduledCommand <TAggregate>( this IEventSourcedRepository <TAggregate> repository, IScheduledCommand <TAggregate> scheduled, Func <Task <bool> > verifyPrecondition = null) where TAggregate : class, IEventSourced { TAggregate aggregate = null; Exception exception = null; try { if (verifyPrecondition != null && !await verifyPrecondition()) { return(await FailScheduledCommand(repository, scheduled, new PreconditionNotMetException())); } aggregate = await repository.GetLatest(scheduled.AggregateId); if (aggregate == null) { if (scheduled.Command is ConstructorCommand <TAggregate> ) { var ctor = typeof(TAggregate).GetConstructor(new[] { scheduled.Command.GetType() }); aggregate = (TAggregate)ctor.Invoke(new[] { scheduled.Command }); } else { // TODO: (ApplyScheduledCommand) this should probably be a different exception type. throw new ConcurrencyException( string.Format("No {0} was found with id {1} so the command could not be applied.", typeof(TAggregate).Name, scheduled.AggregateId), new IEvent[] { scheduled }); } } else { await aggregate.ApplyAsync(scheduled.Command); } await repository.Save(aggregate); return(new CommandSucceeded(scheduled)); } catch (Exception ex) { exception = ex; } return(await FailScheduledCommand(repository, scheduled, exception, aggregate)); }
internal static async Task <ScheduledCommand> StoreScheduledCommand <TAggregate>( IScheduledCommand <TAggregate> scheduledCommand, Func <CommandSchedulerDbContext> createDbContext, Func <IScheduledCommand <TAggregate>, CommandSchedulerDbContext, Task <string> > clockNameForEvent) where TAggregate : class { ScheduledCommand storedScheduledCommand; using (var db = createDbContext()) { var domainTime = Domain.Clock.Now(); // get or create a clock to schedule the command on var clock = scheduledCommand.Clock as Clock; if (clock == null) { var clockName = await clockNameForEvent(scheduledCommand, db); var clockStartTime = scheduledCommand.Clock?.Now() ?? domainTime; clock = await GetOrAddSchedulerClock( db, clockName, clockStartTime); } storedScheduledCommand = CreateStoredScheduledCommand( scheduledCommand, domainTime, clock); if (scheduledCommand.IsDue() && !scheduledCommand.Command.RequiresDurableScheduling()) { storedScheduledCommand.NonDurable = true; return(storedScheduledCommand); } Debug.WriteLine( $"Storing command '{scheduledCommand.Command.CommandName}' ({scheduledCommand.TargetId}:{storedScheduledCommand.SequenceNumber}) on clock '{clock.Name}'"); await SaveScheduledCommandToDatabase(db, storedScheduledCommand, scheduledCommand); } scheduledCommand.IfTypeIs <ScheduledCommand <TAggregate> >() .ThenDo(c => c.SequenceNumber = storedScheduledCommand.SequenceNumber); return(storedScheduledCommand); }
private static string Description(IScheduledCommand <TAggregate> scheduledCommand) { return(new { Name = scheduledCommand.Command.CommandName, DueTime = scheduledCommand.DueTime.IfNotNull() .Then(t => t.ToString("O")) .Else(() => "[null]"), Clocks = Domain.Clock.Current.ToString(), scheduledCommand.AggregateId, scheduledCommand.ETag }.ToString()); }
internal static void EnsureCommandHasETag <TTarget>(this IScheduledCommand <TTarget> scheduledCommand) { var command = scheduledCommand.Command; if (String.IsNullOrEmpty(command.ETag)) { command.IfTypeIs <Command>() .ThenDo(c => c.ETag = CommandContext.Current .IfNotNull() .Then(ctx => ctx.NextETag(scheduledCommand.TargetId)) .Else(() => Guid.NewGuid().ToString("N").ToETag())); } }
public async Task<bool> VerifyPrecondition(IScheduledCommand scheduledCommand) { if (scheduledCommand == null) { throw new ArgumentNullException("scheduledCommand"); } if (scheduledCommand.DeliveryPrecondition == null) { return true; } using (var eventStore = createEventStoreDbContext()) { return await eventStore.Events.AnyAsync( e => e.AggregateId == scheduledCommand.DeliveryPrecondition.AggregateId && e.ETag == scheduledCommand.DeliveryPrecondition.ETag); } }
public static async Task<bool> IsPreconditionSatisfied( this ICommandPreconditionVerifier preconditionVerifier, IScheduledCommand scheduledCommand) { if (preconditionVerifier == null) { throw new ArgumentNullException("preconditionVerifier"); } var precondition = scheduledCommand.DeliveryPrecondition; if (precondition == null) { return true; } return await preconditionVerifier.HasBeenApplied( precondition.AggregateId, precondition.ETag); }
public async Task<bool> IsPreconditionSatisfied(IScheduledCommand scheduledCommand) { if (scheduledCommand == null) { throw new ArgumentNullException("scheduledCommand"); } if (scheduledCommand.DeliveryPrecondition == null) { return true; } var aggregateId = scheduledCommand.DeliveryPrecondition.AggregateId.ToString(); var etag = scheduledCommand.DeliveryPrecondition.ETag; return eventStreams.Values.Any(v => { var eventsForAggregate = v.All(aggregateId).Result; return eventsForAggregate.Any(e => e.ETag == etag); }); }
protected CommandDelivered(IScheduledCommand command) : base(command) { }
public string ClockName(IScheduledCommand forCommand) => getClockName(forCommand);
public string ClockName(IScheduledCommand forCommand) { return getClockName(forCommand); }
internal CommandDeduplicated(IScheduledCommand command, string when) : base(command) { this.when = when; }
private async Task Enqueue(IScheduledCommand scheduledCommand) { var message = new BrokeredMessage(scheduledCommand.ToJson()) { SessionId = scheduledCommand.AggregateId.ToString() }; if (scheduledCommand.DueTime != null) { message.ScheduledEnqueueTimeUtc = scheduledCommand.DueTime.Value.UtcDateTime.Add(MessageDeliveryOffsetFromCommandDueTime); } messageSubject.OnNext(scheduledCommand); using (new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled)) { await queueClient.SendAsync(message); } }
public CommandScheduled(IScheduledCommand command, IClock clock = null) : base(command) { Clock = clock; }
public CommandSucceeded(IScheduledCommand command) : base(command) { }