Skip to content

#4666 Phase B — TelehealthComposite 4+2+2 + rebuild subcommand#4675

Merged
jeremydmiller merged 1 commit into
masterfrom
feature/4666-phase-b-composite-rebuild
Jun 5, 2026
Merged

#4666 Phase B — TelehealthComposite 4+2+2 + rebuild subcommand#4675
jeremydmiller merged 1 commit into
masterfrom
feature/4666-phase-b-composite-rebuild

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Phase B of #4666. Wires the
composite projection topology and the rebuild subcommand so the harness can
actually exercise the daemon's projection paths under load — the regression
bed for the #4667 race fixes.

What ships

Bit Notes
Stage-2 projections (Domain/Stage2Projections.cs) Lifted from src/DaemonTests/TeleHealth/: AppointmentDetailsProjection (multi-stream + enrichment), BoardSummaryProjection (multi-stream + ReferencePeerView<Board>), AppointmentMetricsProjection (custom IProjection).
Stage-3 projections (Domain/Stage3Projections.cs) — NEW ProviderUtilizationProjection (per-provider rollup keyed by ProviderId, reads stage-2 Updated<AppointmentDetails>, enriches with Provider) + TenantDailyRollupProjection (per-day rollup keyed by ISO date string; under conjoined tenancy the date-only key naturally scopes per-tenant, exercising cross-stage chaining + per-tenant aggregation).
CompositeProjection wiring (Program.cs) opts.Projections.CompositeProjectionFor("TelehealthComposite", ...) with the issue's 4+2+2 stage layout.
SourceGen analyzer dependency (csproj) Adds JasperFx.Events.SourceGenerator as Analyzer (matches every Marten test project shape). Without it the rebuild fails with No source-generated dispatcher found for SingleStreamProjection<Board, Guid>.
rebuild CLI subcommand marten-scaletest rebuild [--projection TelehealthComposite] [--shard-timeout-seconds 600]. Builds the daemon, calls RebuildProjectionAsync, prints a Spectre summary. Returns false on any exception so a stress chain can stop early.

Snapshot init guard

AppointmentDetailsProjection.Evolve got a snapshot ??= new ...(id) at the
top: enrichment-added References<T> synthetic events can arrive before the
snapshot-creating Updated<Appointment>, which made the lifted
snapshot!.X = ... pattern NRE on rebuild (smoke caught this immediately —
~16GB memory blow-up before I killed the process, root cause was the
unguarded NRE adding to the daemon's exception/retry queue).

Smoke (local Postgres on docker-compose's localhost:5432)

Step Result
Build clean
seed --tenants 2 --events-per-tenant 500 1,136 events / 150 streams, 0.4s
rebuild --projection TelehealthComposite clean, 0.5s (~2,247 events/sec)

Projection row counts after rebuild (direct psql):

Table Rows
mt_doc_appointment 100
mt_doc_board 6
mt_doc_providershift 36
mt_doc_appointmentmetrics 16
mt_doc_appointmentdetails 108
mt_doc_boardsummary 6
mt_doc_providerutilization 93
mt_doc_tenantdailyrollup 2

All 8 projections populated end-to-end through the composite's 3 stages.

Phase C / D follow-ups

  • Phase C (separate PR): validate subcommand (single-shard baseline diff) + JSON metrics sink + stress chain.
  • Phase D: actually run stress at scale against the dev box; if crash-free, close #4667.

🤖 Generated with Claude Code

What ships:

* Stage-2 projections (Domain/Stage2Projections.cs) — lifted from
  src/DaemonTests/TeleHealth/:
    - AppointmentDetailsProjection (multi-stream + enrichment) +
      AppointmentDetails aggregate.
    - BoardSummaryProjection (multi-stream + ReferencePeerView<Board>) +
      BoardSummary + AssignedAppointment.
    - AppointmentMetricsProjection (custom IProjection) +
      AppointmentMetrics.
  Snapshot init guard added inside AppointmentDetailsProjection.Evolve:
  enrichment-added References<T> events can arrive before the snapshot-
  creating Updated<Appointment>, which made the lifted "snapshot!.X = ..."
  pattern NRE on rebuild. Default-init at the top of Evolve.

* Stage-3 projections (Domain/Stage3Projections.cs) — NEW for the
  scaletesting harness:
    - ProviderUtilizationProjection: per-provider rollup keyed by
      ProviderId. Reads Updated<AppointmentDetails> (stage-2 emission),
      enriches with Provider reference doc, increments
      AssignedCount/CompletedCount/CancelledCount.
    - TenantDailyRollupProjection: per-day rollup keyed by ISO date
      string. Reads Updated<AppointmentDetails>. Under conjoined tenancy
      the date-only key naturally scopes per-tenant. Exercises the
      cross-stage chaining + per-tenant aggregation surface.

* CompositeProjection wiring (Program.cs) — registers TelehealthComposite
  via opts.Projections.CompositeProjectionFor(...) with the issue's 4+2+2
  stage layout:
    Stage 1: AppointmentProjection, ProviderShiftProjection,
             Snapshot<Board>, AppointmentMetricsProjection.
    Stage 2: AppointmentDetailsProjection, BoardSummaryProjection.
    Stage 3: ProviderUtilizationProjection, TenantDailyRollupProjection.

* Source-gen dispatcher dependency: csproj now references
  JasperFx.Events.SourceGenerator as Analyzer (matches every Marten
  test project shape) so the conventional Apply/Create/ShouldDelete
  methods on Board / Appointment / ProviderShift get compiled
  dispatchers — without it the rebuild fails at startup with
  "No source-generated dispatcher found for SingleStreamProjection<Board, Guid>".

* `rebuild` CLI subcommand (Commands/Rebuild{Command,Input}.cs):
    marten-scaletest rebuild [--projection TelehealthComposite]
                             [--shard-timeout-seconds 600]
  Builds the daemon, calls RebuildProjectionAsync against the supplied
  projection, prints throughput + a one-row Spectre table summary.
  Fails fast (returns false from Execute) on any exception so a stress
  chain can stop early.

Smoke (Postgres on docker-compose's localhost:5432):
* 2 tenants × 500 events seed = 1,136 events / 150 streams.
* rebuild TelehealthComposite — clean, 0.5s (~2.2K events/sec).
* Projection row counts after rebuild (via direct psql):
    mt_doc_appointment           100
    mt_doc_board                   6
    mt_doc_providershift          36
    mt_doc_appointmentmetrics     16
    mt_doc_appointmentdetails    108
    mt_doc_boardsummary            6
    mt_doc_providerutilization    93
    mt_doc_tenantdailyrollup       2
  All 8 projections populated end-to-end through the composite's 3 stages.

Phase C (validate subcommand + JSON metrics + stress chain) follows in a
separate PR.

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.

Async daemon: eliminate session-shared dictionary access from projection work (VersionTracker, ItemMap, ChangeTrackers)

1 participant