├── Classes ├── Annotations │ └── Defer.php ├── Command │ ├── JobCommandController.php │ └── QueueCommandController.php ├── Exception.php ├── Job │ ├── Aspect │ │ └── DeferMethodCallAspect.php │ ├── JobInterface.php │ ├── JobManager.php │ └── StaticMethodCallJob.php ├── Queue │ ├── FakeQueue.php │ ├── Message.php │ ├── QueueInterface.php │ └── QueueManager.php └── Utility │ └── VariableDumper.php ├── CodeOfConduct.rst ├── Configuration ├── Caches.yaml ├── Objects.yaml └── Settings.yaml ├── LICENSE ├── README.md ├── Resources └── Private │ └── Schema │ └── Settings │ └── Flowpack.JobQueue.Common.schema.yaml ├── Tests ├── Functional │ ├── AbstractQueueTest.php │ └── Job │ │ └── JobManagerTest.php └── Unit │ ├── Fixtures │ ├── TestJob.php │ └── TestQueue.php │ ├── Job │ └── JobManagerTest.php │ ├── Queue │ └── QueueManagerTest.php │ └── Utility │ └── VariableDumperTest.php └── composer.json /Classes/Annotations/Defer.php: -------------------------------------------------------------------------------- 1 | 123) - Supported options depend on the concrete queue implementation) 32 | * @var array 33 | */ 34 | public $options; 35 | 36 | /** 37 | * @param string|null $queueName 38 | * @param array|null $options 39 | * @param string|null $value 40 | */ 41 | public function __construct(?string $queueName = null, ?array $options = null, ?string $value = null) 42 | { 43 | if ($value === null && $queueName === null) { 44 | throw new \InvalidArgumentException('A Defer attribute must specify a queueName.', 1334128835); 45 | } 46 | $this->queueName = $queueName ?? $value; 47 | $this->options = $options ?? []; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Classes/Command/JobCommandController.php: -------------------------------------------------------------------------------- 1 | exit-after flag can be used in conjunction with cron-jobs in order to manually (re)start 54 | * the worker after a given amount of time. 55 | * 56 | * With the limit flag the number of executed jobs can be limited before the script exits. 57 | * This can be combined with exit-after to exit when either the time or job limit is reached 58 | * 59 | * The verbose flag can be used to gain some insight about which jobs are executed etc. 60 | * 61 | * @param string $queue Name of the queue to fetch messages from. Can also be a comma-separated list of queues. 62 | * @param int $exitAfter If set, this command will exit after the given amount of seconds 63 | * @param int $limit If set, only the given amount of jobs are processed (successful or not) before the script exits 64 | * @param bool $verbose Output debugging information 65 | * @return void 66 | * @throws StopActionException 67 | */ 68 | public function workCommand($queue, $exitAfter = null, $limit = null, $verbose = false) 69 | { 70 | if ($verbose) { 71 | $this->output('Watching queue "%s"', [$queue]); 72 | if ($exitAfter !== null) { 73 | $this->output(' for %d seconds', [$exitAfter]); 74 | } 75 | $this->outputLine('...'); 76 | } 77 | $startTime = time(); 78 | $timeout = null; 79 | $numberOfJobExecutions = 0; 80 | do { 81 | $message = null; 82 | if ($exitAfter !== null) { 83 | $timeout = max(1, $exitAfter - (time() - $startTime)); 84 | } 85 | try { 86 | $message = $this->jobManager->waitAndExecute($queue, $timeout); 87 | } catch (JobQueueException $exception) { 88 | $numberOfJobExecutions ++; 89 | $this->outputLine('%s', [$exception->getMessage()]); 90 | if ($verbose && $exception->getPrevious() instanceof \Exception) { 91 | $this->outputLine('Reason:'); 92 | $this->outputLine($exception->getPrevious()->getMessage()); 93 | } 94 | } catch (\Exception $exception) { 95 | $this->outputLine('Unexpected exception during job execution: %s, aborting...', [$exception->getMessage()]); 96 | $this->quit(1); 97 | } 98 | if ($message !== null) { 99 | $numberOfJobExecutions ++; 100 | if ($verbose) { 101 | $messagePayload = strlen($message->getPayload()) <= 50 ? $message->getPayload() : substr($message->getPayload(), 0, 50) . '...'; 102 | $this->outputLine('Successfully executed job "%s" (%s)', [$message->getIdentifier(), $messagePayload]); 103 | } 104 | } 105 | if ($exitAfter !== null && (time() - $startTime) >= $exitAfter) { 106 | if ($verbose) { 107 | $this->outputLine('Quitting after %d seconds due to --exit-after flag', [time() - $startTime]); 108 | } 109 | $this->quit(); 110 | } 111 | if ($limit !== null && $numberOfJobExecutions >= $limit) { 112 | if ($verbose) { 113 | $this->outputLine('Quitting after %d executed job%s due to --limit flag', [$numberOfJobExecutions, $numberOfJobExecutions > 1 ? 's' : '']); 114 | } 115 | $this->quit(); 116 | } 117 | 118 | } while (true); 119 | } 120 | 121 | /** 122 | * List queued jobs 123 | * 124 | * Shows the label of the next $limit Jobs in a given queue. 125 | * 126 | * @param string $queue The name of the queue 127 | * @param integer $limit Number of jobs to list (some queues only support a limit of 1) 128 | * @return void 129 | * @throws JobQueueException 130 | */ 131 | public function listCommand($queue, $limit = 1) 132 | { 133 | $jobs = $this->jobManager->peek($queue, $limit); 134 | $totalCount = $this->queueManager->getQueue($queue)->countReady(); 135 | foreach ($jobs as $job) { 136 | $this->outputLine('%s', [$job->getLabel()]); 137 | } 138 | 139 | if ($totalCount > count($jobs)) { 140 | $this->outputLine('(%d omitted) ...', [$totalCount - count($jobs)]); 141 | } 142 | $this->outputLine('(%d total)', [$totalCount]); 143 | } 144 | 145 | /** 146 | * Execute one job 147 | * 148 | * @param string $queue 149 | * @param string $messageCacheIdentifier An identifier to receive the message from the cache 150 | * @return void 151 | * @internal This command is mainly used by the JobManager and FakeQueue in order to execute commands in sub requests 152 | * @throws JobQueueException 153 | */ 154 | public function executeCommand($queue, $messageCacheIdentifier) 155 | { 156 | if(!$this->messageCache->has($messageCacheIdentifier)) { 157 | throw new JobQueueException(sprintf('No message with identifier %s was found in the message cache.', $messageCacheIdentifier), 1517868903); 158 | } 159 | 160 | /** @var Message $message */ 161 | $message = $this->messageCache->get($messageCacheIdentifier); 162 | $queue = $this->queueManager->getQueue($queue); 163 | $this->jobManager->executeJobForMessage($queue, $message); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /Classes/Command/QueueCommandController.php: -------------------------------------------------------------------------------- 1 | queueConfigurations as $queueName => $queueConfiguration) { 51 | $queue = $this->queueManager->getQueue($queueName); 52 | try { 53 | $numberOfMessages = $queue->countReady(); 54 | } catch (\Exception $e) { 55 | $numberOfMessages = '-'; 56 | } 57 | $rows[] = [$queue->getName(), TypeHandling::getTypeForValue($queue), $numberOfMessages]; 58 | } 59 | $this->output->outputTable($rows, ['Queue', 'Type', '# messages']); 60 | } 61 | 62 | /** 63 | * Describe a single queue 64 | * 65 | * Displays the configuration for a queue, merged with the preset settings if any. 66 | * 67 | * @param string $queue Name of the queue to describe (e.g. "some-queue") 68 | * @return void 69 | * @throws Exception 70 | */ 71 | public function describeCommand($queue) 72 | { 73 | $queueSettings = $this->queueManager->getQueueSettings($queue); 74 | $this->outputLine('Configuration options for Queue %s:', [$queue]); 75 | $rows = []; 76 | foreach ($queueSettings as $name => $value) { 77 | $rows[] = [$name, is_array($value) ? json_encode($value, JSON_PRETTY_PRINT) : $value]; 78 | } 79 | $this->output->outputTable($rows, ['Option', 'Value']); 80 | } 81 | 82 | /** 83 | * Initialize a queue 84 | * 85 | * Checks connection to the queue backend and sets up prerequisites (e.g. required database tables) 86 | * Most queue implementations don't need to be initialized explicitly, but it doesn't harm and might help to find misconfigurations 87 | * 88 | * @param string $queue Name of the queue to initialize (e.g. "some-queue") 89 | * @return void 90 | * @throws Exception 91 | * @throws StopActionException 92 | */ 93 | public function setupCommand($queue) 94 | { 95 | $queue = $this->queueManager->getQueue($queue); 96 | try { 97 | $queue->setUp(); 98 | } catch (\Exception $exception) { 99 | $this->outputLine('An error occurred while trying to setup queue "%s":', [$queue->getName()]); 100 | $this->outputLine('%s (#%s)', [$exception->getMessage(), $exception->getCode()]); 101 | $this->quit(1); 102 | } 103 | $this->outputLine('Queue "%s" has been initialized successfully.', [$queue->getName()]); 104 | } 105 | 106 | /** 107 | * Remove all messages from a queue! 108 | * 109 | * This command will delete all messages from the given queue. 110 | * Thus it should only be used in tests or with great care! 111 | * 112 | * @param string $queue Name of the queue to flush (e.g. "some-queue") 113 | * @param bool $force This flag is required in order to avoid accidental flushes 114 | * @return void 115 | * @throws Exception 116 | * @throws StopActionException 117 | */ 118 | public function flushCommand($queue, $force = false) 119 | { 120 | $queue = $this->queueManager->getQueue($queue); 121 | if (!$force) { 122 | $this->outputLine('Use the --force flag if you really want to flush queue "%s"', [$queue->getName()]); 123 | $this->outputLine('Warning: This will delete all messages from the queue!'); 124 | $this->quit(1); 125 | } 126 | $queue->flush(); 127 | $this->outputLine('Flushed queue "%s".', [$queue->getName()]); 128 | } 129 | 130 | /** 131 | * Submit a message to a given queue 132 | * 133 | * This command can be used to "manually" add messages to a given queue. 134 | * 135 | * Example: 136 | * flow queue:submit some-queue "some payload" --options '{"delay": 14}' 137 | * 138 | * To make this work with the JobManager the payload has to be a serialized 139 | * instance of an object implementing JobInterface. 140 | * 141 | * @param string $queue Name of the queue to submit a message to (e.g. "some-queue") 142 | * @param string $payload Arbitrary payload, for example a serialized instance of a class implementing JobInterface 143 | * @param string $options JSON encoded, for example '{"some-option": "some-value"}' 144 | * @return void 145 | * @throws Exception 146 | */ 147 | public function submitCommand($queue, $payload, $options = null) 148 | { 149 | $queue = $this->queueManager->getQueue($queue); 150 | if ($options !== null) { 151 | $options = json_decode($options, true); 152 | } 153 | $messageId = $queue->submit($payload, $options !== null ? $options : []); 154 | $this->outputLine('Submitted payload to queue "%s" with ID "%s".', [$queue->getName(), $messageId]); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /Classes/Exception.php: -------------------------------------------------------------------------------- 1 | processingJob) { 54 | return $joinPoint->getAdviceChain()->proceed($joinPoint); 55 | } 56 | /** @var Defer $deferAnnotation */ 57 | $deferAnnotation = $this->reflectionService->getMethodAnnotation($joinPoint->getClassName(), $joinPoint->getMethodName(), Defer::class); 58 | $queueName = $deferAnnotation->queueName; 59 | $job = new StaticMethodCallJob($joinPoint->getClassName(), $joinPoint->getMethodName(), $joinPoint->getMethodArguments()); 60 | $this->jobManager->queue($queueName, $job, $deferAnnotation->options); 61 | return null; 62 | } 63 | 64 | /** 65 | * @param boolean $processingJob 66 | */ 67 | public function setProcessingJob($processingJob) 68 | { 69 | $this->processingJob = $processingJob; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Classes/Job/JobInterface.php: -------------------------------------------------------------------------------- 1 | queueManager->getQueue($queueName); 92 | 93 | $payload = serialize($job); 94 | $messageId = $queue->submit($payload, $options); 95 | $this->emitMessageSubmitted($queue, $messageId, $payload, $options); 96 | } 97 | 98 | /** 99 | * Wait for a job in the given queue and execute it 100 | * A worker using this method should catch exceptions 101 | * 102 | * @param string $queueName 103 | * @param integer $timeout 104 | * @return Message The message that was processed or NULL if no job was executed and a timeout occurred 105 | * @throws \Exception 106 | * @api 107 | */ 108 | public function waitAndExecute(string $queueName, $timeout = null): ?Message 109 | { 110 | 111 | $messageCacheIdentifier = null; 112 | $queue = $this->queueManager->getQueue($queueName); 113 | $message = $queue->waitAndReserve($timeout); 114 | if ($message === null) { 115 | $this->emitMessageTimeout($queue); 116 | // timeout 117 | return null; 118 | } 119 | $this->emitMessageReserved($queue, $message); 120 | 121 | $queueSettings = $this->queueManager->getQueueSettings($queueName); 122 | try { 123 | if (isset($queueSettings['executeIsolated']) && $queueSettings['executeIsolated'] === true) { 124 | $messageCacheIdentifier = sha1(serialize($message)); 125 | $this->messageCache->set($messageCacheIdentifier, $message); 126 | Scripts::executeCommand('flowpack.jobqueue.common:job:execute', $this->flowSettings, $queueSettings['outputResults'] ?? false, ['queue' => $queue->getName(), 'messageCacheIdentifier' => $messageCacheIdentifier]); 127 | } else { 128 | $this->executeJobForMessage($queue, $message); 129 | } 130 | } catch (\Throwable $throwable) { 131 | $maximumNumberOfReleases = isset($queueSettings['maximumNumberOfReleases']) ? 132 | (int)$queueSettings['maximumNumberOfReleases'] : 133 | self::DEFAULT_MAXIMUM_NUMBER_RELEASES; 134 | if ($message->getNumberOfReleases() < $maximumNumberOfReleases) { 135 | $releaseOptions = isset($queueSettings['releaseOptions']) ? $queueSettings['releaseOptions'] : []; 136 | $queue->release($message->getIdentifier(), $releaseOptions); 137 | $this->emitMessageReleased($queue, $message, $releaseOptions, new \RuntimeException($throwable->getMessage(), 1659019014, $throwable)); 138 | $logMessage = $this->throwableStorage->logThrowable($throwable); 139 | $this->logger->error($logMessage, LogEnvironment::fromMethodName(__METHOD__)); 140 | throw new JobQueueException(sprintf('Job execution for job (message: "%s", queue: "%s") failed (%d/%d trials) - RELEASE', $message->getIdentifier(), $queue->getName(), $message->getNumberOfReleases() + 1, $maximumNumberOfReleases + 1), 1334056583, $throwable); 141 | } else { 142 | $queue->abort($message->getIdentifier()); 143 | $this->emitMessageFailed($queue, $message, new \RuntimeException($throwable->getMessage(), 1659019015, $throwable)); 144 | $logMessage = $this->throwableStorage->logThrowable($throwable); 145 | $this->logger->error($logMessage, LogEnvironment::fromMethodName(__METHOD__)); 146 | throw new JobQueueException(sprintf('Job execution for job (message: "%s", queue: "%s") failed (%d/%d trials) - ABORTING', $message->getIdentifier(), $queue->getName(), $message->getNumberOfReleases() + 1, $maximumNumberOfReleases + 1), 1334056584, $throwable); 147 | } 148 | } finally { 149 | if ($messageCacheIdentifier !== null) { 150 | $this->messageCache->remove($messageCacheIdentifier); 151 | } 152 | } 153 | 154 | $queue->finish($message->getIdentifier()); 155 | $this->emitMessageFinished($queue, $message); 156 | 157 | return $message; 158 | } 159 | 160 | /** 161 | * @param QueueInterface $queue 162 | * @param Message $message 163 | * @return void 164 | * @throws JobQueueException 165 | * @internal This method has to be public so that it can be run from the command handler (when "executeIsolated" is set). It is not meant to be called from "user land" 166 | */ 167 | public function executeJobForMessage(QueueInterface $queue, Message $message): void 168 | { 169 | // TODO stabilize unserialize() call (maybe using PHPs unserialize_callback_func directive) 170 | $job = unserialize($message->getPayload()); 171 | if (!$job instanceof JobInterface) { 172 | throw new \RuntimeException(sprintf('The message "%s" in queue "%s" could not be unserialized to a class implementing JobInterface', $message->getIdentifier(), $queue->getName()), 1465901245); 173 | } 174 | $jobExecutionSuccess = $job->execute($queue, $message); 175 | if (!$jobExecutionSuccess) { 176 | throw new JobQueueException(sprintf('execute() for job "%s" did not return TRUE', $job->getLabel()), 1468927872); 177 | } 178 | } 179 | 180 | /** 181 | * 182 | * @param string $queueName 183 | * @param integer $limit 184 | * @return JobInterface[] 185 | * @api 186 | */ 187 | public function peek(string $queueName, int $limit = 1): array 188 | { 189 | $queue = $this->queueManager->getQueue($queueName); 190 | $messages = $queue->peek($limit); 191 | return array_map(function (Message $message) { 192 | $job = unserialize($message->getPayload()); 193 | return $job; 194 | }, $messages); 195 | } 196 | 197 | /** 198 | * Signal that is triggered when a message has been submitted to a queue 199 | * 200 | * @param QueueInterface $queue The queue a message was submitted to 201 | * @param string $messageId The unique id of the message that was submitted (determined by the queue implementation) 202 | * @param mixed $payload The serialized job that has been added to a queue 203 | * @param array $options Optional array of options passed to JobManager::queue() 204 | * @return void 205 | * @Flow\Signal 206 | * @api 207 | */ 208 | protected function emitMessageSubmitted(QueueInterface $queue, $messageId, $payload, array $options = []): void 209 | { 210 | } 211 | 212 | /** 213 | * Signal that is triggered when a message could not be reserved (probably due to a timeout) 214 | * 215 | * @param QueueInterface $queue The queue that returned with a timeout 216 | * @return void 217 | * @Flow\Signal 218 | * @api 219 | */ 220 | protected function emitMessageTimeout(QueueInterface $queue): void 221 | { 222 | } 223 | 224 | /** 225 | * Signal that is triggered when a message was reserved 226 | * 227 | * @param QueueInterface $queue The queue the reserved message belongs to 228 | * @param Message $message The message that was reserved 229 | * @return void 230 | * @Flow\Signal 231 | * @api 232 | */ 233 | protected function emitMessageReserved(QueueInterface $queue, Message $message): void 234 | { 235 | } 236 | 237 | /** 238 | * Signal that is triggered when a message has been processed successfully 239 | * 240 | * @param QueueInterface $queue The queue the finished message belongs to 241 | * @param Message $message The message that was finished successfully 242 | * @return void 243 | * @Flow\Signal 244 | * @api 245 | */ 246 | protected function emitMessageFinished(QueueInterface $queue, Message $message): void 247 | { 248 | } 249 | 250 | /** 251 | * Signal that is triggered when a message has been re-released to the queue 252 | * 253 | * @param QueueInterface $queue The queue the released message belongs to 254 | * @param Message $message The message that was released to the queue again 255 | * @param array $releaseOptions The options that were passed to the release call 256 | * @param \Exception|null $jobExecutionException The exception (if any) thrown by the job execution 257 | * @return void 258 | * @Flow\Signal 259 | * @api 260 | */ 261 | protected function emitMessageReleased(QueueInterface $queue, Message $message, array $releaseOptions, ?\Exception $jobExecutionException = null): void 262 | { 263 | } 264 | 265 | /** 266 | * Signal that is triggered when processing of a message failed 267 | * 268 | * @param QueueInterface $queue The queue the failed message belongs to 269 | * @param Message $message The message that could not be executed successfully 270 | * @param \Exception|null $jobExecutionException The exception (if any) thrown by the job execution 271 | * @return void 272 | * @Flow\Signal 273 | * @api 274 | */ 275 | protected function emitMessageFailed(QueueInterface $queue, Message $message, ?\Exception $jobExecutionException = null): void 276 | { 277 | } 278 | 279 | } 280 | -------------------------------------------------------------------------------- /Classes/Job/StaticMethodCallJob.php: -------------------------------------------------------------------------------- 1 | className = $className; 61 | $this->methodName = $methodName; 62 | $this->arguments = $arguments; 63 | } 64 | 65 | /** 66 | * Execute the job 67 | * 68 | * A job should finish itself after successful execution using the queue methods. 69 | * 70 | * @param QueueInterface $queue 71 | * @param Message $message 72 | * @return boolean TRUE If the execution was successful 73 | * @throws \Exception 74 | */ 75 | public function execute(QueueInterface $queue, Message $message): bool 76 | { 77 | $service = $this->objectManager->get($this->className); 78 | $this->deferMethodCallAspect->setProcessingJob(true); 79 | try { 80 | $methodName = $this->methodName; 81 | call_user_func_array([$service, $methodName], $this->arguments); 82 | return true; 83 | } catch (\Exception $exception) { 84 | throw $exception; 85 | } finally { 86 | $this->deferMethodCallAspect->setProcessingJob(false); 87 | } 88 | } 89 | 90 | /** 91 | * @return string 92 | */ 93 | public function getLabel(): string 94 | { 95 | $arguments = array_map([VariableDumper::class, 'dumpValue'], $this->arguments); 96 | return sprintf('%s::%s(%s)', $this->className, $this->methodName, implode(', ', $arguments)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Classes/Queue/FakeQueue.php: -------------------------------------------------------------------------------- 1 | name = $name; 55 | if (isset($options['async']) && $options['async'] === true) { 56 | $this->async = true; 57 | } 58 | } 59 | 60 | /** 61 | * @inheritdoc 62 | */ 63 | public function setUp(): void 64 | { 65 | // The FakeQueue does not require any setup but we use it to verify the options 66 | if ($this->async && !method_exists(Scripts::class, 'executeCommandAsync')) { 67 | throw new \RuntimeException('The "async" flag is set, but the currently used Flow version doesn\'t support this (Flow 3.3+ is required)', 1468940734); 68 | } 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | public function getName(): string 75 | { 76 | return $this->name; 77 | } 78 | 79 | /** 80 | * @inheritdoc 81 | */ 82 | public function submit($payload, array $options = []): string 83 | { 84 | $messageId = Algorithms::generateUUID(); 85 | $message = new Message($messageId, $payload); 86 | 87 | $messageCacheIdentifier = sha1(serialize($message)); 88 | $this->messageCache->set($messageCacheIdentifier, $message); 89 | 90 | if ($this->async) { 91 | Scripts::executeCommandAsync('flowpack.jobqueue.common:job:execute', $this->flowSettings, ['queue' => $this->name, 'messageCacheIdentifier' => $messageCacheIdentifier]); 92 | } else { 93 | Scripts::executeCommand('flowpack.jobqueue.common:job:execute', $this->flowSettings, true, ['queue' => $this->name, 'messageCacheIdentifier' => $messageCacheIdentifier]); 94 | } 95 | return $messageId; 96 | } 97 | 98 | /** 99 | * @inheritdoc 100 | */ 101 | public function waitAndTake(?int $timeout = null): Message 102 | { 103 | throw new \BadMethodCallException('The FakeQueue does not support reserving of messages.' . chr(10) . 'It is not required to use a worker for this queue as messages are handled immediately upon submission.', 1468425275); 104 | } 105 | 106 | /** 107 | * @inheritdoc 108 | */ 109 | public function waitAndReserve(?int $timeout = null): Message 110 | { 111 | throw new \BadMethodCallException('The FakeQueue does not support reserving of messages.' . chr(10) . 'It is not required to use a worker for this queue as messages are handled immediately upon submission.', 1468425280); 112 | } 113 | 114 | /** 115 | * @inheritdoc 116 | */ 117 | public function release(string $messageId, array $options = []): void 118 | { 119 | throw new \BadMethodCallException('The FakeQueue does not support releasing of failed messages.' . chr(10) . 'The "maximumNumberOfReleases" setting should be removed or set to 0 for this queue!', 1468425285); 120 | } 121 | 122 | /** 123 | * @inheritdoc 124 | */ 125 | public function abort(string $messageId): void 126 | { 127 | // The FakeQueue does not support message abortion 128 | } 129 | 130 | /** 131 | * @inheritdoc 132 | */ 133 | public function finish(string $messageId): bool 134 | { 135 | // The FakeQueue does not support message finishing 136 | return false; 137 | } 138 | 139 | /** 140 | * @inheritdoc 141 | */ 142 | public function peek(int $limit = 1): array 143 | { 144 | return []; 145 | } 146 | 147 | /** 148 | * @inheritdoc 149 | */ 150 | public function countReady(): int 151 | { 152 | return 0; 153 | } 154 | 155 | /** 156 | * @inheritdoc 157 | */ 158 | public function countReserved(): int 159 | { 160 | return 0; 161 | } 162 | 163 | /** 164 | * @inheritdoc 165 | */ 166 | public function countFailed(): int 167 | { 168 | return 0; 169 | } 170 | 171 | /** 172 | * @inheritdoc 173 | */ 174 | public function flush(): void 175 | { 176 | // The FakeQueue does not support message flushing 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /Classes/Queue/Message.php: -------------------------------------------------------------------------------- 1 | identifier = $identifier; 49 | $this->payload = $payload; 50 | $this->numberOfReleases = $numberOfReleases; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function getIdentifier(): string 57 | { 58 | return $this->identifier; 59 | } 60 | 61 | /** 62 | * @return mixed 63 | */ 64 | public function getPayload() 65 | { 66 | return $this->payload; 67 | } 68 | 69 | /** 70 | * @return int 71 | */ 72 | public function getNumberOfReleases(): int 73 | { 74 | return $this->numberOfReleases; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Classes/Queue/QueueInterface.php: -------------------------------------------------------------------------------- 1 | queues[$queueName])) { 59 | return $this->queues[$queueName]; 60 | } 61 | 62 | $queueSettings = $this->getQueueSettings($queueName); 63 | 64 | if (!isset($queueSettings['className'])) { 65 | throw new JobQueueException(sprintf('Option className for queue "%s" is not configured', $queueName), 1334147126); 66 | } 67 | 68 | $queueObjectName = $queueSettings['className']; 69 | if (!class_exists($queueObjectName)) { 70 | throw new JobQueueException(sprintf('Configured class "%s" for queue "%s" does not exist', $queueObjectName, $queueName), 1445611607); 71 | } 72 | 73 | 74 | if (isset($queueSettings['queueNamePrefix'])) { 75 | $queueNameWithPrefix = $queueSettings['queueNamePrefix'] . $queueName; 76 | } else { 77 | $queueNameWithPrefix = $queueName; 78 | } 79 | $options = isset($queueSettings['options']) ? $queueSettings['options'] : []; 80 | $queue = new $queueObjectName($queueNameWithPrefix, $options); 81 | $this->queues[$queueName] = $queue; 82 | 83 | return $queue; 84 | } 85 | 86 | /** 87 | * Returns the settings for the requested queue, merged with the preset defaults if any 88 | * 89 | * @param string $queueName 90 | * @return array 91 | * @throws JobQueueException if no queue for the given $queueName is configured 92 | * @api 93 | */ 94 | public function getQueueSettings(string $queueName): array 95 | { 96 | if (isset($this->queueSettingsRuntimeCache[$queueName])) { 97 | return $this->queueSettingsRuntimeCache[$queueName]; 98 | } 99 | if (!isset($this->settings['queues'][$queueName])) { 100 | throw new JobQueueException(sprintf('Queue "%s" is not configured', $queueName), 1334054137); 101 | } 102 | $queueSettings = $this->settings['queues'][$queueName]; 103 | if (isset($queueSettings['preset'])) { 104 | $presetName = $queueSettings['preset']; 105 | if (!isset($this->settings['presets'][$presetName])) { 106 | throw new JobQueueException(sprintf('Preset "%s", referred to in settings for queue "%s" is not configured', $presetName, $queueName), 1466677893); 107 | } 108 | $queueSettings = Arrays::arrayMergeRecursiveOverrule($this->settings['presets'][$presetName], $queueSettings); 109 | } 110 | $this->queueSettingsRuntimeCache[$queueName] = $queueSettings; 111 | return $this->queueSettingsRuntimeCache[$queueName]; 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /Classes/Utility/VariableDumper.php: -------------------------------------------------------------------------------- 1 | $maximumLength) { 40 | return UnicodeUtilityFunctions::substr($value, 0, $maximumLength - 1) . '…'; 41 | } 42 | return $value; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CodeOfConduct.rst: -------------------------------------------------------------------------------- 1 | Contributor Code of Conduct 2 | --------------------------- 3 | 4 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 5 | 6 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 7 | 8 | Examples of unacceptable behavior by participants include: 9 | 10 | * The use of sexualized language or imagery 11 | * Personal attacks 12 | * Trolling or insulting/derogatory comments 13 | * Public or private harassment 14 | * Publishing other's private information, such as physical or electronic addresses, without explicit permission 15 | * Other unethical or unprofessional conduct. 16 | 17 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 18 | 19 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 20 | 21 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 22 | 23 | This Code of Conduct is adapted from the `Contributor Covenant `_, version 1.2.0, available at (http://contributor-covenant.org/version/1/2/0/ 24 | -------------------------------------------------------------------------------- /Configuration/Caches.yaml: -------------------------------------------------------------------------------- 1 | FlowPackJobQueueCommon_MessageCache: 2 | frontend: Neos\Cache\Frontend\VariableFrontend 3 | persistent: true 4 | -------------------------------------------------------------------------------- /Configuration/Objects.yaml: -------------------------------------------------------------------------------- 1 | Flowpack\JobQueue\Common\Job\JobManager: 2 | properties: 3 | messageCache: 4 | object: 5 | factoryObjectName: Neos\Flow\Cache\CacheManager 6 | factoryMethodName: getCache 7 | arguments: 8 | 1: 9 | value: FlowPackJobQueueCommon_MessageCache 10 | 11 | Flowpack\JobQueue\Common\Command\JobCommandController: 12 | properties: 13 | messageCache: 14 | object: 15 | factoryObjectName: Neos\Flow\Cache\CacheManager 16 | factoryMethodName: getCache 17 | arguments: 18 | 1: 19 | value: FlowPackJobQueueCommon_MessageCache 20 | 21 | Flowpack\JobQueue\Common\Queue\FakeQueue: 22 | properties: 23 | messageCache: 24 | object: 25 | factoryObjectName: Neos\Flow\Cache\CacheManager 26 | factoryMethodName: getCache 27 | arguments: 28 | 1: 29 | value: FlowPackJobQueueCommon_MessageCache 30 | -------------------------------------------------------------------------------- /Configuration/Settings.yaml: -------------------------------------------------------------------------------- 1 | Flowpack: 2 | JobQueue: 3 | Common: 4 | presets: [] 5 | # 'example': 6 | # # FQN of the queue implementation to use 7 | # className: 'Flownative\JobQueue\Sqlite\Queue\SqliteQueue' 8 | # 9 | # # if defined, all queues are prefixed with the configured "queueNamePrefix". This helps if multiple 10 | # # Flow instances share the same queue service. 11 | # queueNamePrefix: '' 12 | # 13 | # # If set jobs are executed on a separate thread avoiding side-effects and memory-leaks 14 | # executeIsolated: true 15 | # 16 | # # If set to true, forwards the full output (stdout + stderr) of the respective job to the stdout of its "parent" 17 | # outputResults: false 18 | # 19 | # # The max number of times a message is released when job execution failed 20 | # maximumNumberOfReleases: 3 21 | # 22 | # # Default options when submitting new jobs 23 | # options: 24 | # 'foo': 'bar' 25 | # # Default options when a message is released again 26 | # releaseOptions: 27 | # delay: 300 28 | # 29 | queues: [] 30 | # 'example': 31 | # preset: 'example' 32 | # options: 33 | # 'overridden': 'option' 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Neos project contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flowpack.JobQueue.Common 2 | 3 | Neos Flow package that allows for asynchronous and distributed execution of tasks. 4 | 5 | ### Table of contents 6 | 7 | * [Quickstart](#quickstart-tldr) 8 | * [Introduction](#introduction) 9 | * [Message Queue](#message-queue) 10 | * [Job Queue](#job-queue) 11 | * [Command Line Interface](#command-line-interface) 12 | * [Signals & Slots](#signal--slots) 13 | * [License](#license) 14 | * [Contributions](#contributions) 15 | 16 | ## Quickstart (TL;DR) 17 | 18 | 1. **Install this package using composer:** 19 | 20 | ``` 21 | composer require flowpack/jobqueue-common 22 | ``` 23 | (or by adding the dependency to the composer manifest of an installed package) 24 | 25 | 2. **Configure a basic queue by adding the following to your `Settings.yaml`:** 26 | 27 | ```yaml 28 | Flowpack: 29 | JobQueue: 30 | Common: 31 | queues: 32 | 'some-queue': 33 | className: 'Flowpack\JobQueue\Common\Queue\FakeQueue' 34 | ``` 35 | 36 | 3. **Initialize the queue (if required)** 37 | 38 | With 39 | 40 | ``` 41 | ./flow queue:setup some-queue 42 | ``` 43 | 44 | you can setup the queue and/or verify its configuration. 45 | In the case of the `FakeQueue` that step is not required. 46 | 47 | *Note:* The `queue:setup` command won't remove any existing messages, there is no harm in calling it multiple times 48 | 49 | 4. **Annotate any *public* method you want to be executed asynchronously:** 50 | 51 | ```php 52 | use Flowpack\JobQueue\Common\Annotations as Job; 53 | 54 | class SomeClass { 55 | 56 | /** 57 | * @Job\Defer(queueName="some-queue") 58 | */ 59 | public function sendEmail($emailAddress) 60 | { 61 | // send some email to $emailAddress 62 | } 63 | } 64 | ``` 65 | 66 | or use attributes instead of annotations (PHP 8.0 and later): 67 | 68 | ```php 69 | use Flowpack\JobQueue\Common\Annotations as Job; 70 | 71 | class SomeClass { 72 | 73 | #[Job\Defer(queueName: "some-queue")] 74 | public function sendEmail($emailAddress) 75 | { 76 | // send some email to $emailAddress 77 | } 78 | } 79 | ``` 80 | 81 | *Note:* The method needs to be *public* and it must not return anything 82 | 83 | 5. **Start the worker (if required)** 84 | 85 | With the above code in place, whenever the method `SomeClass::sendEmail()` is about to be called that method call is converted into a job that is executed asynchronously[1]. 86 | 87 | Unless you use the `FakeQueue` like in the example, a so called `worker` has to be started, to listen for new jobs and execute them:: 88 | 89 | ``` 90 | ./flow flowpack.jobqueue.common:job:work some-queue --verbose 91 | ``` 92 | 93 | ## Introduction 94 | 95 | To get started let's first define some terms: 96 | 97 |
98 |
Message
99 |
100 | A piece of information passed between programs or systems, sometimes also referred to as "Event".
101 | In the JobQueue packages we use messages to transmit `Jobs`. 102 |
103 |
Message Queue
104 |
105 | According to Wikipedia "message queues [...] are software-engineering components used for inter-process communication (IPC), or for inter-thread communication within the same process".
106 | In the context of the JobQueue packages we refer to "Message Queue" as a FIFO buffer that distributes messages to one or more consumers, so that every message is only processed once. 107 |
108 |
Job
109 |
110 | A unit of work to be executed (asynchronously).
111 | In the JobQueue packages we use the Message Queue to store serialized jobs, so it acts as a "Job stream". 112 |
113 |
Job Manager
114 |
115 | Central authority allowing adding and fetching jobs to/from the Message Queue. 116 |
117 |
Worker
118 |
119 | The worker watches a queue and triggers the job execution.
120 | This package comes with a `job:work` command that does this (see below) 121 |
122 |
submit
123 |
124 | New messages are *submitted* to a queue to be processed by a worker 125 |
126 |
reserve
127 |
128 | Before a message can be processed it has to be *reserved*.
129 | The queue guarantees that a single message can never be reserved by two workers (unless it has been released again) 130 |
131 |
release
132 |
133 | A reserved message can be *released* to the queue to be processed at a later time.
134 | The *JobManager* does this if Job execution failed and the `maximumNumberOfReleases` setting for the queue is greater than zero 135 |
136 |
abort
137 |
138 | If a message could not be processed successfully it is *aborted* marking it *failed* in the respective queue so that it can't be reserved again.
139 | The *JobManager* aborts a message if Job execution failed and the message can't be released (again) 140 |
141 |
finish
142 |
143 | If a message was processed successfully it is marked *finished*.
144 | The *JobManager* finishes a message if Job execution succeeded. 145 |
146 |
147 | 148 | ## Message Queue 149 | 150 | The `Flowpack.JobQueue.Common` package comes with a *very basic* Message Queue implementation `Flowpack\JobQueue\Common\Queue\FakeQueue` that allows for execution of Jobs using sub requests. 151 | It doesn't need any 3rd party tools or server loops and works for basic scenarios. But it has a couple of limitations to be aware of: 152 | 153 | 1. It is not actually a queue, but dispatches jobs immediately as they are queued. So it's not possible to distribute the work to multiple workers 154 | 155 | 2. The `JobManager` is not involved in processing of jobs so the jobs need to take care of error handling themselves. 156 | 157 | 3. For the same reason [Signals](#signal--slots) are *not* emitted for the `FakeQueue`. 158 | 159 | 4. With Flow 3.3+ The `FakeQueue` supports a flag `async`. Without that flag set, executing jobs *block* the main thread! 160 | 161 | For advanced usage it is recommended to use one of the implementing packages like one of the following: 162 | * [Flowpack.JobQueue.Doctrine](https://github.com/Flowpack/jobqueue-doctrine) 163 | * [Flowpack.JobQueue.Beanstalkd](https://github.com/Flowpack/jobqueue-beanstalkd) 164 | * [Flowpack.JobQueue.Redis](https://github.com/Flowpack/jobqueue-redis) 165 | 166 | ### Configuration 167 | 168 | This is the simplest configuration for a queue: 169 | 170 | ```yaml 171 | Flowpack: 172 | JobQueue: 173 | Common: 174 | queues: 175 | 'test': 176 | className: 'Flowpack\JobQueue\Common\Queue\FakeQueue' 177 | ``` 178 | 179 | With this a queue named `test` will be available. 180 | 181 | *Note:* For reusable packages you should consider adding a vendor specific prefixes to avoid collisions. We recommend to use a classname or the package name with the function name (e.g. Flowpack.ElasticSearch.ContentRepositoryQueueIndexer. 182 | 183 | ### Queue parameters 184 | 185 | The following parameters are supported by all queues: 186 | 187 | | Parameter | Type | Default | Description | 188 | | ----------------------- |---------|--------:|---------------------------------------------------------------------------------------------------------------------------------| 189 | | className | string | - | FQN of the class implementing the queue | 190 | | maximumNumberOfReleases | integer | 3 | Max. number of times a message is re-
released to the queue if a job failed | 191 | | executeIsolated | boolean | FALSE | If TRUE jobs for this queue are executed in a separate Thread. This makes sense in order to avoid memory leaks and side-effects | 192 | | outputResults | boolean | FALSE | If TRUE the full output (stdout + stderr) of the respective job is forwarded to the stdout of its "parent" (only applicable if `executeIsolated` is `true`) | 193 | | queueNamePrefix | string | - | Optional prefix for the internal queue name,
allowing to re-use the same backend over multiple installations | 194 | | options | array | - | Options for the queue.
Implementation specific (see corresponding package) | 195 | | releaseOptions | array | - | Options that will be passed to `release()` when a job failed.
Implementation specific (see corresponding package) | 196 | 197 | A more complex example could look something like: 198 | 199 | ```yaml 200 | Flowpack: 201 | JobQueue: 202 | Common: 203 | queues: 204 | 'email': 205 | className: 'Flowpack\JobQueue\Beanstalkd\Queue\BeanstalkdQueue' 206 | maximumNumberOfReleases: 5 207 | executeIsolated: true 208 | outputResults: true 209 | queueNamePrefix: 'staging-' 210 | options: 211 | client: 212 | host: 127.0.0.11 213 | port: 11301 214 | defaultTimeout: 50 215 | releaseOptions: 216 | priority: 512 217 | delay: 120 218 | 'log': 219 | className: 'Flowpack\JobQueue\Redis\Queue\RedisQueue' 220 | options: 221 | defaultTimeout: 10 222 | ``` 223 | 224 | As you can see, you can have multiple queues in one installations. That allows you to use different backends/options for queues depending on the requirements. 225 | 226 | ### Presets 227 | 228 | If multiple queries share common configuration **presets** can be used to ease readability and maintainability: 229 | 230 | ```yaml 231 | Flowpack: 232 | JobQueue: 233 | Common: 234 | presets: 235 | 'staging-default': 236 | className: 'Flowpack\JobQueue\Doctrine\Queue\DoctrineQueue' 237 | queueNamePrefix: 'staging-' 238 | options: 239 | pollInterval: 2 240 | queues: 241 | 'email': 242 | preset: 'staging-default' 243 | options: 244 | tableName: 'queue_email' # default table name would be "flowpack_jobqueue_messages_email" 245 | 'log': 246 | preset: 'staging-default' 247 | options: 248 | pollInterval: 1 # overrides "pollInterval" of the preset 249 | ``` 250 | 251 | This will configure two `DoctrineQueue`s "email" and "log" with some common options but different table names and poll intervals. 252 | 253 | ## Job Queue 254 | 255 | 256 | The job is an arbitrary class implementing `Flowpack\JobQueue\Common\Job\JobInterface`. 257 | This package comes with one implementation `StaticMethodCallJob` that allows for invoking a public method (see [Quickstart](#quickstart-tldr)) 258 | but often it makes sense to create a custom Job: 259 | 260 | ```php 261 | emailAddress = $emailAddress; 273 | } 274 | 275 | 276 | public function execute(QueueInterface $queue, Message $message) 277 | { 278 | // TODO: send the email to $this->emailAddress 279 | return true; 280 | } 281 | 282 | public function getIdentifier() 283 | { 284 | return 'SendEmailJob'; 285 | } 286 | 287 | public function getLabel() 288 | { 289 | return sprintf('SendEmailJob (email: "%S")', $this->emailAddress); 290 | } 291 | } 292 | ``` 293 | 294 | *Note:* It's crucial that the `execute()` method returns TRUE on success, otherwise the corresponding message will be released again and/or marked *failed*. 295 | 296 | 297 | With that in place, the new job can be added to a queue like this: 298 | 299 | 300 | ```php 301 | use Flowpack\JobQueue\Common\Job\JobInterface; 302 | use Flowpack\JobQueue\Common\Job\JobManager; 303 | use Neos\Flow\Annotations as Flow; 304 | 305 | class SomeClass { 306 | 307 | /** 308 | * @Flow\Inject 309 | * @var JobManager 310 | */ 311 | protected $jobManager; 312 | 313 | /** 314 | * @return void 315 | */ 316 | public function queueJob() 317 | { 318 | $job = new SendEmailJob('some@email.com'); 319 | $this->jobManager->queue('queue-name', $job); 320 | } 321 | } 322 | ``` 323 | 324 | ## Command Line Interface 325 | 326 | Use the `flowpack.jobqueue.common:queue:*` and `flowpack.jobqueue.common:job:*` commands to interact with the job queues: 327 | 328 | | Command | Description | 329 | | --------------- |----------------------------------------------------------------------------| 330 | | queue:list | List configured queues | 331 | | queue:describe | Shows details for a given queue (settings, ..) | 332 | | queue:setup | Initialize a queue (i.e. create required db tables, check connection, ...) | 333 | | queue:flush | Remove all messages from a queue (requires --force flag) | 334 | | queue:submit | Submit a message to a queue (mainly for testing) | 335 | | job:work | Work on a queue and execute jobs | 336 | | job:list | List queued jobs | 337 | 338 | ## Signal & Slots 339 | 340 | When working with JobQueues proper monitoring is crucial as failures might not be visible immediately. 341 | The `JobManager` emits signals for all relevant events, namely: 342 | 343 | * messageSubmitted 344 | * messageTimeout 345 | * messageReserved 346 | * messageFinished 347 | * messageReleased 348 | * messageFailed 349 | 350 | Those can be used to implement some more sophisticated logging for example: 351 | 352 | ```php 353 | getSignalSlotDispatcher(); 373 | 374 | $dispatcher->connect( 375 | JobManager::class, 'messageFailed', 376 | function(QueueInterface $queue, Message $message, ?\Exception $jobExecutionException = null) use ($bootstrap) { 377 | $additionalData = [ 378 | 'queue' => $queue->getName(), 379 | 'message' => $message->getIdentifier() 380 | ]; 381 | if ($jobExecutionException !== null) { 382 | $additionalData['exception'] = $jobExecutionException->getMessage(); 383 | } 384 | $bootstrap->getObjectManager()->get(SystemLoggerInterface::class)->log('Job failed', LOG_ERR, $additionalData); 385 | } 386 | ); 387 | } 388 | } 389 | ``` 390 | 391 | This would log every failed message to the system log. 392 | 393 | ## License 394 | 395 | This package is licensed under the MIT license 396 | 397 | ## Contributions 398 | 399 | Pull-Requests are more than welcome. Make sure to read the [Code Of Conduct](CodeOfConduct.rst). 400 | 401 | --- 402 | 403 | [1] The `FakeQueue` actually executes Jobs *synchronously* unless the `async` flag is set (requires Flow 3.3+) 404 | -------------------------------------------------------------------------------- /Resources/Private/Schema/Settings/Flowpack.JobQueue.Common.schema.yaml: -------------------------------------------------------------------------------- 1 | type: dictionary 2 | properties: 3 | 'presets': 4 | type: dictionary 5 | required: TRUE 6 | additionalProperties: 7 | type: dictionary 8 | required: TRUE 9 | additionalProperties: FALSE 10 | properties: 11 | 'maximumNumberOfReleases': { type: integer } 12 | 'className': { type: string, format: class-name } 13 | 'queueNamePrefix': { type: string } 14 | 'executeIsolated': { type: boolean } 15 | 'outputResults': { type: boolean } 16 | 'options': { type: dictionary } 17 | 'releaseOptions': { type: dictionary } 18 | 'queues': 19 | type: dictionary 20 | required: TRUE 21 | additionalProperties: 22 | type: dictionary 23 | required: TRUE 24 | additionalProperties: FALSE 25 | properties: 26 | 'preset': { type: string } 27 | 'maximumNumberOfReleases': { type: integer } 28 | 'className': { type: string, format: class-name } 29 | 'queueNamePrefix': { type: string } 30 | 'executeIsolated': { type: boolean } 31 | 'outputResults': { type: boolean } 32 | 'options': { type: dictionary } 33 | 'releaseOptions': { type: dictionary } 34 | -------------------------------------------------------------------------------- /Tests/Functional/AbstractQueueTest.php: -------------------------------------------------------------------------------- 1 | objectManager->get(ConfigurationManager::class); 40 | $packageKey = $this->objectManager->getPackageKeyByObjectName(TypeHandling::getTypeForValue($this)); 41 | $packageSettings = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, $packageKey); 42 | if (!isset($packageSettings['testing']['enabled']) || $packageSettings['testing']['enabled'] !== true) { 43 | $this->markTestSkipped(sprintf('Queue is not configured (%s.testing.enabled != TRUE)', $packageKey)); 44 | } 45 | $this->queueSettings = $packageSettings['testing']; 46 | $this->queue = $this->getQueue(); 47 | $this->queue->setUp(); 48 | $this->queue->flush(); 49 | } 50 | 51 | public function tearDown(): void 52 | { 53 | parent::tearDown(); 54 | $this->queue->flush(); 55 | } 56 | 57 | /** 58 | * @return QueueInterface 59 | */ 60 | abstract protected function getQueue(); 61 | 62 | /** 63 | * @test 64 | */ 65 | public function submitReturnsMessageId() 66 | { 67 | $messageId = $this->queue->submit('some message payload'); 68 | self::assertIsString($messageId); 69 | } 70 | 71 | /** 72 | * @test 73 | */ 74 | public function submitAndWaitWithMessageWorks() 75 | { 76 | $payload = 'Yeah, tell someone it works!'; 77 | $this->queue->submit($payload); 78 | 79 | $message = $this->queue->waitAndTake(1); 80 | self::assertInstanceOf(Message::class, $message, 'waitAndTake should return message'); 81 | self::assertEquals($payload, $message->getPayload(), 'message should have payload as before'); 82 | } 83 | 84 | /** 85 | * @test 86 | */ 87 | public function submitWithDelaySchedulesMessage() 88 | { 89 | $messageId = $this->queue->submit('some message payload', ['delay' => 2]); 90 | self::assertNull($this->queue->waitAndTake(1), 'message was available too soon'); 91 | $message = $this->queue->waitAndTake(2); 92 | self::assertInstanceOf(Message::class, $message, 'waitAndTake should return message'); 93 | } 94 | 95 | /** 96 | * @test 97 | */ 98 | public function waitForMessageTimesOut() 99 | { 100 | self::assertNull($this->queue->waitAndTake(1), 'wait should return NULL after timeout'); 101 | } 102 | 103 | /** 104 | * @test 105 | */ 106 | public function peekReturnsNextMessagesIfQueueHasMessages() 107 | { 108 | $this->queue->submit('First message'); 109 | $this->queue->submit('Another message'); 110 | 111 | $messages = $this->queue->peek(1); 112 | self::assertCount(1, $messages, 'peek should return a message'); 113 | /** @var Message $firstMessage */ 114 | $firstMessage = array_shift($messages); 115 | self::assertEquals('First message', $firstMessage->getPayload()); 116 | 117 | $messages = $this->queue->peek(1); 118 | self::assertCount(1, $messages, 'peek should return a message again'); 119 | /** @var Message $firstMessage */ 120 | $firstMessage = array_shift($messages); 121 | self::assertEquals('First message', $firstMessage->getPayload(), 'second peek should return the same message again'); 122 | } 123 | 124 | /** 125 | * @test 126 | */ 127 | public function peekReturnsEmptyArrayIfQueueHasNoMessage() 128 | { 129 | self::assertEquals([], $this->queue->peek(), 'peek should not return a message'); 130 | } 131 | 132 | /** 133 | * @test 134 | */ 135 | public function waitAndReserveWithFinishRemovesMessage() 136 | { 137 | $payload = 'A message'; 138 | $messageId = $this->queue->submit($payload); 139 | 140 | $message = $this->queue->waitAndReserve(1); 141 | self::assertNotNull($message, 'waitAndReserve should receive message'); 142 | self::assertSame($payload, $message->getPayload(), 'message should have payload as before'); 143 | 144 | $message = $this->queue->peek(); 145 | self::assertEquals([], $message, 'no message should be present in queue'); 146 | 147 | self::assertTrue($this->queue->finish($messageId)); 148 | } 149 | 150 | /** 151 | * @test 152 | */ 153 | public function releasePutsMessageBackToQueue() 154 | { 155 | $messageId = $this->queue->submit('A message'); 156 | 157 | $this->queue->waitAndReserve(1); 158 | self::assertSame(0, $this->queue->countReady()); 159 | 160 | $this->queue->release($messageId); 161 | self::assertSame(1, $this->queue->countReady()); 162 | } 163 | 164 | /** 165 | * @test 166 | */ 167 | public function releaseIncreasesNumberOfReleases() 168 | { 169 | $messageId = $this->queue->submit('A message'); 170 | 171 | $message = $this->queue->waitAndReserve(1); 172 | self::assertSame(0, $message->getNumberOfReleases()); 173 | 174 | $this->queue->release($messageId); 175 | $message = $this->queue->waitAndReserve(1); 176 | self::assertSame(1, $message->getNumberOfReleases()); 177 | 178 | $this->queue->release($messageId); 179 | $message = $this->queue->waitAndReserve(1); 180 | self::assertSame(2, $message->getNumberOfReleases()); 181 | 182 | $this->queue->abort($messageId); 183 | } 184 | 185 | /** 186 | * @test 187 | */ 188 | public function abortRemovesMessageFromActiveQueue() 189 | { 190 | $messageId = $this->queue->submit('A message'); 191 | 192 | $this->queue->waitAndReserve(1); 193 | 194 | $this->queue->abort($messageId); 195 | self::assertSame(0, $this->queue->countReady()); 196 | self::assertNull($this->queue->waitAndTake(1)); 197 | } 198 | 199 | /** 200 | * @test 201 | */ 202 | public function countReadyReturnsZeroByDefault() 203 | { 204 | self::assertSame(0, $this->queue->countReady()); 205 | } 206 | 207 | /** 208 | * @test 209 | */ 210 | public function countReadyReturnsNumberOfReadyJobs() 211 | { 212 | $this->queue->submit('First message'); 213 | $this->queue->submit('Second message'); 214 | 215 | self::assertSame(2, $this->queue->countReady()); 216 | } 217 | 218 | /** 219 | * @test 220 | */ 221 | public function countFailedReturnsZeroByDefault() 222 | { 223 | self::assertSame(0, $this->queue->countFailed()); 224 | } 225 | 226 | /** 227 | * @test 228 | */ 229 | public function countFailedReturnsNumberOfFailedMessages() 230 | { 231 | $messageId = $this->queue->submit('A message'); 232 | 233 | $this->queue->waitAndReserve(1); 234 | self::assertSame(0, $this->queue->countFailed()); 235 | 236 | $this->queue->abort($messageId); 237 | self::assertSame(1, $this->queue->countFailed()); 238 | } 239 | 240 | /** 241 | * @test 242 | */ 243 | public function countReservedReturnsZeroByDefault() 244 | { 245 | self::assertSame(0, $this->queue->countReserved()); 246 | } 247 | 248 | /** 249 | * @test 250 | */ 251 | public function countReservedReturnsNumberOfReservedMessages() 252 | { 253 | $messageId = $this->queue->submit('A message'); 254 | 255 | $this->queue->waitAndReserve(1); 256 | self::assertSame(1, $this->queue->countReserved()); 257 | 258 | $this->queue->abort($messageId); 259 | self::assertSame(0, $this->queue->countReserved()); 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /Tests/Functional/Job/JobManagerTest.php: -------------------------------------------------------------------------------- 1 | mockQueueManager = $this->getMockBuilder(QueueManager::class)->disableOriginalConstructor()->getMock(); 60 | $this->testQueue = new TestQueue('TestQueue'); 61 | $this->mockQueueManager->expects($this->any())->method('getQueue')->with('TestQueue')->will($this->returnValue($this->testQueue)); 62 | $this->mockQueueManager->expects($this->any())->method('getQueueSettings')->with('TestQueue')->will($this->returnCallback(function() { return $this->queueSettings; })); 63 | 64 | $this->jobManager = new JobManager(); 65 | $this->inject($this->jobManager, 'queueManager', $this->mockQueueManager); 66 | 67 | self::$bootstrap->getSignalSlotDispatcher()->connect(JobManager::class, 'messageSubmitted', $this, 'logSignal'); 68 | self::$bootstrap->getSignalSlotDispatcher()->connect(JobManager::class, 'messageTimeout', $this, 'logSignal'); 69 | self::$bootstrap->getSignalSlotDispatcher()->connect(JobManager::class, 'messageReserved', $this, 'logSignal'); 70 | self::$bootstrap->getSignalSlotDispatcher()->connect(JobManager::class, 'messageFinished', $this, 'logSignal'); 71 | self::$bootstrap->getSignalSlotDispatcher()->connect(JobManager::class, 'messageReleased', $this, 'logSignal'); 72 | self::$bootstrap->getSignalSlotDispatcher()->connect(JobManager::class, 'messageFailed', $this, 'logSignal'); 73 | } 74 | 75 | public function tearDown(): void 76 | { 77 | parent::tearDown(); 78 | $this->emittedSignals = []; 79 | } 80 | 81 | /** 82 | * Slot for the JobManager signals (see setUp()) 83 | * 84 | * @return void 85 | */ 86 | public function logSignal() 87 | { 88 | $arguments = func_get_args(); 89 | $signalName = array_pop($arguments); 90 | if (!isset($this->emittedSignals[$signalName])) { 91 | $this->emittedSignals[$signalName] = []; 92 | } 93 | $this->emittedSignals[$signalName][] = $arguments; 94 | } 95 | 96 | /** 97 | * @param string $signalName 98 | * @param array $arguments 99 | */ 100 | protected function assertSignalEmitted($signalName, array $arguments = []) 101 | { 102 | $fullSignalName = JobManager::class . '::' . $signalName; 103 | if (!isset($this->emittedSignals[$fullSignalName])) { 104 | $this->fail('Signal "' . $signalName . '" has not been emitted!'); 105 | } 106 | self::assertCount(1, $this->emittedSignals[$fullSignalName]); 107 | foreach ($arguments as $argumentIndex => $expectedArgument) { 108 | $actualArgument = $this->emittedSignals[$fullSignalName][0][$argumentIndex]; 109 | if ($expectedArgument instanceof Constraint) { 110 | $expectedArgument->evaluate($actualArgument); 111 | } else { 112 | self::assertSame($expectedArgument, $actualArgument); 113 | } 114 | } 115 | } 116 | 117 | /** 118 | * @test 119 | */ 120 | public function queueEmitsMessageSubmittedSignal() 121 | { 122 | $options = ['foo' => 'bar']; 123 | $this->jobManager->queue('TestQueue', new TestJob(), $options); 124 | $this->assertSignalEmitted('messageSubmitted', [0 => $this->testQueue, 3 => $options]); 125 | } 126 | 127 | /** 128 | * @test 129 | */ 130 | public function waitAndExecuteEmitsMessageTimeoutSignal() 131 | { 132 | $this->jobManager->queue('TestQueue', new TestJob()); 133 | $this->jobManager->waitAndExecute('TestQueue', 0); 134 | $this->assertSignalEmitted('messageTimeout', [0 => $this->testQueue]); 135 | } 136 | 137 | /** 138 | * @test 139 | */ 140 | public function waitAndExecuteEmitsMessageReservedSignal() 141 | { 142 | $this->jobManager->queue('TestQueue', new TestJob()); 143 | $this->jobManager->waitAndExecute('TestQueue'); 144 | $this->assertSignalEmitted('messageReserved', [0 => $this->testQueue, 1 => new IsInstanceOf(Message::class)]); 145 | } 146 | 147 | /** 148 | * @test 149 | */ 150 | public function waitAndExecuteEmitsMessageFinishedSignal() 151 | { 152 | $this->jobManager->queue('TestQueue', new TestJob()); 153 | $this->jobManager->waitAndExecute('TestQueue'); 154 | $this->assertSignalEmitted('messageFinished', [0 => $this->testQueue, 1 => new IsInstanceOf(Message::class)]); 155 | } 156 | 157 | /** 158 | * @test 159 | */ 160 | public function waitAndExecuteEmitsMessageReleasedSignal() 161 | { 162 | $releaseOptions = ['some' => 'releaseOption']; 163 | $this->queueSettings = ['maximumNumberOfReleases' => 1, 'releaseOptions' => $releaseOptions]; 164 | $this->jobManager->queue('TestQueue', new TestJob(2)); 165 | try { 166 | $this->jobManager->waitAndExecute('TestQueue'); 167 | } catch (JobQueueException $exception) { 168 | } 169 | $this->assertSignalEmitted('messageReleased', [$this->testQueue, new IsInstanceOf(Message::class), $releaseOptions, new IsInstanceOf(JobQueueException::class)]); 170 | } 171 | 172 | /** 173 | * @test 174 | */ 175 | public function waitAndExecuteEmitsMessageFailedSignal() 176 | { 177 | $this->jobManager->queue('TestQueue', new TestJob(JobManager::DEFAULT_MAXIMUM_NUMBER_RELEASES + 1)); 178 | for ($i = 0; $i <= JobManager::DEFAULT_MAXIMUM_NUMBER_RELEASES; $i ++) { 179 | try { 180 | $this->jobManager->waitAndExecute('TestQueue'); 181 | } catch (JobQueueException $exception) { 182 | } 183 | } 184 | $this->assertSignalEmitted('messageFailed', [$this->testQueue, new IsInstanceOf(Message::class)]); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /Tests/Unit/Fixtures/TestJob.php: -------------------------------------------------------------------------------- 1 | failNumberOfTimes = $failNumberOfTimes; 34 | } 35 | 36 | /** 37 | * Do nothing 38 | * 39 | * @param QueueInterface $queue 40 | * @param Message $message 41 | * @return bool 42 | */ 43 | public function execute(QueueInterface $queue, Message $message): bool 44 | { 45 | if ($this->failNumberOfTimes > $message->getNumberOfReleases()) { 46 | return false; 47 | } 48 | return true; 49 | } 50 | 51 | /** 52 | * Get a readable label for the job 53 | * 54 | * @return string A label for the job 55 | */ 56 | public function getLabel(): string 57 | { 58 | return 'Test Job'; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/Unit/Fixtures/TestQueue.php: -------------------------------------------------------------------------------- 1 | name = $name; 82 | if (isset($options['defaultTimeout'])) { 83 | $this->defaultTimeout = (integer)$options['defaultTimeout']; 84 | } 85 | $this->options = $options; 86 | } 87 | 88 | /** 89 | * @inheritdoc 90 | */ 91 | public function setUp(): void 92 | { 93 | // The TestQueue does not require any setup 94 | } 95 | 96 | /** 97 | * @inheritdoc 98 | */ 99 | public function getName(): string 100 | { 101 | return $this->name; 102 | } 103 | 104 | /** 105 | * @inheritdoc 106 | */ 107 | public function submit($payload, array $options = []): string 108 | { 109 | $this->lastSubmitOptions = $options; 110 | $messageId = Algorithms::generateUUID(); 111 | $this->readyMessages[$messageId] = $payload; 112 | return $messageId; 113 | } 114 | 115 | /** 116 | * @inheritdoc 117 | */ 118 | public function waitAndTake(?int $timeout = null): ?Message 119 | { 120 | $message = $this->reserveMessage($timeout); 121 | if ($message === null) { 122 | return null; 123 | } 124 | unset($this->processingMessages[$message->getIdentifier()]); 125 | 126 | return $message; 127 | } 128 | 129 | /** 130 | * @inheritdoc 131 | */ 132 | public function waitAndReserve(?int $timeout = null): ?Message 133 | { 134 | return $this->reserveMessage($timeout); 135 | } 136 | 137 | /** 138 | * @param int|null $timeout 139 | * @return Message 140 | */ 141 | protected function reserveMessage(?int $timeout = null): ?Message 142 | { 143 | if ($timeout === null) { 144 | $timeout = $this->defaultTimeout; 145 | } 146 | $startTime = time(); 147 | 148 | do { 149 | $nextMessageIdAndPayload = array_slice($this->readyMessages, 0, 1); 150 | if (time() - $startTime >= $timeout) { 151 | return null; 152 | } 153 | } while ($nextMessageIdAndPayload === []); 154 | 155 | $messageId = key($nextMessageIdAndPayload); 156 | $payload = $nextMessageIdAndPayload[$messageId]; 157 | unset($this->readyMessages[$messageId]); 158 | $this->processingMessages[$messageId] = $nextMessageIdAndPayload[$messageId]; 159 | 160 | $numberOfReleases = isset($this->numberOfReleases[$messageId]) ? $this->numberOfReleases[$messageId] : 0; 161 | return new Message($messageId, $payload, $numberOfReleases); 162 | } 163 | 164 | /** 165 | * @inheritdoc 166 | */ 167 | public function release(string $messageId, array $options = []): void 168 | { 169 | $this->lastReleaseOptions = $options; 170 | if (!isset($this->processingMessages[$messageId])) { 171 | return; 172 | } 173 | $payload = $this->processingMessages[$messageId]; 174 | $this->numberOfReleases[$messageId] = isset($this->numberOfReleases[$messageId]) ? $this->numberOfReleases[$messageId] + 1 : 1; 175 | unset($this->processingMessages[$messageId]); 176 | $this->readyMessages[$messageId] = $payload; 177 | } 178 | 179 | /** 180 | * @inheritdoc 181 | */ 182 | public function abort(string $messageId): void 183 | { 184 | if (!isset($this->readyMessages[$messageId])) { 185 | return; 186 | } 187 | $this->failedMessages[$messageId] = $this->readyMessages[$messageId]; 188 | unset($this->readyMessages[$messageId]); 189 | } 190 | 191 | /** 192 | * @inheritdoc 193 | */ 194 | public function finish(string $messageId): bool 195 | { 196 | unset($this->processingMessages[$messageId]); 197 | return true; 198 | } 199 | 200 | /** 201 | * @inheritdoc 202 | */ 203 | public function peek(int $limit = 1): array 204 | { 205 | $messageIdsAndPayload = array_slice($this->readyMessages, 0, $limit); 206 | $messages = []; 207 | foreach ($messageIdsAndPayload as $messageId => $payload) { 208 | $messages[] = new Message($messageId, $payload); 209 | } 210 | return $messages; 211 | } 212 | 213 | /** 214 | * @inheritdoc 215 | */ 216 | public function countReady(): int 217 | { 218 | return count($this->readyMessages); 219 | } 220 | 221 | /** 222 | * @inheritdoc 223 | */ 224 | public function countReserved(): int 225 | { 226 | return count($this->reservedMessages); 227 | } 228 | 229 | /** 230 | * @inheritdoc 231 | */ 232 | public function countFailed(): int 233 | { 234 | return count($this->failedMessages); 235 | } 236 | 237 | /** 238 | * @inheritdoc 239 | */ 240 | public function flush(): void 241 | { 242 | $this->readyMessages = $this->processingMessages = $this->failedMessages = $this->numberOfReleases = []; 243 | } 244 | 245 | /** 246 | * @return array 247 | */ 248 | public function getOptions(): array 249 | { 250 | return $this->options; 251 | } 252 | 253 | /** 254 | * @return array 255 | */ 256 | public function getLastSubmitOptions(): array 257 | { 258 | return $this->lastSubmitOptions; 259 | } 260 | 261 | /** 262 | * @return array 263 | */ 264 | public function getLastReleaseOptions(): array 265 | { 266 | return $this->lastReleaseOptions; 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /Tests/Unit/Job/JobManagerTest.php: -------------------------------------------------------------------------------- 1 | mockQueueManager = $this->getMockBuilder(QueueManager::class)->disableOriginalConstructor()->getMock(); 46 | $this->testQueue = new TestQueue('TestQueue'); 47 | $this->mockQueueManager->expects($this->any())->method('getQueue')->with('TestQueue')->will(self::returnValue($this->testQueue)); 48 | 49 | $this->jobManager = new JobManager(); 50 | $this->inject($this->jobManager, 'queueManager', $this->mockQueueManager); 51 | } 52 | 53 | /** 54 | * @test 55 | */ 56 | public function queueSubmitsMessageToQueue() 57 | { 58 | $job = new TestJob(); 59 | $this->jobManager->queue('TestQueue', $job); 60 | 61 | $messageId = $this->testQueue->peek(); 62 | self::assertNotNull($messageId); 63 | } 64 | 65 | /** 66 | * @test 67 | */ 68 | public function queuePassesOptionsToQueue() 69 | { 70 | $mockOptions = ['foo' => 'Bar', 'baz' => 'Foos']; 71 | $job = new TestJob(); 72 | $this->jobManager->queue('TestQueue', $job, $mockOptions); 73 | 74 | self::assertSame($mockOptions, $this->testQueue->getLastSubmitOptions()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Tests/Unit/Queue/QueueManagerTest.php: -------------------------------------------------------------------------------- 1 | queueManager = new QueueManager(); 31 | $this->inject($this->queueManager, 'settings', [ 32 | 'queues' => [ 33 | 'TestQueue' => [ 34 | 'className' => TestQueue::class 35 | ] 36 | ] 37 | ]); 38 | } 39 | 40 | /** 41 | * @test 42 | */ 43 | public function getQueueSettingsMergesPresetWithQueueSettings() 44 | { 45 | $this->inject($this->queueManager, 'settings', [ 46 | 'presets' => [ 47 | 'somePreset' => [ 48 | 'className' => 'Some\Preset\ClassName', 49 | 'maximumNumberOfReleases' => 123, 50 | 'queueNamePrefix' => 'presetPrefix', 51 | 'options' => [ 52 | 'option1' => 'from preset', 53 | 'option2' => 'from preset', 54 | ], 55 | 'releaseOptions' => [ 56 | 'bar' => 'from preset', 57 | ] 58 | ] 59 | ], 60 | 'queues' => [ 61 | 'TestQueue' => [ 62 | 'preset' => 'somePreset', 63 | 'className' => TestQueue::class, 64 | 'maximumNumberOfReleases' => 321, 65 | 'queueNamePrefix' => 'queuePrefix', 66 | 'options' => [ 67 | 'option2' => 'overridden from queue', 68 | 'option3' => 'from queue', 69 | ], 70 | 'releaseOptions' => [ 71 | 'bar' => 'from queue', 72 | ] 73 | ] 74 | ] 75 | ]); 76 | 77 | $expectedSettings = [ 78 | 'className' => TestQueue::class, 79 | 'maximumNumberOfReleases' => 321, 80 | 'queueNamePrefix' => 'queuePrefix', 81 | 'options' => [ 82 | 'option1' => 'from preset', 83 | 'option2' => 'overridden from queue', 84 | 'option3' => 'from queue', 85 | ], 86 | 'releaseOptions' => [ 87 | 'bar' => 'from queue', 88 | ], 89 | 'preset' => 'somePreset' 90 | ]; 91 | 92 | $queueSettings = $this->queueManager->getQueueSettings('TestQueue'); 93 | self::assertSame($expectedSettings, $queueSettings); 94 | } 95 | 96 | /** 97 | * @test 98 | */ 99 | public function getQueueCreatesInstanceByQueueName() 100 | { 101 | /** @var TestQueue $queue */ 102 | $queue = $this->queueManager->getQueue('TestQueue'); 103 | self::assertInstanceOf(TestQueue::class, $queue); 104 | self::assertSame('TestQueue', $queue->getName()); 105 | } 106 | 107 | /** 108 | * @test 109 | */ 110 | public function getQueueSetsOptionsOnInstance() 111 | { 112 | $this->inject($this->queueManager, 'settings', [ 113 | 'queues' => [ 114 | 'TestQueue' => [ 115 | 'className' => TestQueue::class, 116 | 'options' => [ 117 | 'foo' => 'bar' 118 | ] 119 | ] 120 | ] 121 | ]); 122 | 123 | /** @var TestQueue $queue */ 124 | $queue = $this->queueManager->getQueue('TestQueue'); 125 | self::assertEquals(['foo' => 'bar'], $queue->getOptions()); 126 | } 127 | 128 | /** 129 | * @test 130 | */ 131 | public function getQueueReusesInstances() 132 | { 133 | $queue = $this->queueManager->getQueue('TestQueue'); 134 | self::assertSame($queue, $this->queueManager->getQueue('TestQueue')); 135 | } 136 | 137 | /** 138 | * @test 139 | */ 140 | public function getQueueThrowsExceptionWhenSettingsReferToNonExistingPreset() 141 | { 142 | self::expectException(\Flowpack\JobQueue\Common\Exception::class); 143 | $this->inject($this->queueManager, 'settings', [ 144 | 'queues' => [ 145 | 'TestQueue' => [ 146 | 'className' => TestQueue::class, 147 | 'preset' => 'NonExistingPreset' 148 | ] 149 | ] 150 | ]); 151 | $this->queueManager->getQueue('TestQueue'); 152 | } 153 | 154 | 155 | /** 156 | * @test 157 | */ 158 | public function queueNamesArePrefixedWithDefaultQueueNamePrefix() 159 | { 160 | $this->inject($this->queueManager, 'settings', [ 161 | 'queues' => [ 162 | 'TestQueue' => [ 163 | 'className' => TestQueue::class, 164 | 'queueNamePrefix' => 'specialQueue', 165 | ] 166 | ] 167 | ]); 168 | 169 | /** @var TestQueue $queue */ 170 | $queue = $this->queueManager->getQueue('TestQueue'); 171 | self::assertSame('specialQueueTestQueue', $queue->getName()); 172 | } 173 | 174 | /** 175 | * @test 176 | */ 177 | public function queueNamePrefixFromPresetCanBeOverruled() 178 | { 179 | $this->inject($this->queueManager, 'settings', [ 180 | 'presets' => [ 181 | 'somePreset' => [ 182 | 'queueNamePrefix' => 'presetPrefix', 183 | ] 184 | ], 185 | 'queues' => [ 186 | 'TestQueue' => [ 187 | 'preset' => 'somePreset', 188 | 'queueNamePrefix' => 'overriddenPrefix', 189 | 'className' => TestQueue::class, 190 | ] 191 | ] 192 | ]); 193 | 194 | /** @var TestQueue $queue */ 195 | $queue = $this->queueManager->getQueue('TestQueue'); 196 | self::assertSame('overriddenPrefixTestQueue', $queue->getName()); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Tests/Unit/Utility/VariableDumperTest.php: -------------------------------------------------------------------------------- 1 |