Skip to content

redis: harden connection lifecycle (Handshake enum, promise-settlement fixes, -1251 LOC) - #34829

Open
robobun wants to merge 93 commits into
mainfrom
farm/edd33c02/harden-redis-lifecycle
Open

redis: harden connection lifecycle (Handshake enum, promise-settlement fixes, -1251 LOC)#34829
robobun wants to merge 93 commits into
mainfrom
farm/edd33c02/harden-redis-lifecycle

Conversation

@robobun

@robobun robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Hardening pass over the Redis/Valkey client (src/runtime/valkey_jsc/ + src/valkey/), focused on connection lifecycle correctness. Net -1251 LOC across 20 files.

Connection lifecycle / state machine

  • Handshake state machine: replaced the is_authenticated / is_selecting_db_internal booleans on ConnectionFlags with a Handshake { AwaitingHello, SelectingDb, Ready } enum. connection_ready() now requires Handshake::Ready, and a previously reachable unreachable!() in the response dispatcher is gone.
  • Reconnect failure path: reconnect() previously called the outer JSValkeyClient wrapper that only fires the onclose callback; it never set flags.failed, never rejected queued promises, never closed the socket. Now routes through ValkeyClient::fail_with_js_value so a failed reconnect is a real client failure. The outer wrapper is renamed call_onclose_handler so the two are no longer confusable.
  • onclose on connect error: a synchronous error from connect() during auto-reconnect now fires onclose instead of being dropped.
  • on_close socket release: the event-loop socket ref is now released even when fail() throws mid-teardown.
  • on_close reconnect predicate: simplified and made the reconnect decision readable.
  • Connect errno surfaced: on_connect_error now reports the actual errno (e.g. ECONNREFUSED) instead of a generic "Connection closed".
  • TLS context error surfaced: a failed TLS context build now returns the real error instead of leaking a JsResult out of connect().
  • Auto-flusher stays unregistered when on_auto_flush made no progress, and the dead post-enqueue re-register is removed.

Promise settlement correctness

  • reject_all_pending_commands drains fully: previously used ? mid-drain, so the first Err(Terminated) returned early and dropped the already-mem::replace'd queues, leaving remaining promises pending forever. Now runs both loops to completion and returns the first error afterwards.
  • Queue iteration visits both ring halves: has_any_pending_commands / queue scans used readable_slice(0) alone, which only yields the first contiguous segment of a wrapped ring buffer. Added LinearFifo::iter() (with a unit test) and moved the scan onto it.
  • Subscribe ack only consumes its own promise: a subscribe-ack push no longer pops an unrelated in-flight promise; it only settles a promise whose meta carries SUBSCRIPTION_REQUEST.
  • Meta single source of truth: PromisePair / Entry each carried a duplicate Meta that could diverge from Promise.meta. The duplicates are deleted; divergence is now a compile error.
  • Unsubscribe rollback ordering: handler-map mutation is now deferred until send() succeeds, and the subscribe rollback only removes the handler that was just added.

Protocol / parser

  • RESPValue::read_value returns Option so Err is always a real protocol error and "need more bytes" is None. Extracted read_aggregate_header / read_n_values / read_n_entries to collapse six near-identical loops.
  • RESP3 *-1Null, _\r\n validated, and BigNumber consistently decodes as string.
  • Exhaustive SubscriptionPushMessage match: dropped the is_reply_kind string-compare hack; pmessage pattern is skipped before dispatch so the payload shape matches message.
  • ERR_REDIS_SERVER_ERROR: new error code for server -ERR / ! blob-error replies (previously mislabelled ERR_REDIS_INVALID_RESPONSE). Documented in docs/runtime/redis.mdx.
  • Pruned dead RedisError variants including the JSC-layer JSError / JSTerminated that don't belong in the protocol crate.

