Skip to content

invalidate local cache on is_negative_hits - #1230

Open
oclaw wants to merge 2 commits into
envoyproxy:mainfrom
oclaw:negative-hits-flag-invalidation
Open

oclaw wants to merge 2 commits into
envoyproxy:mainfrom
oclaw:negative-hits-flag-invalidation

Conversation

@oclaw

@oclaw oclaw commented Sep 1, 2026 •

Copy link
Copy Markdown

Problem

is_negative_hits (#1140) lets a descriptor decrement previously consumed quota, but refunds are invisible to the local over-limit cache: once a replica cached a key as over limit, a refund that freed quota in Redis kept being answered OVER_LIMIT from every replica's local cache until the window ended which degrades the feature on long windows

Suggested design

  • Introduce feature flag ENABLE_NEGATIVE_HITS (default off). While off, a request containing a negative-hit descriptor is rejected with UNIMPLEMENTED
  • Refuse to boot the one unfixable combination: negative hits + memcached + local cache (this PR does not add any support for the invalidations with memcached as a backend). Memcached refunds without a local cache enabled keep working.
  • Add Redis pub-sub backed invalidation routine: at-most-once guarantee, single added connection per replica + local cache gen-based guard for eliminating races between invalidation and stale insert

P.S. Agent also added some missing tests for touched components, look fine

// DecrementScript atomically decrements a rate limit counter, floored at 0.
// If the key does not exist there is nothing to refund, so it returns 0 without
// creating a phantom key.
const DecrementScript = `

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

This is not really a common limiter part, moved it closer to Redis backend impl

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Reused driver.go as much as possible but this routine has quite independent connection cycle so decided not to bother too much

if hitsAddends[i].IsNegative {
pipelineAppendDecrement(client, p, cacheKey.Key, hitsAddends[i].Value, &results[i], expirationSeconds)
// The subscriber listens only on the main Redis.
publishInvalidation := this.publishInvalidations && !onPerSecondRedis

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

dedicated per second redis assume high load and redis-backed refunds are barely effective here, not doing them (and the invalidator only listens main redis at the moment)

@oclaw
oclaw force-pushed the negative-hits-flag-invalidation branch 3 times, most recently from 0602802 to ef92df1 Compare September 1, 2026 14:51
@oclaw

oclaw commented Sep 6, 2026

Copy link
Copy Markdown
Author

@collin-lee as a first-time contributor I do not have an opportunity to request for the review it looks like, so pinging you here :)

@rudrakhp rudrakhp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the bug fix. Can we keep it focused on fixing the cache invalidation part though, any reason why we need the ENABLE_NEGATIVE_HITS flag?

@oclaw

oclaw commented Sep 23, 2026

Copy link
Copy Markdown
Author

@rudrakhp The reason is that the bug is still present - I did not support the same invalidation for the memcached backend case as I am not sure how to deliver it properly. So without ENABLE_NEGATIVE_HITS and with local overlimit cache enabled memcached-backed workload will silently keep working in a broken way

@rudrakhp

Copy link
Copy Markdown
Member

@oclaw ok on second thought I think preventing RLS to come up with known unsupported combinations like Memcached + local cache + enable_negative_hits combination makes sense. SGTM.

@oclaw
oclaw force-pushed the negative-hits-flag-invalidation branch from ef92df1 to 0de2a9c Compare October 3, 2026 11:10
@oclaw
oclaw requested a review from rudrakhp October 3, 2026 11:11
Comment thread src/redis/cache_impl.go Outdated
)
if withLocalCache {
localCacheGuard = limiter.NewLocalCacheGuard(localCache)
publishInvalidations = s.EnableNegativeHits && localCacheGuard != nil

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't publishInvalidations be set without local cache as well? We seem to be setting it only if withLocalCache.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yeah, the condition need to be a bit different. Gated it on the fact if the negative hits are enabled at all, not on the local cache

Comment thread src/redis/fixed_cache_impl.go Outdated
Comment on lines +56 to +59
if ARGV[3] == '1' then
local limit = tonumber(ARGV[4])
if old > limit and new_val <= limit then
redis.call('PUBLISH', ARGV[5], KEYS[1])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since we can override limits in requests, I think it is safer to refund regardless of old>limit?

Suggested change
if ARGV[3] == '1' then
local limit = tonumber(ARGV[4])
if old > limit and new_val <= limit then
redis.call('PUBLISH', ARGV[5], KEYS[1])
if ARGV[3] == '1' and new_val < old then
redis.call('PUBLISH', ARGV[5], KEYS[1])

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Makes sense. Switched to publish of every decrement, should be good

Comment thread src/redis/local_cache_invalidator.go Outdated
Comment on lines +43 to +45
// It owns one raw connection instead of reusing the driver's clients: radix
// pub/sub wraps a single Conn (pooled clients cannot subscribe), and losing
// it must never take the process down — the loop reconnects forever.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you explain a bit further? Don't think this warrants entire connection lifecycle management from scratch. I was expecting existing machinery to be good enough while we provide the invalidation logic only.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

It is still better to use an independent connection management here (this pipeline does not need a pool and is intended to operate on long-living connections), that's why I've implemented this machinery. But you're right that it is possible to reuse pretty much all of the dialing stuff from the driver, so baked it into the custom dialer function returned by the driver

@oclaw
oclaw requested a review from rudrakhp October 5, 2026 10:01
@oclaw
oclaw force-pushed the negative-hits-flag-invalidation branch 2 times, most recently from 2453e8c to 86ea20c Compare October 7, 2026 12:41
oclaw added 2 commits October 22, 2026 17:59
…cal cache on refunds

Signed-off-by: Daniil Kogtev <oclaw@icloud.com>
- reuse dial from driver
- simplify Lua script to publish on every decrement
- publish always on negative hits enabled

Signed-off-by: Daniil Kogtev <oclaw@icloud.com>
@oclaw
oclaw force-pushed the negative-hits-flag-invalidation branch from 86ea20c to 07387a3 Compare October 22, 2026 17:04
@oclaw

oclaw commented Oct 22, 2026

Copy link
Copy Markdown
Author

@rudrakhp any chance you can review it again?

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.

2 participants