Skip to content

ACT-102: per-stream reaction priority lanes#697

Merged
Rotorsoft merged 3 commits into
masterfrom
ACT-102
May 10, 2026
Merged

ACT-102: per-stream reaction priority lanes#697
Rotorsoft merged 3 commits into
masterfrom
ACT-102

Conversation

@Rotorsoft
Copy link
Copy Markdown
Owner

Summary

Closes #673. Adds an optional priority field on the resolver target, biases the claim() lagging-frontier ordering by priority DESC, at ASC, and exposes app.prioritize(filter, n) for runtime overrides. Default 0 keeps existing apps unchanged.

Research-led. Before shipping API surface, the research benchmark measured the proposed change against the existing dual-frontier on a saturated workload. Findings (3 back-to-back runs):

Run Priority TTF speedup Total drain delta
1 11.28× −6.6 %
2 11.40× −6.7 %
3 10.66× −5.3 %

(Negative = priority arm finished sooner overall.) Zero starvation cost — same final state in both arms. Decision to ship was data-driven; if the numbers had been ambiguous the feature would have been deferred.

Surface

  • .to({ target, source?, priority? }) on the resolver — static or dynamic.
  • Store.subscribe extended with optional priority; max() invariant across reactions targeting the same stream.
  • Store.prioritize(filter, n) — bulk update with QueryStreams-shaped filter (regex on stream/source, exact-match flags, blocked state). Sets priority outright (operator override of the build-time max).
  • app.prioritize(filter, n) — orchestrator wrapper.
  • StreamPosition.priority for query_streams introspection.
  • PG: schema migration via ALTER TABLE ... ADD COLUMN IF NOT EXISTS, composite index (blocked, priority DESC, at) for the lag CTE. Replaces the old (blocked, at) index.
  • SQLite: schema migration via try/swallow ALTER (no IF NOT EXISTS syntax in libSQL).
  • InMemory: priority field on each stream, sort by priority DESC, at ASC in claim().

Inviolate

Per-stream event ordering is unchanged. Priority biases which streams claim() picks first, never the order events within a stream are processed. Within a stream, events still drain by id ASC — that's a foundational ES guarantee.

When it matters

Only under saturation. With streamLimit ≥ candidate streams every cycle, every stream gets a slot every cycle and priority is irrelevant. Cold-start replays under contention is the primary scenario.

Bonus — file naming consistency

Three framework source files in libs/*/src/ were PascalCase (PostgresStore.ts, SqliteStore.ts, PinoLogger.ts). Renamed to kebab-case for consistency with the rest of the framework (in-memory-store.ts, correlate-cycle.ts, etc.). React component files under packages/ keep PascalCase per the React community convention.

Coverage

100% statements / branches / functions / lines workspace-wide. 1065 tests.

Docs

  • CLAUDE.md — new "Reaction Priority Lanes" section, Store contract amendment.
  • libs/act/README.md, libs/act-pg/README.md updated.
  • docs/docs/architecture/priority-lanes.md (new Docusaurus page).
  • .claude/skills/scaffold-act-app/server.md — priority lane guidance.
  • TSDoc on Resolved.priority, PrioritizeFilter, Store.prioritize, Act.prioritize.
  • libs/act-pg/PERFORMANCE.md — benchmark methodology + results.
  • Book notes saved for the future "Scaling Act" chapter.

Test plan

  • Workspace tests pass (pnpm test) — 1065/1065
  • Lint clean (pnpm lint)
  • Workspace build clean (pnpm build)
  • Coverage 100% across the board
  • Cross-adapter integration tests (PG + SQLite + InMemory)
  • PG schema migration test (manually create old-shape table, re-seed, verify column)
  • Benchmark confirms claim — pnpm -F @rotorsoft/act-pg exec vitest run --config vitest.bench.config.ts test/priority-claim.bench.ts

🤖 Generated with Claude Code

rotorsoft and others added 3 commits May 10, 2026 09:20
Before shipping API surface for #673 (per-stream reaction priority),
measure whether priority-aware claim ordering actually beats the
existing dual-frontier strategy on a saturated workload — and whether
it costs us anything elsewhere.

- New `test/priority-claim.bench.ts` runs two arms back-to-back on
  identical seeded data: baseline (live `claim()` SQL) vs.
  priority-aware (lag CTE adds `priority DESC, at ASC`).
- Workload: 1 source × 500 events, 50 cold-replay targets,
  streamLimit=5 (worker is 10x saturated).
- Captures TTF for the priority target *and* total drain time,
  plus per-stream progress at both moments.
- `PERFORMANCE.md` summarizes the methodology + decision (go).

Findings — three back-to-back runs:
  priority TTF speedup: 11.28× / 11.40× / 10.66×
  total drain delta:   −6.6% / −6.7% / −5.3% (priority arm faster)
  starvation: zero — final state identical to baseline

Decision: ship the feature. Implementation in a follow-up commit.

No production code touched in this commit; only a bench file and
the bench config wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #673. Adds an optional `priority` field on the resolver target,
biases the `claim()` lagging-frontier ordering by `priority DESC, at
ASC`, and exposes `app.prioritize(filter, n)` for runtime overrides.
Default `0` keeps existing apps unchanged.

Research-led: PostgresStore benchmark first (separate commit) measured
~11× speedup on the priority target with no aggregate-throughput
cost. Implementation followed the data.

Surface
- `.to({ target, source?, priority? })` on the resolver.
- `Store.subscribe` extended with optional `priority`; max() invariant
  across reactions targeting the same stream.
- `Store.prioritize(filter, n)` — bulk update with QueryStreams-shaped
  filter (regex on stream/source, exact-match flags, blocked state).
  Sets priority outright (operator override of the build-time max).
- `app.prioritize(filter, n)` — orchestrator wrapper.
- `StreamPosition.priority` for query_streams introspection.
- PG schema migration via ALTER TABLE ADD COLUMN IF NOT EXISTS,
  composite index `(blocked, priority DESC, at)` for the lag CTE.
- SQLite schema migration via try/swallow ALTER (no IF NOT EXISTS).

Per-stream event ordering is unchanged — priority biases which
streams `claim()` picks first, never reorders events within a stream.
Only meaningful under saturation; idle systems are unaffected.

Coverage: 100% across the board (1065 tests).

Also: rename PascalCase framework source files to kebab-case for
consistency with the rest of `libs/*/src/` —
PostgresStore.ts → postgres-store.ts,
SqliteStore.ts → sqlite-store.ts,
PinoLogger.ts → pino-logger.ts.
React component files in packages/ keep PascalCase per the React
community convention.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inspector consumes `StreamPosition` via `query_streams` but currently
drops the new `priority` field on the way to the client. Add it to:

- `streamMeta` query — per-stream priority alongside the existing
  watermark / blocked / leased fields.
- `drainStatus` query — surfaces priority on `blockedStreams` and
  `activeLeases` rows, plus a new `priorityCounts` aggregate
  (number of streams per priority lane, sorted highest-first).

Compatibility: priority is a new column in the response objects,
backwards-compatible additive change. UI work (column rendering,
inline editing, prioritize() mutation surface) is intentionally
deferred to a separate inspector PR — that's a real design exercise
that doesn't belong in ACT-102.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Rotorsoft Rotorsoft merged commit bb67591 into master May 10, 2026
5 of 6 checks passed
@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version @rotorsoft/act-v0.35.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version @rotorsoft/act-pg-v0.20.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version @rotorsoft/act-sqlite-v0.5.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version @rotorsoft/act-pino-v0.4.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ACT-102: per-event reaction priority lanes

1 participant