User-facing behaviour changes (for review)

  • psubscribe() return value: now resolves to the handler count (a number), matching subscribe()'s documented Promise<number> shape. Previously it resolved to the raw push frame {type:"psubscribe", data:[...]}. psubscribe has no .d.ts entry and is undocumented, so nothing published breaks, but it is a change.
  • RESP blob errors reject: a server ! blob-error now rejects the command promise (as simple -ERR replies already do) instead of resolving with an Error instance.
  • PUNSUBSCRIBE typo: the auto-pipeline disallow set had UNPSUBSCRIBE, so punsubscribe() was always auto-pipelined. Now matches correctly, and the check is case-insensitive so client.send("multi", ...) is caught too.
  • Varargs undefined/null now throw: previously del/mget/unlink/touch/zrem/lpush/rpush etc. silently skipped undefined/null in trailing arguments, and set/sadd/srem/hmget silently truncated at the first one (e.g. client.del('a', undefined, 'b') sent DEL a b). These now throw ERR_INVALID_ARG_TYPE synchronously. The published .d.ts never accepted undefined in these positions, so type-correct callers are unaffected.
  • Arity/type error wording: argument errors go through throw_invalid_argument_type / ERR_MISSING_ARGS with consistent codes and labels (e.g. the first arg is now labelled key, not additional arguments).

Resource ownership

  • _secure wrapped in the existing boringssl::c::OwnedSslCtx RAII type; manual SSL_CTX_free dropped.
  • Address / TLS derive Clone; the unreachable!() reconstruction in disconnect() is gone.
  • hset_impl uses JSValue::to_slice for RAII string release.
  • Fixed a WTFStringImpl leak and swallowed throw in the invoke_callbacks debug-log path.
  • SubscriptionCtx saved-flags bools folded into Option<SavedFlags>.

Cleanup

  • mod.rs flattened: the 87-line alias maze (four paths to Command, a dead index.rs, an empty ValkeyContext ZST, self-referential re-export loop) is now 5 pub mod lines + one pub use. ValkeyCommand.rs is renamed command.rs.
  • ~190 lines of dead code removed: connection_strings, Status::is_active, on_valkey_timeout, a duplicate UnwrapOrOom, reader_pos, unreachable subscriber branches, stale Zig-port comments.
  • JsTerminated<T> alias deleted: it was type JsTerminated<T> = JsResult<T>, which lied about narrowing and came with an identity narrow_terminated shim. Callers now see honest -> JsResult<()>.
  • on_valkey_* forwarders inlined; global_object takes &self.
  • valkey.classes.ts table-driven: 55 command prototypes had length: 0; now derived from a table.
  • bun_valkey Cargo.toml: dropped six unused deps (bitflags, const_format, enum-map, enumset, libc, scopeguard).
  • Extracted parse_valkey_url + new_disconnected to dedupe the two constructor sites (Bun.redis default + new RedisClient); the BunObject.rs call site is now a one-liner.

Verification

bun bd                           # builds clean
bun bd test test/js/valkey/      # 49 pass / 1065 skip / 0 fail

The docker-gated integration suites skip locally (no docker daemon); CI exercises them.

Possibly related issues

The lifecycle fixes here overlap with the symptoms in several open issues, but this PR does not include dedicated regression tests for them, so they are linked here rather than marked as fixed:

Related open PRs that touch nearby lifecycle code

This is a structural pass and may textually conflict with several targeted fixes already open: #33306, #32779, #33479, #32803, #32858, #33450, #34760. Happy to rebase over whichever land first.


[review] gate passed · iteration 3 · 21 files touched

fails on main (without fix)
ASAN without fix: 9 failed, 978 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/valkey/reliability/connection-failures.test.ts test/js/valkey/reliability/error-handling.test.ts test/js/valkey/reliability/resp-nesting-depth.test.ts test/js/valkey/valkey-incremental-scan.test.ts test/js/valkey/valkey.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (a90e1e265)

test/js/valkey/valkey.test.ts:
failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory
Redis is not enabled, skipping tests
(skip) Valkey Redis Client (tls) > (unnamed)
(skip) Valkey Redis Client (tls) > Basic Operations > should keep process alive when connecting
(skip) Valkey Redis Client (tls) > Basic Operations > should set and get strings
(skip) Valkey Redis Client (tls) > Basic Ope
... (truncated)

release without fix: 978 skipped
bun test v1.4.0-canary.1 (9a44e8d75)

