Worker.php 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. <?php
  2. namespace Illuminate\Queue;
  3. use Exception;
  4. use Throwable;
  5. use Illuminate\Support\Carbon;
  6. use Illuminate\Contracts\Events\Dispatcher;
  7. use Illuminate\Database\DetectsLostConnections;
  8. use Illuminate\Contracts\Debug\ExceptionHandler;
  9. use Symfony\Component\Debug\Exception\FatalThrowableError;
  10. use Illuminate\Contracts\Cache\Repository as CacheContract;
  11. class Worker
  12. {
  13. use DetectsLostConnections;
  14. /**
  15. * The queue manager instance.
  16. *
  17. * @var \Illuminate\Queue\QueueManager
  18. */
  19. protected $manager;
  20. /**
  21. * The event dispatcher instance.
  22. *
  23. * @var \Illuminate\Contracts\Events\Dispatcher
  24. */
  25. protected $events;
  26. /**
  27. * The cache repository implementation.
  28. *
  29. * @var \Illuminate\Contracts\Cache\Repository
  30. */
  31. protected $cache;
  32. /**
  33. * The exception handler instance.
  34. *
  35. * @var \Illuminate\Contracts\Debug\ExceptionHandler
  36. */
  37. protected $exceptions;
  38. /**
  39. * Indicates if the worker should exit.
  40. *
  41. * @var bool
  42. */
  43. public $shouldQuit = false;
  44. /**
  45. * Indicates if the worker is paused.
  46. *
  47. * @var bool
  48. */
  49. public $paused = false;
  50. /**
  51. * Create a new queue worker.
  52. *
  53. * @param \Illuminate\Queue\QueueManager $manager
  54. * @param \Illuminate\Contracts\Events\Dispatcher $events
  55. * @param \Illuminate\Contracts\Debug\ExceptionHandler $exceptions
  56. * @return void
  57. */
  58. public function __construct(QueueManager $manager,
  59. Dispatcher $events,
  60. ExceptionHandler $exceptions)
  61. {
  62. $this->events = $events;
  63. $this->manager = $manager;
  64. $this->exceptions = $exceptions;
  65. }
  66. /**
  67. * Listen to the given queue in a loop.
  68. *
  69. * @param string $connectionName
  70. * @param string $queue
  71. * @param \Illuminate\Queue\WorkerOptions $options
  72. * @return void
  73. */
  74. public function daemon($connectionName, $queue, WorkerOptions $options)
  75. {
  76. if ($this->supportsAsyncSignals()) {
  77. $this->listenForSignals();
  78. }
  79. $lastRestart = $this->getTimestampOfLastQueueRestart();
  80. while (true) {
  81. // Before reserving any jobs, we will make sure this queue is not paused and
  82. // if it is we will just pause this worker for a given amount of time and
  83. // make sure we do not need to kill this worker process off completely.
  84. if (! $this->daemonShouldRun($options, $connectionName, $queue)) {
  85. $this->pauseWorker($options, $lastRestart);
  86. continue;
  87. }
  88. // First, we will attempt to get the next job off of the queue. We will also
  89. // register the timeout handler and reset the alarm for this job so it is
  90. // not stuck in a frozen state forever. Then, we can fire off this job.
  91. $job = $this->getNextJob(
  92. $this->manager->connection($connectionName), $queue
  93. );
  94. if ($this->supportsAsyncSignals()) {
  95. $this->registerTimeoutHandler($job, $options);
  96. }
  97. // If the daemon should run (not in maintenance mode, etc.), then we can run
  98. // fire off this job for processing. Otherwise, we will need to sleep the
  99. // worker so no more jobs are processed until they should be processed.
  100. if ($job) {
  101. $this->runJob($job, $connectionName, $options);
  102. } else {
  103. $this->sleep($options->sleep);
  104. }
  105. // Finally, we will check to see if we have exceeded our memory limits or if
  106. // the queue should restart based on other indications. If so, we'll stop
  107. // this worker and let whatever is "monitoring" it restart the process.
  108. $this->stopIfNecessary($options, $lastRestart);
  109. }
  110. }
  111. /**
  112. * Register the worker timeout handler.
  113. *
  114. * @param \Illuminate\Contracts\Queue\Job|null $job
  115. * @param \Illuminate\Queue\WorkerOptions $options
  116. * @return void
  117. */
  118. protected function registerTimeoutHandler($job, WorkerOptions $options)
  119. {
  120. // We will register a signal handler for the alarm signal so that we can kill this
  121. // process if it is running too long because it has frozen. This uses the async
  122. // signals supported in recent versions of PHP to accomplish it conveniently.
  123. pcntl_signal(SIGALRM, function () {
  124. $this->kill(1);
  125. });
  126. pcntl_alarm(
  127. max($this->timeoutForJob($job, $options), 0)
  128. );
  129. }
  130. /**
  131. * Get the appropriate timeout for the given job.
  132. *
  133. * @param \Illuminate\Contracts\Queue\Job|null $job
  134. * @param \Illuminate\Queue\WorkerOptions $options
  135. * @return int
  136. */
  137. protected function timeoutForJob($job, WorkerOptions $options)
  138. {
  139. return $job && ! is_null($job->timeout()) ? $job->timeout() : $options->timeout;
  140. }
  141. /**
  142. * Determine if the daemon should process on this iteration.
  143. *
  144. * @param \Illuminate\Queue\WorkerOptions $options
  145. * @param string $connectionName
  146. * @param string $queue
  147. * @return bool
  148. */
  149. protected function daemonShouldRun(WorkerOptions $options, $connectionName, $queue)
  150. {
  151. return ! (($this->manager->isDownForMaintenance() && ! $options->force) ||
  152. $this->paused ||
  153. $this->events->until(new Events\Looping($connectionName, $queue)) === false);
  154. }
  155. /**
  156. * Pause the worker for the current loop.
  157. *
  158. * @param \Illuminate\Queue\WorkerOptions $options
  159. * @param int $lastRestart
  160. * @return void
  161. */
  162. protected function pauseWorker(WorkerOptions $options, $lastRestart)
  163. {
  164. $this->sleep($options->sleep > 0 ? $options->sleep : 1);
  165. $this->stopIfNecessary($options, $lastRestart);
  166. }
  167. /**
  168. * Stop the process if necessary.
  169. *
  170. * @param \Illuminate\Queue\WorkerOptions $options
  171. * @param int $lastRestart
  172. */
  173. protected function stopIfNecessary(WorkerOptions $options, $lastRestart)
  174. {
  175. if ($this->shouldQuit) {
  176. $this->kill();
  177. }
  178. if ($this->memoryExceeded($options->memory)) {
  179. $this->stop(12);
  180. } elseif ($this->queueShouldRestart($lastRestart)) {
  181. $this->stop();
  182. }
  183. }
  184. /**
  185. * Process the next job on the queue.
  186. *
  187. * @param string $connectionName
  188. * @param string $queue
  189. * @param \Illuminate\Queue\WorkerOptions $options
  190. * @return void
  191. */
  192. public function runNextJob($connectionName, $queue, WorkerOptions $options)
  193. {
  194. $job = $this->getNextJob(
  195. $this->manager->connection($connectionName), $queue
  196. );
  197. // If we're able to pull a job off of the stack, we will process it and then return
  198. // from this method. If there is no job on the queue, we will "sleep" the worker
  199. // for the specified number of seconds, then keep processing jobs after sleep.
  200. if ($job) {
  201. return $this->runJob($job, $connectionName, $options);
  202. }
  203. $this->sleep($options->sleep);
  204. }
  205. /**
  206. * Get the next job from the queue connection.
  207. *
  208. * @param \Illuminate\Contracts\Queue\Queue $connection
  209. * @param string $queue
  210. * @return \Illuminate\Contracts\Queue\Job|null
  211. */
  212. protected function getNextJob($connection, $queue)
  213. {
  214. try {
  215. foreach (explode(',', $queue) as $queue) {
  216. if (! is_null($job = $connection->pop($queue))) {
  217. return $job;
  218. }
  219. }
  220. } catch (Exception $e) {
  221. $this->exceptions->report($e);
  222. $this->stopWorkerIfLostConnection($e);
  223. } catch (Throwable $e) {
  224. $this->exceptions->report($e = new FatalThrowableError($e));
  225. $this->stopWorkerIfLostConnection($e);
  226. }
  227. }
  228. /**
  229. * Process the given job.
  230. *
  231. * @param \Illuminate\Contracts\Queue\Job $job
  232. * @param string $connectionName
  233. * @param \Illuminate\Queue\WorkerOptions $options
  234. * @return void
  235. */
  236. protected function runJob($job, $connectionName, WorkerOptions $options)
  237. {
  238. try {
  239. return $this->process($connectionName, $job, $options);
  240. } catch (Exception $e) {
  241. $this->exceptions->report($e);
  242. $this->stopWorkerIfLostConnection($e);
  243. } catch (Throwable $e) {
  244. $this->exceptions->report($e = new FatalThrowableError($e));
  245. $this->stopWorkerIfLostConnection($e);
  246. }
  247. }
  248. /**
  249. * Stop the worker if we have lost connection to a database.
  250. *
  251. * @param \Throwable $e
  252. * @return void
  253. */
  254. protected function stopWorkerIfLostConnection($e)
  255. {
  256. if ($this->causedByLostConnection($e)) {
  257. $this->shouldQuit = true;
  258. }
  259. }
  260. /**
  261. * Process the given job from the queue.
  262. *
  263. * @param string $connectionName
  264. * @param \Illuminate\Contracts\Queue\Job $job
  265. * @param \Illuminate\Queue\WorkerOptions $options
  266. * @return void
  267. *
  268. * @throws \Throwable
  269. */
  270. public function process($connectionName, $job, WorkerOptions $options)
  271. {
  272. try {
  273. // First we will raise the before job event and determine if the job has already ran
  274. // over its maximum attempt limits, which could primarily happen when this job is
  275. // continually timing out and not actually throwing any exceptions from itself.
  276. $this->raiseBeforeJobEvent($connectionName, $job);
  277. $this->markJobAsFailedIfAlreadyExceedsMaxAttempts(
  278. $connectionName, $job, (int) $options->maxTries
  279. );
  280. // Here we will fire off the job and let it process. We will catch any exceptions so
  281. // they can be reported to the developers logs, etc. Once the job is finished the
  282. // proper events will be fired to let any listeners know this job has finished.
  283. $job->fire();
  284. $this->raiseAfterJobEvent($connectionName, $job);
  285. } catch (Exception $e) {
  286. $this->handleJobException($connectionName, $job, $options, $e);
  287. } catch (Throwable $e) {
  288. $this->handleJobException(
  289. $connectionName, $job, $options, new FatalThrowableError($e)
  290. );
  291. }
  292. }
  293. /**
  294. * Handle an exception that occurred while the job was running.
  295. *
  296. * @param string $connectionName
  297. * @param \Illuminate\Contracts\Queue\Job $job
  298. * @param \Illuminate\Queue\WorkerOptions $options
  299. * @param \Exception $e
  300. * @return void
  301. *
  302. * @throws \Exception
  303. */
  304. protected function handleJobException($connectionName, $job, WorkerOptions $options, $e)
  305. {
  306. try {
  307. // First, we will go ahead and mark the job as failed if it will exceed the maximum
  308. // attempts it is allowed to run the next time we process it. If so we will just
  309. // go ahead and mark it as failed now so we do not have to release this again.
  310. if (! $job->hasFailed()) {
  311. $this->markJobAsFailedIfWillExceedMaxAttempts(
  312. $connectionName, $job, (int) $options->maxTries, $e
  313. );
  314. }
  315. $this->raiseExceptionOccurredJobEvent(
  316. $connectionName, $job, $e
  317. );
  318. } finally {
  319. // If we catch an exception, we will attempt to release the job back onto the queue
  320. // so it is not lost entirely. This'll let the job be retried at a later time by
  321. // another listener (or this same one). We will re-throw this exception after.
  322. if (! $job->isDeleted() && ! $job->isReleased() && ! $job->hasFailed()) {
  323. $job->release($options->delay);
  324. }
  325. }
  326. throw $e;
  327. }
  328. /**
  329. * Mark the given job as failed if it has exceeded the maximum allowed attempts.
  330. *
  331. * This will likely be because the job previously exceeded a timeout.
  332. *
  333. * @param string $connectionName
  334. * @param \Illuminate\Contracts\Queue\Job $job
  335. * @param int $maxTries
  336. * @return void
  337. */
  338. protected function markJobAsFailedIfAlreadyExceedsMaxAttempts($connectionName, $job, $maxTries)
  339. {
  340. $maxTries = ! is_null($job->maxTries()) ? $job->maxTries() : $maxTries;
  341. $timeoutAt = $job->timeoutAt();
  342. if ($timeoutAt && Carbon::now()->getTimestamp() <= $timeoutAt) {
  343. return;
  344. }
  345. if (! $timeoutAt && ($maxTries === 0 || $job->attempts() <= $maxTries)) {
  346. return;
  347. }
  348. $this->failJob($connectionName, $job, $e = new MaxAttemptsExceededException(
  349. $job->resolveName().' has been attempted too many times or run too long. The job may have previously timed out.'
  350. ));
  351. throw $e;
  352. }
  353. /**
  354. * Mark the given job as failed if it has exceeded the maximum allowed attempts.
  355. *
  356. * @param string $connectionName
  357. * @param \Illuminate\Contracts\Queue\Job $job
  358. * @param int $maxTries
  359. * @param \Exception $e
  360. * @return void
  361. */
  362. protected function markJobAsFailedIfWillExceedMaxAttempts($connectionName, $job, $maxTries, $e)
  363. {
  364. $maxTries = ! is_null($job->maxTries()) ? $job->maxTries() : $maxTries;
  365. if ($job->timeoutAt() && $job->timeoutAt() <= Carbon::now()->getTimestamp()) {
  366. $this->failJob($connectionName, $job, $e);
  367. }
  368. if ($maxTries > 0 && $job->attempts() >= $maxTries) {
  369. $this->failJob($connectionName, $job, $e);
  370. }
  371. }
  372. /**
  373. * Mark the given job as failed and raise the relevant event.
  374. *
  375. * @param string $connectionName
  376. * @param \Illuminate\Contracts\Queue\Job $job
  377. * @param \Exception $e
  378. * @return void
  379. */
  380. protected function failJob($connectionName, $job, $e)
  381. {
  382. return FailingJob::handle($connectionName, $job, $e);
  383. }
  384. /**
  385. * Raise the before queue job event.
  386. *
  387. * @param string $connectionName
  388. * @param \Illuminate\Contracts\Queue\Job $job
  389. * @return void
  390. */
  391. protected function raiseBeforeJobEvent($connectionName, $job)
  392. {
  393. $this->events->dispatch(new Events\JobProcessing(
  394. $connectionName, $job
  395. ));
  396. }
  397. /**
  398. * Raise the after queue job event.
  399. *
  400. * @param string $connectionName
  401. * @param \Illuminate\Contracts\Queue\Job $job
  402. * @return void
  403. */
  404. protected function raiseAfterJobEvent($connectionName, $job)
  405. {
  406. $this->events->dispatch(new Events\JobProcessed(
  407. $connectionName, $job
  408. ));
  409. }
  410. /**
  411. * Raise the exception occurred queue job event.
  412. *
  413. * @param string $connectionName
  414. * @param \Illuminate\Contracts\Queue\Job $job
  415. * @param \Exception $e
  416. * @return void
  417. */
  418. protected function raiseExceptionOccurredJobEvent($connectionName, $job, $e)
  419. {
  420. $this->events->dispatch(new Events\JobExceptionOccurred(
  421. $connectionName, $job, $e
  422. ));
  423. }
  424. /**
  425. * Determine if the queue worker should restart.
  426. *
  427. * @param int|null $lastRestart
  428. * @return bool
  429. */
  430. protected function queueShouldRestart($lastRestart)
  431. {
  432. return $this->getTimestampOfLastQueueRestart() != $lastRestart;
  433. }
  434. /**
  435. * Get the last queue restart timestamp, or null.
  436. *
  437. * @return int|null
  438. */
  439. protected function getTimestampOfLastQueueRestart()
  440. {
  441. if ($this->cache) {
  442. return $this->cache->get('illuminate:queue:restart');
  443. }
  444. }
  445. /**
  446. * Enable async signals for the process.
  447. *
  448. * @return void
  449. */
  450. protected function listenForSignals()
  451. {
  452. pcntl_async_signals(true);
  453. pcntl_signal(SIGTERM, function () {
  454. $this->shouldQuit = true;
  455. });
  456. pcntl_signal(SIGUSR2, function () {
  457. $this->paused = true;
  458. });
  459. pcntl_signal(SIGCONT, function () {
  460. $this->paused = false;
  461. });
  462. }
  463. /**
  464. * Determine if "async" signals are supported.
  465. *
  466. * @return bool
  467. */
  468. protected function supportsAsyncSignals()
  469. {
  470. return extension_loaded('pcntl');
  471. }
  472. /**
  473. * Determine if the memory limit has been exceeded.
  474. *
  475. * @param int $memoryLimit
  476. * @return bool
  477. */
  478. public function memoryExceeded($memoryLimit)
  479. {
  480. return (memory_get_usage() / 1024 / 1024) >= $memoryLimit;
  481. }
  482. /**
  483. * Stop listening and bail out of the script.
  484. *
  485. * @param int $status
  486. * @return void
  487. */
  488. public function stop($status = 0)
  489. {
  490. $this->events->dispatch(new Events\WorkerStopping);
  491. exit($status);
  492. }
  493. /**
  494. * Kill the process.
  495. *
  496. * @param int $status
  497. * @return void
  498. */
  499. public function kill($status = 0)
  500. {
  501. $this->events->dispatch(new Events\WorkerStopping);
  502. if (extension_loaded('posix')) {
  503. posix_kill(getmypid(), SIGKILL);
  504. }
  505. exit($status);
  506. }
  507. /**
  508. * Sleep the script for a given number of seconds.
  509. *
  510. * @param int|float $seconds
  511. * @return void
  512. */
  513. public function sleep($seconds)
  514. {
  515. if ($seconds < 1) {
  516. usleep($seconds * 1000000);
  517. } else {
  518. sleep($seconds);
  519. }
  520. }
  521. /**
  522. * Set the cache repository implementation.
  523. *
  524. * @param \Illuminate\Contracts\Cache\Repository $cache
  525. * @return void
  526. */
  527. public function setCache(CacheContract $cache)
  528. {
  529. $this->cache = $cache;
  530. }
  531. /**
  532. * Get the queue manager instance.
  533. *
  534. * @return \Illuminate\Queue\QueueManager
  535. */
  536. public function getManager()
  537. {
  538. return $this->manager;
  539. }
  540. /**
  541. * Set the queue manager instance.
  542. *
  543. * @param \Illuminate\Queue\QueueManager $manager
  544. * @return void
  545. */
  546. public function setManager(QueueManager $manager)
  547. {
  548. $this->manager = $manager;
  549. }
  550. }