Commit graph

5 commits

Author SHA1 Message Date
Johannes Millan
5ed1f2c5d0
fix(supersync): prevent migrator advisory-lock leak on deploy timeout (#9126)
A timed-out `docker compose run` in deploy.sh SIGTERMs the compose CLI but
can leave the migrator container (and its DB connection) running detached,
which keeps Prisma's session-level migration advisory lock held and wedges
the next deploy with P1002.

- deploy.sh: run_migrator() names each one-off migrator container from a
  single MIGRATOR_NAME_PREFIX and force-removes it (inline + $$-scoped EXIT
  sweep) so a timed-out run can't orphan it and leak the lock; add timeout -k.
- migrate-deploy.sh: recognize P1002 advisory-lock timeouts and print
  copy-paste diagnosis + cleanup guidance instead of a generic exit 1; never
  auto-terminate a backend (an active CONCURRENTLY build legitimately holds it).
- tests: P1002 fake-npx case + non-vacuous recovery test; assert the
  name/sweep-filter prefix single-source-of-truth invariant.
- env.example: document the recommended higher MIGRATION_TIMEOUT and the new
  force-remove-on-timeout behavior.
2026-07-17 20:18:54 +02:00
Johannes Millan
f15a20ba8d fix(supersync): make interrupted CONCURRENTLY migrations recoverable
A bare CREATE INDEX CONCURRENTLY aborted mid-build (the in-image step
timeout firing at its 1800s default when a raised MIGRATION_TIMEOUT was
not forwarded, an external stop, or OOM) leaves the migration failed and
an INVALID index of the target name, wedging the deploy. Deployed images
reported only a bare "prisma migrate deploy failed (exit 143)" with no
recovery steps, so operators had no way forward.

- deploy.sh: forward MIGRATE_STEP_TIMEOUT into the migrator so a large
  MIGRATION_TIMEOUT is not silently capped at the image default (1800s)
  and kill a slow CREATE INDEX CONCURRENTLY early (the root cause).
- migrate-deploy.sh: normalize BusyBox `timeout` 143 -> 124 so a step
  timeout hits the timeout branch, not the generic one.
- migrate-deploy.sh: emit_interrupted_recovery_hint() prints copy-paste
  recovery (drop the INVALID index, roll the record back, re-run) for an
  interrupted CONCURRENTLY build, from BOTH the 124 timeout branch (where
  a normalized 143 -- the incident's own signal -- lands) and the generic
  non-gate branch (OOM/137), plus the existing P3009 re-run path. Every
  path prints guidance only and NEVER auto-resolves a bare CREATE.
- env.example: document the now-forwarded MIGRATE_STEP_TIMEOUT knob.
- tests: cover 143 normalization, the incident's 143->124 timeout-branch
  recovery, the P3009 and non-gate bare-create recovery, and the
  forwarded step timeout.

The bare CREATE stays bare and fail-loud on purpose: 20260613000001
ships in v18.11.0-v18.14.0 and is applied natively on healthy deploys,
so converting it to a drop-then-create shape would change its checksum
and break every DB that already applied it.

Recovery for the current wedged deploy (20260613000001):
  DROP INDEX CONCURRENTLY IF EXISTS "operations_entity_ids_gin";
  prisma migrate resolve --rolled-back \
    20260613000001_add_operation_entity_ids_gin_index
then re-run the deploy with the step timeout raised enough for the GIN
build (and clear any idle-in-transaction blocker first).
2026-07-17 17:56:57 +02:00
Johannes Millan
3b0cb9b836 fix(supersync): address multi-agent review of CONCURRENTLY recovery
- Tighten recovery guard to the idempotent drop-then-create shape (require
  both DROP and CREATE INDEX CONCURRENTLY). A bare CREATE (20260511000000) is
  now refused by gate, deterministically — matching its documented fail-loud
  intent instead of relying on the CREATE erroring by accident. Reconciles
  README with the committed migration + migration-sql.spec.ts.
- Escape single quotes in print_manual_recovery so the printed escape-hatch
  commands are copy-pasteable for SQL containing quotes (e.g. the full-state
  WHERE op_type IN ('SYNC_IMPORT', ...) migration).
- Fix MIGRATE_LOG temp-file leak across retry attempts.
- Add an in-script per-step timeout (with_timeout) so the Dockerfile CMD /
  helm initContainer paths (no outer timeout) can't hang forever on a blocked
  concurrent build; 124 fails loudly.
- Harden parse_failing_migration: sentence-anchored P3009 parse + reject
  names outside the migration charset (path-traversal defence).
- Gate also accepts SQLSTATE 25001 (stable Postgres contract) not just the
  localizable English message.
- MAX_ATTEMPTS -> documented tight bound (6); fail_loudly wording fixed
  (was contradictory for the non-CONCURRENTLY case).
- Tests: bare-CREATE refusal, P3009 decoy-token parser hardening; trim
  migration-sql.spec.ts to the architectural-invariant subset (no hardcoded
  names) per 'test behavior not implementation'.

Full server suite: 36 files, 727 passed / 5 skipped.
2026-05-15 22:03:25 +02:00
Johannes Millan
3c41be964c fix(supersync): generic in-image CONCURRENTLY migration recovery
prisma migrate deploy wraps each migration in a transaction; CONCURRENTLY
index migrations fail it (P3018/25001) and later deploys then stick (P3009).
Recovery was duplicated and migration-name-hardcoded in the host deploy.sh
and the in-image migrate-deploy.sh. The host script self-updates only via a
best-effort git pull, so a stale host deploy.sh had no recovery branch for a
new CONCURRENTLY migration and failed the deploy (the reported incident).

- migrate-deploy.sh: single, name-agnostic recovery. Parses the failing
  migration from Prisma's own output, gates on the txn-block/P3009 signature
  AND the migration's own SQL containing INDEX CONCURRENTLY, runs that SQL
  out-of-band statement-by-statement, and only marks it applied if every
  statement succeeded; otherwise fails loudly with manual steps. Bounded
  retry loop; aborts instead of looping on re-failure.
- deploy.sh: ~290 lines of hardcoded host-side recovery removed; now invokes
  the in-image scripts/migrate-deploy.sh (always version-locked to
  prisma/migrations in the pulled image) and keeps only timeout/exit policy.
- tests: drive the script end-to-end via a fake npx (P3018, stuck P3009,
  non-CONCURRENTLY refusal, statement-failure, re-failure abort, genuine
  error passthrough, multi-migration chain); migration-sql.spec.ts updated
  to the new contract.
- prisma/migrations/README.md: authoring rules the recovery relies on.

Design: docs/plans/2026-05-15-generic-concurrently-migration-recovery-design.md
2026-05-15 21:43:49 +02:00
Johannes Millan
2d9988dd73
perf(sync): SuperSync server speed + correctness hardening (#7621)
* docs(sync): add super sync server perf plan

* perf(sync): implement supersync server perf phases

* fix(sync): bracket auth cache invalidation

* fix(sync): avoid empty replay state stringify

* fix(sync): harden supersync batch uploads

* fix super sync review findings

* fix(sync): guard payload bytes backfill rollout

* perf(sync): speed up payload_bytes backfill and index its scan

Raise the backfill batch size (DEFAULT 5->500, MAX 25->1000) so a
100M-row operations table backfills in minutes rather than tens of
hours. Add a CONCURRENTLY partial index on (user_id, id) WHERE
payload_bytes = 0: it drains to empty post-backfill so the boot-time
backfill self-check and the BOOL_OR quota probe stop doing a full
sequential scan to prove absence, and it makes the backfill's per-user
keyset paging a true index seek. Wire the new concurrent-index
migration into both deploy scripts' P3018 recovery path. Add
migration-SQL guard tests for the ADD COLUMN (metadata-only fast path)
and the new partial index.

* fix(sync): bound auth cache invalidation map and bracket every delete

The auth verification cache's invalidationVersions map grew one entry
per lifetime-invalidated user with no eviction (unbounded heap on a
long-lived single replica). Cap it at the same 10k LRU bound as the
entries map, re-inserting the just-invalidated user at the MRU tail so
the CAS race protection still holds for the only window that matters
(one DB round trip). Bracket the passkey/magic-link registration
cleanup deletes with pre+post invalidate to match the documented
convention, and invalidate on verifyEmail so a freshly-verified user
isn't denied for up to the cache TTL.

* perf(sync): skip the redundant exact replay-state measurement

The delta accounting is a proven over-estimate of the serialized state
size, so when the running bound stays within the cap the true size is
too and the final exact JSON.stringify is provably redundant. Skip it
in that case (still measure-and-throw whenever the bound does not prove
safety). This collapses the common small/incremental replay back to
zero expensive full stringifications, matching the old per-op loop
instead of regressing it. Name the entity-key JSON overhead constant
and document that assertReplayStateSize's return value is load-bearing.

* refactor(sync): split processOperationBatch into pipeline stages

Extract the 297-line batch upload method into a thin orchestrator plus
six named single-responsibility stage helpers (validate+clamp, intra-
batch dedupe, classify existing duplicates, conflict-detect, reserve
seq + insert, full-state clock). Behavior-preserving: every stage
writes terminal rejections into the shared results array by index and
the two empty-set guards short-circuit exactly as before. Also share
the timestamp clamp, the duplicate-op SELECT, and the merged
full-state clock persistence between the batch and legacy paths so
they cannot silently diverge.

* test(sync): pin batch error-code divergence and aggregate-once

Strengthen the intra-batch duplicate test to assert same-id /
different-content yields DUPLICATE_OPERATION (deliberate divergence
from the legacy INVALID_OP_ID), and document the divergence in the
plan. Replace the single-full-state aggregate test with two
full-state ops + a spy asserting _aggregatePriorVectorClock runs
exactly once and last-write-wins — the old test could not catch a
per-op-aggregate regression. Add a makeOp fixture factory. Correct
the plan's overstated replay-stringification numbers.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: johannesjo <1456265+johannesjo@users.noreply.github.com>
2026-05-15 17:24:16 +02:00