test/js/valkey/valkey.test.ts:
failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory
Redis is not enabled, skipping tests
(skip) Valkey Redis Client (tls) > (unnamed)
(skip) Valkey Redis Client (tls) > Basic Operations > should keep process alive when connecting
(skip) Valkey Redis Client (tls) > Basic Operations > should set and get strings
(skip) Valkey Redis Client (tls) > Basic Operations > should test key existence
(skip) Valkey Redis Client (tls) > Basic Operations > should increment and decrement counters
(skip) Valkey Redis Client (tls) > Basic Operations > should increment by specified amount with INCRBY
(skip) Valkey Redis Client (tls) > Basic Operations > should increment by float amount with INCRBYFLOAT
(skip) Valkey Redis Client (tls) > Basic Operations > should decrement by specified amount with DECRBY
(skip) Valkey Redis Client (tls) > Basic Operations > should rename a key with RENAME
(skip) Valkey Redis Client (tls) > Basic Operations > should rename a key with RENAME overwr
... (truncated)
passes on PR (with fix)
ASAN with fix: 978 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/valkey/reliability/connection-failures.test.ts test/js/valkey/reliability/error-handling.test.ts test/js/valkey/reliability/resp-nesting-depth.test.ts test/js/valkey/valkey-incremental-scan.test.ts test/js/valkey/valkey.test.ts
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
bun test v1.4.0 (a90e1e265)

test/js/valkey/valkey.test.ts:
failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running: dial unix /var/run/docker.sock: connect: no such file or directory
Redis is not enabled, skipping tests
(skip) Valkey Redis Client (tls) > (unnamed)
(skip) Valkey Redis Client (tls) > Basic Operations > should keep process alive when connecting
(skip) Valkey Redis Client (tls) > Basic Operations > should set and get strings
(skip) Valkey Redis Client (tls) > Basic Ope
... (truncated)

release with fix: 978 skipped
$ bun scripts/build.ts --profile=release
info: syncing channel updates for nightly-2026-05-06-x86_64-unknown-linux-gnu
info: latest update on 2026-05-06 for version 1.97.0-nightly (e95e73209 2026-05-05)
info: component rust-src is up to date
info: checking for self-update (current version: 1.29.0)
[configured] bun-profile → bun (stripped) in 695ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/77] gen ErrorCode+*.h
[2/31] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 244 extern-C blocks audited
[3/31] gen ZigGeneratedClasses.{cpp,h,rs}
Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts
  - ResolveMessage (13 fields)
  - BuildMessage (10 fields)
Found 1 classes from /workspace/bun/src/runtime/api/Archive.classes.ts
  - Archive (4 fields, 1 class fields)
Found 2 classes from /workspace/bun/src/runtime/api/BunObject.classes.ts
  - ResourceUsage (8 fields)
  - Subprocess (20 fields)
Found 1 classes from /workspace/bun/src/runtime/api/cron.classes.ts
  - CronJob (5 fields)
Found 3 classes from /workspace/bun/src/runtime/api/filesystem_router.classes.ts
  - FileSyst
... (truncated)
diff hotspot
Cargo.lock                                         |    6 -
 docs/runtime/redis.mdx                             |    1 +
 src/collections/linear_fifo.rs                     |   29 +
 src/jsc/bindings/ErrorCode.ts                      |    4 +-
 src/runtime/api/BunObject.rs                       |   17 +-
 .../valkey_jsc/{ValkeyCommand.rs => command.rs}    |  158 ++-
 src/runtime/valkey_jsc/index.rs                    |   21 -
 src/runtime/valkey_jsc/js_valkey.rs                | 1385 +++++++++---------
 src/runtime/valkey_jsc/js_valkey_functions.rs      | 1468 +++++++-------------
 src/runtime/valkey_jsc/mod.rs                      |   81 +-
 src/runtime/valkey_jsc/protocol_jsc.rs             |  125 +-
 src/runtime/valkey_jsc/valkey.classes.ts           |  728 +++-------
 src/runtime/valkey_jsc/valkey.rs                   |  909 +++++-------
 src/valkey/Cargo.toml                              |    6 -
 src/valkey/lib.rs                                  |    7 +-
 src/valkey/valkey_protocol.rs                      |  392 +++---
 .../valkey/reliability/connection-failures.test.ts |   30 +-
 test/js/valkey/reliability/error-handling.test.ts  |    2 +-
 .../valkey/reliability/resp-nesting-depth.test.ts  |   44 +-
 test/js/valkey/valkey-incremental-scan.test.ts     |    9 +-
 test/js/valkey/valkey.test.ts                      |   12 +-
 21 files changed, 2133 insertions(+), 3301 deletions(-)

