Repository navigation
Description
Found while building ioxide.redis.stackexchange (#335), StackExchange.Redis over the ioxide transport. This is not an ioxide bug: it is in StackExchange.Redis's experimental transport seam (SER009), so any DuplexTransport hits it, SocketSet's tunnel included.
What happens
When a transport-backed connection closes - the server kills it, the network drops it - after the initial connect:
| StackExchange.Redis on its own sockets | StackExchange.Redis on a transport | |
|---|---|---|
| A command caught by the close | fails with RedisConnectionException: SocketClosed on 127.0.0.1:6379/Interactive … outstanding: 1 … |
fails with a bare TaskCanceledException |
ConnectionFailed event |
raised, with the failure details | never raised |
| Reconnect | yes | yes (ConnectionRestored fires, the next command works) |
Reproduced with a connection that kills itself (CLIENT KILL ID <own id> SKIPME no) and sends a GET right after, alternating the two transports over 4 runs: every run on its own sockets gave RedisConnectionException, every run through the ioxide tunnel gave TaskCanceledException and no ConnectionFailed.
Why
PhysicalConnection.RecordConnectionFailed (StackExchange.Redis 3.4.0, PhysicalConnection.cs:571) does all of its reporting inside:
if (_ioStream is not null || isInitialConnect) // if *we* didn't burn the pipe: flag it- bridge
OnDisconnected, theConnectionFailedevent, and failing the commands awaiting a reply with the diagnostics are all in that block; _ioStreamis set only on the socket/stream path (PhysicalConnection.Write.cs:79); a transport connection keeps_transport(PhysicalConnection.cs:345) and never sets_ioStream;- so every close after the initial connect looks like "we burned the pipe ourselves" and is not reported. The commands left awaiting a reply are cancelled when the connection is torn down instead.
The transport side follows the contract: ioxide's transport calls TransportReceiver.OnClosed once, after the socket closes, and StackExchange.Redis's TransportFeed.OnClosed calls RecordConnectionFailed(SocketClosed), which is where it stops.
Why it matters
- code that catches
RedisConnectionExceptionto retry or fail over does not see aTaskCanceledExceptionas a connection failure; - monitoring on
ConnectionFailednever sees a transport connection drop; - the connection diagnostics (outstanding commands, last read and write, state) are lost.
Likely fix (in StackExchange.Redis, untested)
On the transport path _transport plays the part _ioStream plays on the stream path - StackExchange.Redis clears it itself on its own shutdowns (PhysicalConnection.cs:486) - so:
if (_ioStream is not null || _transport is not null || isInitialConnect)reports closes the transport did not cause and keeps self-initiated ones quiet, as for streams.
Here
#335 pins it with a pending test, redis over SE.Redis: a command caught by a server-side close fails with the connection's error (in tests/Ioxide.Tests.Redis/StackExchangeTransportTests.cs). It expects RedisConnectionException and gets TaskCanceledException today; once StackExchange.Redis reports transport closes it passes, and the harness fails it so it becomes a Test. Next steps: report it upstream to StackExchange.Redis (Marc Gravell, who wrote the seam, invited transport experiments), and decide whether #335 should wait for the fix.
Activity
release: 0.20.392 (#392)