public JobServiceConfigurator(IServiceInstanceConfigurator <TReceiveEndpointConfigurator> instanceConfigurator) { _instanceConfigurator = instanceConfigurator; JobService = new JobService(_instanceConfigurator.InstanceAddress); instanceConfigurator.ConnectBusObserver(new JobServiceBusObserver(JobService)); instanceConfigurator.AddSpecification(this); _options = _instanceConfigurator.Options <JobServiceOptions>(options => { options.JobService = JobService; options.JobTypeSagaEndpointName = _instanceConfigurator.EndpointNameFormatter.Saga <JobTypeSaga>(); options.JobStateSagaEndpointName = _instanceConfigurator.EndpointNameFormatter.Saga <JobSaga>(); options.JobAttemptSagaEndpointName = _instanceConfigurator.EndpointNameFormatter.Saga <JobAttemptSaga>(); }); _instanceConfigurator.ConnectEndpointConfigurationObserver(new JobServiceEndpointConfigurationObserver(_options, cfg => { if (_jobTypeSagaEndpointConfigurator != null) { cfg.AddDependency(_jobTypeSagaEndpointConfigurator); } if (_jobSagaEndpointConfigurator != null) { cfg.AddDependency(_jobSagaEndpointConfigurator); } if (_jobAttemptSagaEndpointConfigurator != null) { cfg.AddDependency(_jobAttemptSagaEndpointConfigurator); } })); }
public JobServiceConsumerConfigurationObserver(IReceiveEndpointConfigurator configurator, JobServiceOptions jobServiceOptions, Action <IReceiveEndpointConfigurator> configureEndpoint) { _configurator = configurator; _jobServiceOptions = jobServiceOptions; _configureEndpoint = configureEndpoint; }
public JobService(Uri instanceAddress, JobServiceOptions options) { _options = options; InstanceAddress = instanceAddress; _jobTypes = new Dictionary <Type, IJobTypeRegistration>(); _jobs = new ConcurrentDictionary <Guid, JobHandle>(); }
public JobServiceConsumerConfigurationObserver(IReceiveEndpointConfigurator configurator, JobServiceOptions jobServiceOptions, Action <IReceiveEndpointConfigurator> configureEndpoint) { _configurator = configurator; _jobServiceOptions = jobServiceOptions; _configureEndpoint = configureEndpoint; _consumerConfigurators = new Dictionary <Type, IConsumeConfigurator>(); }
/// <summary> /// Configures support for job consumers on the service instance, which supports executing long-running jobs without blocking the consumer pipeline. /// Job consumers use multiple state machines to track jobs, each of which runs on its own dedicated receive endpoint. Multiple service /// instances will use the competing consumer pattern, so a shared saga repository should be configured. /// This method does not configure the state machine endpoints required to use the job service, and should only be used for services where another /// service has the job service endpoints configured. /// </summary> /// <typeparam name="T">The transport receive endpoint configurator type</typeparam> /// <param name="configurator">The Conductor service instance</param> /// <param name="options"></param> /// <param name="configure"></param> internal static IServiceInstanceConfigurator <T> ConfigureJobService <T>(this IServiceInstanceConfigurator <T> configurator, JobServiceOptions options, Action <IJobServiceConfigurator> configure = default) where T : IReceiveEndpointConfigurator { var jobServiceConfigurator = new JobServiceConfigurator <T>(configurator, options); configure?.Invoke(jobServiceConfigurator); return(configurator); }
/// <summary> /// Configures support for job consumers on the service instance, which supports executing long-running jobs without blocking the consumer pipeline. /// Job consumers use multiple state machines to track jobs, each of which runs on its own dedicated receive endpoint. Multiple service /// instances will use the competing consumer pattern, so a shared saga repository should be configured. /// This method does not configure the state machine endpoints required to use the job service, and should only be used for services where another /// service has the job service endpoints configured. /// </summary> /// <typeparam name="T">The transport receive endpoint configurator type</typeparam> /// <param name="configurator">The Conductor service instance</param> /// <param name="options"></param> internal static IServiceInstanceConfigurator <T> ConfigureJobService <T>(this IServiceInstanceConfigurator <T> configurator, JobServiceOptions options) where T : IReceiveEndpointConfigurator { var jobServiceConfigurator = new JobServiceConfigurator <T>(configurator); jobServiceConfigurator.ApplyJobServiceOptions(options); return(configurator); }
public JobService(IServiceInstanceConfigurator configurator, JobServiceOptions options) { _options = options; InstanceAddress = configurator.InstanceAddress; _jobTypes = new Dictionary <Type, IJobTypeRegistration>(); _jobs = new ConcurrentDictionary <Guid, JobHandle>(); ConfigureSuperviseJobConsumer(configurator.InstanceEndpointConfigurator); }
public JobTypeStateMachine(JobServiceOptions options) { Event(() => JobSlotRequested, x => { x.CorrelateById(m => m.Message.JobTypeId); x.ConfigureConsumeTopology = false; }); Event(() => JobSlotReleased, x => { x.CorrelateById(m => m.Message.JobTypeId); x.ConfigureConsumeTopology = false; }); Event(() => SetConcurrentJobLimit, x => x.CorrelateById(m => m.Message.JobTypeId)); InstanceState(x => x.CurrentState, Active, Idle); During(Initial, Active, Idle, When(JobSlotRequested) .IfElse(context => context.IsSlotAvailable(options.HeartbeatTimeout), allocate => allocate .TransitionTo(Active), unavailable => unavailable .RespondAsync(context => context.Init <JobSlotUnavailable>(new { context.Data.JobId })))); During(Active, When(JobSlotReleased) .If(context => context.Instance.ActiveJobs.Any(x => x.JobId == context.Data.JobId), release => release .Then(context => { var activeJob = context.Instance.ActiveJobs.FirstOrDefault(x => x.JobId == context.Data.JobId); if (activeJob != null) { context.Instance.ActiveJobs.Remove(activeJob); context.Instance.ActiveJobCount--; LogContext.Debug?.Log("Released Job Slot: {JobId} ({JobCount}): {InstanceAddress}", activeJob.JobId, context.Instance.ActiveJobCount, activeJob.InstanceAddress); } })) .If(context => context.Instance.ActiveJobCount == 0, empty => empty.TransitionTo(Idle))); During(Initial, When(SetConcurrentJobLimit) .SetConcurrentLimit() .TransitionTo(Idle)); During(Active, Idle, When(SetConcurrentJobLimit) .SetConcurrentLimit() ); }
public JobAttemptStateMachine(JobServiceOptions options) { _options = options; SuspectJobRetryCount = options.SuspectJobRetryCount; SuspectJobRetryDelay = options.SuspectJobRetryDelay ?? options.SlotWaitTime; Event(() => StartJobAttempt, x => x.CorrelateById(context => context.Message.AttemptId)); Event(() => StartJobFaulted, x => x.CorrelateById(context => context.Message.Message.AttemptId)); Event(() => AttemptCanceled, x => x.CorrelateById(context => context.Message.AttemptId)); Event(() => AttemptCompleted, x => x.CorrelateById(context => context.Message.AttemptId)); Event(() => AttemptFaulted, x => x.CorrelateById(context => context.Message.AttemptId)); Event(() => AttemptStarted, x => x.CorrelateById(context => context.Message.AttemptId)); Event(() => AttemptStatus, x => x.CorrelateById(context => context.Message.AttemptId)); Schedule(() => StatusCheckRequested, instance => instance.StatusCheckTokenId, x => { x.Delay = options.StatusCheckInterval; x.Received = r => r.CorrelateById(context => context.Message.AttemptId); }); InstanceState(x => x.CurrentState, Starting, Running, Faulted, CheckingStatus, Suspect); During(Initial, Starting, When(StartJobAttempt) .Then(context => { context.Instance.JobId = context.Data.JobId; context.Instance.RetryAttempt = context.Data.RetryAttempt; context.Instance.ServiceAddress ??= context.Data.ServiceAddress; }) .SendStartJob() .RespondAsync(context => context.Init <JobAttemptCreated>(context.Data)) .TransitionTo(Starting)); During(Starting, When(StartJobFaulted) .Then(context => { context.Instance.Faulted = context.Data.Timestamp; context.Instance.InstanceAddress ??= context.GetPayload <ConsumeContext>().SourceAddress; }) .SendJobAttemptFaulted(this) .TransitionTo(Faulted)); During(Initial, Starting, Running, When(AttemptStarted) .Then(context => { context.Instance.JobId = context.Data.JobId; context.Instance.Started = context.Data.Timestamp; context.Instance.RetryAttempt = context.Data.RetryAttempt; context.Instance.InstanceAddress ??= context.Data.InstanceAddress; }) .ScheduleJobStatusCheck(this) .TransitionTo(Running)); During(Running, CheckingStatus, Suspect, When(AttemptCompleted) .Unschedule(StatusCheckRequested) .Finalize(), When(AttemptCanceled) .Unschedule(StatusCheckRequested) .Finalize(), When(AttemptFaulted) .Then(context => { context.Instance.Faulted = context.Data.Timestamp; context.Instance.InstanceAddress ??= context.GetPayload <ConsumeContext>().SourceAddress; }) .Unschedule(StatusCheckRequested) .TransitionTo(Faulted)); During(Running, When(StatusCheckRequested.Received) .SendCheckJobStatus(this) .TransitionTo(CheckingStatus) .Catch <Exception>(eb => eb.TransitionTo(Suspect)) .ScheduleJobStatusCheck(this)); During(CheckingStatus, When(StatusCheckRequested.Received) .SendCheckJobStatus(this) .TransitionTo(Suspect) .Catch <Exception>(eb => eb.TransitionTo(Suspect)) .ScheduleJobStatusCheck(this)); During(Running, CheckingStatus, Suspect, When(AttemptStatus, context => context.Data.Status == JobStatus.Running) .TransitionTo(Running), When(AttemptStatus, context => context.Data.Status == JobStatus.Canceled || context.Data.Status == JobStatus.Completed) .Unschedule(StatusCheckRequested) .Finalize(), When(AttemptStatus, context => context.Data.Status == JobStatus.Faulted) .Unschedule(StatusCheckRequested) .TransitionTo(Faulted)); During(Suspect, When(StatusCheckRequested.Received) .SendJobAttemptFaulted(this) .TransitionTo(Faulted)); During(Faulted, Ignore(StatusCheckRequested.Received), Ignore(AttemptFaulted)); During(Initial, When(AttemptCompleted) .Finalize(), When(AttemptCanceled) .Finalize(), When(StatusCheckRequested.Received) .Finalize(), When(AttemptStatus) .Finalize()); SetCompletedWhenFinalized(); }
public JobStateMachine(JobServiceOptions options) { JobTypeSagaEndpointAddress = options.JobTypeSagaEndpointAddress; JobSagaEndpointAddress = options.JobSagaEndpointAddress; JobAttemptSagaEndpointAddress = options.JobAttemptSagaEndpointAddress; Event(() => JobSubmitted, x => x.CorrelateById(m => m.Message.JobId)); Event(() => JobSlotAllocated, x => x.CorrelateById(m => m.Message.JobId)); Event(() => JobSlotUnavailable, x => x.CorrelateById(m => m.Message.JobId)); Event(() => AllocateJobSlotFaulted, x => x.CorrelateById(m => m.Message.Message.JobId)); Event(() => JobAttemptCreated, x => x.CorrelateById(m => m.Message.JobId)); Event(() => StartJobAttemptFaulted, x => x.CorrelateById(m => m.Message.Message.JobId)); Event(() => AttemptCanceled, x => x.CorrelateById(m => m.Message.JobId)); Event(() => AttemptCompleted, x => x.CorrelateById(m => m.Message.JobId)); Event(() => AttemptFaulted, x => x.CorrelateById(m => m.Message.JobId)); Event(() => AttemptStarted, x => x.CorrelateById(m => m.Message.JobId)); Schedule(() => JobSlotWaitElapsed, instance => instance.JobSlotWaitToken, x => { x.Delay = options.SlotWaitTime; x.Received = r => r.CorrelateById(context => context.Message.JobId); }); Schedule(() => JobRetryDelayElapsed, instance => instance.JobRetryDelayToken, x => { x.Received = r => r.CorrelateById(context => context.Message.JobId); }); InstanceState(x => x.CurrentState, Submitted, WaitingToStart, WaitingForSlot, Started, Completed, Faulted, Canceled, StartingJobAttempt, AllocatingJobSlot, WaitingToRetry); Initially( When(JobSubmitted) .Then(OnJobSubmitted) .RequestJobSlot(this)); During(AllocatingJobSlot, When(JobSlotAllocated) .RequestStartJob(this), When(JobSlotUnavailable) .WaitForJobSlot(this), When(AllocateJobSlotFaulted) .WaitForJobSlot(this)); During(WaitingForSlot, When(JobSlotWaitElapsed.Received) .RequestJobSlot(this)); During(StartingJobAttempt, When(JobAttemptCreated) .If(context => context.Data.AttemptId == context.Instance.AttemptId, x => x.TransitionTo(WaitingToStart)), When(StartJobAttemptFaulted) .Then(context => { context.Instance.Reason = context.Data.Exceptions.FirstOrDefault()?.Message; }) .PublishJobFaulted() .TransitionTo(Faulted)); During(Started, Completed, Faulted, Ignore(JobAttemptCreated), Ignore(StartJobAttemptFaulted)); During(StartingJobAttempt, WaitingToStart, Started, When(AttemptStarted) .Then(context => context.Instance.Started = context.Data.Timestamp) .PublishJobStarted() .TransitionTo(Started)); During(StartingJobAttempt, WaitingToStart, Started, When(AttemptCompleted) .Then(context => { context.Instance.Completed = context.Data.Timestamp; context.Instance.Duration = context.Data.Duration; }) .PublishJobCompleted() .TransitionTo(Completed)); During(Completed, When(AttemptCompleted) .PublishJobCompleted()); During(StartingJobAttempt, WaitingToStart, Started, When(AttemptFaulted) .Then(context => { context.Instance.Faulted = context.Data.Timestamp; }) .IfElse(context => context.Data.RetryDelay.HasValue, retry => retry .Schedule(JobRetryDelayElapsed, context => context.Init <JobRetryDelayElapsed>(new { context.Data.JobId }), context => context.Data.RetryDelay.Value) .TransitionTo(WaitingToRetry), fault => fault .PublishJobFaulted() .TransitionTo(Faulted))); During(Faulted, When(AttemptFaulted) .PublishJobFaulted()); During(WaitingToRetry, Ignore(AttemptFaulted), When(JobRetryDelayElapsed.Received) .Then(context => { context.Instance.AttemptId = NewId.NextGuid(); context.Instance.RetryAttempt++; }) .RequestJobSlot(this) ); During(StartingJobAttempt, WaitingToStart, Started, When(AttemptCanceled) .Then(context => { context.Instance.Faulted = context.Data.Timestamp; }) .PublishJobCanceled() .TransitionTo(Canceled)); During(Canceled, When(AttemptCanceled) .PublishJobCanceled()); WhenEnter(Completed, x => x.SendJobSlotReleased(options.JobTypeSagaEndpointAddress)); WhenEnter(Canceled, x => x.SendJobSlotReleased(options.JobTypeSagaEndpointAddress)); WhenEnter(Faulted, x => x.SendJobSlotReleased(options.JobTypeSagaEndpointAddress)); WhenEnter(WaitingToRetry, x => x.SendJobSlotReleased(options.JobTypeSagaEndpointAddress)); }
public JobStateMachine(JobServiceOptions options) { _options = options; Event(() => JobSubmitted, x => x.CorrelateById(m => m.Message.JobId)); Event(() => JobSlotAllocated, x => { x.CorrelateById(m => m.Message.JobId); x.ConfigureConsumeTopology = false; }); Event(() => JobSlotUnavailable, x => { x.CorrelateById(m => m.Message.JobId); x.ConfigureConsumeTopology = false; }); Event(() => AllocateJobSlotFaulted, x => { x.CorrelateById(m => m.Message.Message.JobId); x.ConfigureConsumeTopology = false; }); Event(() => JobAttemptCreated, x => { x.CorrelateById(m => m.Message.JobId); x.ConfigureConsumeTopology = false; }); Event(() => StartJobAttemptFaulted, x => { x.CorrelateById(m => m.Message.Message.JobId); x.ConfigureConsumeTopology = false; }); Event(() => AttemptCanceled, x => x.CorrelateById(m => m.Message.JobId)); Event(() => AttemptCompleted, x => x.CorrelateById(m => m.Message.JobId)); Event(() => AttemptFaulted, x => x.CorrelateById(m => m.Message.JobId)); Event(() => AttemptStarted, x => x.CorrelateById(m => m.Message.JobId)); Event(() => JobCompleted, x => x.CorrelateById(m => m.Message.JobId)); Schedule(() => JobSlotWaitElapsed, instance => instance.JobSlotWaitToken, x => { x.Delay = options.SlotWaitTime; x.Received = r => { r.CorrelateById(context => context.Message.JobId); r.ConfigureConsumeTopology = false; }; }); Schedule(() => JobRetryDelayElapsed, instance => instance.JobRetryDelayToken, x => { x.Received = r => { r.CorrelateById(context => context.Message.JobId); r.ConfigureConsumeTopology = false; }; }); InstanceState(x => x.CurrentState, Submitted, WaitingToStart, WaitingForSlot, Started, Completed, Faulted, Canceled, StartingJobAttempt, AllocatingJobSlot, WaitingToRetry); Initially( When(JobSubmitted) .InitializeJob() .RequestJobSlot(this)); During(AllocatingJobSlot, When(JobSlotAllocated) .RequestStartJob(this), When(JobSlotUnavailable) .WaitForJobSlot(this), When(AllocateJobSlotFaulted) .WaitForJobSlot(this)); During(WaitingForSlot, When(JobSlotWaitElapsed.Received) .RequestJobSlot(this)); During(StartingJobAttempt, When(StartJobAttemptFaulted) .Then(context => { context.Instance.Reason = context.Data.Exceptions.FirstOrDefault()?.Message; }) .NotifyJobFaulted() .TransitionTo(Faulted)); During(Started, Completed, Faulted, Ignore(JobAttemptCreated), Ignore(StartJobAttemptFaulted)); During(StartingJobAttempt, WaitingToStart, Started, When(AttemptStarted) .Then(context => context.Instance.Started = context.Data.Timestamp) .PublishJobStarted() .TransitionTo(Started)); During(StartingJobAttempt, WaitingToStart, Started, When(AttemptCompleted) .Then(context => { context.Instance.Completed = context.Data.Timestamp; context.Instance.Duration = context.Data.Duration; }) .NotifyJobCompleted() .TransitionTo(Completed)); During(StartingJobAttempt, WaitingToStart, Started, When(AttemptFaulted) .Then(context => { context.Instance.Faulted = context.Data.Timestamp; context.Instance.Reason = context.Data.Exceptions?.Message ?? "Job Attempt Faulted (unknown reason)"; }) .IfElse(context => context.Data.RetryDelay.HasValue, retry => retry .Schedule(JobRetryDelayElapsed, context => context.Init <JobRetryDelayElapsed>(new { context.Data.JobId }), context => context.Data.RetryDelay.Value) .TransitionTo(WaitingToRetry), fault => fault .NotifyJobFaulted() .TransitionTo(Faulted))); During(Completed, When(AttemptCompleted) .NotifyJobCompleted(), When(AttemptStarted) .Then(context => context.Instance.Started = context.Data.Timestamp) .PublishJobStarted(), When(JobCompleted) .If(_ => options.FinalizeCompleted, x => x.Finalize())); During(Faulted, When(AttemptFaulted) .NotifyJobFaulted(), When(AttemptStarted) .Then(context => context.Instance.Started = context.Data.Timestamp) .PublishJobStarted()); During(WaitingToRetry, Ignore(AttemptFaulted), When(JobRetryDelayElapsed.Received) .Then(context => { context.Instance.AttemptId = NewId.NextGuid(); context.Instance.RetryAttempt++; }) .RequestJobSlot(this) ); During(StartingJobAttempt, WaitingToStart, Started, When(AttemptCanceled) .Then(context => { context.Instance.Faulted = context.Data.Timestamp; context.Instance.Reason = "Job Attempt Canceled"; }) .PublishJobCanceled() .TransitionTo(Canceled)); During(Canceled, When(AttemptCanceled) .PublishJobCanceled()); WhenEnter(Completed, x => x.SendJobSlotReleased(this, JobSlotDisposition.Completed)); WhenEnter(Canceled, x => x.SendJobSlotReleased(this, JobSlotDisposition.Canceled)); WhenEnter(Faulted, x => x.SendJobSlotReleased(this, JobSlotDisposition.Faulted)); WhenEnter(WaitingToRetry, x => x.SendJobSlotReleased(this, JobSlotDisposition.Faulted)); SetCompletedWhenFinalized(); }
public JobAttemptStateMachine(JobServiceOptions options) { Event(() => StartJobAttempt, x => x.CorrelateById(context => context.Message.AttemptId)); Event(() => StartJobFaulted, x => x.CorrelateById(context => context.Message.Message.AttemptId)); Event(() => AttemptCanceled, x => x.CorrelateById(context => context.Message.AttemptId)); Event(() => AttemptCompleted, x => x.CorrelateById(context => context.Message.AttemptId)); Event(() => AttemptFaulted, x => x.CorrelateById(context => context.Message.AttemptId)); Event(() => AttemptStarted, x => x.CorrelateById(context => context.Message.AttemptId)); Schedule(() => StatusCheckRequested, instance => instance.StatusCheckTokenId, x => { x.Delay = options.StatusCheckInterval; x.Received = r => r.CorrelateById(context => context.Message.AttemptId); }); InstanceState(x => x.CurrentState, Starting, Running, Faulted, Waiting); During(Initial, Starting, When(StartJobAttempt) .Then(context => { context.Instance.JobId = context.Data.JobId; context.Instance.RetryAttempt = context.Data.RetryAttempt; context.Instance.ServiceAddress ??= context.Data.ServiceAddress; }) .SendStartJob() .RespondAsync(context => context.Init <JobAttemptCreated>(context.Data)) .TransitionTo(Starting)); During(Starting, When(StartJobFaulted) .Then(context => { context.Instance.Faulted = context.Data.Timestamp; context.Instance.InstanceAddress ??= context.GetPayload <ConsumeContext>().SourceAddress; }) .PublishJobAttemptFaulted() .TransitionTo(Faulted)); During(Initial, Starting, Running, When(AttemptStarted) .Then(context => { context.Instance.JobId = context.Data.JobId; context.Instance.Started = context.Data.Timestamp; context.Instance.RetryAttempt = context.Data.RetryAttempt; context.Instance.InstanceAddress ??= context.Data.InstanceAddress; }) .Schedule(StatusCheckRequested, x => x.Init <JobStatusCheckRequested>(new { AttemptId = x.Instance.CorrelationId })) .TransitionTo(Running)); During(Running, When(AttemptCompleted) .Unschedule(StatusCheckRequested) .Finalize(), When(AttemptCanceled) .Unschedule(StatusCheckRequested) .Finalize(), When(AttemptFaulted) .Then(context => { context.Instance.Faulted = context.Data.Timestamp; context.Instance.InstanceAddress ??= context.GetPayload <ConsumeContext>().SourceAddress; }) .Unschedule(StatusCheckRequested) .TransitionTo(Faulted)); During(Running, When(StatusCheckRequested.Received)); During(Faulted, Ignore(StatusCheckRequested.Received), Ignore(AttemptFaulted)); }
public A_single_job_service_instance() { TestTimeout = TimeSpan.FromSeconds(5); _jobServiceOptions = new JobServiceOptions(); }
public A_single_job_service_instance_running_multiple_jobs() { TestTimeout = TimeSpan.FromSeconds(10); _jobServiceOptions = new JobServiceOptions(); }
public JobServiceEndpointConfigurationObserver(JobServiceOptions jobServiceOptions, Action <IReceiveEndpointConfigurator> configureEndpoint) { _jobServiceOptions = jobServiceOptions; _configureEndpoint = configureEndpoint; }