Skip to content

StackExchange.Redis transport seam: a transport connection's close is not reported (no ConnectionFailed, in-flight commands cancelled) #336

Open

Description

@MDA2AV

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, the ConnectionFailed event, and failing the commands awaiting a reply with the diagnostics are all in that block;
  • _ioStream is 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 RedisConnectionException to retry or fail over does not see a TaskCanceledException as a connection failure;
  • monitoring on ConnectionFailed never 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

  1. added a commit that references this issue on Oct 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions