Skip to content

feat: add SetOnInvalidations to DedicatedClient#967

Merged
rueian merged 8 commits intoredis:mainfrom
jinbum-kim:feat/set-on-invalidations
Apr 7, 2026
Merged

feat: add SetOnInvalidations to DedicatedClient#967
rueian merged 8 commits intoredis:mainfrom
jinbum-kim:feat/set-on-invalidations

Conversation

@jinbum-kim
Copy link
Copy Markdown
Contributor

@jinbum-kim jinbum-kim commented Mar 12, 2026

Closes #927

pipe.handlePush currently intercepts RESP3 invalidate push messages, so DedicatedClient users cannot receive client-side caching invalidations on the same dedicated connection they use for CLIENT TRACKING.

Instead, they currently need a separate client configured with ClientOption.OnInvalidations.

This PR adds SetOnInvalidations to DedicatedClient so invalidation messages can be handled on that same dedicated connection.

Changes:

  • add SetOnInvalidations to DedicatedClient
  • implement it for both standalone and cluster dedicated clients
  • preserve existing PubSubHooks and replace only the invalidation callback
  • forward RESP3 invalidate push messages to the dedicated invalidation hook path
  • send CLIENT TRACKING OFF when a dedicated connection is returned to the pool, so no tracking state is retained across reuse
  • update wrappers/mocks and add tests for the new dedicated invalidation hook path

Example:

dc, cancel := client.Dedicate()
defer cancel()

ch := dc.SetOnInvalidations(func(messages []rueidis.RedisMessage) {
    // handle invalidations
})

dc.Do(ctx, dc.B().ClientTracking().On().Prefix().Prefix(prefix).Bcast().Build())
<-ch

Note

Medium Risk
Adds a new callback path for RESP3 invalidate push messages and changes dedicated-connection pooling to issue CLIENT TRACKING OFF on reuse, which could affect client-side caching behavior if misused. Scope is contained to DedicatedClient/pubsub hook plumbing and has test coverage.

Overview
Adds DedicatedClient.SetOnInvalidations(fn) to let dedicated standalone and cluster clients receive RESP3 invalidate push messages on the same connection used for CLIENT TRACKING, without overwriting existing PubSubHooks.

Plumbs invalidation delivery through PubSubHooks (new onInvalidations field), adds wire.GetPubSubHooks(), and updates pipe.handlePush to fan out invalidate events to both the existing ClientOption callback and the dedicated hook path.

Hardens pooling semantics: when a dedicated wire is returned to the pool, mux.Store clears hooks/subscriptions and, if invalidation hooks were installed, sends CLIENT TRACKING OFF to avoid leaking tracking state across reuse. Updates mocks/wrappers and adds targeted tests for hook preservation, reset behavior, and tracking-off on store.

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

Signed-off-by: jinbum9958 <jinbum9958@gmail.com>
Signed-off-by: jinbum9958 <jinbum9958@gmail.com>
Comment thread client.go
Comment thread internal/cmds/cmds.go

// ClientTrackingOffCmd is predefined CLIENT TRACKING OFF
ClientTrackingOffCmd = Completed{
cs: newCommandSlice([]string{"CLIENT", "TRACKING", "OFF"}),
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.

Hi @jinbum-kim,

I think using this is probably wrong because we will never turn client tracking on again, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. I've removed CLIENT TRACKING OFF from CleanSubscriptions and dropped ClientTrackingOffCmd from this PR.

Should we instead send CLIENT TRACKING OFF when the invalidation hook is explicitly cleared, or leave that entirely to the caller?

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.

Hmm, this is a bit tough.

After a second thought, doing CLIENT TRACKING OFF automatically when the connection is returned to the pool might actually be a correct move because, for the dedicated pool, neither users nor we rely on client tracking in the pool. Client tracking is essentially off in the pool in the first place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I think this is a bit tricky too 👀
Let me think about it a bit more and get back to you.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi, It seems more appropriate to clean it up when the dedicated connection is returned to the pool, so that no tracking state is retained across reuse. I'll revise the PR in this way.

…ntTrackingOffCmd

Signed-off-by: jinbum9958 <jinbum9958@gmail.com>
Comment thread pipe.go
…on close

Signed-off-by: jinbum9958 <jinbum9958@gmail.com>
Comment thread pipe.go
…ns via wire getter

Signed-off-by: jinbum9958 <jinbum9958@gmail.com>
@jinbum-kim jinbum-kim force-pushed the feat/set-on-invalidations branch from 4d6bdf6 to 8da5d92 Compare March 16, 2026 11:16
…o pool

Signed-off-by: jinbum9958 <jinbum9958@gmail.com>
Copy link
Copy Markdown

@cursor cursor Bot left a comment

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 and found 2 potential issues.

Fix All in Cursor

Comment thread cluster.go
Comment thread mux.go
Comment thread mux.go Outdated
func (m *mux) Store(w wire) {
w.SetPubSubHooks(PubSubHooks{})
w.CleanSubscriptions()
if w.Version() >= 6 {
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.

Let's do this only when onInvalidations was set on the hooks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Should I also keep the w.Version() >= 6 guard, or is checking onInvalidations != nil alone sufficient?

hasOnInvalidations := w.GetPubSubHooks().onInvalidations != nil
...
if hasOnInvalidations (``&& w.Version() >= `6`) {
		w.Do(context.Background(), cmds.ClientTrackingOffCmd)
	}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've updated it so that it only applies when onInvalidations is set. If you have any further suggestions, please let me know.

Signed-off-by: jinbum9958 <jinbum9958@gmail.com>
Comment thread pipe.go Outdated

type pshks struct {
hooks PubSubHooks
orig PubSubHooks
Copy link
Copy Markdown
Collaborator

@rueian rueian Mar 31, 2026

Choose a reason for hiding this comment

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

What will happen if we don't save this orig? Can we remove it?

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.

Hi @jinbum-kim, is it possible to remove this orig?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hi @rueian, Apologies for the delayed response. I recently started a new job, so my available time has been a bit limited 😅

I think orig is still needed.

SetPubSubHooks canonicalizes nil OnMessage / OnSubscription into internal no-op handlers, and SetOnInvalidations now does a read-modify-write through GetPubSubHooks():

hooks := c.wire.GetPubSubHooks()
hooks.onInvalidations = fn
return c.SetPubSubHooks(hooks)

(in this commit)

SetPubSubHooks checks isZero() before canonicalization, so this works only if GetPubSubHooks() returns the original user-intent shape.

If GetPubSubHooks() returned the canonicalized form, then clearing fn would no longer reset back to zero when invalidations was the only originally-installed hook, because OnMessage / OnSubscription would already be non-nil.

That would break the documented behavior of SetOnInvalidations, which returns nil when clearing fn leaves no hooks installed.

So unless we preserve the original nil-ness some other way, I think we should keep orig.

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.

Thanks, I just removed the canonicalization.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Are there any additional points I should look into?

Signed-off-by: Rueian <rueiancsie@gmail.com>
@rueian rueian merged commit a69fbf5 into redis:main Apr 7, 2026
28 checks passed
@rueian
Copy link
Copy Markdown
Collaborator

rueian commented Apr 7, 2026

Thanks for the contribution! @jinbum-kim

@jinbum-kim
Copy link
Copy Markdown
Contributor Author

My pleasure 😀 @rueian
If there's anything else I can help with, feel free to tag me anytime.

@jinbum-kim jinbum-kim deleted the feat/set-on-invalidations branch April 7, 2026 10:05
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.

No messages from SUBSCRIBE __redis__:invalidate

2 participants