-
Notifications
You must be signed in to change notification settings - Fork 2
refactor(redis): bound rangeList / readValueAt VerifyLeaderForKey to per-call ctx #752
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1761,7 +1761,13 @@ type txnValue struct { | |
| } | ||
|
|
||
| type txnContext struct { | ||
| server *RedisServer | ||
| server *RedisServer | ||
| // ctx is the per-EXEC dispatch context (redisDispatchTimeout-bounded | ||
| // at the call site in runTransaction). Plumbed through so reads | ||
| // inside the EXEC such as load() → readValueAt() respect the | ||
| // caller's deadline rather than falling back to handlerContext + | ||
| // the verifyLeaderEngineCtx safety net. | ||
| ctx context.Context //nolint:containedctx // EXEC is a long-lived value type that wraps a single client command, ctx must travel with it. | ||
| working map[string]*txnValue | ||
| listStates map[string]*listTxnState | ||
| zsetStates map[string]*zsetTxnState | ||
|
|
@@ -1870,7 +1876,7 @@ func (t *txnContext) load(key []byte) (*txnValue, error) { | |
| tv.ttl = ttl | ||
| } else { | ||
| var err error | ||
| val, err = t.server.readValueAt(storageKey, t.startTS) | ||
| val, err = t.server.readValueAt(t.ctx, storageKey, t.startTS) | ||
| if err != nil && !errors.Is(err, store.ErrKeyNotFound) { | ||
| return nil, errors.WithStack(err) | ||
| } | ||
|
|
@@ -2797,6 +2803,7 @@ func (r *RedisServer) runTransaction(queue []redcon.Command) ([]redisResult, err | |
|
|
||
| txn := &txnContext{ | ||
| server: r, | ||
| ctx: dispatchCtx, | ||
| working: map[string]*txnValue{}, | ||
| listStates: map[string]*listTxnState{}, | ||
| zsetStates: map[string]*zsetTxnState{}, | ||
|
|
@@ -3244,7 +3251,7 @@ func (r *RedisServer) fetchListRange(ctx context.Context, key []byte, meta store | |
| return out, nil | ||
| } | ||
|
|
||
| func (r *RedisServer) rangeList(key []byte, startRaw, endRaw []byte) ([]string, error) { | ||
| func (r *RedisServer) rangeList(ctx context.Context, key []byte, startRaw, endRaw []byte) ([]string, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While ctx is now passed to rangeList, it is only used for the VerifyLeaderForKey call. Other context-aware operations in this method, such as keyTypeAt (line 3260), resolveListMeta (line 3279), and fetchListRange (line 3292), still use context.Background(). To fully adhere to the goal of bounding the command execution and ensuring prompt cancellation, these calls should also use the provided ctx. Additionally, per the rules for linearizable reads in MVCC, ensure the timestamp for the read is acquired after the linearizable read fence (VerifyLeaderForKey) to guarantee a consistent snapshot and prevent stale data reads. References
|
||
| if !r.coordinator.IsLeaderForKey(key) { | ||
| return r.proxyLRange(key, startRaw, endRaw) | ||
| } | ||
|
|
@@ -3261,7 +3268,11 @@ func (r *RedisServer) rangeList(key []byte, startRaw, endRaw []byte) ([]string, | |
| return nil, wrongTypeError() | ||
| } | ||
|
|
||
| if err := r.coordinator.VerifyLeaderForKey(r.handlerContext(), key); err != nil { | ||
| // PR #749 follow-up: pass the per-call dispatch ctx so a stalled | ||
| // VerifyLeaderForKey honours the caller's deadline rather than the | ||
| // long-lived handlerContext + verifyLeaderEngineCtx fallback. Same | ||
| // shape as keys() / FLUSHDB. | ||
| if err := r.coordinator.VerifyLeaderForKey(ctx, key); err != nil { | ||
| return nil, errors.WithStack(err) | ||
| } | ||
|
|
||
|
|
@@ -3498,7 +3509,7 @@ func (r *RedisServer) tryLeaderGetAt(key []byte, ts uint64) ([]byte, error) { | |
| return resp.Value, nil | ||
| } | ||
|
|
||
| func (r *RedisServer) readValueAt(key []byte, readTS uint64) ([]byte, error) { | ||
| func (r *RedisServer) readValueAt(ctx context.Context, key []byte, readTS uint64) ([]byte, error) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to rangeList, readValueAt now receives a ctx but continues to use context.Background() for hasExpired (line 3522) and store.GetAt (line 3538). These should be updated to use the passed ctx to ensure the entire read path respects the dispatch deadline. Furthermore, ensure that the timestamp used for the MVCC read is acquired after the linearizable read fence has completed to guarantee a consistent snapshot view and prevent stale data reads, as required for readValueAt. References
|
||
| ttlKey := key | ||
| nonStringInternal := false | ||
| if userKey := extractRedisInternalUserKey(key); userKey != nil { | ||
|
|
@@ -3517,7 +3528,11 @@ func (r *RedisServer) readValueAt(key []byte, readTS uint64) ([]byte, error) { | |
| } | ||
|
|
||
| if r.coordinator.IsLeaderForKey(key) { | ||
| if err := r.coordinator.VerifyLeaderForKey(r.handlerContext(), key); err != nil { | ||
| // PR #749 follow-up: caller-supplied ctx (with | ||
| // redisDispatchTimeout from the dispatch handler) replaces | ||
| // r.handlerContext() so VerifyLeaderForKey honours the | ||
| // per-command deadline. Same shape as keys() / FLUSHDB. | ||
| if err := r.coordinator.VerifyLeaderForKey(ctx, key); err != nil { | ||
| return nil, errors.WithStack(err) | ||
| } | ||
| v, err := r.store.GetAt(context.Background(), key, readTS) | ||
|
|
@@ -3567,7 +3582,9 @@ func (r *RedisServer) rpush(conn redcon.Conn, cmd redcon.Command) { | |
| } | ||
|
|
||
| func (r *RedisServer) lrange(conn redcon.Conn, cmd redcon.Command) { | ||
| items, err := r.rangeList(cmd.Args[1], cmd.Args[2], cmd.Args[3]) | ||
| ctx, cancel := context.WithTimeout(r.handlerContext(), redisDispatchTimeout) | ||
| defer cancel() | ||
| items, err := r.rangeList(ctx, cmd.Args[1], cmd.Args[2], cmd.Args[3]) | ||
| if err != nil { | ||
| conn.WriteError(err.Error()) | ||
| return | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: bootjp/elastickv
Length of output: 4263
🏁 Script executed:
Repository: bootjp/elastickv
Length of output: 1429
🏁 Script executed:
Repository: bootjp/elastickv
Length of output: 1953
🏁 Script executed:
Repository: bootjp/elastickv
Length of output: 477
🏁 Script executed:
Repository: bootjp/elastickv
Length of output: 1009
🏁 Script executed:
Repository: bootjp/elastickv
Length of output: 639
🏁 Script executed:
rg -n --type=go 'func.*readValueAt' adapter/redis.goRepository: bootjp/elastickv
Length of output: 165
🏁 Script executed:
Repository: bootjp/elastickv
Length of output: 302
Add a nil-safe fallback for
t.ctxinload()to support test fixtures that omit the field.Test fixtures in
redis_txn_test.go(lines 37, 73, 108, 146) constructtxnContextliterals withoutctx. WhenstageKeyDeletion()is called—for example, during key deletion in a transaction—it invokest.load(internalKey)in a loop (lines 2380–2392), which passest.ctxtoreadValueAt()at line 1879. Ifctxis nil, any coordinator implementation or mock that usescontext.WithTimeout(ctx, …)would panic.Currently, test fixtures only call
loadListState(), which usescontext.Background()internally, so the issue is latent. However, this matches the existing pattern used forstreamDeletions(line 2399): "test fixtures that build a minimal txnContext literal without this field still work." A one-line defensive fallback inload()preserves that design principle and adds robustness:Suggested fix
} else { var err error - val, err = t.server.readValueAt(t.ctx, storageKey, t.startTS) + ctx := t.ctx + if ctx == nil { + ctx = context.Background() + } + val, err = t.server.readValueAt(ctx, storageKey, t.startTS) if err != nil && !errors.Is(err, store.ErrKeyNotFound) { return nil, errors.WithStack(err) } }🤖 Prompt for AI Agents