Skip to content

#4666 Phase A — Marten.ScaleTesting CLI + event seeder#4672

Merged
jeremydmiller merged 1 commit into
masterfrom
feature/4666-phase-a-scaletesting-seeder
Jun 5, 2026
Merged

#4666 Phase A — Marten.ScaleTesting CLI + event seeder#4672
jeremydmiller merged 1 commit into
masterfrom
feature/4666-phase-a-scaletesting-seeder

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Phase A of #4666 — internal
dev-tool harness for driving 20M+ event seeds against the async daemon. The
regression bed for the #4667
race fixes (now landed) and the optimization-engine basis for the upcoming
daemon-thread-safety work.

Not packed, not in CI, not a benchmark. Pure tooling.

What ships in Phase A

Bit Notes
src/Marten.ScaleTesting/ project net10.0 console runner, JasperFx.CommandLine. Modelled on src/EventAppenderPerfTester/.
Lifted Telehealth domain (Domain/) Appointment / Board / ProviderShift aggregates + events + Patient / Provider / RoutingReason / Specialty reference data. Copy-paste from src/DaemonTests/TeleHealth/ — harness owns its own fork per the issue's "lift, don't share" directive.
Event seeder (Seeding/) Per-stream generators with realistic Telehealth shapes (4-8 events/Appointment with 5% early cancel, 6-14 events/Board, 4-10 events/ProviderShift). Weighted-random k-way merge across stream types (70/25/5 appointment/shift/board). Bounded Channel<EventBatch> + N-way writer fan-out. Deterministic via (rootSeed, tenantIdx, streamKind, streamIdx)-derived RNG per stream.
Reference data seeder 200 patients + 50 providers + 4 routing reasons + 8 specialties per tenant — Phase B's enrichment projections will look these up.
Conjoined multi-tenancy bootstrap AllDocumentsAreMultiTenantedWithPartitioning(ByHash) mirroring multi_stage_projections.cs:246-254. The schema shape Phase B will rebuild against.
seed subcommand marten-scaletest seed [--wipe] --tenants N --events-per-tenant M --writers W --seed S --buckets B. Defaults: 50 × 400K × 8 buckets × 8 writers = 20M. Idempotent.

Design note: one batch per stream

The first cut chunked each stream into multiple smaller batches to interleave
mid-stream events at the per-batch level. Under Quick append mode this raced
on the per-stream version sequence (pk_mt_events_stream_and_version
unique-constraint violation when two writers picked up consecutive batches of
the same stream).

Final shape: each batch is one complete stream. Cross-stream interleaving
at the mt_events table level still happens via the producer's draw order
across tenants and stream types. Writer fan-out is fully parallel because no
two batches ever touch the same stream.

Phase A intentionally skips snapshot registration

Registering Snapshot<T> would require running the
JasperFx.Events.SourceGenerator on this project to emit each aggregate's
partial-class dispatcher — dead weight for a pure-seeding run. StartStream<T>
on the seeder just tags the stream with the aggregate type name; Phase B's
CompositeReplayExecutor reads the tag back when picking the right
projection.

Acceptance criteria

  • seed runs to completion at the requested event count
  • Rerun is no-op (idempotent via mt_events per-tenant rollup)
  • mt_events row count matches the requested total

Verification

Local on net10.0 against docker-compose's Postgres on localhost:5432:

Step Result
Build clean (561 pre-existing warnings from Marten, 0 errors)
Seed 2 tenants × 5K events × 4 writers 11,292 events in 0.9s (~12,352 evts/sec — above the 5K/sec/writer EventAppenderPerfTester baseline)
Idempotency rerun (no --wipe) Seed skipped — all 2 tenants already have ≥ 5000 events.

Deferred (Phases B / C / D from the issue)

🤖 Generated with Claude Code

New src/Marten.ScaleTesting/ project: a JasperFx.CommandLine console runner
that drives 20M+ event seeds for the async-daemon scale harness. Internal
dev tool only — not packed, not wired into CI, not a benchmark.

