/// <summary> /// Get a handle to the next trigger to be fired, and mark it as 'reserved' /// by the calling scheduler. /// </summary> /// <seealso cref="ITrigger" /> public virtual IList<IOperableTrigger> AcquireNextTriggers(DateTimeOffset noLaterThan, int maxCount, TimeSpan timeWindow) { lock (lockObject) { // multiple instances management this.Schedulers.Save(new BsonDocument( new BsonElement("_id", this.instanceId), new BsonElement("Expires", (SystemTime.Now() + new TimeSpan(0, 10, 0)).UtcDateTime), new BsonElement("State", "Running"))); this.Schedulers.Remove( Query.LT("Expires", SystemTime.Now().UtcDateTime)); IEnumerable<BsonValue> activeInstances = this.Schedulers.Distinct("_id"); this.Triggers.Update( Query.NotIn("SchedulerInstanceId", activeInstances), Update.Unset("SchedulerInstanceId") .Set("State", "Waiting")); List<IOperableTrigger> result = new List<IOperableTrigger>(); Collection.ISet<JobKey> acquiredJobKeysForNoConcurrentExec = new Collection.HashSet<JobKey>(); DateTimeOffset? firstAcquiredTriggerFireTime = null; var candidates = this.Triggers.FindAs<Spi.IOperableTrigger>( Query.And( Query.EQ("State", "Waiting"), Query.LTE("nextFireTimeUtc", (noLaterThan + timeWindow).UtcDateTime))) .OrderBy(t => t.GetNextFireTimeUtc()).ThenByDescending(t => t.Priority); foreach (IOperableTrigger trigger in candidates) { if (trigger.GetNextFireTimeUtc() == null) { continue; } // it's possible that we've selected triggers way outside of the max fire ahead time for batches // (up to idleWaitTime + fireAheadTime) so we need to make sure not to include such triggers. // So we select from the first next trigger to fire up until the max fire ahead time after that... // which will perfectly honor the fireAheadTime window because the no firing will occur until // the first acquired trigger's fire time arrives. if (firstAcquiredTriggerFireTime != null && trigger.GetNextFireTimeUtc() > (firstAcquiredTriggerFireTime.Value + timeWindow)) { break; } if (this.ApplyMisfire(trigger)) { if (trigger.GetNextFireTimeUtc() == null || trigger.GetNextFireTimeUtc() > noLaterThan + timeWindow) { continue; } } // If trigger's job is set as @DisallowConcurrentExecution, and it has already been added to result, then // put it back into the timeTriggers set and continue to search for next trigger. JobKey jobKey = trigger.JobKey; IJobDetail job = this.Jobs.FindOneByIdAs<IJobDetail>(jobKey.ToBsonDocument()); if (job.ConcurrentExecutionDisallowed) { if (acquiredJobKeysForNoConcurrentExec.Contains(jobKey)) { continue; // go to next trigger in store. } else { acquiredJobKeysForNoConcurrentExec.Add(jobKey); } } trigger.FireInstanceId = this.GetFiredTriggerRecordId(); var acquired = this.Triggers.FindAndModify( Query.And( Query.EQ("_id", trigger.Key.ToBsonDocument()), Query.EQ("State", "Waiting")), SortBy.Null, Update.Set("State", "Acquired") .Set("SchedulerInstanceId", this.instanceId) .Set("FireInstanceId", trigger.FireInstanceId)); if (acquired.ModifiedDocument != null) { result.Add(trigger); if (firstAcquiredTriggerFireTime == null) { firstAcquiredTriggerFireTime = trigger.GetNextFireTimeUtc(); } } if (result.Count == maxCount) { break; } } return result; } }
// TODO: this really ought to return something like a FiredTriggerBundle, // so that the fireInstanceId doesn't have to be on the trigger... protected virtual IList<IOperableTrigger> AcquireNextTrigger(ConnectionAndTransactionHolder conn, DateTimeOffset noLaterThan, int maxCount, TimeSpan timeWindow) { List<IOperableTrigger> acquiredTriggers = new List<IOperableTrigger>(); Collection.ISet<JobKey> acquiredJobKeysForNoConcurrentExec = new Collection.HashSet<JobKey>(); const int MaxDoLoopRetry = 3; int currentLoopCount = 0; DateTimeOffset? firstAcquiredTriggerFireTime = null; do { currentLoopCount ++; try { IList<TriggerKey> keys; // If timeWindow is specified, then we need to select trigger fire time with wider range! if (timeWindow > TimeSpan.Zero) { keys = Delegate.SelectTriggerToAcquire(conn, noLaterThan + timeWindow, MisfireTime, maxCount); } else { keys = Delegate.SelectTriggerToAcquire(conn, noLaterThan, MisfireTime, maxCount); } // No trigger is ready to fire yet. if (keys == null || keys.Count == 0) { return acquiredTriggers; } foreach (TriggerKey triggerKey in keys) { // If our trigger is no longer available, try a new one. IOperableTrigger nextTrigger = RetrieveTrigger(conn, triggerKey); if (nextTrigger == null) { continue; // next trigger } // it's possible that we've selected triggers way outside of the max fire ahead time for batches // (up to idleWaitTime + fireAheadTime) so we need to make sure not to include such triggers. // So we select from the first next trigger to fire up until the max fire ahead time after that... // which will perfectly honor the fireAheadTime window because the no firing will occur until // the first acquired trigger's fire time arrives. if (firstAcquiredTriggerFireTime != null && nextTrigger.GetNextFireTimeUtc() > (firstAcquiredTriggerFireTime.Value + timeWindow)) { break; } // If trigger's job is set as @DisallowConcurrentExecution, and it has already been added to result, then // put it back into the timeTriggers set and continue to search for next trigger. JobKey jobKey = nextTrigger.JobKey; IJobDetail job = Delegate.SelectJobDetail(conn, jobKey, TypeLoadHelper); if (job.ConcurrentExecutionDisallowed) { if (acquiredJobKeysForNoConcurrentExec.Contains(jobKey)) { continue; // next trigger } else { acquiredJobKeysForNoConcurrentExec.Add(jobKey); } } // We now have a acquired trigger, let's add to return list. // If our trigger was no longer in the expected state, try a new one. int rowsUpdated = Delegate.UpdateTriggerStateFromOtherState(conn, triggerKey, StateAcquired, StateWaiting); if (rowsUpdated <= 0) { // TODO: Hum... shouldn't we log a warning here? continue; // next trigger } nextTrigger.FireInstanceId = GetFiredTriggerRecordId(); Delegate.InsertFiredTrigger(conn, nextTrigger, StateAcquired, null); acquiredTriggers.Add(nextTrigger); if (firstAcquiredTriggerFireTime == null) { firstAcquiredTriggerFireTime = nextTrigger.GetNextFireTimeUtc(); } } // if we didn't end up with any trigger to fire from that first // batch, try again for another batch. We allow with a max retry count. if (acquiredTriggers.Count == 0 && currentLoopCount < MaxDoLoopRetry) { continue; } // We are done with the while loop. break; } catch (Exception e) { throw new JobPersistenceException("Couldn't acquire next trigger: " + e.Message, e); } } while (true); // Return the acquired trigger list return acquiredTriggers; }