├── src ├── Event │ ├── TaskEventInterface.php │ ├── WorkerEventInterface.php │ ├── TaskUnscheduledEvent.php │ ├── WorkerPausedEvent.php │ ├── TaskFailedEvent.php │ ├── WorkerStoppedEvent.php │ ├── WorkerRestartedEvent.php │ ├── TaskScheduledEvent.php │ ├── SchedulerRebootedEvent.php │ ├── WorkerStartedEvent.php │ ├── WorkerForkedEvent.php │ ├── TaskExecutedEvent.php │ ├── WorkerRunningEvent.php │ ├── WorkerSleepingEvent.php │ └── TaskExecutingEvent.php ├── TaskBag │ ├── TaskBagInterface.php │ ├── AccessLockBag.php │ └── NotificationTaskBag.php ├── SchedulePolicy │ ├── CachedPolicyInterface.php │ ├── SchedulePolicyOrchestratorInterface.php │ ├── FirstInLastOutPolicy.php │ ├── IdlePolicy.php │ ├── FirstInFirstOutPolicy.php │ ├── MemoryUsagePolicy.php │ ├── PolicyInterface.php │ ├── ExecutionDurationPolicy.php │ ├── RoundRobinPolicy.php │ ├── BatchPolicy.php │ ├── NicePolicy.php │ └── SchedulePolicyOrchestrator.php ├── Exception │ ├── MiddlewareException.php │ ├── TransportException.php │ ├── ConfigurationException.php │ ├── UndefinedRunnerException.php │ ├── ExceptionInterface.php │ ├── UndefinedFormatterException.php │ ├── BadMethodCallException.php │ ├── AlreadyScheduledTaskException.php │ ├── InvalidArgumentException.php │ ├── LogicException.php │ ├── RuntimeException.php │ └── InvalidExpressionException.php ├── SchedulerAwareInterface.php ├── Middleware │ ├── RequiredMiddlewareInterface.php │ ├── MiddlewareStackInterface.php │ ├── OrderedMiddlewareInterface.php │ ├── ProbeTaskMiddleware.php │ ├── PreExecutionMiddlewareInterface.php │ ├── PostSchedulingMiddlewareInterface.php │ ├── PreSchedulingMiddlewareInterface.php │ ├── PostExecutionMiddlewareInterface.php │ ├── TaskExecutionMiddleware.php │ ├── WorkerMiddlewareStackInterface.php │ ├── TaskUpdateMiddleware.php │ ├── SchedulerMiddlewareStackInterface.php │ ├── WorkerMiddlewareStack.php │ ├── SchedulerMiddlewareStack.php │ ├── FiberAwareWorkerMiddlewareStack.php │ ├── SingleRunTaskMiddleware.php │ ├── FiberAwareSchedulerMiddlewareStack.php │ ├── MiddlewareRegistryInterface.php │ └── TaskLockBagMiddleware.php ├── Task │ ├── TaskBuilderInterface.php │ ├── NullTask.php │ ├── Builder │ │ ├── BuilderInterface.php │ │ ├── NullBuilder.php │ │ ├── ShellBuilder.php │ │ ├── HttpBuilder.php │ │ ├── CommandBuilder.php │ │ ├── ProbeTaskBuilder.php │ │ ├── AbstractTaskBuilder.php │ │ └── ChainedBuilder.php │ ├── TaskExecutionTrackerInterface.php │ ├── MessengerTask.php │ ├── FailedTask.php │ ├── Output.php │ ├── LazyTask.php │ ├── TaskBuilder.php │ ├── ChainedTask.php │ ├── TaskExecutionTracker.php │ ├── ProbeTask.php │ ├── NotificationTask.php │ └── CallbackTask.php ├── LazyInterface.php ├── Messenger │ ├── TaskToPauseMessage.php │ ├── TaskToYieldMessage.php │ ├── TaskToUpdateMessage.php │ ├── TaskToExecuteMessage.php │ ├── TaskToPauseMessageHandler.php │ ├── TaskToYieldMessageHandler.php │ ├── TaskToUpdateMessageHandler.php │ └── TaskToExecuteMessageHandler.php ├── Worker │ ├── WorkerRegistryInterface.php │ ├── ExecutionPolicy │ │ ├── ExecutionPolicyInterface.php │ │ ├── ExecutionPolicyRegistryInterface.php │ │ ├── DefaultPolicy.php │ │ └── FiberPolicy.php │ └── WorkerRegistry.php ├── Pool │ ├── SchedulerPoolInterface.php │ ├── SchedulerPool.php │ └── Configuration │ │ └── SchedulerConfiguration.php ├── Expression │ ├── BuilderInterface.php │ ├── CronExpressionBuilder.php │ ├── ExpressionBuilderInterface.php │ ├── ExpressionBuilder.php │ ├── ExactExpressionBuilder.php │ ├── FluentExpressionBuilder.php │ └── ComputedExpressionBuilder.php ├── Transport │ ├── Configuration │ │ ├── ConfigurationRegistryInterface.php │ │ ├── ConfigurationFactoryInterface.php │ │ ├── InMemoryConfigurationFactory.php │ │ ├── CacheConfigurationFactory.php │ │ ├── LongTailConfigurationFactory.php │ │ ├── FailOverConfigurationFactory.php │ │ ├── LongTailConfiguration.php │ │ ├── ConfigurationFactory.php │ │ ├── FiberConfigurationFactory.php │ │ ├── LazyConfigurationFactory.php │ │ ├── FailOverConfiguration.php │ │ ├── AbstractConfiguration.php │ │ ├── ConfigurationRegistry.php │ │ ├── ExternalConnectionInterface.php │ │ ├── ConfigurationInterface.php │ │ ├── AbstractExternalConfiguration.php │ │ └── AbstractCompoundConfigurationFactory.php │ ├── TransportRegistryInterface.php │ ├── TransportFactoryInterface.php │ ├── LongTailTransport.php │ ├── InMemoryTransportFactory.php │ ├── CacheTransportFactory.php │ ├── TransportFactory.php │ ├── LongTailTransportFactory.php │ ├── FailOverTransport.php │ ├── FilesystemTransportFactory.php │ ├── RoundRobinTransportFactory.php │ ├── FailOverTransportFactory.php │ ├── TransportRegistry.php │ ├── LazyTransportFactory.php │ ├── FiberTransportFactory.php │ ├── AbstractTransport.php │ └── AbstractCompoundTransportFactory.php ├── Bridge │ ├── Doctrine │ │ ├── SchemaAwareInterface.php │ │ ├── Transport │ │ │ ├── Configuration │ │ │ │ ├── DoctrineConfiguration.php │ │ │ │ └── DoctrineConfigurationFactory.php │ │ │ └── DoctrineTransport.php │ │ └── Connection │ │ │ └── AbstractDoctrineConnection.php │ └── Redis │ │ └── Transport │ │ └── RedisTransport.php ├── Runner │ ├── NullTaskRunner.php │ ├── RunnerRegistryInterface.php │ ├── RunnerInterface.php │ ├── CallbackTaskRunner.php │ ├── ChainedTaskRunner.php │ ├── MessengerTaskRunner.php │ ├── HttpTaskRunner.php │ ├── NotificationTaskRunner.php │ ├── ShellTaskRunner.php │ ├── ProbeTaskRunner.php │ └── RunnerRegistry.php ├── Test │ └── Constraint │ │ ├── Probe │ │ ├── ProbeEnabled.php │ │ ├── ProbeFailedTask.php │ │ ├── ProbeExecutedTask.php │ │ ├── ProbeScheduledTask.php │ │ └── ProbeState.php │ │ ├── Scheduler │ │ └── SchedulerDueTask.php │ │ ├── TaskScheduled.php │ │ ├── TaskFailed.php │ │ ├── TaskUnscheduled.php │ │ ├── TaskQueued.php │ │ └── TaskExecuted.php ├── SchedulerBundle.php ├── Probe │ ├── ProbeInterface.php │ └── Probe.php ├── Fiber │ └── AbstractFiberHandler.php ├── EventListener │ ├── StopWorkerOnTaskLimitSubscriber.php │ ├── TaskLoggerSubscriber.php │ ├── ProbeStateSubscriber.php │ ├── StopWorkerOnTimeLimitSubscriber.php │ ├── StopWorkerOnFailureLimitSubscriber.php │ └── TaskLifecycleSubscriber.php ├── DependencyInjection │ └── SchedulerPass.php └── Command │ ├── DebugConfigurationCommand.php │ └── ListFailedTasksCommand.php └── phpstan.neon.8.2.dist /src/Event/TaskEventInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface TaskEventInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/TaskBag/TaskBagInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface TaskBagInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/SchedulePolicy/CachedPolicyInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface CachedPolicyInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Exception/MiddlewareException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class MiddlewareException extends RuntimeException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Exception/TransportException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class TransportException extends RuntimeException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Exception/ConfigurationException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class ConfigurationException extends RuntimeException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Exception/UndefinedRunnerException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class UndefinedRunnerException extends RuntimeException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface ExceptionInterface extends Throwable 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/UndefinedFormatterException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class UndefinedFormatterException extends RuntimeException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface SchedulerAwareInterface 11 | { 12 | public function schedule(SchedulerInterface $scheduler): void; 13 | } 14 | -------------------------------------------------------------------------------- /src/Exception/AlreadyScheduledTaskException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class AlreadyScheduledTaskException extends RuntimeException implements ExceptionInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Middleware/RequiredMiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface RequiredMiddlewareInterface 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class LogicException extends InternalLogicException implements ExceptionInterface 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class RuntimeException extends InternalRuntimeException implements ExceptionInterface 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/InvalidExpressionException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class InvalidExpressionException extends InvalidArgumentException implements ExceptionInterface 13 | { 14 | } 15 | -------------------------------------------------------------------------------- /src/Task/TaskBuilderInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface TaskBuilderInterface 11 | { 12 | /** 13 | * @param array $options 14 | */ 15 | public function create(array $options = []): TaskInterface; 16 | } 17 | -------------------------------------------------------------------------------- /src/LazyInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface LazyInterface 11 | { 12 | /** 13 | * Define if the current implementation has been initialized, the implementation is up to the final class. 14 | */ 15 | public function isInitialized(): bool; 16 | } 17 | -------------------------------------------------------------------------------- /src/Messenger/TaskToPauseMessage.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class TaskToPauseMessage 11 | { 12 | public function __construct(private string $task) 13 | { 14 | } 15 | 16 | public function getTask(): string 17 | { 18 | return $this->task; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Messenger/TaskToYieldMessage.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class TaskToYieldMessage 11 | { 12 | public function __construct(private string $name) 13 | { 14 | } 15 | 16 | public function getName(): string 17 | { 18 | return $this->name; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Event/WorkerEventInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface WorkerEventInterface 13 | { 14 | /** 15 | * Return the current {@see WorkerInterface} even if it's a fork. 16 | */ 17 | public function getWorker(): WorkerInterface; 18 | } 19 | -------------------------------------------------------------------------------- /src/Middleware/MiddlewareStackInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface MiddlewareStackInterface 11 | { 12 | /** 13 | * Return the middleware used by a specific middleware stack. 14 | * 15 | * @return array 16 | */ 17 | public function getMiddlewareList(): array; 18 | } 19 | -------------------------------------------------------------------------------- /src/Worker/WorkerRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface WorkerRegistryInterface extends Countable 13 | { 14 | /** 15 | * Return the workers. 16 | * 17 | * @return array 18 | */ 19 | public function getWorkers(): iterable; 20 | } 21 | -------------------------------------------------------------------------------- /src/TaskBag/AccessLockBag.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class AccessLockBag implements TaskBagInterface 13 | { 14 | public function __construct(private ?Key $key = null) 15 | { 16 | } 17 | 18 | public function getKey(): ?Key 19 | { 20 | return $this->key; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Pool/SchedulerPoolInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface SchedulerPoolInterface extends Countable 14 | { 15 | public function add(string $endpoint, SchedulerInterface $scheduler): void; 16 | 17 | public function get(string $endpoint): SchedulerInterface; 18 | } 19 | -------------------------------------------------------------------------------- /src/Task/NullTask.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class NullTask extends AbstractTask 11 | { 12 | /** 13 | * @param array $options 14 | */ 15 | public function __construct(string $name, array $options = []) 16 | { 17 | $this->defineOptions(options: $options); 18 | 19 | parent::__construct(name: $name); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Middleware/OrderedMiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface OrderedMiddlewareInterface 11 | { 12 | /** 13 | * Defines the priority of the middleware when filtering it in the corresponding stack. 14 | * 15 | * The closer the priority to 1, the earlier the middleware is called. 16 | */ 17 | public function getPriority(): int; 18 | } 19 | -------------------------------------------------------------------------------- /src/Event/TaskUnscheduledEvent.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class TaskUnscheduledEvent extends Event implements TaskEventInterface 13 | { 14 | public function __construct(private string $task) 15 | { 16 | } 17 | 18 | public function getTask(): string 19 | { 20 | return $this->task; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Event/WorkerPausedEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class WorkerPausedEvent extends Event 14 | { 15 | public function __construct(private WorkerInterface $worker) 16 | { 17 | } 18 | 19 | public function getWorker(): WorkerInterface 20 | { 21 | return $this->worker; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Expression/BuilderInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface BuilderInterface 11 | { 12 | /** 13 | * The builder must return a valid {@see Expression} using both: 14 | * 15 | * - The submitted @param string $expression 16 | * - The submitted @param string|null $timezone 17 | */ 18 | public function build(string $expression, ?string $timezone = null): Expression; 19 | } 20 | -------------------------------------------------------------------------------- /src/Event/TaskFailedEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class TaskFailedEvent extends Event implements TaskEventInterface 14 | { 15 | public function __construct(private FailedTask $task) 16 | { 17 | } 18 | 19 | public function getTask(): FailedTask 20 | { 21 | return $this->task; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Event/WorkerStoppedEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class WorkerStoppedEvent extends Event 14 | { 15 | public function __construct(private WorkerInterface $worker) 16 | { 17 | } 18 | 19 | public function getWorker(): WorkerInterface 20 | { 21 | return $this->worker; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Event/WorkerRestartedEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class WorkerRestartedEvent extends Event 14 | { 15 | public function __construct(private WorkerInterface $worker) 16 | { 17 | } 18 | 19 | public function getWorker(): WorkerInterface 20 | { 21 | return $this->worker; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Event/TaskScheduledEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class TaskScheduledEvent extends Event implements TaskEventInterface 14 | { 15 | public function __construct(private TaskInterface $task) 16 | { 17 | } 18 | 19 | public function getTask(): TaskInterface 20 | { 21 | return $this->task; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Event/SchedulerRebootedEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class SchedulerRebootedEvent extends Event 14 | { 15 | public function __construct(private SchedulerInterface $scheduler) 16 | { 17 | } 18 | 19 | public function getScheduler(): SchedulerInterface 20 | { 21 | return $this->scheduler; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Event/WorkerStartedEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class WorkerStartedEvent extends Event implements WorkerEventInterface 14 | { 15 | public function __construct(private WorkerInterface $worker) 16 | { 17 | } 18 | 19 | public function getWorker(): WorkerInterface 20 | { 21 | return $this->worker; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpstan.neon.8.2.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - vendor/phpstan/phpstan-deprecation-rules/rules.neon 3 | - vendor/phpstan/phpstan-doctrine/extension.neon 4 | - vendor/phpstan/phpstan-phpunit/extension.neon 5 | - vendor/phpstan/phpstan-strict-rules/rules.neon 6 | - vendor/phpstan/phpstan-symfony/extension.neon 7 | #- vendor/tomasvotruba/cognitive-complexity/config/extension.neon 8 | 9 | parameters: 10 | level: 8 11 | paths: 12 | - src 13 | - tests 14 | excludePaths: 15 | - vendor 16 | #cognitive_complexity: 17 | # class: 50 18 | # function: 8 19 | -------------------------------------------------------------------------------- /src/Task/Builder/BuilderInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface BuilderInterface 14 | { 15 | /** 16 | * @param array $options 17 | */ 18 | public function build(PropertyAccessorInterface $propertyAccessor, array $options = []): TaskInterface; 19 | 20 | public function support(?string $type = null): bool; 21 | } 22 | -------------------------------------------------------------------------------- /src/Transport/Configuration/ConfigurationRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 13 | * 14 | * @extends IteratorAggregate 15 | */ 16 | interface ConfigurationRegistryInterface extends Countable, IteratorAggregate 17 | { 18 | public function usort(Closure $func): ConfigurationRegistryInterface; 19 | 20 | public function reset(): ConfigurationInterface; 21 | } 22 | -------------------------------------------------------------------------------- /src/Messenger/TaskToUpdateMessage.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class TaskToUpdateMessage 13 | { 14 | public function __construct(private string $taskName, private TaskInterface $task) 15 | { 16 | } 17 | 18 | public function getTaskName(): string 19 | { 20 | return $this->taskName; 21 | } 22 | 23 | public function getTask(): TaskInterface 24 | { 25 | return $this->task; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Bridge/Doctrine/SchemaAwareInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface SchemaAwareInterface 14 | { 15 | /** 16 | * Allo the transport and/or configuration to interact with the @param Connection $dbalConnection. 17 | * 18 | * The schema can be defined using @param Schema $schema. 19 | */ 20 | public function configureSchema(Schema $schema, Connection $dbalConnection): void; 21 | } 22 | -------------------------------------------------------------------------------- /src/Worker/ExecutionPolicy/ExecutionPolicyInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface ExecutionPolicyInterface 14 | { 15 | public function execute( 16 | TaskListInterface $toExecuteTasks, 17 | Closure $handleTaskFunc 18 | ): void; 19 | 20 | /** 21 | * Determine if the current policy supports the given @param string $policy. 22 | */ 23 | public function support(string $policy): bool; 24 | } 25 | -------------------------------------------------------------------------------- /src/Messenger/TaskToExecuteMessage.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class TaskToExecuteMessage 13 | { 14 | public function __construct( 15 | private TaskInterface $task, 16 | private int $workerTimeout = 1 17 | ) { 18 | } 19 | 20 | public function getTask(): TaskInterface 21 | { 22 | return $this->task; 23 | } 24 | 25 | public function getWorkerTimeout(): int 26 | { 27 | return $this->workerTimeout; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Messenger/TaskToPauseMessageHandler.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | #[AsMessageHandler] 14 | final class TaskToPauseMessageHandler 15 | { 16 | public function __construct(private SchedulerInterface $scheduler) 17 | { 18 | } 19 | 20 | public function __invoke(TaskToPauseMessage $taskToPauseMessage): void 21 | { 22 | $this->scheduler->pause($taskToPauseMessage->getTask()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Messenger/TaskToYieldMessageHandler.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | #[AsMessageHandler] 14 | final class TaskToYieldMessageHandler 15 | { 16 | public function __construct(private SchedulerInterface $scheduler) 17 | { 18 | } 19 | 20 | public function __invoke(TaskToYieldMessage $taskToYieldMessage): void 21 | { 22 | $this->scheduler->yieldTask($taskToYieldMessage->getName()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Messenger/TaskToUpdateMessageHandler.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | #[AsMessageHandler] 14 | final class TaskToUpdateMessageHandler 15 | { 16 | public function __construct(private TransportInterface $transport) 17 | { 18 | } 19 | 20 | public function __invoke(TaskToUpdateMessage $message): void 21 | { 22 | $this->transport->update($message->getTaskName(), $message->getTask()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Task/TaskExecutionTrackerInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface TaskExecutionTrackerInterface 11 | { 12 | /** 13 | * Allow to track a task, the task must authorize the "tracking" thanks to {@see AbstractTask::setTracked()}. 14 | */ 15 | public function startTracking(TaskInterface $task): void; 16 | 17 | /** 18 | * End the tracking of a task, the execution time is available via {@see AbstractTask::getExecutionComputationTime()}. 19 | */ 20 | public function endTracking(TaskInterface $task): void; 21 | } 22 | -------------------------------------------------------------------------------- /src/Task/MessengerTask.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class MessengerTask extends AbstractTask 11 | { 12 | public function __construct(string $name, private object $message) 13 | { 14 | $this->defineOptions(); 15 | 16 | parent::__construct(name: $name); 17 | } 18 | 19 | public function getMessage(): object 20 | { 21 | return $this->message; 22 | } 23 | 24 | public function setMessage(object $message): self 25 | { 26 | $this->message = $message; 27 | 28 | return $this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Worker/ExecutionPolicy/ExecutionPolicyRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface ExecutionPolicyRegistryInterface extends Countable 14 | { 15 | /** 16 | * Return a {@see ExecutionPolicyInterface} that supports the @param string $policy. 17 | */ 18 | public function find(string $policy): ExecutionPolicyInterface; 19 | 20 | public function filter(Closure $func): ExecutionPolicyRegistryInterface; 21 | 22 | public function current(): ExecutionPolicyInterface; 23 | } 24 | -------------------------------------------------------------------------------- /src/Event/WorkerForkedEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class WorkerForkedEvent extends Event 14 | { 15 | public function __construct(private WorkerInterface $forkedWorker, private WorkerInterface $newWorker) 16 | { 17 | } 18 | 19 | public function getForkedWorker(): WorkerInterface 20 | { 21 | return $this->forkedWorker; 22 | } 23 | 24 | public function getNewWorker(): WorkerInterface 25 | { 26 | return $this->newWorker; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Event/TaskExecutedEvent.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class TaskExecutedEvent extends Event implements TaskEventInterface 15 | { 16 | public function __construct(private TaskInterface $task, private Output $output) 17 | { 18 | } 19 | 20 | public function getTask(): TaskInterface 21 | { 22 | return $this->task; 23 | } 24 | 25 | public function getOutput(): Output 26 | { 27 | return $this->output; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Event/WorkerRunningEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class WorkerRunningEvent extends Event implements WorkerEventInterface 14 | { 15 | public function __construct( 16 | private WorkerInterface $worker, 17 | private bool $isIdle = false 18 | ) { 19 | } 20 | 21 | public function getWorker(): WorkerInterface 22 | { 23 | return $this->worker; 24 | } 25 | 26 | public function isIdle(): bool 27 | { 28 | return $this->isIdle; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/SchedulePolicy/SchedulePolicyOrchestratorInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface SchedulePolicyOrchestratorInterface 14 | { 15 | /** 16 | * Sort tasks using @param string $policy to decide which policy must be applied. 17 | * 18 | * @param TaskListInterface $tasks 19 | * 20 | * @return TaskListInterface 21 | */ 22 | public function sort(string $policy, TaskListInterface $tasks): TaskListInterface; 23 | } 24 | -------------------------------------------------------------------------------- /src/Expression/CronExpressionBuilder.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class CronExpressionBuilder implements ExpressionBuilderInterface 13 | { 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | public function build(string $expression, string $timezone = 'UTC'): Expression 18 | { 19 | return Expression::createFromString($expression); 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function support(string $expression): bool 26 | { 27 | return CronExpression::isValidExpression($expression); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Middleware/ProbeTaskMiddleware.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ProbeTaskMiddleware implements PreExecutionMiddlewareInterface 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function preExecute(TaskInterface $task): void 19 | { 20 | if (!$task instanceof ProbeTask) { 21 | return; 22 | } 23 | 24 | if (0 === $task->getDelay()) { 25 | return; 26 | } 27 | 28 | usleep(microseconds: $task->getDelay()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Event/WorkerSleepingEvent.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class WorkerSleepingEvent extends Event implements WorkerEventInterface 14 | { 15 | public function __construct( 16 | private int $sleepDuration, 17 | private WorkerInterface $worker 18 | ) { 19 | } 20 | 21 | public function getSleepDuration(): int 22 | { 23 | return $this->sleepDuration; 24 | } 25 | 26 | public function getWorker(): WorkerInterface 27 | { 28 | return $this->worker; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Middleware/PreExecutionMiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface PreExecutionMiddlewareInterface 15 | { 16 | /** 17 | * Allow executing logic BEFORE executing the task, the @param TaskInterface $task is the one passed through {@see SchedulerInterface::getDueTasks()} 18 | * or {@see SchedulerInterface::getTasks()}. 19 | * 20 | * @throws Throwable If an error|exception occurs, it must be thrown back. 21 | */ 22 | public function preExecute(TaskInterface $task): void; 23 | } 24 | -------------------------------------------------------------------------------- /src/Middleware/PostSchedulingMiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface PostSchedulingMiddlewareInterface 15 | { 16 | /** 17 | * Allow executing logic after scheduling the task (the @param TaskInterface $task is the one returned via {@see SchedulerInterface::schedule()} and AFTER the transport stores it). 18 | * 19 | * @throws Throwable If an error|exception occurs, it must be thrown back. 20 | */ 21 | public function postScheduling(TaskInterface $task, SchedulerInterface $scheduler): void; 22 | } 23 | -------------------------------------------------------------------------------- /src/Middleware/PreSchedulingMiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface PreSchedulingMiddlewareInterface 15 | { 16 | /** 17 | * Allow to execute logic before scheduling the task 18 | * (the @param TaskInterface $task is the one passed through {@see SchedulerInterface::schedule()} and BEFORE any modification). 19 | * 20 | * 21 | * @throws Throwable If an error|exception occurs, it must be thrown back. 22 | */ 23 | public function preScheduling(TaskInterface $task, SchedulerInterface $scheduler): void; 24 | } 25 | -------------------------------------------------------------------------------- /src/Middleware/PostExecutionMiddlewareInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface PostExecutionMiddlewareInterface 15 | { 16 | /** 17 | * Allow executing logic AFTER executing the task, 18 | * the @param TaskInterface $task is the one returned via {@see SchedulerInterface::getDueTasks()} or 19 | * {@see SchedulerInterface::getTasks()}. 20 | * 21 | * @throws Throwable If an error|exception occurs, it must be thrown back. 22 | */ 23 | public function postExecute(TaskInterface $task, WorkerInterface $worker): void; 24 | } 25 | -------------------------------------------------------------------------------- /src/Runner/NullTaskRunner.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class NullTaskRunner implements RunnerInterface 16 | { 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | public function run(TaskInterface $task, WorkerInterface $worker): Output 21 | { 22 | return new Output(task: $task, output: null); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function support(TaskInterface $task): bool 29 | { 30 | return $task instanceof NullTask; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Transport/Configuration/ConfigurationFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface ConfigurationFactoryInterface 14 | { 15 | /** 16 | * Create a new configuration (which can contains the options stored in @param Dsn $dsn) and receive the @param SerializerInterface $serializer. 17 | */ 18 | public function create(Dsn $dsn, SerializerInterface $serializer): ConfigurationInterface; 19 | 20 | /** 21 | * Determine if the factory can create a configuration using @param string $dsn. 22 | */ 23 | public function support(string $dsn): bool; 24 | } 25 | -------------------------------------------------------------------------------- /src/SchedulePolicy/FirstInLastOutPolicy.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class FirstInLastOutPolicy implements PolicyInterface 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function sort(TaskListInterface $tasks): TaskListInterface 19 | { 20 | return $tasks->uasort(func: static fn (TaskInterface $task, TaskInterface $nextTask): int => $task->getScheduledAt() <=> $nextTask->getScheduledAt()); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function support(string $policy): bool 27 | { 28 | return 'first_in_last_out' === $policy; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/SchedulePolicy/IdlePolicy.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class IdlePolicy implements PolicyInterface 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function sort(TaskListInterface $tasks): TaskListInterface 19 | { 20 | return $tasks->uasort(func: static fn (TaskInterface $task, TaskInterface $nextTask): int => $task->getPriority() <= 19 && $task->getPriority() < $nextTask->getPriority() ? 1 : -1); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function support(string $policy): bool 27 | { 28 | return 'idle' === $policy; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/SchedulePolicy/FirstInFirstOutPolicy.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class FirstInFirstOutPolicy implements PolicyInterface 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function sort(TaskListInterface $tasks): TaskListInterface 19 | { 20 | return $tasks->uasort(func: static fn (TaskInterface $task, TaskInterface $nextTask): int => $task->getScheduledAt() <=> $nextTask->getScheduledAt()); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function support(string $policy): bool 27 | { 28 | return 'first_in_first_out' === $policy; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Expression/ExpressionBuilderInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface ExpressionBuilderInterface 13 | { 14 | /** 15 | * The builder must build a valid {@see ExpressionBuilder} using both: 16 | * 17 | * - The submitted @param string $expression 18 | * - The submitted @param string $timezone 19 | * 20 | * @throws Throwable Not required, depends on the builder implementation. 21 | */ 22 | public function build(string $expression, string $timezone = 'UTC'): Expression; 23 | 24 | /** 25 | * Define if the builder can build an expression using the submitted <@param string $expression. 26 | */ 27 | public function support(string $expression): bool; 28 | } 29 | -------------------------------------------------------------------------------- /src/SchedulePolicy/MemoryUsagePolicy.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class MemoryUsagePolicy implements PolicyInterface 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function sort(TaskListInterface $tasks): TaskListInterface 19 | { 20 | return $tasks->uasort(func: static fn (TaskInterface $task, TaskInterface $nextTask): int => $task->getExecutionMemoryUsage() <=> $nextTask->getExecutionMemoryUsage()); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function support(string $policy): bool 27 | { 28 | return 'memory_usage' === $policy; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Test/Constraint/Probe/ProbeEnabled.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class ProbeEnabled extends Constraint 15 | { 16 | public function __construct(private bool $expectedState) 17 | { 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function toString(): string 24 | { 25 | return sprintf('match the current probe state, current state: %s', $this->expectedState ? 'enabled' : 'disabled'); 26 | } 27 | 28 | /** 29 | * @param mixed|bool $other 30 | */ 31 | protected function matches($other): bool 32 | { 33 | return $this->expectedState === $other; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Middleware/TaskExecutionMiddleware.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class TaskExecutionMiddleware implements PreExecutionMiddlewareInterface, OrderedMiddlewareInterface, RequiredMiddlewareInterface 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public function preExecute(TaskInterface $task): void 20 | { 21 | $executionDelay = $task->getExecutionDelay(); 22 | 23 | if (null !== $executionDelay) { 24 | usleep(microseconds: $executionDelay); 25 | } 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function getPriority(): int 32 | { 33 | return 1; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SchedulerBundle.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class SchedulerBundle extends Bundle 16 | { 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | public function getContainerExtension(): SchedulerBundleExtension 21 | { 22 | return new SchedulerBundleExtension(); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function build(ContainerBuilder $container): void 29 | { 30 | $container->addCompilerPass(pass: new SchedulerPass()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Transport/Configuration/InMemoryConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class InMemoryConfigurationFactory implements ConfigurationFactoryInterface 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function create(Dsn $dsn, SerializerInterface $serializer): InMemoryConfiguration 19 | { 20 | return new InMemoryConfiguration(); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function support(string $dsn): bool 27 | { 28 | return str_starts_with($dsn, 'configuration://memory') || str_starts_with($dsn, 'configuration://array'); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/SchedulePolicy/PolicyInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface PolicyInterface 14 | { 15 | /** 16 | * The current sort logic is up to the implementation, if needed, the {@see TaskListInterface::uasort()} method can be used. 17 | * 18 | * @param TaskListInterface $tasks 19 | * 20 | * @return TaskListInterface 21 | */ 22 | public function sort(TaskListInterface $tasks): TaskListInterface; 23 | 24 | /** 25 | * Define if the @param string $policy is supported by the current policy. 26 | */ 27 | public function support(string $policy): bool; 28 | } 29 | -------------------------------------------------------------------------------- /src/Runner/RunnerRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface RunnerRegistryInterface extends Countable 15 | { 16 | /** 17 | * Find a {@see RunnerInterface} depending on a given task. 18 | */ 19 | public function find(TaskInterface $task): RunnerInterface; 20 | 21 | /** 22 | * Filter the runners using @param Closure $func. 23 | * 24 | * A new {@see RunnerRegistryInterface} is returned with the filtered runners. 25 | */ 26 | public function filter(Closure $func): RunnerRegistryInterface; 27 | 28 | /** 29 | * Return the current runner {@see current()} 30 | */ 31 | public function current(): RunnerInterface; 32 | } 33 | -------------------------------------------------------------------------------- /src/SchedulePolicy/ExecutionDurationPolicy.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ExecutionDurationPolicy implements PolicyInterface 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function sort(TaskListInterface $tasks): TaskListInterface 19 | { 20 | return $tasks->uasort(func: static fn (TaskInterface $task, TaskInterface $nextTask): int => $task->getExecutionComputationTime() <=> $nextTask->getExecutionComputationTime()); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function support(string $policy): bool 27 | { 28 | return 'execution_duration' === $policy; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Task/Builder/NullBuilder.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class NullBuilder extends AbstractTaskBuilder implements BuilderInterface 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public function build(PropertyAccessorInterface $propertyAccessor, array $options = []): TaskInterface 20 | { 21 | return $this->handleTaskAttributes(new NullTask($options['name']), $options, $propertyAccessor); 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function support(?string $type = null): bool 28 | { 29 | return 'null' === $type; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Pool/SchedulerPool.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class SchedulerPool implements SchedulerPoolInterface 15 | { 16 | /** 17 | * @var array 18 | */ 19 | private array $schedulers = []; 20 | 21 | public function add(string $endpoint, SchedulerInterface $scheduler): void 22 | { 23 | $this->schedulers[$endpoint] = $scheduler; 24 | } 25 | 26 | public function get(string $endpoint): SchedulerInterface 27 | { 28 | return $this->schedulers[$endpoint]; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function count(): int 35 | { 36 | return count(value: $this->schedulers); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Worker/ExecutionPolicy/DefaultPolicy.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class DefaultPolicy implements ExecutionPolicyInterface 15 | { 16 | public function execute( 17 | TaskListInterface $toExecuteTasks, 18 | Closure $handleTaskFunc 19 | ): void { 20 | $toExecuteTasks->walk(func: static function (TaskInterface $task) use ($handleTaskFunc, $toExecuteTasks): void { 21 | $handleTaskFunc($task, $toExecuteTasks); 22 | }); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function support(string $policy): bool 29 | { 30 | return 'default' === $policy; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Task/FailedTask.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class FailedTask extends AbstractTask 15 | { 16 | private DateTimeImmutable $failedAt; 17 | 18 | public function __construct(private TaskInterface $task, private string $reason) 19 | { 20 | $this->failedAt = new DateTimeImmutable(); 21 | 22 | parent::__construct(name: sprintf('%s.failed', $task->getName())); 23 | } 24 | 25 | public function getTask(): TaskInterface 26 | { 27 | return $this->task; 28 | } 29 | 30 | public function getReason(): string 31 | { 32 | return $this->reason; 33 | } 34 | 35 | public function getFailedAt(): DateTimeImmutable 36 | { 37 | return $this->failedAt; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/SchedulePolicy/RoundRobinPolicy.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class RoundRobinPolicy implements PolicyInterface 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function sort(TaskListInterface $tasks): TaskListInterface 19 | { 20 | return $tasks->uasort(func: static fn (TaskInterface $task, TaskInterface $nextTask): int => $task->getExecutionComputationTime() >= $task->getMaxDuration() && $task->getExecutionComputationTime() < $nextTask->getExecutionComputationTime() ? 1 : -1); 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function support(string $policy): bool 27 | { 28 | return 'round_robin' === $policy; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Test/Constraint/Probe/ProbeFailedTask.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class ProbeFailedTask extends Constraint 16 | { 17 | public function __construct(private int $expectedCount) 18 | { 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function toString(): string 25 | { 26 | return sprintf('has found %s failed task%s', $this->expectedCount, 1 < $this->expectedCount ? 's' : ''); 27 | } 28 | 29 | /** 30 | * @param mixed|ProbeInterface $other 31 | */ 32 | protected function matches($other): bool 33 | { 34 | return $this->expectedCount === $other->getFailedTasks(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Test/Constraint/Probe/ProbeExecutedTask.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class ProbeExecutedTask extends Constraint 16 | { 17 | public function __construct(private int $expectedCount) 18 | { 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function toString(): string 25 | { 26 | return sprintf('has found %s executed task%s', $this->expectedCount, 1 < $this->expectedCount ? 's' : ''); 27 | } 28 | 29 | /** 30 | * @param mixed|ProbeInterface $other 31 | */ 32 | protected function matches($other): bool 33 | { 34 | return $this->expectedCount === $other->getExecutedTasks(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Test/Constraint/Scheduler/SchedulerDueTask.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class SchedulerDueTask extends Constraint 16 | { 17 | public function __construct(private int $expectedCount) 18 | { 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function toString(): string 25 | { 26 | return sprintf('%s %s due', $this->expectedCount, $this->expectedCount > 1 ? 'are' : 'is'); 27 | } 28 | 29 | /** 30 | * @param mixed|SchedulerInterface $other 31 | */ 32 | protected function matches($other): bool 33 | { 34 | return $this->expectedCount === $other->getDueTasks()->count(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Transport/TransportRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 14 | * 15 | * @extends IteratorAggregate 16 | */ 17 | interface TransportRegistryInterface extends Countable, IteratorAggregate 18 | { 19 | /** 20 | * Return the sorted transports using @param Closure $func. 21 | * 22 | * @return TransportRegistryInterface 23 | */ 24 | public function usort(Closure $func): TransportRegistryInterface; 25 | 26 | /** 27 | * Reset the internal pointer to the first transport available. 28 | * 29 | * @throws RuntimeException If no transport is found. 30 | */ 31 | public function reset(): TransportInterface; 32 | } 33 | -------------------------------------------------------------------------------- /src/Test/Constraint/Probe/ProbeScheduledTask.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class ProbeScheduledTask extends Constraint 16 | { 17 | public function __construct(private int $expectedCount) 18 | { 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function toString(): string 25 | { 26 | return sprintf('has found %s scheduled task%s', $this->expectedCount, 1 < $this->expectedCount ? 's' : ''); 27 | } 28 | 29 | /** 30 | * @param mixed|ProbeInterface $other 31 | */ 32 | protected function matches($other): bool 33 | { 34 | return $this->expectedCount === $other->getScheduledTasks(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Middleware/WorkerMiddlewareStackInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface WorkerMiddlewareStackInterface extends MiddlewareStackInterface 15 | { 16 | /** 17 | * @throws Throwable {@see PreExecutionMiddlewareInterface::preExecute()} 18 | * @throws Throwable {@see AbstractMiddlewareStack::runMiddleware()} 19 | */ 20 | public function runPreExecutionMiddleware(TaskInterface $task): void; 21 | 22 | /** 23 | * @throws Throwable {@see PostExecutionMiddlewareInterface::postExecute()} 24 | * @throws Throwable {@see AbstractMiddlewareStack::runMiddleware()} 25 | */ 26 | public function runPostExecutionMiddleware(TaskInterface $task, WorkerInterface $worker): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/TaskBag/NotificationTaskBag.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class NotificationTaskBag implements TaskBagInterface 14 | { 15 | /** 16 | * @var Recipient[] 17 | */ 18 | private array $recipients; 19 | 20 | public function __construct( 21 | private Notification $notification, 22 | Recipient ...$recipients 23 | ) { 24 | $this->recipients = $recipients; 25 | } 26 | 27 | public function getNotification(): Notification 28 | { 29 | return $this->notification; 30 | } 31 | 32 | /** 33 | * @return Recipient[] 34 | */ 35 | public function getRecipients(): array 36 | { 37 | return $this->recipients; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Bridge/Redis/Transport/RedisTransport.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class RedisTransport extends AbstractExternalTransport 16 | { 17 | public function __construct( 18 | ConfigurationInterface $configuration, 19 | SerializerInterface $serializer, 20 | SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator 21 | ) { 22 | parent::__construct( 23 | $configuration, 24 | new Connection($configuration, $serializer), 25 | $schedulePolicyOrchestrator 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Middleware/TaskUpdateMiddleware.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class TaskUpdateMiddleware implements PostExecutionMiddlewareInterface, OrderedMiddlewareInterface, RequiredMiddlewareInterface 15 | { 16 | public function __construct(private TransportInterface $transport) 17 | { 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function postExecute(TaskInterface $task, WorkerInterface $worker): void 24 | { 25 | $this->transport->update(name: $task->getName(), updatedTask: $task); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function getPriority(): int 32 | { 33 | return 10; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Probe/ProbeInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface ProbeInterface 15 | { 16 | /** 17 | * Return the amount of executed tasks, the {@see TaskInterface::getState()} value is not relevant. 18 | */ 19 | public function getExecutedTasks(): int; 20 | 21 | /** 22 | * Return the amount of failed tasks during the latest worker execution. 23 | * 24 | * @see WorkerInterface::getFailedTasks() 25 | */ 26 | public function getFailedTasks(): int; 27 | 28 | /** 29 | * Return the amount of scheduled tasks via {@see SchedulerInterface::getTasks()} 30 | * 31 | * @throws Throwable {@see SchedulerInterface::getTasks()} 32 | */ 33 | public function getScheduledTasks(): int; 34 | } 35 | -------------------------------------------------------------------------------- /src/Transport/TransportFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface TransportFactoryInterface 15 | { 16 | /** 17 | * @param array $options 18 | */ 19 | public function createTransport( 20 | Dsn $dsn, 21 | array $options, 22 | ConfigurationInterface $configuration, 23 | SerializerInterface $serializer, 24 | SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator 25 | ): TransportInterface; 26 | 27 | /** 28 | * @param array $options 29 | */ 30 | public function support(string $dsn, array $options = []): bool; 31 | } 32 | -------------------------------------------------------------------------------- /src/Middleware/SchedulerMiddlewareStackInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface SchedulerMiddlewareStackInterface extends MiddlewareStackInterface 15 | { 16 | /** 17 | * @throws Throwable {@see PreSchedulingMiddlewareInterface::preScheduling()} 18 | * @throws Throwable {@see AbstractMiddlewareStack::runMiddleware()} 19 | */ 20 | public function runPreSchedulingMiddleware(TaskInterface $task, SchedulerInterface $scheduler): void; 21 | 22 | /** 23 | * @throws Throwable {@see PostSchedulingMiddlewareInterface::postScheduling()} 24 | * @throws Throwable {@see AbstractMiddlewareStack::runMiddleware()} 25 | */ 26 | public function runPostSchedulingMiddleware(TaskInterface $task, SchedulerInterface $scheduler): void; 27 | } 28 | -------------------------------------------------------------------------------- /src/Test/Constraint/TaskScheduled.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class TaskScheduled extends Constraint 17 | { 18 | public function __construct(private int $expectedCount) 19 | { 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function toString(): string 26 | { 27 | return sprintf('%s %s been scheduled', $this->expectedCount, $this->expectedCount > 1 ? 'have' : 'has'); 28 | } 29 | 30 | /** 31 | * @param mixed|TaskEventList $other 32 | */ 33 | protected function matches($other): bool 34 | { 35 | return $this->expectedCount === (is_countable($other->getScheduledTaskEvents()) ? count($other->getScheduledTaskEvents()) : 0); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Transport/Configuration/CacheConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class CacheConfigurationFactory implements ConfigurationFactoryInterface 17 | { 18 | public function __construct(private CacheItemPoolInterface $pool) 19 | { 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function create(Dsn $dsn, SerializerInterface $serializer): CacheConfiguration 26 | { 27 | return new CacheConfiguration($this->pool); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function support(string $dsn): bool 34 | { 35 | return str_starts_with($dsn, 'configuration://cache'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Worker/WorkerRegistry.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class WorkerRegistry implements WorkerRegistryInterface 15 | { 16 | /** 17 | * @var WorkerInterface[] 18 | */ 19 | private array $workers; 20 | 21 | /** 22 | * @param WorkerInterface[] $workers 23 | */ 24 | public function __construct(iterable $workers) 25 | { 26 | $this->workers = is_array(value: $workers) ? $workers : iterator_to_array(iterator: $workers); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function getWorkers(): iterable 33 | { 34 | return $this->workers; 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function count(): int 41 | { 42 | return count(value: $this->workers); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Test/Constraint/TaskFailed.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class TaskFailed extends Constraint 18 | { 19 | public function __construct(private int $expectedCount) 20 | { 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function toString(): string 27 | { 28 | return sprintf('%s %s failed', $this->expectedCount, $this->expectedCount > 1 ? 'have' : 'has'); 29 | } 30 | 31 | /** 32 | * @param mixed|TaskEventList $other 33 | */ 34 | protected function matches($other): bool 35 | { 36 | return $this->expectedCount === (is_countable($other->getFailedTaskEvents()) ? count($other->getFailedTaskEvents()) : 0); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Test/Constraint/TaskUnscheduled.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class TaskUnscheduled extends Constraint 17 | { 18 | public function __construct(private int $expectedCount) 19 | { 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function toString(): string 26 | { 27 | return sprintf('%s %s been unscheduled', $this->expectedCount, $this->expectedCount > 1 ? 'have' : 'has'); 28 | } 29 | 30 | /** 31 | * @param mixed|TaskEventList $other 32 | */ 33 | protected function matches($other): bool 34 | { 35 | return $this->expectedCount === (is_countable($other->getUnscheduledTaskEvents()) ? count($other->getUnscheduledTaskEvents()) : 0); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/SchedulePolicy/BatchPolicy.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class BatchPolicy implements PolicyInterface 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function sort(TaskListInterface $tasks): TaskListInterface 19 | { 20 | $tasks->walk(func: static function (TaskInterface $task): void { 21 | $priority = $task->getPriority(); 22 | $task->setPriority(priority: --$priority); 23 | }); 24 | 25 | return $tasks->uasort(func: static fn (TaskInterface $task, TaskInterface $nextTask): int => $task->getPriority() <=> $nextTask->getPriority()); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function support(string $policy): bool 32 | { 33 | return 'batch' === $policy; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/SchedulePolicy/NicePolicy.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class NicePolicy implements PolicyInterface 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function sort(TaskListInterface $tasks): TaskListInterface 19 | { 20 | return $tasks->uasort(func: static function (TaskInterface $task, TaskInterface $nextTask): int { 21 | if ($task->getPriority() > 0) { 22 | return 1; 23 | } 24 | 25 | if ($nextTask->getPriority() > 0) { 26 | return 1; 27 | } 28 | 29 | return $task->getNice() <=> $nextTask->getNice(); 30 | }); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function support(string $policy): bool 37 | { 38 | return 'nice' === $policy; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Task/Output.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class Output implements Stringable 13 | { 14 | /** 15 | * @var string 16 | */ 17 | public const SUCCESS = 'success'; 18 | 19 | /** 20 | * @var string 21 | */ 22 | public const ERROR = 'error'; 23 | 24 | public function __construct( 25 | private TaskInterface $task, 26 | private ?string $output = 'undefined', 27 | private string $type = self::SUCCESS 28 | ) { 29 | } 30 | 31 | public function __toString(): string 32 | { 33 | return $this->output ?? ''; 34 | } 35 | 36 | public function getOutput(): ?string 37 | { 38 | return $this->output; 39 | } 40 | 41 | public function getTask(): TaskInterface 42 | { 43 | return $this->task; 44 | } 45 | 46 | public function getType(): string 47 | { 48 | return $this->type; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Task/Builder/ShellBuilder.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class ShellBuilder extends AbstractTaskBuilder implements BuilderInterface 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public function build(PropertyAccessorInterface $propertyAccessor, array $options = []): TaskInterface 20 | { 21 | return $this->handleTaskAttributes( 22 | new ShellTask($options['name'], $options['command'], $options['cwd'] ?? null, $options['environment_variables'] ?? [], $options['timeout'] ?? 60), 23 | $options, 24 | $propertyAccessor 25 | ); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function support(?string $type = null): bool 32 | { 33 | return 'shell' === $type; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Task/LazyTask.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class LazyTask extends AbstractTask implements LazyInterface 14 | { 15 | private bool $initialized = false; 16 | 17 | public function __construct(string $name, private Closure $sourceTaskClosure) 18 | { 19 | parent::__construct(name: sprintf('%s.lazy', $name)); 20 | } 21 | 22 | public function getTask(): TaskInterface 23 | { 24 | $this->initialize(); 25 | 26 | $task = $this->sourceTaskClosure; 27 | 28 | return $task(); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function isInitialized(): bool 35 | { 36 | return $this->initialized; 37 | } 38 | 39 | private function initialize(): void 40 | { 41 | if ($this->initialized) { 42 | return; 43 | } 44 | 45 | $this->initialized = true; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Transport/LongTailTransport.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class LongTailTransport extends AbstractCompoundTransport 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | protected function execute(Closure $func) 20 | { 21 | if (0 === $this->registry->count()) { 22 | throw new TransportException('No transport found'); 23 | } 24 | 25 | $this->registry->usort(static fn (TransportInterface $transport, TransportInterface $nextTransport): int => $transport->list()->count() <=> $nextTransport->list()->count()); 26 | 27 | $transport = $this->registry->reset(); 28 | 29 | try { 30 | return $func($transport); 31 | } catch (Throwable $throwable) { 32 | throw new TransportException('The transport failed to execute the requested action', 0, $throwable); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Task/Builder/HttpBuilder.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class HttpBuilder extends AbstractTaskBuilder implements BuilderInterface 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public function build(PropertyAccessorInterface $propertyAccessor, array $options = []): TaskInterface 20 | { 21 | $options['method'] ??= 'GET'; 22 | $options['client_options'] ??= []; 23 | 24 | return $this->handleTaskAttributes( 25 | new HttpTask($options['name'], $options['url'], $options['method'], $options['client_options']), 26 | $options, 27 | $propertyAccessor 28 | ); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function support(?string $type = null): bool 35 | { 36 | return 'http' === $type; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Event/TaskExecutingEvent.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class TaskExecutingEvent extends Event implements TaskEventInterface, WorkerEventInterface 16 | { 17 | public function __construct(private TaskInterface $task, private WorkerInterface $worker, private TaskListInterface $currentTasks) 18 | { 19 | } 20 | 21 | public function getTask(): TaskInterface 22 | { 23 | return $this->task; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getWorker(): WorkerInterface 30 | { 31 | return $this->worker; 32 | } 33 | 34 | /** 35 | * @return TaskListInterface 36 | */ 37 | public function getCurrentTasks(): TaskListInterface 38 | { 39 | return $this->currentTasks; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Task/Builder/CommandBuilder.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class CommandBuilder extends AbstractTaskBuilder implements BuilderInterface 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public function build(PropertyAccessorInterface $propertyAccessor, array $options = []): TaskInterface 20 | { 21 | $options['arguments'] ??= []; 22 | $options['options'] ??= []; 23 | 24 | return $this->handleTaskAttributes( 25 | new CommandTask($options['name'], $options['command'], $options['arguments'], $options['options']), 26 | $options, 27 | $propertyAccessor 28 | ); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function support(?string $type = null): bool 35 | { 36 | return 'command' === $type; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Bridge/Doctrine/Transport/Configuration/DoctrineConfiguration.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class DoctrineConfiguration extends AbstractExternalConfiguration implements SchemaAwareInterface 16 | { 17 | public function __construct( 18 | DbalConnection $connection, 19 | protected bool $autoSetup = false 20 | ) { 21 | parent::__construct(new Connection($connection, $autoSetup)); 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function configureSchema(Schema $schema, DbalConnection $dbalConnection): void 28 | { 29 | if (!$this->connection instanceof Connection) { 30 | return; 31 | } 32 | 33 | $this->connection->configureSchema($schema, $dbalConnection); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Transport/Configuration/LongTailConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class LongTailConfigurationFactory extends AbstractCompoundConfigurationFactory 14 | { 15 | /** 16 | * @param ConfigurationFactoryInterface[] $factories 17 | */ 18 | public function __construct(private iterable $factories) 19 | { 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function create(Dsn $dsn, SerializerInterface $serializer): LongTailConfiguration 26 | { 27 | return new LongTailConfiguration(new ConfigurationRegistry($this->handleCompoundConfiguration(' <> ', $dsn, $this->factories, $serializer))); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function support(string $dsn): bool 34 | { 35 | return str_starts_with($dsn, 'configuration://longtail') || str_starts_with($dsn, 'configuration://lt'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Expression/ExpressionBuilder.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ExpressionBuilder implements BuilderInterface 14 | { 15 | /** 16 | * @param iterable|ExpressionBuilderInterface[] $builders 17 | */ 18 | public function __construct(private iterable $builders) 19 | { 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function build(string $expression, ?string $timezone = null): Expression 26 | { 27 | if ([] === $this->builders) { 28 | throw new RuntimeException('No builder found'); 29 | } 30 | 31 | foreach ($this->builders as $builder) { 32 | if (!$builder->support($expression)) { 33 | continue; 34 | } 35 | 36 | return $builder->build($expression, $timezone ?? 'UTC'); 37 | } 38 | 39 | throw new InvalidArgumentException('The expression cannot be used'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Task/Builder/ProbeTaskBuilder.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class ProbeTaskBuilder extends AbstractTaskBuilder 15 | { 16 | /** 17 | * @param array $options 18 | */ 19 | public function build(PropertyAccessorInterface $propertyAccessor, array $options = []): TaskInterface 20 | { 21 | $options['errorOnFailedTasks'] ??= false; 22 | $options['delay'] ??= 0; 23 | 24 | return $this->handleTaskAttributes( 25 | new ProbeTask( 26 | $options['name'], 27 | $options['externalProbePath'], 28 | $options['errorOnFailedTasks'], 29 | $options['delay'] 30 | ), 31 | $options, 32 | $propertyAccessor 33 | ); 34 | } 35 | 36 | public function support(?string $type = null): bool 37 | { 38 | return 'probe' === $type; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Fiber/AbstractFiberHandler.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | abstract class AbstractFiberHandler 19 | { 20 | private LoggerInterface $logger; 21 | 22 | public function __construct(?LoggerInterface $logger = null) 23 | { 24 | $this->logger = $logger ?? new NullLogger(); 25 | } 26 | 27 | protected function handleOperationViaFiber(Closure $func): mixed 28 | { 29 | $fiber = new Fiber(callback: function (Closure $operation): void { 30 | $value = $operation(); 31 | 32 | Fiber::suspend(value: $value); 33 | }); 34 | 35 | try { 36 | $return = $fiber->start($func); 37 | } catch (Throwable $throwable) { 38 | $this->logger->critical(message: sprintf('An error occurred while performing the action: %s', $throwable->getMessage())); 39 | 40 | throw $throwable; 41 | } 42 | 43 | return $return; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Transport/Configuration/FailOverConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class FailOverConfigurationFactory extends AbstractCompoundConfigurationFactory 16 | { 17 | /** 18 | * @param ConfigurationFactoryInterface[] $factories 19 | */ 20 | public function __construct(private iterable $factories) 21 | { 22 | } 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function create(Dsn $dsn, SerializerInterface $serializer): FailOverConfiguration 28 | { 29 | return new FailOverConfiguration(new ConfigurationRegistry($this->handleCompoundConfiguration(' || ', $dsn, $this->factories, $serializer))); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function support(string $dsn): bool 36 | { 37 | return str_starts_with($dsn, 'configuration://fo') || str_starts_with($dsn, 'configuration://failover'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Transport/InMemoryTransportFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class InMemoryTransportFactory implements TransportFactoryInterface 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public function createTransport( 20 | Dsn $dsn, 21 | array $options, 22 | ConfigurationInterface $configuration, 23 | SerializerInterface $serializer, 24 | SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator 25 | ): InMemoryTransport { 26 | $configuration->set('execution_mode', $dsn->getHost()); 27 | 28 | return new InMemoryTransport($configuration, $schedulePolicyOrchestrator); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function support(string $dsn, array $options = []): bool 35 | { 36 | return str_starts_with($dsn, 'memory://'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Pool/Configuration/SchedulerConfiguration.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class SchedulerConfiguration 17 | { 18 | private TaskListInterface $dueTasks; 19 | 20 | public function __construct( 21 | private DateTimeZone $timezone, 22 | private DateTimeImmutable $synchronizedDate, 23 | TaskInterface ...$dueTasks 24 | ) { 25 | $this->dueTasks = new TaskList(tasks: $dueTasks); 26 | } 27 | 28 | public function getTimezone(): DateTimeZone 29 | { 30 | return $this->timezone; 31 | } 32 | 33 | public function getSynchronizedDate(): DateTimeImmutable 34 | { 35 | return $this->synchronizedDate; 36 | } 37 | 38 | /** 39 | * @return TaskListInterface 40 | */ 41 | public function getDueTasks(): TaskListInterface 42 | { 43 | return $this->dueTasks; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Test/Constraint/Probe/ProbeState.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class ProbeState extends Constraint 17 | { 18 | /** 19 | * @param array $expectedState 20 | */ 21 | public function __construct(private array $expectedState) 22 | { 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function toString(): string 29 | { 30 | return sprintf('match current probe state: %s', json_encode($this->expectedState, JSON_THROW_ON_ERROR)); 31 | } 32 | 33 | /** 34 | * @param mixed|ProbeInterface $other 35 | */ 36 | protected function matches($other): bool 37 | { 38 | return $this->expectedState === [ 39 | 'executedTasks' => $other->getExecutedTasks(), 40 | 'failedTasks' => $other->getFailedTasks(), 41 | 'scheduledTasks' => $other->getScheduledTasks(), 42 | ]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Transport/Configuration/LongTailConfiguration.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class LongTailConfiguration extends AbstractCompoundConfiguration 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | protected function execute(Closure $func) 20 | { 21 | if (0 === $this->configurationRegistry->count()) { 22 | throw new ConfigurationException('No configuration found'); 23 | } 24 | 25 | $this->configurationRegistry->usort(static fn (ConfigurationInterface $configuration, ConfigurationInterface $nextConfiguration): int => $configuration->count() <=> $nextConfiguration->count()); 26 | 27 | $configuration = $this->configurationRegistry->reset(); 28 | 29 | try { 30 | return $func($configuration); 31 | } catch (Throwable $throwable) { 32 | throw new ConfigurationException('The configuration failed to execute the requested action', 0, $throwable); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Task/TaskBuilder.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class TaskBuilder implements TaskBuilderInterface 17 | { 18 | /** 19 | * @param iterable|BuilderInterface[] $builders 20 | */ 21 | public function __construct( 22 | private iterable $builders, 23 | private PropertyAccessorInterface $propertyAccessor 24 | ) { 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function create(array $options = []): TaskInterface 31 | { 32 | foreach ($this->builders as $builder) { 33 | if (!$builder->support(type: $options['type'])) { 34 | continue; 35 | } 36 | 37 | return $builder->build(propertyAccessor: $this->propertyAccessor, options: $options); 38 | } 39 | 40 | throw new InvalidArgumentException(message: sprintf('The task cannot be created as no builder has been defined for "%s"', $options['type'])); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Runner/RunnerInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface RunnerInterface 16 | { 17 | /** 18 | * Execute the @param TaskInterface $task and define an {@see Output} regarding the execution process. 19 | * 20 | * If required, the @param WorkerInterface $worker can be used to execute the task according to the needs. 21 | * 22 | * The {@see TaskInterface::setExecutionState()} method SHOULD NOT be called during the execution process as 23 | * the {@see Worker::defineTaskExecutionState()} does the call, any call of this method BEFORE the worker will be 24 | * ignored and the execution state overridden by the worker. 25 | */ 26 | public function run(TaskInterface $task, WorkerInterface $worker): Output; 27 | 28 | /** 29 | * Determine if a @param TaskInterface $task is supported by the runner. 30 | * 31 | * The determination process is totally up to the runner. 32 | */ 33 | public function support(TaskInterface $task): bool; 34 | } 35 | -------------------------------------------------------------------------------- /src/Worker/ExecutionPolicy/FiberPolicy.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class FiberPolicy extends AbstractFiberHandler implements ExecutionPolicyInterface 17 | { 18 | /** 19 | * {@inheritdoc} 20 | * 21 | * @throws Throwable {@see AbstractFiberHandler::handleOperationViaFiber()} 22 | */ 23 | public function execute( 24 | TaskListInterface $toExecuteTasks, 25 | Closure $handleTaskFunc 26 | ): void { 27 | $toExecuteTasks->walk(func: function (TaskInterface $toExecuteTask) use ($toExecuteTasks, $handleTaskFunc): void { 28 | $this->handleOperationViaFiber(func: static function () use ($toExecuteTask, $toExecuteTasks, $handleTaskFunc): void { 29 | $handleTaskFunc($toExecuteTask, $toExecuteTasks); 30 | }); 31 | }); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function support(string $policy): bool 38 | { 39 | return 'fiber' === $policy; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Transport/Configuration/ConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class ConfigurationFactory 18 | { 19 | /** 20 | * @param ConfigurationFactoryInterface[] $factories 21 | */ 22 | public function __construct(private iterable $factories) 23 | { 24 | } 25 | 26 | public function build(string $dsn, SerializerInterface $serializer): ConfigurationInterface 27 | { 28 | if ([] === $this->factories) { 29 | throw new RuntimeException('No factory found for the desired configuration'); 30 | } 31 | 32 | foreach ($this->factories as $factory) { 33 | if (!$factory->support($dsn)) { 34 | continue; 35 | } 36 | 37 | return $factory->create(Dsn::fromString($dsn), $serializer); 38 | } 39 | 40 | throw new InvalidArgumentException(sprintf('The DSN "%s" cannot be used to create a configuration', $dsn)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Task/ChainedTask.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class ChainedTask extends AbstractTask 11 | { 12 | /** 13 | * @var TaskListInterface 14 | */ 15 | private TaskListInterface $tasks; 16 | 17 | public function __construct(string $name, TaskInterface ...$tasks) 18 | { 19 | $this->tasks = new TaskList(tasks: $tasks); 20 | 21 | $this->defineOptions(); 22 | 23 | parent::__construct(name: $name); 24 | } 25 | 26 | public function addTask(TaskInterface $task): self 27 | { 28 | $this->getTasks()->add(task: $task); 29 | 30 | return $this; 31 | } 32 | 33 | /** 34 | * @param TaskListInterface $list 35 | */ 36 | public function setTasks(TaskListInterface $list): self 37 | { 38 | $this->tasks = $list; 39 | 40 | return $this; 41 | } 42 | 43 | public function getTask(string $name): ?TaskInterface 44 | { 45 | return $this->getTasks()->get(taskName: $name); 46 | } 47 | 48 | /** 49 | * @return TaskListInterface 50 | */ 51 | public function getTasks(): TaskListInterface 52 | { 53 | return $this->tasks; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Transport/Configuration/FiberConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class FiberConfigurationFactory implements ConfigurationFactoryInterface 18 | { 19 | /** 20 | * @param ConfigurationFactoryInterface[] $factories 21 | */ 22 | public function __construct(private iterable $factories) 23 | { 24 | } 25 | 26 | public function create(Dsn $dsn, SerializerInterface $serializer): FiberConfiguration 27 | { 28 | foreach ($this->factories as $factory) { 29 | if (!$factory->support($dsn->getOptions()[0])) { 30 | continue; 31 | } 32 | 33 | $dsn = Dsn::fromString($dsn->getOptions()[0]); 34 | 35 | return new FiberConfiguration($factory->create($dsn, $serializer)); 36 | } 37 | 38 | throw new RuntimeException(sprintf('No factory found for the DSN "%s"', $dsn->getRoot())); 39 | } 40 | 41 | public function support(string $dsn): bool 42 | { 43 | return str_starts_with($dsn, 'configuration://fiber'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Task/TaskExecutionTracker.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class TaskExecutionTracker implements TaskExecutionTrackerInterface 16 | { 17 | public function __construct(private Stopwatch $watch) 18 | { 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function startTracking(TaskInterface $task): void 25 | { 26 | if (!$task->isTracked()) { 27 | return; 28 | } 29 | 30 | $this->watch->start(name: sprintf('task_execution.%s', $task->getName())); 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | public function endTracking(TaskInterface $task): void 37 | { 38 | if (!$task->isTracked()) { 39 | return; 40 | } 41 | 42 | $task->setExecutionMemoryUsage(memory_get_usage()); 43 | 44 | if (!$this->watch->isStarted(name: sprintf('task_execution.%s', $task->getName()))) { 45 | return; 46 | } 47 | 48 | $stopwatchEvent = $this->watch->stop(name: sprintf('task_execution.%s', $task->getName())); 49 | $task->setExecutionComputationTime(executionComputationTime: $stopwatchEvent->getDuration()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Task/ProbeTask.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class ProbeTask extends AbstractTask 11 | { 12 | public function __construct( 13 | string $name, 14 | private string $externalProbePath, 15 | private bool $errorOnFailedTasks = false, 16 | private int $delay = 0 17 | ) { 18 | $this->defineOptions(); 19 | 20 | parent::__construct(name: $name); 21 | } 22 | 23 | public function setExternalProbePath(string $externalProbePath): self 24 | { 25 | $this->externalProbePath = $externalProbePath; 26 | 27 | return $this; 28 | } 29 | 30 | public function getExternalProbePath(): string 31 | { 32 | return $this->externalProbePath; 33 | } 34 | 35 | public function setErrorOnFailedTasks(bool $errorOnFailedTasks): self 36 | { 37 | $this->errorOnFailedTasks = $errorOnFailedTasks; 38 | 39 | return $this; 40 | } 41 | 42 | public function getErrorOnFailedTasks(): bool 43 | { 44 | return $this->errorOnFailedTasks; 45 | } 46 | 47 | public function setDelay(int $delay): self 48 | { 49 | $this->delay = $delay; 50 | 51 | return $this; 52 | } 53 | 54 | public function getDelay(): int 55 | { 56 | return $this->delay; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Transport/CacheTransportFactory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class CacheTransportFactory implements TransportFactoryInterface 16 | { 17 | public function __construct(private CacheItemPoolInterface $pool) 18 | { 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function createTransport( 25 | Dsn $dsn, 26 | array $options, 27 | ConfigurationInterface $configuration, 28 | SerializerInterface $serializer, 29 | SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator 30 | ): CacheTransport { 31 | $configuration->init([ 32 | 'execution_mode' => $dsn->getOption('execution_mode', 'first_in_first_out'), 33 | ]); 34 | 35 | return new CacheTransport($configuration, $this->pool, $serializer, $schedulePolicyOrchestrator); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function support(string $dsn, array $options = []): bool 42 | { 43 | return str_starts_with($dsn, 'cache://'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Expression/ExactExpressionBuilder.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class ExactExpressionBuilder implements ExpressionBuilderInterface 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function build(string $expression, string $timezone = 'UTC'): Expression 23 | { 24 | $date = DateTimeImmutable::createFromFormat('U', (string) strtotime($expression)); 25 | if (false === $date) { 26 | throw new InvalidArgumentException(sprintf('The "%s" expression cannot be used to create a date', $expression)); 27 | } 28 | 29 | $date = $date->setTimezone(new DateTimeZone($timezone)); 30 | 31 | $expression = new Expression(); 32 | $expression->setExpression(sprintf( 33 | '%d %s %s %s *', 34 | $date->format('i'), 35 | $date->format('G'), 36 | $date->format('j'), 37 | $date->format('n') 38 | )); 39 | 40 | return $expression; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function support(string $expression): bool 47 | { 48 | return false !== strtotime($expression); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Runner/CallbackTaskRunner.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class CallbackTaskRunner implements RunnerInterface 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function run(TaskInterface $task, WorkerInterface $worker): Output 25 | { 26 | if (!$task instanceof CallbackTask) { 27 | return new Output(task: $task, output: null, type: Output::ERROR); 28 | } 29 | 30 | try { 31 | $output = call_user_func_array($task->getCallback(), $task->getArguments()); 32 | 33 | if (false === $output) { 34 | return new Output(task: $task, output: null, type: Output::ERROR); 35 | } 36 | 37 | return new Output(task: $task, output: trim((string) $output)); 38 | } catch (Throwable) { 39 | return new Output(task: $task, output: null, type: Output::ERROR); 40 | } 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function support(TaskInterface $task): bool 47 | { 48 | return $task instanceof CallbackTask; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Transport/Configuration/LazyConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class LazyConfigurationFactory implements ConfigurationFactoryInterface 18 | { 19 | /** 20 | * @param ConfigurationFactoryInterface[] $factories 21 | */ 22 | public function __construct(private iterable $factories) 23 | { 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function create(Dsn $dsn, SerializerInterface $serializer): LazyConfiguration 30 | { 31 | foreach ($this->factories as $factory) { 32 | if (!$factory->support($dsn->getOptions()[0])) { 33 | continue; 34 | } 35 | 36 | $dsn = Dsn::fromString($dsn->getOptions()[0]); 37 | 38 | return new LazyConfiguration($factory->create($dsn, $serializer)); 39 | } 40 | 41 | throw new RuntimeException(sprintf('No factory found for the DSN "%s"', $dsn->getRoot())); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function support(string $dsn): bool 48 | { 49 | return str_starts_with($dsn, 'configuration://lazy'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Expression/FluentExpressionBuilder.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class FluentExpressionBuilder implements ExpressionBuilderInterface 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function build(string $expression, string $timezone = 'UTC'): Expression 23 | { 24 | $date = DateTimeImmutable::createFromFormat('U', (string) strtotime($expression)); 25 | if (false === $date) { 26 | throw new InvalidArgumentException(sprintf('The "%s" expression cannot be used to create a date', $expression)); 27 | } 28 | 29 | $date = $date->setTimezone(new DateTimeZone($timezone)); 30 | 31 | $expression = new Expression(); 32 | $expression->setExpression(sprintf( 33 | '%d %s %s %s %s', 34 | $date->format('i'), 35 | $date->format('G'), 36 | $date->format('j'), 37 | $date->format('n'), 38 | $date->format('w') 39 | )); 40 | 41 | return $expression; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function support(string $expression): bool 48 | { 49 | return false !== strtotime($expression); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Runner/ChainedTaskRunner.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class ChainedTaskRunner implements RunnerInterface 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function run(TaskInterface $task, WorkerInterface $worker): Output 23 | { 24 | if (!$task instanceof ChainedTask) { 25 | return new Output($task, null, Output::ERROR); 26 | } 27 | 28 | $forkedWorker = $worker->fork(); 29 | 30 | try { 31 | $task->getTasks()->walk(static function (TaskInterface $task) use ($forkedWorker): void { 32 | $forkedWorker->execute(WorkerConfiguration::create(), $task); 33 | }); 34 | } catch (Throwable $throwable) { 35 | return new Output($task, $throwable->getMessage(), Output::ERROR); 36 | } finally { 37 | $forkedWorker->stop(); 38 | } 39 | 40 | return new Output($task, null, Output::SUCCESS); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function support(TaskInterface $task): bool 47 | { 48 | return $task instanceof ChainedTask; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Transport/TransportFactory.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class TransportFactory 18 | { 19 | /** 20 | * @param TransportFactoryInterface[] $factories 21 | */ 22 | public function __construct(private iterable $factories) 23 | { 24 | } 25 | 26 | /** 27 | * @param array $options 28 | * 29 | */ 30 | public function createTransport( 31 | string $dsn, 32 | array $options, 33 | ConfigurationInterface $configuration, 34 | SerializerInterface $serializer, 35 | SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator 36 | ): TransportInterface { 37 | foreach ($this->factories as $factory) { 38 | if ($factory->support($dsn, $options)) { 39 | return $factory->createTransport(Dsn::fromString($dsn), $options, $configuration, $serializer, $schedulePolicyOrchestrator); 40 | } 41 | } 42 | 43 | throw new InvalidArgumentException(sprintf('No transport supports the given Scheduler DSN "%s".', $dsn)); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/EventListener/StopWorkerOnTaskLimitSubscriber.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class StopWorkerOnTaskLimitSubscriber implements EventSubscriberInterface 16 | { 17 | private int $consumedTasks = 0; 18 | private LoggerInterface $logger; 19 | 20 | public function __construct( 21 | private int $maximumTasks, 22 | ?LoggerInterface $logger = null 23 | ) { 24 | $this->logger = $logger ?? new NullLogger(); 25 | } 26 | 27 | public function onWorkerRunning(WorkerRunningEvent $workerRunningEvent): void 28 | { 29 | if (!$workerRunningEvent->isIdle() && ++$this->consumedTasks >= $this->maximumTasks) { 30 | $worker = $workerRunningEvent->getWorker(); 31 | 32 | $worker->stop(); 33 | 34 | $this->logger->info(message: 'The worker has been stopped due to maximum tasks executed', context: [ 35 | 'count' => $this->consumedTasks, 36 | ]); 37 | } 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public static function getSubscribedEvents(): array 44 | { 45 | return [ 46 | WorkerRunningEvent::class => 'onWorkerRunning', 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Test/Constraint/TaskQueued.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class TaskQueued extends Constraint 17 | { 18 | public function __construct(private int $expectedCount) 19 | { 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function toString(): string 26 | { 27 | return sprintf('contains %s task%s that %s been queued', $this->expectedCount, $this->expectedCount > 1 ? 's' : '', $this->expectedCount > 1 ? 'have' : 'has'); 28 | } 29 | 30 | /** 31 | * @param mixed|TaskEventList $other 32 | */ 33 | protected function matches($other): bool 34 | { 35 | return $this->expectedCount === $this->countQueuedTasks($other); 36 | } 37 | 38 | private function countQueuedTasks(TaskEventList $taskEventList): int 39 | { 40 | $count = 0; 41 | foreach ($taskEventList->getEvents() as $taskEvent) { 42 | if (!$taskEvent instanceof TaskScheduledEvent) { 43 | continue; 44 | } 45 | 46 | if (!$taskEvent->getTask()->isQueued()) { 47 | continue; 48 | } 49 | 50 | ++$count; 51 | } 52 | 53 | return $count; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Test/Constraint/TaskExecuted.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class TaskExecuted extends Constraint 18 | { 19 | public function __construct(private int $expectedCount) 20 | { 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function toString(): string 27 | { 28 | return sprintf('%s %s been executed', $this->expectedCount, $this->expectedCount > 1 ? 'have' : 'has'); 29 | } 30 | 31 | /** 32 | * @param mixed|TaskEventList $other 33 | */ 34 | protected function matches($other): bool 35 | { 36 | return $this->expectedCount === $this->countExecutedTasks($other); 37 | } 38 | 39 | private function countExecutedTasks(TaskEventList $taskEventList): int 40 | { 41 | $count = 0; 42 | foreach ($taskEventList->getEvents() as $taskEvent) { 43 | if (!$taskEvent instanceof TaskExecutedEvent) { 44 | continue; 45 | } 46 | 47 | if (TaskInterface::SUCCEED !== $taskEvent->getTask()->getExecutionState()) { 48 | continue; 49 | } 50 | 51 | ++$count; 52 | } 53 | 54 | return $count; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Runner/MessengerTaskRunner.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class MessengerTaskRunner implements RunnerInterface 18 | { 19 | public function __construct(private ?MessageBusInterface $bus = null) 20 | { 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function run(TaskInterface $task, WorkerInterface $worker): Output 27 | { 28 | if (!$task instanceof MessengerTask) { 29 | return new Output($task, null, Output::ERROR); 30 | } 31 | 32 | try { 33 | if (!$this->bus instanceof MessageBusInterface) { 34 | return new Output($task, 'The task cannot be handled as the bus is not defined', Output::ERROR); 35 | } 36 | 37 | $this->bus->dispatch($task->getMessage()); 38 | 39 | return new Output($task, null); 40 | } catch (Throwable $throwable) { 41 | return new Output($task, $throwable->getMessage(), Output::ERROR); 42 | } 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function support(TaskInterface $task): bool 49 | { 50 | return $task instanceof MessengerTask; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Task/Builder/AbstractTaskBuilder.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | abstract class AbstractTaskBuilder 16 | { 17 | public function __construct(private ExpressionBuilderInterface $expressionBuilder) 18 | { 19 | } 20 | 21 | /** 22 | * @param array $options 23 | */ 24 | protected function handleTaskAttributes(TaskInterface $task, array $options, PropertyAccessorInterface $propertyAccessor): TaskInterface 25 | { 26 | foreach ($options as $option => $value) { 27 | if (!$propertyAccessor->isWritable($task, $option)) { 28 | continue; 29 | } 30 | 31 | if ('timezone' === $option) { 32 | $propertyAccessor->setValue($task, $option, new DateTimeZone($value)); 33 | 34 | continue; 35 | } 36 | 37 | if ('expression' === $option) { 38 | $propertyAccessor->setValue($task, $option, $this->expressionBuilder->build($value, $options['timezone'] ?? null)->getExpression()); 39 | 40 | continue; 41 | } 42 | 43 | $propertyAccessor->setValue($task, $option, $value); 44 | } 45 | 46 | return $task; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/EventListener/TaskLoggerSubscriber.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class TaskLoggerSubscriber implements EventSubscriberInterface 20 | { 21 | private TaskEventList $events; 22 | 23 | public function __construct() 24 | { 25 | $this->events = new TaskEventList(); 26 | } 27 | 28 | public function onTask(TaskEventInterface $taskEvent): void 29 | { 30 | $this->events->addEvent($taskEvent); 31 | } 32 | 33 | public function getEvents(): TaskEventList 34 | { 35 | return $this->events; 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public static function getSubscribedEvents(): array 42 | { 43 | return [ 44 | TaskExecutedEvent::class => ['onTask', -255], 45 | TaskFailedEvent::class => ['onTask', -255], 46 | TaskQueued::class => ['onTask', -255], 47 | TaskScheduledEvent::class => ['onTask', -255], 48 | TaskUnscheduledEvent::class => ['onTask', -255], 49 | ]; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Runner/HttpTaskRunner.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class HttpTaskRunner implements RunnerInterface 19 | { 20 | private HttpClientInterface $httpClient; 21 | 22 | public function __construct(?HttpClientInterface $httpClient = null) 23 | { 24 | $this->httpClient = $httpClient ?? HttpClient::create(); 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function run(TaskInterface $task, WorkerInterface $worker): Output 31 | { 32 | if (!$task instanceof HttpTask) { 33 | return new Output($task, null, Output::ERROR); 34 | } 35 | 36 | try { 37 | $response = $this->httpClient->request($task->getMethod(), $task->getUrl(), $task->getClientOptions()); 38 | return new Output($task, $response->getContent()); 39 | } catch (Throwable $throwable) { 40 | return new Output($task, $throwable->getMessage(), Output::ERROR); 41 | } 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function support(TaskInterface $task): bool 48 | { 49 | return $task instanceof HttpTask; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Task/NotificationTask.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class NotificationTask extends AbstractTask 14 | { 15 | /** 16 | * @var Recipient[] 17 | */ 18 | private array $recipients; 19 | 20 | public function __construct( 21 | string $name, 22 | private Notification $notification, 23 | Recipient ...$recipients 24 | ) { 25 | $this->recipients = $recipients; 26 | 27 | $this->defineOptions(); 28 | 29 | parent::__construct(name: $name); 30 | } 31 | 32 | public function getNotification(): Notification 33 | { 34 | return $this->notification; 35 | } 36 | 37 | public function setNotification(Notification $notification): self 38 | { 39 | $this->notification = $notification; 40 | 41 | return $this; 42 | } 43 | 44 | /** 45 | * @return Recipient[] 46 | */ 47 | public function getRecipients(): array 48 | { 49 | return $this->recipients; 50 | } 51 | 52 | public function addRecipient(Recipient $recipient): self 53 | { 54 | $this->recipients[] = $recipient; 55 | 56 | return $this; 57 | } 58 | 59 | public function setRecipients(Recipient ...$recipients): self 60 | { 61 | $this->recipients = $recipients; 62 | 63 | return $this; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Transport/LongTailTransportFactory.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class LongTailTransportFactory extends AbstractCompoundTransportFactory 17 | { 18 | /** 19 | * @param iterable|TransportFactoryInterface[] $transportFactories 20 | */ 21 | public function __construct(private iterable $transportFactories) 22 | { 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function createTransport( 29 | Dsn $dsn, 30 | array $options, 31 | ConfigurationInterface $configuration, 32 | SerializerInterface $serializer, 33 | SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator 34 | ): LongTailTransport { 35 | return new LongTailTransport( 36 | new TransportRegistry($this->handleTransportDsn(' <> ', $dsn, $this->transportFactories, $options, $configuration, $serializer, $schedulePolicyOrchestrator)), 37 | $configuration 38 | ); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function support(string $dsn, array $options = []): bool 45 | { 46 | return str_starts_with($dsn, 'longtail://') || str_starts_with($dsn, 'lt://'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Runner/NotificationTaskRunner.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class NotificationTaskRunner implements RunnerInterface 18 | { 19 | public function __construct(private ?NotifierInterface $notifier = null) 20 | { 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function run(TaskInterface $task, WorkerInterface $worker): Output 27 | { 28 | if (!$task instanceof NotificationTask) { 29 | return new Output($task, null, Output::ERROR); 30 | } 31 | 32 | try { 33 | if (!$this->notifier instanceof NotifierInterface) { 34 | return new Output($task, 'The task cannot be handled as the notifier is not defined', Output::ERROR); 35 | } 36 | 37 | $this->notifier->send($task->getNotification(), ...$task->getRecipients()); 38 | 39 | return new Output($task, null); 40 | } catch (Throwable $throwable) { 41 | return new Output($task, $throwable->getMessage(), Output::ERROR); 42 | } 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function support(TaskInterface $task): bool 49 | { 50 | return $task instanceof NotificationTask; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Messenger/TaskToExecuteMessageHandler.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | #[AsMessageHandler] 23 | final class TaskToExecuteMessageHandler 24 | { 25 | private LoggerInterface $logger; 26 | 27 | public function __construct( 28 | private WorkerInterface $worker, 29 | ?LoggerInterface $logger = null 30 | ) { 31 | $this->logger = $logger ?? new NullLogger(); 32 | } 33 | 34 | /** 35 | * @throws Exception 36 | */ 37 | public function __invoke(TaskToExecuteMessage $taskMessage): void 38 | { 39 | $task = $taskMessage->getTask(); 40 | $timezone = $task->getTimezone() ?? new DateTimeZone('UTC'); 41 | 42 | if (!(new CronExpression($task->getExpression()))->isDue(new DateTimeImmutable('now', $timezone), $timezone->getName())) { 43 | return; 44 | } 45 | 46 | while ($this->worker->isRunning()) { 47 | $this->logger->info(sprintf('The task "%s" cannot be executed for now as the worker is currently running', $task->getName())); 48 | } 49 | 50 | $this->worker->execute(WorkerConfiguration::create(), $task); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Transport/Configuration/FailOverConfiguration.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class FailOverConfiguration extends AbstractCompoundConfiguration 16 | { 17 | /** 18 | * @var SplObjectStorage 19 | */ 20 | private SplObjectStorage $failedConfigurations; 21 | 22 | public function __construct(ConfigurationRegistryInterface $configurationRegistry) 23 | { 24 | $this->failedConfigurations = new SplObjectStorage(); 25 | 26 | parent::__construct($configurationRegistry); 27 | } 28 | 29 | protected function execute(Closure $func) 30 | { 31 | if (0 === $this->configurationRegistry->count()) { 32 | throw new ConfigurationException('No configuration found'); 33 | } 34 | 35 | foreach ($this->configurationRegistry as $configurationStorage) { 36 | if ($this->failedConfigurations->contains($configurationStorage)) { 37 | continue; 38 | } 39 | 40 | try { 41 | return $func($configurationStorage); 42 | } catch (Throwable) { 43 | $this->failedConfigurations->attach($configurationStorage); 44 | 45 | continue; 46 | } 47 | } 48 | 49 | throw new ConfigurationException('All the configurationStorages failed to execute the requested action'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Probe/Probe.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class Probe implements ProbeInterface 17 | { 18 | public function __construct( 19 | private SchedulerInterface $scheduler, 20 | private WorkerInterface $worker 21 | ) { 22 | } 23 | 24 | /** 25 | * @throws Throwable {@see SchedulerInterface::getTasks()} 26 | */ 27 | public function getExecutedTasks(): int 28 | { 29 | return $this->scheduler->getTasks()->filter(filter: static function (TaskInterface $task): bool { 30 | $lastExecutionDate = $task->getLastExecution(); 31 | if (!$lastExecutionDate instanceof DateTimeImmutable) { 32 | return false; 33 | } 34 | 35 | return $lastExecutionDate->format(format: 'Y-m-d h:i') === (new DateTimeImmutable())->format(format: 'Y-m-d h:i'); 36 | })->count(); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function getFailedTasks(): int 43 | { 44 | return $this->worker->getFailedTasks()->count(); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function getScheduledTasks(): int 51 | { 52 | return $this->scheduler->getTasks()->filter(filter: static fn (TaskInterface $task): bool => null !== $task->getScheduledAt())->count(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Middleware/WorkerMiddlewareStack.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class WorkerMiddlewareStack extends AbstractMiddlewareStack implements WorkerMiddlewareStackInterface 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function runPreExecutionMiddleware(TaskInterface $task): void 23 | { 24 | $this->runMiddleware(middlewareList: $this->getPreExecutionMiddleware(), func: static function (PreExecutionMiddlewareInterface $middleware) use ($task): void { 25 | $middleware->preExecute(task: $task); 26 | }); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function runPostExecutionMiddleware(TaskInterface $task, WorkerInterface $worker): void 33 | { 34 | $this->runMiddleware(middlewareList: $this->getPostExecutionMiddleware(), func: static function (PostExecutionMiddlewareInterface $middleware) use ($task, $worker): void { 35 | $middleware->postExecute(task: $task, worker: $worker); 36 | }); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function getMiddlewareList(): array 43 | { 44 | return array_unique(array: [ 45 | ...$this->getPreExecutionMiddleware()->toArray(), 46 | ...$this->getPostExecutionMiddleware()->toArray(), 47 | ], flags: SORT_REGULAR); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Transport/FailOverTransport.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class FailOverTransport extends AbstractCompoundTransport 17 | { 18 | /** 19 | * @var SplObjectStorage 20 | */ 21 | private SplObjectStorage $failedTransports; 22 | 23 | public function __construct( 24 | TransportRegistryInterface $registry, 25 | ConfigurationInterface $configuration 26 | ) { 27 | $this->failedTransports = new SplObjectStorage(); 28 | 29 | parent::__construct($registry, $configuration); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function execute(Closure $func) 36 | { 37 | if (0 === $this->registry->count()) { 38 | throw new TransportException('No transport found'); 39 | } 40 | 41 | foreach ($this->registry as $transport) { 42 | if ($this->failedTransports->contains($transport)) { 43 | continue; 44 | } 45 | 46 | try { 47 | return $func($transport); 48 | } catch (Throwable) { 49 | $this->failedTransports->attach($transport); 50 | 51 | continue; 52 | } 53 | } 54 | 55 | throw new TransportException('All the transports failed to execute the requested action'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Transport/FilesystemTransportFactory.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class FilesystemTransportFactory implements TransportFactoryInterface 17 | { 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function createTransport( 22 | Dsn $dsn, 23 | array $options, 24 | ConfigurationInterface $configuration, 25 | SerializerInterface $serializer, 26 | SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator 27 | ): FilesystemTransport { 28 | $configuration->init([ 29 | 'execution_mode' => $dsn->getHost(), 30 | 'filename_mask' => $dsn->getOption('filename_mask', '%s/_symfony_scheduler_/%s.json'), 31 | 'path' => $dsn->getOption('path', $options['path'] ?? sys_get_temp_dir()), 32 | ], [ 33 | 'filename_mask' => 'string', 34 | 'path' => 'string', 35 | ]); 36 | 37 | return new FilesystemTransport($configuration, $serializer, $schedulePolicyOrchestrator); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function support(string $dsn, array $options = []): bool 44 | { 45 | return str_starts_with($dsn, 'fs://') || str_starts_with($dsn, 'filesystem://') || str_starts_with($dsn, 'file://'); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/SchedulePolicy/SchedulePolicyOrchestrator.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class SchedulePolicyOrchestrator implements SchedulePolicyOrchestratorInterface 19 | { 20 | /** 21 | * @param PolicyInterface[] $policies 22 | */ 23 | public function __construct(private iterable $policies) 24 | { 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function sort(string $policy, TaskListInterface $tasks): TaskListInterface 31 | { 32 | if ([] === $this->policies) { 33 | throw new RuntimeException(message: 'The tasks cannot be sorted as no policies have been defined'); 34 | } 35 | 36 | $tasks->walk(func: function (TaskInterface $task) use ($policy): void { 37 | if ($task instanceof ChainedTask) { 38 | $task->setTasks(list: $this->sort(policy: $policy, tasks: $task->getTasks())); 39 | } 40 | }); 41 | 42 | foreach ($this->policies as $schedulePolicy) { 43 | if (!$schedulePolicy->support(policy: $policy)) { 44 | continue; 45 | } 46 | 47 | return $schedulePolicy->sort(tasks: $tasks); 48 | } 49 | 50 | throw new InvalidArgumentException(message: sprintf('The policy "%s" cannot be used', $policy)); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Transport/RoundRobinTransportFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class RoundRobinTransportFactory extends AbstractCompoundTransportFactory 15 | { 16 | /** 17 | * @param TransportFactoryInterface[] $transportFactories 18 | */ 19 | public function __construct(private iterable $transportFactories) 20 | { 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function createTransport( 27 | Dsn $dsn, 28 | array $options, 29 | ConfigurationInterface $configuration, 30 | SerializerInterface $serializer, 31 | SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator 32 | ): RoundRobinTransport { 33 | $configuration->init([ 34 | 'quantum' => $dsn->getOptionAsInt('quantum', 2), 35 | ], [ 36 | 'quantum' => 'int', 37 | ]); 38 | 39 | return new RoundRobinTransport( 40 | new TransportRegistry($this->handleTransportDsn(' && ', $dsn, $this->transportFactories, $options, $configuration, $serializer, $schedulePolicyOrchestrator)), 41 | $configuration 42 | ); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function support(string $dsn, array $options = []): bool 49 | { 50 | return str_starts_with($dsn, 'roundrobin://') || str_starts_with($dsn, 'rr://'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Expression/ComputedExpressionBuilder.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class ComputedExpressionBuilder implements ExpressionBuilderInterface 16 | { 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | public function build(string $expression, string $timezone = 'UTC'): Expression 21 | { 22 | $parts = explode(' ', $expression); 23 | 24 | foreach ($parts as $position => $part) { 25 | if (0 === $position && $part === '#') { 26 | $parts[$position] = random_int(0, 59); 27 | } 28 | 29 | if (1 === $position && $part === '#') { 30 | $parts[$position] = random_int(0, 23); 31 | } 32 | 33 | if (2 === $position && $part === '#') { 34 | $parts[$position] = random_int(1, 31); 35 | } 36 | 37 | if (3 === $position && $part === '#') { 38 | $parts[$position] = random_int(1, 12); 39 | } 40 | 41 | if (4 !== $position) { 42 | continue; 43 | } 44 | 45 | if ($part !== '#') { 46 | continue; 47 | } 48 | 49 | $parts[$position] = random_int(0, 6); 50 | } 51 | 52 | return Expression::createFromString(implode(' ', $parts)); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function support(string $expression): bool 59 | { 60 | return in_array('#', explode(' ', $expression), true); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Middleware/SchedulerMiddlewareStack.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class SchedulerMiddlewareStack extends AbstractMiddlewareStack implements SchedulerMiddlewareStackInterface 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function runPreSchedulingMiddleware(TaskInterface $task, SchedulerInterface $scheduler): void 23 | { 24 | $this->runMiddleware(middlewareList: $this->getPreSchedulingMiddleware(), func: static function (PreSchedulingMiddlewareInterface $middleware) use ($task, $scheduler): void { 25 | $middleware->preScheduling(task: $task, scheduler: $scheduler); 26 | }); 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function runPostSchedulingMiddleware(TaskInterface $task, SchedulerInterface $scheduler): void 33 | { 34 | $this->runMiddleware(middlewareList: $this->getPostSchedulingMiddleware(), func: static function (PostSchedulingMiddlewareInterface $middleware) use ($task, $scheduler): void { 35 | $middleware->postScheduling(task: $task, scheduler: $scheduler); 36 | }); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | public function getMiddlewareList(): array 43 | { 44 | return array_unique(array: [ 45 | ...$this->getPreSchedulingMiddleware()->toArray(), 46 | ...$this->getPostSchedulingMiddleware()->toArray(), 47 | ], flags: SORT_REGULAR); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Transport/FailOverTransportFactory.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class FailOverTransportFactory extends AbstractCompoundTransportFactory 17 | { 18 | /** 19 | * @param TransportFactoryInterface[] $transportFactories 20 | */ 21 | public function __construct(private iterable $transportFactories) 22 | { 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function createTransport( 29 | Dsn $dsn, 30 | array $options, 31 | ConfigurationInterface $configuration, 32 | SerializerInterface $serializer, 33 | SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator 34 | ): FailOverTransport { 35 | $configuration->init([ 36 | 'mode' => $options['mode'] ?? $dsn->getOption('mode', 'normal'), 37 | ], [ 38 | 'mode' => 'string', 39 | ]); 40 | 41 | return new FailOverTransport( 42 | new TransportRegistry($this->handleTransportDsn(' || ', $dsn, $this->transportFactories, $options, $configuration, $serializer, $schedulePolicyOrchestrator)), 43 | $configuration 44 | ); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function support(string $dsn, array $options = []): bool 51 | { 52 | return str_starts_with($dsn, 'failover://') || str_starts_with($dsn, 'fo://'); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Transport/Configuration/AbstractConfiguration.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | abstract class AbstractConfiguration implements ConfigurationInterface 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public function init(array $options, array $extraOptions = []): void 20 | { 21 | $finalOptions = $this->defineOptions($options, $extraOptions); 22 | 23 | array_walk($finalOptions, function (mixed $option, string $key): void { 24 | $this->set($key, $option); 25 | }); 26 | } 27 | 28 | /** 29 | * @param array $options The default options required to make the configuration work. 30 | * @param array $extraOptions A set of extra options that can be passed if required. 31 | * 32 | * @return array 33 | */ 34 | private function defineOptions(array $options = [], array $extraOptions = []): array 35 | { 36 | $resolver = new OptionsResolver(); 37 | $resolver->setDefaults([ 38 | 'execution_mode' => 'first_in_first_out', 39 | ]); 40 | 41 | $resolver->setAllowedTypes('execution_mode', 'string'); 42 | 43 | if ([] === $extraOptions) { 44 | return $resolver->resolve($options); 45 | } 46 | 47 | foreach ($extraOptions as $extraOption => $allowedTypes) { 48 | $resolver->setDefined($extraOption); 49 | $resolver->setAllowedTypes($extraOption, $allowedTypes); 50 | } 51 | 52 | return $resolver->resolve($options); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Transport/TransportRegistry.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class TransportRegistry implements TransportRegistryInterface 20 | { 21 | /** 22 | * @var TransportInterface[] 23 | */ 24 | private array $transports; 25 | 26 | /** 27 | * @param TransportInterface[] $transports 28 | */ 29 | public function __construct(iterable $transports) 30 | { 31 | $this->transports = is_array(value: $transports) ? $transports : iterator_to_array(iterator: $transports); 32 | } 33 | 34 | public function usort(Closure $func): TransportRegistryInterface 35 | { 36 | usort(array: $this->transports, callback: $func); 37 | 38 | return $this; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function reset(): TransportInterface 45 | { 46 | $firstTransport = reset(array: $this->transports); 47 | if (!$firstTransport instanceof TransportInterface) { 48 | throw new RuntimeException(message: 'The transport registry is empty'); 49 | } 50 | 51 | return $firstTransport; 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function count(): int 58 | { 59 | return count(value: $this->transports); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function getIterator(): Traversable 66 | { 67 | return new ArrayIterator(array: $this->transports); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Middleware/FiberAwareWorkerMiddlewareStack.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class FiberAwareWorkerMiddlewareStack extends AbstractFiberHandler implements WorkerMiddlewareStackInterface 17 | { 18 | public function __construct( 19 | private WorkerMiddlewareStackInterface $middlewareStack, 20 | ?LoggerInterface $logger = null 21 | ) { 22 | parent::__construct(logger: $logger); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function runPreExecutionMiddleware(TaskInterface $task): void 29 | { 30 | $this->handleOperationViaFiber(func: function () use ($task): void { 31 | $this->middlewareStack->runPreExecutionMiddleware(task: $task); 32 | }); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function runPostExecutionMiddleware(TaskInterface $task, WorkerInterface $worker): void 39 | { 40 | $this->handleOperationViaFiber(func: function () use ($task, $worker): void { 41 | $this->middlewareStack->runPostExecutionMiddleware(task: $task, worker: $worker); 42 | }); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | * 48 | * @throws Throwable {@see AbstractFiberHandler::handleOperationViaFiber()} 49 | */ 50 | public function getMiddlewareList(): array 51 | { 52 | return $this->handleOperationViaFiber(func: fn (): array => $this->middlewareStack->getMiddlewareList()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/EventListener/ProbeStateSubscriber.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | final class ProbeStateSubscriber implements EventSubscriberInterface 22 | { 23 | public function __construct(private ProbeInterface $probe, private string $path = '/_probe') 24 | { 25 | } 26 | 27 | /** 28 | * @throws Throwable {@see SchedulerInterface::getTasks()} 29 | */ 30 | public function onKernelRequest(RequestEvent $event): void 31 | { 32 | $request = $event->getRequest(); 33 | 34 | if ($this->path !== rawurldecode($request->getPathInfo())) { 35 | return; 36 | } 37 | 38 | if (Request::METHOD_GET !== $request->getMethod()) { 39 | return; 40 | } 41 | 42 | $event->setResponse(new JsonResponse([ 43 | 'scheduledTasks' => $this->probe->getScheduledTasks(), 44 | 'executedTasks' => $this->probe->getExecutedTasks(), 45 | 'failedTasks' => $this->probe->getFailedTasks(), 46 | ])); 47 | } 48 | 49 | /** 50 | * {@inheritdoc} 51 | */ 52 | public static function getSubscribedEvents(): array 53 | { 54 | return [ 55 | KernelEvents::REQUEST => [['onKernelRequest', 50]], 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Transport/LazyTransportFactory.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class LazyTransportFactory implements TransportFactoryInterface 19 | { 20 | /** 21 | * @param TransportFactoryInterface[] $factories 22 | */ 23 | public function __construct(private iterable $factories) 24 | { 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function createTransport( 31 | Dsn $dsn, 32 | array $options, 33 | ConfigurationInterface $configuration, 34 | SerializerInterface $serializer, 35 | SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator 36 | ): LazyTransport { 37 | foreach ($this->factories as $factory) { 38 | if (!$factory->support($dsn->getOptions()[0])) { 39 | continue; 40 | } 41 | 42 | $dsn = Dsn::fromString($dsn->getOptions()[0]); 43 | 44 | return new LazyTransport($factory->createTransport($dsn, $options, $configuration, $serializer, $schedulePolicyOrchestrator)); 45 | } 46 | 47 | throw new RuntimeException(sprintf('No factory found for the DSN "%s"', $dsn->getRoot())); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function support(string $dsn, array $options = []): bool 54 | { 55 | return str_starts_with($dsn, 'lazy://'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Middleware/SingleRunTaskMiddleware.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class SingleRunTaskMiddleware implements PostExecutionMiddlewareInterface, OrderedMiddlewareInterface, RequiredMiddlewareInterface 20 | { 21 | private LoggerInterface $logger; 22 | 23 | public function __construct( 24 | private TransportInterface $transport, 25 | ?LoggerInterface $logger = null 26 | ) { 27 | $this->logger = $logger ?? new NullLogger(); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function postExecute(TaskInterface $task, WorkerInterface $worker): void 34 | { 35 | if (in_array(needle: $task->getExecutionState(), haystack: [TaskInterface::INCOMPLETE, TaskInterface::TO_RETRY], strict: true)) { 36 | $this->logger->warning(message: sprintf('The task "%s" is marked as incomplete or to retry, the "is_single" option is not used', $task->getName())); 37 | 38 | return; 39 | } 40 | 41 | if (!$task->isSingleRun()) { 42 | return; 43 | } 44 | 45 | if ($task->isDeleteAfterExecute()) { 46 | $this->transport->delete(name: $task->getName()); 47 | 48 | return; 49 | } 50 | 51 | $this->transport->pause(name: $task->getName()); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function getPriority(): int 58 | { 59 | return 15; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Runner/ShellTaskRunner.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class ShellTaskRunner implements RunnerInterface 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function run(TaskInterface $task, WorkerInterface $worker): Output 24 | { 25 | if (!$task instanceof ShellTask) { 26 | return new Output($task, null, Output::ERROR); 27 | } 28 | 29 | $process = new Process( 30 | $task->getCommand(), 31 | $task->getCwd(), 32 | $task->getEnvironmentVariables(), 33 | null, 34 | $task->getTimeout() 35 | ); 36 | 37 | if ($task->mustRunInBackground()) { 38 | $process->run(null, $task->getEnvironmentVariables()); 39 | 40 | $task->setExecutionState(TaskInterface::INCOMPLETE); 41 | 42 | return new Output($task, 'Task is running in background, output is not available'); 43 | } 44 | 45 | $exitCode = $process->run(null, $task->getEnvironmentVariables()); 46 | 47 | $output = $task->isOutput() ? trim($process->getOutput()) : null; 48 | if (0 !== $exitCode) { 49 | return new Output($task, $process->getErrorOutput(), Output::ERROR); 50 | } 51 | 52 | return new Output($task, $output); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function support(TaskInterface $task): bool 59 | { 60 | return $task instanceof ShellTask; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Middleware/FiberAwareSchedulerMiddlewareStack.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class FiberAwareSchedulerMiddlewareStack extends AbstractFiberHandler implements SchedulerMiddlewareStackInterface 17 | { 18 | public function __construct( 19 | private SchedulerMiddlewareStackInterface $middlewareStack, 20 | ?LoggerInterface $logger = null 21 | ) { 22 | parent::__construct(logger: $logger); 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function runPreSchedulingMiddleware(TaskInterface $task, SchedulerInterface $scheduler): void 29 | { 30 | $this->handleOperationViaFiber(func: function () use ($task, $scheduler): void { 31 | $this->middlewareStack->runPreSchedulingMiddleware(task: $task, scheduler: $scheduler); 32 | }); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function runPostSchedulingMiddleware(TaskInterface $task, SchedulerInterface $scheduler): void 39 | { 40 | $this->handleOperationViaFiber(func: function () use ($task, $scheduler): void { 41 | $this->middlewareStack->runPostSchedulingMiddleware(task: $task, scheduler: $scheduler); 42 | }); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | * 48 | * @throws Throwable {@see AbstractFiberHandler::handleOperationViaFiber()} 49 | */ 50 | public function getMiddlewareList(): array 51 | { 52 | return $this->handleOperationViaFiber(func: fn (): array => $this->middlewareStack->getMiddlewareList()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Transport/Configuration/ConfigurationRegistry.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class ConfigurationRegistry implements ConfigurationRegistryInterface 21 | { 22 | /** 23 | * @var ConfigurationInterface[] 24 | */ 25 | private array $configurations; 26 | 27 | /** 28 | * @param ConfigurationInterface[] $configurations 29 | */ 30 | public function __construct(iterable $configurations) 31 | { 32 | $this->configurations = is_array(value: $configurations) ? $configurations : iterator_to_array(iterator: $configurations); 33 | } 34 | 35 | public function usort(Closure $func): ConfigurationRegistryInterface 36 | { 37 | usort(array: $this->configurations, callback: $func); 38 | 39 | return $this; 40 | } 41 | 42 | public function reset(): ConfigurationInterface 43 | { 44 | $firstConfiguration = reset(array:$this->configurations); 45 | if (!$firstConfiguration instanceof ConfigurationInterface) { 46 | throw new RuntimeException(message: 'The configuration registry is empty'); 47 | } 48 | 49 | return $firstConfiguration; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function getIterator(): Traversable 56 | { 57 | return new ArrayIterator(array: $this->configurations); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function count(): int 64 | { 65 | return count(value: $this->configurations); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Task/CallbackTask.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class CallbackTask extends AbstractTask 15 | { 16 | /** 17 | * @param callable|Closure|string|array $callback 18 | * @param array $arguments 19 | * @param array $options 20 | */ 21 | public function __construct( 22 | string $name, 23 | callable|Closure|string|array $callback, 24 | array $arguments = [], 25 | array $options = [] 26 | ) { 27 | $this->defineOptions(options: $options + [ 28 | 'callback' => $callback, 29 | 'arguments' => $arguments, 30 | ], additionalOptions: [ 31 | 'callback' => ['callable', 'string', 'array'], 32 | 'arguments' => ['array', 'string[]', 'int[]'], 33 | ]); 34 | 35 | parent::__construct(name: $name); 36 | } 37 | 38 | public function getCallback(): callable 39 | { 40 | return $this->options['callback']; 41 | } 42 | 43 | public function setCallback(callable $callback): self 44 | { 45 | $this->options['callback'] = $callback; 46 | 47 | return $this; 48 | } 49 | 50 | /** 51 | * @return array 52 | */ 53 | public function getArguments(): array 54 | { 55 | return is_array(value: $this->options['arguments']) ? $this->options['arguments'] : []; 56 | } 57 | 58 | /** 59 | * @param array $arguments 60 | */ 61 | public function setArguments(array $arguments): self 62 | { 63 | $this->options['arguments'] = $arguments; 64 | 65 | return $this; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Runner/ProbeTaskRunner.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | final class ProbeTaskRunner implements RunnerInterface 22 | { 23 | private HttpClientInterface $httpClient; 24 | 25 | public function __construct(?HttpClientInterface $httpClient = null) 26 | { 27 | $this->httpClient = $httpClient ?? HttpClient::create(); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function run(TaskInterface $task, WorkerInterface $worker): Output 34 | { 35 | if (!$task instanceof ProbeTask) { 36 | return new Output($task, null, Output::ERROR); 37 | } 38 | 39 | try { 40 | $response = $this->httpClient->request('GET', $task->getExternalProbePath()); 41 | $body = $response->toArray(true); 42 | if (!array_key_exists('failedTasks', $body) || ($task->getErrorOnFailedTasks() && 0 !== $body['failedTasks'])) { 43 | throw new RuntimeException('The probe state is invalid'); 44 | } 45 | 46 | return new Output($task, 'The probe succeed'); 47 | } catch (Throwable $throwable) { 48 | return new Output($task, $throwable->getMessage(), Output::ERROR); 49 | } 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function support(TaskInterface $task): bool 56 | { 57 | return $task instanceof ProbeTask; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Task/Builder/ChainedBuilder.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class ChainedBuilder extends AbstractTaskBuilder implements BuilderInterface 19 | { 20 | /** 21 | * @param BuilderInterface[] $builders 22 | */ 23 | public function __construct(ExpressionBuilderInterface $expressionBuilder, private iterable $builders = []) 24 | { 25 | parent::__construct($expressionBuilder); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function build(PropertyAccessorInterface $propertyAccessor, array $options = []): TaskInterface 32 | { 33 | $chainedTask = new ChainedTask($options['name'], ...array_map(function (array $task) use ($propertyAccessor): TaskInterface { 34 | foreach ($this->builders as $builder) { 35 | if (!$builder->support($task['type'])) { 36 | continue; 37 | } 38 | 39 | return $builder->build($propertyAccessor, $task); 40 | } 41 | 42 | throw new InvalidArgumentException('The given task cannot be created as no related builder can be found'); 43 | }, $options['tasks'])); 44 | 45 | unset($options['tasks']); 46 | 47 | return $this->handleTaskAttributes($chainedTask, $options, $propertyAccessor); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function support(?string $type = null): bool 54 | { 55 | return 'chained' === $type; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Bridge/Doctrine/Transport/DoctrineTransport.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class DoctrineTransport extends AbstractExternalTransport implements SchemaAwareInterface 21 | { 22 | public function __construct( 23 | ConfigurationInterface $configuration, 24 | DbalConnection $dbalConnection, 25 | SerializerInterface $serializer, 26 | SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator 27 | ) { 28 | parent::__construct($configuration, new Connection( 29 | $configuration, 30 | $dbalConnection, 31 | $serializer, 32 | $schedulePolicyOrchestrator 33 | ), $schedulePolicyOrchestrator); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function list(bool $lazy = false): TaskListInterface|LazyTaskList 40 | { 41 | $list = $this->connection->list(); 42 | 43 | return $lazy ? new LazyTaskList($list) : $list; 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function configureSchema(Schema $schema, DbalConnection $dbalConnection): void 50 | { 51 | if (!$this->connection instanceof Connection) { 52 | return; 53 | } 54 | 55 | $this->connection->configureSchema($schema, $dbalConnection); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/EventListener/StopWorkerOnTimeLimitSubscriber.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class StopWorkerOnTimeLimitSubscriber implements EventSubscriberInterface 21 | { 22 | private ?float $endTime = null; 23 | private LoggerInterface $logger; 24 | 25 | public function __construct( 26 | private int $timeLimitInSeconds, 27 | ?LoggerInterface $logger = null 28 | ) { 29 | $this->logger = $logger ?? new NullLogger(); 30 | } 31 | 32 | public function onWorkerStarted(): void 33 | { 34 | $this->endTime = microtime(true) + $this->timeLimitInSeconds; 35 | } 36 | 37 | public function onWorkerRunning(WorkerRunningEvent $workerRunningEvent): void 38 | { 39 | if ($this->endTime < microtime(true)) { 40 | $worker = $workerRunningEvent->getWorker(); 41 | $worker->stop(); 42 | 43 | $lastExecutedTask = $worker->getLastExecutedTask(); 44 | $this->logger->info(sprintf('Worker stopped due to time limit of %d seconds exceeded', $this->timeLimitInSeconds), [ 45 | 'lastExecutedTask' => $lastExecutedTask instanceof TaskInterface ? $lastExecutedTask->getName() : null, 46 | ]); 47 | } 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public static function getSubscribedEvents(): array 54 | { 55 | return [ 56 | WorkerStartedEvent::class => 'onWorkerStarted', 57 | WorkerRunningEvent::class => 'onWorkerRunning', 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Transport/Configuration/ExternalConnectionInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface ExternalConnectionInterface extends Countable 14 | { 15 | /** 16 | * @param array $options The default options required to make the configuration work. 17 | * @param array $extraOptions A set of extra options that can be passed if required. 18 | */ 19 | public function init(array $options, array $extraOptions = []): void; 20 | 21 | /** 22 | * Define a new @param string $key. 23 | */ 24 | public function set(string $key, mixed $value): void; 25 | 26 | /** 27 | * Update a configuration @param string $key 28 | * 29 | * @param mixed $newValue The new value stored in the configuration. 30 | */ 31 | public function update(string $key, mixed $newValue): void; 32 | 33 | /** 34 | * Return a configuration value using the @param string $key. 35 | */ 36 | public function get(string $key): mixed; 37 | 38 | /** 39 | * Remove the option stored under the @param string $key. 40 | */ 41 | public function remove(string $key): void; 42 | 43 | /** 44 | * Return the current configuration after applying the @param Closure $func to each value. 45 | */ 46 | public function walk(Closure $func): void; 47 | 48 | /** 49 | * Apply the @param Closure $func to each option. 50 | * 51 | * The options are not updated once the Closure has been applied. 52 | * 53 | * @return array 54 | */ 55 | public function map(Closure $func): array; 56 | 57 | /** 58 | * @return array 59 | */ 60 | public function toArray(): array; 61 | 62 | /** 63 | * Remove each keys and values stored. 64 | */ 65 | public function clear(): void; 66 | } 67 | -------------------------------------------------------------------------------- /src/Transport/Configuration/ConfigurationInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface ConfigurationInterface extends Countable 14 | { 15 | /** 16 | * @param array $options The default options required to make the configuration work. 17 | * @param array $extraOptions A set of extra options that can be passed if required. 18 | */ 19 | public function init(array $options, array $extraOptions = []): void; 20 | 21 | /** 22 | * Set a new @param string $value stored via the @param string $key 23 | */ 24 | public function set(string $key, mixed $value): void; 25 | 26 | /** 27 | * Update a configuration @param string $key 28 | * 29 | * @param mixed $newValue The new value stored in the configuration. 30 | */ 31 | public function update(string $key, mixed $newValue): void; 32 | 33 | /** 34 | * Return a configuration value using the @param string $key. 35 | */ 36 | public function get(string $key): mixed; 37 | 38 | /** 39 | * Remove the option stored under the @param string $key. 40 | */ 41 | public function remove(string $key): void; 42 | 43 | /** 44 | * Return the current configuration after applying the @param Closure $func to each value. 45 | */ 46 | public function walk(Closure $func): ConfigurationInterface; 47 | 48 | /** 49 | * Apply the @param Closure $func to each configuration and return an array after applying the closure. 50 | * 51 | * @return array 52 | */ 53 | public function map(Closure $func): array; 54 | 55 | /** 56 | * @return array 57 | */ 58 | public function toArray(): array; 59 | 60 | /** 61 | * Remove each key in the configuration. 62 | */ 63 | public function clear(): void; 64 | } 65 | -------------------------------------------------------------------------------- /src/Bridge/Doctrine/Transport/Configuration/DoctrineConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | final class DoctrineConfigurationFactory implements ConfigurationFactoryInterface 22 | { 23 | public function __construct(private ConnectionRegistry $registry) 24 | { 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function create(Dsn $dsn, SerializerInterface $serializer): DoctrineConfiguration 31 | { 32 | try { 33 | $doctrineConnection = $this->registry->getConnection($dsn->getHost()); 34 | } catch (InternalInvalidArgumentException $invalidArgumentException) { 35 | throw new ConfigurationException(sprintf('Could not find Doctrine connection from Scheduler configuration DSN "doctrine://%s".', $dsn->getHost()), 0, $invalidArgumentException); 36 | } 37 | 38 | if (!$doctrineConnection instanceof DoctrineConnection) { 39 | throw new InvalidArgumentException('The connection is not a valid one'); 40 | } 41 | 42 | return new DoctrineConfiguration($doctrineConnection, $dsn->getOptionAsBool('auto_setup', false)); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function support(string $dsn): bool 49 | { 50 | return str_starts_with($dsn, 'configuration://doctrine') || str_starts_with($dsn, 'configuration://dbal'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Middleware/MiddlewareRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 17 | * 18 | * @extends IteratorAggregate 19 | */ 20 | interface MiddlewareRegistryInterface extends Countable, IteratorAggregate 21 | { 22 | /** 23 | * Filter the current middleware list using @param Closure $func. 24 | * 25 | * The filter receive both key and middleware object {@see ARRAY_FILTER_USE_BOTH}, the given filter SHOULD return a bool. 26 | * 27 | * The filter is done in an atomic approach, a new {@see MiddlewareRegistryInterface} is returned. 28 | */ 29 | public function filter(Closure $func): MiddlewareRegistryInterface; 30 | 31 | /** 32 | * Apply the @param Closure $func to every middleware in the list. 33 | * 34 | * The filter is done in an atomic approach, a new {@see MiddlewareRegistryInterface} is returned. 35 | */ 36 | public function walk(Closure $func): MiddlewareRegistryInterface; 37 | 38 | /** 39 | * Allow to sort the current middleware list using @param Closure $func. 40 | * 41 | * The index association is maintained, {@see uasort()} 42 | * 43 | * The filter is done in an atomic approach, a new {@see MiddlewareRegistryInterface} is returned. 44 | */ 45 | public function uasort(Closure $func): MiddlewareRegistryInterface; 46 | 47 | /** 48 | * @return array 49 | */ 50 | public function toArray(): array; 51 | } 52 | -------------------------------------------------------------------------------- /src/Transport/Configuration/AbstractExternalConfiguration.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | abstract class AbstractExternalConfiguration extends AbstractConfiguration implements ConfigurationInterface 13 | { 14 | public function __construct(protected ExternalConnectionInterface $connection) 15 | { 16 | } 17 | 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function set(string $key, mixed $value): void 22 | { 23 | $this->connection->set($key, $value); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function update(string $key, $newValue): void 30 | { 31 | $this->connection->update($key, $newValue); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function get(string $key): mixed 38 | { 39 | return $this->connection->get($key); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function remove(string $key): void 46 | { 47 | $this->connection->remove($key); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function walk(Closure $func): ConfigurationInterface 54 | { 55 | $this->connection->walk($func); 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function map(Closure $func): array 64 | { 65 | return $this->connection->map($func); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function toArray(): array 72 | { 73 | return $this->connection->toArray(); 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function clear(): void 80 | { 81 | $this->connection->clear(); 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function count(): int 88 | { 89 | return $this->connection->count(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Transport/FiberTransportFactory.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class FiberTransportFactory implements TransportFactoryInterface 21 | { 22 | /** 23 | * @param TransportFactoryInterface[] $factories 24 | */ 25 | public function __construct( 26 | private iterable $factories, 27 | private ?LoggerInterface $logger = null 28 | ) { 29 | $this->logger = $logger ?? new NullLogger(); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function createTransport( 36 | Dsn $dsn, 37 | array $options, 38 | ConfigurationInterface $configuration, 39 | SerializerInterface $serializer, 40 | SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator 41 | ): FiberTransport { 42 | foreach ($this->factories as $factory) { 43 | if (!$factory->support($dsn->getOptions()[0])) { 44 | continue; 45 | } 46 | 47 | $dsn = Dsn::fromString($dsn->getOptions()[0]); 48 | 49 | return new FiberTransport( 50 | $factory->createTransport($dsn, $options, $configuration, $serializer, $schedulePolicyOrchestrator), 51 | $this->logger 52 | ); 53 | } 54 | 55 | throw new RuntimeException(sprintf('No factory found for the DSN "%s"', $dsn->getRoot())); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function support(string $dsn, array $options = []): bool 62 | { 63 | return str_starts_with($dsn, 'fiber://'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/DependencyInjection/SchedulerPass.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class SchedulerPass implements CompilerPassInterface 19 | { 20 | public function __construct( 21 | private string $schedulerExtraTag = 'scheduler.extra', 22 | private string $schedulerEntryPointTag = 'scheduler.entry_point' 23 | ) { 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function process(ContainerBuilder $container): void 30 | { 31 | $this->registerExtra(container: $container); 32 | $this->registerSchedulerEntrypoint(container: $container); 33 | } 34 | 35 | private function registerExtra(ContainerBuilder $container): void 36 | { 37 | foreach ($container->findTaggedServiceIds(name: $this->schedulerExtraTag) as $service => $args) { 38 | if (!$container->hasDefinition(id: $args[0]['require'])) { 39 | $container->removeDefinition(id: $service); 40 | 41 | continue; 42 | } 43 | 44 | $container->getDefinition(id: $service)->addTag(name: $args[0]['tag']); 45 | } 46 | } 47 | 48 | private function registerSchedulerEntrypoint(ContainerBuilder $container): void 49 | { 50 | foreach (array_keys(array: $container->findTaggedServiceIds(name: $this->schedulerEntryPointTag)) as $service) { 51 | $container->getDefinition(id: $service)->addMethodCall(method: 'schedule', arguments: [ 52 | new Reference(id: SchedulerInterface::class, invalidBehavior: ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE), 53 | ]); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Command/DebugConfigurationCommand.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | #[AsCommand( 21 | name: 'scheduler:debug:configuration', 22 | description: 'Display the current transport configuration keys and values', 23 | )] 24 | final class DebugConfigurationCommand extends Command 25 | { 26 | public function __construct(private ConfigurationInterface $configuration) 27 | { 28 | parent::__construct(); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function configure(): void 35 | { 36 | $this 37 | ->setHelp( 38 | help: 39 | <<<'EOF' 40 | The %command.name% command display the current configuration. 41 | 42 | php %command.full_name% 43 | EOF 44 | ) 45 | ; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | */ 51 | protected function execute(InputInterface $input, OutputInterface $output): int 52 | { 53 | $style = new SymfonyStyle(input: $input, output: $output); 54 | 55 | $style->info(message: sprintf('Found %d keys', $this->configuration->count())); 56 | 57 | $table = new Table(output: $output); 58 | $table->setHeaders(headers: ['Key', 'Value']); 59 | $this->configuration->walk(func: static function (string|bool|float|int $value, string $key) use ($table): void { 60 | $table->addRow(row: [$key, $value]); 61 | }); 62 | 63 | $table->render(); 64 | 65 | return Command::SUCCESS; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Transport/Configuration/AbstractCompoundConfigurationFactory.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | abstract class AbstractCompoundConfigurationFactory implements ConfigurationFactoryInterface 20 | { 21 | /** 22 | * @param ConfigurationFactoryInterface[] $factories 23 | * 24 | * @return ConfigurationInterface[] 25 | */ 26 | protected function handleCompoundConfiguration(string $delimiter, Dsn $dsn, iterable $factories, SerializerInterface $serializer): array 27 | { 28 | if ('' === $delimiter) { 29 | throw new InvalidArgumentException('The delimiter cannot be empty, consider using a valid one like " && " or " || "'); 30 | } 31 | 32 | $dsnList = $dsn->getOptions(); 33 | if ([] === $dsnList) { 34 | throw new LogicException(sprintf('The %s configuration factory cannot create a configuration', static::class)); 35 | } 36 | 37 | $finalDsnList = explode($delimiter, $dsnList[0]); 38 | if ($dsnList[0] === $finalDsnList[0]) { 39 | throw new InvalidArgumentException('The embedded dsn cannot be used to create a configuration'); 40 | } 41 | 42 | return array_map(static function (string $configurationDsn) use ($factories, $serializer): ConfigurationInterface { 43 | foreach ($factories as $factory) { 44 | if (!$factory->support($configurationDsn)) { 45 | continue; 46 | } 47 | 48 | return $factory->create(Dsn::fromString($configurationDsn), $serializer); 49 | } 50 | 51 | throw new InvalidArgumentException('The given dsn cannot be used to create a configuration'); 52 | }, $finalDsnList); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Transport/AbstractTransport.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | abstract class AbstractTransport implements TransportInterface 14 | { 15 | /** 16 | * @var array 17 | */ 18 | protected array $options = [ 19 | 'execution_mode' => 'first_in_first_out', 20 | ]; 21 | 22 | public function __construct(protected ConfigurationInterface $configuration) 23 | { 24 | } 25 | 26 | /** 27 | * @param array $options 28 | * @param array $additionalOptions 29 | */ 30 | protected function defineOptions(array $options = [], array $additionalOptions = []): void 31 | { 32 | $optionsResolver = new OptionsResolver(); 33 | $optionsResolver->setDefaults([ 34 | 'execution_mode' => 'first_in_first_out', 35 | ]); 36 | 37 | $optionsResolver->setAllowedTypes('execution_mode', ['string', 'null']); 38 | 39 | if ([] === $additionalOptions) { 40 | $this->options = $optionsResolver->resolve($options); 41 | } 42 | 43 | foreach ($additionalOptions as $additionalOption => $allowedTypes) { 44 | $optionsResolver->setDefined($additionalOption); 45 | $optionsResolver->setAllowedTypes($additionalOption, $allowedTypes); 46 | } 47 | 48 | $this->options = $optionsResolver->resolve($options); 49 | } 50 | 51 | public function getExecutionMode(): string 52 | { 53 | return $this->configuration->get(key: 'execution_mode'); 54 | } 55 | 56 | public function setExecutionMode(string $executionMode): self 57 | { 58 | $this->configuration->set(key: 'execution_mode', value: $executionMode); 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function getConfiguration(): ConfigurationInterface 67 | { 68 | return $this->configuration; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Command/ListFailedTasksCommand.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | #[AsCommand( 25 | name: 'scheduler:list:failed', 26 | description: 'List all the failed tasks', 27 | )] 28 | final class ListFailedTasksCommand extends Command 29 | { 30 | public function __construct(private WorkerInterface $worker) 31 | { 32 | parent::__construct(); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | protected function execute(InputInterface $input, OutputInterface $output): int 39 | { 40 | $symfonyStyle = new SymfonyStyle(input: $input, output: $output); 41 | 42 | $failedTasksList = $this->worker->getFailedTasks(); 43 | if (0 === $failedTasksList->count()) { 44 | $symfonyStyle->warning(message: 'No failed task has been found'); 45 | 46 | return self::SUCCESS; 47 | } 48 | 49 | $table = new Table(output: $output); 50 | $table->setHeaders(headers: ['Name', 'Expression', 'Reason', 'Date']); 51 | 52 | $failedTasksList->walk(func: static function (FailedTask $task) use ($table): void { 53 | $table->addRow(row: [ 54 | $task->getName(), 55 | $task->getTask()->getExpression(), 56 | $task->getReason(), 57 | $task->getFailedAt()->format(DATE_ATOM), 58 | ]); 59 | }); 60 | 61 | $symfonyStyle->success(message: sprintf('%d task%s found', count(value: $failedTasksList), count(value: $failedTasksList) > 1 ? 's' : '')); 62 | $table->render(); 63 | 64 | return self::SUCCESS; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/EventListener/StopWorkerOnFailureLimitSubscriber.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class StopWorkerOnFailureLimitSubscriber implements EventSubscriberInterface 20 | { 21 | private LoggerInterface $logger; 22 | private int $failedTasks = 0; 23 | 24 | public function __construct( 25 | private int $maximumFailedTasks, 26 | ?LoggerInterface $logger = null 27 | ) { 28 | $this->logger = $logger ?? new NullLogger(); 29 | 30 | if ($maximumFailedTasks <= 0) { 31 | throw new InvalidArgumentException(sprintf('The failure limit must be greater than 0, given %d', $maximumFailedTasks)); 32 | } 33 | } 34 | 35 | public function onTaskFailedEvent(): void 36 | { 37 | ++$this->failedTasks; 38 | } 39 | 40 | public function onWorkerStarted(WorkerRunningEvent $workerRunningEvent): void 41 | { 42 | $worker = $workerRunningEvent->getWorker(); 43 | 44 | if ($workerRunningEvent->isIdle() && $this->failedTasks >= $this->maximumFailedTasks) { 45 | $this->failedTasks = 0; 46 | $worker->stop(); 47 | 48 | $this->logger->info(sprintf( 49 | 'Worker has stopped due to the failure limit of %d exceeded', 50 | $this->maximumFailedTasks 51 | )); 52 | $this->logger->info(sprintf( 53 | 'Failure limit back to: %d', 54 | $this->failedTasks 55 | )); 56 | } 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public static function getSubscribedEvents(): array 63 | { 64 | return [ 65 | TaskFailedEvent::class => 'onTaskFailedEvent', 66 | WorkerRunningEvent::class => 'onWorkerStarted', 67 | ]; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/EventListener/TaskLifecycleSubscriber.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class TaskLifecycleSubscriber implements EventSubscriberInterface 19 | { 20 | private LoggerInterface $logger; 21 | 22 | public function __construct(?LoggerInterface $logger = null) 23 | { 24 | $this->logger = $logger ?? new NullLogger(); 25 | } 26 | 27 | public function onTaskScheduled(TaskScheduledEvent $taskScheduledEvent): void 28 | { 29 | $this->logger->info('A task has been scheduled', [ 30 | 'task' => $taskScheduledEvent->getTask()->getName(), 31 | ]); 32 | } 33 | 34 | public function onTaskUnscheduled(TaskUnscheduledEvent $taskUnscheduledEvent): void 35 | { 36 | $this->logger->info('A task has been unscheduled', [ 37 | 'task' => $taskUnscheduledEvent->getTask(), 38 | ]); 39 | } 40 | 41 | public function onTaskExecuted(TaskExecutedEvent $taskExecutedEvent): void 42 | { 43 | $this->logger->info('A task has been executed', [ 44 | 'task' => $taskExecutedEvent->getTask()->getName(), 45 | ]); 46 | } 47 | 48 | public function onTaskFailed(TaskFailedEvent $taskFailedEvent): void 49 | { 50 | $this->logger->error('A task execution has failed', [ 51 | 'task' => $taskFailedEvent->getTask()->getTask()->getName(), 52 | ]); 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public static function getSubscribedEvents(): array 59 | { 60 | return [ 61 | TaskScheduledEvent::class => 'onTaskScheduled', 62 | TaskUnscheduledEvent::class => 'onTaskUnscheduled', 63 | TaskExecutedEvent::class => 'onTaskExecuted', 64 | TaskFailedEvent::class => 'onTaskFailed', 65 | ]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Middleware/TaskLockBagMiddleware.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | final class TaskLockBagMiddleware implements PostExecutionMiddlewareInterface, OrderedMiddlewareInterface 23 | { 24 | private const TASK_LOCK_MASK = '_symfony_scheduler_foo_'; 25 | 26 | private LoggerInterface $logger; 27 | 28 | public function __construct( 29 | private LockFactory $lockFactory, 30 | ?LoggerInterface $logger = null 31 | ) { 32 | $this->logger = $logger ?? new NullLogger(); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function postExecute(TaskInterface $task, WorkerInterface $worker): void 39 | { 40 | $accessLockBag = $task->getAccessLockBag(); 41 | if (!$accessLockBag instanceof AccessLockBag) { 42 | throw new RuntimeException(message: sprintf('The task "%s" must be linked to an access lock bag, consider using %s::execute() or %s::schedule()', $task->getName(), WorkerInterface::class, SchedulerInterface::class)); 43 | } 44 | 45 | if (!$accessLockBag->getKey() instanceof Key) { 46 | return; 47 | } 48 | 49 | $lock = $this->lockFactory->createLockFromKey(key: $accessLockBag->getKey()); 50 | $lock->release(); 51 | 52 | $this->logger->info(message: sprintf('The lock for task "%s" has been released', $task->getName())); 53 | 54 | $task->setAccessLockBag(); 55 | } 56 | 57 | /** 58 | * {@inheritdoc} 59 | */ 60 | public function getPriority(): int 61 | { 62 | return 5; 63 | } 64 | 65 | public static function createKey(TaskInterface $task): Key 66 | { 67 | return new Key(resource: sprintf('%s_%s', self::TASK_LOCK_MASK, $task->getName())); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Bridge/Doctrine/Connection/AbstractDoctrineConnection.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | abstract class AbstractDoctrineConnection 19 | { 20 | public function __construct(private DBALConnection $driverConnection) 21 | { 22 | } 23 | 24 | /** 25 | * Determine the table that should be added to the current @param Schema $schema. 26 | */ 27 | abstract protected function addTableToSchema(Schema $schema): void; 28 | 29 | /** 30 | * @param array $parameters 31 | * @param array $types 32 | * @return mixed 33 | */ 34 | abstract protected function executeQuery(string $sql, array $parameters = [], array $types = []); 35 | 36 | abstract public function configureSchema(Schema $schema, DBALConnection $dbalConnection): void; 37 | 38 | /** 39 | * @throws Exception 40 | */ 41 | protected function updateSchema(): void 42 | { 43 | $schemaManager = $this->driverConnection->createSchemaManager(); 44 | $comparator = $schemaManager->createComparator(); 45 | 46 | $schemaDiff = $comparator->compareSchemas($schemaManager->createSchema(), $this->getSchema()); 47 | 48 | foreach ($schemaDiff->toSaveSql($this->driverConnection->getDatabasePlatform()) as $sql) { 49 | $this->driverConnection->executeStatement($sql); 50 | } 51 | } 52 | 53 | protected function createQueryBuilder(string $table, string $alias): QueryBuilder 54 | { 55 | return $this->driverConnection->createQueryBuilder() 56 | ->select(sprintf('%s.*', $alias)) 57 | ->from($table, $alias) 58 | ; 59 | } 60 | 61 | private function getSchema(): Schema 62 | { 63 | $schemaManager = $this->driverConnection->createSchemaManager(); 64 | 65 | $schema = new Schema([], [], $schemaManager->createSchemaConfig()); 66 | $this->addTableToSchema($schema); 67 | 68 | return $schema; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Runner/RunnerRegistry.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | final class RunnerRegistry implements RunnerRegistryInterface 22 | { 23 | /** 24 | * @var RunnerInterface[] 25 | */ 26 | private array $runners; 27 | 28 | /** 29 | * @param RunnerInterface[] $runners 30 | */ 31 | public function __construct(iterable $runners) 32 | { 33 | $this->runners = is_array(value: $runners) ? $runners : iterator_to_array(iterator: $runners); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function find(TaskInterface $task): RunnerInterface 40 | { 41 | $list = $this->filter(func: static fn (RunnerInterface $runner): bool => $runner->support(task: $task)); 42 | if (0 === $list->count()) { 43 | throw new InvalidArgumentException(message: 'No runner found for this task'); 44 | } 45 | 46 | if (1 < $list->count()) { 47 | throw new InvalidArgumentException(message: 'More than one runner found, consider improving the task and/or the runner(s)'); 48 | } 49 | 50 | return $list->current(); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function filter(Closure $func): RunnerRegistryInterface 57 | { 58 | return new self(runners: array_filter(array: $this->runners, callback: $func, mode: ARRAY_FILTER_USE_BOTH)); 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function current(): RunnerInterface 65 | { 66 | $currentRunner = current(array: $this->runners); 67 | if (false === $currentRunner) { 68 | throw new RuntimeException(message: 'The current runner cannot be found'); 69 | } 70 | 71 | return $currentRunner; 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function count(): int 78 | { 79 | return count(value: $this->runners); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Transport/AbstractCompoundTransportFactory.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | abstract class AbstractCompoundTransportFactory implements TransportFactoryInterface 21 | { 22 | /** 23 | * @param TransportFactoryInterface[] $transportFactories 24 | * @param array $options 25 | * 26 | * @return TransportInterface[] 27 | */ 28 | protected function handleTransportDsn(string $delimiter, Dsn $dsn, iterable $transportFactories, array $options, ConfigurationInterface $configuration, SerializerInterface $serializer, SchedulePolicyOrchestratorInterface $schedulePolicyOrchestrator): array 29 | { 30 | if ('' === $delimiter) { 31 | throw new InvalidArgumentException('The delimiter must not be an empty string, consider using " && " or & " || " or similar'); 32 | } 33 | 34 | $dsnList = $dsn->getOptions(); 35 | if ([] === $dsnList) { 36 | throw new LogicException(sprintf('The %s transport factory cannot create a transport', static::class)); 37 | } 38 | 39 | $transportsConfiguration = clone $configuration; 40 | 41 | return array_map(static function (string $transportDsn) use ($transportFactories, $options, $transportsConfiguration, $serializer, $schedulePolicyOrchestrator): TransportInterface { 42 | foreach ($transportFactories as $transportFactory) { 43 | if (!$transportFactory->support($transportDsn)) { 44 | continue; 45 | } 46 | 47 | return $transportFactory->createTransport(Dsn::fromString($transportDsn), $options, $transportsConfiguration, $serializer, $schedulePolicyOrchestrator); 48 | } 49 | 50 | throw new InvalidArgumentException('The given dsn cannot be used to create a transport'); 51 | }, explode($delimiter, $dsnList[0])); 52 | } 53 | } 54 | --------------------------------------------------------------------------------