gate history · 7 passed · 1 rejected · iteration 3

evidence per changed file
file                                           reads  edits  tests
Cargo.lock                                         0      0      0
docs/runtime/redis.mdx                             0      0      0
src/collections/linear_fifo.rs                     1      1      0
src/jsc/bindings/ErrorCode.ts                      1      1      0
src/runtime/api/BunObject.rs                       0      0      0
src/runtime/valkey_jsc/command.rs                  1      1      0
src/runtime/valkey_jsc/index.rs                    0      0      0
src/runtime/valkey_jsc/js_valkey.rs               14      8      0
src/runtime/valkey_jsc/js_valkey_functions.rs      0      0      0
src/runtime/valkey_jsc/mod.rs                      0      0      0
src/runtime/valkey_jsc/protocol_jsc.rs             0      0      0
src/runtime/valkey_jsc/valkey.classes.ts           0      0      0
src/runtime/valkey_jsc/valkey.rs                   7      4      0
src/valkey/Cargo.toml                              0      0      0
src/valkey/lib.rs                                  0      0      0
src/valkey/valkey_protocol.rs                      0      0      0
(+ 5 more files)

robobun added 30 commits July 20, 2026 10:10
Comment thread src/runtime/valkey_jsc/js_valkey.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/valkey_jsc/valkey.rs (1)

1234-1244: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Do not complete the connection before SELECT succeeds.

mark_connected() resolves the JS connection promise while SELECT is pending, and the SELECT path marks the handshake ready before validating its response. A command queued from that resolved promise can auto-flush after a failing SELECT (for example, SELECT 99) and execute against the default database before its promise is rejected.

  • src/runtime/valkey_jsc/valkey.rs#L1234-L1244: defer Status::Connected and on_valkey_connect() until database selection has succeeded.
  • src/runtime/valkey_jsc/valkey.rs#L1003-L1025: validate +OK before setting Handshake::Ready, then complete the deferred connect flow.

Add a failing-SELECT-followed-by-write regression test. As per coding guidelines, “Every behavioral change must include an automated regression test in the same change.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/valkey_jsc/valkey.rs` around lines 1234 - 1244, The connection
lifecycle currently resolves before database selection is confirmed. In
src/runtime/valkey_jsc/valkey.rs#L1234-L1244, update mark_connected so
Status::Connected and on_valkey_connect are deferred when selecting a database;
in src/runtime/valkey_jsc/valkey.rs#L1003-L1025, validate the SELECT response is
+OK before setting Handshake::Ready, then complete the deferred connection flow
only on success. Add a regression test covering a failing SELECT followed by a
write.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/runtime/valkey_jsc/valkey.rs`:
- Around line 905-908: Condense the invariant comment above the subscription
acknowledgment handling to three lines or fewer, while preserving the
distinction that only plain subscribe acknowledgments enter subscriber mode and
pattern/shard acknowledgments merely settle their promises.

In `@test/js/valkey/reliability/resp-nesting-depth.test.ts`:
- Around line 364-386: Extend the reliability tests beside the existing
psubscribe() case with an equivalent ssubscribe() acknowledgment followed by
client.get("k"). Use the SSUBSCRIBE RESP acknowledgment and assert it returns 0,
then verify get still returns "value", preserving the same setup and cleanup
pattern to cover the sibling entry point.

---

Outside diff comments:
In `@src/runtime/valkey_jsc/valkey.rs`:
- Around line 1234-1244: The connection lifecycle currently resolves before
database selection is confirmed. In
src/runtime/valkey_jsc/valkey.rs#L1234-L1244, update mark_connected so
Status::Connected and on_valkey_connect are deferred when selecting a database;
in src/runtime/valkey_jsc/valkey.rs#L1003-L1025, validate the SELECT response is
+OK before setting Handshake::Ready, then complete the deferred connection flow
only on success. Add a regression test covering a failing SELECT followed by a
write.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7725609b-f0e6-4574-81d2-4353b96e294f

📥 Commits

Reviewing files that changed from the base of the PR and between 836f27d and 77fc987.

📒 Files selected for processing (4)
  • src/runtime/valkey_jsc/command.rs
  • src/runtime/valkey_jsc/js_valkey.rs
  • src/runtime/valkey_jsc/valkey.rs
  • test/js/valkey/reliability/resp-nesting-depth.test.ts

