@rotorsoft/act-pg 1.16.1 → 1.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -16,7 +16,6 @@ import {
16
16
  } from "@rotorsoft/act-crypto";
17
17
  import pg from "pg";
18
18
  var logger = log();
19
- var SOURCE_METACHARACTER_CLASS = "[]^$.*+?()[{}|\\\\]";
20
19
  var { Pool, types } = pg;
21
20
  var JSONB_OID = types.builtins.JSONB;
22
21
  var scopedTypes = {
@@ -336,6 +335,11 @@ var PostgresStore = class {
336
335
  `ALTER TABLE ${this._fqs}
337
336
  ADD COLUMN IF NOT EXISTS correlated_at int;`
338
337
  );
338
+ await client.query(
339
+ `UPDATE ${this._fqs}
340
+ SET correlated_at = (SELECT COALESCE(MAX(id), -1) FROM ${this._fqt})
341
+ WHERE correlated_at IS NULL;`
342
+ );
339
343
  await client.query(
340
344
  `DO $$
341
345
  BEGIN
@@ -389,6 +393,11 @@ var PostgresStore = class {
389
393
  ON ${this._fqs} (lane, priority DESC, at)
390
394
  WHERE blocked = false AND at < correlated_at;`
391
395
  );
396
+ await client.query(
397
+ `CREATE INDEX IF NOT EXISTS "${this.config.table}_streams_at_ix"
398
+ ON ${this._fqs} (at)
399
+ WHERE blocked = false AND at < correlated_at;`
400
+ );
392
401
  await client.query("COMMIT");
393
402
  logger.info(
394
403
  `Seeded schema "${this.config.schema}" with table "${this.config.table}"`
@@ -648,129 +657,73 @@ var PostgresStore = class {
648
657
  -- and lock EVERY claimable stream for this transaction, starving
649
658
  -- overlapping competing consumers. We only lock the small
650
659
  -- lagging+leading candidate slice, down in the "locked" CTE.
651
- -- Eligibility, split by SOURCE CLASS rather than expressed as one
652
- -- disjunction (#1448). The three classes are mutually exclusive, so
653
- -- the UNION ALL returns exactly what the old OR-chain did \u2014 but each
654
- -- arm now carries a single, planner-legible predicate.
655
660
  --
656
- -- That matters for one arm in particular. Written as
657
- -- "s.source IS NULL OR (... AND e.stream = s.source) OR (... ~ ...)",
658
- -- the equality is buried inside a disjunction and is NOT sargable:
659
- -- no index on (stream, id) can serve it, so every probe degenerated
660
- -- into a forward scan of the pk from the stream's watermark. With
661
- -- the classes separated, the literal arm \u2014 every per-aggregate
662
- -- .to(e => ({target: e.stream})) reaction, i.e. the overwhelming
663
- -- majority \u2014 becomes an index seek on (stream, id).
661
+ -- Eligibility is a pure subscription-table predicate (#1488).
662
+ -- claim does not read the event log at all: correlate marks the
663
+ -- highest event id that resolves to a target, and at <
664
+ -- correlated_at is the whole question. The probe this replaced ran
665
+ -- an EXISTS against the events table once per eligible row, which
666
+ -- cost O(subscribed streams) per claim per worker no matter how
667
+ -- little work was pending.
668
+ --
669
+ -- The predicate is repeated in each arm below rather than factored
670
+ -- into a shared CTE, and that repetition is load-bearing (#1510). A
671
+ -- CTE referenced more than once is materialized, so LIMIT 8 was
672
+ -- applied to a fully-built 100,000-row result instead of pushing into
673
+ -- the index: measured at 75.6 ms for a claim that returns 8 rows.
674
+ -- Reading the base table in each arm lets the planner stop after the
675
+ -- limit \u2014 19.9 ms with the existing index, 2.0 ms once the
676
+ -- at-ordered partial index below serves the two watermark arms.
664
677
  --
665
- -- Measured at 10k subscribed streams with 10 pending: 5,792 ms
666
- -- before, 10.3 ms after. See libs/act-pg/PERFORMANCE.md for #1448.
667
- eligible AS (
668
- SELECT stream, source, at, priority, lane, correlated_at
678
+ -- A row with no mark compares unknown and is excluded by SQL's own
679
+ -- rules. That is definitional (#1446): a subscription is claimable iff
680
+ -- a mark says so. seed() marks rows that predate the column.
681
+ --
682
+ -- Priority lanes (ACT-102): higher priority first, then
683
+ -- lagging-watermark order. With everyone at priority=0 the ORDER BY
684
+ -- collapses to plain at ASC, so existing workloads see no change.
685
+ --
686
+ -- The lagging frontier is a UNION of two portions (ACT-1223): the
687
+ -- priority portion takes the first (lagging - fair) slots by
688
+ -- priority DESC, at ASC; a fairness reserve then fills fair more
689
+ -- by pure at ASC (priority ignored), excluding the ones already
690
+ -- chosen, so a default-priority lagging stream is never starved out
691
+ -- by sustained higher-priority load.
692
+ prio AS (
693
+ SELECT stream, source, at, lane, TRUE AS lagging
669
694
  FROM ${this._fqs} s
670
- WHERE blocked = false
695
+ WHERE s.blocked = false
696
+ AND s.at < s.correlated_at
671
697
  ${lane_clause}
672
- AND (leased_by IS NULL OR leased_until <= NOW())
673
- AND (deferred_at IS NULL OR deferred_at <= NOW())
698
+ AND (s.leased_by IS NULL OR s.leased_until <= NOW())
699
+ AND (s.deferred_at IS NULL OR s.deferred_at <= NOW())
700
+ ORDER BY s.priority DESC, s.at ASC
701
+ LIMIT ($1::int - $5::int)
674
702
  ),
675
- available AS (
676
- -- Fast arm (#1485): a marked stream answers from the subscription
677
- -- row alone. Read from the base table, NOT from the eligible CTE:
678
- -- it is referenced several times so PG materializes it, and a
679
- -- materialized scan cannot use the partial index this predicate was
680
- -- built for (WHERE blocked = false AND at < correlated_at).
681
- -- The comparison is NULL-safe: an unmarked row compares
682
- -- unknown and is excluded here, falling through to the legacy arms.
683
- SELECT stream, source, at, priority, lane
703
+ fair AS (
704
+ SELECT stream, source, at, lane, TRUE AS lagging
684
705
  FROM ${this._fqs} s
685
706
  WHERE s.blocked = false
686
707
  AND s.at < s.correlated_at
687
708
  ${lane_clause}
688
709
  AND (s.leased_by IS NULL OR s.leased_until <= NOW())
689
710
  AND (s.deferred_at IS NULL OR s.deferred_at <= NOW())
690
- UNION ALL
691
- -- Legacy arms, gated on an absent mark. NULL means "unknown", so
692
- -- an install that predates the column probes the event log exactly
693
- -- as it did before, and each row migrates to the fast arm the first
694
- -- time correlate marks it. Deleted once correlate marks universally.
695
- --
696
- -- Every arm below is watermark-agnostic (#1446): a fresh
697
- -- subscription sits at -1, and e.id > s.at already answers
698
- -- correctly there, since the first event has a greater id.
699
- -- There used to be a fourth arm claiming at < 0 unconditionally,
700
- -- which handed out an empty lease and a no-op ack for every
701
- -- subscription with no matching events yet.
702
- -- Source-less subscription: any non-snapshot event past the
703
- -- watermark counts, so the id index alone answers it.
704
- SELECT stream, source, at, priority, lane FROM eligible s
705
- WHERE s.correlated_at IS NULL
706
- AND s.source IS NULL
707
- AND EXISTS (
708
- SELECT 1 FROM ${this._fqt} e
709
- WHERE e.id > s.at AND e.name <> '${SNAP_EVENT}' LIMIT 1
710
- )
711
- UNION ALL
712
- -- Literal source (no regex metacharacter): exact equality, so
713
- -- "s1" never claims "s12", and (stream, id) is usable.
714
- SELECT stream, source, at, priority, lane FROM eligible s
715
- WHERE s.correlated_at IS NULL
716
- AND s.source IS NOT NULL
717
- AND s.source !~ '${SOURCE_METACHARACTER_CLASS}'
718
- AND EXISTS (
719
- SELECT 1 FROM ${this._fqt} e
720
- WHERE e.stream = s.source AND e.id > s.at
721
- AND e.name <> '${SNAP_EVENT}' LIMIT 1
722
- )
723
- UNION ALL
724
- -- Pattern source (e.g. '^(A|B)$'): POSIX match, so the
725
- -- calculator's static regex reaction is claimed for every stream
726
- -- it anchors. Unavoidably a scan \u2014 but only for the subscriptions
727
- -- that actually declare a pattern.
728
- SELECT stream, source, at, priority, lane FROM eligible s
729
- WHERE s.correlated_at IS NULL
730
- AND s.source IS NOT NULL
731
- AND s.source ~ '${SOURCE_METACHARACTER_CLASS}'
732
- AND EXISTS (
733
- SELECT 1 FROM ${this._fqt} e
734
- WHERE e.id > s.at AND e.name <> '${SNAP_EVENT}'
735
- AND e.stream ~ s.source LIMIT 1
736
- )
711
+ AND s.stream NOT IN (SELECT stream FROM prio)
712
+ ORDER BY s.at ASC
713
+ LIMIT $5
737
714
  ),
738
- -- Priority lanes (ACT-102): higher priority first, then
739
- -- lagging-watermark order. With everyone at priority=0 the
740
- -- ORDER BY collapses to plain at ASC so existing workloads
741
- -- see no behavior change.
742
- --
743
- -- The lagging frontier is a UNION of two portions (ACT-1223): the
744
- -- priority-ordered portion takes the first (lagging - fair) slots
745
- -- by priority DESC, at ASC; a fairness reserve then fills fair more
746
- -- slots by pure at ASC (priority ignored), excluding the ones
747
- -- already chosen, so a default-priority lagging stream is never
748
- -- starved out by sustained higher-priority load. With all
749
- -- priorities equal both portions order by at, a no-op merge.
750
715
  lag AS (
751
- (
752
- SELECT stream, source, at, lane, TRUE AS lagging
753
- FROM available
754
- ORDER BY priority DESC, at ASC
755
- LIMIT ($1::int - $5::int)
756
- )
757
- UNION
758
- (
759
- SELECT stream, source, at, lane, TRUE AS lagging
760
- FROM available
761
- WHERE stream NOT IN (
762
- SELECT stream FROM available
763
- ORDER BY priority DESC, at ASC
764
- LIMIT ($1::int - $5::int)
765
- )
766
- ORDER BY at ASC
767
- LIMIT $5
768
- )
716
+ SELECT * FROM prio UNION SELECT * FROM fair
769
717
  ),
770
718
  lead AS (
771
719
  SELECT stream, source, at, lane, FALSE AS lagging
772
- FROM available
773
- ORDER BY at DESC
720
+ FROM ${this._fqs} s
721
+ WHERE s.blocked = false
722
+ AND s.at < s.correlated_at
723
+ ${lane_clause}
724
+ AND (s.leased_by IS NULL OR s.leased_until <= NOW())
725
+ AND (s.deferred_at IS NULL OR s.deferred_at <= NOW())
726
+ ORDER BY s.at DESC
774
727
  LIMIT $2
775
728
  ),
776
729
  combined AS (