From 6abf925db7e0a726cf447c7fbb0cd7b12b3096bd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 14 Oct 2026 13:28:06 +0000 Subject: [PATCH] (0.10.x) - Make TimerCallbackTest match libcurl 8.5 timer updates Ubuntu 24.04's libcurl 8.5 does not invoke CURLMOPT_TIMERFUNCTION on every curl_multi_socket_action(CURL_SOCKET_TIMEOUT): it clears timer_lastcall first, then skips the callback when the new timeout is -1. Localhost transfers with no CURLOPT_TIMEOUT look like that after EXPIRE_RUN_NOW is consumed, so the suite's socket-action and completion-cancel assertions never saw a callback. Give those easies CURLOPT_TIMEOUT_MS so 8.5 has an expire left to report (and still reports -1 at completion). The C trampoline was already 1:1. Co-authored-by: Angel Gonzalez --- .okf/api/multi.md | 4 ++-- .okf/log.md | 4 ++++ tests/TimerCallbackTest.php | 26 +++++++++++++++++++++----- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.okf/api/multi.md b/.okf/api/multi.md index 591a492..f5b5358 100644 --- a/.okf/api/multi.md +++ b/.okf/api/multi.md @@ -5,7 +5,7 @@ description: Static 1:1 bindings for libcurl multi calls ext-curl leaves out resource: /pcurl/multi.zep tags: [pcurl, api, libcurl] status: draft -generated: { by: claude-code/claude-opus-5.5, at: "2026-10-11T12:00:00Z" } +generated: { by: cursor-agent/grok-4.6, at: "2026-10-14T13:30:00Z" } sources: - id: zep resource: /pcurl/multi.zep @@ -41,7 +41,7 @@ Every method is named after the C call it binds and takes ext-curl's `CurlMultiH * Long options (`< CURLOPTTYPE_OBJECTPOINT`) go straight to `curl_multi_setopt`. * `CURLMOPT_SOCKETFUNCTION` (20001): `callable(?CurlHandle $easy, int $s, int $what, mixed $clientp, mixed $socketp): int`. `$easy` is the ext-curl object found in the multi's handle list, or `null`. -* `CURLMOPT_TIMERFUNCTION` (20004): `callable(CurlMultiHandle $multi, int $timeoutMs, mixed $clientp): int`. +* `CURLMOPT_TIMERFUNCTION` (20004): `callable(CurlMultiHandle $multi, int $timeoutMs, mixed $clientp): int`. libcurl invokes it when the timeout it wants changes, not on every `curl_multi_socket_action(CURL_SOCKET_TIMEOUT)`. On 8.5 that TIMEOUT action clears `timer_lastcall` then skips the callback if the new timeout is `-1`; 8.11+ keeps `last_timeout_ms` and still reports `-1`. The suite's timer tests set `CURLOPT_TIMEOUT_MS` so both trees have an expire to report. * `CURLMOPT_SOCKETDATA` (10002) / `CURLMOPT_TIMERDATA` (10005): any value, passed back as `$clientp`. libcurl's own data pointer is always pcurl's state, never the PHP value. * `null` uninstalls a function; a non-callable returns `CURLM_BAD_FUNCTION_ARGUMENT` (10) and leaves the old one in place. * Any other object/function option → `CURLM_UNKNOWN_OPTION` (6). `CURLMOPT_PUSHFUNCTION` stays with ext-curl's `curl_multi_setopt`. diff --git a/.okf/log.md b/.okf/log.md index 7d0c28a..12b4b1e 100644 --- a/.okf/log.md +++ b/.okf/log.md @@ -1,5 +1,9 @@ # Directory Update Log +## 2026-10-14 + +* **TimerCallbackTest vs libcurl 8.5**: Ubuntu 24.04 CI (libcurl 8.5.0) failed 4 tests that assumed `curlMultiSocketAction(CURL_SOCKET_TIMEOUT)` always invokes `CURLMOPT_TIMERFUNCTION`. It doesn't: 8.5's `multi_socket` memsets `timer_lastcall` to `{0,0}` on `CURL_SOCKET_TIMEOUT`, then `Curl_update_timer` skips the callback when the new timeout is `-1` (already "none"). A localhost transfer with no `CURLOPT_TIMEOUT` looks exactly like that after `EXPIRE_RUN_NOW` is consumed — add_handle still fires `0`, `curl_multi_exec` still fires, completion never sends `-1`. 8.11 (`a58b50f`) split that bookkeeping into `last_expire_ts` (cleared) and `last_timeout_ms` (kept), so the same TIMEOUT call reports `-1`; Homebrew 8.22 and the Pi's 8.14.1 follow that path and were green. The tests now put `CURLOPT_TIMEOUT_MS` on the easy so 8.5 has an expire left to report. C unchanged: the trampoline was firing whenever libcurl asked. + ## 2026-10-12 * **Version**: 0.10.5 in `config.json`, `composer.json`, `PHP_PCURL_VERSION`. No API change; `ide/0.10.5/` generated with `zephir stubs`, identical to `ide/0.10.1/`. Steps past the 0.10.3 and 0.10.4 numbers to line up with the sibling extensions. diff --git a/tests/TimerCallbackTest.php b/tests/TimerCallbackTest.php index 6cb693e..c587baa 100644 --- a/tests/TimerCallbackTest.php +++ b/tests/TimerCallbackTest.php @@ -27,7 +27,7 @@ it('is asked to cancel with -1 once the transfer completes', function (): void { $timeouts = []; - $easy = easyFor('/cancel'); + $easy = easyWithTimeout('/cancel'); drive($this->multi, [$easy], null, function (CurlMultiHandle $m, int $timeoutMs) use (&$timeouts): void { $timeouts[] = $timeoutMs; }); @@ -38,14 +38,30 @@ }); /* - * libcurl calls the timer callback only when the timeout it wants changes. Each test below installs a - * no-op callback before adding the handle, so libcurl has a recorded timeout (0) that the next call - * moves, then swaps in the callback under test. + * libcurl calls TIMERFUNCTION only when the timeout it wants changes. + * + * curl_multi_socket_action(CURL_SOCKET_TIMEOUT) on libcurl 8.5 (Ubuntu 24.04) memsets + * timer_lastcall to {0,0} first, then skips the callback if the new timeout is -1 — which + * is what a localhost transfer looks like once RUN_NOW has been consumed and nothing else + * is in the expire tree. 8.11+ keeps last_timeout_ms across that memset, so the same call + * reports -1 even without an easy timeout. + * + * CURLOPT_TIMEOUT_MS leaves an expire in the tree: TIMEOUT then reports the remaining ms, + * and completion reports -1. Each test below installs a no-op callback before adding that + * easy, so libcurl records 0, then swaps in the callback under test for the next change. */ +function easyWithTimeout(string $path): CurlHandle +{ + $easy = easyFor($path); + curl_setopt($easy, CURLOPT_TIMEOUT_MS, 5_000); + + return $easy; +} + function multiWithPendingTransfer(CurlMultiHandle $multi, string $path): void { Multi::curlMultiSetopt($multi, CURLMOPT_TIMERFUNCTION, fn (): int => 0); - curl_multi_add_handle($multi, easyFor($path)); + curl_multi_add_handle($multi, easyWithTimeout($path)); } it('treats a null return as 0, so libcurl carries on', function (): void {