perf(curl): run immediate timeout work without delay

BI
7d ago
+135-7

Summary

Libcurl uses a zero timeout to request work as soon as its timer callback returns. The native cURL hook turns that request into a 1 ms Swoole timer, so curl_exec() yields before starting the transfer.

This is visible on fast local requests, where the extra reactor wait can take longer than the transfer itself.

Change

Keep zero-timeout work pending in the existing selector and run it through curl_multi_socket_action() after libcurl's callback returns. The hook no longer suspends while that immediate work is waiting.

Positive timeouts still use Swoole timers. A multi handle already waiting in another coroutine still uses the existing deferred wakeup path.

The zero-timeout path also makes an existing queued-wakeup lifetime bug easy to hit: a multi handle can be released after its wakeup is queued but before that callback runs. Master already has the same lifetime bug through socket readiness and select timeouts. This PR keeps the object alive until its pending callback finishes, matching the 6.1 fix in #6303.

Performance

Measured on Linux with PHP 8.4 and libcurl 8.5 against a local Swoole HTTP server. Each result is the range of p50 values from five rounds of 500 warmed requests using a reused easy handle.

MeasurementUnchanged masterWith this change
curl_exec() wall time1.261–1.293 ms0.066–0.086 ms
libcurl total time0.149–0.157 ms0.060–0.078 ms

Tests

  • Added a deterministic file:// regression that verifies immediate work starts before a queued reactor defer. It fails on unchanged master and passes with this change.
  • Added a local regression for cancellation and release after the zero-timeout path queues a wakeup. It crashes without the ownership handoff and passes with it, including under Valgrind.
  • Built the PHP 8.4 extension with sockets and the native cURL hook.
  • Ran the complete cURL multi-handle test directory and the affected easy-handle lifecycle paths.