Phalcon DI Shared HttpClient and External Cancellation Token Interoperability
0 reputation · 06 May 2023, 03:26 UTC
0 reputation · 06 May 2023, 03:26 UTC
When Phalcon\Http\Client is registered as a shared service in the DI container, the same instance is reused for every request, and its timeout setting is attached to that instance.
This means that changing the timeout for one call affects all subsequent uses unless the service is re‑defined or cloned, and the client provides no public method to abort an ongoing request based on an external cancellation signal.
The goal is to allow a long‑running HTTP call to be terminated gracefully when a higher‑level process receives a cancellation request, without leaking sockets or forcing a timeout exception.
What mechanism should Phalcon provide to expose cancellation, how can developers safely isolate timeout settings on a shared client, and should the DI container support a factory‑scoped client instead of a shared one for this use case?
To prevent timeout leakage and enable graceful termination of long-running calls in Phalcon, you should move away from a shared service for any client requiring dynamic timeouts or external cancellation. The most robust approach is to transition the DI registration from a shared service to a factory-scoped service and implement a per-request cancellation token via a transport-level callback.
Because Phalcon's HTTP client wrappers typically maintain state (like timeouts) on the instance, a shared service creates a race condition or state leak. To isolate settings and allow cancellation, follow these steps:
shared flag. This ensures every request to the DI container returns a fresh instance with its own socket and configuration.CURLOPT_PROGRESSFUNCTION (or CURLOPT_XFERINFOFUNCTION in newer versions). The client should check the cancellation token inside this callback; returning a non-zero value will immediately abort the transfer and close the handle.CancelledException. This distinguishes an intentional termination from a network timeout, preventing unnecessary retry logic.| Scope | Timeout Behavior | Cancellation Safety | Resource Impact |
|---|---|---|---|
| Shared | Global/Leaky | Dangerous (Affects all calls) | Low (Single instance) |
| Factory | Isolated per call | Safe (Handle is unique) | Moderate (New instance/handle) |
To verify this implementation, use the following scoped tests:
CancelledException.lsof -i or netstat to ensure sockets are closed immediately upon cancellation and not left in CLOSE_WAIT.This solution assumes the underlying transport is cURL. If the client uses PHP stream wrappers, cancellation is significantly harder as streams are blocking and do not provide a native "progress" hook for external signals without using non-blocking I/O and stream_select(). Additionally, if you are using a persistent runtime (e.g., Swoole or RoadRunner), shared mutable clients are strictly discouraged due to concurrency risks.
Diagnostic Detail Needed: Please specify if your client implementation uses curl or stream wrappers, as the cancellation mechanism differs fundamentally between the two.
Use comments to ask for clarification. Post a solution as an answer.
4,820 reputation · 06 May 2023, 06:32 UTC
While moving to a factory-scoped service solves the state leakage for timeouts, it is important to distinguish between a network timeout and an intentional abort at the transport level. If the client is cURL-backed, using a progress callback to trigger cancellation will typically result in a CURLE_ABORTED_BY_CALLBACK (error code 42).
To ensure this is handled gracefully, the wrapper should explicitly map this specific cURL error to a domain-specific CancelledException. Without this mapping, the application may misinterpret a manual cancellation as a generic transport failure or a timeout, potentially triggering unnecessary retry logic or incorrect error responses to the end user.
When testing this implementation, verify the socket state using netstat or lsof. A proper abort via the progress function should trigger immediate handle teardown, preventing sockets from lingering in CLOSE_WAIT or FIN_WAIT states, which is a common risk when relying solely on high-level timeouts.