Skip to content

redis: drop ioxide.redis - #337

Merged
MDA2AV merged 1 commit into
mainfrom
chore/drop-ioxide-redis
Oct 12, 2026
Merged

MDA2AV merged 1 commit into
mainfrom
chore/drop-ioxide-redis

Conversation

@MDA2AV

@MDA2AV MDA2AV commented Oct 12, 2026

Copy link
Copy Markdown
Owner

Drops ioxide.redis. It is weight the project does not want to carry: a whole Redis client to maintain, with five open fixes against it, when StackExchange.Redis can run on the reactor's ring instead - #335, ioxide.redis.stackexchange.

Removed

  • src/clients/ioxide.redis (the package) and its CI pack step.
  • Playground/Clients/Redis, its README row, and Env.OverrideRedis, which only it used.
  • RedisTests and RedisHandlers. The Redis suite stays - empty, still in the CI loop with its redis sidecar - for the transport's tests in redis.stackexchange: StackExchange.Redis with its connections on the reactor's ring (experimental) #335.
  • Its bench run in bench/run.sh and its row in bench/samples.tsv.
  • Its pane on the docs page (tab, menu entry, styles, generator entry), and the "a Redis command" in one of the page's cards.
  • docs/learn/redis.html and docs/learn/dev-redis.html, with every link to them (each learn page's menu, how-ioxide-does-h3.html, the overview, and the next-page links from the Postgres pages).
  • The passing Redis mentions in the Http3/Nghttp3Buffered sample's comments (pane regenerated).

The solution builds, and the Redis suite runs with no tests. src/ioxide/IRingHost.cs still names Redis among the clients a ring can carry; it is core, and still true with #335.

Not in this PR

ioxide.redis is weight the project does not want to carry: a whole Redis client to maintain - five
open fixes against it - when StackExchange.Redis can run on the reactor's ring instead (#335,
ioxide.redis.stackexchange).

Removed: the package, its Playground sample (Clients/Redis) and the Env helper only it used, its
tests (the Redis suite stays, empty, for the transport's tests), its CI pack step, its bench run and
row, its pane on the docs page, and docs/learn's two Redis pages with their links. The passing Redis
mentions in the Http3/Nghttp3Buffered sample's comments go too. The versions already on nuget.org
are untouched.
@MDA2AV
MDA2AV merged commit 0ec3549 into main Oct 12, 2026
1 check passed
MDA2AV added a commit that referenced this pull request Oct 12, 2026
…d recv has returned (#292)

* pg, redis: a timed-out connection is shut down, and freed only once its armed recv has returned

A command timeout disposed the connection while its reader was parked on a ring recv: close(fd),
then the buffers freed. close() does not end a socket an io_uring op still holds, so the socket
stayed ESTABLISHED and the recv stayed armed until the server finally answered, and the kernel then
wrote that reply into the freed receive buffer - which the allocator had already handed to the
replacement connection (same address, same fd number). Pick's dispose of a connection broken by one
loop could hit the other loop's op the same way.

Dispose now shuts the socket down while a loop is parked on an op, which completes it, and the loop
that exits last closes the fd and frees the buffers. Dispose marks the connection broken, so it
takes no new commands in between.

"pg:" and "redis: a timed-out connection's socket ends at the timeout, not when the late reply
comes" time a command out on the sidecar (pg_sleep(30), BLPOP 30) and require the pool connection
that timed out to leave ESTABLISHED within 10 s, while its sibling stays ESTABLISHED and the pool
still answers. Confirmed both ways: both fail on the unmodified source (still ESTABLISHED after
10 s) and pass with the fix.

* pg-only: the Redis half goes with ioxide.redis (#337)
MDA2AV added a commit that referenced this pull request Oct 12, 2026
…ing forever (#318)

* pg, redis: a connect and its startup or handshake end at CommandTimeoutMs instead of waiting forever

Nothing bounded opening a connection. A server that accepted the TCP connection and never answered
the pg startup, or a redis AUTH or SELECT, held ConnectAsync forever. A connect whose SYN was never
answered waited out the kernel's retries, about two minutes. The pool coalesces opens: the slow
path starts one only when none is in flight, and the ticker replenishes only then too. So one open
that never finished held off every later one, and a pool left without a live connection parked
its queries for good, even after the server recovered.

ConnectAsync now arms a ring timeout of CommandTimeoutMs next to the open. If the open is still
running when it fires, it shuts the socket down, which completes whatever ring op the open is
parked on (connect, send or recv). The open then fails with an error naming the step that stalled,
and the pool's existing failure path frees the slot for the next attempt. A finished or failed open
disarms it first, so a late expiry never touches an fd that may have been reused. CommandTimeoutMs
= 0 still disables it. Sweep cannot do this with what the pool holds: an open in progress is not in
_connections, and _opening is only a count.

New tests, PgOpenTests and RedisOpenTests, with a 1000 ms timeout. A FakeServer that accepts and
never answers must fail the pg startup and the redis handshake (Database 1, so the open sends a
SELECT). A listener with a full backlog, which drops the SYN, must fail the pg connect step. For
each client, a pool of one whose first open is never answered must connect on its next attempt:
the FakeServer forwards later connections to the real server through a new harness Forwarder.
Confirmed both ways: all five time out (10 s) on f298af0, and pass with the fix.

* pg-only: the Redis half goes with ioxide.redis (#337)
MDA2AV added a commit that referenced this pull request Oct 12, 2026
…ever its index holds afterwards (#313)

* pg, redis: the timeout sweep removes the connection that timed out, not whatever its index holds afterwards

Sweep failed a timed-out connection's waiters through CheckTimeout and then removed and disposed
_connections[i]. The waiters resume inline, so a handler that queries again picks from the pool
inside the sweep, and Pick evicts the timed-out connection, which is broken, shifting the list.
Sweep then disposed whatever had moved into index i: a live connection holding the queries just
placed on it, which failed with "send failed: errno 9". When the timed-out connection was the
last entry, the index was past the end and the ticker faulted, skipping the rest of that sweep.

Sweep now holds the connection it checked, removes it by reference (a no-op if a Pick already
did), and clamps the index to the list's current length. No new state.

New tests, PgSweepTests and RedisSweepTests: a pool of 2 with a 1000 ms command timeout, a
pg_sleep(30) or BLPOP for 30 s on index 0, and a handler that issues two more commands when the
timeout fails it. The handler records that the timed-out connection is still listed when it runs,
which shows it ran inline in the sweep. Confirmed both ways: on f298af0 both commands fail with
errno 9 (pg and redis), and pass with the fix. With the sleep moved to index 1, f298af0 logs
"ticker faulted: Index was out of range" and the fix does not.

* pg-only: the Redis half goes with ioxide.redis (#337)
@MDA2AV MDA2AV mentioned this pull request Oct 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant