Native channels: completed public-API channels retain entries after transfer and creator disconnect
Summary
Channels created through the public engine::channels::create API retain their registry entry/access key after both endpoints have completed. They also remain after the creating worker disconnects. Reusing a completed reference still receives HTTP 101 and then a WebSocket close because the endpoint has already been taken.
This is not a claim that transferred data can be replayed. Wrong keys and unknown channel IDs were denied. The concern is bounded reclamation of terminal channel records and the absence of a supported caller-side cancellation/reclamation path for a long-lived channel gateway.
Versions and observed reproduction
- Engine release:
iii/v0.23.0, source8356a0012a22e7d5732b49328215f9ebfbbc7bfd. - Node SDK and public browser SDK:
0.20.0. - Actual browser and actual registered native worker, not a mocked Engine.
- Two channels, buffer size 2; one finite 1,048,576-byte binary transfer in each direction, in 8,192-byte application chunks.
Reproduction sequence (the equivalent sequence below describes the executed fixture; it is not a claim that a newly reduced standalone script was separately run):
- Register a creating worker and a distinct byte-processing worker.
- Call
createChannel(creator, 2)fromiii-sdk/helpers. - Pass the complementary reference to a registered function. The Node SDK resolves the reference to
ChannelReader/ChannelWriterbefore the handler runs. - Connect the public browser
ChannelWriterorChannelReaderto the other reference through a fixed native-channel-only proxy. Transfer the finite source, close the writer, consume EOF, and await the native function result. Verify length, SHA-256 and bytes. - Try a new WebSocket handshake to the same native
/ws/channels/{channel_id}?key={access_key}&dir={direction}reference. - Shut down the creator. Independently poll
engine::workers::listthrough the other worker until the original creator ID is absent. Repeat step 5. - As negative controls, try a random key for the existing ID and the real key for a random ID.
Observed for both directions:
{
"after_transfer": {"handshakeAccepted": true, "closed": true},
"after_creator_disconnect": {"handshakeAccepted": true, "closed": true},
"creatorAbsent": true,
"wrongKeyDenied": true,
"unknownChannelDenied": true
}Both binary transfers passed. The fully completed reference remained addressable after independently confirmed creator absence. All fixture resources were then removed by exact ownership. This was a small bounded reproduction, not a production leak-rate measurement.
Current main source inspection
The relevant paths are still present at 741e2a6e42984ea9cde83d7757055944926c37ad (inspection only; the runtime reproduction above used the stated release):
engine::channels::createpassesNoneasowner_worker_id, soremove_channels_by_workercannot reclaim these public-API channels.sweep_stale_channelsintentionally excludes channels whose sender and receiver have both been taken. Its comment expectsws_handlerorviews.rsto clean them up.handle_channel_socketcloses/drops the endpoints but does not remove the registry record. The read side also waits onrx.recv()without separately observing a disconnected idle reader.- The existing stale-channel test explicitly retains the both-taken state, which cannot currently distinguish active from terminal.
I did not wait for the five-minute sweeper in the runtime fixture; its inability to reclaim the both-taken case is a source-level finding, not an additional timed runtime observation.
Expected behavior / smallest proposed repair
Please confirm whether there is already a supported cancellation/reclamation API or a newer supported remedy that this misses.
Otherwise, a narrowly scoped native lifecycle repair appears necessary:
- Track actual endpoint completion/cancellation separately from whether an endpoint was taken. Reclaim a terminal channel and reject further use of its references.
- Preserve a writer-completed channel while a legitimate reader still needs to connect/drain buffered data; simply removing the record on any socket close would be incorrect.
- Observe disconnect/cancellation while an idle reader or backpressured writer is waiting. A failed duplicate attachment must not tear down the legitimate active transfer.
- Bind public creation to the authenticated creator where the worker/session contract supports it, or provide an exact authorized cancellation operation; do not trust a caller-supplied arbitrary owner ID.
- Add bounded regressions for normal drain, delayed reader, duplicate attachment, early reader disconnect, blocked writer, explicit cancellation and creator disconnect.
No Engine patch or engine-recycling workaround has been deployed. We are keeping bulk transfer separate from ordinary JSON RPC rather than increasing RPC envelope limits.
Source: iii-hq/iii