Separate Data Interception from Lifecycle Events #149
Replies: 4 comments
-
Implementation note: ChainedInterceptor deferredThe initial implementation (PR forthcoming) does not include The current PR delivers the core |
Beta Was this translation helpful? Give feedback.
-
|
Implementation note: |
Beta Was this translation helpful? Give feedback.
-
|
this was implemented without the chainedinterceptors so i am leaving this open until we decide not to do chained or they are done. |
Beta Was this translation helpful? Give feedback.
-
|
The DataInterceptor design from this discussion was implemented in PR #151, then removed in PR #160 as part of a redesign that made SSL a first-class concern of TCPConnection. The discussions that led to the removal:
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Separate Data Interception from Lifecycle Events
Ref: #137
Problem
Lori's lifecycle event receiver chain uses a single linked list via
_next_lifecycle_event_receiver(). This chain defines one wrapping order for all operations. But correct protocol layering requires opposite orderings for reading and writing.Consider three layers: base TCP, SSL, and a custom handler:
The reading order is SSL-then-custom. The writing order is custom-then-SSL. A single chain can only represent one ordering. With SSL + compression:
If SSL wraps Compression wraps User: read is correct (decrypt then decompress) but send is wrong (encrypts then compresses). If the wrapping order is reversed: send is correct but read is wrong. Neither wrapping order works for both directions.
Why it works today for SSL alone
The SSL implementation (
NetSSLClientConnection/NetSSLServerConnection) works around this by manually routing data outside the chain's ordering. On receive, SSL intercepts raw bytes, decrypts, and pushes decrypted data to the wrapped receiver. On send, SSL delegates the application's_on_sendthrough the chain first, then encrypts the result and sends encrypted bytes directly via_send_final(), bypassing the normal return path. This manual routing gives correct per-direction ordering for SSL specifically, but doesn't generalize to additional layers.Analysis of Current Design
The lifecycle event receiver traits conflate two concerns:
Data transformation (protocol level):
_on_receivedtransforms incoming data (SSL decryption),_on_sendtransforms outgoing data (SSL encryption),_on_expect_setadjusts framing. These are protocol concerns where ordering matters and differs per direction.Application callbacks (application level):
_on_connected/_on_started,_on_closed,_on_throttled/_on_unthrottled, and_on_receivedas the final data consumer. These are application concerns with typically one handler.SSL participates in both: it transforms data AND intercepts lifecycle events (swallowing
_on_connecteduntil handshake completes, cleaning up on close, overriding expect). The current single trait carries all of this, which is the root of the composability problem.Proposed Design
Separate the two concerns: data interception (protocol level) and lifecycle handling (application level). Interceptors are composed by the programmer — no automatic chaining, no magic.
Interfaces
TCPConnection contract with interceptors
Setup timing:
on_setupis called only after TCP establishment succeeds. For client connections,_on_connectingand_on_connection_failurego directly to the lifecycle receiver — the interceptor is not involved in pre-connection events.Ready signaling: TCPConnection defers
_on_connected/_on_starteduntilsignal_ready(). It guards against multiplesignal_ready()calls (delivers_on_connected/_on_startedat most once).Close during handshake: If the connection closes before
signal_ready(),on_teardown()is called for cleanup but_on_connected/_on_startedis never delivered. The lifecycle receiver sees_on_closedwithout a preceding_on_connected, which is different from a normal close. For client connections,_on_connection_failurecould be called instead to distinguish handshake failure from post-connection close, but this is an implementation detail to resolve.Never-signaling interceptors: If an interceptor never calls
signal_ready(), the application is never notified of the connection. Data continues flowing throughincoming()indefinitely. Eventually the remote side or a timeout closes the connection, triggeringon_teardown()and_on_closed. This is documented as a "don't do that" — interceptors must either signal ready or close.Send path: When an interceptor is present,
TCPConnection.send()delegates entirely tointerceptor.outgoing()and does NOT send the original data itself. The interceptor controls what reaches the wire viawire.send(). Without an interceptor,send()calls_send_final()directly.Lifecycle Event Receiver Simplification
With data transformation extracted, lifecycle receivers drop
_next_lifecycle_event_receiver(),_on_send, and_on_expect_set:No chaining. No auto-delegation. One lifecycle receiver per connection.
Behavioral change from removing
_on_send: Currently, the application's_on_sendsees plaintext before encryption (SSL delegates_on_sendthrough the chain before encrypting). In the new design, there is no equivalent callback — outgoing data goes directly fromsend()to the interceptor. Applications that need to inspect outgoing data before protocol transformation can write a thin interceptor. In practice, none of the existing examples or tests override_on_sendfor application logic.TCPConnection Constructor Changes
Data flow with an interceptor:
Without an interceptor: identical to current non-SSL behavior.
Composing Multiple Interceptors
The programmer composes interceptors manually, controlling ordering per direction. No framework magic — the user writes the composition.
ChainedInterceptor Helper
For the common case of two interceptors where the receive order should be reversed for send (the standard protocol stack pattern), Lori provides a helper:
Note:
_ChainedSenderreceivesWireSender ref(notSetupControl ref), so inner interceptors cannot accidentally callsignal_ready()orclose().Manual Composition
For cases where
ChainedInterceptordoesn't fit (asymmetric setup, non-standard ordering), the programmer writes a custom composition class implementingDataInterceptordirectly. The manual composition follows the same adapter pattern shown inChainedInterceptor.SSL as a DataInterceptor
The current
NetSSLClientConnection(107 lines) andNetSSLServerConnection(104 lines) becomeSSLClientInterceptorandSSLServerInterceptorimplementingDataInterceptor. Sketch ofSSLClientInterceptorto validate the interface:Note on outgoing vs. incoming _ssl_poll: The current code uses a unified
_ssl_poll()that checks SSL state and delivers both decrypted data and encrypted protocol data, called from both_on_receivedand_on_send. In this design,outgoing()does not have access toIncomingDataReceiver, so it cannot deliver decrypted data. In practice, SSL's write path produces encrypted output but does not produce decrypted input — decrypted data only becomes available after_ssl.receive(), which is called inincoming(). The split is behaviorally equivalent to the current code for all practical scenarios.SSL-specific callbacks (
on_alpn_negotiated,on_ssl_auth_failed,on_ssl_error) are passed to the SSL interceptor's constructor as an optionalNetSSLLifecycleEventReceiver ref. The application actor implements this interface (as it does today) and passesthis. This decouples SSL-specific callbacks from the base interceptor interface.Client vs. server differences live in the implementation classes (
SSLClientInterceptorvs.SSLServerInterceptor), not in theDataInterceptorinterface, which is unified.Example Usage
Simple server (no interceptor, no change from today except removing _next_lifecycle_event_receiver)
SSL client (interceptor replaces NetSSLClientConnection wrapping)
SSL + compression (composed interceptors)
Migration Impact
Every implementor of the lifecycle receiver traits needs to:
_next_lifecycle_event_receiver()— all current implementations returnNone; this is mechanical deletion_on_sendif overridden — none of the examples or tests override it for application logic; pure deletion_on_expect_setif overridden — sameSSL users need to:
NetSSLClientConnection/NetSSLServerConnectionwrapping with the new SSL interceptor parameterlet sslc = NetSSLClientConnection(consume ssl, this); TCPConnection.client(auth, host, port, "", this, sslc)let interceptor = SSLClientInterceptor(consume ssl, this); TCPConnection.client(auth, host, port, "", this, this, interceptor)The migration steps are:
DataInterceptor,IncomingDataReceiver,WireSender,SetupControlinterfacesinterceptorparameter toTCPConnection.client()and.server()SSLClientInterceptorandSSLServerInterceptorChainedInterceptorhelper_next_lifecycle_event_receiver(),_on_send,_on_expect_setfrom lifecycle traitsNetSSLClientConnection/NetSSLServerConnectionBeta Was this translation helpful? Give feedback.
All reactions