Skip to content

fix(api): carry the full project id in cache and lock keys - #6284

Open
WhoamiI00 wants to merge 2 commits into
Agenta-AI:mainfrom
WhoamiI00:fix/full-project-id-cache-keys
Open

fix(api): carry the full project id in cache and lock keys#6284
WhoamiI00 wants to merge 2 commits into
Agenta-AI:mainfrom
WhoamiI00:fix/full-project-id-cache-keys

Conversation

@WhoamiI00

@WhoamiI00 WhoamiI00 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #6166.

_pack in api/oss/src/utils/caching.py cut every scope segment down to the last 12 characters of the id, so two projects whose ids share that suffix shared every cache entry in every namespace — including check_permissions and check_action_access, which decide authorization. Project ids are server-generated UUID4s, so a caller cannot steer a collision and the odds are remote, but one project reading another's cached permission result is not a risk worth carrying by default. Ids are carried whole now.

before and after

The strings in the image are real _pack output, not illustrations.

Two of the three listed blockers had already dissolved

The issue's "What a fix needs" list was written against a full_project_id opt-out that no longer exists — it went away with the vault list cache in #6164 (grep -r full_project_id api/ returns nothing today). That removes the first bullet entirely: with no flag, there is nothing to plumb through invalidate_cache. Readers, writers, the pattern branch of invalidate_cache, and the lock keys all derive from the same _pack, so they move together by construction. cache:p: is built in exactly one place and nothing parses a key back apart.

The tenancy test named in the third bullet also isn't on main any more, so it's a new file rather than an update.

The dash padding stays, so an absent or short id produces the same fixed-width segment it always did — only ids that were actually being cut change shape.

The lock namespace, and the one trade-off

This is the part that isn't just a key change. During a rolling deploy, pods still on the previous release take the truncated key, so a lock held only under the new key would not exclude them — mutual exclusion would be lost for the length of the deploy.

Taking the second of the two options in the issue, lock operations cover both keys for one release. locking.py is the only caller of _pack outside caching.py, and all of the lock call sites (eval runtime, attachment sweep, account creation, and the EE spans/sessions/events/billing routers) funnel through its three functions, so the transition lives in one module and is marked for deletion next release.

The ordering is what makes it correct: the legacy key is claimed first. A pod on the previous release sets only that one, so taking it is what makes the two generations exclude each other; claiming it second would let both hold their own key and enter together. A caller that then loses the race on the primary key releases the legacy key it just took, so a failed acquire doesn't block the section for a full TTL.

The trade-off worth your explicit sign-off: while that cover is in place, two projects with colliding ids keep serializing against each other on locks. That is deliberate — letting two pods into the same critical section is worse than two unrelated tenants queueing — and it ends when the cover is removed. Cache keys, where the permission caches live and where the security consequence is, separate immediately. There's a test pinning this so it's a decision on record rather than a surprise.

If you'd rather drain than dual-cover, the whole transition is _lock_keys' second return value plus three legacy_key branches, and I'm happy to strip it.

Coordination

#6192 is open against the same id-normalization block in _pack (wildcards for an omitted user_id under pattern=True). The two are orthogonal — I kept this strictly to the truncation and will rebase if that one lands first.

Testing

Verified locally

pytest oss/tests/pytest/unit   ->  2685 passed, 73 skipped
pytest ee/tests/pytest/unit    ->   347 passed
ruff format --check / ruff check on the three changed files -> clean

(The four test_web_entrypoint_email_env.py failures on my machine are a CRLF checkout of web/entrypoint.sh, unrelated to this branch and green in CI.)

I also printed real _pack output for two colliding ids to confirm the before/after keys in the image are genuine rather than hand-written.

Added or updated tests

New api/oss/tests/pytest/unit/utils/test_cache_key_tenancy.py — 14 tests, the file the issue asks for:

  • colliding project ids and colliding user ids no longer share a cache key; the whole id is present
  • an invalidation pattern is scoped to one project
  • absent / short / exactly-12-character ids keep their historical segment
  • the opt-in legacy shape still collides — that is what makes it transitional
  • locks: ordinary projects don't block each other; the same project still excludes itself; a holder on the previous release blocks this one; a blocked acquire doesn't strand the legacy key; release clears both generations; renew keeps both alive; a short id takes only one key (a double SET NX on one key would make every such acquire look blocked)

fakeredis only runs Lua with the optional lupa backend, which isn't a dependency here, so the two ownership scripts are supplied as an equivalent shim in the fixture — acquire_lock / renew_lock / release_lock themselves run as written. No new dependency.

QA follow-up