This is the regression bed for the #4667 race fixes (now landed) and the
optimization-engine basis for the upcoming daemon-thread-safety work.

What ships in Phase A:

* Scaffold + CLI: Microsoft.Extensions.Hosting + JasperFx.CommandLine,
  modelled on src/EventAppenderPerfTester/. Spectre.Console comes in
  transitively via JasperFx 2.8 (which requires Spectre 0.55+) so we don't
  pin it directly and trip the CPM downgrade gate.
* Lifted Telehealth domain (Domain/): Appointment / Board / ProviderShift
  aggregates + their events, plus Patient / Provider / RoutingReason /
  Specialty reference data. Copy-paste from src/DaemonTests/TeleHealth/ —
  the harness owns its own fork so we can extend without disturbing test
  fixtures (per the issue's "lift, don't share" directive).
* Event seeder (Seeding/): per-stream generators with realistic Telehealth
  event shapes (4-8 events/Appointment with 5% early cancel, 6-14
  events/Board with optional alert + finish, 4-10 events/ProviderShift with
  N cycles), weighted-random k-way merge across stream types
  (70/25/5 appointment/shift/board), bounded Channel<EventBatch> producer
  with N-way writer fan-out, deterministic-via-seed (`(rootSeed, tenantIdx,
  streamKind, streamIdx)`-derived RNG per stream).
* Cross-stream interleaving happens at the events-table level via the
  producer's draw order across tenants and stream types. Each batch is one
  full stream — collapsing earlier per-stream chunking that raced on the
  per-stream version sequence under Quick append. One stream per batch =
  full writer fan-out with no contention.
* `seed` subcommand:
    marten-scaletest seed [--wipe] --tenants N --events-per-tenant M --writers W --seed S --buckets B
  Defaults match the issue: 50 tenants × 400K events × 8 hash buckets × 8
  writers = 20M events. Idempotent: queries mt_events grouped by tenant_id
  and skips tenants already meeting the target.
* Reference data seeder per tenant (200 patients + 50 providers + 4 routing
  reasons + 8 specialties) so the Phase B enrichment projections have data
  to look up.
* Conjoined multi-tenancy + AllDocumentsAreMultiTenantedWithPartitioning
  (ByHash buckets) bootstrap that mirrors
  multi_stage_projections.cs:246-254 — the schema shape Phase B will
  rebuild against.

Phase A intentionally does NOT register any Snapshot<T>: registering would
require running the JasperFx.Events.SourceGenerator on this project to emit
each aggregate's partial-class dispatcher, which is dead weight for a
pure-seeding run. StartStream<T> on the seeder just tags the stream with
the aggregate type name; Phase B's CompositeReplayExecutor reads the tag
back when picking the right projection.

Verified locally (Postgres on docker-compose's localhost:5432):
* Build clean (561 pre-existing warnings carried in from Marten, 0 errors).
* Seed 2 tenants × 5,000 events × 4 writers: 11,292 events in 0.9s
  (~12,352 evts/sec on the dev box, well above the 5K/sec/writer
  EventAppenderPerfTester baseline).
* Idempotency: second run with same args + no `--wipe` correctly skipped
  with `Seed skipped — all 2 tenants already have ≥ 5000 events.`
* The Marten.slnx and Directory.Packages.props edits: add the new project
  + add Microsoft.Extensions.Hosting 10.0.0 to CPM (Hosting.Abstractions
  was already there).

Phases B / C / D from the issue are deferred:
* Phase B: composite projection topology (4 stage-1 + 2 stage-2 + 2 NEW
  stage-3) + `rebuild` subcommand on the single-pass
  CompositeReplayExecutor path.
* Phase C: `validate` (single-shard baseline diff) + `stress` chain + JSON
  metrics sink.
* Phase D: use the harness to drive the daemon-thread-safety synthesis
  fixes per the #4667 follow-up plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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