Skip to content

feat(csc): full-duplex reader-miss coalescing + invalidation batching - #3965

Open
ndyakov wants to merge 45 commits into
feature/csc-refresh-and-miss-coalescingfrom
ndyakov/csc-coalesce-modes
Open

feat(csc): full-duplex reader-miss coalescing + invalidation batching#3965
ndyakov wants to merge 45 commits into
feature/csc-refresh-and-miss-coalescingfrom
ndyakov/csc-coalesce-modes

Conversation

@ndyakov

@ndyakov ndyakov commented Aug 13, 2026

Copy link
Copy Markdown
Member

Full-duplex reader-miss coalescing + invalidation batching

Builds on the CSC refresh-on-invalidate + base reader-miss coalescing PR
(feature/csc-refresh-and-miss-coalescing). Two additions, both config-driven.

1. Full-duplex miss coalescing (reader-miss path)

Concurrent cache misses of the same key are already deduped to a single fetch
via a per-key reservation (one owner fetches; the rest block on it and share the
result). This PR adds the engine that dispatches the owners' fetches: a held
tracked connection with a writer + reader goroutine pair
that pipelines
reserved misses — commands stream out while replies stream back.

Misses are caller-blocking (a real request waits on every fetch), so the
engine is latency-first:

  • a lone miss is written immediately — batching is opportunistic (packs only
    what is already queued), never waited-for;
  • new misses stream out while earlier replies are in flight: ~1 RTT per miss,
    no batch phase-lock, no pool Get on the hot path;
  • the session releases its connection after an idle grace and re-acquires on the
    next miss, so an idle coalescer holds zero connections;
  • the session's reader drains RESP3 pushes (invalidations, maintenance) on the
    held connection even while idle, and returns the connection to the pool for
    handoff/lifetime/pool-hook processing (idle grace, recycle age bounded by the
    connection's remaining ConnMaxLifetime, ShouldHandoff checks).

Ordering is preserved. Each caller blocks on its own request's completion,
so a goroutine never issues its next command until this value is in hand — the
engine only overlaps independent callers' fetches on the wire.

Earlier engine variants ("workers": N pooled connections with half-duplex
batches; "pinned": a benchmark prototype) were removed during review: the
per-batch round trip added tail latency to exactly the path where a caller is
waiting, and the batching advantage is preserved by the writer's opportunistic
packing. One engine, no tuning surface.

2. Invalidation batching

Coalesce invalidation-driven cache deletes within a configurable window instead
of one delete per push frame — smooths bursty invalidation churn. Background
traffic is batching-first by design (nobody waits on it): windowed, deduped,
with the delete queue preserved across batcher rebuilds and dropped on
FLUSHDB/FLUSHALL (a full flush supersedes queued per-key deletes).

Config (no environment variables)

New Options fields (flat ClientSideCache*, matching
ClientSideCacheRefreshOnInvalidate), mirrored in UniversalOptions:

  • ClientSideCacheCoalesceMisses bool — enables the coalescer (requires the
    built-in LocalCache; ignored for custom Cache implementations).
  • ClientSideCacheInvalidationBatchWindow time.Duration — 0 (default) applies
    invalidations inline; a nonzero window batches them (set it no larger than the
    cache MaxStaleness). Shared-handler clients fold windows strictest-wins.

Observability rides the client's normal telemetry: coalesced misses fire the
otel operation-duration and error callbacks like any other command path (no
separate stats API — the engine's internal counters are test-only).

Hardening (from review)

The review rounds hardened the engine's lifecycle and edge behavior, including:
single-flight token settlement on every path (no caller ever hangs, no
reservation leaks); a CAS ownership interlock so a ctx-cancelled caller's Cmder
is never written concurrently; Close interruption of blocked acquisitions and
socket reads (bounded drain, then conn close); retry-uncached fallback when CSC
is disabled mid-miss; GC cleanup for clients dropped without Close;
WithTimeout clones sharing the coalescer; and held-connection probe safety
(no deadline clobbering of concurrent I/O; opaque-transport idle-drain fallback).


Note

High Risk
Large changes to CSC concurrency, connection holding, invalidation timing, and push draining on the hot path; incorrect behavior could cause stale cache, hangs, or connection desync, though coverage is extensive.

Overview
Adds config-driven client-side cache (CSC) improvements: full-duplex reader-miss coalescing, windowed invalidation batching, and extensive lifecycle/correctness hardening. Env-var gates for coalescing/refresh are removed in favor of Options / UniversalOptions fields.

Miss coalescing (ClientSideCacheCoalesceMisses, LocalCache only) replaces the prototype worker pool with a held tracked connection using concurrent writer/reader goroutines: misses stream out while replies return (~1 RTT), idle sessions release the conn after a grace period, and the reader drains invalidations on the held socket. Includes CAS-based Cmder ownership on cancel, wire snapshots at enqueue, errCSCRetryUncached when CSC stops mid-miss, GC cleanup for clients dropped without Close, and WithTimeout clones that bypass the owner’s coalescer when timeouts differ.

Invalidation batching (ClientSideCacheInvalidationBatchWindow) defers per-key deletes to a background batcher with epoch-based supersede on full flush, strictest-window-wins across shared handlers, and refresh-queue stacking so closing one client does not break siblings.

Other notable changes: refresh refetches use the main tracked pool (not pipeline); push drains use longer hard deadlines and HasBufferedData; pool checkForData clears stale read deadlines; FT.HYBRID RESP3 map parsing fixed; proto.Scan copies escaping types; SECURITY.md + disclosure updates; minor CI/doc bumps.

Reviewed by Cursor Bugbot for commit d33c190. Bugbot is set up for automated code reviews on this repo. Configure here.

ndyakov and others added 3 commits August 4, 2026 12:14
The buffered-push-data notice in isHealthyConn fired unconditionally.
  With client-side caching every tracked write parks an invalidate frame
  on idle pooled conns, so at default log level the line floods the log
  on nearly every pool Get under write-heavy workloads. It documents the
  normal healthy path, not a problem - gate it behind LogLevelDebug like
  the equivalent notices in maintnotifications.
…nvalidation batching

Builds on the CSC refresh-on-invalidate + base miss-coalescing PR. Adds:

- Coalescing MODES for the reader-miss path, selected by config:
  * "workers" (default): a small pinned worker pool fetches reserved misses.
  * "fullduplex": one held connection with a writer + reader goroutine pair
    pipelines the reserved misses (concurrent, replies streamed back). Ordering
    is preserved: the miss-coalescer's reservation dedups concurrent misses of
    the same key to a single fetch, and each caller blocks on its own request's
    completion — the caching client is blocking per goroutine, so no goroutine
    ever sees its own reads reordered; full-duplex only overlaps independent
    goroutines' fetches on the wire.
- Invalidation batching: coalesce invalidation-driven cache deletes within a
  configurable window instead of one delete per push.

Config (no environment variables): new AutoPipeline-free Options fields —
ClientSideCacheCoalesceMisses (enable), ClientSideCacheCoalesceMode
("workers"/"fullduplex"), ClientSideCacheCoalesceWorkers, and
ClientSideCacheInvalidationBatchWindow. Prototype-only read-path telemetry
(READPATH_LOG + LocalCache stat counters) removed; no stats surface added.

Measured on a 50ms-RTT WAN proxy under invalidation churn, the coalescing fixes
turn published v9.22.0's miss-stampede collapse (p99 up to ~1.1s, throughput
floored) into ~5x throughput at ~1 RTT p99, matching rueidis; the full-duplex
mode gives the tighter mid-range tail at fewer connections. See
AP_CSC_TWOCLIENT_VS_RUEIDIS.md.

Depends on: feature/csc-refresh-and-miss-coalescing.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: eb8681e82b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread csc_miss_coalesce_modes.go
Comment thread csc_miss_coalesce_modes.go
Comment thread csc_miss_coalesce_modes.go
Comment thread csc_miss_coalesce_modes.go Outdated
Comment thread csc_integration.go Outdated
Comment thread options.go
Comment thread csc_miss_coalesce_modes.go Outdated
Comment thread csc_miss_coalesce_modes.go Outdated
Comment thread csc_integration.go
- lint (the only red CI check): drop the redundant `chan struct{}` type from the
  recycle declaration (ST1023) and //nolint:unused the deliberately-off, measured
  cscRefreshCooldown knob.
- Reject the "pinned" PROTOTYPE engine from the public ClientSideCacheCoalesceMode
  option: it holds a connection with no idle invalidation drain and can serve
  stale values (cursor HIGH). It now falls back to "workers" and is reachable
  only via an internal benchmark hook (cscForcePinned).
- Pass the new CSC miss-coalescing / invalidation-batching knobs through
  UniversalOptions.Simple() (ClientSideCacheRefreshOnInvalidate, CoalesceMisses,
  CoalesceMode, CoalesceWorkers, InvalidationBatchWindow) so UniversalClient
  users can enable them (codex P2).
- Give the invalidation batcher a stop path: it is stopped and cleared when the
  last user releases the binding (releaseLocked), and ensureBatcher refuses to
  start one for an already-released binding, so its goroutine no longer lives
  past the binding re-arming its timer forever; a later re-acquire starts fresh.
- Full-duplex session: acquire the pool connection only after the first miss
  arrives, and release it if CSC serving was disabled meanwhile, so an idle
  session no longer starves a small pool (PoolSize:1) until PoolTimeout.

Adds unit tests for the pinned-mode rejection and the UniversalOptions passthrough.
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

case <-mc.stop:
stopFlag.Store(true)
doRecycle()

P2 Badge Close the socket when stopping full-duplex sessions

In full-duplex CSC, Client.Close stops the miss coalescer before the pool is closed, so this stop branch only requests a graceful recycle and leaves any reader already blocked in WithReader/ReadRawReply waiting for the server reply. If the server or network stalls while a miss is in flight, and ReadTimeout is disabled (ReadTimeout: -1) or set very long, Close can hang indefinitely; close or cancel the held connection on mc.stop so the reader goroutine can exit.


getCtx, getCancel := opCtx()
cn, err := c.getConn(getCtx)
getCancel()

P2 Badge Apply the limiter to each full-duplex miss

With ClientSideCacheCoalesceMode: "fullduplex", this is the only getConn/Limiter.Allow call for the entire session, but the writer can keep accepting later misses from mc.ch for up to the recycle age. In clients that configure Options.Limiter as a rate limiter or circuit breaker, those subsequent user commands bypass Allow and their successes/failures are collapsed into one ReportResult when the session releases the connection, so throttling and failure accounting are substantially under-enforced for cached misses.


swg.Wait()
close(superDone)

P2 Badge Join the supervisor before re-pooling the connection

After a clean full-duplex recycle, close(superDone) only signals the supervisor and does not wait for it to exit. If the supervisor is not scheduled until after this function returns, the deferred scancel() makes both superDone and sctx.Done() ready, so its select can take the sctx.Done() branch and call cn.Close() after the healthy connection has already been returned to the pool; that can asynchronously close a connection that another command has reused. Wait for the supervisor to finish before re-pooling, or make the close-on-sctx.Done() branch impossible once superDone is closed.

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@ndyakov

ndyakov commented Aug 13, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI 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.

Pull request overview

This PR extends the client-side caching (CSC) subsystem by moving miss-coalescing and invalidation-batching controls into Options/UniversalOptions, adding a full-duplex miss-coalescing engine, and introducing windowed background batching for invalidation-driven deletes.

Changes:

  • Add new CSC configuration knobs to Options and propagate them through UniversalOptions.Simple().
  • Introduce selectable miss-coalescing modes (workers default, fullduplex), including a new full-duplex session engine.
  • Add optional windowed invalidation batching to offload cache deletes from the push-notification read path.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
universal.go Adds new CSC config fields to UniversalOptions and propagates them into Options via Simple().
options.go Adds new CSC config fields to Options and documents coalescing/batching behavior.
csc_refresh_support.go Removes env-var support helpers now that options drive behavior.
csc_refresh_on_invalidate.go Removes env-var gates/knobs and switches refresh publishing to tracked main-pool connections.
csc_miss_coalesce.go Refactors miss coalescer to be options-driven and to support multiple engines with shared helpers/stats.
csc_miss_coalesce_modes.go Adds the pinned prototype and full-duplex miss-coalescing engines and related session lifecycle logic.
csc_miss_coalesce_modes_test.go Adds coverage for full-duplex idle push draining correctness (positive + negative control).
csc_inval_batch.go Introduces the windowed background invalidation batcher implementation.
csc_integration.go Integrates invalidation batching into the push invalidation handler and threads the window from Options.
csc_coalesce_options_test.go Adds tests for public mode selection behavior and UniversalOptions.Simple() propagation.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread csc_miss_coalesce_modes.go
Comment thread csc_integration.go
Comment thread options.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2f6a9e55ef

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread csc_inval_batch.go
Comment thread csc_miss_coalesce_modes.go
Comment thread csc_miss_coalesce_modes.go Outdated
Comment thread csc_miss_coalesce_modes.go Outdated
…; doc

- Full-duplex readOne now applies the read reply and lets fulfillCached gate
  only the cache publish on the captured conn id/generation -- matching the
  workers/pinned engines -- instead of failing the caller with ErrClosed and
  losing a good reply on a mid-flight id/gen change.
- peekAndProcessPushNotifications also drains when the reader has buffered bytes
  (HasBufferedData), not only when the socket is readable (MaybeHasData), so a
  buffered invalidation is processed on the idle tick.
- Workers-mode doc corrected: a tracked connection is acquired/released per
  batch, not held across batches.
Comment thread csc_inval_batch.go

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a5a231afa5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread csc_miss_coalesce.go
Comment thread csc_miss_coalesce.go Outdated

Copilot AI 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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

Suppressed comments (2)

csc_miss_coalesce_modes.go:14

  • The file header labels these engines as "PROTOTYPE", but the fullduplex engine is selectable via the public ClientSideCacheCoalesceMode option and described as feature-complete in this PR. This comment is misleading for maintainers/users reading the code.
// Alternate miss-coalescer engines (PROTOTYPE, benchmark comparison).

csc_miss_coalesce_modes.go:134

  • The comment says a clean recycle continues immediately, but the code always waits for cscModeBackoff after every session end. Either skip the backoff on clean recycle, or update the comment so behavior and documentation match.
		// Session ended on a connection error or a clean recycle. On error, back
		// off briefly so a persistent dial failure does not hot-spin; a clean
		// recycle continues immediately.

saddamr3e and others added 2 commits August 14, 2026 11:52
FTHybridCmd.readReply used ReadSlice, which reads a RESP3 map header's
length as an element count and consumes only half its frames, leaving the
connection desynced. Peek the type and read a map with ReadReply, like the
other FT.* parsers.
- A coalesced miss re-runs uncached when CSC serving is disabled after the miss
  was reserved (RESP3 downgrade / CLIENT TRACKING loss during a conn re-init),
  via an internal retry-uncached sentinel that processCached catches, instead of
  surfacing a spurious pool.ErrClosed for a valid cacheable read.
- A caller whose context cancels mid-fetch no longer races the coalescer: a CAS
  interlock (claimAbandon/claimApply) hands the Cmder to exactly one of the caller
  or the applying worker, so the worker never writes a Cmder the caller has taken
  back. The reply is still classified and published to the shared cache, and a
  cancelled Get returns the context error deterministically (matching the
  non-coalesced path).
- A fetch after coalescer shutdown returns pool.ErrClosed instead of hanging on a
  post-drain enqueue race.
Adds TestClassifyCachedReply, TestCSCMissReqClaimInterlock,
TestCSCMissCoalesceAbandonedFetchNoRace, TestFullDuplexDisabledMidMissRetriesUncached.

Copilot AI 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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

Suppressed comments (2)

csc_miss_coalesce_modes.go:216

  • sessErr is an atomic.Value that is only Store()'d on I/O failure, but errored()/reasonErr() call Load() unconditionally. atomic.Value panics on Load before the first Store, so the graceful-recycle path can crash the client. Initialize sessErr with a non-nil typed holder (or switch to atomic.Pointer) so Load is always safe.
	errored := func() bool { _, ok := sessErr.Load().(error); return ok }
	reasonErr := func() error {
		if e, ok := sessErr.Load().(error); ok {
			return e
		}

csc_miss_coalesce.go:25

  • The header comment still says miss coalescing is "env-gated", but the gating was moved to Options.ClientSideCacheCoalesceMisses. This is now misleading for readers and users.
// Reader-miss coalescing (PROTOTYPE, env-gated).

Comment thread csc_integration.go

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5cae5e11b8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread csc_integration.go
…ange

- A full cache flush (FLUSHDB/FLUSHALL) cleared the cache but left the
  invalidation batcher's queued per-key deletes, which then fired and evicted
  entries repopulated after the flush (an extra miss within the window). Drop the
  batcher's pending queue on flush.
- A running batcher's window is fixed at creation, so a second client binding to
  the same shared handler with a stricter window kept the old cadence and its
  staleness bound did not hold. setInvalBatchWindow now drops the running batcher
  on a window change so the next invalidation starts a fresh one with the new
  window.
setup-go's "1.26.x" resolved to go1.26.5, which govulncheck flags for two
standard-library vulnerabilities fixed in go1.26.6: GO-2026-6090 (crypto/tls)
and GO-2026-5972 (encoding/asn1). Track the latest stable toolchain so future
security patches are picked up automatically instead of pinning a patch.

Copilot AI 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.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Suppressed comments (4)

csc_inval_batch.go:60

  • The buffered dropCh signal does not order ahead of the timer case and cannot stop an apply already in progress. Around a window boundary, run can select t.C and apply the pre-flush batch after cache.Flush() (and after a reader repopulates the key), evicting the fresh entry despite this method's guarantee. Coordinate flush/drop with an epoch or mutex and wait for/neutralize any in-progress apply so all pre-FLUSH deletes are definitively superseded.
	// Signal run() to clear its in-progress batch; the cap-1 buffer means a signal
	// is never lost even if run() is not currently selecting.
	select {
	case b.dropCh <- struct{}{}:
	default:

csc_miss_coalesce.go:336

  • getConn calls Limiter.Allow only once for the whole batch, while every request in batch is a separate client operation. This lets batches bypass per-operation rate/circuit limits and reports only one aggregate result, contrary to the Limiter contract in options.go:37-45. Check and report the limiter per request, excluding denied requests from the wire batch; use _getConn for the underlying batch connection so it does not consume an extra limiter operation.
	cn, err := c.getConn(ctx)

csc_miss_coalesce_modes.go:168

  • This acquires one limiter permit via getConn and holds it for the entire full-duplex session, which can execute thousands of independent operations for up to 30 seconds. A concurrency limiter may therefore reject unrelated commands while this permit sits idle, and a rate/circuit limiter never sees individual miss results. Apply Allow/ReportResult per request and acquire the session connection through _getConn instead.
	getCtx, getCancel := opCtx()
	cn, err := c.getConn(getCtx)
	getCancel()

csc_miss_coalesce_modes.go:158

  • Waiting for the first miss does not prevent idle starvation after that miss completes: the session keeps the connection until the 30-second recycle timer. With the valid PoolSize: 1 configuration, a miss succeeds but the caller's next non-cacheable command (for example SET or PING) cannot acquire the sole pool turn and times out. Return the session connection once its in-flight queue drains and no miss is queued, or reject/fallback from full-duplex mode when the pool cannot reserve another connection.
	// Do not hold a pool connection while idle: wait for the first miss BEFORE
	// acquiring. An eagerly-held session connection would, at a small pool
	// (PoolSize:1), starve non-cacheable commands (PING/SET/uncached reads) until
	// PoolTimeout while the session sat waiting for work. The pulled miss is
	// written first by the writer below.

Comment thread csc_miss_coalesce.go Outdated
Comment thread csc_integration.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ce21ffb50f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread csc_miss_coalesce_modes.go
Comment thread csc_miss_coalesce_modes.go
Comment thread csc_integration.go
- Attribute write-side failures to the held session connection: the miss
  coalescer sets req.servedBy for the whole batch before the write, so a
  WithWriter error (or ctx cancel during the inflight handoff) settles with the
  connection attribution the reply path records, not a nil conn / zero attempts.

- Tolerate fragmented push frames on held sessions: once readiness is
  established, the drain uses the longer cscDrainHardReadCap budget (as the
  background drainer does) instead of the 1ms probe cap, which could time out
  mid-frame over TLS and fatally close a healthy session. The 1ms cap is
  reserved for speculative no-data probes (opaque transports).

- Route a WithTimeout clone's CSC push-handler Close through the owner wrapper:
  the clone's cscClientWeak now points at the canonical owner, so closeCanonical
  calls owner.Close() (full CSC + autopipeliner teardown) instead of
  baseClient.Close closing shared pools while the owner's drainer runs.
@ndyakov

ndyakov commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e6d30f98e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread csc_refresh_on_invalidate.go
Comment thread csc_refresh_on_invalidate.go

Copilot AI 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.

Pull request overview

Copilot reviewed 16 out of 16 changed files in this pull request and generated no new comments.

Suppressed comments (3)

redis.go:2669

  • This cleanup can re-arm a deadline instead of clearing it. WithReaderHardDeadline already clears its deadline on return; calling WithReader(..., 0, ...) afterward runs getEffectiveReadTimeout, so an active maintenance relaxation installs a positive deadline. For ReadTimeout < 0, later reads intentionally skip SetReadDeadline and can then fail at that stale deadline. Remove this defer and rely on WithReaderHardDeadline's clear.
	defer func() {
		if c.opt.ReadTimeout < 0 {
			_ = cn.WithReader(context.Background(), 0, func(*proto.Reader) error { return nil })
		}

csc_miss_coalesce.go:325

  • Passing context.Background() makes this error callback differ from the normal command path, which passes the caller context (redis.go:1276). Custom recorders and OpenTelemetry exemplars therefore lose the request's trace/context specifically for coalesced reply errors, despite the promised telemetry parity. Preserve the fetch context on the request and pass it here.
			errorCallback(context.Background(), errorType, req.servedBy, statusCode, isInternal, 0)

csc_miss_coalesce.go:377

  • This transport-error callback also discards the caller context, unlike processWithRetry (redis.go:1276,1288). As a result, coalesced acquisition/write/read failures cannot be correlated with the originating trace by custom or OpenTelemetry recorders. Store the request context and pass it through here.
			errorCallback(context.Background(), errorType, req.servedBy, statusCode, isInternal, 0)

Two fixes to the refresh-on-invalidate reply reader, matching the
miss-coalescer / background-drainer policy:

- Propagate push-processor errors instead of logging and continuing. A
  surfaced error means bytes may have been consumed mid-frame; reading the next
  reply on a desynchronized stream could publish a push fragment under the
  wrong cache key. Returning the error aborts the batch so withConn retires the
  connection.

- Use the nonblocking cscHandlerClient adapter for the drain. This reader is
  part of the refresher's waitgroup, so a custom push handler calling Close()
  on the raw client would deadlock shutdown (Close waits on the refresher
  goroutine parked in the handler).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bc297923ee

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread csc_miss_coalesce_modes.go
Comment thread csc_miss_coalesce.go
…ed (#3969)

* Update options.go

* Update options_test.go

* Update options.go

* fix flaky test

* fix(maintnotifications): skip endpoint DNS detect when mode is disabled
@ndyakov

ndyakov commented Aug 18, 2026

Copy link
Copy Markdown
Member Author

@codex review

Copilot AI 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.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: bc297923ee

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

vlady-kotsev and others added 2 commits August 18, 2026 17:28
#3972)

* perf(proto): add zero-copy semantics to scan, remove redundant data conversions in reader

* chore: unpin govulncheck version

* chore: require govulncheck to use latest go version

* chore: pin govulncheck and resolve go-version to stable

---------

Co-authored-by: Nedyalko Dyakov <1547186+ndyakov@users.noreply.github.com>
A caller that cancels its context while waiting on a coalesced miss returned
early without recording an error metric: the serving session still completes
the background fetch (applyAndSettle success or settleErr), so neither records
THIS caller's cancellation, undercounting the cancellation rate whenever miss
coalescing is enabled. Report it here, attributed to the caller's ctx, matching
processWithRetry.

The conn is passed as nil deliberately: req.servedBy is written by the session
and only safe to read after the req.done receive (the happens-before edge),
which has not happened on the cancellation branch, and is nil anyway when the
cancel beats the serve. The metric recorder treats a nil conn as "no peer
attributes".
Comment thread csc_miss_coalesce_modes.go
When getConn fails because acquireCtx was cancelled by mc.stop during coalescer
teardown, the session settled the pulled miss and drained the queue with the raw
context.Canceled. processCached only retries uncached on the errCSCRetryUncached
sentinel, so in-flight coalesced reads surfaced a spurious cancellation even
though the caller's context was still valid, instead of re-running on the
still-open pool like the other stop paths.

acquireCtx derives from context.Background() and is cancelled only by mc.stop
(its sole non-deadline cancel source), so a context.Canceled here uniquely means
teardown: settle first and the queue drain with errCSCRetryUncached and report
stopped so the loop exits without backing off. Genuine acquire failures (dial
error, deadline) keep the raw error and the backoff path.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 90180a1163

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread csc_miss_coalesce_modes.go Outdated
The full-duplex miss coalescer mapped a getConn cancellation to
errCSCRetryUncached by testing errors.Is(err, context.Canceled). But getConn runs
the dialer, credentials callbacks, and init hooks, any of which can return
context.Canceled from their OWN context while the coalescer is not stopping.
Treating the error value as proof that mc.stop fired permanently exited the sole
full-duplex worker; the current miss retried uncached, but later misses still
enqueued through the still-live coalescer with no worker to settle them, hanging
deadline-free callers and leaking IN_PROGRESS reservations.

Detect teardown by checking mc.stop directly instead. mc.stop is closed only by
stopWorkers, and close(mc.stop) is ordered before acquireCtx cancels its context
(before getConn returns), so a real teardown is always visible. On stop, settle
retry-uncached and exit; on any other acquire failure — including an unrelated
context.Canceled — fail the batch and back off so the worker stays alive. This
supersedes the errors.Is check added in the previous round.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ee25c4e66c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread csc_miss_coalesce_modes.go
ndyakov and others added 3 commits August 19, 2026 00:24
ClientSideCacheCoalesceMisses holds one connection per coalescer session and
serves many misses on it, so Options.Limiter is admitted (Allow/ReportResult)
once per session — per held connection — not per coalesced miss, unlike the
plain per-command path. This is inherent to the held-connection model; a
per-miss Allow would defeat the coalescing and re-admit a connection already
held. Document it on the option so callers that need strict per-command
admission or circuit-breaking know not to enable miss coalescing.
Bumps [rojopolis/spellcheck-github-actions](https://github.com/rojopolis/spellcheck-github-actions) from 0.63.0 to 0.66.0.
- [Release notes](https://github.com/rojopolis/spellcheck-github-actions/releases)
- [Changelog](https://github.com/rojopolis/spellcheck-github-actions/blob/master/CHANGELOG.md)
- [Commits](rojopolis/spellcheck-github-actions@0.63.0...0.66.0)

---
updated-dependencies:
- dependency-name: rojopolis/spellcheck-github-actions
  dependency-version: 0.66.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0a0cb0fe9d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread redis.go
The held-session push drain (pushDrainWithin) dispatched the built-in processor
through ProcessPendingNotifications, which swallows an error that occurs AFTER
partially consuming a frame — e.g. a RESP3 attribute whose remainder arrives
slower than the hard read cap. A swallowed mid-frame error left the connection
desynchronized, so later fragments could be misread as command replies and cached
under the wrong key, or fail unrelated misses.

Use ProcessPendingNotificationsBuffered for the built-in processor, exactly as
drainPushNotifications already does, so a post-consumption error is propagated and
the caller retires the desynced session. Custom processors keep the unbuffered
call (their contract is "invoked only when notifications are known present").

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d33c190. Configure here.

Comment thread csc_integration.go
// still applies — its post-flush delete must not be lost.
if batcher != nil {
batcher.drop()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Flush misses replaced batcher

Medium Severity

HandlePushNotification snapshots batcher under RLock then calls drop() only on that pointer before Flush. A concurrent window/refresh rebuild can stop() and nil the handler batcher first, so FLUSHDB/FLUSHALL never bumps that batcher’s epoch. Its async stop-drain still applies the pre-flush deletes afterward and can evict entries repopulated after the flush, the failure mode TestInvalBatchDroppedOnFlush already pins for the non-racy path.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit d33c190. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d33c190d69

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread redis.go
// which is treated as fatal and fatally closes a healthy session (failing its
// in-flight misses). The 1ms cap is reserved for speculative no-data probes
// (timedPushDrain, the opaque-transport fallback).
return c.pushDrainWithin(ctx, cn, cscDrainHardReadCap)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Verify TLS application data before custom push processing

When miss coalescing runs over TLS with a custom NotificationProcessor, raw-socket readiness does not prove that a RESP notification exists: MaybeHasData unwraps TLS, while internal/pool/conn_check.go explicitly notes that the readable bytes may be only a post-handshake record. This call then invokes the custom processor under the 50 ms hard deadline; unlike the built-in buffered processor, a conforming custom processor may surface the resulting empty-read timeout, which the session reader treats as fatal at csc_miss_coalesce_modes.go:431-433, closing a healthy connection and failing any in-flight misses. Confirm an application-level push before invoking a custom processor, or restrict this transport-readiness path to the built-in processor.

AGENTS.md reference: AGENTS.md:L126-L132

Useful? React with 👍 / 👎.

Comment thread csc_miss_coalesce.go
Comment on lines 244 to +246
case <-ctx.Done():
mc.c.csc.Cancel(cacheKey, token)
return ctx.Err()
return nil, ctx.Err()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Record cancellations that happen before enqueue

When the coalescer queue is full, or its worker is stalled acquiring a connection, a caller can expire in this enqueue select. This path cancels the reservation and returns ctx.Err() without invoking GetMetricErrorCallback; unlike the later post-enqueue cancellation path, no worker owns the request and no background settlement can record the error, so cancellation metrics are silently undercounted precisely during overload. Emit the same cancellation callback here before returning.

Useful? React with 👍 / 👎.

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.

5 participants