[PECL-CVS] [pecl-networking-gearman] master: Merge pull request #53 from php/fix-worker-exit-job-retry

[email protected] (Rasmus Lerdorf via GitHub) Sat, 4 Apr 2026 14:17:34 +0000
Newsgroups php.pecl.cvs
Message-ID <[email protected]>
Author: Rasmus Lerdorf (rlerdorf)
Committer: GitHub (web-flow)
Pusher: rlerdorf
Date: 2026-04-04T15:17:32+01:00

Commit: https://github.com/php/pecl-networking-gearman/commit/7fba0b1ffafeda593e1bd23be3e84c977d081482
Raw diff: https://github.com/php/pecl-networking-gearman/commit/7fba0b1ffafeda593e1bd23be3e84c977d081482.diff

Merge pull request #53 from php/fix-worker-exit-job-retry

fixes #26

Changed paths:
  A  tests/gearman_worker_integration_test_002.phpt
  M  php_gearman_worker.c


Diff:

diff --git a/php_gearman_worker.c b/php_gearman_worker.c
index ed75022..840b938 100644
--- a/php_gearman_worker.c
+++ b/php_gearman_worker.c
@@ -524,6 +524,25 @@ static void *_php_worker_function_callback(gearman_job_st *job,
 
         *ret_ptr = jobj->ret;
 
+#if PHP_VERSION_ID >= 80000
+        /* If exit()/die() was called inside the callback, PHP sets an
+         * UnwindExit exception but returns control here. Without this
+         * check, the code below would send WORK_EXCEPTION to gearmand,
+         * marking the job as failed instead of returning it to the queue.
+         * Force a bailout so the process exits without sending any gearman
+         * protocol response — gearmand will detect the disconnect and
+         * return the job to the queue for retry. See issue #26. */
+        if (EG(exception) && zend_is_unwind_exit(EG(exception))) {
+                if (!Z_ISUNDEF(argv[0])) {
+                        zval_ptr_dtor_nogc(&argv[0]);
+                }
+                if (!Z_ISUNDEF(argv[1])) {
+                        zval_ptr_dtor_nogc(&argv[1]);
+                }
+                zend_bailout();
+        }
+#endif
+
         if (EG(exception)) {
                 *ret_ptr = GEARMAN_WORK_EXCEPTION;
 
diff --git a/tests/gearman_worker_integration_test_002.phpt b/tests/gearman_worker_integration_test_002.phpt
new file mode 100644
index 0000000..a76c5cb
--- /dev/null
+++ b/tests/gearman_worker_integration_test_002.phpt
@@ -0,0 +1,72 @@
+--TEST--
+Worker exit() mid-callback returns job to queue (issue #26)
+--SKIPIF--
+<?php
+require_once('skipif.inc');
+require_once('skipifconnect.inc');
+?>
+--FILE--
+<?php
+require_once('connect.inc');
+
+$func = 'issue26_' . getmypid() . '_' . time();
+
+/* Submit a background job */
+$client = new GearmanClient();
+if ($client->addServer($host, $port) !== true) {
+    die("FAIL: could not add server");
+}
+$handle = $client->doBackground($func, 'test_payload');
+if ($client->returnCode() !== GEARMAN_SUCCESS) {
+    die("FAIL: could not submit job");
+}
+
+/* Worker 1: grabs the job and exit()s without completing it */
+$pid1 = pcntl_fork();
+if ($pid1 === -1) {
+    die("FAIL: could not fork worker 1");
+}
+if ($pid1 === 0) {
+    $w = new GearmanWorker();
+    if ($w->addServer($host, $port) !== true) exit(2);
+    $w->setTimeout(10000);
+    if ($w->addFunction($func, function($job) { exit(1); }) !== true) exit(2);
+    $w->work();
+    exit(0);
+}
+pcntl_waitpid($pid1, $status1, 0);
+if (!pcntl_wifexited($status1) || pcntl_wexitstatus($status1) !== 1) {
+    die("FAIL: worker 1 did not exit from inside the callback");
+}
+
+/* Brief pause for gearmand to detect the disconnect */
+usleep(500000);
+
+/* Worker 2: should receive the retried job */
+$pid2 = pcntl_fork();
+if ($pid2 === -1) {
+    die("FAIL: could not fork worker 2");
+}
+if ($pid2 === 0) {
+    $w = new GearmanWorker();
+    if ($w->addServer($host, $port) !== true) exit(3);
+    $w->setTimeout(10000);
+    if ($w->addFunction($func, function($job) {
+        echo "payload: " . $job->workload() . PHP_EOL;
+        return "done";
+    }) !== true) exit(3);
+    $ret = $w->work();
+    exit($ret ? 0 : 2);
+}
+pcntl_waitpid($pid2, $status2, 0);
+
+if (!pcntl_wifexited($status2)) {
+    echo "FAIL: worker 2 did not exit normally" . PHP_EOL;
+} elseif (pcntl_wexitstatus($status2) === 0) {
+    echo "PASS" . PHP_EOL;
+} else {
+    echo "FAIL: job was not retried" . PHP_EOL;
+}
+--EXPECT--
+payload: test_payload
+PASS