Skip to content

feat(filler): opt-in partition-parallel drain for large sales-filter backlogs#71

Open
igorls wants to merge 1 commit into
atomicassets:mainfrom
igorls:feat/parallel-sales-filter-drain
Open

feat(filler): opt-in partition-parallel drain for large sales-filter backlogs#71
igorls wants to merge 1 commit into
atomicassets:mainfrom
igorls:feat/parallel-sales-filter-drain

Conversation

@igorls

@igorls igorls commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Problem

A deep catchup, bulk load, or extended drain outage leaves atomicmarket_sales_filters_updates with a multi-million-row backlog. The drain is (correctly) single-flight — the 1.7.11 advisory lock exists because concurrent unpartitioned drains could recompute the same sale from two backends and deadlock on atomicmarket_sales_filters — but that leaves no recovery path that uses the hardware: the backlog clears at single-connection random-read speed while /v2/sales filters go stale. On our production WAX deployment (460M assets), a 24.9M-row post-catchup backlog drained at a net ~17 rows/s ≈ weeks.

The recompute parallelizes perfectly across disjoint sales, which hash-partitioning the queue provides.

Changes (no steady-state behavior change; nothing runs unless an operator launches it)

  • update_atomicmarket_sales_filters_partition(part_count, part_index, batch_size) — the 1.7.11 drain restricted to sale rows with sale_id % part_count = part_index. The recompute CTE block is byte-for-byte 1.7.11 (preserving the 1.3.3-identical output guarantee); only the claim predicate and lock protocol differ.
  • normalize_atomicmarket_sales_filters_offers(batch_size) — converts queued offer rows into the equivalent queued sale rows (the same offer→sales resolution the stock drain performs per batch), so partition workers can own them. 1.7.11 seq-guard claim/release protocol.
  • Index on atomicmarket_sales_filters (assets_contract, offer_id) — the drain's offer→sales resolution was a hash join scanning the multi-GB filter partitions on every batch containing offer rows; now it is index probes. This also speeds the stock drain. (Live-DB recipe: pre-build per-partition CONCURRENTLY under the same names; the migration's CREATE INDEX IF NOT EXISTS then attaches without rebuilding — documented in the migration header.)
  • bin/drain-sales-filters.js [--partitions N] [--batch B] [--skip-offers] — operator runner: normalizes offers, runs N partition workers until empty, ANALYZEs the churned tables after.

Lock protocol

stock drain   EXCLUSIVE(global)                          -- 1.7.11, unchanged
normalizer    SHARED(global) + EXCLUSIVE(normalize key)
worker i      SHARED(global) + EXCLUSIVE(partition key, i)

Workers coexist with each other (shared-shared); the stock drain no-ops while any worker batch is in flight and vice versa (its normal contended path — the filler retries next cycle); duplicate workers on a partition no-op. All xact-scoped try-locks: no waiting, nothing orphaned on crash, every claim/recompute/release is one crash-safe transaction exactly like 1.7.11. Workers never claim asset rows (one asset fans out to arbitrary sales — not partitionable), so the stock drain keeps owning that path.

Validation

Integration tests (sales-filters-parallel-drain.integration.test.ts, full suite 332 passing):

  • PARITY: partition recompute recreates a byte-identical filter row vs the stock drain (to_jsonb equality after delete + re-enqueue + partition drain).
  • Scope: workers consume only their own hash partition; asset/offer rows untouched.
  • Lock matrix on real concurrent backends: stock⊣worker, worker⊣stock, duplicate-worker no-op, cross-partition concurrency.
  • Offer normalization end-to-end; invalid-args rejection.

Production (WAX mainnet, 460M assets, live filler at head throughout):

  • 8 workers drained 3,276,752 queued sales in 229s (~14,300 sales/s vs ~17/s net before) — the projected-weeks backlog cleared in under 4 minutes; second pass: 504k offer rows normalized in ~7s via the new index (previously a multi-GB scan per batch), their 78,627 sales drained in 27s.
  • Reader stayed at head the whole time (zero stalls, zero filler restarts); the filler's own drain resumed owning the live trickle automatically afterwards.

Notes for review

@igorls
igorls marked this pull request as ready for review June 10, 2026 02:43
Copilot AI review requested due to automatic review settings June 10, 2026 02:43

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds an opt-in, partition-parallel draining path to speed up recovery from large atomicmarket_sales_filters_updates backlogs, including SQL functions, an operator CLI, and integration coverage for lock/disjointness/parity guarantees.

