Skip to content

feat(bb-distributed-data): atomic Counter primitive for DSQL#207

Closed
hfurkanbozkurt wants to merge 1 commit into
mainfrom
feat/atomic-counter-primitive
Closed

feat(bb-distributed-data): atomic Counter primitive for DSQL#207
hfurkanbozkurt wants to merge 1 commit into
mainfrom
feat/atomic-counter-primitive

Conversation

@hfurkanbozkurt

Copy link
Copy Markdown
Contributor

Summary

Adds an atomic Counter primitive to DistributedDatabase (Aurora DSQL) so applications can get a race-free monotonic sequence number without hand-rolling a SELECT MAX(seq) + 1 read-modify-write.

The gap this closes

Aurora DSQL has no sequences — the framework's own validation layer rejects SERIAL / BIGSERIAL / CREATE SEQUENCE. So code that needs a per-entity sequence number is pushed into:

SELECT COALESCE(MAX(seq), 0) + 1 FROM notes WHERE user_id = $1;  -- then a SEPARATE INSERT

Two concurrent same-user writes both read seq = N and both insert seq = N+1duplicate sequence numbers and unstable ordering. This was surfaced as finding C3-F3 in the PR #194 single-table-bench audit (oidc-dsql-notes task), tagged there as "arguably FRAMEWORK — no atomic sequence/auto-increment helper" and not fixable inside the bench task harness. This PR is that framework fix.

What's added

db.counter(name) returns a Counter with:

  • next(delta = 1) — atomically increment and return the new value
  • current() — read without incrementing (0 if never seen)
  • reset(value = 0) — set to an explicit value

Each mutation is a single atomic upsert wrapped in OCC retry:

INSERT INTO _blocks_counters (name, value) VALUES ($name, $delta)
  ON CONFLICT (name) DO UPDATE SET value = _blocks_counters.value + $delta
  RETURNING value;

DSQL supports INSERT ... ON CONFLICT DO UPDATE ... RETURNING, so there is no read-then-write window for a competing writer. It runs inside transaction({ retryOnConflict: true }) so a DSQL OCC conflict on the row retries transparently.

The backing store is a framework-managed _blocks_counters table:

  • Created by the migration Lambda (admin) on every deploy, exactly like _migrations — no user migration required.
  • provisionAppRole already grants the app role DML on every admin-owned table != _migrations, so the app runtime automatically gets SELECT/INSERT/UPDATE/DELETE on it (the app runtime stays DML-only and never issues DDL).
  • The local DsqlMockEngine creates the same table inside its withDdl init window → identical behavior against PGlite.

Tests

11 new tests in counter.test.ts (real PGlite via DsqlMockEngine, no mocks/stubs):

  • monotonic next() → 1, 2, 3…; custom + negative deltas; current() / unseen = 0; reset(); two named counters isolated
  • OCC: simulateConflict() then next() still returns the correct value and does not double-count (proves the rolled-back attempt is discarded)
  • Concurrency: Promise.all of 25 concurrent next() calls → 25 unique values, min 1 / max 25 (proves no duplicate sequence — the exact oidc-dsql-notes race)
  • input validation (empty name, non-integer delta)
  • DistributedDatabase mock integration: counter table auto-created without migrationsPath; value persists in _blocks_counters

Full package suite: 157/157 passing. Build clean (tsc --build). API report regenerated via api-extractor.

Placement rationale (new PR off main, not bench PR #194)

This is a runtime capability gap in deployed Lambda code. A bench/harness helper runs in the test harness, not the deployed app, so it structurally cannot fix a runtime concurrency bug. The PR #194 audit itself separated this: findings #1–#8 (task-test gaps) belong in the bench PR, while C3-F3 was tagged "arguably FRAMEWORK / not tasks-fixable". The fix archetype — "an atomic increment() / conditional-update on the data block, or a Counter block" — is exactly a framework primitive. Hence a new branch off main.

Opened as draft.

🤖 Generated with Roko AI Agent

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