Skip to content

refactor(activity): use curl for the activity log stream - #258

Merged
pjcdawkins merged 1 commit into
mainfrom
activity-log-curl
Oct 21, 2026
Merged

pjcdawkins merged 1 commit into
mainfrom
activity-log-curl

Conversation

@pjcdawkins

@pjcdawkins pjcdawkins commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Activity logs now use the same HTTP client settings as other API requests, and show new log lines sooner.

  • Proxies: the log stream picks the proxy by URL scheme and honors no_proxy, like the API client. Previously it used a single proxy for all URLs and ignored no_proxy.
  • Latency: new log lines are shown as soon as they arrive. Previously the stream was polled with a fixed 200ms sleep, adding up to 200ms of delay.
  • Errors: if the log stream cannot be opened, the error now includes the reason (HTTP status, timeout or curl error).

Implementation:

  • Logs are read with ext-curl (curl_multi) instead of a PHP stream opened with fopen(), so allow_url_fopen is no longer needed.
  • Util\CurlStream: a non-blocking reader for an HTTP response body. It waits for the response headers and treats non-2xx statuses as failures.
  • Api::getCurlOptions() replaces Api::getStreamContext(), using the API client's CA bundle, api.skip_ssl, proxy and user agent settings.
  • The retry behavior is unchanged: up to 2 minutes with a 0.5s interval and a 10s timeout per attempt, reconnecting on EOF.
  • ext-curl is now required in composer.json.

Unlike fopen(), redirects are not followed; the log endpoint does not redirect.

🤖 Generated with Claude Code

Read activity logs with ext-curl (curl_multi), consistent with the API
client, instead of a PHP stream opened with fopen().

- Add Util\CurlStream: a non-blocking reader for an HTTP response body,
  which waits for the response headers and treats non-2xx statuses as
  failures.
- Replace Api::getStreamContext() with Api::getCurlOptions(), which uses
  the API client's CA bundle, api.skip_ssl, proxy and user agent
  settings.
- Keep the existing behavior: retries for up to 2 minutes with a 0.5s
  interval and a 10s timeout per attempt, reconnecting on EOF.
- Require ext-curl in composer.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

Reviewed — No blocking findings · 🔵 3 minor points

🔍 Full review · 7 files reviewed

🔵 Minor points

  • legacy/src/Service/ActivityMonitor.php:193 — The PR removes the fixed 200ms sleep after an empty read, but leaves the one at line 216: if (empty($items)) { if (!$seal) { \usleep(self::STREAM_WAIT); } continue; }. parseLog() only handles complete lines and keeps the partial tail in $buffer. A chunk that ends mid-line therefore produces no items, so the loop sleeps 200ms before it reads the rest of the line, and curl chunks rarely end on line boundaries. The same happens for chunks that only contain duplicate items after a reconnect. read() already waits on the socket, so this sleep only adds latency.
  • legacy/src/Service/ActivityMonitor.php:239 — The stream is only closed on the normal exit path. Each CurlStream holds a reference cycle: the curl handle's HEADERFUNCTION and WRITEFUNCTION closures capture $this. So __destruct never runs on refcount alone. If the loop throws, for example from $activity->refresh() or log parsing, the HTTP connection and curl handles stay alive until the cycle collector runs, if it runs before the process exits. A try/finally around the loop, or a weak or static callback, would release them.
  • legacy/tests/Util/CurlStreamTest.php:74 — The read loop in testRequestHeaders (while (!$stream->eof())) has no deadline, unlike the one in testStream. If the server never finishes the response (a stuck built-in server, or a regression where perform() stops setting done), the test spins forever and hangs the PHPUnit run rather than failing.
Verification
  • onHeader sets headersReceived only on the blank line after a status of 200 or above, so a 100 Continue response is skipped; the regex also matches HTTP/2 200.
  • getCurlOptions applies the CA bundle the same way as Config::getStreamContextOptions (CAPATH for a directory, CAINFO for a file), and also honors api.skip_ssl.
  • Every retry in getLogStream closes the failed CurlStream before it creates a new one, and the EOF reconnect path in waitAndLog closes the old stream too.
  • perform() returns early once done is set, so calling close() and then __destruct (with multi null) is safe.

The new CurlStreamTest covers streaming, request headers, 404, header timeout and connection errors against a PHP built-in server. It runs in the CI make test-php step (Linux). No test covers the ActivityMonitor retry and reconnect loop or Api::getCurlOptions (proxy, no_proxy, CA and skip_ssl options).

Review details
  • Commit: 7bc2ceb
  • Model: claude-opus-5-5

Review 1 of 10 for this pull request · View the full run

@pjcdawkins
pjcdawkins merged commit a4c141c into main Oct 21, 2026
7 checks passed
@pjcdawkins
pjcdawkins deleted the activity-log-curl branch October 21, 2026 15:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant