Skip to content

fix(cds-caching): atomic UPSERT in KeyvCDS.set to stop PK-collision error noise (#3102) - #3107

Merged
jung-thomas merged 4 commits into
DEVfrom
worktree-fix-3102-cds-caching-upsert
Nov 11, 2026
Merged

jung-thomas merged 4 commits into
DEVfrom
worktree-fix-3102-cds-caching-upsert

Conversation

@jung-thomas

Copy link
Copy Markdown
Contributor

Fixes #3102.

Problem

In PROD (tutorials-srv), cds-caching logs recurring error-level lines when two HA instances concurrently write the same cache key — seen for the KG rate-limiter (kg:rl:api:auth:..., kg:rl:homepage:anon:<ip>):

KeyvCDS set error for key kg:rl:...: unique constraint violated:
Table(PLUGIN_CDS_CACHING_CACHESTORE), Index(_SYS_TREE_CS_#..._#P0) ... key: ID=kg:rl:...

Root cause: the vendored plugin's KeyvCDS.set() does a read-then-create/update (TOCTOU). Under multi-instance CF, two instances both read "no row", both take the INSERT branch, and the second INSERT collides on the CacheStore primary-key unique index. The plugin logs the collision at error and swallows it — so it's functionally fail-open (the rate limiter already over-allows under races, which is acceptable), but it spams prod error monitoring and drops the losing write.

Fix

A committed patch-package patch (patches/cds-caching+2.1.0.patch) replaces that branch with a single atomic UPSERT:

await db.run(cds.ql.UPSERT.into(entity).entries({ ID: key, value, expiresAt }));

CacheStore has a single-column PK (ID), so this compiles to HANA UPSERT ... WITH PRIMARY KEY / SQLite INSERT OR REPLACE — last-write-wins, no read-then-write race, one fewer round-trip. The patch re-applies on every npm ci / npm install via a new postinstall hook (prepare/git-hooks left intact).

Scope is deliberately set() only. get/has/delete also read-then-write, but they only ever delete (idempotent under concurrency) — they don't collide on an INSERT, so they produce no error noise.

Tests

  • srv/lib/__tests__/keyv-cds-upsert.test.js drives the real KeyvCDS against a minimal in-memory CAP fixture (srv/lib/__tests__/__fixtures__/keyv-cds-app/). The decisive test models the multi-instance TOCTOU window deterministically (stubs db.read to miss while the row physically exists); it fails on the unpatched plugin (1 set error + lost write) and passes on the patched plugin (0 errors, value persisted). Verified both directions.
  • cds compile srv is clean; the fixture's CacheStore does not leak into the main CAP model (fixture package.json boundary + __tests__ convention).

Upstream

The same read-then-insert is still on cds-caching main. Upstreamed as mikezaschka/cds-caching#44 (issue) + mikezaschka/cds-caching#45 (PR). Drop condition: delete the patch once a published release carries the fix and we adopt it. Docs: docs/developers/reference/cds-caching-store.md -> "KeyvCDS.set UPSERT patch (#3102)".

Reviewed

Whole-branch review (fresh agent): APPROVE, no Critical/Important findings; patch apply, hunk math, cds.ql.UPSERT resolution, and the RED->GREEN regression all independently verified.

…rror noise (#3102)

KeyvCDS.set did a read-then-create/update (TOCTOU). Under multi-instance
CF two instances both read 'no row', both INSERT the same CacheStore key,
and the second INSERT violates the PK unique index — logged at error level
(the PROD kg:rl:* rate-limiter noise) with the losing write dropped.

Replace that branch with an atomic single-key UPSERT (last-write-wins) via
a committed patch-package patch against cds-caching@2.1.0. Add a regression
test driving the real KeyvCDS against an in-memory CAP fixture, modelling
the TOCTOU window deterministically.
…3102)

Add patch-package devDep + a postinstall hook so the cds-caching@2.1.0
UPSERT patch re-applies on every npm ci / npm install. prepare (git hooks)
is left intact; both hooks fire on install.
@jung-thomas
jung-thomas merged commit 4475114 into DEV Nov 11, 2026
8 checks passed
@jung-thomas
jung-thomas deleted the worktree-fix-3102-cds-caching-upsert branch November 11, 2026 19:23
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.

1 participant