From 08464247dcc6a33382e96c33aa345b9862cce542 Mon Sep 17 00:00:00 2001 From: Maksim Kotlyar Date: Tue, 14 May 2019 13:24:52 +0300 Subject: [PATCH 1/4] Move yadm store to bridge. --- pkg/quartz/ModelClassFactory.php | 6 +- pkg/quartz/Scheduler/Store/YadmStore.php | 1212 -------------- .../Scheduler/Store/YadmStoreResource.php | 253 --- .../Scheduler/Store/YadmStoreTest.php | 1442 ----------------- pkg/quartz/composer.json | 5 +- .../examples/calendar-interval-trigger.php | 6 +- pkg/quartz/examples/cron-trigger.php | 6 +- .../examples/daily-interval-trigger.php | 6 +- pkg/quartz/examples/scheduler.php | 6 +- pkg/quartz/examples/simple-trigger.php | 6 +- 10 files changed, 20 insertions(+), 2928 deletions(-) delete mode 100644 pkg/quartz/Scheduler/Store/YadmStore.php delete mode 100644 pkg/quartz/Scheduler/Store/YadmStoreResource.php delete mode 100644 pkg/quartz/Tests/Functional/Scheduler/Store/YadmStoreTest.php diff --git a/pkg/quartz/ModelClassFactory.php b/pkg/quartz/ModelClassFactory.php index d24aff3..27f8db0 100644 --- a/pkg/quartz/ModelClassFactory.php +++ b/pkg/quartz/ModelClassFactory.php @@ -17,13 +17,9 @@ class ModelClassFactory { /** - * @param array $values - * - * @return string - * * @throws SchedulerException */ - public static function getClass($values) + public static function getClass(array $values): string { if (false == isset($values['instance'])) { throw new SchedulerException('Values has no "instance" field'); diff --git a/pkg/quartz/Scheduler/Store/YadmStore.php b/pkg/quartz/Scheduler/Store/YadmStore.php deleted file mode 100644 index f813856..0000000 --- a/pkg/quartz/Scheduler/Store/YadmStore.php +++ /dev/null @@ -1,1212 +0,0 @@ -res = $res; - } - - /** - * {@inheritdoc} - */ - public function initialize(StdScheduler $scheduler) - { - $this->scheduler = $scheduler; - } - - /** - * @return int - */ - public function getMisfireThreshold() - { - return $this->misfireThreshold; - } - - /** - * @param int $misfireThreshold - */ - public function setMisfireThreshold($misfireThreshold) - { - if (false == is_int($misfireThreshold)) { - throw new \InvalidArgumentException('Expected misfire threshold is int'); - } - - $this->misfireThreshold = $misfireThreshold; - } - - /** - * {@inheritdoc} - */ - public function storeJobAndTrigger(JobDetail $newJob, Trigger $newTrigger) - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($newJob, $newTrigger) { - $this->doStoreJob($newJob); - $this->doStoreTrigger($newTrigger); - }); - } - - /** - * {@inheritdoc} - */ - public function storeJob(JobDetail $newJob, $replaceExisting = false) - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($newJob, $replaceExisting) { - $this->doStoreJob($newJob, $replaceExisting); - }); - } - - /** - * @param JobDetail $newJob - * @param bool $replaceExisting - * - * @throws JobPersistenceException - * @throws ObjectAlreadyExistsException - */ - protected function doStoreJob(JobDetail $newJob, $replaceExisting = false) - { - $jobKey = $newJob->getKey(); - $existingJob = $this->retrieveJob($jobKey); - - if ($existingJob && false == $replaceExisting) { - throw ObjectAlreadyExistsException::create($newJob); - } - - if ($existingJob) { - $id = get_object_id($existingJob); - $values = get_values($newJob); - - set_values($existingJob, $values); - set_object_id($existingJob, $id); - - $result = $this->res->getJobStorage()->update($existingJob, [ - 'name' => $jobKey->getName(), - 'group' => $jobKey->getGroup() - ], ['upsert' => false]); - - if ($result && (false == $result->isAcknowledged() || false == $result->getModifiedCount())) { - throw new JobPersistenceException(sprintf('Couldn\'t store job. Update failed: "%s"', $jobKey)); - } - } else { - $result = $this->res->getJobStorage()->update($newJob, [ - 'name' => $jobKey->getName(), - 'group' => $jobKey->getGroup() - ], ['upsert' => true]); - - if ($result && (false == $result->isAcknowledged() || false == $result->getUpsertedCount())) { - throw new JobPersistenceException(sprintf('Couldn\'t store job. Insert failed: "%s"', $jobKey)); - } - } - } - - /** - * {@inheritdoc} - */ - public function removeJob(Key $jobKey) - { - return $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($jobKey) { - return $this->doRemoveJob($jobKey); - }); - } - - /** - * @param Key $jobKey - * - * @return bool - * - * @throws JobPersistenceException - */ - protected function doRemoveJob(Key $jobKey) - { - if (false == $job = $this->retrieveJob($jobKey)) { - return false; - } - - $result = $this->res->getTriggerStorage()->getCollection()->deleteMany([ - 'jobName' => $jobKey->getName(), - 'jobGroup' => $jobKey->getGroup() - ]); - - if (false == $result->isAcknowledged()) { - throw new JobPersistenceException(sprintf('Couldn\'t remove job: "%s"', $jobKey)); - } - - $result = $this->res->getJobStorage()->delete($job); - - if (false == $result->isAcknowledged() || false == $result->getDeletedCount()) { - return false; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function removeJobs(array $jobKeys) - { - foreach ($jobKeys as $key) { - if (false == $key instanceof Key) { - throw new \InvalidArgumentException(sprintf('Expected Key instance but got: "%s"', - is_object($key) ? get_class($key) : gettype($key))); - } - } - - return $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($jobKeys) { - $allFound = true; - - foreach ($jobKeys as $key) { - $allFound = $this->doRemoveJob($key) && $allFound; - } - - return $allFound; - }); - } - - /** - * {@inheritdoc} - */ - public function retrieveJob(Key $jobKey) - { - // no locks necessary for read... - return $this->res->getJobStorage()->findOne([ - 'name' => $jobKey->getName(), - 'group' => $jobKey->getGroup(), - ]); - } - - /** - * {@inheritdoc} - */ - public function storeTrigger(Trigger $newTrigger, $replaceExisting = false) - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($newTrigger, $replaceExisting) { - $this->doStoreTrigger($newTrigger, $replaceExisting, Trigger::STATE_WAITING); - }); - } - - /** - * @param Trigger $newTrigger - * @param bool $replaceExisting - * @param string $state - * - * @throws JobPersistenceException - * @throws ObjectAlreadyExistsException - */ - protected function doStoreTrigger(Trigger $newTrigger, $replaceExisting = false, $state = Trigger::STATE_WAITING) - { - $triggerKey = $newTrigger->getKey(); - $existingTrigger = $this->retrieveTrigger($triggerKey); - - if ($existingTrigger && false == $replaceExisting) { - throw ObjectAlreadyExistsException::create($newTrigger); - } - - $shouldBePaused = $this->isTriggerGroupPaused($triggerKey->getGroup()); - - if (false == $shouldBePaused) { - $shouldBePaused = $this->isTriggerGroupPaused(self::ALL_GROUPS_PAUSED); - - if ($shouldBePaused) { - $this->insertPausedTriggerGroup($triggerKey->getGroup()); - } - } - - if ($shouldBePaused && ($state == Trigger::STATE_WAITING || $state == Trigger::STATE_ACQUIRED)) { - $state = Trigger::STATE_PAUSED; - } - - $newTrigger->setState($state); - -// if (null == $job) { -// $job = $this->retrieveJob($newTrigger->getJobKey()); -// } -// -// if (null == $job) { -// throw new JobPersistenceException(sprintf('The job referenced by the trigger does not exist. trigger: "%s", job: "%s"', -// $triggerKey, $newTrigger->getJobKey())); -// } - - if ($existingTrigger) { - $id = get_object_id($existingTrigger); - $values = get_values($newTrigger); - - set_values($existingTrigger, $values); - set_object_id($existingTrigger, $id); - - $result = $this->res->getTriggerStorage()->update($existingTrigger, [ - 'name' => $triggerKey->getName(), - 'group' => $triggerKey->getGroup() - ], ['upsert' => false]); - - if ($result && (false == $result->isAcknowledged() || false == $result->getModifiedCount())) { - throw new JobPersistenceException(sprintf('Couldn\'t store trigger. Update failed: "%s"', $triggerKey)); - } - } else { - $result = $this->res->getTriggerStorage()->update($newTrigger, [ - 'name' => $triggerKey->getName(), - 'group' => $triggerKey->getGroup() - ], ['upsert' => true]); - - if ($result && (false == $result->isAcknowledged() || false == $result->getUpsertedCount())) { - throw new JobPersistenceException(sprintf('Couldn\'t store trigger. Insert failed: "%s"', $triggerKey)); - } - } - } - - /** - * {@inheritdoc} - */ - public function removeTrigger(Key $triggerKey) - { - return $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($triggerKey) { - return $this->doRemoveTrigger($triggerKey); - }); - } - - /** - * @param Key $triggerKey - * - * @return bool - * - * @throws JobPersistenceException - */ - protected function doRemoveTrigger(Key $triggerKey) - { - if (false == $trigger = $this->retrieveTrigger($triggerKey)) { - return false; - } - - $result = $this->res->getTriggerStorage()->delete($trigger); - - $removedTrigger = true; - if (false == $result->isAcknowledged() || false == $result->getDeletedCount()) { - $removedTrigger = false; - } - - $jobKey = $trigger->getJobKey(); - $job = $this->retrieveJob($jobKey); - - if ($job && false == $job->isDurable()) { - $numTriggers = $this->res->getTriggerStorage()->count([ - 'jobName' => $jobKey->getName(), - 'jobGroup' => $jobKey->getGroup(), - ]); - - if ($numTriggers == 0) { - // Don't call removeJob() because we don't want to check for triggers again. - $this->res->getJobStorage()->delete($job); - } - } - - return $removedTrigger; - } - - /** - * {@inheritdoc} - */ - public function removeTriggers(array $triggerKeys) - { - foreach ($triggerKeys as $key) { - if (false == $key instanceof Key) { - throw new \InvalidArgumentException(sprintf('Expected Key instance but got: "%s"', - is_object($key) ? get_class($key) : gettype($key))); - } - } - - return $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($triggerKeys) { - $allFound = true; - - foreach ($triggerKeys as $key) { - $allFound = $this->doRemoveTrigger($key) && $allFound; - } - - return $allFound; - }); - } - - public function replaceTrigger(Key $triggerKey, Trigger $newTrigger) - { - // TODO: Implement replaceTrigger() method. - } - - /** - * {@inheritdoc} - */ - public function retrieveTrigger(Key $triggerKey) - { - // no locks necessary for read... - return $this->res->getTriggerStorage()->findOne([ - 'name' => $triggerKey->getName(), - 'group' => $triggerKey->getGroup(), - ]); - } - - /** - * {@inheritdoc} - */ - public function checkJobExists(Key $jobKey) - { - // no locks necessary for read... - return (bool) $this->res->getJobStorage()->count([ - 'name' => $jobKey->getName(), - 'group' => $jobKey->getGroup(), - ]); - } - - /** - * {@inheritdoc} - */ - public function checkTriggerExists(Key $triggerKey) - { - // no locks necessary for read... - return (bool) $this->res->getTriggerStorage()->count([ - 'name' => $triggerKey->getName(), - 'group' => $triggerKey->getGroup(), - ]); - } - - /** - * {@inheritdoc} - */ - public function clearAllSchedulingData() - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () { - $this->doClearAllSchedulingData(); - }); - } - - public function doClearAllSchedulingData() - { - $this->res->getTriggerStorage()->getCollection()->deleteMany([]); - $this->res->getJobStorage()->getCollection()->deleteMany([]); - $this->res->getCalendarStorage()->getCollection()->deleteMany([]); - $this->res->getPausedTriggerCol()->deleteMany([]); - $this->res->getFiredTriggerStorage()->getCollection()->deleteMany([]); - } - - /** - * {@inheritdoc} - */ - public function storeCalendar($name, Calendar $calendar, $replaceExisting = false, $updateTriggers = false) - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($name, $calendar, $replaceExisting, $updateTriggers) { - $this->doStoreCalendar($name, $calendar, $replaceExisting, $updateTriggers); - }); - } - - /** - * @param string $name - * @param Calendar $calendar - * @param bool $replaceExisting - * @param bool $updateTriggers - * - * @throws JobPersistenceException - * @throws ObjectAlreadyExistsException - */ - protected function doStoreCalendar($name, Calendar $calendar, $replaceExisting, $updateTriggers) - { - $existingCal = $this->retrieveCalendar($name); - - if ($existingCal && false == $replaceExisting) { - throw new ObjectAlreadyExistsException(sprintf('Calendar with name already exists: "%s"', $name)); - } - - if ($existingCal) { - $id = get_object_id($existingCal); - $values = get_values($calendar); - $values['name'] = $name; - - set_values($existingCal, $values); - set_object_id($existingCal, $id); - - $result = $this->res->getCalendarStorage()->update($existingCal, ['name' => $name], ['upsert' => false]); - - if ($result && (false == $result->isAcknowledged() || false == $result->getModifiedCount())) { - throw new JobPersistenceException(sprintf('Couldn\'t store calendar. Update failed: "%s"', $name)); - } - } else { - set_value($calendar, 'name', $name); - $result = $this->res->getCalendarStorage()->update($calendar, ['name' => $name], ['upsert' => true]); - - if ($result && (false == $result->isAcknowledged() || false == $result->getUpsertedCount())) { - throw new JobPersistenceException(sprintf('Couldn\'t store calendar. Insert failed: "%s"', $name)); - } - } - - if ($existingCal && $updateTriggers) { - /** @var Trigger $trigger */ - foreach ($this->res->getTriggerStorage()->find(['calendarName' => $name]) as $trigger) { - $trigger->updateWithNewCalendar($existingCal, $this->misfireThreshold); - $this->doStoreTrigger($trigger, true, Trigger::STATE_WAITING); - } - } - } - - /** - * {@inheritdoc} - */ - public function removeCalendar($calName) - { - return $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($calName) { - return $this->doRemoveCalendar($calName); - }); - } - - /** - * @param string $calName - * - * @return bool - * - * @throws JobPersistenceException - */ - protected function doRemoveCalendar($calName) - { - if ($this->res->getTriggerStorage()->count(['calendarName' => $calName])) { - throw new JobPersistenceException(sprintf('Calendar cannot be removed if it referenced by a trigger!. calendar: "%s"', $calName)); - } - - $result = $this->res->getCalendarStorage()->getCollection()->deleteOne(['name' => $calName]); - - return $result->isAcknowledged() && $result->getDeletedCount(); - } - - /** - * {@inheritdoc} - */ - public function retrieveCalendar($calName) - { - // no locks necessary for read... - return $this->res->getCalendarStorage()->findOne(['name' => $calName]); - } - - /** - * {@inheritdoc} - */ - public function pauseTrigger(Key $triggerKey) - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($triggerKey) { - $this->doPauseTrigger($triggerKey); - }); - } - - /** - * @param Key $triggerKey - * - * @throws JobPersistenceException - */ - protected function doPauseTrigger(Key $triggerKey) - { - if (false == $trigger = $this->retrieveTrigger($triggerKey)) { - return; - } - - if (false == in_array($trigger->getState(), [Trigger::STATE_WAITING, Trigger::STATE_ACQUIRED], true)) { - return; - } - - $trigger->setState(Trigger::STATE_PAUSED); - $result = $this->res->getTriggerStorage()->update($trigger); - - if (false == $result->isAcknowledged() || false == $result->getModifiedCount()) { - throw new JobPersistenceException(sprintf('Couldn\'t pause trigger: "%s"', $triggerKey)); - } - } - - public function pauseTriggers(GroupMatcher $matcher) - { - // TODO: Implement pauseTriggers() method. - } - - /** - * {@inheritdoc} - */ - public function pauseJob(Key $jobKey) - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($jobKey) { - $this->doPauseJob($jobKey); - }); - } - - /** - * @param Key $jobKey - */ - protected function doPauseJob(Key $jobKey) - { - foreach ($this->getTriggersForJob($jobKey) as $trigger) { - $this->doPauseTrigger($trigger->getKey()); - } - } - - public function pauseJobs(GroupMatcher $groupMatcher) - { - // TODO: Implement pauseJobs() method. - } - - /** - * {@inheritdoc} - */ - public function resumeTrigger(Key $triggerKey) - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($triggerKey) { - $this->doResumeTrigger($triggerKey); - }); - } - - /** - * @param Key $triggerKey - * - * @throws JobPersistenceException - */ - protected function doResumeTrigger(Key $triggerKey) - { - if (false == $trigger = $this->retrieveTrigger($triggerKey)) { - return; - } - - if (false == $trigger->getNextFireTime()) { - return; - } - - $misfired = false; - if (((int) $trigger->getNextFireTime()->format('U')) < time()) { - $misfired = $this->updateMisfiredTrigger($triggerKey, Trigger::STATE_WAITING); - } - - if (false == $misfired) { - $trigger->setState(Trigger::STATE_WAITING); - $result = $this->res->getTriggerStorage()->update($trigger); - - if (false == $result->isAcknowledged() || false == $result->getModifiedCount()) { - throw new JobPersistenceException(sprintf('Couldn\'t resume trigger: "%s"', $triggerKey)); - } - } - } - - /** - * @param Key $triggerKey - * @param string $newStateIfNotComplete - * - * @return bool - */ - protected function updateMisfiredTrigger(Key $triggerKey, $newStateIfNotComplete) - { - $trigger = $this->retrieveTrigger($triggerKey); - - $misfireTime = time(); - if ($this->misfireThreshold > 0) { - $misfireTime -= $this->misfireThreshold; - } - - if (((int) $trigger->getNextFireTime()->format('U')) > $misfireTime) { - return false; - } - - $this->doUpdateOfMisfiredTrigger($trigger, $newStateIfNotComplete); - - return true; - } - - /** - * @param Trigger $trig - * @param string $newStateIfNotComplete - */ - protected function doUpdateOfMisfiredTrigger(Trigger $trig, $newStateIfNotComplete) - { - $calendar = null; - if ($trig->getCalendarName()) { - $calendar = $this->retrieveCalendar($trig->getCalendarName()); - } - - $this->scheduler->notifyTriggerListenersMisfired($trig); - - $trig->updateAfterMisfire($calendar); - - if (null == $trig->getNextFireTime()) { - $this->doStoreTrigger($trig, true, Trigger::STATE_COMPLETE); - - $this->scheduler->notifySchedulerListenersFinalized($trig); - } else { - $this->doStoreTrigger($trig, true, $newStateIfNotComplete); - } - } - - public function resumeTriggers(GroupMatcher $matcher) - { - // TODO: Implement resumeTriggers() method. - } - - /** - * {@inheritdoc} - */ - public function getPausedTriggerGroups() - { - return $this->res->getPausedTriggerCol()->distinct('groupName'); - } - - /** - * {@inheritdoc} - */ - public function resumeJob(Key $jobKey) - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($jobKey) { - $this->doResumeJob($jobKey); - }); - } - - /** - * @param Key $jobKey - */ - protected function doResumeJob(Key $jobKey) - { - foreach ($this->getTriggersForJob($jobKey) as $trigger) { - $this->doResumeTrigger($trigger->getKey()); - } - } - - public function resumeJobs(GroupMatcher $matcher) - { - // TODO: Implement resumeJobs() method. - } - - /** - * {@inheritdoc} - */ - public function pauseAll() - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () { - $this->doPauseAll(); - }); - } - - protected function doPauseAll() - { - foreach ($this->getTriggerGroupNames() as $name) { - $triggers = $this->res->getTriggerStorage()->find([ - 'group' => $name, - ]); - - /** @var Trigger $trigger */ - foreach ($triggers as $trigger) { - $this->doPauseTrigger($trigger->getKey()); - } - - $this->insertPausedTriggerGroup($name); - } - - $this->insertPausedTriggerGroup(self::ALL_GROUPS_PAUSED); - } - - /** - * {@inheritdoc} - */ - public function resumeAll() - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () { - $this->doResumeAll(); - }); - } - - protected function doResumeAll() - { - foreach ($this->getTriggerGroupNames() as $name) { - $triggers = $this->res->getTriggerStorage()->find([ - 'group' => $name, - ]); - - /** @var Trigger $trigger */ - foreach ($triggers as $trigger) { - $this->doResumeTrigger($trigger->getKey()); - } - - $this->deletePausedTriggerGroup($name); - } - - $this->deletePausedTriggerGroup(self::ALL_GROUPS_PAUSED); - } - - /** - * {@inheritdoc} - */ - public function getTriggersForJob(Key $jobKey) - { - // no locks necessary for read... - return $this->res->getTriggerStorage()->find([ - 'jobName' => $jobKey->getName(), - 'jobGroup' => $jobKey->getGroup(), - ]); - } - - /** - * {@inheritdoc} - */ - public function getJobGroupNames() - { - // no locks necessary for read... - return $this->res->getJobStorage()->getCollection()->distinct('group'); - } - - /** - * {@inheritdoc} - */ - public function getTriggerGroupNames() - { - // no locks necessary for read... - return $this->res->getTriggerStorage()->getCollection()->distinct('group'); - } - - /** - * {@inheritdoc} - */ - public function getTriggerState(Key $triggerKey) - { - if (null == $trigger = $this->retrieveTrigger($triggerKey)) { - throw new JobPersistenceException(sprintf('There is no trigger with key: "%s"', (string) $triggerKey)); - } - - return $trigger->getState(); - } - - /** - * {@inheritdoc} - */ - public function getCalendarNames() - { - // no locks necessary for read... - return $this->res->getCalendarStorage()->getCollection()->distinct('name'); - } - - /** - * {@inheritdoc} - */ - public function resetTriggerFromErrorState(Key $triggerKey) - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function() use ($triggerKey) { - $this->doResetTriggerFromErrorState($triggerKey); - }); - } - - /** - * @param Key $triggerKey - * - * @throws JobPersistenceException - */ - public function doResetTriggerFromErrorState(Key $triggerKey) - { - if (null == $trigger = $this->retrieveTrigger($triggerKey)) { - throw new JobPersistenceException(sprintf('There is no trigger with identity: "%s"', (string) $triggerKey)); - } - - if ($trigger->getState() !== Trigger::STATE_ERROR) { - return; - } - - $state = Trigger::STATE_WAITING; - if ($this->isTriggerGroupPaused($triggerKey->getGroup())) { - $state = Trigger::STATE_PAUSED; - } - - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $trigger->getKey()->getName(), - 'group' => $trigger->getKey()->getGroup(), - ], [ - '$set' => [ - 'state' => $state, - ] - ]); - } - - /** - * {@inheritdoc} - */ - public function acquireNextTriggers($noLaterThan, $maxCount, $timeWindow) - { - return $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($noLaterThan, $maxCount, $timeWindow) { - return $this->doAcquireNextTriggers($noLaterThan, $maxCount, $timeWindow); - }); - } - - /** - * @param int $noLaterThan - * @param int $maxCount - * @param int $timeWindow - * - * @return Trigger[] - * - * @throws JobPersistenceException - */ - protected function doAcquireNextTriggers($noLaterThan, $maxCount, $timeWindow) - { - $noEarlyThen = time() - $this->misfireThreshold; - $acquiredTriggers = []; - - $triggers = $this->res->getTriggerStorage()->find([ - 'state' => Trigger::STATE_WAITING, - 'nextFireTime.unix' => [ - '$gte' => $noEarlyThen, - '$lte' => $noLaterThan + $timeWindow, - ] - ], [ - 'limit' => $maxCount, - 'sort' => [ - 'nextFireTime.unix' => 1, - 'priority' => -1, - ] - ]); - - $ids = []; - foreach ($triggers as $trigger) { - $ids[] = get_object_id($trigger); - $acquiredTriggers[] = $trigger; - } - - // find misfired triggers only if free space left - if (($leftCount = $maxCount - count($ids)) > 0) { - $misfiredTriggers = $this->res->getTriggerStorage()->find([ - 'state' => Trigger::STATE_WAITING, - 'nextFireTime.unix' => [ - '$lt' => $noEarlyThen, - ] - ], [ - 'limit' => $leftCount, - 'sort' => [ - 'nextFireTime.unix' => 1, - 'priority' => -1, - ] - ]); - - foreach ($misfiredTriggers as $trigger) { - $ids[] = get_object_id($trigger); - $acquiredTriggers[] = $trigger; - } - } - - // acquire found triggers - if ($acquiredTriggers) { - $result = $this->res->getTriggerStorage()->getCollection()->updateMany([ - '_id' => [ - '$in' => $ids, - ], - ], [ - '$set' => [ - 'state' => Trigger::STATE_ACQUIRED, - ] - ]); - - if (false == $result->isAcknowledged() || false == $result->getModifiedCount()) { - throw new JobPersistenceException('Couldn\'t acquire next trigger'); - } - } - - return $acquiredTriggers; - } - - public function releaseAcquiredTrigger(Trigger $trigger) - { - // TODO: Implement releaseAcquiredTrigger() method. - } - - /** - * {@inheritdoc} - */ - public function triggersFired(array $triggers, $noLaterThan) - { - foreach ($triggers as $trigger) { - if (false == $trigger instanceof Trigger) { - throw new \InvalidArgumentException(sprintf('Expected array of "%s" but got: "%s"', - Trigger::class, is_object($trigger) ? get_class($trigger) : gettype($trigger))); - } - } - - $firedTriggers = []; - foreach ($triggers as $trigger) { - $_triggers = $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($trigger, $noLaterThan) { - return $this->doTriggerFired($trigger, $noLaterThan); - }); - - if ($_triggers) { - $firedTriggers = array_merge($firedTriggers, $_triggers); - } - } - - return $firedTriggers; - } - - /** - * @param Trigger $trigger - * @param int $noLaterThan - * - * @return Trigger[] - * - * @throws JobPersistenceException - */ - public function doTriggerFired(Trigger $trigger, $noLaterThan) - { - // Make sure trigger wasn't deleted, paused, or completed... - if (false == $trigger = $this->retrieveTrigger($trigger->getKey())) { - return[]; - } - - if ($trigger->getState() !== Trigger::STATE_ACQUIRED) { - return []; - } - - $cal = null; - if ($trigger->getCalendarName()) { - if (null == $cal = $this->retrieveCalendar($trigger->getCalendarName())) { - return []; - } - } - - $firedTriggers = []; - $fireTime = new \DateTime(); - $misfireTime = time() - $this->misfireThreshold; - - // update misfired trigger - if (((int) $trigger->getNextFireTime()->format('U')) < $misfireTime) { - $this->scheduler->notifyTriggerListenersMisfired($trigger); - $trigger->updateAfterMisfire($cal); - } - - while (($nextFireTime = $trigger->getNextFireTime()) && (((int) $nextFireTime->format('U')) <= $noLaterThan )) { - $firedTrigger = clone $trigger; - $scheduledFireTime = clone $trigger->getNextFireTime(); - - $trigger->triggered($cal); - - $firedTrigger->setFireInstanceId(Uuid::uuid4()->toString()); - $firedTrigger->setState(Trigger::STATE_EXECUTING); // @todo need extra statuses for fired triggers - $firedTrigger->setFireTime(clone $fireTime); - $firedTrigger->setScheduledFireTime($scheduledFireTime); - $firedTrigger->setNextFireTime($trigger->getNextFireTime() ? clone $trigger->getNextFireTime() : null); - - $firedTriggers[] = $firedTrigger; - } - - $state = Trigger::STATE_WAITING; - if (false == $trigger->getNextFireTime()) { - $state = Trigger::STATE_COMPLETE; - } - - $this->doStoreTrigger($trigger, true, $state); - - if ($trigger->getState() === Trigger::STATE_COMPLETE) { - $this->scheduler->notifySchedulerListenersFinalized($trigger); - } - - foreach ($firedTriggers as $firedTrigger) { - $this->res->getFiredTriggerStorage()->insert($firedTrigger); - } - - return $firedTriggers; - } - - /** - * {@inheritdoc} - */ - public function triggeredJobComplete(Trigger $trigger, JobDetail $jobDetail, $triggerInstCode) - { - $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($trigger, $jobDetail, $triggerInstCode) { - $this->doTriggeredJobComplete($trigger, $jobDetail, $triggerInstCode); - }); - } - - /** - * @param Trigger $trigger - * @param JobDetail $jobDetail - * @param string $triggerInstCode - * - * @throws JobPersistenceException - */ - public function doTriggeredJobComplete(Trigger $trigger, JobDetail $jobDetail, $triggerInstCode) - { - if ($triggerInstCode === CompletedExecutionInstruction::DELETE_TRIGGER) { - // remove trigger - $this->doRemoveTrigger($trigger->getKey()); - } elseif ($triggerInstCode === CompletedExecutionInstruction::SET_TRIGGER_COMPLETE) { - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $trigger->getKey()->getName(), - 'group' => $trigger->getKey()->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_COMPLETE, - 'errorMessage' => $trigger->getErrorMessage(), - ] - ]); - } elseif ($triggerInstCode === CompletedExecutionInstruction::SET_TRIGGER_ERROR) { - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $trigger->getKey()->getName(), - 'group' => $trigger->getKey()->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ERROR, - 'errorMessage' => $trigger->getErrorMessage(), - ] - ]); - } elseif ($triggerInstCode === CompletedExecutionInstruction::SET_ALL_JOB_TRIGGERS_COMPLETE) { - $this->res->getTriggerStorage()->getCollection()->updateMany([ - 'jobName' => $trigger->getJobKey()->getName(), - 'jobGroup' => $trigger->getJobKey()->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_COMPLETE, - 'errorMessage' => $trigger->getErrorMessage(), - ] - ]); - } elseif ($triggerInstCode === CompletedExecutionInstruction::SET_ALL_JOB_TRIGGERS_ERROR) { - $this->res->getTriggerStorage()->getCollection()->updateMany([ - 'jobName' => $trigger->getJobKey()->getName(), - 'jobGroup' => $trigger->getJobKey()->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ERROR, - 'errorMessage' => $trigger->getErrorMessage(), - ] - ]); - } - - // remove fired triggers - // TODO: update fired trigger but not remove - $result = $this->res->getFiredTriggerStorage()->getCollection()->deleteOne([ - 'fireInstanceId' => $trigger->getFireInstanceId(), - ]); - - if (false == $result->isAcknowledged() || false == $result->getDeletedCount()) { - throw new JobPersistenceException(sprintf('Couldn\'t delete fired trigger: fireInstanceId "%s"', $trigger->getFireInstanceId())); - } - } - - /** - * @param string $lockName - * @param callable $callback - * - * @return mixed - */ - private function executeInLock($lockName, callable $callback) - { - $this->res->getManagementLock()->lock($lockName); - - try { - return call_user_func($callback); - } finally { - $this->res->getManagementLock()->unlock($lockName); - } - } - - /** - * @param string $groupName - * - * @return boolean - */ - public function insertPausedTriggerGroup($groupName) - { - $result = $this->res->getPausedTriggerCol()->updateOne( - ['groupName' => $groupName], - ['$set' => ['groupName' => $groupName]], - ['upsert' => true] - ); - - return $result->isAcknowledged() && $result->getUpsertedCount(); - } - - /** - * @param string $groupName - * - * @return boolean - */ - public function deletePausedTriggerGroup($groupName) - { - $result = $this->res->getPausedTriggerCol()->deleteOne(['groupName' => $groupName]); - - return $result->isAcknowledged() && $result->getDeletedCount(); - } - - /** - * @param string $groupName - * - * @return boolean - */ - public function isTriggerGroupPaused($groupName) - { - return (bool) $this->res->getPausedTriggerCol()->count(['groupName' => $groupName]); - } - - /** - * {@inheritdoc} - */ - public function schedulerStarted() - { - $this->res->getManagementLock()->unlockAll(); - - // recover failed and acquired trigger - $this->res->getTriggerStorage()->getCollection()->updateMany([ - 'state' => [ - '$in' => [Trigger::STATE_ACQUIRED, Trigger::STATE_ERROR], - ] - ], [ - '$set' => [ - 'state' => Trigger::STATE_WAITING, - ] - ]); - - // clear fired triggers - $this->res->getFiredTriggerStorage()->getCollection()->deleteMany([]); - } - - /** - * {@inheritdoc} - */ - public function retrieveFireTrigger($fireInstanceId) - { - return $this->res->getFiredTriggerStorage()->findOne([ - 'fireInstanceId' => $fireInstanceId, - ]); - } - - /** - * TODO: is not part of interface - */ - public function createIndexes() - { - $this->res->dropIndexes(); - $this->res->createIndexes(); - } -} diff --git a/pkg/quartz/Scheduler/Store/YadmStoreResource.php b/pkg/quartz/Scheduler/Store/YadmStoreResource.php deleted file mode 100644 index e3eb333..0000000 --- a/pkg/quartz/Scheduler/Store/YadmStoreResource.php +++ /dev/null @@ -1,253 +0,0 @@ -options = array_replace([ - 'uri' => 'mongodb://localhost:27017', - 'uriOptions' => [], - 'driverOptions' => [], - 'sessionId' => 'quartz', - 'dbName' => 'quartz', - 'managementLockCol' => 'managementLock', - 'calendarCol' => 'calendar', - 'triggerCol' => 'trigger', - 'firedTriggerCol' => 'firedTrigger', - 'jobCol' => 'job', - 'pausedTriggerCol' => 'pausedTrigger', - - ], $options); - } - - public function getClient(): Client - { - return $this->getClientProvider()->getClient(); - } - - public function getClientProvider(): ClientProvider - { - if (false == $this->clientProvider) { - $this->clientProvider = new ClientProvider($this->options['uri'], $this->options['uriOptions'], $this->options['driverOptions']); - } - - return $this->clientProvider; - } - - public function getCollection(string $name): Collection - { - $factory = new CollectionFactory($this->getClientProvider(), $this->options['uri']); - - return $factory->create($name, $this->options['dbName']); - } - - /** - * @return PessimisticLock - */ - public function getManagementLock() - { - if (false == $this->managementLock) { - $this->managementLock = new PessimisticLock($this->getCollection($this->options['managementLockCol']), $this->options['sessionId']); - } - - return $this->managementLock; - } - - /** - * @return Storage - */ - public function getCalendarStorage() - { - if (false == $this->calendarStorage) { - $collection = $this->getCollection($this->options['calendarCol']); - $hydrator = new Hydrator(function($values) { - return ModelClassFactory::getClass($values); - }); - - $this->calendarStorage = new Storage($collection, $hydrator); - } - - return $this->calendarStorage; - } - - /** - * @return Storage - */ - public function getTriggerStorage() - { - if (false == $this->triggerStorage) { - $collection = $this->getCollection($this->options['triggerCol']); - $hydrator = new Hydrator(function($values) { - return ModelClassFactory::getClass($values); - }); - - $this->triggerStorage = new Storage($collection, $hydrator); - } - - return $this->triggerStorage; - } - - /** - * @return Storage - */ - public function getFiredTriggerStorage() - { - if (false == $this->firedTriggerStorage) { - $collection = $this->getCollection($this->options['firedTriggerCol']); - $hydrator = new Hydrator(function($values) { - return ModelClassFactory::getClass($values); - }); - - $this->firedTriggerStorage = new Storage($collection, $hydrator); - } - - return $this->firedTriggerStorage; - } - - /** - * @return Storage - */ - public function getJobStorage() - { - if (false == $this->jobStorage) { - $collection = $this->getCollection($this->options['jobCol']); - $hydrator = new Hydrator(JobDetail::class); - - $this->jobStorage = new Storage($collection, $hydrator); - } - - return $this->jobStorage; - } - - /** - * @return Collection - */ - public function getPausedTriggerCol() - { - if (false == $this->pausedTriggerCol) { - $this->pausedTriggerCol = $this->getCollection($this->options['pausedTriggerCol']); - } - - return $this->pausedTriggerCol; - } - - public function dropIndexes() - { - try { - $this->getTriggerStorage()->getCollection()->dropIndexes(); - $this->getJobStorage()->getCollection()->dropIndexes(); - $this->getCalendarStorage()->getCollection()->dropIndexes(); - $this->getPausedTriggerCol()->dropIndexes(); - $this->getFiredTriggerStorage()->getCollection()->dropIndexes(); - } catch (RuntimeException $e) { - } - } - - public function createIndexes() - { - $this->getManagementLock()->createIndexes(); - - $this->getTriggerStorage()->getCollection()->createIndexes([ - [ - 'key' => ['name' => 1, 'group' => 1], 'unique' => true, - ], - [ - 'key' => ['group' => 1], - ], - [ - 'key' => ['jobName' => 1, 'jobGroup' => 1], - ], - [ - 'key' => ['jobGroup' => 1], - ], - [ - 'key' => ['calendarName' => 1], - ], - [ - 'key' => ['state' => 1], - ], - [ - 'key' => ['nextFireTime.unix' => 1], - ], - ]); - - $this->getJobStorage()->getCollection()->createIndexes([ - [ - 'key' => ['name' => 1, 'group' => 1], 'unique' => true, - ], - [ - 'key' => ['group' => 1], - ], - ]); - - $this->getCalendarStorage()->getCollection()->createIndexes([ - [ - 'key' => ['name' => 1], 'unique' => true, - ], - ]); - - $this->getPausedTriggerCol()->createIndexes([ - [ - 'key' => ['groupName' => 1], - ], - ]); - - $this->getFiredTriggerStorage()->getCollection()->createIndexes([ - [ - 'key' => ['fireInstanceId' => 1], - ], - ]); - } -} diff --git a/pkg/quartz/Tests/Functional/Scheduler/Store/YadmStoreTest.php b/pkg/quartz/Tests/Functional/Scheduler/Store/YadmStoreTest.php deleted file mode 100644 index 2a8664a..0000000 --- a/pkg/quartz/Tests/Functional/Scheduler/Store/YadmStoreTest.php +++ /dev/null @@ -1,1442 +0,0 @@ - sprintf('mongodb://%s:%s', getenv('MONGODB_HOST'), getenv('MONGODB_PORT')), - 'dbName' => getenv('MONGODB_DB') - ]; - - $this->res = new YadmStoreResource($config); - $this->store = new YadmStore($this->res); - $this->store->initialize($this->createMock(StdScheduler::class)); - $this->store->clearAllSchedulingData(); - } - - public function testCouldInsertNewPausedGroupAndGetThem() - { - // guard - $this->assertEmpty($this->store->getPausedTriggerGroups()); - - // test - $this->store->insertPausedTriggerGroup('group1'); - $this->store->insertPausedTriggerGroup('group2'); - - $this->assertSame(['group1', 'group2'], $this->store->getPausedTriggerGroups()); - } - - public function testCouldDeletePausedGroup() - { - // guard - $this->store->insertPausedTriggerGroup('group1'); - $this->store->insertPausedTriggerGroup('group2'); - $this->assertSame(['group1', 'group2'], $this->store->getPausedTriggerGroups()); - - // test - $this->store->deletePausedTriggerGroup('group2'); - - $this->assertSame(['group1'], $this->store->getPausedTriggerGroups()); - } - - public function testShouldReturnGroupIsPausedOrNot() - { - // guard - $this->store->insertPausedTriggerGroup('group1'); - $this->assertSame(['group1'], $this->store->getPausedTriggerGroups()); - - // test - $this->assertTrue($this->store->isTriggerGroupPaused('group1')); - $this->assertFalse($this->store->isTriggerGroupPaused('group2')); - } - - public function testCouldStoreAndRetrieveCalendar() - { - // guard - $this->assertNull($this->store->retrieveCalendar('theCal')); - - // test - $calendar = new HolidayCalendar(); - $calendar->setDescription('theCalDescription'); - $this->store->storeCalendar('theCal', $calendar); - - $storedCalendar = $this->store->retrieveCalendar('theCal'); - - $this->assertInstanceOf(HolidayCalendar::class, $storedCalendar); - $this->assertSame('theCalDescription', $storedCalendar->getDescription()); - } - - public function testCouldRemoveCalendar() - { - $calendar = new HolidayCalendar(); - $this->store->storeCalendar('theCal', $calendar); - - // guard - $this->assertNotNull($this->store->retrieveCalendar('theCal')); - - // test - $this->assertTrue($this->store->removeCalendar('theCal')); - $this->assertNull($this->store->retrieveCalendar('theCal')); - } - - public function testThrowExceptionIfCalendarHasAssoiatedTriggers() - { - $calendar = new HolidayCalendar(); - - $trigger = new SimpleTrigger(); - $trigger->setKey(new Key('name', 'group')); - $trigger->setCalendarName('theCal'); - - $this->store->storeCalendar('theCal', $calendar); - $this->store->storeTrigger($trigger); - - $this->expectException(JobPersistenceException::class); - $this->expectExceptionMessage('Calendar cannot be removed if it referenced by a trigger!. calendar: "theCal"'); - - $this->store->removeCalendar('theCal'); - } - - public function testShouldThrowObjectAlreadyExistsWhenCalendarAlreadyExistsAndReplaceArgIsFalse() - { - $calendar = new HolidayCalendar(); - - $this->expectException(ObjectAlreadyExistsException::class); - $this->expectExceptionMessage('Calendar with name already exists: "theCal"'); - - $this->store->storeCalendar('theCal', $calendar); - $this->store->storeCalendar('theCal', $calendar); - } - - public function testShouldReplaceExistingCalendarWithNewOne() - { - $calendar = new HolidayCalendar(); - $calendar->setDescription('desc1'); - - $this->store->storeCalendar('theCal', $calendar); - // guard - $calendar = $this->store->retrieveCalendar('theCal'); - $this->assertSame('desc1', $calendar->getDescription()); - - // test - $calendar = new HolidayCalendar(); - $calendar->setDescription('desc2'); - - $this->store->storeCalendar('theCal', $calendar, true); - - $calendar = $this->store->retrieveCalendar('theCal'); - $this->assertSame('desc2', $calendar->getDescription()); - } - - public function testCouldStoreAndRetrieveTrigger() - { - $key = new Key('name', 'group'); - - // guard - $this->assertNull($this->store->retrieveTrigger($key)); - - // test - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setDescription('theDesc'); - - $this->store->storeTrigger($trigger); - - $this->assertInstanceOf(SimpleTrigger::class, $trigger = $this->store->retrieveTrigger($key)); - $this->assertSame('theDesc', $trigger->getDescription()); - $this->assertSame(Trigger::STATE_WAITING, $trigger->getState()); - } - - public function testShouldThrowObjectAlreadyExistsWhenTriggerExistsAndReplaceExistingIsFalse() - { - $trigger = new SimpleTrigger(); - $trigger->setKey(new Key('name', 'group')); - $trigger->setDescription('theDesc'); - - $this->store->storeTrigger($trigger); - - $this->expectException(ObjectAlreadyExistsException::class); - $this->expectExceptionMessage('Unable to store Trigger with name: "name" and group: "group", because one already exists with this identification.'); - - $this->store->storeTrigger($trigger); - } - - public function testShouldSetTriggerStatePausedIfGroupIsPaused() - { - $key = new Key('name', 'group'); - - // guard - $this->assertNull($this->store->retrieveTrigger($key)); - - // test - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - - $this->store->insertPausedTriggerGroup('group'); - $this->store->storeTrigger($trigger); - - $this->assertInstanceOf(SimpleTrigger::class, $trigger = $this->store->retrieveTrigger($key)); - $this->assertSame(Trigger::STATE_PAUSED, $trigger->getState()); - } - - public function testShouldSetTriggerStatePausedIfAllGroupsArePaused() - { - $key = new Key('name', 'group'); - - // guard - $this->assertNull($this->store->retrieveTrigger($key)); - - // test - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - - $this->store->insertPausedTriggerGroup(YadmStore::ALL_GROUPS_PAUSED); - $this->store->storeTrigger($trigger); - - $this->assertInstanceOf(SimpleTrigger::class, $trigger = $this->store->retrieveTrigger($key)); - $this->assertSame(Trigger::STATE_PAUSED, $trigger->getState()); - } - - public function testShouldReplaceTriggerWithNewTrigger() - { - $key = new Key('name', 'group'); - - // guard - $this->assertNull($this->store->retrieveTrigger($key)); - - // test - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setDescription('theDesc'); - - $this->store->storeTrigger($trigger); - $this->assertInstanceOf(SimpleTrigger::class, $trigger = $this->store->retrieveTrigger($key)); - $this->assertSame('theDesc', $trigger->getDescription()); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setDescription('theNewDesc'); - - $this->store->storeTrigger($trigger, true); - $this->assertInstanceOf(SimpleTrigger::class, $trigger = $this->store->retrieveTrigger($key)); - $this->assertSame('theNewDesc', $trigger->getDescription()); - } - - public function testCouldRemoveTrigger() - { - $key = new Key('name', 'group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setJobKey(new Key('name', 'group')); - - $this->store->storeTrigger($trigger); - - // guard - $this->assertNotNull($this->store->retrieveTrigger($key)); - - // test - $this->assertTrue($this->store->removeTrigger($key)); - $this->assertNull($this->store->retrieveTrigger($key)); - } - - public function testShouldRemoveTriggerAndJobIfNotDurable() - { - $key = new Key('name', 'group'); - $jobKey = new Key('job-name', 'job-group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setJobKey($jobKey); - - $job = new JobDetail(); - $job->setKey($jobKey); - $job->setDurable(false); - - $this->store->storeTrigger($trigger); - $this->store->storeJob($job); - - // guard - $this->assertNotNull($this->store->retrieveTrigger($key)); - $this->assertNotNull($this->store->retrieveJob($jobKey)); - - // test - $this->assertTrue($this->store->removeTrigger($key)); - - $this->assertNull($this->store->retrieveTrigger($key)); - $this->assertNull($this->store->retrieveJob($jobKey)); - } - - public function testShouldRemoveTriggerButNotJobIfJobIsDurable() - { - $key = new Key('name', 'group'); - $jobKey = new Key('job-name', 'job-group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setJobKey($jobKey); - - $job = new JobDetail(); - $job->setKey($jobKey); - $job->setDurable(true); - - $this->store->storeTrigger($trigger); - $this->store->storeJob($job); - - // guard - $this->assertNotNull($this->store->retrieveTrigger($key)); - $this->assertNotNull($this->store->retrieveJob($jobKey)); - - // test - $this->assertTrue($this->store->removeTrigger($key)); - - $this->assertNull($this->store->retrieveTrigger($key)); - $this->assertNotNull($this->store->retrieveJob($jobKey)); - } - - public function testShouldRemoveTriggerButNotJobIfJobReferencesToExistentTrigger() - { - $key1 = new Key('name1', 'group'); - $key2 = new Key('name2', 'group'); - $jobKey = new Key('job-name', 'job-group'); - - $trigger1 = new SimpleTrigger(); - $trigger1->setKey($key1); - $trigger1->setJobKey($jobKey); - - $trigger2 = new SimpleTrigger(); - $trigger2->setKey($key2); - $trigger2->setJobKey($jobKey); - - $job = new JobDetail(); - $job->setKey($jobKey); - $job->setDurable(false); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - - $this->store->storeJob($job); - - // guard - $this->assertNotNull($this->store->retrieveTrigger($key1)); - $this->assertNotNull($this->store->retrieveTrigger($key2)); - $this->assertNotNull($this->store->retrieveJob($jobKey)); - - // test - $this->assertTrue($this->store->removeTrigger($key1)); - - $this->assertNull($this->store->retrieveTrigger($key1)); - $this->assertNotNull($this->store->retrieveTrigger($key2)); - $this->assertNotNull($this->store->retrieveJob($jobKey)); - } - - public function testShouldRemoveManyTriggers() - { - $key1 = new Key('name1', 'group'); - $key2 = new Key('name2', 'group'); - - $trigger1 = new SimpleTrigger(); - $trigger1->setKey($key1); - $trigger1->setJobKey(new Key('job-name', 'job-group')); - - $trigger2 = new SimpleTrigger(); - $trigger2->setKey($key2); - $trigger2->setJobKey(new Key('job-name', 'job-group')); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - - // guard - $this->assertNotNull($this->store->retrieveTrigger($key1)); - $this->assertNotNull($this->store->retrieveTrigger($key2)); - - // test - $this->assertTrue($this->store->removeTriggers([$key2, $key1])); - - $this->assertNull($this->store->retrieveTrigger($key1)); - $this->assertNull($this->store->retrieveTrigger($key2)); - } - - public function testShouldThrowInvalidArgumentOnRemoveManyTriggersIfArrayContainsNotKeyInstance() - { - $this->expectException(\InvalidArgumentException::class); - - $this->store->removeTriggers([new \stdClass()]); - } - - public function testShouldReturnTrueIfTriggerExists() - { - $key = new Key('name', 'group'); - - // guard - $this->assertNull($this->store->retrieveTrigger($key)); - $this->assertFalse($this->store->checkTriggerExists($key)); - - // test - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - - $this->store->storeTrigger($trigger); - - $this->assertTrue($this->store->checkTriggerExists($key)); - } - - public function testCouldStoreAndRetrieveJob() - { - $key = new Key('name', 'group'); - - // guard - $this->assertNull($this->store->retrieveJob($key)); - - // test - $job = new JobDetail(); - $job->setKey($key); - $job->setDescription('theDesc'); - - $this->store->storeJob($job); - - $this->assertInstanceOf(JobDetail::class, $job = $this->store->retrieveJob($key)); - $this->assertSame('theDesc', $job->getDescription()); - } - - public function testShouldThrowObjectAlreadyExistsWhenJobExistsAndReplaceExistingIsFalse() - { - $job = new JobDetail(); - $job->setKey(new Key('name', 'group')); - $job->setDescription('theDesc'); - - $this->store->storeJob($job); - - $this->expectException(ObjectAlreadyExistsException::class); - $this->expectExceptionMessage('Unable to store Job : "group.name", because one already exists with this identification.'); - - $this->store->storeJob($job); - } - - public function testShouldReplaceJobWithNewJob() - { - $key = new Key('name', 'group'); - - // guard - $this->assertNull($this->store->retrieveJob($key)); - - // test - $job = new JobDetail(); - $job->setKey($key); - $job->setDescription('theDesc'); - - $this->store->storeJob($job); - $this->assertInstanceOf(JobDetail::class, $job = $this->store->retrieveJob($key)); - $this->assertSame('theDesc', $job->getDescription()); - - $job = new JobDetail(); - $job->setKey($key); - $job->setDescription('theNewDesc'); - - $this->store->storeJob($job, true); - $this->assertInstanceOf(JobDetail::class, $job = $this->store->retrieveJob($key)); - $this->assertSame('theNewDesc', $job->getDescription()); - } - - public function testCouldRemoveJob() - { - $key = new Key('name', 'group'); - - $job = new JobDetail(); - $job->setKey($key); - - $this->store->storeJob($job); - - // guard - $this->assertNotNull($this->store->retrieveJob($key)); - - // test - $this->assertTrue($this->store->removeJob($key)); - $this->assertNull($this->store->retrieveJob($key)); - } - - public function testShouldRemoveJobAndAssociatedTriggers() - { - $key1 = new Key('name1', 'group'); - $key2 = new Key('name2', 'group'); - $jobKey = new Key('job-name', 'job-group'); - - $trigger1 = new SimpleTrigger(); - $trigger1->setKey($key1); - $trigger1->setJobKey($jobKey); - - $trigger2 = new SimpleTrigger(); - $trigger2->setKey($key2); - $trigger2->setJobKey($jobKey); - - $job = new JobDetail(); - $job->setKey($jobKey); - $job->setDurable(false); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - $this->store->storeJob($job); - - // guard - $this->assertNotNull($this->store->retrieveJob($jobKey)); - $this->assertNotNull($this->store->retrieveTrigger($key1)); - $this->assertNotNull($this->store->retrieveTrigger($key2)); - - // test - $this->assertTrue($this->store->removeJob($jobKey)); - - $this->assertNull($this->store->retrieveJob($jobKey)); - $this->assertNull($this->store->retrieveTrigger($key1)); - $this->assertNull($this->store->retrieveTrigger($key2)); - } - - public function testShouldRemoveManyJobs() - { - $key1 = new Key('name1', 'group'); - $key2 = new Key('name2', 'group'); - - $job1 = new JobDetail(); - $job1->setKey($key1); - - $job2 = new JobDetail(); - $job2->setKey($key2); - - $this->store->storeJob($job1); - $this->store->storeJob($job2); - - // guard - $this->assertNotNull($this->store->retrieveJob($key1)); - $this->assertNotNull($this->store->retrieveJob($key2)); - - // test - $this->assertTrue($this->store->removeJobs([$key2, $key1])); - - $this->assertNull($this->store->retrieveJob($key1)); - $this->assertNull($this->store->retrieveJob($key2)); - } - - public function testShouldThrowInvalidArgumentOnRemoveManyJobsIfArrayContainsNotKeyInstance() - { - $this->expectException(\InvalidArgumentException::class); - - $this->store->removeJobs([new \stdClass()]); - } - - public function testShouldReturnTrueIfJobExists() - { - $key = new Key('name', 'group'); - - // guard - $this->assertNull($this->store->retrieveJob($key)); - $this->assertFalse($this->store->checkJobExists($key)); - - // test - $job = new JobDetail(); - $job->setKey($key); - - $this->store->storeJob($job); - - $this->assertTrue($this->store->checkJobExists($key)); - } - - public function testCouldStoreJobAndTrigger() - { - $key = new Key('name', 'group'); - $jobKey = new Key('job-name', 'job-group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - - $job = new JobDetail(); - $job->setKey($jobKey); - - // guard - $this->assertNull($this->store->retrieveTrigger($key)); - $this->assertNull($this->store->retrieveJob($jobKey)); - - // test - $this->store->storeJobAndTrigger($job, $trigger); - - $this->assertNotNull($this->store->retrieveTrigger($key)); - $this->assertNotNull($this->store->retrieveJob($jobKey)); - } - - public function testCouldPauseTrigger() - { - $key = new Key('name', 'group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - - $this->store->storeTrigger($trigger); - - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key)->getState()); - - $this->store->pauseTrigger($key); - - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key)->getState()); - } - - public function testOnPauseJobShouldPauseAllAssociatedTriggers() - { - $key1 = new Key('name1', 'group'); - $key2 = new Key('name2', 'group'); - $jobKey = new Key('job-name', 'job-group'); - - $trigger1 = new SimpleTrigger(); - $trigger1->setKey($key1); - $trigger1->setJobKey(clone $jobKey); - - $trigger2 = new SimpleTrigger(); - $trigger2->setKey($key2); - $trigger2->setJobKey(clone $jobKey); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); - - $this->store->pauseJob($jobKey); - - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key1)->getState()); - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key2)->getState()); - } - - public function testShouldResumeTrigger() - { - $key = new Key('name', 'group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setNextFireTime(new \DateTime('+1 minute')); - - $this->store->storeTrigger($trigger); - $this->store->pauseTrigger($key); - - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key)->getState()); - - $this->store->resumeTrigger($key); - - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key)->getState()); - } - - public function testOnResumeTriggerShouldUpdateMisfiredTrigger() - { - $key = new Key('name', 'group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setNextFireTime(new \DateTime('-1 hours')); - $trigger->setMisfireInstruction(SimpleTrigger::MISFIRE_INSTRUCTION_FIRE_NOW); - - $this->store->storeTrigger($trigger); - $this->store->pauseTrigger($key); - - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key)->getState()); - - $this->store->resumeTrigger($key); - - $trigger = $this->store->retrieveTrigger($key); - - $this->assertSame(Trigger::STATE_WAITING, $trigger->getState()); - // new next fire time was updated and it is closer to now - $this->assertEquals(time(), $trigger->getNextFireTime()->format('U'), '', 10); - } - - public function testOnResumeTriggerShouldUpdateMisfiredTriggerAndSetStateComplete() - { - $key = new Key('name', 'group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setNextFireTime(new \DateTime('-1 hours')); - $trigger->setEndTime(new \DateTime('-10 minutes')); - $trigger->setMisfireInstruction(SimpleTrigger::MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT); - - $this->store->storeTrigger($trigger); - $this->store->pauseTrigger($key); - - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key)->getState()); - - $this->store->resumeTrigger($key); - - $trigger = $this->store->retrieveTrigger($key); - - $this->assertSame(Trigger::STATE_COMPLETE, $trigger->getState()); - $this->assertNull($trigger->getNextFireTime()); - } - - public function testOnResumeJobShouldResumeAllAssociatedTriggers() - { - $key1 = new Key('name1', 'group'); - $key2 = new Key('name2', 'group'); - $jobKey = new Key('name', 'group'); - - $trigger1 = new SimpleTrigger(); - $trigger1->setKey($key1); - $trigger1->setJobKey($jobKey); - $trigger1->setNextFireTime(new \DateTime('+1 minute')); - - $trigger2 = new SimpleTrigger(); - $trigger2->setKey($key2); - $trigger2->setJobKey($jobKey); - $trigger2->setNextFireTime(new \DateTime('+1 minute')); - - $job = new JobDetail(); - $job->setKey($jobKey); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - $this->store->storeJob($job); - - $this->store->pauseJob($jobKey); - - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key1)->getState()); - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key2)->getState()); - - $this->store->resumeJob($jobKey); - - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); - } - - public function testShouldPauseAllTriggers() - { - $key1 = new Key('name1', 'group1'); - $key2 = new Key('name2', 'group2'); - - $trigger1 = new SimpleTrigger(); - $trigger1->setKey($key1); - $trigger1->setNextFireTime(new \DateTime('+1 minute')); - - $trigger2 = new SimpleTrigger(); - $trigger2->setKey($key2); - $trigger2->setNextFireTime(new \DateTime('+1 minute')); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); - $this->assertEmpty($this->store->getPausedTriggerGroups()); - - $this->store->pauseAll(); - - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key1)->getState()); - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key2)->getState()); - $this->assertSame(['group1', 'group2', '_$_ALL_GROUPS_PAUSED_$_'], $this->store->getPausedTriggerGroups()); - } - - public function testShouldResumeAllTriggers() - { - $key1 = new Key('name1', 'group1'); - $key2 = new Key('name2', 'group2'); - - $trigger1 = new SimpleTrigger(); - $trigger1->setKey($key1); - $trigger1->setNextFireTime(new \DateTime('+1 minute')); - - $trigger2 = new SimpleTrigger(); - $trigger2->setKey($key2); - $trigger2->setNextFireTime(new \DateTime('+1 minute')); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - - $this->store->pauseAll(); - - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key1)->getState()); - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key2)->getState()); - $this->assertSame(['group1', 'group2', '_$_ALL_GROUPS_PAUSED_$_'], $this->store->getPausedTriggerGroups()); - - $this->store->resumeAll(); - - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); - $this->assertEmpty($this->store->getPausedTriggerGroups()); - } - - public function testShouldReturnAllTriggersForThisJob() - { - $key1 = new Key('name1', 'group'); - $key2 = new Key('name2', 'group'); - $key3 = new Key('name3', 'group'); - $jobKey = new Key('name', 'group'); - - $trigger1 = new SimpleTrigger(); - $trigger1->setKey($key1); - $trigger1->setJobKey(clone $jobKey); - - $trigger2 = new SimpleTrigger(); - $trigger2->setKey($key2); - $trigger2->setJobKey(clone $jobKey); - - $trigger3 = new SimpleTrigger(); - $trigger3->setKey($key3); - - $job = new JobDetail(); - $job->setKey($jobKey); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - $this->store->storeTrigger($trigger3); - $this->store->storeJob($job); - - $triggers = $this->store->getTriggersForJob($jobKey); - - $this->assertCount(2, $triggers); - } - - public function testShouldReturnAllJobGroupNames() - { - $job1 = new JobDetail(); - $job1->setKey(new Key('name', 'group1')); - - $job2 = new JobDetail(); - $job2->setKey(new Key('name', 'group2')); - - $this->store->storeJob($job1); - $this->store->storeJob($job2); - - $this->assertEquals(['group1', 'group2'], $this->store->getJobGroupNames()); - } - - public function testShouldReturnAllTriggerGroupNames() - { - $trigger1 = new SimpleTrigger(); - $trigger1->setKey(new Key('name', 'group1')); - - $trigger2 = new SimpleTrigger(); - $trigger2->setKey(new Key('name', 'group2')); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - - $this->assertEquals(['group1', 'group2'], $this->store->getTriggerGroupNames()); - } - - public function testShouldReturnTriggerState() - { - $key = new Key('name', 'group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - - $this->store->storeTrigger($trigger); - $this->store->pauseTrigger($key); - - $this->assertEquals(Trigger::STATE_PAUSED, $this->store->getTriggerState($key)); - } - - public function testOnGetTriggerStateShouldThrowExceptionIfTriggerDoesNotExist() - { - $this->expectException(SchedulerException::class); - $this->expectExceptionMessage('There is no trigger with key: "group.name"'); - - $this->store->getTriggerState(new Key('name', 'group')); - } - - public function testShouldReturnAllCalendarNames() - { - $this->store->storeCalendar('cal1', new HolidayCalendar()); - $this->store->storeCalendar('cal2', new HolidayCalendar()); - - $this->assertEquals(['cal1', 'cal2'], $this->store->getCalendarNames()); - } - - public function testShouldResetTriggerFromErrorState() - { - $key = new Key('name', 'group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - - $this->store->storeTrigger($trigger); - - // force trigger error state - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $key->getName(), - 'group' => $key->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ERROR, - ] - ]); - - // guard - $this->assertSame(Trigger::STATE_ERROR, $this->store->retrieveTrigger($key)->getState()); - - // test - $this->store->resetTriggerFromErrorState($key); - - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key)->getState()); - } - - public function testOnResetTriggerFromErrorStateShouldSetStatePausedIfGroupPaused() - { - $key = new Key('name', 'group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - - $this->store->storeTrigger($trigger); - $this->store->insertPausedTriggerGroup('group'); - - // force trigger error state - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $key->getName(), - 'group' => $key->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ERROR, - ] - ]); - - // guard - $this->assertSame(Trigger::STATE_ERROR, $this->store->retrieveTrigger($key)->getState()); - - // test - $this->store->resetTriggerFromErrorState($key); - - $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key)->getState()); - } - - public function testOnResetTriggerFromErrorStateShouldThrowExceptionIfTriggerDoesNotExist() - { - $this->expectException(SchedulerException::class); - $this->expectExceptionMessage('There is no trigger with identity: "group.name"'); - - // test - $this->store->resetTriggerFromErrorState(new Key('name', 'group')); - } - - public function testShouldAcquireNextTriggers() - { - $key1 = new Key('name1', 'group'); - $key2 = new Key('name2', 'group'); - - $trigger1 = new SimpleTrigger(); - $trigger1->setNextFireTime(new \DateTime()); - $trigger1->setKey($key1); - - $trigger2 = new SimpleTrigger(); - $trigger2->setNextFireTime(new \DateTime('+10 minutes')); - $trigger2->setKey($key2); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); - - $triggers = $this->store->acquireNextTriggers(time() + 10, 10, 0); - - $this->assertCount(1, $triggers); - $this->assertSame((string) $key1, (string) $triggers[0]->getKey()); - $this->assertSame(Trigger::STATE_ACQUIRED, $this->store->retrieveTrigger($key1)->getState()); - } - - public function testShouldAcquireMisfiredTriggers() - { - $key1 = new Key('name1', 'group'); - $key2 = new Key('name2', 'group'); - $key3 = new Key('name3', 'group'); - - $trigger1 = new SimpleTrigger(); - $trigger1->setNextFireTime(new \DateTime('-1 hour')); - $trigger1->setKey($key1); - - $trigger2 = new SimpleTrigger(); - $trigger2->setNextFireTime(new \DateTime()); - $trigger2->setKey($key2); - - $trigger3 = new SimpleTrigger(); - $trigger3->setNextFireTime(new \DateTime('+1 hour')); - $trigger3->setKey($key3); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - $this->store->storeTrigger($trigger3); - - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key3)->getState()); - - $triggers = $this->store->acquireNextTriggers(time() + 10, 10, 0); - - $this->assertCount(2, $triggers); - $this->assertSame((string) $key2, (string) $triggers[0]->getKey()); - $this->assertSame((string) $key1, (string) $triggers[1]->getKey()); - $this->assertSame(Trigger::STATE_ACQUIRED, $this->store->retrieveTrigger($key2)->getState()); - $this->assertSame(Trigger::STATE_ACQUIRED, $this->store->retrieveTrigger($key1)->getState()); - } - - public function testShouldNotAcquireMisfiredTriggersIfThereIsNoFreeSpace() - { - $key1 = new Key('name1', 'group'); - $key2 = new Key('name2', 'group'); - $key3 = new Key('name3', 'group'); - - $trigger1 = new SimpleTrigger(); - $trigger1->setNextFireTime(new \DateTime('-1 hour')); - $trigger1->setKey($key1); - - $trigger2 = new SimpleTrigger(); - $trigger2->setNextFireTime(new \DateTime()); - $trigger2->setKey($key2); - - $trigger3 = new SimpleTrigger(); - $trigger3->setNextFireTime(new \DateTime('+1 hour')); - $trigger3->setKey($key3); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - $this->store->storeTrigger($trigger3); - - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); - $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key3)->getState()); - - $triggers = $this->store->acquireNextTriggers(time() + 10, 1, 0); // only one trigger - - $this->assertCount(1, $triggers); - $this->assertSame((string) $key2, (string) $triggers[0]->getKey()); - $this->assertSame(Trigger::STATE_ACQUIRED, $this->store->retrieveTrigger($key2)->getState()); - } - - public function testOnTriggerFiredShouldCreateFiredTrigger() - { - $key = new Key('name', 'group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setRepeatCount(0); - $trigger->setStartTime(new \DateTime()); - $trigger->setRepeatInterval(10); - $trigger->computeFirstFireTime(); - - $this->store->storeTrigger($trigger); - - // force acquired state - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $key->getName(), - 'group' => $key->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ACQUIRED, - ] - ]); - - $firedTriggers = $this->store->triggersFired([$trigger], time() + 30); - - $this->assertCount(1, $firedTriggers); - - $this->assertNotNull($this->store->retrieveFireTrigger($firedTriggers[0]->getFireInstanceId())); - } - - public function testOnTriggerFiredShouldCreateSeveralFiredTriggerIfNextFireTimeBeforeNoLaterThan() - { - $key = new Key('name', 'group'); - - $startTime = new \DateTime('2012-12-12 00:00:00'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setRepeatInterval(10); - $trigger->setRepeatCount(SimpleTrigger::REPEAT_INDEFINITELY); - $trigger->setStartTime($startTime); - $trigger->setNextFireTime($startTime); - $trigger->setMisfireInstruction(Trigger::MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY); - - $this->store->storeTrigger($trigger); - - // force acquired state - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $key->getName(), - 'group' => $key->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ACQUIRED, - ] - ]); - - $noLaterThan = ((int) $startTime->format('U')) + 25; - - $firedTriggers = $this->store->triggersFired([$trigger], $noLaterThan); - - $this->assertCount(3, $firedTriggers); - } - - public function testOnTriggerFiredShouldReturnEmptyIfThereIsNotTriggerWithKey() - { - $key = new Key('name', 'group'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - - // $this->store->storeTrigger($trigger); do not store this trigger - - $this->assertEmpty($this->store->triggersFired([$trigger], time() + 100)); - } - - public function testOnTriggerFiredShouldReturnEmptyIfTriggerStateIsNotAcquired() - { - $key = new Key('name', 'group'); - - $startTime = new \DateTime(); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setRepeatInterval(10); - $trigger->setRepeatCount(SimpleTrigger::REPEAT_INDEFINITELY); - $trigger->setStartTime($startTime); - $trigger->setNextFireTime($startTime); - $trigger->setMisfireInstruction(Trigger::MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY); - - $this->store->storeTrigger($trigger); - - $this->assertEmpty($this->store->triggersFired([$trigger], time() + 100)); - } - - public function testOnTriggerFiredShouldReturnEmptyIfCalendarWasNotFound() - { - $key = new Key('name', 'group'); - - $startTime = new \DateTime(); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setRepeatInterval(10); - $trigger->setRepeatCount(SimpleTrigger::REPEAT_INDEFINITELY); - $trigger->setStartTime($startTime); - $trigger->setNextFireTime($startTime); - $trigger->setMisfireInstruction(Trigger::MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY); - $trigger->setCalendarName('missing-calendar'); - - $this->store->storeTrigger($trigger); - - // force acquired state - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $key->getName(), - 'group' => $key->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ACQUIRED, - ] - ]); - - $this->assertEmpty($this->store->triggersFired([$trigger], time() + 100)); - } - - public function testOnTriggerFiredShouldSetStateCompletedIfTriggerNextFireTimeIsNull() - { - $key = new Key('name', 'group'); - - $startTime = new \DateTime(); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setRepeatInterval(10); - $trigger->setRepeatCount(0); - $trigger->setStartTime($startTime); - $trigger->setNextFireTime($startTime); - - $this->store->storeTrigger($trigger); - - // force acquired state - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $key->getName(), - 'group' => $key->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ACQUIRED, - ] - ]); - - $fireTriggers = $this->store->triggersFired([$trigger], time() + 100); - - $this->assertCount(1, $fireTriggers); - $this->assertSame(Trigger::STATE_COMPLETE, $this->store->retrieveTrigger($key)->getState()); - } - - public function testOnTriggerFiredShouldUpdateMisfiredTrigger() - { - $key = new Key('name', 'group'); - - $startTime = new \DateTime('-1 hour'); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setRepeatInterval(10); - $trigger->setRepeatCount(1); - $trigger->setStartTime($startTime); - $trigger->setNextFireTime($startTime); - $trigger->setMisfireInstruction(SimpleTrigger::MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT); - - $this->store->storeTrigger($trigger); - - // force acquired state - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $key->getName(), - 'group' => $key->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ACQUIRED, - ] - ]); - - $fireTriggers = $this->store->triggersFired([$trigger], time() + 100); - - // fires now and plus one repeat - $this->assertCount(2, $fireTriggers); - } - - public function testOnTriggerJobCompleteShouldRemoveTriggerAndFiredTrigger() - { - $key = new Key('name', 'group'); - - $startTime = new \DateTime(); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setJobKey(new Key('name', 'group')); - $trigger->setRepeatInterval(10); - $trigger->setRepeatCount(0); - $trigger->setStartTime($startTime); - $trigger->setNextFireTime($startTime); - - $this->store->storeTrigger($trigger); - - // force acquired state - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $key->getName(), - 'group' => $key->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ACQUIRED, - ] - ]); - - $fireTriggers = $this->store->triggersFired([$trigger], time() + 100); - - $this->assertCount(1, $fireTriggers); - - // test - $this->store->triggeredJobComplete($fireTriggers[0], new JobDetail(), CompletedExecutionInstruction::DELETE_TRIGGER); - - $this->assertNull($this->store->retrieveTrigger($key)); - $this->assertNotEmpty($fireTriggers[0]->getFireInstanceId()); - $this->assertNull($this->store->retrieveFireTrigger($fireTriggers[0]->getFireInstanceId())); - } - - public function testOnTriggerJobCompleteShouldSetTriggerStateCompleteAndDeleteFiredTrigger() - { - $key = new Key('name', 'group'); - - $startTime = new \DateTime(); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setRepeatInterval(10); - $trigger->setRepeatCount(0); - $trigger->setStartTime($startTime); - $trigger->setNextFireTime($startTime); - - $this->store->storeTrigger($trigger); - - // force acquired state - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $key->getName(), - 'group' => $key->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ACQUIRED, - ] - ]); - - $fireTriggers = $this->store->triggersFired([$trigger], time() + 100); - - $this->assertCount(1, $fireTriggers); - - // test - $this->store->triggeredJobComplete($fireTriggers[0], new JobDetail(), CompletedExecutionInstruction::SET_TRIGGER_COMPLETE); - - $this->assertSame(Trigger::STATE_COMPLETE, $this->store->retrieveTrigger($key)->getState()); - $this->assertNotEmpty($fireTriggers[0]->getFireInstanceId()); - $this->assertNull($this->store->retrieveFireTrigger($fireTriggers[0]->getFireInstanceId())); - } - - public function testOnTriggerJobCompleteShouldSetTriggerStateErrorAndDeleteFiredTrigger() - { - $key = new Key('name', 'group'); - - $startTime = new \DateTime(); - - $trigger = new SimpleTrigger(); - $trigger->setKey($key); - $trigger->setRepeatInterval(10); - $trigger->setRepeatCount(0); - $trigger->setStartTime($startTime); - $trigger->setNextFireTime($startTime); - - $this->store->storeTrigger($trigger); - - // force acquired state - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $key->getName(), - 'group' => $key->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ACQUIRED, - ] - ]); - - $fireTriggers = $this->store->triggersFired([$trigger], time() + 100); - - $this->assertCount(1, $fireTriggers); - - // test - $fireTriggers[0]->setErrorMessage('the error message'); - $this->store->triggeredJobComplete($fireTriggers[0], new JobDetail(), CompletedExecutionInstruction::SET_TRIGGER_ERROR); - - $trigger = $this->store->retrieveTrigger($key); - - $this->assertSame(Trigger::STATE_ERROR, $trigger->getState()); - $this->assertSame('the error message', $trigger->getErrorMessage()); - $this->assertNotEmpty($fireTriggers[0]->getFireInstanceId()); - $this->assertNull($this->store->retrieveFireTrigger($fireTriggers[0]->getFireInstanceId())); - } - - public function testOnTriggerJobCompleteShouldSetAllJobTriggersStateCompleteAndDeleteFiredTrigger() - { - $key1 = new Key('name1', 'group'); - $key2 = new Key('name2', 'group'); - $jobKey = new Key('job-name', 'group'); - - $startTime = new \DateTime(); - - $trigger1 = new SimpleTrigger(); - $trigger1->setKey($key1); - $trigger1->setJobKey(clone $jobKey); - $trigger1->setRepeatInterval(10); - $trigger1->setRepeatCount(0); - $trigger1->setStartTime($startTime); - $trigger1->setNextFireTime($startTime); - - $trigger2 = new SimpleTrigger(); - $trigger2->setKey($key2); - $trigger2->setJobKey(clone $jobKey); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - - // force acquired state - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $key1->getName(), - 'group' => $key1->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ACQUIRED, - ] - ]); - - $fireTriggers = $this->store->triggersFired([$trigger1], time() + 100); - - $this->assertCount(1, $fireTriggers); - - // test - $this->store->triggeredJobComplete($fireTriggers[0], new JobDetail(), CompletedExecutionInstruction::SET_ALL_JOB_TRIGGERS_COMPLETE); - - $this->assertSame(Trigger::STATE_COMPLETE, $this->store->retrieveTrigger($key1)->getState()); - $this->assertSame(Trigger::STATE_COMPLETE, $this->store->retrieveTrigger($key2)->getState()); - $this->assertNotEmpty($fireTriggers[0]->getFireInstanceId()); - $this->assertNull($this->store->retrieveFireTrigger($fireTriggers[0]->getFireInstanceId())); - } - - public function testOnTriggerJobCompleteShouldSetAllJobTriggersStateErrorAndDeleteFiredTrigger() - { - $key1 = new Key('name1', 'group'); - $key2 = new Key('name2', 'group'); - $jobKey = new Key('job-name', 'group'); - - $startTime = new \DateTime(); - - $trigger1 = new SimpleTrigger(); - $trigger1->setKey($key1); - $trigger1->setJobKey(clone $jobKey); - $trigger1->setRepeatInterval(10); - $trigger1->setRepeatCount(0); - $trigger1->setStartTime($startTime); - $trigger1->setNextFireTime($startTime); - - $trigger2 = new SimpleTrigger(); - $trigger2->setKey($key2); - $trigger2->setJobKey(clone $jobKey); - - $this->store->storeTrigger($trigger1); - $this->store->storeTrigger($trigger2); - - // force acquired state - $this->res->getTriggerStorage()->getCollection()->updateOne([ - 'name' => $key1->getName(), - 'group' => $key1->getGroup(), - ], [ - '$set' => [ - 'state' => Trigger::STATE_ACQUIRED, - ] - ]); - - $fireTriggers = $this->store->triggersFired([$trigger1], time() + 100); - - $this->assertCount(1, $fireTriggers); - - // test - $fireTriggers[0]->setErrorMessage('the error message'); - $this->store->triggeredJobComplete($fireTriggers[0], new JobDetail(), CompletedExecutionInstruction::SET_ALL_JOB_TRIGGERS_ERROR); - - - $trigger1 = $this->store->retrieveTrigger($key1); - $this->assertSame(Trigger::STATE_ERROR, $trigger1->getState()); - $this->assertSame('the error message', $trigger1->getErrorMessage()); - - $trigger2 = $this->store->retrieveTrigger($key2); - $this->assertSame(Trigger::STATE_ERROR, $trigger2->getState()); - $this->assertSame('the error message', $trigger2->getErrorMessage()); - - $this->assertNotEmpty($fireTriggers[0]->getFireInstanceId()); - $this->assertNull($this->store->retrieveFireTrigger($fireTriggers[0]->getFireInstanceId())); - } -} diff --git a/pkg/quartz/composer.json b/pkg/quartz/composer.json index d126218..71c4671 100644 --- a/pkg/quartz/composer.json +++ b/pkg/quartz/composer.json @@ -8,13 +8,16 @@ "require": { "php": "^7.1.3", "ramsey/uuid": "^2|^3.5", - "formapro/yadm": "^0.5", "g4/cron": "^0.1", "symfony/event-dispatcher": "^3.4|^4" }, "require-dev": { + "formapro/yadm": "^0.5.5", "phpunit/phpunit": "^5.5" }, + "suggest": { + "php-quartz/bridge": "Provides a Yadm storage, Enqueue integration and other extension." + }, "autoload": { "psr-4": { "Quartz\\": "" }, "exclude-from-classmap": [ diff --git a/pkg/quartz/examples/calendar-interval-trigger.php b/pkg/quartz/examples/calendar-interval-trigger.php index e060576..ce15a61 100644 --- a/pkg/quartz/examples/calendar-interval-trigger.php +++ b/pkg/quartz/examples/calendar-interval-trigger.php @@ -10,8 +10,8 @@ use Quartz\Scheduler\StdJobRunShellFactory; use Quartz\Core\TriggerBuilder; use Quartz\Scheduler\StdScheduler; -use Quartz\Scheduler\Store\YadmStore; -use Quartz\Scheduler\Store\YadmStoreResource; +use Quartz\Bridge\Yadm\YadmStore; +use Quartz\Bridge\Yadm\SimpleStoreResource; use Symfony\Component\EventDispatcher\EventDispatcher; require_once '../vendor/autoload.php'; @@ -39,7 +39,7 @@ public function execute(JobExecutionContext $context) ->withSchedule(CalendarIntervalScheduleBuilder::calendarIntervalSchedule()->withIntervalInSeconds(10)) ->build(); -$store = new YadmStore(new YadmStoreResource($config)); +$store = new YadmStore(new SimpleStoreResource($config)); $store->clearAllSchedulingData(); $scheduler = new StdScheduler($store, new StdJobRunShellFactory(new StdJobRunShell()), new SimpleJobFactory(), new EventDispatcher()); diff --git a/pkg/quartz/examples/cron-trigger.php b/pkg/quartz/examples/cron-trigger.php index 52d7ecd..283ee98 100644 --- a/pkg/quartz/examples/cron-trigger.php +++ b/pkg/quartz/examples/cron-trigger.php @@ -10,8 +10,8 @@ use Quartz\Scheduler\StdJobRunShellFactory; use Quartz\Core\TriggerBuilder; use Quartz\Scheduler\StdScheduler; -use Quartz\Scheduler\Store\YadmStore; -use Quartz\Scheduler\Store\YadmStoreResource; +use Quartz\Bridge\Yadm\YadmStore; +use Quartz\Bridge\Yadm\SimpleStoreResource; use Symfony\Component\EventDispatcher\EventDispatcher; require_once '../vendor/autoload.php'; @@ -39,7 +39,7 @@ public function execute(JobExecutionContext $context) ->withSchedule(CronScheduleBuilder::cronSchedule('*/5 * * * * *')) ->build(); -$store = new YadmStore(new YadmStoreResource($config)); +$store = new YadmStore(new SimpleStoreResource($config)); $store->clearAllSchedulingData(); $scheduler = new StdScheduler($store, new StdJobRunShellFactory(new StdJobRunShell()), new SimpleJobFactory(), new EventDispatcher()); diff --git a/pkg/quartz/examples/daily-interval-trigger.php b/pkg/quartz/examples/daily-interval-trigger.php index fc03698..80991ce 100644 --- a/pkg/quartz/examples/daily-interval-trigger.php +++ b/pkg/quartz/examples/daily-interval-trigger.php @@ -10,8 +10,8 @@ use Quartz\Scheduler\StdJobRunShellFactory; use Quartz\Core\TriggerBuilder; use Quartz\Scheduler\StdScheduler; -use Quartz\Scheduler\Store\YadmStore; -use Quartz\Scheduler\Store\YadmStoreResource; +use Quartz\Bridge\Yadm\YadmStore; +use Quartz\Bridge\Yadm\SimpleStoreResource; use Symfony\Component\EventDispatcher\EventDispatcher; $dir = realpath(dirname($_SERVER['PHP_SELF'])); @@ -40,7 +40,7 @@ public function execute(JobExecutionContext $context) ->withSchedule(DailyTimeIntervalScheduleBuilder::dailyTimeIntervalSchedule()->withIntervalInSeconds(10)) ->build(); -$store = new YadmStore(new YadmStoreResource($config)); +$store = new YadmStore(new SimpleStoreResource($config)); $store->clearAllSchedulingData(); $scheduler = new StdScheduler($store, new StdJobRunShellFactory(new StdJobRunShell()), new SimpleJobFactory(), new EventDispatcher()); diff --git a/pkg/quartz/examples/scheduler.php b/pkg/quartz/examples/scheduler.php index 68e5e18..509eb97 100644 --- a/pkg/quartz/examples/scheduler.php +++ b/pkg/quartz/examples/scheduler.php @@ -7,8 +7,8 @@ use Quartz\Scheduler\StdJobRunShell; use Quartz\Scheduler\StdJobRunShellFactory; use Quartz\Scheduler\StdScheduler; -use Quartz\Scheduler\Store\YadmStore; -use Quartz\Scheduler\Store\YadmStoreResource; +use Quartz\Bridge\Yadm\YadmStore; +use Quartz\Bridge\Yadm\SimpleStoreResource; use Symfony\Component\EventDispatcher\EventDispatcher; require_once '../vendor/autoload.php'; @@ -20,7 +20,7 @@ 'dbName' => getenv('MONGODB_DB') ]; -$store = new YadmStore(new YadmStoreResource($config)); +$store = new YadmStore(new SimpleStoreResource($config)); $store->clearAllSchedulingData(); class MyJob implements Job diff --git a/pkg/quartz/examples/simple-trigger.php b/pkg/quartz/examples/simple-trigger.php index 8ed1e8e..a0f9ac2 100644 --- a/pkg/quartz/examples/simple-trigger.php +++ b/pkg/quartz/examples/simple-trigger.php @@ -10,8 +10,8 @@ use Quartz\Scheduler\StdJobRunShellFactory; use Quartz\Core\TriggerBuilder; use Quartz\Scheduler\StdScheduler; -use Quartz\Scheduler\Store\YadmStore; -use Quartz\Scheduler\Store\YadmStoreResource; +use Quartz\Bridge\Yadm\YadmStore; +use Quartz\Bridge\Yadm\SimpleStoreResource; use Symfony\Component\EventDispatcher\EventDispatcher; require_once '../vendor/autoload.php'; @@ -39,7 +39,7 @@ public function execute(JobExecutionContext $context) ->withSchedule(SimpleScheduleBuilder::repeatSecondlyForever(5)) ->build(); -$store = new YadmStore(new YadmStoreResource($config)); +$store = new YadmStore(new SimpleStoreResource($config)); $store->clearAllSchedulingData(); $scheduler = new StdScheduler($store, new StdJobRunShellFactory(new StdJobRunShell()), new SimpleJobFactory(), new EventDispatcher()); From e14e47bafc5c690817e3d70f091fac1ba6490810 Mon Sep 17 00:00:00 2001 From: Maksim Kotlyar Date: Tue, 14 May 2019 13:26:16 +0300 Subject: [PATCH 2/4] Add yadm store, hydrator, simple and bundle resource and model storages. --- pkg/bridge/DI/QuartzConfiguration.php | 26 +- pkg/bridge/DI/QuartzExtension.php | 31 +- pkg/bridge/Scheduler/SchedulerFactory.php | 6 +- .../Scheduler/JobRunShellProcessorTest.php | 2 +- pkg/bridge/Tests/Yadm/YadmStoreTest.php | 1442 +++++++++++++++++ pkg/bridge/Yadm/BundleStoreResource.php | 93 ++ pkg/bridge/Yadm/CalendarStorage.php | 28 + pkg/bridge/Yadm/FiredTriggerStorage.php | 28 + pkg/bridge/Yadm/JobStorage.php | 29 + pkg/bridge/Yadm/ModelHydrator.php | 21 + pkg/bridge/Yadm/PausedTriggerStorage.php | 28 + pkg/bridge/Yadm/SimpleStoreResource.php | 163 ++ pkg/bridge/Yadm/StoreResource.php | 29 + pkg/bridge/Yadm/TriggerStorage.php | 34 + pkg/bridge/Yadm/YadmStore.php | 1238 ++++++++++++++ pkg/bridge/composer.json | 1 + 16 files changed, 3179 insertions(+), 20 deletions(-) create mode 100644 pkg/bridge/Tests/Yadm/YadmStoreTest.php create mode 100644 pkg/bridge/Yadm/BundleStoreResource.php create mode 100644 pkg/bridge/Yadm/CalendarStorage.php create mode 100644 pkg/bridge/Yadm/FiredTriggerStorage.php create mode 100644 pkg/bridge/Yadm/JobStorage.php create mode 100644 pkg/bridge/Yadm/ModelHydrator.php create mode 100644 pkg/bridge/Yadm/PausedTriggerStorage.php create mode 100644 pkg/bridge/Yadm/SimpleStoreResource.php create mode 100644 pkg/bridge/Yadm/StoreResource.php create mode 100644 pkg/bridge/Yadm/TriggerStorage.php create mode 100644 pkg/bridge/Yadm/YadmStore.php diff --git a/pkg/bridge/DI/QuartzConfiguration.php b/pkg/bridge/DI/QuartzConfiguration.php index 48b7235..8decaea 100644 --- a/pkg/bridge/DI/QuartzConfiguration.php +++ b/pkg/bridge/DI/QuartzConfiguration.php @@ -7,27 +7,33 @@ class QuartzConfiguration implements ConfigurationInterface { - /** - * {@inheritdoc} - */ public function getConfigTreeBuilder() { $tb = new TreeBuilder(); $rootNode = $tb->root('quartz'); $rootNode->children() - ->arrayNode('store')->addDefaultsIfNotSet()->children() + ->arrayNode('yadm_simple_store')->children() ->scalarNode('uri')->defaultValue('mongodb://localhost:27017')->end() ->variableNode('uriOptions')->defaultValue([])->end() ->variableNode('driverOptions')->defaultValue([])->end() ->scalarNode('sessionId')->defaultValue('quartz')->end() ->scalarNode('dbName')->defaultValue(null)->end() - ->scalarNode('managementLockCol')->defaultValue('managementLock')->end() - ->scalarNode('calendarCol')->defaultValue('calendar')->end() - ->scalarNode('triggerCol')->defaultValue('trigger')->end() - ->scalarNode('firedTriggerCol')->defaultValue('firedTrigger')->end() - ->scalarNode('jobCol')->defaultValue('job')->end() - ->scalarNode('pausedTriggerCol')->defaultValue('pausedTrigger')->end() + ->scalarNode('managementLockCol')->defaultValue('quartz_management_lock')->end() + ->scalarNode('calendarCol')->defaultValue('quartz_calendar')->end() + ->scalarNode('triggerCol')->defaultValue('quartz_trigger')->end() + ->scalarNode('firedTriggerCol')->defaultValue('quartz_fired_trigger')->end() + ->scalarNode('jobCol')->defaultValue('quartz_job')->end() + ->scalarNode('pausedTriggerCol')->defaultValue('quartz_paused_trigger')->end() + ->end()->end() + ->arrayNode('yadm_bundle_store')->children() + ->scalarNode('sessionId')->defaultValue('quartz')->end() + ->scalarNode('managementLockCol')->defaultValue('quartz_management_lock')->end() + ->scalarNode('calendarStorage')->defaultValue('quartz_calendar')->end() + ->scalarNode('triggerStorage')->defaultValue('quartz_trigger')->end() + ->scalarNode('firedTriggerStorage')->defaultValue('quartz_fired_trigger')->end() + ->scalarNode('jobStorage')->defaultValue('quartz_job')->end() + ->scalarNode('pausedTriggerStorage')->defaultValue('quartz_paused_trigger')->end() ->end()->end() ->integerNode('misfireThreshold')->min(10)->defaultValue(60)->end() ; diff --git a/pkg/bridge/DI/QuartzExtension.php b/pkg/bridge/DI/QuartzExtension.php index 7ad7742..5e5b268 100644 --- a/pkg/bridge/DI/QuartzExtension.php +++ b/pkg/bridge/DI/QuartzExtension.php @@ -7,12 +7,13 @@ use Quartz\Bridge\Scheduler\EnqueueJobRunShell; use Quartz\Bridge\Scheduler\JobRunShellProcessor; use Quartz\Bridge\Scheduler\RpcProtocol; +use Quartz\Bridge\Yadm\BundleStoreResource; +use Quartz\Bridge\Yadm\SimpleStoreResource; +use Quartz\Bridge\Yadm\YadmStore; use Quartz\Core\SimpleJobFactory; use Quartz\Scheduler\StdJobRunShell; use Quartz\Scheduler\StdJobRunShellFactory; use Quartz\Scheduler\StdScheduler; -use Quartz\Scheduler\Store\YadmStore; -use Quartz\Scheduler\Store\YadmStoreResource; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; use Symfony\Component\DependencyInjection\Reference; @@ -47,12 +48,30 @@ public function load(array $configs, ContainerBuilder $container) { $config = $this->processConfiguration(new QuartzConfiguration(), $configs); - $container->register($this->format('store_resource'), YadmStoreResource::class) - ->setArguments([$config['store']]) - ; + if (false == empty($config['yadm_simple_store']) && false == empty($config['yadm_bundle_store'])) { + throw new \LogicException('Either yadm_simple_store or yadm_bundle_store must be set. Both are set'); + } + if (empty($config['yadm_simple_store']) && empty($config['yadm_bundle_store'])) { + throw new \LogicException('Either yadm_simple_store or yadm_bundle_store must be set. None is set'); + } + + if (false == empty($config['yadm_simple_store'])) { + $container->register($this->format('store_resource'), SimpleStoreResource::class) + ->addArgument($config['yadm_simple_store']) + ; + } + + if (false == empty($config['yadm_bundle_store'])) { + $container->register($this->format('store_resource'), BundleStoreResource::class) + ->addArgument(new Reference('yadm.client_provider')) + ->addArgument(new Reference('yadm.collection_factory')) + ->addArgument(new Reference('yadm')) + ->addArgument($config['yadm_bundle_store']) + ; + } $container->register($this->format('store'), YadmStore::class) - ->setArguments([new Reference($this->format('store_resource'))]) + ->addArgument(new Reference($this->format('store_resource'))) ->addMethodCall('setMisfireThreshold', [$config['misfireThreshold']]) ; diff --git a/pkg/bridge/Scheduler/SchedulerFactory.php b/pkg/bridge/Scheduler/SchedulerFactory.php index 03759eb..2bf4825 100644 --- a/pkg/bridge/Scheduler/SchedulerFactory.php +++ b/pkg/bridge/Scheduler/SchedulerFactory.php @@ -5,6 +5,8 @@ use Quartz\Bridge\Enqueue\EnqueueRemoteTransport; use Quartz\Bridge\Enqueue\EnqueueRemoteTransportProcessor; use Quartz\Bridge\Enqueue\EnqueueResponseJob; +use Quartz\Bridge\Yadm\SimpleStoreResource; +use Quartz\Bridge\Yadm\YadmStore; use Quartz\Core\Scheduler; use Quartz\Core\SchedulerFactory as BaseSchedulerFactory; use Quartz\Core\SimpleJobFactory; @@ -12,8 +14,6 @@ use Quartz\Scheduler\StdJobRunShell; use Quartz\Scheduler\StdJobRunShellFactory; use Quartz\Scheduler\StdScheduler; -use Quartz\Scheduler\Store\YadmStore; -use Quartz\Scheduler\Store\YadmStoreResource; use Symfony\Component\EventDispatcher\EventDispatcher; class SchedulerFactory implements BaseSchedulerFactory @@ -134,7 +134,7 @@ public function getStore() { if (null == $this->store) { $config = isset($this->config['store']) ? $this->config['store'] : []; - $this->store = new YadmStore(new YadmStoreResource($config)); + $this->store = new YadmStore(new SimpleStoreResource($config)); } return $this->store; diff --git a/pkg/bridge/Tests/Scheduler/JobRunShellProcessorTest.php b/pkg/bridge/Tests/Scheduler/JobRunShellProcessorTest.php index 7b6b118..2853734 100644 --- a/pkg/bridge/Tests/Scheduler/JobRunShellProcessorTest.php +++ b/pkg/bridge/Tests/Scheduler/JobRunShellProcessorTest.php @@ -10,8 +10,8 @@ use Interop\Queue\Processor; use PHPUnit\Framework\TestCase; use Quartz\Bridge\Scheduler\JobRunShellProcessor; +use Quartz\Bridge\Yadm\YadmStore; use Quartz\Scheduler\StdJobRunShell; -use Quartz\Scheduler\Store\YadmStore; use Quartz\Triggers\SimpleTrigger; class JobRunShellProcessorTest extends TestCase diff --git a/pkg/bridge/Tests/Yadm/YadmStoreTest.php b/pkg/bridge/Tests/Yadm/YadmStoreTest.php new file mode 100644 index 0000000..b01dc00 --- /dev/null +++ b/pkg/bridge/Tests/Yadm/YadmStoreTest.php @@ -0,0 +1,1442 @@ + sprintf('mongodb://%s:%s', getenv('MONGODB_HOST'), getenv('MONGODB_PORT')), + 'dbName' => getenv('MONGODB_DB') + ]; + + $this->res = new SimpleStoreResource($config); + $this->store = new YadmStore($this->res); + $this->store->initialize($this->createMock(StdScheduler::class)); + $this->store->clearAllSchedulingData(); + } + + public function testCouldInsertNewPausedGroupAndGetThem() + { + // guard + $this->assertEmpty($this->store->getPausedTriggerGroups()); + + // test + $this->store->insertPausedTriggerGroup('group1'); + $this->store->insertPausedTriggerGroup('group2'); + + $this->assertSame(['group1', 'group2'], $this->store->getPausedTriggerGroups()); + } + + public function testCouldDeletePausedGroup() + { + // guard + $this->store->insertPausedTriggerGroup('group1'); + $this->store->insertPausedTriggerGroup('group2'); + $this->assertSame(['group1', 'group2'], $this->store->getPausedTriggerGroups()); + + // test + $this->store->deletePausedTriggerGroup('group2'); + + $this->assertSame(['group1'], $this->store->getPausedTriggerGroups()); + } + + public function testShouldReturnGroupIsPausedOrNot() + { + // guard + $this->store->insertPausedTriggerGroup('group1'); + $this->assertSame(['group1'], $this->store->getPausedTriggerGroups()); + + // test + $this->assertTrue($this->store->isTriggerGroupPaused('group1')); + $this->assertFalse($this->store->isTriggerGroupPaused('group2')); + } + + public function testCouldStoreAndRetrieveCalendar() + { + // guard + $this->assertNull($this->store->retrieveCalendar('theCal')); + + // test + $calendar = new HolidayCalendar(); + $calendar->setDescription('theCalDescription'); + $this->store->storeCalendar('theCal', $calendar); + + $storedCalendar = $this->store->retrieveCalendar('theCal'); + + $this->assertInstanceOf(HolidayCalendar::class, $storedCalendar); + $this->assertSame('theCalDescription', $storedCalendar->getDescription()); + } + + public function testCouldRemoveCalendar() + { + $calendar = new HolidayCalendar(); + $this->store->storeCalendar('theCal', $calendar); + + // guard + $this->assertNotNull($this->store->retrieveCalendar('theCal')); + + // test + $this->assertTrue($this->store->removeCalendar('theCal')); + $this->assertNull($this->store->retrieveCalendar('theCal')); + } + + public function testThrowExceptionIfCalendarHasAssoiatedTriggers() + { + $calendar = new HolidayCalendar(); + + $trigger = new SimpleTrigger(); + $trigger->setKey(new Key('name', 'group')); + $trigger->setCalendarName('theCal'); + + $this->store->storeCalendar('theCal', $calendar); + $this->store->storeTrigger($trigger); + + $this->expectException(JobPersistenceException::class); + $this->expectExceptionMessage('Calendar cannot be removed if it referenced by a trigger!. calendar: "theCal"'); + + $this->store->removeCalendar('theCal'); + } + + public function testShouldThrowObjectAlreadyExistsWhenCalendarAlreadyExistsAndReplaceArgIsFalse() + { + $calendar = new HolidayCalendar(); + + $this->expectException(ObjectAlreadyExistsException::class); + $this->expectExceptionMessage('Calendar with name already exists: "theCal"'); + + $this->store->storeCalendar('theCal', $calendar); + $this->store->storeCalendar('theCal', $calendar); + } + + public function testShouldReplaceExistingCalendarWithNewOne() + { + $calendar = new HolidayCalendar(); + $calendar->setDescription('desc1'); + + $this->store->storeCalendar('theCal', $calendar); + // guard + $calendar = $this->store->retrieveCalendar('theCal'); + $this->assertSame('desc1', $calendar->getDescription()); + + // test + $calendar = new HolidayCalendar(); + $calendar->setDescription('desc2'); + + $this->store->storeCalendar('theCal', $calendar, true); + + $calendar = $this->store->retrieveCalendar('theCal'); + $this->assertSame('desc2', $calendar->getDescription()); + } + + public function testCouldStoreAndRetrieveTrigger() + { + $key = new Key('name', 'group'); + + // guard + $this->assertNull($this->store->retrieveTrigger($key)); + + // test + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setDescription('theDesc'); + + $this->store->storeTrigger($trigger); + + $this->assertInstanceOf(SimpleTrigger::class, $trigger = $this->store->retrieveTrigger($key)); + $this->assertSame('theDesc', $trigger->getDescription()); + $this->assertSame(Trigger::STATE_WAITING, $trigger->getState()); + } + + public function testShouldThrowObjectAlreadyExistsWhenTriggerExistsAndReplaceExistingIsFalse() + { + $trigger = new SimpleTrigger(); + $trigger->setKey(new Key('name', 'group')); + $trigger->setDescription('theDesc'); + + $this->store->storeTrigger($trigger); + + $this->expectException(ObjectAlreadyExistsException::class); + $this->expectExceptionMessage('Unable to store Trigger with name: "name" and group: "group", because one already exists with this identification.'); + + $this->store->storeTrigger($trigger); + } + + public function testShouldSetTriggerStatePausedIfGroupIsPaused() + { + $key = new Key('name', 'group'); + + // guard + $this->assertNull($this->store->retrieveTrigger($key)); + + // test + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + + $this->store->insertPausedTriggerGroup('group'); + $this->store->storeTrigger($trigger); + + $this->assertInstanceOf(SimpleTrigger::class, $trigger = $this->store->retrieveTrigger($key)); + $this->assertSame(Trigger::STATE_PAUSED, $trigger->getState()); + } + + public function testShouldSetTriggerStatePausedIfAllGroupsArePaused() + { + $key = new Key('name', 'group'); + + // guard + $this->assertNull($this->store->retrieveTrigger($key)); + + // test + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + + $this->store->insertPausedTriggerGroup(YadmStore::ALL_GROUPS_PAUSED); + $this->store->storeTrigger($trigger); + + $this->assertInstanceOf(SimpleTrigger::class, $trigger = $this->store->retrieveTrigger($key)); + $this->assertSame(Trigger::STATE_PAUSED, $trigger->getState()); + } + + public function testShouldReplaceTriggerWithNewTrigger() + { + $key = new Key('name', 'group'); + + // guard + $this->assertNull($this->store->retrieveTrigger($key)); + + // test + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setDescription('theDesc'); + + $this->store->storeTrigger($trigger); + $this->assertInstanceOf(SimpleTrigger::class, $trigger = $this->store->retrieveTrigger($key)); + $this->assertSame('theDesc', $trigger->getDescription()); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setDescription('theNewDesc'); + + $this->store->storeTrigger($trigger, true); + $this->assertInstanceOf(SimpleTrigger::class, $trigger = $this->store->retrieveTrigger($key)); + $this->assertSame('theNewDesc', $trigger->getDescription()); + } + + public function testCouldRemoveTrigger() + { + $key = new Key('name', 'group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setJobKey(new Key('name', 'group')); + + $this->store->storeTrigger($trigger); + + // guard + $this->assertNotNull($this->store->retrieveTrigger($key)); + + // test + $this->assertTrue($this->store->removeTrigger($key)); + $this->assertNull($this->store->retrieveTrigger($key)); + } + + public function testShouldRemoveTriggerAndJobIfNotDurable() + { + $key = new Key('name', 'group'); + $jobKey = new Key('job-name', 'job-group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setJobKey($jobKey); + + $job = new JobDetail(); + $job->setKey($jobKey); + $job->setDurable(false); + + $this->store->storeTrigger($trigger); + $this->store->storeJob($job); + + // guard + $this->assertNotNull($this->store->retrieveTrigger($key)); + $this->assertNotNull($this->store->retrieveJob($jobKey)); + + // test + $this->assertTrue($this->store->removeTrigger($key)); + + $this->assertNull($this->store->retrieveTrigger($key)); + $this->assertNull($this->store->retrieveJob($jobKey)); + } + + public function testShouldRemoveTriggerButNotJobIfJobIsDurable() + { + $key = new Key('name', 'group'); + $jobKey = new Key('job-name', 'job-group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setJobKey($jobKey); + + $job = new JobDetail(); + $job->setKey($jobKey); + $job->setDurable(true); + + $this->store->storeTrigger($trigger); + $this->store->storeJob($job); + + // guard + $this->assertNotNull($this->store->retrieveTrigger($key)); + $this->assertNotNull($this->store->retrieveJob($jobKey)); + + // test + $this->assertTrue($this->store->removeTrigger($key)); + + $this->assertNull($this->store->retrieveTrigger($key)); + $this->assertNotNull($this->store->retrieveJob($jobKey)); + } + + public function testShouldRemoveTriggerButNotJobIfJobReferencesToExistentTrigger() + { + $key1 = new Key('name1', 'group'); + $key2 = new Key('name2', 'group'); + $jobKey = new Key('job-name', 'job-group'); + + $trigger1 = new SimpleTrigger(); + $trigger1->setKey($key1); + $trigger1->setJobKey($jobKey); + + $trigger2 = new SimpleTrigger(); + $trigger2->setKey($key2); + $trigger2->setJobKey($jobKey); + + $job = new JobDetail(); + $job->setKey($jobKey); + $job->setDurable(false); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + + $this->store->storeJob($job); + + // guard + $this->assertNotNull($this->store->retrieveTrigger($key1)); + $this->assertNotNull($this->store->retrieveTrigger($key2)); + $this->assertNotNull($this->store->retrieveJob($jobKey)); + + // test + $this->assertTrue($this->store->removeTrigger($key1)); + + $this->assertNull($this->store->retrieveTrigger($key1)); + $this->assertNotNull($this->store->retrieveTrigger($key2)); + $this->assertNotNull($this->store->retrieveJob($jobKey)); + } + + public function testShouldRemoveManyTriggers() + { + $key1 = new Key('name1', 'group'); + $key2 = new Key('name2', 'group'); + + $trigger1 = new SimpleTrigger(); + $trigger1->setKey($key1); + $trigger1->setJobKey(new Key('job-name', 'job-group')); + + $trigger2 = new SimpleTrigger(); + $trigger2->setKey($key2); + $trigger2->setJobKey(new Key('job-name', 'job-group')); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + + // guard + $this->assertNotNull($this->store->retrieveTrigger($key1)); + $this->assertNotNull($this->store->retrieveTrigger($key2)); + + // test + $this->assertTrue($this->store->removeTriggers([$key2, $key1])); + + $this->assertNull($this->store->retrieveTrigger($key1)); + $this->assertNull($this->store->retrieveTrigger($key2)); + } + + public function testShouldThrowInvalidArgumentOnRemoveManyTriggersIfArrayContainsNotKeyInstance() + { + $this->expectException(\InvalidArgumentException::class); + + $this->store->removeTriggers([new \stdClass()]); + } + + public function testShouldReturnTrueIfTriggerExists() + { + $key = new Key('name', 'group'); + + // guard + $this->assertNull($this->store->retrieveTrigger($key)); + $this->assertFalse($this->store->checkTriggerExists($key)); + + // test + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + + $this->store->storeTrigger($trigger); + + $this->assertTrue($this->store->checkTriggerExists($key)); + } + + public function testCouldStoreAndRetrieveJob() + { + $key = new Key('name', 'group'); + + // guard + $this->assertNull($this->store->retrieveJob($key)); + + // test + $job = new JobDetail(); + $job->setKey($key); + $job->setDescription('theDesc'); + + $this->store->storeJob($job); + + $this->assertInstanceOf(JobDetail::class, $job = $this->store->retrieveJob($key)); + $this->assertSame('theDesc', $job->getDescription()); + } + + public function testShouldThrowObjectAlreadyExistsWhenJobExistsAndReplaceExistingIsFalse() + { + $job = new JobDetail(); + $job->setKey(new Key('name', 'group')); + $job->setDescription('theDesc'); + + $this->store->storeJob($job); + + $this->expectException(ObjectAlreadyExistsException::class); + $this->expectExceptionMessage('Unable to store Job : "group.name", because one already exists with this identification.'); + + $this->store->storeJob($job); + } + + public function testShouldReplaceJobWithNewJob() + { + $key = new Key('name', 'group'); + + // guard + $this->assertNull($this->store->retrieveJob($key)); + + // test + $job = new JobDetail(); + $job->setKey($key); + $job->setDescription('theDesc'); + + $this->store->storeJob($job); + $this->assertInstanceOf(JobDetail::class, $job = $this->store->retrieveJob($key)); + $this->assertSame('theDesc', $job->getDescription()); + + $job = new JobDetail(); + $job->setKey($key); + $job->setDescription('theNewDesc'); + + $this->store->storeJob($job, true); + $this->assertInstanceOf(JobDetail::class, $job = $this->store->retrieveJob($key)); + $this->assertSame('theNewDesc', $job->getDescription()); + } + + public function testCouldRemoveJob() + { + $key = new Key('name', 'group'); + + $job = new JobDetail(); + $job->setKey($key); + + $this->store->storeJob($job); + + // guard + $this->assertNotNull($this->store->retrieveJob($key)); + + // test + $this->assertTrue($this->store->removeJob($key)); + $this->assertNull($this->store->retrieveJob($key)); + } + + public function testShouldRemoveJobAndAssociatedTriggers() + { + $key1 = new Key('name1', 'group'); + $key2 = new Key('name2', 'group'); + $jobKey = new Key('job-name', 'job-group'); + + $trigger1 = new SimpleTrigger(); + $trigger1->setKey($key1); + $trigger1->setJobKey($jobKey); + + $trigger2 = new SimpleTrigger(); + $trigger2->setKey($key2); + $trigger2->setJobKey($jobKey); + + $job = new JobDetail(); + $job->setKey($jobKey); + $job->setDurable(false); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + $this->store->storeJob($job); + + // guard + $this->assertNotNull($this->store->retrieveJob($jobKey)); + $this->assertNotNull($this->store->retrieveTrigger($key1)); + $this->assertNotNull($this->store->retrieveTrigger($key2)); + + // test + $this->assertTrue($this->store->removeJob($jobKey)); + + $this->assertNull($this->store->retrieveJob($jobKey)); + $this->assertNull($this->store->retrieveTrigger($key1)); + $this->assertNull($this->store->retrieveTrigger($key2)); + } + + public function testShouldRemoveManyJobs() + { + $key1 = new Key('name1', 'group'); + $key2 = new Key('name2', 'group'); + + $job1 = new JobDetail(); + $job1->setKey($key1); + + $job2 = new JobDetail(); + $job2->setKey($key2); + + $this->store->storeJob($job1); + $this->store->storeJob($job2); + + // guard + $this->assertNotNull($this->store->retrieveJob($key1)); + $this->assertNotNull($this->store->retrieveJob($key2)); + + // test + $this->assertTrue($this->store->removeJobs([$key2, $key1])); + + $this->assertNull($this->store->retrieveJob($key1)); + $this->assertNull($this->store->retrieveJob($key2)); + } + + public function testShouldThrowInvalidArgumentOnRemoveManyJobsIfArrayContainsNotKeyInstance() + { + $this->expectException(\InvalidArgumentException::class); + + $this->store->removeJobs([new \stdClass()]); + } + + public function testShouldReturnTrueIfJobExists() + { + $key = new Key('name', 'group'); + + // guard + $this->assertNull($this->store->retrieveJob($key)); + $this->assertFalse($this->store->checkJobExists($key)); + + // test + $job = new JobDetail(); + $job->setKey($key); + + $this->store->storeJob($job); + + $this->assertTrue($this->store->checkJobExists($key)); + } + + public function testCouldStoreJobAndTrigger() + { + $key = new Key('name', 'group'); + $jobKey = new Key('job-name', 'job-group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + + $job = new JobDetail(); + $job->setKey($jobKey); + + // guard + $this->assertNull($this->store->retrieveTrigger($key)); + $this->assertNull($this->store->retrieveJob($jobKey)); + + // test + $this->store->storeJobAndTrigger($job, $trigger); + + $this->assertNotNull($this->store->retrieveTrigger($key)); + $this->assertNotNull($this->store->retrieveJob($jobKey)); + } + + public function testCouldPauseTrigger() + { + $key = new Key('name', 'group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + + $this->store->storeTrigger($trigger); + + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key)->getState()); + + $this->store->pauseTrigger($key); + + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key)->getState()); + } + + public function testOnPauseJobShouldPauseAllAssociatedTriggers() + { + $key1 = new Key('name1', 'group'); + $key2 = new Key('name2', 'group'); + $jobKey = new Key('job-name', 'job-group'); + + $trigger1 = new SimpleTrigger(); + $trigger1->setKey($key1); + $trigger1->setJobKey(clone $jobKey); + + $trigger2 = new SimpleTrigger(); + $trigger2->setKey($key2); + $trigger2->setJobKey(clone $jobKey); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); + + $this->store->pauseJob($jobKey); + + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key1)->getState()); + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key2)->getState()); + } + + public function testShouldResumeTrigger() + { + $key = new Key('name', 'group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setNextFireTime(new \DateTime('+1 minute')); + + $this->store->storeTrigger($trigger); + $this->store->pauseTrigger($key); + + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key)->getState()); + + $this->store->resumeTrigger($key); + + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key)->getState()); + } + + public function testOnResumeTriggerShouldUpdateMisfiredTrigger() + { + $key = new Key('name', 'group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setNextFireTime(new \DateTime('-1 hours')); + $trigger->setMisfireInstruction(SimpleTrigger::MISFIRE_INSTRUCTION_FIRE_NOW); + + $this->store->storeTrigger($trigger); + $this->store->pauseTrigger($key); + + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key)->getState()); + + $this->store->resumeTrigger($key); + + $trigger = $this->store->retrieveTrigger($key); + + $this->assertSame(Trigger::STATE_WAITING, $trigger->getState()); + // new next fire time was updated and it is closer to now + $this->assertEquals(time(), $trigger->getNextFireTime()->format('U'), '', 10); + } + + public function testOnResumeTriggerShouldUpdateMisfiredTriggerAndSetStateComplete() + { + $key = new Key('name', 'group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setNextFireTime(new \DateTime('-1 hours')); + $trigger->setEndTime(new \DateTime('-10 minutes')); + $trigger->setMisfireInstruction(SimpleTrigger::MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT); + + $this->store->storeTrigger($trigger); + $this->store->pauseTrigger($key); + + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key)->getState()); + + $this->store->resumeTrigger($key); + + $trigger = $this->store->retrieveTrigger($key); + + $this->assertSame(Trigger::STATE_COMPLETE, $trigger->getState()); + $this->assertNull($trigger->getNextFireTime()); + } + + public function testOnResumeJobShouldResumeAllAssociatedTriggers() + { + $key1 = new Key('name1', 'group'); + $key2 = new Key('name2', 'group'); + $jobKey = new Key('name', 'group'); + + $trigger1 = new SimpleTrigger(); + $trigger1->setKey($key1); + $trigger1->setJobKey($jobKey); + $trigger1->setNextFireTime(new \DateTime('+1 minute')); + + $trigger2 = new SimpleTrigger(); + $trigger2->setKey($key2); + $trigger2->setJobKey($jobKey); + $trigger2->setNextFireTime(new \DateTime('+1 minute')); + + $job = new JobDetail(); + $job->setKey($jobKey); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + $this->store->storeJob($job); + + $this->store->pauseJob($jobKey); + + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key1)->getState()); + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key2)->getState()); + + $this->store->resumeJob($jobKey); + + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); + } + + public function testShouldPauseAllTriggers() + { + $key1 = new Key('name1', 'group1'); + $key2 = new Key('name2', 'group2'); + + $trigger1 = new SimpleTrigger(); + $trigger1->setKey($key1); + $trigger1->setNextFireTime(new \DateTime('+1 minute')); + + $trigger2 = new SimpleTrigger(); + $trigger2->setKey($key2); + $trigger2->setNextFireTime(new \DateTime('+1 minute')); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); + $this->assertEmpty($this->store->getPausedTriggerGroups()); + + $this->store->pauseAll(); + + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key1)->getState()); + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key2)->getState()); + $this->assertSame(['group1', 'group2', '_$_ALL_GROUPS_PAUSED_$_'], $this->store->getPausedTriggerGroups()); + } + + public function testShouldResumeAllTriggers() + { + $key1 = new Key('name1', 'group1'); + $key2 = new Key('name2', 'group2'); + + $trigger1 = new SimpleTrigger(); + $trigger1->setKey($key1); + $trigger1->setNextFireTime(new \DateTime('+1 minute')); + + $trigger2 = new SimpleTrigger(); + $trigger2->setKey($key2); + $trigger2->setNextFireTime(new \DateTime('+1 minute')); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + + $this->store->pauseAll(); + + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key1)->getState()); + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key2)->getState()); + $this->assertSame(['group1', 'group2', '_$_ALL_GROUPS_PAUSED_$_'], $this->store->getPausedTriggerGroups()); + + $this->store->resumeAll(); + + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); + $this->assertEmpty($this->store->getPausedTriggerGroups()); + } + + public function testShouldReturnAllTriggersForThisJob() + { + $key1 = new Key('name1', 'group'); + $key2 = new Key('name2', 'group'); + $key3 = new Key('name3', 'group'); + $jobKey = new Key('name', 'group'); + + $trigger1 = new SimpleTrigger(); + $trigger1->setKey($key1); + $trigger1->setJobKey(clone $jobKey); + + $trigger2 = new SimpleTrigger(); + $trigger2->setKey($key2); + $trigger2->setJobKey(clone $jobKey); + + $trigger3 = new SimpleTrigger(); + $trigger3->setKey($key3); + + $job = new JobDetail(); + $job->setKey($jobKey); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + $this->store->storeTrigger($trigger3); + $this->store->storeJob($job); + + $triggers = $this->store->getTriggersForJob($jobKey); + + $this->assertCount(2, $triggers); + } + + public function testShouldReturnAllJobGroupNames() + { + $job1 = new JobDetail(); + $job1->setKey(new Key('name', 'group1')); + + $job2 = new JobDetail(); + $job2->setKey(new Key('name', 'group2')); + + $this->store->storeJob($job1); + $this->store->storeJob($job2); + + $this->assertEquals(['group1', 'group2'], $this->store->getJobGroupNames()); + } + + public function testShouldReturnAllTriggerGroupNames() + { + $trigger1 = new SimpleTrigger(); + $trigger1->setKey(new Key('name', 'group1')); + + $trigger2 = new SimpleTrigger(); + $trigger2->setKey(new Key('name', 'group2')); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + + $this->assertEquals(['group1', 'group2'], $this->store->getTriggerGroupNames()); + } + + public function testShouldReturnTriggerState() + { + $key = new Key('name', 'group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + + $this->store->storeTrigger($trigger); + $this->store->pauseTrigger($key); + + $this->assertEquals(Trigger::STATE_PAUSED, $this->store->getTriggerState($key)); + } + + public function testOnGetTriggerStateShouldThrowExceptionIfTriggerDoesNotExist() + { + $this->expectException(SchedulerException::class); + $this->expectExceptionMessage('There is no trigger with key: "group.name"'); + + $this->store->getTriggerState(new Key('name', 'group')); + } + + public function testShouldReturnAllCalendarNames() + { + $this->store->storeCalendar('cal1', new HolidayCalendar()); + $this->store->storeCalendar('cal2', new HolidayCalendar()); + + $this->assertEquals(['cal1', 'cal2'], $this->store->getCalendarNames()); + } + + public function testShouldResetTriggerFromErrorState() + { + $key = new Key('name', 'group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + + $this->store->storeTrigger($trigger); + + // force trigger error state + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $key->getName(), + 'group' => $key->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ERROR, + ] + ]); + + // guard + $this->assertSame(Trigger::STATE_ERROR, $this->store->retrieveTrigger($key)->getState()); + + // test + $this->store->resetTriggerFromErrorState($key); + + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key)->getState()); + } + + public function testOnResetTriggerFromErrorStateShouldSetStatePausedIfGroupPaused() + { + $key = new Key('name', 'group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + + $this->store->storeTrigger($trigger); + $this->store->insertPausedTriggerGroup('group'); + + // force trigger error state + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $key->getName(), + 'group' => $key->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ERROR, + ] + ]); + + // guard + $this->assertSame(Trigger::STATE_ERROR, $this->store->retrieveTrigger($key)->getState()); + + // test + $this->store->resetTriggerFromErrorState($key); + + $this->assertSame(Trigger::STATE_PAUSED, $this->store->retrieveTrigger($key)->getState()); + } + + public function testOnResetTriggerFromErrorStateShouldThrowExceptionIfTriggerDoesNotExist() + { + $this->expectException(SchedulerException::class); + $this->expectExceptionMessage('There is no trigger with identity: "group.name"'); + + // test + $this->store->resetTriggerFromErrorState(new Key('name', 'group')); + } + + public function testShouldAcquireNextTriggers() + { + $key1 = new Key('name1', 'group'); + $key2 = new Key('name2', 'group'); + + $trigger1 = new SimpleTrigger(); + $trigger1->setNextFireTime(new \DateTime()); + $trigger1->setKey($key1); + + $trigger2 = new SimpleTrigger(); + $trigger2->setNextFireTime(new \DateTime('+10 minutes')); + $trigger2->setKey($key2); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); + + $triggers = $this->store->acquireNextTriggers(time() + 10, 10, 0); + + $this->assertCount(1, $triggers); + $this->assertSame((string) $key1, (string) $triggers[0]->getKey()); + $this->assertSame(Trigger::STATE_ACQUIRED, $this->store->retrieveTrigger($key1)->getState()); + } + + public function testShouldAcquireMisfiredTriggers() + { + $key1 = new Key('name1', 'group'); + $key2 = new Key('name2', 'group'); + $key3 = new Key('name3', 'group'); + + $trigger1 = new SimpleTrigger(); + $trigger1->setNextFireTime(new \DateTime('-1 hour')); + $trigger1->setKey($key1); + + $trigger2 = new SimpleTrigger(); + $trigger2->setNextFireTime(new \DateTime()); + $trigger2->setKey($key2); + + $trigger3 = new SimpleTrigger(); + $trigger3->setNextFireTime(new \DateTime('+1 hour')); + $trigger3->setKey($key3); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + $this->store->storeTrigger($trigger3); + + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key3)->getState()); + + $triggers = $this->store->acquireNextTriggers(time() + 10, 10, 0); + + $this->assertCount(2, $triggers); + $this->assertSame((string) $key2, (string) $triggers[0]->getKey()); + $this->assertSame((string) $key1, (string) $triggers[1]->getKey()); + $this->assertSame(Trigger::STATE_ACQUIRED, $this->store->retrieveTrigger($key2)->getState()); + $this->assertSame(Trigger::STATE_ACQUIRED, $this->store->retrieveTrigger($key1)->getState()); + } + + public function testShouldNotAcquireMisfiredTriggersIfThereIsNoFreeSpace() + { + $key1 = new Key('name1', 'group'); + $key2 = new Key('name2', 'group'); + $key3 = new Key('name3', 'group'); + + $trigger1 = new SimpleTrigger(); + $trigger1->setNextFireTime(new \DateTime('-1 hour')); + $trigger1->setKey($key1); + + $trigger2 = new SimpleTrigger(); + $trigger2->setNextFireTime(new \DateTime()); + $trigger2->setKey($key2); + + $trigger3 = new SimpleTrigger(); + $trigger3->setNextFireTime(new \DateTime('+1 hour')); + $trigger3->setKey($key3); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + $this->store->storeTrigger($trigger3); + + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key1)->getState()); + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key2)->getState()); + $this->assertSame(Trigger::STATE_WAITING, $this->store->retrieveTrigger($key3)->getState()); + + $triggers = $this->store->acquireNextTriggers(time() + 10, 1, 0); // only one trigger + + $this->assertCount(1, $triggers); + $this->assertSame((string) $key2, (string) $triggers[0]->getKey()); + $this->assertSame(Trigger::STATE_ACQUIRED, $this->store->retrieveTrigger($key2)->getState()); + } + + public function testOnTriggerFiredShouldCreateFiredTrigger() + { + $key = new Key('name', 'group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setRepeatCount(0); + $trigger->setStartTime(new \DateTime()); + $trigger->setRepeatInterval(10); + $trigger->computeFirstFireTime(); + + $this->store->storeTrigger($trigger); + + // force acquired state + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $key->getName(), + 'group' => $key->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ACQUIRED, + ] + ]); + + $firedTriggers = $this->store->triggersFired([$trigger], time() + 30); + + $this->assertCount(1, $firedTriggers); + + $this->assertNotNull($this->store->retrieveFireTrigger($firedTriggers[0]->getFireInstanceId())); + } + + public function testOnTriggerFiredShouldCreateSeveralFiredTriggerIfNextFireTimeBeforeNoLaterThan() + { + $key = new Key('name', 'group'); + + $startTime = new \DateTime('2012-12-12 00:00:00'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setRepeatInterval(10); + $trigger->setRepeatCount(SimpleTrigger::REPEAT_INDEFINITELY); + $trigger->setStartTime($startTime); + $trigger->setNextFireTime($startTime); + $trigger->setMisfireInstruction(Trigger::MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY); + + $this->store->storeTrigger($trigger); + + // force acquired state + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $key->getName(), + 'group' => $key->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ACQUIRED, + ] + ]); + + $noLaterThan = ((int) $startTime->format('U')) + 25; + + $firedTriggers = $this->store->triggersFired([$trigger], $noLaterThan); + + $this->assertCount(3, $firedTriggers); + } + + public function testOnTriggerFiredShouldReturnEmptyIfThereIsNotTriggerWithKey() + { + $key = new Key('name', 'group'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + + // $this->store->storeTrigger($trigger); do not store this trigger + + $this->assertEmpty($this->store->triggersFired([$trigger], time() + 100)); + } + + public function testOnTriggerFiredShouldReturnEmptyIfTriggerStateIsNotAcquired() + { + $key = new Key('name', 'group'); + + $startTime = new \DateTime(); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setRepeatInterval(10); + $trigger->setRepeatCount(SimpleTrigger::REPEAT_INDEFINITELY); + $trigger->setStartTime($startTime); + $trigger->setNextFireTime($startTime); + $trigger->setMisfireInstruction(Trigger::MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY); + + $this->store->storeTrigger($trigger); + + $this->assertEmpty($this->store->triggersFired([$trigger], time() + 100)); + } + + public function testOnTriggerFiredShouldReturnEmptyIfCalendarWasNotFound() + { + $key = new Key('name', 'group'); + + $startTime = new \DateTime(); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setRepeatInterval(10); + $trigger->setRepeatCount(SimpleTrigger::REPEAT_INDEFINITELY); + $trigger->setStartTime($startTime); + $trigger->setNextFireTime($startTime); + $trigger->setMisfireInstruction(Trigger::MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY); + $trigger->setCalendarName('missing-calendar'); + + $this->store->storeTrigger($trigger); + + // force acquired state + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $key->getName(), + 'group' => $key->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ACQUIRED, + ] + ]); + + $this->assertEmpty($this->store->triggersFired([$trigger], time() + 100)); + } + + public function testOnTriggerFiredShouldSetStateCompletedIfTriggerNextFireTimeIsNull() + { + $key = new Key('name', 'group'); + + $startTime = new \DateTime(); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setRepeatInterval(10); + $trigger->setRepeatCount(0); + $trigger->setStartTime($startTime); + $trigger->setNextFireTime($startTime); + + $this->store->storeTrigger($trigger); + + // force acquired state + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $key->getName(), + 'group' => $key->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ACQUIRED, + ] + ]); + + $fireTriggers = $this->store->triggersFired([$trigger], time() + 100); + + $this->assertCount(1, $fireTriggers); + $this->assertSame(Trigger::STATE_COMPLETE, $this->store->retrieveTrigger($key)->getState()); + } + + public function testOnTriggerFiredShouldUpdateMisfiredTrigger() + { + $key = new Key('name', 'group'); + + $startTime = new \DateTime('-1 hour'); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setRepeatInterval(10); + $trigger->setRepeatCount(1); + $trigger->setStartTime($startTime); + $trigger->setNextFireTime($startTime); + $trigger->setMisfireInstruction(SimpleTrigger::MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT); + + $this->store->storeTrigger($trigger); + + // force acquired state + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $key->getName(), + 'group' => $key->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ACQUIRED, + ] + ]); + + $fireTriggers = $this->store->triggersFired([$trigger], time() + 100); + + // fires now and plus one repeat + $this->assertCount(2, $fireTriggers); + } + + public function testOnTriggerJobCompleteShouldRemoveTriggerAndFiredTrigger() + { + $key = new Key('name', 'group'); + + $startTime = new \DateTime(); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setJobKey(new Key('name', 'group')); + $trigger->setRepeatInterval(10); + $trigger->setRepeatCount(0); + $trigger->setStartTime($startTime); + $trigger->setNextFireTime($startTime); + + $this->store->storeTrigger($trigger); + + // force acquired state + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $key->getName(), + 'group' => $key->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ACQUIRED, + ] + ]); + + $fireTriggers = $this->store->triggersFired([$trigger], time() + 100); + + $this->assertCount(1, $fireTriggers); + + // test + $this->store->triggeredJobComplete($fireTriggers[0], new JobDetail(), CompletedExecutionInstruction::DELETE_TRIGGER); + + $this->assertNull($this->store->retrieveTrigger($key)); + $this->assertNotEmpty($fireTriggers[0]->getFireInstanceId()); + $this->assertNull($this->store->retrieveFireTrigger($fireTriggers[0]->getFireInstanceId())); + } + + public function testOnTriggerJobCompleteShouldSetTriggerStateCompleteAndDeleteFiredTrigger() + { + $key = new Key('name', 'group'); + + $startTime = new \DateTime(); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setRepeatInterval(10); + $trigger->setRepeatCount(0); + $trigger->setStartTime($startTime); + $trigger->setNextFireTime($startTime); + + $this->store->storeTrigger($trigger); + + // force acquired state + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $key->getName(), + 'group' => $key->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ACQUIRED, + ] + ]); + + $fireTriggers = $this->store->triggersFired([$trigger], time() + 100); + + $this->assertCount(1, $fireTriggers); + + // test + $this->store->triggeredJobComplete($fireTriggers[0], new JobDetail(), CompletedExecutionInstruction::SET_TRIGGER_COMPLETE); + + $this->assertSame(Trigger::STATE_COMPLETE, $this->store->retrieveTrigger($key)->getState()); + $this->assertNotEmpty($fireTriggers[0]->getFireInstanceId()); + $this->assertNull($this->store->retrieveFireTrigger($fireTriggers[0]->getFireInstanceId())); + } + + public function testOnTriggerJobCompleteShouldSetTriggerStateErrorAndDeleteFiredTrigger() + { + $key = new Key('name', 'group'); + + $startTime = new \DateTime(); + + $trigger = new SimpleTrigger(); + $trigger->setKey($key); + $trigger->setRepeatInterval(10); + $trigger->setRepeatCount(0); + $trigger->setStartTime($startTime); + $trigger->setNextFireTime($startTime); + + $this->store->storeTrigger($trigger); + + // force acquired state + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $key->getName(), + 'group' => $key->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ACQUIRED, + ] + ]); + + $fireTriggers = $this->store->triggersFired([$trigger], time() + 100); + + $this->assertCount(1, $fireTriggers); + + // test + $fireTriggers[0]->setErrorMessage('the error message'); + $this->store->triggeredJobComplete($fireTriggers[0], new JobDetail(), CompletedExecutionInstruction::SET_TRIGGER_ERROR); + + $trigger = $this->store->retrieveTrigger($key); + + $this->assertSame(Trigger::STATE_ERROR, $trigger->getState()); + $this->assertSame('the error message', $trigger->getErrorMessage()); + $this->assertNotEmpty($fireTriggers[0]->getFireInstanceId()); + $this->assertNull($this->store->retrieveFireTrigger($fireTriggers[0]->getFireInstanceId())); + } + + public function testOnTriggerJobCompleteShouldSetAllJobTriggersStateCompleteAndDeleteFiredTrigger() + { + $key1 = new Key('name1', 'group'); + $key2 = new Key('name2', 'group'); + $jobKey = new Key('job-name', 'group'); + + $startTime = new \DateTime(); + + $trigger1 = new SimpleTrigger(); + $trigger1->setKey($key1); + $trigger1->setJobKey(clone $jobKey); + $trigger1->setRepeatInterval(10); + $trigger1->setRepeatCount(0); + $trigger1->setStartTime($startTime); + $trigger1->setNextFireTime($startTime); + + $trigger2 = new SimpleTrigger(); + $trigger2->setKey($key2); + $trigger2->setJobKey(clone $jobKey); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + + // force acquired state + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $key1->getName(), + 'group' => $key1->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ACQUIRED, + ] + ]); + + $fireTriggers = $this->store->triggersFired([$trigger1], time() + 100); + + $this->assertCount(1, $fireTriggers); + + // test + $this->store->triggeredJobComplete($fireTriggers[0], new JobDetail(), CompletedExecutionInstruction::SET_ALL_JOB_TRIGGERS_COMPLETE); + + $this->assertSame(Trigger::STATE_COMPLETE, $this->store->retrieveTrigger($key1)->getState()); + $this->assertSame(Trigger::STATE_COMPLETE, $this->store->retrieveTrigger($key2)->getState()); + $this->assertNotEmpty($fireTriggers[0]->getFireInstanceId()); + $this->assertNull($this->store->retrieveFireTrigger($fireTriggers[0]->getFireInstanceId())); + } + + public function testOnTriggerJobCompleteShouldSetAllJobTriggersStateErrorAndDeleteFiredTrigger() + { + $key1 = new Key('name1', 'group'); + $key2 = new Key('name2', 'group'); + $jobKey = new Key('job-name', 'group'); + + $startTime = new \DateTime(); + + $trigger1 = new SimpleTrigger(); + $trigger1->setKey($key1); + $trigger1->setJobKey(clone $jobKey); + $trigger1->setRepeatInterval(10); + $trigger1->setRepeatCount(0); + $trigger1->setStartTime($startTime); + $trigger1->setNextFireTime($startTime); + + $trigger2 = new SimpleTrigger(); + $trigger2->setKey($key2); + $trigger2->setJobKey(clone $jobKey); + + $this->store->storeTrigger($trigger1); + $this->store->storeTrigger($trigger2); + + // force acquired state + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $key1->getName(), + 'group' => $key1->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ACQUIRED, + ] + ]); + + $fireTriggers = $this->store->triggersFired([$trigger1], time() + 100); + + $this->assertCount(1, $fireTriggers); + + // test + $fireTriggers[0]->setErrorMessage('the error message'); + $this->store->triggeredJobComplete($fireTriggers[0], new JobDetail(), CompletedExecutionInstruction::SET_ALL_JOB_TRIGGERS_ERROR); + + + $trigger1 = $this->store->retrieveTrigger($key1); + $this->assertSame(Trigger::STATE_ERROR, $trigger1->getState()); + $this->assertSame('the error message', $trigger1->getErrorMessage()); + + $trigger2 = $this->store->retrieveTrigger($key2); + $this->assertSame(Trigger::STATE_ERROR, $trigger2->getState()); + $this->assertSame('the error message', $trigger2->getErrorMessage()); + + $this->assertNotEmpty($fireTriggers[0]->getFireInstanceId()); + $this->assertNull($this->store->retrieveFireTrigger($fireTriggers[0]->getFireInstanceId())); + } +} diff --git a/pkg/bridge/Yadm/BundleStoreResource.php b/pkg/bridge/Yadm/BundleStoreResource.php new file mode 100644 index 0000000..20752d9 --- /dev/null +++ b/pkg/bridge/Yadm/BundleStoreResource.php @@ -0,0 +1,93 @@ +options = array_replace([ + 'sessionId' => 'quartz', + 'managementLockCol' => 'quartz_management_lock', + 'calendarStorage' => 'quartz_calendar', + 'triggerStorage' => 'quartz_trigger', + 'firedTriggerStorage' => 'quartz_fired_trigger', + 'jobStorage' => 'quartz_job', + 'pausedTriggerStorage' => 'quartz_paused_trigger', + ], $options); + + $this->clientProvider = $clientProvider; + $this->collectionFactory = $collectionFactory; + $this->registry = $registry; + } + + public function getClient(): Client + { + return $this->getClientProvider()->getClient(); + } + + public function getClientProvider(): ClientProvider + { + return $this->clientProvider; + } + + public function getCollectionFactory(): CollectionFactory + { + return $this->collectionFactory; + } + + public function getManagementLock(): PessimisticLock + { + if (false == $this->managementLock) { + $collection = $this->getCollectionFactory()->create($this->options['managementLockCol']); + + $this->managementLock = new PessimisticLock($collection, $this->options['sessionId']); + } + + return $this->managementLock; + } + + public function getCalendarStorage(): CalendarStorage + { + return $this->registry->getStorage($this->options['calendarStorage']); + } + + public function getTriggerStorage(): TriggerStorage + { + return $this->registry->getStorage($this->options['triggerStorage']); + } + + public function getFiredTriggerStorage(): FiredTriggerStorage + { + return $this->registry->getStorage($this->options['firedTriggerStorage']); + } + + public function getJobStorage(): JobStorage + { + return $this->registry->getStorage($this->options['jobStorage']); + } + + public function getPausedTriggerStorage(): PausedTriggerStorage + { + return $this->registry->getStorage($this->options['pausedTriggerStorage']); + } +} diff --git a/pkg/bridge/Yadm/CalendarStorage.php b/pkg/bridge/Yadm/CalendarStorage.php new file mode 100644 index 0000000..c57e025 --- /dev/null +++ b/pkg/bridge/Yadm/CalendarStorage.php @@ -0,0 +1,28 @@ + 1], ['unique' => true]), + ]; + } + + public function getCreateCollectionOptions(): array + { + return []; + } +} \ No newline at end of file diff --git a/pkg/bridge/Yadm/FiredTriggerStorage.php b/pkg/bridge/Yadm/FiredTriggerStorage.php new file mode 100644 index 0000000..02a4da8 --- /dev/null +++ b/pkg/bridge/Yadm/FiredTriggerStorage.php @@ -0,0 +1,28 @@ + 1]), + ]; + } + + public function getCreateCollectionOptions(): array + { + return []; + } +} \ No newline at end of file diff --git a/pkg/bridge/Yadm/JobStorage.php b/pkg/bridge/Yadm/JobStorage.php new file mode 100644 index 0000000..2f2a099 --- /dev/null +++ b/pkg/bridge/Yadm/JobStorage.php @@ -0,0 +1,29 @@ + 1, 'group' => 1], ['unique' => true]), + new Index(['group' => 1]), + ]; + } + + public function getCreateCollectionOptions(): array + { + return []; + } +} diff --git a/pkg/bridge/Yadm/ModelHydrator.php b/pkg/bridge/Yadm/ModelHydrator.php new file mode 100644 index 0000000..1c3da48 --- /dev/null +++ b/pkg/bridge/Yadm/ModelHydrator.php @@ -0,0 +1,21 @@ +hydrate($values, build_object($class, $values)); + } +} \ No newline at end of file diff --git a/pkg/bridge/Yadm/PausedTriggerStorage.php b/pkg/bridge/Yadm/PausedTriggerStorage.php new file mode 100644 index 0000000..a4de817 --- /dev/null +++ b/pkg/bridge/Yadm/PausedTriggerStorage.php @@ -0,0 +1,28 @@ + 1]), + ]; + } + + public function getCreateCollectionOptions(): array + { + return []; + } +} \ No newline at end of file diff --git a/pkg/bridge/Yadm/SimpleStoreResource.php b/pkg/bridge/Yadm/SimpleStoreResource.php new file mode 100644 index 0000000..bceb006 --- /dev/null +++ b/pkg/bridge/Yadm/SimpleStoreResource.php @@ -0,0 +1,163 @@ +options = array_replace([ + 'uri' => 'mongodb://localhost:27017', + 'uriOptions' => [], + 'driverOptions' => [], + 'sessionId' => 'quartz', + 'dbName' => 'quartz', + 'managementLockCol' => 'managementLock', + 'calendarCol' => 'calendar', + 'triggerCol' => 'trigger', + 'firedTriggerCol' => 'firedTrigger', + 'jobCol' => 'job', + 'pausedTriggerCol' => 'pausedTrigger', + + ], $options); + } + + public function getClient(): Client + { + return $this->getClientProvider()->getClient(); + } + + public function getClientProvider(): ClientProvider + { + if (false == $this->clientProvider) { + $this->clientProvider = new ClientProvider($this->options['uri'], $this->options['uriOptions'], $this->options['driverOptions']); + } + + return $this->clientProvider; + } + + public function getCollectionFactory(): CollectionFactory + { + if (false == $this->collectionFactory) { + $this->collectionFactory = new CollectionFactory($this->getClientProvider(), $this->options['uri']); + } + + return $this->collectionFactory; + } + + public function getManagementLock(): PessimisticLock + { + if (false == $this->managementLock) { + $collection = $this->getCollectionFactory()->create($this->options['managementLockCol']); + + $this->managementLock = new PessimisticLock($collection, $this->options['sessionId']); + } + + return $this->managementLock; + } + + public function getCalendarStorage(): CalendarStorage + { + if (false == $this->calendarStorage) { + $this->calendarStorage = new CalendarStorage( + $this->options['calendarCol'], + $this->getCollectionFactory(), + new ModelHydrator() + ); + } + + return $this->calendarStorage; + } + + public function getTriggerStorage(): TriggerStorage + { + if (false == $this->triggerStorage) { + $this->triggerStorage = new TriggerStorage( + $this->options['triggerCol'], + $this->getCollectionFactory(), + new ModelHydrator() + ); + } + + return $this->triggerStorage; + } + + public function getFiredTriggerStorage(): FiredTriggerStorage + { + if (false == $this->firedTriggerStorage) { + $this->firedTriggerStorage = new FiredTriggerStorage( + $this->options['firedTriggerCol'], + $this->getCollectionFactory(), + new ModelHydrator() + ); + } + + return $this->firedTriggerStorage; + } + + public function getJobStorage(): JobStorage + { + if (false == $this->jobStorage) { + $this->jobStorage = new JobStorage( + $this->options['jobCol'], + $this->getCollectionFactory(), + new ModelHydrator() + ); + } + + return $this->jobStorage; + } + + public function getPausedTriggerStorage(): PausedTriggerStorage + { + if (false == $this->pausedTriggerStorage) { + $this->pausedTriggerStorage = new PausedTriggerStorage( + $this->options['pausedTriggerCol'], + $this->getCollectionFactory(), + new ModelHydrator() + ); + } + + return $this->pausedTriggerStorage; + } + + public function dropIndexes(): void + { + try { + $this->getTriggerStorage()->getCollection()->dropIndexes(); + $this->getJobStorage()->getCollection()->dropIndexes(); + $this->getCalendarStorage()->getCollection()->dropIndexes(); + $this->getPausedTriggerStorage()->getCollection()->dropIndexes(); + $this->getFiredTriggerStorage()->getCollection()->dropIndexes(); + } catch (RuntimeException $e) { + } + } + + public function createIndexes(): void + { + + } +} diff --git a/pkg/bridge/Yadm/StoreResource.php b/pkg/bridge/Yadm/StoreResource.php new file mode 100644 index 0000000..049eacd --- /dev/null +++ b/pkg/bridge/Yadm/StoreResource.php @@ -0,0 +1,29 @@ + 1, 'group' => 1], ['unique' => true]), + new Index(['group' => 1]), + new Index(['jobName' => 1, 'jobGroup' => 1]), + new Index(['jobGroup' => 1]), + new Index(['calendarName' => 1]), + new Index(['state' => 1]), + new Index(['nextFireTime.unix' => 1]), + ]; + } + + public function getCreateCollectionOptions(): array + { + return []; + } +} \ No newline at end of file diff --git a/pkg/bridge/Yadm/YadmStore.php b/pkg/bridge/Yadm/YadmStore.php new file mode 100644 index 0000000..178eec0 --- /dev/null +++ b/pkg/bridge/Yadm/YadmStore.php @@ -0,0 +1,1238 @@ +res = $res; + } + + /** + * {@inheritdoc} + */ + public function initialize(StdScheduler $scheduler) + { + $this->scheduler = $scheduler; + } + + /** + * @return int + */ + public function getMisfireThreshold() + { + return $this->misfireThreshold; + } + + /** + * @param int $misfireThreshold + */ + public function setMisfireThreshold($misfireThreshold) + { + if (false == is_int($misfireThreshold)) { + throw new \InvalidArgumentException('Expected misfire threshold is int'); + } + + $this->misfireThreshold = $misfireThreshold; + } + + /** + * {@inheritdoc} + */ + public function storeJobAndTrigger(JobDetail $newJob, Trigger $newTrigger) + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($newJob, $newTrigger) { + $this->doStoreJob($newJob); + $this->doStoreTrigger($newTrigger); + }); + } + + /** + * {@inheritdoc} + */ + public function storeJob(JobDetail $newJob, $replaceExisting = false) + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($newJob, $replaceExisting) { + $this->doStoreJob($newJob, $replaceExisting); + }); + } + + /** + * @param JobDetail $newJob + * @param bool $replaceExisting + * + * @throws JobPersistenceException + * @throws ObjectAlreadyExistsException + */ + protected function doStoreJob(JobDetail $newJob, $replaceExisting = false) + { + $jobKey = $newJob->getKey(); + $existingJob = $this->retrieveJob($jobKey); + + if ($existingJob && false == $replaceExisting) { + throw ObjectAlreadyExistsException::create($newJob); + } + + if ($existingJob) { + $id = get_object_id($existingJob); + $values = get_values($newJob); + + set_values($existingJob, $values); + set_object_id($existingJob, $id); + + $result = $this->res->getJobStorage()->update($existingJob, [ + 'name' => $jobKey->getName(), + 'group' => $jobKey->getGroup() + ], ['upsert' => false]); + + if ($result && (false == $result->isAcknowledged() || false == $result->getModifiedCount())) { + throw new JobPersistenceException(sprintf('Couldn\'t store job. Update failed: "%s"', $jobKey)); + } + } else { + $result = $this->res->getJobStorage()->update($newJob, [ + 'name' => $jobKey->getName(), + 'group' => $jobKey->getGroup() + ], ['upsert' => true]); + + if ($result && (false == $result->isAcknowledged() || false == $result->getUpsertedCount())) { + throw new JobPersistenceException(sprintf('Couldn\'t store job. Insert failed: "%s"', $jobKey)); + } + } + } + + /** + * {@inheritdoc} + */ + public function removeJob(Key $jobKey) + { + return $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($jobKey) { + return $this->doRemoveJob($jobKey); + }); + } + + /** + * @param Key $jobKey + * + * @return bool + * + * @throws JobPersistenceException + */ + protected function doRemoveJob(Key $jobKey) + { + if (false == $job = $this->retrieveJob($jobKey)) { + return false; + } + + $result = $this->res->getTriggerStorage()->getCollection()->deleteMany([ + 'jobName' => $jobKey->getName(), + 'jobGroup' => $jobKey->getGroup() + ]); + + if (false == $result->isAcknowledged()) { + throw new JobPersistenceException(sprintf('Couldn\'t remove job: "%s"', $jobKey)); + } + + $result = $this->res->getJobStorage()->delete($job); + + if (false == $result->isAcknowledged() || false == $result->getDeletedCount()) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function removeJobs(array $jobKeys) + { + foreach ($jobKeys as $key) { + if (false == $key instanceof Key) { + throw new \InvalidArgumentException(sprintf('Expected Key instance but got: "%s"', + is_object($key) ? get_class($key) : gettype($key))); + } + } + + return $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($jobKeys) { + $allFound = true; + + foreach ($jobKeys as $key) { + $allFound = $this->doRemoveJob($key) && $allFound; + } + + return $allFound; + }); + } + + /** + * {@inheritdoc} + */ + public function retrieveJob(Key $jobKey) + { + // no locks necessary for read... + return $this->res->getJobStorage()->findOne([ + 'name' => $jobKey->getName(), + 'group' => $jobKey->getGroup(), + ]); + } + + /** + * {@inheritdoc} + */ + public function storeTrigger(Trigger $newTrigger, $replaceExisting = false) + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($newTrigger, $replaceExisting) { + $this->doStoreTrigger($newTrigger, $replaceExisting, Trigger::STATE_WAITING); + }); + } + + /** + * @param Trigger $newTrigger + * @param bool $replaceExisting + * @param string $state + * + * @throws JobPersistenceException + * @throws ObjectAlreadyExistsException + */ + protected function doStoreTrigger(Trigger $newTrigger, $replaceExisting = false, $state = Trigger::STATE_WAITING) + { + $triggerKey = $newTrigger->getKey(); + $existingTrigger = $this->retrieveTrigger($triggerKey); + + if ($existingTrigger && false == $replaceExisting) { + throw ObjectAlreadyExistsException::create($newTrigger); + } + + $shouldBePaused = $this->isTriggerGroupPaused($triggerKey->getGroup()); + + if (false == $shouldBePaused) { + $shouldBePaused = $this->isTriggerGroupPaused(self::ALL_GROUPS_PAUSED); + + if ($shouldBePaused) { + $this->insertPausedTriggerGroup($triggerKey->getGroup()); + } + } + + if ($shouldBePaused && ($state == Trigger::STATE_WAITING || $state == Trigger::STATE_ACQUIRED)) { + $state = Trigger::STATE_PAUSED; + } + + $newTrigger->setState($state); + +// if (null == $job) { +// $job = $this->retrieveJob($newTrigger->getJobKey()); +// } +// +// if (null == $job) { +// throw new JobPersistenceException(sprintf('The job referenced by the trigger does not exist. trigger: "%s", job: "%s"', +// $triggerKey, $newTrigger->getJobKey())); +// } + + if ($existingTrigger) { + $id = get_object_id($existingTrigger); + $values = get_values($newTrigger); + + set_values($existingTrigger, $values); + set_object_id($existingTrigger, $id); + + $result = $this->res->getTriggerStorage()->update($existingTrigger, [ + 'name' => $triggerKey->getName(), + 'group' => $triggerKey->getGroup() + ], ['upsert' => false]); + + if ($result && (false == $result->isAcknowledged() || false == $result->getModifiedCount())) { + throw new JobPersistenceException(sprintf('Couldn\'t store trigger. Update failed: "%s"', $triggerKey)); + } + } else { + $result = $this->res->getTriggerStorage()->update($newTrigger, [ + 'name' => $triggerKey->getName(), + 'group' => $triggerKey->getGroup() + ], ['upsert' => true]); + + if ($result && (false == $result->isAcknowledged() || false == $result->getUpsertedCount())) { + throw new JobPersistenceException(sprintf('Couldn\'t store trigger. Insert failed: "%s"', $triggerKey)); + } + } + } + + /** + * {@inheritdoc} + */ + public function removeTrigger(Key $triggerKey) + { + return $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($triggerKey) { + return $this->doRemoveTrigger($triggerKey); + }); + } + + /** + * @param Key $triggerKey + * + * @return bool + * + * @throws JobPersistenceException + */ + protected function doRemoveTrigger(Key $triggerKey) + { + if (false == $trigger = $this->retrieveTrigger($triggerKey)) { + return false; + } + + $result = $this->res->getTriggerStorage()->delete($trigger); + + $removedTrigger = true; + if (false == $result->isAcknowledged() || false == $result->getDeletedCount()) { + $removedTrigger = false; + } + + $jobKey = $trigger->getJobKey(); + $job = $this->retrieveJob($jobKey); + + if ($job && false == $job->isDurable()) { + $numTriggers = $this->res->getTriggerStorage()->count([ + 'jobName' => $jobKey->getName(), + 'jobGroup' => $jobKey->getGroup(), + ]); + + if ($numTriggers == 0) { + // Don't call removeJob() because we don't want to check for triggers again. + $this->res->getJobStorage()->delete($job); + } + } + + return $removedTrigger; + } + + /** + * {@inheritdoc} + */ + public function removeTriggers(array $triggerKeys) + { + foreach ($triggerKeys as $key) { + if (false == $key instanceof Key) { + throw new \InvalidArgumentException(sprintf('Expected Key instance but got: "%s"', + is_object($key) ? get_class($key) : gettype($key))); + } + } + + return $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($triggerKeys) { + $allFound = true; + + foreach ($triggerKeys as $key) { + $allFound = $this->doRemoveTrigger($key) && $allFound; + } + + return $allFound; + }); + } + + public function replaceTrigger(Key $triggerKey, Trigger $newTrigger) + { + // TODO: Implement replaceTrigger() method. + } + + /** + * {@inheritdoc} + */ + public function retrieveTrigger(Key $triggerKey) + { + // no locks necessary for read... + return $this->res->getTriggerStorage()->findOne([ + 'name' => $triggerKey->getName(), + 'group' => $triggerKey->getGroup(), + ]); + } + + /** + * {@inheritdoc} + */ + public function checkJobExists(Key $jobKey) + { + // no locks necessary for read... + return (bool) $this->res->getJobStorage()->count([ + 'name' => $jobKey->getName(), + 'group' => $jobKey->getGroup(), + ]); + } + + /** + * {@inheritdoc} + */ + public function checkTriggerExists(Key $triggerKey) + { + // no locks necessary for read... + return (bool) $this->res->getTriggerStorage()->count([ + 'name' => $triggerKey->getName(), + 'group' => $triggerKey->getGroup(), + ]); + } + + /** + * {@inheritdoc} + */ + public function clearAllSchedulingData() + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () { + $this->doClearAllSchedulingData(); + }); + } + + public function doClearAllSchedulingData() + { + $this->res->getTriggerStorage()->getCollection()->deleteMany([]); + $this->res->getJobStorage()->getCollection()->deleteMany([]); + $this->res->getCalendarStorage()->getCollection()->deleteMany([]); + $this->res->getPausedTriggerStorage()->getCollection()->deleteMany([]); + $this->res->getFiredTriggerStorage()->getCollection()->deleteMany([]); + } + + /** + * {@inheritdoc} + */ + public function storeCalendar($name, Calendar $calendar, $replaceExisting = false, $updateTriggers = false) + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($name, $calendar, $replaceExisting, $updateTriggers) { + $this->doStoreCalendar($name, $calendar, $replaceExisting, $updateTriggers); + }); + } + + /** + * @param string $name + * @param Calendar $calendar + * @param bool $replaceExisting + * @param bool $updateTriggers + * + * @throws JobPersistenceException + * @throws ObjectAlreadyExistsException + */ + protected function doStoreCalendar($name, Calendar $calendar, $replaceExisting, $updateTriggers) + { + $existingCal = $this->retrieveCalendar($name); + + if ($existingCal && false == $replaceExisting) { + throw new ObjectAlreadyExistsException(sprintf('Calendar with name already exists: "%s"', $name)); + } + + if ($existingCal) { + $id = get_object_id($existingCal); + $values = get_values($calendar); + $values['name'] = $name; + + set_values($existingCal, $values); + set_object_id($existingCal, $id); + + $result = $this->res->getCalendarStorage()->update($existingCal, ['name' => $name], ['upsert' => false]); + + if ($result && (false == $result->isAcknowledged() || false == $result->getModifiedCount())) { + throw new JobPersistenceException(sprintf('Couldn\'t store calendar. Update failed: "%s"', $name)); + } + } else { + set_value($calendar, 'name', $name); + $result = $this->res->getCalendarStorage()->update($calendar, ['name' => $name], ['upsert' => true]); + + if ($result && (false == $result->isAcknowledged() || false == $result->getUpsertedCount())) { + throw new JobPersistenceException(sprintf('Couldn\'t store calendar. Insert failed: "%s"', $name)); + } + } + + if ($existingCal && $updateTriggers) { + /** @var Trigger $trigger */ + foreach ($this->res->getTriggerStorage()->find(['calendarName' => $name]) as $trigger) { + $trigger->updateWithNewCalendar($existingCal, $this->misfireThreshold); + $this->doStoreTrigger($trigger, true, Trigger::STATE_WAITING); + } + } + } + + /** + * {@inheritdoc} + */ + public function removeCalendar($calName) + { + return $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($calName) { + return $this->doRemoveCalendar($calName); + }); + } + + /** + * @param string $calName + * + * @return bool + * + * @throws JobPersistenceException + */ + protected function doRemoveCalendar($calName) + { + if ($this->res->getTriggerStorage()->count(['calendarName' => $calName])) { + throw new JobPersistenceException(sprintf('Calendar cannot be removed if it referenced by a trigger!. calendar: "%s"', $calName)); + } + + $result = $this->res->getCalendarStorage()->getCollection()->deleteOne(['name' => $calName]); + + return $result->isAcknowledged() && $result->getDeletedCount(); + } + + /** + * {@inheritdoc} + */ + public function retrieveCalendar($calName) + { + // no locks necessary for read... + return $this->res->getCalendarStorage()->findOne(['name' => $calName]); + } + + /** + * {@inheritdoc} + */ + public function pauseTrigger(Key $triggerKey) + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($triggerKey) { + $this->doPauseTrigger($triggerKey); + }); + } + + /** + * @param Key $triggerKey + * + * @throws JobPersistenceException + */ + protected function doPauseTrigger(Key $triggerKey) + { + if (false == $trigger = $this->retrieveTrigger($triggerKey)) { + return; + } + + if (false == in_array($trigger->getState(), [Trigger::STATE_WAITING, Trigger::STATE_ACQUIRED], true)) { + return; + } + + $trigger->setState(Trigger::STATE_PAUSED); + $result = $this->res->getTriggerStorage()->update($trigger); + + if (false == $result->isAcknowledged() || false == $result->getModifiedCount()) { + throw new JobPersistenceException(sprintf('Couldn\'t pause trigger: "%s"', $triggerKey)); + } + } + + public function pauseTriggers(GroupMatcher $matcher) + { + // TODO: Implement pauseTriggers() method. + } + + /** + * {@inheritdoc} + */ + public function pauseJob(Key $jobKey) + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($jobKey) { + $this->doPauseJob($jobKey); + }); + } + + /** + * @param Key $jobKey + */ + protected function doPauseJob(Key $jobKey) + { + foreach ($this->getTriggersForJob($jobKey) as $trigger) { + $this->doPauseTrigger($trigger->getKey()); + } + } + + public function pauseJobs(GroupMatcher $groupMatcher) + { + // TODO: Implement pauseJobs() method. + } + + /** + * {@inheritdoc} + */ + public function resumeTrigger(Key $triggerKey) + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($triggerKey) { + $this->doResumeTrigger($triggerKey); + }); + } + + /** + * @param Key $triggerKey + * + * @throws JobPersistenceException + */ + protected function doResumeTrigger(Key $triggerKey) + { + if (false == $trigger = $this->retrieveTrigger($triggerKey)) { + return; + } + + if (false == $trigger->getNextFireTime()) { + return; + } + + $misfired = false; + if (((int) $trigger->getNextFireTime()->format('U')) < time()) { + $misfired = $this->updateMisfiredTrigger($triggerKey, Trigger::STATE_WAITING); + } + + if (false == $misfired) { + $trigger->setState(Trigger::STATE_WAITING); + $result = $this->res->getTriggerStorage()->update($trigger); + + if (false == $result->isAcknowledged() || false == $result->getModifiedCount()) { + throw new JobPersistenceException(sprintf('Couldn\'t resume trigger: "%s"', $triggerKey)); + } + } + } + + /** + * @param Key $triggerKey + * @param string $newStateIfNotComplete + * + * @return bool + */ + protected function updateMisfiredTrigger(Key $triggerKey, $newStateIfNotComplete) + { + $trigger = $this->retrieveTrigger($triggerKey); + + $misfireTime = time(); + if ($this->misfireThreshold > 0) { + $misfireTime -= $this->misfireThreshold; + } + + if (((int) $trigger->getNextFireTime()->format('U')) > $misfireTime) { + return false; + } + + $this->doUpdateOfMisfiredTrigger($trigger, $newStateIfNotComplete); + + return true; + } + + /** + * @param Trigger $trig + * @param string $newStateIfNotComplete + */ + protected function doUpdateOfMisfiredTrigger(Trigger $trig, $newStateIfNotComplete) + { + $calendar = null; + if ($trig->getCalendarName()) { + $calendar = $this->retrieveCalendar($trig->getCalendarName()); + } + + $this->scheduler->notifyTriggerListenersMisfired($trig); + + $trig->updateAfterMisfire($calendar); + + if (null == $trig->getNextFireTime()) { + $this->doStoreTrigger($trig, true, Trigger::STATE_COMPLETE); + + $this->scheduler->notifySchedulerListenersFinalized($trig); + } else { + $this->doStoreTrigger($trig, true, $newStateIfNotComplete); + } + } + + public function resumeTriggers(GroupMatcher $matcher) + { + // TODO: Implement resumeTriggers() method. + } + + /** + * {@inheritdoc} + */ + public function getPausedTriggerGroups() + { + return $this->res->getPausedTriggerStorage()->getCollection()->distinct('groupName'); + } + + /** + * {@inheritdoc} + */ + public function resumeJob(Key $jobKey) + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($jobKey) { + $this->doResumeJob($jobKey); + }); + } + + /** + * @param Key $jobKey + */ + protected function doResumeJob(Key $jobKey) + { + foreach ($this->getTriggersForJob($jobKey) as $trigger) { + $this->doResumeTrigger($trigger->getKey()); + } + } + + public function resumeJobs(GroupMatcher $matcher) + { + // TODO: Implement resumeJobs() method. + } + + /** + * {@inheritdoc} + */ + public function pauseAll() + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () { + $this->doPauseAll(); + }); + } + + protected function doPauseAll() + { + foreach ($this->getTriggerGroupNames() as $name) { + $triggers = $this->res->getTriggerStorage()->find([ + 'group' => $name, + ]); + + /** @var Trigger $trigger */ + foreach ($triggers as $trigger) { + $this->doPauseTrigger($trigger->getKey()); + } + + $this->insertPausedTriggerGroup($name); + } + + $this->insertPausedTriggerGroup(self::ALL_GROUPS_PAUSED); + } + + /** + * {@inheritdoc} + */ + public function resumeAll() + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () { + $this->doResumeAll(); + }); + } + + protected function doResumeAll() + { + foreach ($this->getTriggerGroupNames() as $name) { + $triggers = $this->res->getTriggerStorage()->find([ + 'group' => $name, + ]); + + /** @var Trigger $trigger */ + foreach ($triggers as $trigger) { + $this->doResumeTrigger($trigger->getKey()); + } + + $this->deletePausedTriggerGroup($name); + } + + $this->deletePausedTriggerGroup(self::ALL_GROUPS_PAUSED); + } + + /** + * {@inheritdoc} + */ + public function getTriggersForJob(Key $jobKey) + { + // no locks necessary for read... + return $this->res->getTriggerStorage()->find([ + 'jobName' => $jobKey->getName(), + 'jobGroup' => $jobKey->getGroup(), + ]); + } + + /** + * {@inheritdoc} + */ + public function getJobGroupNames() + { + // no locks necessary for read... + return $this->res->getJobStorage()->getCollection()->distinct('group'); + } + + /** + * {@inheritdoc} + */ + public function getTriggerGroupNames() + { + // no locks necessary for read... + return $this->res->getTriggerStorage()->getCollection()->distinct('group'); + } + + /** + * {@inheritdoc} + */ + public function getTriggerState(Key $triggerKey) + { + if (null == $trigger = $this->retrieveTrigger($triggerKey)) { + throw new JobPersistenceException(sprintf('There is no trigger with key: "%s"', (string) $triggerKey)); + } + + return $trigger->getState(); + } + + /** + * {@inheritdoc} + */ + public function getCalendarNames() + { + // no locks necessary for read... + return $this->res->getCalendarStorage()->getCollection()->distinct('name'); + } + + /** + * {@inheritdoc} + */ + public function resetTriggerFromErrorState(Key $triggerKey) + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function() use ($triggerKey) { + $this->doResetTriggerFromErrorState($triggerKey); + }); + } + + /** + * @param Key $triggerKey + * + * @throws JobPersistenceException + */ + public function doResetTriggerFromErrorState(Key $triggerKey) + { + if (null == $trigger = $this->retrieveTrigger($triggerKey)) { + throw new JobPersistenceException(sprintf('There is no trigger with identity: "%s"', (string) $triggerKey)); + } + + if ($trigger->getState() !== Trigger::STATE_ERROR) { + return; + } + + $state = Trigger::STATE_WAITING; + if ($this->isTriggerGroupPaused($triggerKey->getGroup())) { + $state = Trigger::STATE_PAUSED; + } + + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $trigger->getKey()->getName(), + 'group' => $trigger->getKey()->getGroup(), + ], [ + '$set' => [ + 'state' => $state, + ] + ]); + } + + /** + * {@inheritdoc} + */ + public function acquireNextTriggers($noLaterThan, $maxCount, $timeWindow) + { + return $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($noLaterThan, $maxCount, $timeWindow) { + return $this->doAcquireNextTriggers($noLaterThan, $maxCount, $timeWindow); + }); + } + + /** + * @param int $noLaterThan + * @param int $maxCount + * @param int $timeWindow + * + * @return Trigger[] + * + * @throws JobPersistenceException + */ + protected function doAcquireNextTriggers($noLaterThan, $maxCount, $timeWindow) + { + $noEarlyThen = time() - $this->misfireThreshold; + $acquiredTriggers = []; + + $triggers = $this->res->getTriggerStorage()->find([ + 'state' => Trigger::STATE_WAITING, + 'nextFireTime.unix' => [ + '$gte' => $noEarlyThen, + '$lte' => $noLaterThan + $timeWindow, + ] + ], [ + 'limit' => $maxCount, + 'sort' => [ + 'nextFireTime.unix' => 1, + 'priority' => -1, + ] + ]); + + $ids = []; + foreach ($triggers as $trigger) { + $ids[] = get_object_id($trigger); + $acquiredTriggers[] = $trigger; + } + + // find misfired triggers only if free space left + if (($leftCount = $maxCount - count($ids)) > 0) { + $misfiredTriggers = $this->res->getTriggerStorage()->find([ + 'state' => Trigger::STATE_WAITING, + 'nextFireTime.unix' => [ + '$lt' => $noEarlyThen, + ] + ], [ + 'limit' => $leftCount, + 'sort' => [ + 'nextFireTime.unix' => 1, + 'priority' => -1, + ] + ]); + + foreach ($misfiredTriggers as $trigger) { + $ids[] = get_object_id($trigger); + $acquiredTriggers[] = $trigger; + } + } + + // acquire found triggers + if ($acquiredTriggers) { + $result = $this->res->getTriggerStorage()->getCollection()->updateMany([ + '_id' => [ + '$in' => $ids, + ], + ], [ + '$set' => [ + 'state' => Trigger::STATE_ACQUIRED, + ] + ]); + + if (false == $result->isAcknowledged() || false == $result->getModifiedCount()) { + throw new JobPersistenceException('Couldn\'t acquire next trigger'); + } + } + + return $acquiredTriggers; + } + + public function releaseAcquiredTrigger(Trigger $trigger) + { + // TODO: Implement releaseAcquiredTrigger() method. + } + + /** + * {@inheritdoc} + */ + public function triggersFired(array $triggers, $noLaterThan) + { + foreach ($triggers as $trigger) { + if (false == $trigger instanceof Trigger) { + throw new \InvalidArgumentException(sprintf('Expected array of "%s" but got: "%s"', + Trigger::class, is_object($trigger) ? get_class($trigger) : gettype($trigger))); + } + } + + $firedTriggers = []; + foreach ($triggers as $trigger) { + $_triggers = $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($trigger, $noLaterThan) { + return $this->doTriggerFired($trigger, $noLaterThan); + }); + + if ($_triggers) { + $firedTriggers = array_merge($firedTriggers, $_triggers); + } + } + + return $firedTriggers; + } + + /** + * @param Trigger $trigger + * @param int $noLaterThan + * + * @return Trigger[] + * + * @throws JobPersistenceException + */ + public function doTriggerFired(Trigger $trigger, $noLaterThan) + { + // Make sure trigger wasn't deleted, paused, or completed... + if (false == $trigger = $this->retrieveTrigger($trigger->getKey())) { + return[]; + } + + if ($trigger->getState() !== Trigger::STATE_ACQUIRED) { + return []; + } + + $cal = null; + if ($trigger->getCalendarName()) { + if (null == $cal = $this->retrieveCalendar($trigger->getCalendarName())) { + return []; + } + } + + $firedTriggers = []; + $fireTime = new \DateTime(); + $misfireTime = time() - $this->misfireThreshold; + + // update misfired trigger + if (((int) $trigger->getNextFireTime()->format('U')) < $misfireTime) { + $this->scheduler->notifyTriggerListenersMisfired($trigger); + $trigger->updateAfterMisfire($cal); + } + + while (($nextFireTime = $trigger->getNextFireTime()) && (((int) $nextFireTime->format('U')) <= $noLaterThan )) { + $firedTrigger = clone $trigger; + $scheduledFireTime = clone $trigger->getNextFireTime(); + + $trigger->triggered($cal); + + $firedTrigger->setFireInstanceId(Uuid::uuid4()->toString()); + $firedTrigger->setState(Trigger::STATE_EXECUTING); // @todo need extra statuses for fired triggers + $firedTrigger->setFireTime(clone $fireTime); + $firedTrigger->setScheduledFireTime($scheduledFireTime); + $firedTrigger->setNextFireTime($trigger->getNextFireTime() ? clone $trigger->getNextFireTime() : null); + + $firedTriggers[] = $firedTrigger; + } + + $state = Trigger::STATE_WAITING; + if (false == $trigger->getNextFireTime()) { + $state = Trigger::STATE_COMPLETE; + } + + $this->doStoreTrigger($trigger, true, $state); + + if ($trigger->getState() === Trigger::STATE_COMPLETE) { + $this->scheduler->notifySchedulerListenersFinalized($trigger); + } + + foreach ($firedTriggers as $firedTrigger) { + $this->res->getFiredTriggerStorage()->insert($firedTrigger); + } + + return $firedTriggers; + } + + /** + * {@inheritdoc} + */ + public function triggeredJobComplete(Trigger $trigger, JobDetail $jobDetail, $triggerInstCode) + { + $this->executeInLock(self::LOCK_TRIGGER_ACCESS, function () use ($trigger, $jobDetail, $triggerInstCode) { + $this->doTriggeredJobComplete($trigger, $jobDetail, $triggerInstCode); + }); + } + + /** + * @param Trigger $trigger + * @param JobDetail $jobDetail + * @param string $triggerInstCode + * + * @throws JobPersistenceException + */ + public function doTriggeredJobComplete(Trigger $trigger, JobDetail $jobDetail, $triggerInstCode) + { + if ($triggerInstCode === CompletedExecutionInstruction::DELETE_TRIGGER) { + // remove trigger + $this->doRemoveTrigger($trigger->getKey()); + } elseif ($triggerInstCode === CompletedExecutionInstruction::SET_TRIGGER_COMPLETE) { + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $trigger->getKey()->getName(), + 'group' => $trigger->getKey()->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_COMPLETE, + 'errorMessage' => $trigger->getErrorMessage(), + ] + ]); + } elseif ($triggerInstCode === CompletedExecutionInstruction::SET_TRIGGER_ERROR) { + $this->res->getTriggerStorage()->getCollection()->updateOne([ + 'name' => $trigger->getKey()->getName(), + 'group' => $trigger->getKey()->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ERROR, + 'errorMessage' => $trigger->getErrorMessage(), + ] + ]); + } elseif ($triggerInstCode === CompletedExecutionInstruction::SET_ALL_JOB_TRIGGERS_COMPLETE) { + $this->res->getTriggerStorage()->getCollection()->updateMany([ + 'jobName' => $trigger->getJobKey()->getName(), + 'jobGroup' => $trigger->getJobKey()->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_COMPLETE, + 'errorMessage' => $trigger->getErrorMessage(), + ] + ]); + } elseif ($triggerInstCode === CompletedExecutionInstruction::SET_ALL_JOB_TRIGGERS_ERROR) { + $this->res->getTriggerStorage()->getCollection()->updateMany([ + 'jobName' => $trigger->getJobKey()->getName(), + 'jobGroup' => $trigger->getJobKey()->getGroup(), + ], [ + '$set' => [ + 'state' => Trigger::STATE_ERROR, + 'errorMessage' => $trigger->getErrorMessage(), + ] + ]); + } + + // remove fired triggers + // TODO: update fired trigger but not remove + $result = $this->res->getFiredTriggerStorage()->getCollection()->deleteOne([ + 'fireInstanceId' => $trigger->getFireInstanceId(), + ]); + + if (false == $result->isAcknowledged() || false == $result->getDeletedCount()) { + throw new JobPersistenceException(sprintf('Couldn\'t delete fired trigger: fireInstanceId "%s"', $trigger->getFireInstanceId())); + } + } + + /** + * @param string $lockName + * @param callable $callback + * + * @return mixed + */ + private function executeInLock($lockName, callable $callback) + { + $this->res->getManagementLock()->lock($lockName); + + try { + return call_user_func($callback); + } finally { + $this->res->getManagementLock()->unlock($lockName); + } + } + + /** + * @param string $groupName + * + * @return boolean + */ + public function insertPausedTriggerGroup($groupName) + { + $result = $this->res->getPausedTriggerStorage()->getCollection()->updateOne( + ['groupName' => $groupName], + ['$set' => ['groupName' => $groupName]], + ['upsert' => true] + ); + + return $result->isAcknowledged() && $result->getUpsertedCount(); + } + + /** + * @param string $groupName + * + * @return boolean + */ + public function deletePausedTriggerGroup($groupName) + { + $result = $this->res->getPausedTriggerStorage()->getCollection()->deleteOne(['groupName' => $groupName]); + + return $result->isAcknowledged() && $result->getDeletedCount(); + } + + /** + * @param string $groupName + * + * @return boolean + */ + public function isTriggerGroupPaused($groupName) + { + return (bool) $this->res->getPausedTriggerStorage()->getCollection()->count(['groupName' => $groupName]); + } + + /** + * {@inheritdoc} + */ + public function schedulerStarted() + { + $this->res->getManagementLock()->unlockAll(); + + // recover failed and acquired trigger + $this->res->getTriggerStorage()->getCollection()->updateMany([ + 'state' => [ + '$in' => [Trigger::STATE_ACQUIRED, Trigger::STATE_ERROR], + ] + ], [ + '$set' => [ + 'state' => Trigger::STATE_WAITING, + ] + ]); + + // clear fired triggers + $this->res->getFiredTriggerStorage()->getCollection()->deleteMany([]); + } + + /** + * {@inheritdoc} + */ + public function retrieveFireTrigger($fireInstanceId) + { + return $this->res->getFiredTriggerStorage()->findOne([ + 'fireInstanceId' => $fireInstanceId, + ]); + } + + /** + * TODO: is not part of interface + */ + public function createIndexes() + { + try { + $this->res->getTriggerStorage()->getCollection()->dropIndexes(); + $this->res->getJobStorage()->getCollection()->dropIndexes(); + $this->res->getCalendarStorage()->getCollection()->dropIndexes(); + $this->res->getPausedTriggerStorage()->getCollection()->dropIndexes(); + $this->res->getFiredTriggerStorage()->getCollection()->dropIndexes(); + } catch (RuntimeException $e) { + } + + $lock = $this->res->getManagementLock(); + foreach ($lock->getIndexes() as $index) { + $lock->getCollection()->createIndexes($index->getKey(), $index->getOptions()); + } + + $storages = [ + $this->res->getCalendarStorage(), + $this->res->getTriggerStorage(), + $this->res->getJobStorage(), + $this->res->getPausedTriggerStorage(), + $this->res->getFiredTriggerStorage(), + ]; + + foreach ($storages as $storage) { + /** @var Storage|StorageMetaInterface $storage */ + foreach ($storage->getIndexes() as $index) { + $storage->getCollection()->createIndexes($index->getKey(), $index->getOptions()); + } + } + } +} diff --git a/pkg/bridge/composer.json b/pkg/bridge/composer.json index 06d5468..92f06db 100644 --- a/pkg/bridge/composer.json +++ b/pkg/bridge/composer.json @@ -9,6 +9,7 @@ "php": "^7.1.3", "symfony/framework-bundle": "^3|^4", "enqueue/enqueue": "^0.9", + "formapro/yadm": "^0.5.5", "queue-interop/queue-interop": "^0.7|^0.8", "php-quartz/quartz": "^0.2" }, From 4149541e8e63a4708d99942cce839fbce46df6db Mon Sep 17 00:00:00 2001 From: Maksim Kotlyar Date: Tue, 14 May 2019 13:26:40 +0300 Subject: [PATCH 3/4] fix namespace --- pkg/bundle/Tests/Command/ManagementCommandTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/bundle/Tests/Command/ManagementCommandTest.php b/pkg/bundle/Tests/Command/ManagementCommandTest.php index ac31759..0c4beb9 100644 --- a/pkg/bundle/Tests/Command/ManagementCommandTest.php +++ b/pkg/bundle/Tests/Command/ManagementCommandTest.php @@ -2,9 +2,9 @@ namespace Quartz\Bundle\Tests\Command; use PHPUnit\Framework\TestCase; +use Quartz\Bridge\Yadm\YadmStore; use Quartz\Bundle\Command\ManagementCommand; use Quartz\Scheduler\StdScheduler; -use Quartz\Scheduler\Store\YadmStore; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\HelperSet; use Symfony\Component\Console\Helper\QuestionHelper; From a0de06b10966d156beb1aa44d4fad5dc6ebbb7fa Mon Sep 17 00:00:00 2001 From: Maksim Kotlyar Date: Tue, 14 May 2019 13:32:21 +0300 Subject: [PATCH 4/4] cleanup code --- pkg/bridge/Yadm/SimpleStoreResource.php | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pkg/bridge/Yadm/SimpleStoreResource.php b/pkg/bridge/Yadm/SimpleStoreResource.php index bceb006..1b7bb0f 100644 --- a/pkg/bridge/Yadm/SimpleStoreResource.php +++ b/pkg/bridge/Yadm/SimpleStoreResource.php @@ -143,21 +143,4 @@ public function getPausedTriggerStorage(): PausedTriggerStorage return $this->pausedTriggerStorage; } - - public function dropIndexes(): void - { - try { - $this->getTriggerStorage()->getCollection()->dropIndexes(); - $this->getJobStorage()->getCollection()->dropIndexes(); - $this->getCalendarStorage()->getCollection()->dropIndexes(); - $this->getPausedTriggerStorage()->getCollection()->dropIndexes(); - $this->getFiredTriggerStorage()->getCollection()->dropIndexes(); - } catch (RuntimeException $e) { - } - } - - public function createIndexes(): void - { - - } }