[PECL-CVS] [pecl-networking-gearman] fix-worker-exit-job-retry: fixes #26

[email protected] (Rasmus Lerdorf) Sat, 4 Apr 2026 13:44:28 +0000
Newsgroups php.pecl.cvs
Message-ID <[email protected]>
Author: Rasmus Lerdorf (rlerdorf)
Date: 2026-04-04T09:44:13-04:00

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

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..9cae246
--- /dev/null
+++ b/tests/gearman_worker_integration_test_002.phpt
@@ -0,0 +1,60 @@
+--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();
+$client->addServer($host, $port);
+$handle = $client->doBackground($func, 'test_payload');
+if ($client->returnCode() !== GEARMAN_SUCCESS) {
+    die("Could not submit job");
+}
+
+/* Worker 1: grabs the job and exit()s without completing it */
+$pid1 = pcntl_fork();
+if ($pid1 === 0) {
+    $w = new GearmanWorker();
+    $w->addServer($host, $port);
+    $w->addFunction($func, function($job) {
+        exit(1);
+    });
+    $w->work();
+    exit(0);
+}
+pcntl_waitpid($pid1, $status1, 0);
+
+/* Brief pause for gearmand to detect the disconnect */
+usleep(500000);
+
+/* Worker 2: should receive the retried job */
+$pid2 = pcntl_fork();
+if ($pid2 === 0) {
+    $w = new GearmanWorker();
+    $w->addServer($host, $port);
+    $w->setTimeout(10000);
+    $w->addFunction($func, function($job) {
+        echo "payload: " . $job->workload() . PHP_EOL;
+        return "done";
+    });
+    $ret = $w->work();
+    exit($ret ? 0 : 2);
+}
+pcntl_waitpid($pid2, $status2, 0);
+
+if (pcntl_wexitstatus($status2) === 0) {
+    echo "PASS" . PHP_EOL;
+} else {
+    echo "FAIL: job was not retried" . PHP_EOL;
+}
+--EXPECT--
+payload: test_payload
+PASS