Changes:

  • Introduces update_atomicmarket_sales_filters_partition and normalize_atomicmarket_sales_filters_offers, plus an index to speed offer→sale resolution.
  • Adds a drain-sales-filters CLI to run multi-worker partition drains (and optional offer normalization).
  • Adds integration tests validating parity with the stock drain, partition scoping, and advisory-lock behavior.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/filler/handlers/atomicmarket/sales-filters-parallel-drain.integration.test.ts New integration tests for partition drain semantics, parity, and advisory-lock protocol.
src/bin/drain-sales-filters.ts New operator CLI to normalize offer rows and drain queued sale rows in parallel by hash partition.
definitions/migrations/1.7.13/database.sql Bumps DB version to 1.7.13 with brief migration notes.
definitions/migrations/1.7.13/atomicmarket.sql Adds index + new PL/pgSQL functions implementing normalization and partition-parallel draining.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread definitions/migrations/1.7.13/atomicmarket.sql Outdated
Comment thread definitions/migrations/1.7.13/atomicmarket.sql Outdated
Comment thread definitions/migrations/1.7.13/atomicmarket.sql Outdated
Comment thread definitions/migrations/1.7.13/atomicmarket.sql Outdated
igorls added a commit to igorls/atomicassets-api that referenced this pull request Jun 10, 2026
…tionale

Review follow-ups on atomicassets#71 (comment-only, no behavior change):
- Replace the loose 'pre-build under the same names' note with the actual
  ON ONLY + CREATE INDEX CONCURRENTLY + ATTACH PARTITION recipe (attachment
  matches on definition equivalence, not names) as used for the production
  validation run.
- State the plain-temp-table rationale (1.7.11 precedent: reused pool
  connections, no DROP-IF-EXISTS notice spam; the claimed (key,seq) set must
  survive across statements so a CTE chain cannot replace it; per-call DDL is
  negligible at drain cadence).
@igorls

igorls commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

v2 port opened as a separate PR per the rc2 cherry-pick workflow: the same three commits apply cleanly onto feat/atomicassets-v2 (merge-base is v1.7.11; the 1.7.11 migration there is byte-identical to main's; 2.0.0 doesn't touch the drain), with the migration renumbered 1.7.132.0.1 so it still applies to databases initialized from the 2.0.0-rc tags. Full v2 suite + the new tests: 349 passing on PostgreSQL 18.

@robrigo robrigo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review (FACINGS) — LGTM with one merge-sequencing flag.

Verified mechanically, not just by reading:

  • The byte-for-byte recompute claim checks out. Extracted the recompute block from 1.7.11/atomicmarket.sql and from update_atomicmarket_sales_filters_partition, diffed them: the only deltas are the RELEASE section (workers delete only their claimed _part_sales; stock deletes its three claim tables) and comments. The filter math, create_atomicmarket_sales_filter call, and nx/nb flags are identical.
  • Lock semantics verified against the 1.7.11 source: stock takes pg_try_advisory_xact_lock(hashtext('update_atomicmarket_sales_filters')) (exclusive); workers/normalizer take the same key _shared plus their own exclusive sub-keys. Shared-shared coexists, shared-exclusive conflicts — matches the documented matrix, and the integration tests exercise it on real concurrent backends.
  • Ran the suite on PostgreSQL 18 (our production CNPG major): 9/9 new parallel-drain tests pass (incl. the PARITY byte-identical check), full integration 332 passing, typecheck clean.
  • The hardcoded /home/node/app/config/... path matches the existing filler.ts/server.ts convention; the index pre-build recipe mirrors the 1.7.12 pattern. Worker disjointness reasoning (sale-keyed writes, asset rows left to the stock drain) is sound.

One sequencing hazard before merge: main has no 1.7.12 yet — that's #70, still open. If this merges first and any DB upgrades to 1.7.13, the runner's strictly-greater-than filter means #70's 1.7.12 would be skipped forever on that DB when it lands later. Merge #70 before #71, or renumber this one above whatever #70 ships as.

The production numbers (24.9M backlog, weeks → 4 minutes) speak for themselves. Nice work.

@robrigo
robrigo requested a review from Copilot June 10, 2026 12:59
robrigo pushed a commit that referenced this pull request Jun 10, 2026
…tionale