Comment thread src/runtime/valkey_jsc/valkey.rs Outdated
Comment thread test/js/valkey/reliability/resp-nesting-depth.test.ts Outdated
@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Re the outside-diff finding about mark_connected() resolving connectionPromise before SELECT succeeds: that sequencing is unchanged from main. On main the HELLO-response handler sets status = Connected and calls on_valkey_connect() (which resolves connectionPromise) at valkey.rs:995-999 / 1035-1039, while the SELECT response is handled afterwards at 1062-1091 via is_selecting_db_internal. This PR folds those two bools into the Handshake enum and extracts mark_connected(), but preserves the same ordering; connection_ready() still requires Handshake::Ready, so queued user commands are not written until SELECT completes. Changing when connectionPromise resolves relative to SELECT would be a user-visible timing change and is out of scope for this hardening pass.

Comment thread src/runtime/valkey_jsc/valkey.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/valkey_jsc/valkey.rs (1)

419-424: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Gate auto-flushing on connection_ready().

mark_connected() sets Status::Connected before SELECT succeeds. A command queued from the connection callback can therefore be auto-flushed while Handshake::SelectingDb; if SELECT fails, that command may execute against the server’s previous database. Keep it queued until Handshake::Ready, and add a regression covering a failed SELECT plus an immediately queued pipelineable write.