The deploy itself is the thing to watch, and it is a one-time event:

  • Every cache namespace goes cold at once when this ships (5-minute TTLs, so it refills quickly). Worth being aware of as a brief load bump on the DB rather than a correctness concern.
  • During the rolling deploy, confirm eval-runtime jobs are not double-claimed. That is exactly what the legacy-key cover is there to prevent, and it is the behaviour I could only test against fakeredis.
  • The transitional cover should be removed once no pod predating this change is running. It is marked REMOVE in locking.py.

Demo

N/A — backend only. The image above shows the key shape before and after.

Checklist

  • I have included a video or screen recording for UI changes, or marked Demo as N/A
  • Relevant tests pass locally
  • Relevant linting and formatting pass locally
  • I have signed the CLA, or I will sign it when the bot prompts me

`_pack` cut every scope segment down to the last 12 characters of the id,
so two projects whose ids share that suffix shared every cache entry in
every namespace that did not opt out — including `check_permissions` and
`check_action_access`, which decide authorization. Project ids are
server-generated UUID4s, so a caller cannot steer a collision and the odds
of one arising are remote, but one project reading another's cached
permission result is not a risk worth carrying by default.

Ids are carried whole now. Readers, writers, the pattern branch of
`invalidate_cache` and the lock keys all derive from this one function, so
they move together and no namespace is left unable to clear its own
entries. The dash padding stays, so an absent or short id produces the
same fixed-width segment it always did.

The lock namespace is the one place the key shape cannot simply change:
during a rolling deploy, pods still on the previous release take the
truncated key, and a lock held only under the new key would not exclude
them. Lock operations therefore cover both keys for one release, claiming
the legacy key first — a pod on the previous release sets only that one,
so taking it is what makes the two generations exclude each other.

That cover keeps colliding projects serializing against each other on
locks until it is removed, which is deliberate: letting two pods into the
same critical section is worse than two unrelated tenants queueing. Cache
keys, where the permission caches live, separate immediately.

Closes Agenta-AI#6166
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. bug python Pull requests that update Python code tests labels Aug 25, 2026
@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

@WhoamiI00 is attempting to deploy a commit to the agenta projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

✅ Thanks @WhoamiI00! This PR now meets the contribution requirements and has been reopened. A maintainer will review it soon.

@github-actions github-actions Bot added the incomplete-pr PR is missing required template sections or a demo recording label Aug 25, 2026
@github-actions github-actions Bot closed this Aug 25, 2026
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 398f52b2-008b-4e0f-8db8-13c2c65bc848

📥 Commits

Reviewing files that changed from the base of the PR and between a09a0f0 and 1d6e9df.

⛔ Files ignored due to path filters (1)
  • .github/pr-assets/6166-full-project-id-cache-keys.png is excluded by !**/*.png
📒 Files selected for processing (3)
  • api/oss/src/utils/caching.py
  • api/oss/src/utils/locking.py
  • api/oss/tests/pytest/unit/utils/test_cache_key_tenancy.py

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved cache isolation by preserving complete project and user identifiers, preventing collisions between similarly named resources.
    • Maintained compatibility with existing locks during rolling deployments, reducing interruptions and preventing conflicting operations.
    • Improved lock renewal, release, and cleanup behavior across deployment transitions.
    • Preserved established behavior for short or unavailable identifiers.

Walkthrough

The change preserves full project and user IDs in cache keys by default. It adds opt-in legacy truncation and dual-key lock operations for rolling deployments. Unit tests cover cache isolation, lock compatibility, ownership, cleanup, renewal, release, and short scopes.

Changes

Cache and lock compatibility

Layer / File(s) Summary
Full cache scope formatting
api/oss/src/utils/caching.py, api/oss/tests/pytest/unit/utils/test_cache_key_tenancy.py
_scope preserves full identifiers by default and retains padding for short or absent values. _pack forwards the legacy truncation option. Tests cover isolation and key formatting.
Dual-key lock operations
api/oss/src/utils/locking.py
Lock acquisition checks legacy and current keys. Renewal and release use owner-aware operations for both keys. Failed acquisition removes a newly claimed legacy key.
Lock compatibility validation
api/oss/tests/pytest/unit/utils/test_cache_key_tenancy.py
Tests cover project isolation, legacy-key blocking, failed-acquisition cleanup, release, renewal, and single-key handling for short scopes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant acquire_lock
  participant Redis
  participant lock_helpers
  acquire_lock->>Redis: Claim legacy lock key
  acquire_lock->>Redis: Claim current lock key
  Redis-->>acquire_lock: Return acquisition result
  acquire_lock->>lock_helpers: Renew or release owner keys
  lock_helpers->>Redis: Update both lock keys
Loading
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot removed the incomplete-pr PR is missing required template sections or a demo recording label Aug 25, 2026
@github-actions github-actions Bot reopened this Aug 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug python Pull requests that update Python code size:L This PR changes 100-499 lines, ignoring generated files. tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(api): pack cache keys with the full project id outside the vault namespaces

1 participant