Worker.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // | Author: yunwuxin <448901948@qq.com>
  10. // +----------------------------------------------------------------------
  11. namespace think\queue;
  12. use Carbon\Carbon;
  13. use Exception;
  14. use RuntimeException;
  15. use think\Cache;
  16. use think\Event;
  17. use think\exception\Handle;
  18. use think\Queue;
  19. use think\queue\event\JobExceptionOccurred;
  20. use think\queue\event\JobFailed;
  21. use think\queue\event\JobProcessed;
  22. use think\queue\event\JobProcessing;
  23. use think\queue\event\WorkerStopping;
  24. use think\queue\exception\MaxAttemptsExceededException;
  25. use Throwable;
  26. class Worker
  27. {
  28. /** @var Event */
  29. protected $event;
  30. /** @var Handle */
  31. protected $handle;
  32. /** @var Queue */
  33. protected $queue;
  34. /** @var Cache */
  35. protected $cache;
  36. /**
  37. * Indicates if the worker should exit.
  38. *
  39. * @var bool
  40. */
  41. public $shouldQuit = false;
  42. /**
  43. * Indicates if the worker is paused.
  44. *
  45. * @var bool
  46. */
  47. public $paused = false;
  48. public function __construct(Queue $queue, Event $event, Handle $handle, Cache $cache = null)
  49. {
  50. $this->queue = $queue;
  51. $this->event = $event;
  52. $this->handle = $handle;
  53. $this->cache = $cache;
  54. }
  55. /**
  56. * @param string $connection
  57. * @param string $queue
  58. * @param int $delay
  59. * @param int $sleep
  60. * @param int $maxTries
  61. * @param int $memory
  62. * @param int $timeout
  63. */
  64. public function daemon($connection, $queue, $delay = 0, $sleep = 3, $maxTries = 0, $memory = 128, $timeout = 60)
  65. {
  66. if ($this->supportsAsyncSignals()) {
  67. $this->listenForSignals();
  68. }
  69. $lastRestart = $this->getTimestampOfLastQueueRestart();
  70. while (true) {
  71. $job = $this->getNextJob(
  72. $this->queue->connection($connection),
  73. $queue
  74. );
  75. if ($this->supportsAsyncSignals()) {
  76. $this->registerTimeoutHandler($job, $timeout);
  77. }
  78. if ($job) {
  79. $this->runJob($job, $connection, $maxTries, $delay);
  80. } else {
  81. $this->sleep($sleep);
  82. }
  83. $this->stopIfNecessary($job, $lastRestart, $memory);
  84. }
  85. }
  86. protected function stopIfNecessary($job, $lastRestart, $memory)
  87. {
  88. if ($this->shouldQuit || $this->queueShouldRestart($lastRestart)) {
  89. $this->stop();
  90. } elseif ($this->memoryExceeded($memory)) {
  91. $this->stop(12);
  92. }
  93. }
  94. /**
  95. * Determine if the queue worker should restart.
  96. *
  97. * @param int|null $lastRestart
  98. * @return bool
  99. */
  100. protected function queueShouldRestart($lastRestart)
  101. {
  102. return $this->getTimestampOfLastQueueRestart() != $lastRestart;
  103. }
  104. /**
  105. * Determine if the memory limit has been exceeded.
  106. *
  107. * @param int $memoryLimit
  108. * @return bool
  109. */
  110. public function memoryExceeded($memoryLimit)
  111. {
  112. return (memory_get_usage(true) / 1024 / 1024) >= $memoryLimit;
  113. }
  114. /**
  115. * 获取队列重启时间
  116. * @return mixed
  117. */
  118. protected function getTimestampOfLastQueueRestart()
  119. {
  120. if ($this->cache) {
  121. return $this->cache->get('think:queue:restart');
  122. }
  123. }
  124. /**
  125. * Register the worker timeout handler.
  126. *
  127. * @param Job|null $job
  128. * @param int $timeout
  129. * @return void
  130. */
  131. protected function registerTimeoutHandler($job, $timeout)
  132. {
  133. pcntl_signal(SIGALRM, function () {
  134. $this->kill(1);
  135. });
  136. pcntl_alarm(
  137. max($this->timeoutForJob($job, $timeout), 0)
  138. );
  139. }
  140. /**
  141. * Stop listening and bail out of the script.
  142. *
  143. * @param int $status
  144. * @return void
  145. */
  146. public function stop($status = 0)
  147. {
  148. $this->event->trigger(new WorkerStopping($status));
  149. exit($status);
  150. }
  151. /**
  152. * Kill the process.
  153. *
  154. * @param int $status
  155. * @return void
  156. */
  157. public function kill($status = 0)
  158. {
  159. $this->event->trigger(new WorkerStopping($status));
  160. if (extension_loaded('posix')) {
  161. posix_kill(getmypid(), SIGKILL);
  162. }
  163. exit($status);
  164. }
  165. /**
  166. * Get the appropriate timeout for the given job.
  167. *
  168. * @param Job|null $job
  169. * @param int $timeout
  170. * @return int
  171. */
  172. protected function timeoutForJob($job, $timeout)
  173. {
  174. return $job && !is_null($job->timeout()) ? $job->timeout() : $timeout;
  175. }
  176. /**
  177. * Determine if "async" signals are supported.
  178. *
  179. * @return bool
  180. */
  181. protected function supportsAsyncSignals()
  182. {
  183. return extension_loaded('pcntl');
  184. }
  185. /**
  186. * Enable async signals for the process.
  187. *
  188. * @return void
  189. */
  190. protected function listenForSignals()
  191. {
  192. pcntl_async_signals(true);
  193. pcntl_signal(SIGTERM, function () {
  194. $this->shouldQuit = true;
  195. });
  196. pcntl_signal(SIGUSR2, function () {
  197. $this->paused = true;
  198. });
  199. pcntl_signal(SIGCONT, function () {
  200. $this->paused = false;
  201. });
  202. }
  203. /**
  204. * 执行下个任务
  205. * @param string $connection
  206. * @param string $queue
  207. * @param int $delay
  208. * @param int $sleep
  209. * @param int $maxTries
  210. * @return void
  211. * @throws Exception
  212. */
  213. public function runNextJob($connection, $queue, $delay = 0, $sleep = 3, $maxTries = 0)
  214. {
  215. $job = $this->getNextJob($this->queue->connection($connection), $queue);
  216. if ($job) {
  217. $this->runJob($job, $connection, $maxTries, $delay);
  218. } else {
  219. $this->sleep($sleep);
  220. }
  221. }
  222. /**
  223. * 执行任务
  224. * @param Job $job
  225. * @param string $connection
  226. * @param int $maxTries
  227. * @param int $delay
  228. * @return void
  229. */
  230. protected function runJob($job, $connection, $maxTries, $delay)
  231. {
  232. try {
  233. $this->process($connection, $job, $maxTries, $delay);
  234. } catch (Exception | Throwable $e) {
  235. $this->handle->report($e);
  236. }
  237. }
  238. /**
  239. * 获取下个任务
  240. * @param Connector $connector
  241. * @param string $queue
  242. * @return Job
  243. */
  244. protected function getNextJob($connector, $queue)
  245. {
  246. try {
  247. foreach (explode(',', $queue) as $queue) {
  248. if (!is_null($job = $connector->pop($queue))) {
  249. return $job;
  250. }
  251. }
  252. } catch (Exception | Throwable $e) {
  253. $this->handle->report($e);
  254. $this->sleep(1);
  255. }
  256. }
  257. /**
  258. * Process a given job from the queue.
  259. * @param string $connection
  260. * @param Job $job
  261. * @param int $maxTries
  262. * @param int $delay
  263. * @return void
  264. * @throws Exception
  265. */
  266. public function process($connection, $job, $maxTries = 0, $delay = 0)
  267. {
  268. try {
  269. $this->event->trigger(new JobProcessing($connection, $job));
  270. $this->markJobAsFailedIfAlreadyExceedsMaxAttempts(
  271. $connection,
  272. $job,
  273. (int) $maxTries
  274. );
  275. $job->fire();
  276. $this->event->trigger(new JobProcessed($connection, $job));
  277. } catch (Exception | Throwable $e) {
  278. try {
  279. if (!$job->hasFailed()) {
  280. $this->markJobAsFailedIfWillExceedMaxAttempts($connection, $job, (int) $maxTries, $e);
  281. }
  282. $this->event->trigger(new JobExceptionOccurred($connection, $job, $e));
  283. } finally {
  284. if (!$job->isDeleted() && !$job->isReleased() && !$job->hasFailed()) {
  285. $job->release($delay);
  286. }
  287. }
  288. throw $e;
  289. }
  290. }
  291. /**
  292. * @param string $connection
  293. * @param Job $job
  294. * @param int $maxTries
  295. */
  296. protected function markJobAsFailedIfAlreadyExceedsMaxAttempts($connection, $job, $maxTries)
  297. {
  298. $maxTries = !is_null($job->maxTries()) ? $job->maxTries() : $maxTries;
  299. $timeoutAt = $job->timeoutAt();
  300. if ($timeoutAt && Carbon::now()->getTimestamp() <= $timeoutAt) {
  301. return;
  302. }
  303. if (!$timeoutAt && (0 === $maxTries || $job->attempts() <= $maxTries)) {
  304. return;
  305. }
  306. $this->failJob($connection, $job, $e = new MaxAttemptsExceededException(
  307. $job->getName() . ' has been attempted too many times or run too long. The job may have previously timed out.'
  308. ));
  309. throw $e;
  310. }
  311. /**
  312. * @param string $connection
  313. * @param Job $job
  314. * @param int $maxTries
  315. * @param Exception $e
  316. */
  317. protected function markJobAsFailedIfWillExceedMaxAttempts($connection, $job, $maxTries, $e)
  318. {
  319. $maxTries = !is_null($job->maxTries()) ? $job->maxTries() : $maxTries;
  320. if ($job->timeoutAt() && $job->timeoutAt() <= Carbon::now()->getTimestamp()) {
  321. $this->failJob($connection, $job, $e);
  322. }
  323. if ($maxTries > 0 && $job->attempts() >= $maxTries) {
  324. $this->failJob($connection, $job, $e);
  325. }
  326. }
  327. /**
  328. * @param string $connection
  329. * @param Job $job
  330. * @param Exception $e
  331. */
  332. protected function failJob($connection, $job, $e)
  333. {
  334. $job->markAsFailed();
  335. if ($job->isDeleted()) {
  336. return;
  337. }
  338. try {
  339. $job->delete();
  340. $job->failed($e);
  341. } finally {
  342. $this->event->trigger(new JobFailed(
  343. $connection,
  344. $job,
  345. $e ?: new RuntimeException('ManuallyFailed')
  346. ));
  347. }
  348. }
  349. /**
  350. * Sleep the script for a given number of seconds.
  351. * @param int $seconds
  352. * @return void
  353. */
  354. public function sleep($seconds)
  355. {
  356. if ($seconds < 1) {
  357. usleep($seconds * 1000000);
  358. } else {
  359. sleep($seconds);
  360. }
  361. }
  362. }