Skip to content

Add SETNX cache stampede protection#378

Open
silentgeckoaudit3801 wants to merge 3 commits into
Quantarq:mainfrom
silentgeckoaudit3801:perf/cache-setnx-stampede-236
Open

Add SETNX cache stampede protection#378
silentgeckoaudit3801 wants to merge 3 commits into
Quantarq:mainfrom
silentgeckoaudit3801:perf/cache-setnx-stampede-236

Conversation

@silentgeckoaudit3801

Copy link
Copy Markdown

Fixes #236.

This adds TTL-bound cache stampede protection to get_cached_or_fetch:

  • cache misses attempt SET :refresh-lock NX EX 5 before refreshing
  • the lock holder computes and writes the cached value as before
  • concurrent miss callers poll the cache for up to five seconds and return the value published by the lock holder before falling back to their own fetch
  • Redis read/claim/write failures still degrade to the existing fetch behavior
  • focused static coverage locks the SETNX claim and wait-before-fetch wiring

I did not run local pytest in this workspace because we are intentionally avoiding dependency installation and project toolchain execution.



def test_cache_miss_uses_setnx_refresh_claim():
assert "claim_key = f\"{key}:refresh-lock\"" in CACHE_SOURCE

def test_cache_miss_uses_setnx_refresh_claim():
assert "claim_key = f\"{key}:refresh-lock\"" in CACHE_SOURCE
assert "nx=True" in CACHE_SOURCE
def test_cache_miss_uses_setnx_refresh_claim():
assert "claim_key = f\"{key}:refresh-lock\"" in CACHE_SOURCE
assert "nx=True" in CACHE_SOURCE
assert "ex=_REFRESH_CLAIM_SECONDS" in CACHE_SOURCE
assert "claim_key = f\"{key}:refresh-lock\"" in CACHE_SOURCE
assert "nx=True" in CACHE_SOURCE
assert "ex=_REFRESH_CLAIM_SECONDS" in CACHE_SOURCE
assert "_REFRESH_CLAIM_SECONDS = 5" in CACHE_SOURCE


def test_concurrent_miss_waits_for_claimer_before_fetching():
assert "if not claimed:" in CACHE_SOURCE
assert "if not claimed:" in CACHE_SOURCE
assert "refreshed = await _wait_for_refresh(client, key)" in CACHE_SOURCE
assert "if refreshed is not None:" in CACHE_SOURCE
assert "return refreshed" in CACHE_SOURCE
assert "refreshed = await _wait_for_refresh(client, key)" in CACHE_SOURCE
assert "if refreshed is not None:" in CACHE_SOURCE
assert "return refreshed" in CACHE_SOURCE
assert "value = await fetch_fn()" in CACHE_SOURCE


def test_wait_loop_polls_cached_value_with_deadline():
assert "deadline = asyncio.get_running_loop().time() + _REFRESH_CLAIM_SECONDS" in CACHE_SOURCE

def test_wait_loop_polls_cached_value_with_deadline():
assert "deadline = asyncio.get_running_loop().time() + _REFRESH_CLAIM_SECONDS" in CACHE_SOURCE
assert "await asyncio.sleep(_REFRESH_POLL_SECONDS)" in CACHE_SOURCE
def test_wait_loop_polls_cached_value_with_deadline():
assert "deadline = asyncio.get_running_loop().time() + _REFRESH_CLAIM_SECONDS" in CACHE_SOURCE
assert "await asyncio.sleep(_REFRESH_POLL_SECONDS)" in CACHE_SOURCE
assert "cached = await _read_cached_value(client, key)" in CACHE_SOURCE No newline at end of file
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.

Stampede protection via SETNX early-claim

2 participants