From 4ce65d59ca91871cfd126497158200a818720bce Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 13 Apr 2024 13:30:08 +0200 Subject: Adding upstream version 0.13.1. Signed-off-by: Daniel Baumann --- vendor/ipl/scheduler/composer.json | 39 +++ vendor/ipl/scheduler/src/Common/Promises.php | 108 +++++++ vendor/ipl/scheduler/src/Common/TaskProperties.php | 83 ++++++ vendor/ipl/scheduler/src/Common/Timers.php | 60 ++++ vendor/ipl/scheduler/src/Contract/Frequency.php | 62 ++++ vendor/ipl/scheduler/src/Contract/Task.php | 39 +++ vendor/ipl/scheduler/src/Cron.php | 203 +++++++++++++ vendor/ipl/scheduler/src/OneOff.php | 69 +++++ vendor/ipl/scheduler/src/RRule.php | 328 +++++++++++++++++++++ vendor/ipl/scheduler/src/Scheduler.php | 323 ++++++++++++++++++++ vendor/ipl/scheduler/src/register_cron_aliases.php | 11 + 11 files changed, 1325 insertions(+) create mode 100644 vendor/ipl/scheduler/composer.json create mode 100644 vendor/ipl/scheduler/src/Common/Promises.php create mode 100644 vendor/ipl/scheduler/src/Common/TaskProperties.php create mode 100644 vendor/ipl/scheduler/src/Common/Timers.php create mode 100644 vendor/ipl/scheduler/src/Contract/Frequency.php create mode 100644 vendor/ipl/scheduler/src/Contract/Task.php create mode 100644 vendor/ipl/scheduler/src/Cron.php create mode 100644 vendor/ipl/scheduler/src/OneOff.php create mode 100644 vendor/ipl/scheduler/src/RRule.php create mode 100644 vendor/ipl/scheduler/src/Scheduler.php create mode 100644 vendor/ipl/scheduler/src/register_cron_aliases.php (limited to 'vendor/ipl/scheduler') diff --git a/vendor/ipl/scheduler/composer.json b/vendor/ipl/scheduler/composer.json new file mode 100644 index 0000000..5431be8 --- /dev/null +++ b/vendor/ipl/scheduler/composer.json @@ -0,0 +1,39 @@ +{ + "name": "ipl/scheduler", + "type": "library", + "description": "Icinga PHP Library - Tasks scheduler", + "keywords": ["task", "job", "scheduler", "cron"], + "homepage": "https://github.com/Icinga/ipl-scheduler", + "license": "MIT", + "config": { + "sort-packages": true + }, + "require": { + "php": ">=7.2", + "ext-json": "*", + "dragonmantank/cron-expression": "^3", + "psr/log": "^1", + "ramsey/uuid": "^4.2.3", + "react/event-loop": "^1.4", + "react/promise": "^2.10", + "simshaun/recurr": "^5", + "ipl/stdlib": ">=0.12.0" + }, + "require-dev": { + "ipl/stdlib": "dev-main" + }, + "suggest": { + "ext-ev": "Improves performance, efficiency and avoids system limitations. Highly recommended! (See https://www.php.net/manual/en/intro.ev.php for details)" + }, + "autoload": { + "files": ["src/register_cron_aliases.php"], + "psr-4": { + "ipl\\Scheduler\\": "src" + } + }, + "autoload-dev": { + "psr-4": { + "ipl\\Tests\\Scheduler\\": "tests" + } + } +} diff --git a/vendor/ipl/scheduler/src/Common/Promises.php b/vendor/ipl/scheduler/src/Common/Promises.php new file mode 100644 index 0000000..b896627 --- /dev/null +++ b/vendor/ipl/scheduler/src/Common/Promises.php @@ -0,0 +1,108 @@ +> */ + protected $promises; + + /** + * Add the given promise for the specified UUID + * + * **Example Usage:** + * + * ```php + * $promise = work(); + * $promises->addPromise($uuid, $promise); + * ``` + * + * @param UuidInterface $uuid + * @param PromiseInterface $promise + * + * @return $this + */ + protected function addPromise(UuidInterface $uuid, PromiseInterface $promise): self + { + if (! $this->promises->contains($uuid)) { + $this->promises->attach($uuid, new ArrayObject()); + } + + $this->promises[$uuid][] = $promise; + + return $this; + } + + /** + * Remove the given promise for the specified UUID + * + * **Example Usage:** + * + * ```php + * $promise->always(function () use ($uuid, $promise) { + * $promises->removePromise($uuid, $promise); + * }) + * ``` + * + * @param UuidInterface $uuid + * @param PromiseInterface $promise + * + * @return $this + * + * @throws InvalidArgumentException If the given UUID doesn't have any registered promises or when the specified + * UUID promises doesn't contain the provided promise + */ + protected function removePromise(UuidInterface $uuid, PromiseInterface $promise): self + { + if (! $this->promises->contains($uuid)) { + throw new InvalidArgumentException( + sprintf('There are no registered promises for UUID %s', $uuid->toString()) + ); + } + + foreach ($this->promises[$uuid] as $k => $v) { + if ($v === $promise) { + unset($this->promises[$uuid][$k]); + + return $this; + } + } + + throw new InvalidArgumentException( + sprintf('There is no such promise for UUID %s', $uuid->toString()) + ); + } + + /** + * Detach and return promises for the given UUID, if any + * + * **Example Usage:** + * + * ```php + * foreach ($promises->detachPromises($uuid) as $promise) { + * $promise->cancel(); + * } + * ``` + * + * @param UuidInterface $uuid + * + * @return PromiseInterface[] + */ + protected function detachPromises(UuidInterface $uuid): array + { + if (! $this->promises->contains($uuid)) { + return []; + } + + $promises = $this->promises[$uuid]; + $this->promises->detach($uuid); + + return $promises->getArrayCopy(); + } +} diff --git a/vendor/ipl/scheduler/src/Common/TaskProperties.php b/vendor/ipl/scheduler/src/Common/TaskProperties.php new file mode 100644 index 0000000..4ab65e2 --- /dev/null +++ b/vendor/ipl/scheduler/src/Common/TaskProperties.php @@ -0,0 +1,83 @@ +description = $desc; + + return $this; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function getName(): string + { + if (! $this->name) { + throw new LogicException('Task name must not be null'); + } + + return $this->name; + } + + /** + * Set the name of this Task + * + * @param string $name + * + * @return $this + */ + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getUuid(): UuidInterface + { + if (! $this->uuid) { + throw new LogicException('Task UUID must not be null'); + } + + return $this->uuid; + } + + /** + * Set the UUID of this task + * + * @param UuidInterface $uuid + * + * @return $this + */ + public function setUuid(UuidInterface $uuid): self + { + $this->uuid = $uuid; + + return $this; + } +} diff --git a/vendor/ipl/scheduler/src/Common/Timers.php b/vendor/ipl/scheduler/src/Common/Timers.php new file mode 100644 index 0000000..2d0641f --- /dev/null +++ b/vendor/ipl/scheduler/src/Common/Timers.php @@ -0,0 +1,60 @@ + */ + protected $timers; + + /** + * Set a timer for the given UUID + * + * **Example Usage:** + * + * ```php + * $timers->attachTimer($uuid, Loop::addTimer($interval, $callback)); + * ``` + * + * @param UuidInterface $uuid + * @param TimerInterface $timer + * + * @return $this + */ + protected function attachTimer(UuidInterface $uuid, TimerInterface $timer): self + { + $this->timers->attach($uuid, $timer); + + return $this; + } + + /** + * Detach and return the timer for the given UUID, if any + * + * **Example Usage:** + * + * ```php + * Loop::cancelTimer($timers->detachTimer($uuid)); + * ``` + * + * @param UuidInterface $uuid + * + * @return ?TimerInterface + */ + protected function detachTimer(UuidInterface $uuid): ?TimerInterface + { + if (! $this->timers->contains($uuid)) { + return null; + } + + $timer = $this->timers->offsetGet($uuid); + + $this->timers->detach($uuid); + + return $timer; + } +} diff --git a/vendor/ipl/scheduler/src/Contract/Frequency.php b/vendor/ipl/scheduler/src/Contract/Frequency.php new file mode 100644 index 0000000..2235787 --- /dev/null +++ b/vendor/ipl/scheduler/src/Contract/Frequency.php @@ -0,0 +1,62 @@ +cron = new CronExpression($expression); + $this->expression = $expression; + } + + public function isDue(DateTimeInterface $dateTime): bool + { + if ($this->isExpired($dateTime) || $dateTime < $this->start) { + return false; + } + + return $this->cron->isDue($dateTime); + } + + public function getNextDue(DateTimeInterface $dateTime): DateTimeInterface + { + if ($this->isExpired($dateTime)) { + return $this->end; + } + + if ($dateTime < $this->start) { + return $this->start; + } + + return $this->cron->getNextRunDate($dateTime); + } + + public function isExpired(DateTimeInterface $dateTime): bool + { + return $this->end !== null && $this->end < $dateTime; + } + + public function getStart(): ?DateTimeInterface + { + return $this->start; + } + + public function getEnd(): ?DateTimeInterface + { + return $this->end; + } + + /** + * Get the configured cron expression + * + * @return string + */ + public function getExpression(): string + { + return $this->expression; + } + + /** + * Set the start time of this frequency + * + * @param DateTimeInterface $start + * + * @return $this + */ + public function startAt(DateTimeInterface $start): self + { + $this->start = clone $start; + $this->start->setTimezone(new DateTimeZone(date_default_timezone_get())); + + return $this; + } + + /** + * Set the end time of this frequency + * + * @param DateTimeInterface $end + * + * @return $this + */ + public function endAt(DateTimeInterface $end): Frequency + { + $this->end = clone $end; + $this->end->setTimezone(new DateTimeZone(date_default_timezone_get())); + + return $this; + } + + /** + * Get the given part of the underlying cron expression + * + * @param int $part One of the classes `PART_*` constants + * + * @return string + * + * @throws InvalidArgumentException If the given part is invalid + */ + public function getPart(int $part): string + { + $value = $this->cron->getExpression($part); + if ($value === null) { + throw new InvalidArgumentException(sprintf('Invalid expression part specified: %d', $part)); + } + + return $value; + } + + /** + * Get the parts of the underlying cron expression as an array + * + * @return string[] + */ + public function getParts(): array + { + return $this->cron->getParts(); + } + + /** + * Get whether the given cron expression is valid + * + * @param string $expression + * + * @return bool + */ + public static function isValid(string $expression): bool + { + return CronExpression::isValidExpression($expression); + } + + public static function fromJson(string $json): Frequency + { + $data = json_decode($json, true); + if (! is_array($data)) { + throw new InvalidArgumentException( + sprintf( + '%s expects json decoded value to be an array, got %s instead', + __METHOD__, + get_php_type($data) + ) + ); + } + + $self = new static($data['expression']); + if (isset($data['start'])) { + $self->startAt(new DateTime($data['start'])); + } + + if (isset($data['end'])) { + $self->endAt(new DateTime($data['end'])); + } + + return $self; + } + + public function jsonSerialize(): array + { + $data = ['expression' => $this->getExpression()]; + if ($this->start) { + $data['start'] = $this->start->format(static::SERIALIZED_DATETIME_FORMAT); + } + + if ($this->end) { + $data['end'] = $this->end->format(static::SERIALIZED_DATETIME_FORMAT); + } + + return $data; + } +} diff --git a/vendor/ipl/scheduler/src/OneOff.php b/vendor/ipl/scheduler/src/OneOff.php new file mode 100644 index 0000000..ebe945d --- /dev/null +++ b/vendor/ipl/scheduler/src/OneOff.php @@ -0,0 +1,69 @@ +dateTime = clone $dateTime; + $this->dateTime->setTimezone(new DateTimeZone(date_default_timezone_get())); + } + + public function isDue(DateTimeInterface $dateTime): bool + { + return ! $this->isExpired($dateTime) && $this->dateTime == $dateTime; + } + + public function getNextDue(DateTimeInterface $dateTime): DateTimeInterface + { + return $this->dateTime; + } + + public function isExpired(DateTimeInterface $dateTime): bool + { + return $this->dateTime < $dateTime; + } + + public function getStart(): ?DateTimeInterface + { + return $this->dateTime; + } + + public function getEnd(): ?DateTimeInterface + { + return $this->getStart(); + } + + public static function fromJson(string $json): Frequency + { + $data = json_decode($json, true); + if (! is_string($data)) { + throw new InvalidArgumentException( + sprintf( + '%s expects json decoded value to be string, got %s instead', + __METHOD__, + get_php_type($data) + ) + ); + } + + return new static(new DateTime($data)); + } + + public function jsonSerialize(): string + { + return $this->dateTime->format(static::SERIALIZED_DATETIME_FORMAT); + } +} diff --git a/vendor/ipl/scheduler/src/RRule.php b/vendor/ipl/scheduler/src/RRule.php new file mode 100644 index 0000000..bfad0e5 --- /dev/null +++ b/vendor/ipl/scheduler/src/RRule.php @@ -0,0 +1,328 @@ + $rule + * + * @throws InvalidRRule + */ + public function __construct($rule) + { + $this->rrule = new RecurrRule($rule); + $this->frequency = $this->rrule->getFreqAsText(); + $this->transformerConfig = new ArrayTransformerConfig(); + $this->transformerConfig->setVirtualLimit(self::DEFAULT_LIMIT); + + // If the run day isn't set explicitly, we can enable the last day of month + // fix, so that it doesn't skip some months which doesn't have e.g. 29,30,31 days. + if ( + $this->getFrequency() === static::MONTHLY + && ! $this->rrule->getByDay() + && ! $this->rrule->getByMonthDay() + ) { + $this->transformerConfig->enableLastDayOfMonthFix(); + } + + $this->transformer = new ArrayTransformer($this->transformerConfig); + } + + /** + * Get an RRule instance from the provided frequency + * + * @param string $frequency + * + * @return $this + */ + public static function fromFrequency(string $frequency): self + { + $frequencies = array_flip([ + static::MINUTELY, + static::HOURLY, + static::DAILY, + static::WEEKLY, + static::MONTHLY, + static::QUARTERLY, + static::YEARLY + ]); + + if (! isset($frequencies[$frequency])) { + throw new InvalidArgumentException(sprintf('Unknown frequency provided: %s', $frequency)); + } + + if ($frequency === static::QUARTERLY) { + $repeat = static::MONTHLY; + $rule = "FREQ=$repeat;INTERVAL=3"; + } else { + $rule = "FREQ=$frequency"; + } + + $self = new static($rule); + $self->frequency = $frequency; + + return $self; + } + + public static function fromJson(string $json): Frequency + { + /** @var stdClass $data */ + $data = json_decode($json); + $self = new static($data->rrule); + $self->frequency = $data->frequency; + if (isset($data->start)) { + $start = DateTime::createFromFormat(static::SERIALIZED_DATETIME_FORMAT, $data->start); + if (! $start) { + throw new InvalidArgumentException(sprintf('Cannot deserialize start time: %s', $data->start)); + } + + $self->startAt($start); + } + + return $self; + } + + public function isDue(DateTimeInterface $dateTime): bool + { + if ($dateTime < $this->rrule->getStartDate() || $this->isExpired($dateTime)) { + return false; + } + + $nextDue = $this->getNextRecurrences($dateTime); + if (! $nextDue->valid()) { + return false; + } + + return $nextDue->current() == $dateTime; + } + + public function getNextDue(DateTimeInterface $dateTime): DateTimeInterface + { + if ($this->isExpired($dateTime)) { + return $this->getEnd(); + } + + $nextDue = $this->getNextRecurrences($dateTime, 1, false); + if (! $nextDue->valid()) { + return $dateTime; + } + + return $nextDue->current(); + } + + public function isExpired(DateTimeInterface $dateTime): bool + { + if ($this->rrule->repeatsIndefinitely()) { + return false; + } + + return $this->getEnd() !== null && $this->getEnd() < $dateTime; + } + + /** + * Set the start time of this frequency + * + * The given datetime will be cloned and microseconds removed since iCalendar datetimes only work to the second. + * + * @param DateTimeInterface $start + * + * @return $this + */ + public function startAt(DateTimeInterface $start): self + { + $startDate = clone $start; + // When the start time contains microseconds, the first recurrence will always be skipped, as + // the transformer operates only up to seconds level. See also the upstream issue #155 + $startDate->setTime($start->format('H'), $start->format('i'), $start->format('s')); + // In case start time uses a different tz than what the rrule internally does, we force it to use the same + $startDate->setTimezone(new DateTimeZone($this->rrule->getTimezone())); + + $this->rrule->setStartDate($startDate); + + return $this; + } + + public function getStart(): ?DateTimeInterface + { + return $this->rrule->getStartDate(); + } + + /** + * Set the time until this frequency lasts + * + * The given datetime will be cloned and microseconds removed since iCalendar datetimes only work to the second. + * + * @param DateTimeInterface $end + * + * @return $this + */ + public function endAt(DateTimeInterface $end): self + { + $end = clone $end; + $end->setTime($end->format('H'), $end->format('i'), $end->format('s')); + + $this->rrule->setUntil($end); + + return $this; + } + + public function getEnd(): ?DateTimeInterface + { + return $this->rrule->getEndDate() ?? $this->rrule->getUntil(); + } + + /** + * Get the frequency of this rule + * + * @return string + */ + public function getFrequency(): string + { + return $this->frequency; + } + + /** + * Get a set of recurrences relative to the given time + * + * @param DateTimeInterface $dateTime + * @param int $limit Limit the recurrences to be generated to the given value + * @param bool $include Whether to include the passed time in the result set + * + * @return Generator + */ + public function getNextRecurrences( + DateTimeInterface $dateTime, + int $limit = self::DEFAULT_LIMIT, + bool $include = true + ): Generator { + $resetTransformerConfig = function (int $limit = self::DEFAULT_LIMIT): void { + $this->transformerConfig->setVirtualLimit($limit); + $this->transformer->setConfig($this->transformerConfig); + }; + + if ($limit > self::DEFAULT_LIMIT) { + $resetTransformerConfig($limit); + } + + $constraint = new AfterConstraint($dateTime, $include); + if (! $this->rrule->repeatsIndefinitely()) { + // When accessing this method externally (not by using `getNextDue()`), the transformer may + // generate recurrences beyond the configured end time. + $constraint = new BetweenConstraint($dateTime, $this->getEnd(), $include); + } + + // Setting the start date to a date time smaller than now causes the underlying library + // not to generate any recurrences when using the regular frequencies such as `MINUTELY` etc. + // and the `$countConstraintFailures` is set to true. We need also to tell the transformer + // not to count the recurrences that fail the constraint's test! + $recurrences = $this->transformer->transform($this->rrule, $constraint, false); + foreach ($recurrences as $recurrence) { + yield $recurrence->getStart(); + } + + if ($limit > self::DEFAULT_LIMIT) { + $resetTransformerConfig(); + } + } + + public function jsonSerialize(): array + { + $data = [ + 'rrule' => $this->rrule->getString(RecurrRule::TZ_FIXED), + 'frequency' => $this->frequency + ]; + + $start = $this->getStart(); + if ($start) { + $data['start'] = $start->format(static::SERIALIZED_DATETIME_FORMAT); + } + + return $data; + } + + /** + * Redirect all public method calls to the underlying rrule object + * + * @param string $methodName + * @param array $args + * + * @return mixed + * + * @throws BadMethodCallException If the given method doesn't exist or when setter method is called + */ + public function __call(string $methodName, array $args) + { + if (! method_exists($this->rrule, $methodName)) { + throw new BadMethodCallException( + sprintf('Call to undefined method %s::%s()', get_php_type($this->rrule), $methodName) + ); + } + + if (strtolower(substr($methodName, 0, 3)) !== 'get') { + throw new BadMethodCallException( + sprintf('Dynamic method %s is not supported. Only getters (get*) are', $methodName) + ); + } + + return call_user_func_array([$this->rrule, $methodName], $args); + } +} diff --git a/vendor/ipl/scheduler/src/Scheduler.php b/vendor/ipl/scheduler/src/Scheduler.php new file mode 100644 index 0000000..25ad3a1 --- /dev/null +++ b/vendor/ipl/scheduler/src/Scheduler.php @@ -0,0 +1,323 @@ +on($scheduler::ON_TASK_CANCEL, function (Task $task, array $_) use ($logger) { + * $logger->info(sprintf('Task %s cancelled', $task->getName())); + * }); + * ``` + */ + public const ON_TASK_CANCEL = 'task-cancel'; + + /** + * Event raised when an operation of a {@link Task task} is done + * + * The task and the operation result are passed as parameters to the event callbacks. + * + * **Example usage:** + * + * ```php + * $scheduler->on($scheduler::ON_TASK_DONE, function (Task $task, $result) use ($logger) { + * $logger->info(sprintf('Operation of task %s done: %s', $task->getName(), $result)); + * }); + * ``` + */ + public const ON_TASK_DONE = 'task-done'; + + /** + * Event raised when an operation of a {@link Task task} failed + * + * The task and the {@link Throwable reason} why the operation failed + * are passed as parameters to the event callbacks. + * + * **Example usage:** + * + * ```php + * $scheduler->on($scheduler::ON_TASK_FAILED, function (Task $task, Throwable $e) use ($logger) { + * $logger->error( + * sprintf('Operation of task %s failed: %s', $task->getName(), $e), + * ['exception' => $e] + * ); + * }); + * ``` + */ + public const ON_TASK_FAILED = 'task-failed'; + + /** + * Event raised when a {@link Task task} operation is scheduled + * + * The task and the {@link DateTime time} when it should run + * are passed as parameters to the event callbacks. + * + * **Example usage:** + * + * ```php + * $scheduler->on($scheduler::ON_TASK_SCHEDULED, function (Task $task, DateTime $dateTime) use ($logger) { + * $logger->info(sprintf( + * 'Scheduling task %s to run at %s', + * $task->getName(), + * IntlDateFormatter::formatObject($dateTime) + * )); + * }); + * ``` + */ + public const ON_TASK_SCHEDULED = 'task-scheduled'; + + /** + * Event raised upon operation of a {@link Task task} + * + * The task and the possibly not yet completed result of the operation as a {@link ExtendedPromiseInterface promise} + * are passed as parameters to the event callbacks. + * + * **Example usage:** + * + * ```php + * $scheduler->on($scheduler::ON_TASK_OPERATION, function (Task $task, ExtendedPromiseInterface $_) use ($logger) { + * $logger->info(sprintf('Task %s operating', $task->getName())); + * }); + * ``` + */ + public const ON_TASK_RUN = 'task-run'; + + /** + * Event raised when a {@see Task task} is expired + * + * The task and the {@see DateTime expire time} are passed as parameters to the event callbacks. + * Note that the expiration time is the first time that is considered expired based on the frequency + * of the task and can be later than the specified end time. + * + * **Example usage:** + * + * ```php + * $scheduler->on(Scheduler::ON_TASK_EXPIRED, function (Task $task, DateTime $dateTime) use ($logger) { + * $logger->info(sprintf('Removing expired task %s at %s', $task->getName(), $dateTime->format('Y-m-d H:i:s'))); + * }); + * ``` + */ + public const ON_TASK_EXPIRED = 'task-expired'; + + /** @var SplObjectStorage The scheduled tasks of this scheduler */ + protected $tasks; + + public function __construct() + { + $this->tasks = new SplObjectStorage(); + + $this->promises = new SplObjectStorage(); + $this->timers = new SplObjectStorage(); + + $this->init(); + } + + /** + * Initialize this scheduler + */ + protected function init(): void + { + } + + /** + * Remove and cancel the given task + * + * @param Task $task + * + * @return $this + * + * @throws InvalidArgumentException If the given task isn't scheduled + */ + public function remove(Task $task): self + { + if (! $this->hasTask($task)) { + throw new InvalidArgumentException(sprintf('Task %s not scheduled', $task->getName())); + } + + $this->cancelTask($task); + + $this->tasks->detach($task); + + return $this; + } + + /** + * Remove and cancel all tasks + * + * @return $this + */ + public function removeTasks(): self + { + foreach ($this->tasks as $task) { + $this->cancelTask($task); + } + + $this->tasks = new SplObjectStorage(); + + return $this; + } + + /** + * Get whether the specified task is scheduled + * + * @param Task $task + * + * @return bool + */ + public function hasTask(Task $task): bool + { + return $this->tasks->contains($task); + } + + /** + * Schedule the given task based on the specified frequency + * + * @param Task $task + * @param Frequency $frequency + * + * @return $this + */ + public function schedule(Task $task, Frequency $frequency): self + { + $now = new DateTime(); + if ($frequency->isExpired($now)) { + return $this; + } + + if ($frequency->isDue($now)) { + Loop::futureTick(function () use ($task): void { + $promise = $this->runTask($task); + $this->emit(static::ON_TASK_RUN, [$task, $promise]); + }); + $this->emit(static::ON_TASK_SCHEDULED, [$task, $now]); + + if ($frequency instanceof OneOff) { + return $this; + } + } + + $loop = function () use (&$loop, $task, $frequency): void { + $promise = $this->runTask($task); + $this->emit(static::ON_TASK_RUN, [$task, $promise]); + + $now = new DateTime(); + $nextDue = $frequency->getNextDue($now); + if ($frequency instanceof OneOff || $frequency->isExpired($nextDue)) { + $removeTask = function () use ($task, $nextDue): void { + $this->remove($task); + $this->emit(static::ON_TASK_EXPIRED, [$task, $nextDue]); + }; + + if ($this->promises->contains($task->getUuid())) { + $pendingPromises = (array) $this->promises->offsetGet($task->getUuid()); + Promise\all($pendingPromises)->always($removeTask); + } else { + $removeTask(); + } + + return; + } + + $this->attachTimer( + $task->getUuid(), + Loop::addTimer($nextDue->getTimestamp() - $now->getTimestamp(), $loop) + ); + $this->emit(static::ON_TASK_SCHEDULED, [$task, $nextDue]); + }; + + $nextDue = $frequency->getNextDue($now); + $this->attachTimer( + $task->getUuid(), + Loop::addTimer($nextDue->getTimestamp() - $now->getTimestamp(), $loop) + ); + $this->emit(static::ON_TASK_SCHEDULED, [$task, $nextDue]); + + $this->tasks->attach($task); + + return $this; + } + + public function isValidEvent(string $event): bool + { + $events = array_flip([ + static::ON_TASK_CANCEL, + static::ON_TASK_DONE, + static::ON_TASK_EXPIRED, + static::ON_TASK_FAILED, + static::ON_TASK_RUN, + static::ON_TASK_SCHEDULED + ]); + + return isset($events[$event]); + } + + /** + * Cancel the timer of the task and all pending operations + * + * @param Task $task + */ + protected function cancelTask(Task $task): void + { + Loop::cancelTimer($this->detachTimer($task->getUuid())); + + /** @var ExtendedPromiseInterface[] $promises */ + $promises = $this->detachPromises($task->getUuid()); + if (! empty($promises)) { + /** @var Promise\CancellablePromiseInterface $promise */ + foreach ($promises as $promise) { + $promise->cancel(); + } + $this->emit(self::ON_TASK_CANCEL, [$task, $promises]); + } + } + + /** + * Runs the given task immediately and registers handlers for the returned promise + * + * @param Task $task + * + * @return ExtendedPromiseInterface + */ + protected function runTask(Task $task): ExtendedPromiseInterface + { + $promise = $task->run(); + $this->addPromise($task->getUuid(), $promise); + + return $promise->then( + function ($result) use ($task): void { + $this->emit(self::ON_TASK_DONE, [$task, $result]); + }, + function (Throwable $reason) use ($task): void { + $this->emit(self::ON_TASK_FAILED, [$task, $reason]); + } + )->always(function () use ($task, $promise): void { + // Unregister the promise without canceling it as it's already resolved + $this->removePromise($task->getUuid(), $promise); + }); + } +} diff --git a/vendor/ipl/scheduler/src/register_cron_aliases.php b/vendor/ipl/scheduler/src/register_cron_aliases.php new file mode 100644 index 0000000..2987248 --- /dev/null +++ b/vendor/ipl/scheduler/src/register_cron_aliases.php @@ -0,0 +1,11 @@ +