private void ScheduleRecurringJob(BackgroundProcessContext context, IStorageConnection connection, string recurringJobId, RecurringJobEntity recurringJob, DateTime now) { // We always start a transaction, regardless our recurring job was updated or not, // to prevent from infinite loop, when there's an old processing server (pre-1.7.0) // in our environment that doesn't know it should modify the score for entries in // the recurring jobs set. using (var transaction = connection.CreateWriteTransaction()) { try { // We can't handle recurring job with unsupported versions - there may be additional // features. we don't know about. We also shouldn't stop the whole scheduler as // there may be jobs with lower versions. Instead, we'll re-schedule such a job and // emit a warning message to the log. if (recurringJob.Version.HasValue && recurringJob.Version > MaxSupportedVersion) { throw new NotSupportedException($"Server '{context.ServerId}' can't process recurring job '{recurringJobId}' of version '{recurringJob.Version ?? 1}'. Max supported version of this server is '{MaxSupportedVersion}'."); } BackgroundJob backgroundJob = null; IReadOnlyDictionary <string, string> changedFields; if (recurringJob.TrySchedule(out var nextExecution, out var error)) { if (nextExecution.HasValue && nextExecution <= now) { backgroundJob = _factory.TriggerRecurringJob(context.Storage, connection, _profiler, recurringJob, now); if (String.IsNullOrEmpty(backgroundJob?.Id)) { _logger.Debug($"Recurring job '{recurringJobId}' execution at '{nextExecution}' has been canceled."); } } recurringJob.IsChanged(out changedFields, out nextExecution); } else { throw new InvalidOperationException("Recurring job can't be scheduled, see inner exception for details.", error); } if (backgroundJob != null) { _factory.StateMachine.EnqueueBackgroundJob( context.Storage, connection, transaction, recurringJob, backgroundJob, "Triggered by recurring job scheduler", _profiler); } transaction.UpdateRecurringJob(recurringJob, changedFields, nextExecution, _logger); } catch (Exception ex) { throw new BackgroundJobClientException(ex.Message, ex); } // We should commit transaction outside of the internal try/catch block, because these // exceptions are always due to network issues, and in case of a timeout exception we // can't determine whether it was actually succeeded or not, and can't perform an // idempotent retry in this case. transaction.Commit(); } }