Context
PR #4039's BoxProvisioningHostedService.StartAsync provisions all
registered boxes sequentially in a single foreach:
// src/Paramore.Brighter.BoxProvisioning/BoxProvisioningHostedService.cs:82-90
var ordered = _provisioners.OrderBy(p => p.BoxType); // Outbox before Inbox
foreach (var provisioner in ordered)
{
await provisioner.ProvisionAsync(cancellationToken);
}
For a single-database deployment this is correct (the database-level
migration lock would serialise concurrent provisioners anyway).
Multi-tenant deployments with N independent databases pay a linear
startup cost: total time = Σ per-tenant migration time, where
ideally it should be max(per-tenant migration time).
What needs doing
Group provisioners by BoxType and run each phase with
Task.WhenAll, preserving the Outbox-before-Inbox ordering required
by the API contract:
var byPhase = _provisioners.GroupBy(p => p.BoxType).OrderBy(g => g.Key);
foreach (var phase in byPhase)
{
await Task.WhenAll(phase.Select(p => p.ProvisionAsync(cancellationToken)));
}
Considerations:
- Per-tenant migration locks remain per-database, so parallelism does
not affect lock semantics.
- Exception aggregation:
Task.WhenAll aggregates faulted tasks into
a single AggregateException. The current sequential code fails
fast on the first throw. Decide whether to preserve fail-fast (use
Task.WhenEach or a custom wait pattern) or accept aggregation.
- Cancellation propagation: each
ProvisionAsync already accepts a
shared CancellationToken; no change needed.
- Logging shape: the existing per-provisioner Information logs will
interleave; consider whether to keep that or wrap each Task with a
scoped logger.
Why this is a follow-up, not part of PR #4039
The reviewer flagged this as observational ("Worth deferring unless
multi-tenant scenarios surface as a real concern"). No deployment
in-scope for PR #4039 exercises the multi-DB path.
Acceptance
- Per-phase parallel provisioning behind
Task.WhenAll.
- Tests covering: (a) parallel exec preserves Outbox-before-Inbox
ordering, (b) exception in one provisioner does not prevent siblings
from finishing (or, if fail-fast preferred, that the first failure
cancels siblings).
- No regression on single-database deployments.
Reference
PR #4039 review comment:
#4039 (comment)
🤖 Generated with Claude Code
Context
PR #4039's
BoxProvisioningHostedService.StartAsyncprovisions allregistered boxes sequentially in a single
foreach:For a single-database deployment this is correct (the database-level
migration lock would serialise concurrent provisioners anyway).
Multi-tenant deployments with N independent databases pay a linear
startup cost: total time = Σ per-tenant migration time, where
ideally it should be max(per-tenant migration time).
What needs doing
Group provisioners by
BoxTypeand run each phase withTask.WhenAll, preserving the Outbox-before-Inbox ordering requiredby the API contract:
Considerations:
not affect lock semantics.
Task.WhenAllaggregates faulted tasks intoa single
AggregateException. The current sequential code failsfast on the first throw. Decide whether to preserve fail-fast (use
Task.WhenEachor a custom wait pattern) or accept aggregation.ProvisionAsyncalready accepts ashared
CancellationToken; no change needed.interleave; consider whether to keep that or wrap each Task with a
scoped logger.
Why this is a follow-up, not part of PR #4039
The reviewer flagged this as observational ("Worth deferring unless
multi-tenant scenarios surface as a real concern"). No deployment
in-scope for PR #4039 exercises the multi-DB path.
Acceptance
Task.WhenAll.ordering, (b) exception in one provisioner does not prevent siblings
from finishing (or, if fail-fast preferred, that the first failure
cancels siblings).
Reference
PR #4039 review comment:
#4039 (comment)
🤖 Generated with Claude Code