Redesigning Lori's Send System #150
Replies: 1 comment
-
ReviewMajor: Design needs updating for the DataInterceptor architectureThe main body of the design references The "Interaction with DataInterceptor Design (#149)" section acknowledges compatibility, but it's a postscript rather than the primary design. Specific impacts:
|
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Ref: #136
Problem
Lori's send system mirrors the Pony standard library's
netpackage design, which makes choices that don't fit Lori's architecture:Silent failures:
send()does not check connection state. Data sent on a closed connection is silently dropped by_send_final. No error is reported to the caller.Library-managed queuing: On POSIX,
_send_finalpushes data to aList[(ByteSeq, USize)]and drains it via_send_pending_writes. During backpressure the library buffers indefinitely rather than letting the application decide whether to queue, drop, or take other action.No send completion tracking: No mechanism exists to know when data has been handed to the OS. The application can't correlate a
send()call with actual transmission.No writeability query: The enclosing actor can't check socket writeability before sending.
Goals
From #136:
sendbecomes fallible — reports failure when the connection can't accept datasend()calls with_on_sent()callbacks_on_sentcallback — notifies when data has been handed to the OSis_writeable()query — lets the actor check writeability before sendingDesign
send()Return TypeReturns a
SendTokenon success, or aSendErrorexplaining the failure.When
send()succeeds: The data has been accepted. It may have been fully written to the OS already (common case), or partially written with the remainder pending (backpressure applied). Either way,_on_sent(token)will fire when the data is fully handed to the OS.When
send()fails: No data was sent, no token is issued, no callback will fire. The application decides what to do — queue the data, drop it, close the connection, etc.SendTokenManaged by TCPConnection via a monotonically increasing counter. Private constructor prevents external creation. Token equality is structural (based on
id), scoped per connection — eachTCPConnectionhas its own counter. Applications managing multiple connections should pair tokens with connection identity to avoid ambiguity.SendErrorThree error types because the failure reasons have different semantics and different recovery signals:
SendErrorNotConnected— permanent; retry won't help without reconnectingSendErrorNotWriteable— transient; wait for_on_unthrottledSendErrorNotReady— transient; wait for_on_connected/_on_startedis_writeable()QueryReturns whether the connection can currently accept a
send()call. Checks socket writeability AND whether any interceptor layer is ready. Uses the existing_interceptor_readyfield, which is set when the interceptor callssignal_ready()during setup. Connections without an interceptor are always considered ready._on_sentCallbackAdded to both lifecycle event receiver traits:
Always deferred:
_on_sentis always deferred to a subsequent behavior turn via a new behavior onTCPConnectionActor:This deferral applies in all cases — both immediate completions and pending write flushes. The consistency avoids re-entrancy hazards: if
_on_sentfired synchronously during_send_pending_writes(before_release_backpressure), asend()call from within the callback could set new pending data, and the subsequent_release_backpressure()would incorrectly fire. Deferring_on_sentensures backpressure state is settled before the application can act.Connection close: If the connection closes while a send is pending,
_on_sentwill not fire. The application receives_on_closedand can treat any outstanding tokens as implicitly failed.Pending Writes
The existing
_pending: List[(ByteSeq, USize)]is retained. The unbounded growth problem is solved at thesend()level:send()refuses new data when not writeable, so user data can contribute at most one item to the list (a partial write). The list also absorbs small, infrequent protocol data from interceptors (e.g., SSL session tickets, key updates) that may arrive via_send_finalduringincoming()processing.New fields for token tracking:
Revised
send()FlowThe interceptor path uses the existing
_WireSenderAdapter, which routes eachwire.send()call to_send_final. The interceptor'soutgoing()may callwire.send()multiple times (e.g., SSL producing multiple encrypted records); the pending list absorbs these naturally._send_finalcalls_send_pending_writes()after each push, draining as much as possible.Revised
_send_pending_writes(POSIX)_event_notifycalls_send_pending_writes()when the socket becomes writeable.Graceful Close
close()does not wait for pending writes. Pending writes may partially drain betweenclose()andhard_close()(if the socket becomes writeable in between), and the token fires normally if they happen to complete. Otherwise, onhard_close, the pending token is cleared along with the pending list:The abandoned token means no
_on_sentfires;_on_closedis the signal that outstanding tokens are implicitly failed.SSL Integration
The DataInterceptor architecture handles SSL integration with minimal changes. The key interactions:
Writeability check:
TCPConnection.is_writeable()checks_interceptor_ready, which is set when the SSL interceptor callscontrol.signal_ready()after handshake completion.send()returnsSendErrorNotReadybefore the handshake completes.Outgoing data: SSL's
outgoing()method is unchanged. It encrypts data via the SSL library and pushes encrypted chunks towire.send(). Sincesend()blocks calls before the handshake completes (via_interceptor_readycheck), the SSL interceptor's pre-handshake buffering (_pendinglist inSSLClientInterceptor/SSLServerInterceptor) becomes dead code and should be removed during implementation.Protocol data during
incoming(): SSL's_ssl_poll()sends protocol data (handshake responses, session tickets, key updates) viawire.send()duringincoming()processing. This routes through_WireSenderAdapter→_send_final()→ the pending list. If user data is also pending (partial write), the protocol data is appended to the list and drained together. The token fires when the entire list drains, which may include both user data and protocol data. In practice, protocol data is small and infrequent (a few hundred bytes for session tickets), so this adds negligible delay to token delivery.Protocol data can cause backpressure: If protocol data sent during
incoming()processing causes a partial write,_apply_backpressuresets_writeable = false. If the application callssend()from within the same_on_receivedcallback (a common pattern — e.g., echo servers), it will seeSendErrorNotWriteableeven though the backpressure was caused by protocol overhead, not application data. This is correct behavior (the socket genuinely isn't writeable), but applications should be prepared forsend()to fail during_on_received.Interceptor errors during
send(): If the SSL interceptor encounters an error duringoutgoing()(e.g.,_ssl.write()fails), it callscontrol.close(), which closes the connection. Back insend(),is_open()returns false andSendErrorNotConnectedis returned. Any partially encrypted chunks already pushed to the pending list are cleaned up byhard_close().Platform Considerations: Windows
The public API (
send()return type,SendToken,SendError,is_writeable(),_on_sent) is platform-independent. The IOCP-specific implementation details (how_send_final,_send_pending_writes, and_write_completedwork under Windows) are deferred to a separate design effort. The conceptual model maps naturally:send()submits data,_write_completedreports completion, and the token fires when the write is confirmed.Migration Impact
This is a breaking change to all users of
send().Before
After
New Optional Callbacks
Applications that want send completion tracking implement
_on_sent:Applications that don't care can rely on the default no-op implementation.
SSL Users
No changes to the SSL wrapping pattern. The SSL interceptor's internal buffering of pre-handshake sends becomes unnecessary —
send()fails explicitly withSendErrorNotReadybefore the handshake completes. Applications that relied on send-before-handshake working silently need to handle the error (queue the data themselves, or wait for_on_connected/_on_started).Summary of New/Changed API Surface
TCPConnection.send()(SendToken | SendError)instead ofNoneTCPConnection.is_writeable()TCPConnection._fire_on_sent(token)SendTokenEquatable[SendToken])SendErrorNotConnectedSendErrorNotWriteableSendErrorNotReadySendError_on_sent(token)_notify_sent(token)TCPConnectionActor_pending_tokenTCPConnection_next_token_idTCPConnection_pendinglistBeta Was this translation helpful? Give feedback.
All reactions