-
Notifications
You must be signed in to change notification settings - Fork 9
Refine connection exception for internal control flow #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
cclilshy
merged 7 commits into
main
from
cursor/refine-connection-exception-for-internal-control-flow-d087
Sep 10, 2025
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
c673f98
Refactor stream exception handling with improved lifecycle events
cursoragent 337a73c
Refactor stream exceptions and improve error handling in transport layer
cursoragent 0234748
style: format
cclilshy c22d538
fix: 修复对readonly属性写导致异常
cclilshy 3d3c02d
fix: 取消对文件类型的stream进行epoll操作
cclilshy 6d2880e
fix: 恢复Promise.await的默认展平行为
cclilshy a213c4f
doc: 统一代码风格并补充文档
cclilshy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| # Stream Exception Handling Guide | ||
|
|
||
| ## Overview | ||
|
|
||
| The Stream module uses a clear exception hierarchy that separates internal control-flow exceptions from application-level exceptions that user code should handle. | ||
|
|
||
| ## Exception Hierarchy | ||
|
|
||
| ``` | ||
| RuntimeException | ||
| ├── StreamException (base for user-catchable exceptions) | ||
| │ └── TransportException (recoverable transport errors) | ||
| │ ├── TransportTimeoutException (timeout errors) | ||
| │ ├── ConnectionTimeoutException (connection timeouts) | ||
| │ └── WriteClosedException (write to closed stream) | ||
| └── ConnectionException (internal control-flow - DO NOT CATCH) | ||
| ``` | ||
|
|
||
| ## Key Principles | ||
|
|
||
| ### 1. Internal Control-Flow Exceptions (DO NOT CATCH) | ||
|
|
||
| **ConnectionException** is an internal exception used exclusively by the reactor for connection termination. It implements the `AbortConnection` marker interface. | ||
|
|
||
| - **Purpose**: Signal immediate connection termination to the reactor | ||
| - **Usage**: Only thrown internally when connection becomes unusable | ||
| - **Handling**: Only caught by reactor's exception boundary, never by user code | ||
|
|
||
| ```php | ||
| // ❌ NEVER do this | ||
| try { | ||
| $stream->read(1024); | ||
| } catch (ConnectionException $e) { | ||
| // This breaks the reactor's control flow! | ||
| } | ||
|
|
||
| // ✅ Use connection lifecycle events instead | ||
| $stream->onClose(function (CloseEvent $event) { | ||
| echo "Connection closed: {$event->reason->value}"; | ||
| }); | ||
| ``` | ||
|
|
||
| ### 2. Application-Level Exceptions (Safe to Catch) | ||
|
|
||
| **TransportException** and its subclasses represent recoverable errors that application code can handle. | ||
|
|
||
| ```php | ||
| // ✅ Safe to catch and handle | ||
| try { | ||
| $stream->write($data); | ||
| } catch (WriteClosedException $e) { | ||
| // Write side is closed, but connection might still be readable | ||
| echo "Cannot write: {$e->getMessage()}"; | ||
| } catch (TransportException $e) { | ||
| // Other transport-level errors | ||
| echo "Transport error: {$e->getMessage()}"; | ||
| } | ||
| ``` | ||
|
|
||
| ## Connection Lifecycle Events | ||
|
|
||
| Instead of catching exceptions, use lifecycle events to handle connection state changes: | ||
|
|
||
| ### onClose(CloseEvent) | ||
| Triggered once when the connection terminates. | ||
|
|
||
| ```php | ||
| $stream->onClose(function (CloseEvent $event) { | ||
| echo "Closed: {$event->reason->value} by {$event->initiator}"; | ||
| // Cleanup resources, update connection pools, etc. | ||
| }); | ||
| ``` | ||
|
|
||
| **CloseEvent** provides: | ||
| - `reason`: ConnectionAbortReason enum (PEER_CLOSED, RESET, TIMEOUT, etc.) | ||
| - `initiator`: 'peer' | 'local' | 'system' | ||
| - `message`: Optional descriptive message | ||
| - `lastError`: Optional underlying exception | ||
| - `timestamp`: When the close occurred | ||
|
|
||
| ### onReadableEnd() | ||
| Triggered when the read side closes (EOF) but connection may still be writable. | ||
|
|
||
| ```php | ||
| $stream->onReadableEnd(function () use ($stream) { | ||
| echo "Read side closed - no more data from peer"; | ||
| // Can still write final response, then close | ||
| $stream->write("HTTP/1.1 200 OK\r\n\r\nGoodbye"); | ||
| $stream->close(); | ||
| }); | ||
| ``` | ||
|
|
||
| ### onWritableEnd() | ||
| Triggered when the write side closes but connection may still be readable. | ||
|
|
||
| ```php | ||
| $stream->onWritableEnd(function () use ($stream) { | ||
| echo "Write side closed - cannot send more data"; | ||
| // Can still read remaining data from peer | ||
| }); | ||
| ``` | ||
|
|
||
| ## Half-Close Support | ||
|
|
||
| Half-close allows one side of a connection to be closed while the other remains open. This is useful for protocols like HTTP where the client sends a complete request, then the server sends a complete response. | ||
|
|
||
| ### Configuration | ||
|
|
||
| ```php | ||
| $stream = new Stream($resource); | ||
| $stream->supportsHalfClose = true; // Default: true | ||
| ``` | ||
|
|
||
| ### Behavior | ||
|
|
||
| When `supportsHalfClose = true`: | ||
| - `read()` returning EOF triggers `onReadableEnd()` if registered, otherwise throws `ConnectionException` | ||
| - `write()` getting EPIPE triggers `onWritableEnd()` if registered, otherwise throws `ConnectionException` | ||
|
|
||
| When `supportsHalfClose = false`: | ||
| - EOF or EPIPE immediately throws `ConnectionException` for reactor termination | ||
|
|
||
| ## Error Classification | ||
|
|
||
| ### Fatal Errors (→ ConnectionException) | ||
| These errors indicate the connection is no longer usable: | ||
| - Peer closed connection (EOF without half-close support) | ||
| - Connection reset by peer (ECONNRESET) | ||
| - TLS fatal alerts | ||
| - Broken pipe (EPIPE without half-close support) | ||
|
|
||
| ### Recoverable Errors (→ TransportException) | ||
| These errors can be handled by application logic: | ||
| - Connection timeouts (can retry) | ||
| - Write to closed stream (can detect and handle) | ||
| - Protocol-level errors (application can decide response) | ||
| - Temporary resource unavailability | ||
|
|
||
| ## Migration from Old API | ||
|
|
||
| ### Before | ||
| ```php | ||
| try { | ||
| $data = $stream->read(1024); | ||
| } catch (ConnectionException $e) { | ||
| if ($e->getCode() === ConnectionException::CONNECTION_CLOSED) { | ||
| // Handle close | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### After | ||
| ```php | ||
| // Use events for lifecycle management | ||
| $stream->onClose(function (CloseEvent $event) { | ||
| if ($event->reason === ConnectionAbortReason::PEER_CLOSED) { | ||
| // Handle close | ||
| } | ||
| }); | ||
|
|
||
| $stream->onReadableEnd(function () { | ||
| // Handle EOF/half-close | ||
| }); | ||
|
|
||
| // Only catch recoverable exceptions | ||
| try { | ||
| $data = $stream->read(1024); | ||
| } catch (TransportException $e) { | ||
| // Handle recoverable errors only | ||
| } | ||
| ``` | ||
|
|
||
| ## Best Practices | ||
|
|
||
| 1. **Never catch ConnectionException** - use lifecycle events instead | ||
| 2. **Register onClose for cleanup** - guaranteed to be called once per connection | ||
| 3. **Use onReadableEnd/onWritableEnd** for half-close protocols | ||
| 4. **Catch TransportException** for recoverable error handling | ||
| 5. **Let the reactor handle fatal errors** - it will clean up and emit events | ||
|
|
||
| ## Common Patterns | ||
|
|
||
| ### HTTP Server | ||
| ```php | ||
| $stream->onReadableEnd(function () use ($stream, $response) { | ||
| // Client finished sending request, send response | ||
| $stream->write($response); | ||
| $stream->close(); | ||
| }); | ||
|
|
||
| $stream->onClose(function (CloseEvent $event) use ($connectionPool) { | ||
| $connectionPool->remove($stream); | ||
| }); | ||
| ``` | ||
|
|
||
| ### Database Client | ||
| ```php | ||
| $stream->onClose(function (CloseEvent $event) use ($pendingQueries) { | ||
| // Fail all pending queries | ||
| foreach ($pendingQueries as $query) { | ||
| $query->fail(new TransportException("Connection lost: {$event->reason->value}")); | ||
| } | ||
| }); | ||
| ``` | ||
|
|
||
| ### WebSocket | ||
| ```php | ||
| $stream->onClose(function (CloseEvent $event) use ($subscriptions) { | ||
| // Clean up subscriptions | ||
| foreach ($subscriptions as $sub) { | ||
| $sub->cancel(); | ||
| } | ||
| }); | ||
| ``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| include __DIR__ . '/../vendor/autoload.php'; | ||
|
|
||
| use Ripple\Socket; | ||
| use Ripple\Stream\CloseEvent; | ||
| use Ripple\Stream\Exception\TransportException; | ||
| use Ripple\Stream\Exception\WriteClosedException; | ||
| use Ripple\Utils\Output; | ||
|
|
||
| use function Co\wait; | ||
|
|
||
| /** | ||
| * Example demonstrating proper usage of the enhanced Stream API | ||
| * | ||
| * This example shows: | ||
| * 1. How to handle connection lifecycle events (onClose, onReadableEnd) | ||
| * 2. Proper exception handling (catch TransportException, not ConnectionException) | ||
| * 3. Half-close support for HTTP-like protocols | ||
| */ | ||
|
|
||
| try { | ||
| $stream = Socket::connect('tcp://httpbin.org:80'); | ||
| $stream->setBlocking(false); | ||
|
|
||
| // Register connection lifecycle events | ||
| $stream->onClose(function (CloseEvent $event) { | ||
| Output::info("Connection closed: {$event->reason->value} by {$event->initiator}"); | ||
| }); | ||
|
|
||
| $stream->onReadableEnd(function () use ($stream) { | ||
| Output::info("Read side closed - server finished sending response"); | ||
| // We can still write if needed, but in HTTP we typically close now | ||
| $stream->close(); | ||
| }); | ||
|
|
||
| // Send HTTP request | ||
| $request = "GET /get HTTP/1.1\r\nHost: httpbin.org\r\nConnection: close\r\n\r\n"; | ||
| $stream->write($request); | ||
|
|
||
| // Read response | ||
| $stream->onReadable(function () use ($stream) { | ||
| try { | ||
| $data = $stream->read(1024); | ||
| if ($data !== '') { | ||
| echo $data; | ||
| } | ||
| // Note: When server closes connection, onReadableEnd will be triggered | ||
| // instead of an exception, allowing graceful handling | ||
| } catch (WriteClosedException $e) { | ||
| // This is a recoverable exception - safe to catch | ||
| Output::warning("Attempted to write to closed connection: {$e->getMessage()}"); | ||
| } catch (TransportException $e) { | ||
| // This is a recoverable exception - safe to catch | ||
| Output::error("Transport error: {$e->getMessage()}"); | ||
| $stream->close(); | ||
| } | ||
| // Note: We NEVER catch ConnectionException - that's handled internally by the reactor | ||
| }); | ||
|
|
||
| } catch (TransportException $e) { | ||
| // Connection establishment failed - this is recoverable | ||
| Output::error("Failed to connect: {$e->getMessage()}"); | ||
| exit(1); | ||
| } | ||
|
|
||
| wait(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.