Lori as the Pony Standard Networking Library #199
Replies: 7 comments 1 reply
|
PR #200 adds
|
10. Write yield mechanism — setting aside for nowAfter looking at this more closely, I'm not convinced write yield is a real problem in practice. The write loop is purely mechanical — writev into the kernel until the socket buffer is full (EWOULDBLOCK), then stop. The OS socket buffer is the natural throttle. Unlike the read side, where each iteration runs application logic via The monopolization scenario would require a large socket buffer (or a peer reading aggressively enough that the buffer never fills) combined with a large pending data backlog. In practice, socket buffers are typically 128KB–256KB — we'd fill that in a handful of writev calls and hit EWOULDBLOCK. Setting this aside as "won't do for now, will monitor." If it turns out to be a real problem under load, the straightforward fix is a persistent byte threshold checked in the write loop (similar to stdlib's |
|
Configurable read buffer size implementation plan: #212 |
|
Section 13 (TCP_NODELAY and socket buffer sizes) addressed by PR #217. Adds |
|
Section 14 (General socket option access) addressed by PR #221. Adds |
|
Getting ready to do this. |
Uh oh!
There was an error while loading. Please reload this page.
Framing: lori replaces stdlib's
netpackage as the standard networking library. Lori has a two-tier design: a core API (class + trait composition for full actor control) and a convenience API (standalone actors with notifier pattern for quick programs). See #7.What Lori Needs to Absorb
1. NetAddress
NetAddressis a pure data type (IPv4/IPv6 address representation, getnameinfo for reverse DNS). It has no dependency on TCP/UDP actors. Currently lives in stdlib net; lori imports it viause net = "net"and references it asnet.NetAddressthroughout (tcp_connection.pony,tcp_listener.pony,pony_tcp.pony).As the standard networking library, lori should own
NetAddress. Once absorbed, theuse net = "net"import goes away and allnet.NetAddressreferences throughout lori become plainNetAddress.2. DNS
The
DNSprimitive provides synchronous name resolution (getaddrinfo wrapper), IPv4/IPv6 broadcast address lookup, and IP literal detection (is_ip4/is_ip6). Currently lives in stdlib net.As the standard networking library, lori should own DNS. Name resolution is core networking functionality.
Circular dependency concern: The
ponylang/sslpackage usesDNS.is_ip4()/DNS.is_ip6()inssl.pony(SNI hostname validation) andx509.pony(certificate name matching). Today the dependency chain is: lori →ponylang/ssl→ stdlibnet(for DNS). No circularity. But if lori absorbs DNS, it becomes: lori →ponylang/ssl→ lori. Circular.Options to resolve:
is_ip4/is_ip6into a standalone IP-literal-detection utility that both lori andponylang/sslcan depend on without depending on each otherponylang/ssl(the functions are thin wrappers aroundpony_os_ipv4_addr/pony_os_ipv6_addrFFI calls)DNSprimitive in a small standalone package that both lori andponylang/sslimportponylang/sslso SSL/SSLContext don't depend on DNS (the IP literal checks could move into lori's SSL integration code instead of living in the ssl package)3. Auth primitives
Stdlib net defines:
NetAuth,DNSAuth,UDPAuth,TCPAuth,TCPListenAuth,TCPConnectAuth.Lori currently defines:
NetAuth,TCPAuth,TCPListenAuth,TCPConnectAuth,TCPServerAuth.Lori would need to add
DNSAuth(for the DNS functionality it's absorbing) andUDPAuth(for the UDP functionality it will implement).TCPServerAuth(lori's addition for accepted connections) becomes the standard.4. OSSockOpt and _OSSocket
Stdlib net has
OSSockOpt(~1,245 socket option constants via FFI) and_OSSocket(getsockopt/setsockopt wrappers). Lori already has its own copies of both (ossocketopt.ponyandossocket.pony). As the standard networking library, lori's copies become canonical. These are general networking infrastructure used by both TCP and UDP.5. UDP
Stdlib net provides
UDPSocket(actor) andUDPNotify(interface) with:ip4/ip6constructor variants)local_address(): NetAddressgetsockopt/setsockoptplus convenience wrappers)set_notify()for hot-swapping the notifierLori will need to implement UDP following its two-tier design:
Key design questions:
send()should be fallible (returning an error type) as with TCP, or whether UDP's fire-and-forget nature makes silent discard acceptableUDPSocket.writeviterates and calls_writeper element (not a single syscall like TCP's writev) — whether to match this or improve itTwo-Tier API Design
Lori's design has two levels (#7:
StandaloneTCPListener/StandaloneTCPConnection). This same pattern applies to UDP. Understanding what belongs at each tier is key to evaluating which stdlib features carry over and how.Core API (class + trait)
The power-user API. TCPConnection is a class; the user's actor implements
TCPConnectionActor+ lifecycle receiver traits. The user has full control over actor design, constructor arguments, supervision, etc. This is what lori has today.At this level:
set_notify()— protocol upgrades use internal state machines_on_accept(fd)returns aTCPConnectionActor— the application creates the connection actorsend()is fallible, returning(SendToken | SendError)Convenience API (standalone actors + notifier)
The quick-start API. Pre-built actors (
StandaloneTCPConnection,StandaloneTCPListener, and UDP equivalents) that take a notifier object, similar to stdlib's pattern. Built on top of the core API — these actors implement the traits internally and delegate to the user's notifier.At this level, a notifier pattern comes back — but it's a new, simpler notifier, not stdlib's
TCPConnectionNotify. Key differences from stdlib:Separate client and server notifiers. Lori's core has separate
ClientLifecycleEventReceiverandServerLifecycleEventReceivertraits with different callback sets (_on_connected/_on_connecting/_on_connection_failurefor clients;_on_started/_on_start_failurefor servers). The convenience layer follows suit — separate client and server notifier interfaces. Stdlib's singleTCPConnectionNotifyis a source of bugs: it exposes bothconnected()(client) andaccepted()(server) on the same interface, so implementors can easily use the wrong one (e.g., implementingconnected()on a server-side notifier whenaccepted()is what fires). Separate notifiers make this class of bug impossible — each interface only has the callbacks relevant to its role.What the new notifier should include:
connected,connecting,connection_failurestarted(stdlib'saccepted),start_failurereceived(returningBoolwithtimesfor transient yield control — see section 12),closed,throttled/unthrottled,sent/send_failed(delivery confirmations, not transforms),idle_timeouttls_ready,tls_failure— needed for STARTTLS via the convenience APIlistening,not_listening,closed,connectedfactoryexpect()as a behavior on the standalone actor — users need to set framing from withinreceived()callbacks. Theexpect()callback chain through the notifier (stdlib's middleware hook) is gone, but the ability to callexpect()on the connection is essential.local_address()/remote_address()exposed on the standalone actor — the user doesn't have access to the underlying TCPConnection classWhat the new notifier should NOT include:
sent()/sentv()data transformers — SSL is internal to TCPConnection, and the transform middleware pattern is unnecessary. (Note:sent/send_failedas delivery confirmations ARE included above — these are different from the transform hooks.)proxy_via()— belongs at a higher layer (HTTP library)auth_failed()— exists in stdlib for nested protocol wrapping, which lori eliminatesexpect()as a notifier callback — the middleware-style interception is gone.expect()is a method/behavior on the actor, not a callback on the notifier.Design questions for the convenience layer:
set_notify()for hot-swapping: The natural place for it if protocol upgrades are wanted (the standalone actor can swap the notifier while the underlying TCPConnection stays the same). But this is also the most complex thing to get right — worth considering whether internal state machines are better even at the convenience level.send()semantics: Ifsend()on the standalone actor is a behavior (fire-and-forget, matching stdlib'swrite()), it cannot return(SendToken | SendError)— behaviors returnNone. This meansSendTokentracking would need to work differently: the standalone actor assigns tokens internally and delivers them viasent(token)/send_failed(token)callbacks on the notifier, but the caller never sees the token at send time. Alternatively,send()could be afun refon the actor (preserving the return type) rather than a behavior, but this requires the caller to haverefaccess. This tension between convenience and lori's explicit-error philosophy needs resolution.connected()callback returns a notifier (like stdlib), the standalone listener creates theStandaloneTCPConnectioninternally — simpler, but the user can't customize connection parameters (read buffer size, idle timeout, etc.) per-connection. If it returns a standalone actor, the user creates it — more flexible but less convenient. A middle ground: the callback could receive the fd and connection configuration defaults, allowing the user to create a configured standalone actor.MaxSpawnonTCPListener. The standalone listener needs to expose this (constructor parameter).TCPListeneracceptsyield_after_reading/yield_after_writingand forwards them to spawned connections. If the standalone listener creates connections internally, it needs to accept and forward these parameters too.What Fundamentally Changes
6. The
ponylang/sslpackage'sSSLConnectionbecomes obsoleteSSLConnectionis aTCPConnectionNotifywrapper — it sits between stdlib's TCP actor and the user's notifier, intercepting events to encrypt/decrypt. The entiressl/netsub-package (SSLConnection,ALPNProtocolNotify, ALPN resolver interfaces) is built around this wrapping pattern.In lori, SSL is internal to
TCPConnection.SSLConnectionhas no reason to exist — this is true for both the core API and the convenience API. Even the convenience notifier doesn't need SSL wrapping because encryption happens inside the TCPConnection class before callbacks reach the notifier. Theponylang/sslpackage would need restructuring — the coreSSLclass andSSLContextstay (lori uses them), but theTCPConnectionNotify-wrapping layer and ALPN notification interfaces go away. ALPN is not currently exposed through lori — if wanted, it would need a new integration point (e.g., a lifecycle callback or notifier callback for the negotiated protocol).7. The notifier pattern changes, not disappears
Stdlib's notifier serves as both the event handler AND a middleware layer (data transforms, expect interception, proxy routing). Lori's convenience notifier would be purely an event handler — a simpler interface. The middleware responsibilities either move inside TCPConnection (SSL, expect) or are dropped entirely (proxy, data transforms).
This means migrating from stdlib to lori's convenience API is mostly a matter of:
sent()/sentv()/proxy_via()/auth_failed()/expect()callbackssend()semantics (however those surface in the convenience API)8. Silent discard vs explicit error on writes
Stdlib's
write()/writev()are behaviors that silently discard data if the connection isn't ready. Lori's coresend()returns(SendToken | SendError). How this surfaces in the convenience API is a design question (see section above), but either way the underlying behavior is different from stdlib.What Lori Should Add
9. Read yield mechanism
This is the most significant gap. Stdlib has three read flow control mechanisms working together: a configurable
yield_after_readingbyte threshold that breaks the read loop and yields, a hardcoded_max_received_called = 50cap on callback invocations per read cycle, andreceived()returningBool(with atimesparameter) for per-call transient yields. Lori has one:mute()/unmute(), which is persistent (stops reading until explicitly reversed). Stdlib also hasmute()/unmute(), so it has all of lori's flow control plus these additional mechanisms.Lori's read loop has no yield at all. The
_queue_read()call intcp_connection.ponylooks like a yield but isn't — it schedules a backup_read_again()while the current loop continues unbounded. The loop only exits when the socket would-block or errors. Under heavy inbound traffic, a single connection can monopolize the scheduler.For a standard library, scheduler fairness under load is a correctness requirement, not a nice-to-have.
The
yield_after_readingthreshold and_max_received_calledcap belong in the core API (TCPConnection class). Thereceived()returningBoolpattern belongs in the convenience notifier — in the core API, the actor controls its own read loop via mute/unmute.10. Write yield mechanism
Stdlib has
yield_after_writing— after writing this many bytes, it yields via_write_again()instead of continuing synchronously. Lori's_send_pending_writes()loops until all pending data is written or backpressure hits, with no byte-count threshold. A connection with a large outbound buffer can monopolize the scheduler on the write side just as much as heavy inbound traffic can on the read side.This deserves equal attention with the read yield gap. Belongs in the core API.
11. Configurable read buffer size
Both default to 16384, but stdlib takes it as a constructor parameter while lori hardcodes it. Applications with different read profiles (small control messages vs large file transfers) need to tune this. Belongs in the core API (TCPConnection constructor parameters). The convenience API could either expose it as a standalone actor constructor parameter or use sensible defaults.
12.
received()continuation controlStdlib's
received()returns aBool—falsebreaks out of the current read loop, reading resumes automatically next scheduler turn. Thetimes: USizeparameter tracks call count so the handler can say "stop after N messages." This is a transient yield — reading pauses momentarily then resumes without explicit action.Lori's core
_on_received()returns nothing. The alternative ismute()/unmute(), which is semantically different: persistent until explicitly reversed.This feature belongs in the convenience notifier, not the core API. In the core API, the actor IS the handler — if it wants to stop reading, it calls
mute()synchronously from within_on_received(). The return-value pattern would add nothing because the actor can already modify its own state directly. But in the convenience API, the notifier is a separate object that can't directly callmute()on the underlying TCPConnection. The return value is the notifier's way of communicating "pause reading" back to the standalone actor that owns the connection.The core API still needs the yield thresholds from sections 9–10 to prevent scheduler monopolization even when the actor doesn't mute.
13. TCP_NODELAY and socket buffer sizes
Stdlib exposes
set_nodelay(),get_so_rcvbuf/set_so_rcvbuf,get_so_sndbuf/set_so_sndbufas public methods. Lori haskeepalive()(parity with stdlib'sset_keepalive()) but not these others. Lori has the internal infrastructure (_OSSocket) but keeps it private. For a standard library, these commonly-tuned options should be part of the public API on the core TCPConnection class. The convenience standalone actors would need to re-expose them as behaviors, since the user doesn't have direct access to the underlying TCPConnection class.14. General socket option access
Stdlib exposes raw
getsockopt/setsockoptas public methods on bothTCPConnectionandUDPSocket, giving applications access to any socket option. Lori keeps this internal. A standard library should probably expose this on the core classes with documented caveats about which options could conflict with internal state.15. IPv4-only / IPv6-only variants
Stdlib has
ip4()andip6()constructors forTCPConnection,TCPListener, andUDPSocket. Lori only has dual-stack. Some environments need to force a specific protocol version. The FFI functions (pony_os_connect_tcp4, etc.) already exist in the Pony runtime — lori just needs to declare and call them. Applies to both core and convenience APIs.What Can Be Dropped
Proxy support
Stdlib's
Proxy/NoProxyandproxy_via()are thin, coupled to the old notifier's middleware role, and belong at a higher layer. Stdlib'sTCPConnectioncalls_notify.proxy_via(host, service)during connection setup — lori doesn't have this call and doesn't need it. Neither the core API nor the convenience notifier should include proxy support.sent()/sentv()transformation hooksThe transform pattern exists primarily to support
SSLConnection, which is obsolete in lori. Since SSL is internal to TCPConnection, the convenience notifier doesn't need transform callbacks. Application-level wrapping ofsend()is more explicit.auth_failed()callbackExists in stdlib for nested protocol wrapping (lower-level protocol reports auth failure to outer protocol). Since SSL is internal and lori has no protocol wrapping, this callback is structurally unnecessary — in both the core API and the convenience notifier.
Practical Migration Questions
Package naming
If lori replaces stdlib net, what's the package name? Options include keeping
lori, renaming tonetfor continuity, or something else. Code that currently doesuse "net"to getNetAddress,DNS,TCPConnection, etc. would need to change its import paths. This affects stdlib'sbackpressuredocstring,ponylang/ssl, and all third-party code.Impact within stdlib
Stdlib net is remarkably isolated. Only the
backpressurepackage references it — in its docstring example, not in code. That example would need a substantial rewrite to use lori's patterns. No other stdlib package has a code dependency on net.Impact on
ponylang/sslThe
ponylang/sslpackage needs three changes: removing theSSLConnectionwrapping layer (section 6), resolving the DNS circular dependency (section 2), and updating import paths.Impact on third-party code
Libraries built on stdlib net's actor+notifier model would need adaptation. The convenience API (standalone actors + notifier) provides a smoother migration path than the core API alone — many programs can move from stdlib's
TCPConnectionNotifyto lori's convenience notifier with relatively straightforward changes (dropping unused callbacks, adapting to new send semantics).All reactions