Review follow-ups on #71 (comment-only, no behavior change):
- Replace the loose 'pre-build under the same names' note with the actual
  ON ONLY + CREATE INDEX CONCURRENTLY + ATTACH PARTITION recipe (attachment
  matches on definition equivalence, not names) as used for the production
  validation run.
- State the plain-temp-table rationale (1.7.11 precedent: reused pool
  connections, no DROP-IF-EXISTS notice spam; the claimed (key,seq) set must
  survive across statements so a CTE chain cannot replace it; per-call DDL is
  negligible at drain cadence).
robrigo pushed a commit that referenced this pull request Jun 10, 2026
….0.1

The v2 line's migration chain is at 2.0.0 (rc tags already initialize databases
at that version), so the main-line 1.7.13 directory would sort before 2.0.0 and
never apply to them. Renumbered to 2.0.1; SQL content unchanged from PR #71.
robrigo added a commit that referenced this pull request Jun 10, 2026
port(v2): partition-parallel drain for large sales-filter backlogs (#71 on the v2 line)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 4 changed files in this pull request and generated no new comments.

…backlogs

A deep catchup or bulk load leaves atomicmarket_sales_filters_updates with a
multi-million-row backlog that the (correctly) single-flight drain clears at
random-read I/O speed - days to weeks of stale /v2/sales filters. The recompute
parallelizes perfectly across disjoint sales:

* update_atomicmarket_sales_filters_partition(part_count, part_index, batch):
  the stock drain restricted to sale rows with sale_id % part_count = part_index.
  Recompute CTE block is byte-for-byte the stock 1.7.11 recompute. Workers take
  the stock drain's advisory key SHARED plus an exclusive per-partition sub-lock:
  N workers coexist, never with the stock drain, duplicates no-op. Nothing runs
  unless an operator launches it.
* normalize_atomicmarket_sales_filters_offers(batch): converts queued offer rows
  into equivalent queued sale rows so workers can own them; seq-guard protocol.
* index atomicmarket_sales_filters (assets_contract, offer_id): turns the drain's
  offer->sales resolution from a multi-GB hash join into index probes. Also speeds
  the stock drain.
* bin/drain-sales-filters: operator runner - normalizes offers, runs N partition
  workers to empty, ANALYZEs after.

Additive only (new functions + index + CLI); does not redefine the stock drain,
so it composes with the 1.7.13 two-lane prio queue (atomicassets#73). Every queue interaction
uses explicit column lists and key/seq deletes, so the prio column added by 1.7.13
is transparent here. The partition CLI claims by hash partition + seq
(prio-agnostic) by design: it is an operator bulk-recovery tool where draining the
whole backlog matters, not steady-state freshness ordering.

Rebased onto main and renumbered 1.7.13 -> 1.7.18: the original 1.7.13 slot was
taken upstream by atomicassets#73, and atomicassets#70's transfers-PK converge already landed as 1.7.12 -
which resolves the merge-sequencing hazard flagged in review. 1.7.18 sorts after
the current head migration (1.7.17).

Validated against a production WAX dataset (multi-million-row backlog).
@igorls
igorls force-pushed the feat/parallel-sales-filter-drain branch from f1b89fe to 63fbf1b Compare June 24, 2026 01:52
@igorls

igorls commented Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and renumbered the migration 1.7.13 → 1.7.18, addressing the merge-sequencing flag from your review:

Compatibility with #73's prio lane (which merged after your review): this PR is additive only — it adds update_atomicmarket_sales_filters_partition, normalize_atomicmarket_sales_filters_offers, the (assets_contract, offer_id) index, and the CLI; it does not redefine the stock drain, so it composes with the prio queue rather than clobbering it. Every atomicmarket_sales_filters_updates interaction uses explicit column lists and key/seq-based deletes, so the prio SMALLINT column #73 added is transparent here. The partition CLI deliberately claims by hash partition + seq (prio-agnostic): it's an operator bulk-recovery tool where draining the whole backlog is the point, not steady-state freshness ordering. The PARITY test compares against the stock drain, whose computed filter rows are order-independent, so it still holds.

The two prior commits are collapsed into one clean commit on top of main to keep the re-review diff tight (4 files, +770). Worth a re-run of the parallel-drain suite on your PG18 setup to confirm green against the post-#73 schema.

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.

3 participants