Proposed fix
-        if self.status != Status::Connected {
+        if !self.connection_ready() {
             self.auto_flusher.registered.set(false);
             return false;
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/valkey_jsc/valkey.rs` around lines 419 - 424, Update the
auto-flusher registration decision in the surrounding flush logic to require
connection_ready() in addition to pending pipelineable work and remaining queue
entries, so commands remain queued throughout Handshake::SelectingDb and only
flush after Handshake::Ready. Add a regression test covering a failed SELECT
followed by an immediately queued pipelineable write, verifying the write is not
executed against the prior database.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/runtime/valkey_jsc/valkey.rs`:
- Around line 419-424: Update the auto-flusher registration decision in the
surrounding flush logic to require connection_ready() in addition to pending
pipelineable work and remaining queue entries, so commands remain queued
throughout Handshake::SelectingDb and only flush after Handshake::Ready. Add a
regression test covering a failed SELECT followed by an immediately queued
pipelineable write, verifying the write is not executed against the prior
database.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6d6362e7-bed2-4878-bc48-580d9c3cf76d

📥 Commits

Reviewing files that changed from the base of the PR and between 77fc987 and edb5524.

📒 Files selected for processing (2)
  • src/runtime/valkey_jsc/valkey.rs
  • test/js/valkey/reliability/resp-nesting-depth.test.ts

Comment thread src/runtime/valkey_jsc/js_valkey_functions.rs

@Jarred-Sumner Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robobun fix conflicts

Resolve conflicts in js_valkey.rs:
- new_disconnected() now uses RefCountedTimer::new() from #34760
- clone_without_connecting keeps the new_disconnected() form
- on_valkey_reconnect uses reconnect_timer.arm() from #34760
- destroy path keeps timer ref_held asserts from #34760; ssl_ctx is
  OwnedSslCtx so the manual SSL_CTX_free block is dropped
- restore core::cell::Cell import for RefCountedTimer.ref_held
@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

Merged main and resolved conflicts in js_valkey.rs (9a44e8d): new_disconnected() now constructs RefCountedTimer from #34760, the reconnect path uses .arm(), and the destroy path keeps the ref_held asserts (the manual SSL_CTX_free is dropped since ssl_ctx is RAII via OwnedSslCtx). Builds clean and 53 valkey tests pass locally.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three regressions I flagged earlier (psubscribe subscriber-mode, unsettled connectionPromise on reconnect error, send('SUBSCRIBE') leaky-proxy gate) are addressed in a1df82d / 77fc987 / edb5524, and no new issues surfaced after the main merge. Deferring to a human for the explicitly-listed user-facing behaviour changes (psubscribe return shape, blob-error reject, undefined/null varargs now throw, PUNSUBSCRIBE auto-pipeline fix) and because the docker-gated integration suite (~1065 tests) doesn't run locally.

What was reviewed:

  • Handshake enum vs. old bool pair — connection_ready() / mark_connected() ordering preserved from main; SELECT-before-resolve timing unchanged.
  • reject_all_pending_commands full-drain and LinearFifo::iter() ring-wrap coverage.
  • sub_count > 0 gate for add_subscription() covers all three subscribe entry points (psubscribe / send SUBSCRIBE / send SSUBSCRIBE), with mock-server tests.
  • OwnedSslCtx RAII replacing manual SSL_CTX_free; on_close socket-ref release ordering.
Extended reasoning...

Overview

Net -1251 LOC hardening pass over the Redis/Valkey client: replaces is_authenticated/is_selecting_db_internal bools with a Handshake enum, fixes several promise-settlement leaks (reject_all_pending_commands early-return, wrapped-ring readable_slice(0) scans, subscribe-ack popping unrelated promises), reworks reconnect failure to actually fail the client, and collapses ~1400 lines of duplicated per-command boilerplate in js_valkey_functions.rs into shared require_arg/collect_varargs helpers plus meta-parametrized macros. Also flattens the mod.rs alias maze, prunes dead RedisError variants and unused Cargo deps, moves _secure to RAII OwnedSslCtx, and rewrites valkey.classes.ts as a table.

Security risks

None identified. The RESP parser changes (read_blob, read_aggregate_header, expect_crlf) tighten bounds handling — the buffer.len() - pos < len check avoids the pos + len wrap the old code had. TLS context handling moves from a raw *mut SslCtx + manual SSL_CTX_free to OwnedSslCtx RAII, and the reject_unauthorized path is unchanged.

Level of scrutiny

High. This is a large structural refactor of a network client's connection state machine with multiple deliberate user-facing behaviour changes that the PR description explicitly flags for review. The memory-safety surface (JSC promise strong refs, SSL_CTX ownership, ring-buffer iteration, socket keep-alive refs across reconnect) is non-trivial, and the primary test coverage (~1065 docker-gated integration tests) only runs in CI, not locally.

Other factors

  • I flagged three regressions across earlier revisions; all were fixed with targeted commits and mock-server tests. This run found nothing new.
  • The PR textually conflicts with 7+ open lifecycle-adjacent PRs; author has already rebased over #34760.
  • Several open issues (#27861, #18895, #22812, #33103) plausibly overlap but are not claimed as fixed with dedicated regression tests — a maintainer should decide whether to require those before merge.
  • The get_reconnect_delay() computation is now dead in on_socket_closed() (delay is applied via on_valkey_reconnect's timer, but the delay_ms == 0 early-bail from the old path is gone) — worth a quick sanity check that reconnect backoff still behaves as before, though this looks intentional.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/runtime/valkey_jsc/js_valkey.rs (1)

306-314: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Keep this ownership comment within three lines.

The nine-line doc block exceeds the repository comment limit; retain only the ref-count and offset invariants.

Proposed reduction
-/// Intrusive [`EventLoopTimer`] slot that owns one strong ref on
-/// [`JSValkeyClient`] while armed. `ref_held` mirrors the `ref_()` taken in
-/// [`arm`] so [`disarm`] and [`take_fire_ref`] release it exactly once even
-/// when the fire/close/reconnect paths re-enter each other.
-///
-/// [`EventLoopTimer`]: Timer::EventLoopTimer
-/// [`arm`]: Self::arm
-/// [`disarm`]: Self::disarm
-/// [`take_fire_ref`]: Self::take_fire_ref
+/// Owns one client ref while armed; `ref_held` balances `arm`, `disarm`, and firing.
+/// The timer must remain first so dispatch can recover the enclosing client.

As per coding guidelines, “Keep code comments to three lines or fewer.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/valkey_jsc/js_valkey.rs` around lines 306 - 314, Shorten the
documentation above EventLoopTimer to no more than three lines, retaining only
the invariant that the armed slot owns one strong JSValkeyClient reference and
that ref_held ensures disarm and take_fire_ref release it exactly once.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/runtime/valkey_jsc/js_valkey.rs`:
- Around line 306-314: Shorten the documentation above EventLoopTimer to no more
than three lines, retaining only the invariant that the armed slot owns one
strong JSValkeyClient reference and that ref_held ensures disarm and
take_fire_ref release it exactly once.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ba04fa8f-ec76-471b-a440-f5f491dc1c28

📥 Commits

Reviewing files that changed from the base of the PR and between edb5524 and a90e1e2.

📒 Files selected for processing (2)
  • src/jsc/bindings/ErrorCode.ts
  • src/runtime/valkey_jsc/js_valkey.rs

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No new issues found this pass — the earlier inline findings (psubscribe/send-SUBSCRIBE subscriber-mode regression, unrejected connectionPromise on reconnect-connect error, varargs throw semantics) are all addressed. Deferring to a human reviewer given the scope: this is a ~-1250 LOC structural rewrite of the Valkey client's connection lifecycle, promise-settlement paths, and RESP parser, plus several user-facing behaviour changes (psubscribe() return shape, blob-error reject-vs-resolve, undefined/null varargs now throw, PUNSUBSCRIBE auto-pipeline gate) that warrant a maintainer's sign-off.

What was reviewed:

  • Handshake enum / connection_ready() gating vs. the old bool pair — queued user commands still wait for Ready.
  • reject_all_pending_commands full-drain and LinearFifo::iter() covering both ring halves.
  • Subscribe-ack routing after the sub_count > 0 gate; send('SUBSCRIBE'/'PSUBSCRIBE'/'SSUBSCRIBE') no longer flips subscriber mode (mock-server tests cover all three).
  • on_socket_closed reconnect predicate and the reconnect-error path now rejecting the cached connectionPromise.
Extended reasoning...

Overview

This PR restructures src/runtime/valkey_jsc/ and src/valkey/: replaces the is_authenticated/is_selecting_db_internal bools with a Handshake enum, rewrites on_close/on_connect_error into a shared on_socket_closed, fixes reject_all_pending_commands to drain fully, adds LinearFifo::iter() for wrapped-ring scans, changes RESPValue::read_value to return Option so Err is always a real protocol error, collapses ~140 hand-written prototype methods onto shared cmd_*! macros / collect_varargs, table-drives valkey.classes.ts, and prunes ~190 lines of dead code plus six unused Cargo deps. It also introduces ERR_REDIS_SERVER_ERROR, wraps _secure in OwnedSslCtx, and reorders subscribe/unsubscribe rollback so handler-map mutation happens only after send() succeeds.

Security risks

Low. TLS handling is touched (SSL_CTX moved to RAII OwnedSslCtx, TLS-context build errors now surfaced), but the verification/hostname-check path in on_handshake_ is unchanged in substance. URL parsing is extracted verbatim into parse_valkey_url. No new untrusted-input parsing beyond the existing RESP parser, whose bounds checks are tightened (read_blob subtracts instead of adding to avoid pos + len wrap).

Level of scrutiny

High. The Valkey client's connection lifecycle and in-flight promise bookkeeping are exactly the class of code where a subtle ordering bug produces forever-pending promises or response-mismatch (issue #27861). Three earlier review rounds on this PR each surfaced a real regression that was then fixed, which is evidence the change surface is risk-bearing. The docker-gated integration suite skips locally; CI must be green on those lanes before merge.

Other factors

  • User-facing behaviour changes are the main reason to defer: psubscribe() now resolves to a number instead of the raw push frame; RESP3 blob-errors now reject instead of resolving with an Error; undefined/null in varargs positions now throw where they previously skipped/truncated; PUNSUBSCRIBE is now correctly excluded from auto-pipelining. These are defensible but need a maintainer's call.
  • All prior inline threads (mine and CodeRabbit's) are resolved; the mark_connected-before-SELECT ordering was confirmed as pre-existing behaviour and left unchanged.
  • The PR conflicts textually with several open targeted-fix PRs (#33306, #32779, etc.) — merge ordering is a human decision.
  • coerce_redis_arg now passes allow_file = true (which rejects file-backed blobs) for the generic path, changing e.g. set(key, Bun.file(...)) behaviour; the comment explains why, but it's another semantic shift worth a glance.

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

CI build 76661: all valkey/redis test files are green across every lane. The only [new] failure is test/js/bun/http/bun-server.test.ts ("should not use 100% CPU when websocket is idle", measured 2.69% vs its threshold) on a single darwin-aarch64 lane, which this PR does not touch (git diff origin/main..HEAD --name-only has no http/server/websocket paths). The remaining seven failures are [flaky] (passed on retry). The diff is ready from the valkey side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants