Skip to content

Commit fc4fc00

Browse files
committed
Run ten functional tests concurrently and balance cluster jobs
1 parent c08507f commit fc4fc00

3 files changed

Lines changed: 105 additions & 19 deletions

File tree

‎docs/ADR/ADR-148-parallel-ci-shards.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ Build and Tests ran every suite one after another in one job on a 4-vCPU ubuntu-
1515
Build and Tests is a set of parallel jobs driven by one checked-in manifest, `scripts/Features/TestInfrastructure/ci-test-shards.json`, and its runner `ci-test-shards.mjs`:
1616

1717
- **Build:** governance and website inventory checks, the static shard partition check and `dotnet build KeyLoad.slnx --configuration Release -warnaserror`.
18-
- **Units:** four shards. Each runs one process per namespace with the assembly-pinned filter `/KeyLoad.FunctionalTests/<namespace>/*/*` at processor-count parallelism.
18+
- **Functional operations:** four descriptive shards. Each runs one process per namespace with the assembly-pinned filter `/KeyLoad.FunctionalTests/<namespace>/*/*` and ten concurrent tests, both locally and in CI.
1919
- **Analyzers and process recovery:** one job with filters `/KeyLoad.AnalyzerTests/*/*/*` and `/KeyLoad.ProcessRecoveryTests/*/*/*`.
20-
- **Aspire topology:** twelve shards of two lanes each.
21-
- A lane is its own `dotnet test` process with `--maximum-parallel-tests 1` and a 45-minute deadline, so a runner holds at most two clusters. Measured lanes take 20–25 minutes, so the deadline sits well above a complete lane.
20+
- **Aspire topology:** twenty descriptive shards containing thirty-two lanes, with one or two lanes per runner.
21+
- A lane is its own `dotnet test` process with `--maximum-parallel-tests 1` and a 45-minute deadline, so a runner holds at most two clusters.
2222
- Lanes are typed: one keyed shared cluster, own clusters, other keyed clusters, or one long functional gate method.
2323
- Every lane process owns its own keyed shared cluster, so a test that kills, restarts or cold-reopens nodes cannot affect another lane.
2424
- **Verify test partition:** reads every shard's TRX artifact and proves that each test ran exactly once, in its assigned shard and lane.
@@ -37,8 +37,8 @@ Partition correctness is checked twice:
3737

3838
## Consequences
3939

40-
- Wall time is bounded by the slowest shard instead of the sum of all suites. The estimate is about 25–28 minutes, assuming two lanes per runner run about 1.3 times slower than alone. Lane estimates come from run 40091311302 and need rebalancing from the first sharded reports.
41-
- The workflow uses 19 test-related jobs at once (Build, the planning job, four unit shards, one analyzers-and-recovery job, twelve topology shards) plus Verify test partition at the end.
40+
- Original Linux reports from run 40510797452 identify serial lane workloads of up to 41 minutes, including approximately 27 minutes in the four composite-prefix cases. Dedicated jobs separate composite-prefix, subscription cold-cut, revision-plan, graph-frontier, SQL/JSON/CSharp equivalence, ordered-queue and file-ownership operations. Those original case timings inform assignment; they exclude build, queue, shared fixture startup and teardown. Speedup remains unqualified until the changed partition completes on Linux.
41+
- The matrix permits twenty cluster jobs with thirty-two concurrent serial lanes, plus four functional jobs with ten concurrent tests each and one analyzers-and-process-recovery job. Build and planning are separate jobs; the final partition check waits for test results. Actual concurrency also depends on available GitHub runner capacity.
4242
- Adding a test class is a one-line manifest change. `inventory <suite>` shows the cluster each class uses and whether it disrupts nodes. A class missing from the manifest fails Build.
4343
- Product behaviour and test assertions are unchanged. No product request or admission limit is added; the bound is on concurrent test processes per runner.
4444
- Static source scanning is not runtime proof. Discovery, the TRX identities and the topology lanes are qualified only by actual Linux GitHub runs.

‎docs/Features/TestInfrastructure.md‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,12 +152,14 @@ Whole regression: existing ImagesProduceClosedManifestAndRejectTamperingWithoutR
152152
| Job | Contents | Filter and concurrency |
153153
|---|---|---|
154154
| Build | governance, website source inventory, static shard partition check, `dotnet build KeyLoad.slnx -warnaserror` | none |
155-
| Named functional operation groups | the namespaces listed for the shard | one process per namespace selected by its descriptive group ID, `/KeyLoad.FunctionalTests/<namespace>/*/*`, `--maximum-parallel-tests 4` on local and CI runners |
156-
| Analyzers and process recovery | shards AN then RC | `/KeyLoad.AnalyzerTests/*/*/*` and `/KeyLoad.ProcessRecoveryTests/*/*/*` |
157-
| Named cluster capability groups | two lanes per runner | each lane is its own `dotnet test` process, `--maximum-parallel-tests 1 --timeout 45m`, class or method alternation under `/KeyLoad.ClusterTests/*/` |
155+
| Named functional operation groups | the namespaces listed for the shard | one process per namespace selected by its descriptive group ID, `/KeyLoad.FunctionalTests/<namespace>/*/*`, `--maximum-parallel-tests 10` on local and CI runners |
156+
| Analyzers and process recovery | compiler analyzers, then process crash recovery | `/KeyLoad.AnalyzerTests/*/*/*` and `/KeyLoad.ProcessRecoveryTests/*/*/*` |
157+
| Named cluster capability groups | one or two lanes per runner | each lane is its own `dotnet test` process, `--maximum-parallel-tests 1 --timeout 45m`, class or method alternation under `/KeyLoad.ClusterTests/*/` |
158158
| Verify test partition | every shard's TRX artifact | `ci-test-shards.mjs verify TestResults` |
159159

160-
Every test job runs `dotnet build KeyLoad.slnx -warnaserror` in the default Debug configuration, because tests read compiled outputs of projects their test project does not reference: ProductionSourceManifestOperationTests reads the image of every solution project, and ColdBootstrapSdkTests launches `src/KeyLoad.Cli/bin/Debug/net10.0/KeyLoad.Cli.dll`. Each job then writes TRX reports to `TestResults/<suite>/<shard>/<lane or namespace>` and uploads them as `test-results-<suite>-<shard>` even when tests fail. The filters pin the assembly segment, so the process recovery tests that the unit host also registers run once, in RC.
160+
Every test job runs `dotnet build KeyLoad.slnx --configuration Release -warnaserror`, because tests read compiled outputs of projects their test project does not reference: ProductionSourceManifestOperationTests reads the image of every solution project, and ColdBootstrapSdkTests launches the CLI from the selected configuration. Local development uses the SDK default Debug configuration. Each job writes original TRX reports below its test assembly's `bin/Release/net10.0/ci-results/<suite>/<shard>/<lane or namespace>` and uploads them even when tests fail. The filters pin the assembly segment, so process recovery tests registered by the functional host run only in the process crash recovery shard.
161+
162+
The manifest separates the long composite-prefix, subscription cold-cut, revision-plan, graph-frontier, SQL/JSON/CSharp equivalence, ordered-queue and file-ownership operations into dedicated jobs. Twenty cluster jobs contain thirty-two lanes in total. Original Linux reports identified these long serial sets; the new wall time requires an actual run of this partition. Additional parallelism comes from separate runners while each runner retains its two-cluster bound.
161163

162164
A lane runs one test at a time, so it holds at most one 3-node cluster at a time and a 4-vCPU runner holds two. Lane kinds keep that bound: `shared-readers` and `shared-exclusive` lanes use one keyed shared cluster and create no clusters of their own; `own-clusters` and `functional-gate` lanes use no shared key; `isolated-clusters` lanes hold the other keyed clusters and tests without a cluster. Each lane process has its own keyed shared cluster, so a test that kills or restarts nodes affects only its own lane. Playwright is installed only on the shards that the manifest marks `playwright`.
163165

‎scripts/Features/TestInfrastructure/ci-test-shards.json‎

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"sources": "tests/KeyLoad.FunctionalTests",
2020
"assembly": "KeyLoad.FunctionalTests",
2121
"partition": "namespace",
22-
"parallelism": 4,
22+
"parallelism": 10,
2323
"timeoutMinutes": 20,
2424
"shards": [
2525
{
@@ -87,6 +87,98 @@
8787
"HeavyLoad"
8888
],
8989
"shards": [
90+
{
91+
"id": "cluster-composite-prefix-cold-recovery",
92+
"name": "Composite prefixes and cold recovery",
93+
"lanes": [
94+
{
95+
"id": "composite-prefix-public-adapters",
96+
"kind": "shared-exclusive",
97+
"classes": [
98+
"SparseCompositePrefixClusterTests"
99+
]
100+
}
101+
]
102+
},
103+
{
104+
"id": "cluster-subscription-policy-and-cold-cuts",
105+
"name": "Subscription policy and cold cuts",
106+
"lanes": [
107+
{
108+
"id": "subscription-policy-and-cold-cuts",
109+
"kind": "own-clusters",
110+
"classes": [
111+
"SubscriptionPhasePublicColdTests"
112+
]
113+
}
114+
]
115+
},
116+
{
117+
"id": "cluster-query-revision-and-index-plans",
118+
"name": "Query revisions and index plans",
119+
"lanes": [
120+
{
121+
"id": "metadata-revision-and-index-plans",
122+
"kind": "own-clusters",
123+
"classes": [
124+
"QueryRuleOptimizationClusterTests"
125+
]
126+
}
127+
]
128+
},
129+
{
130+
"id": "cluster-graph-frontiers-and-vertex-policy",
131+
"name": "Graph frontiers and vertex policy",
132+
"lanes": [
133+
{
134+
"id": "graph-frontiers-and-vertex-policy",
135+
"kind": "shared-exclusive",
136+
"classes": [
137+
"GraphLabelFrontierPublicTests",
138+
"GraphVertexPublicWholeFlowTests"
139+
]
140+
}
141+
]
142+
},
143+
{
144+
"id": "cluster-sql-json-and-csharp-equivalence",
145+
"name": "SQL JSON and CSharp equivalence",
146+
"lanes": [
147+
{
148+
"id": "sql-json-and-csharp-equivalence",
149+
"kind": "own-clusters",
150+
"classes": [
151+
"MetamorphicPublicClusterTests"
152+
]
153+
}
154+
]
155+
},
156+
{
157+
"id": "cluster-ordered-queue-retry-and-cold-cuts",
158+
"name": "Ordered queue retry and cold cuts",
159+
"lanes": [
160+
{
161+
"id": "ordered-queue-retry-and-cold-cuts",
162+
"kind": "shared-exclusive",
163+
"classes": [
164+
"QueueOrderedRetryPublicClusterTests"
165+
]
166+
}
167+
]
168+
},
169+
{
170+
"id": "cluster-file-ownership-and-cold-recovery",
171+
"name": "File ownership and cold recovery",
172+
"lanes": [
173+
{
174+
"id": "file-ownership-and-cold-recovery",
175+
"kind": "own-clusters",
176+
"classes": [
177+
"FunctionalFileOwnershipClusterTests"
178+
]
179+
}
180+
]
181+
},
90182
{
91183
"id": "cluster-public-apis-and-administration",
92184
"playwright": true,
@@ -227,7 +319,6 @@
227319
"classes": [
228320
"AtomicTriadStagedClusterTests",
229321
"AuthenticationExecutionTests",
230-
"QueryRuleOptimizationClusterTests",
231322
"QueueDeadlineSiloClusterTests",
232323
"StrongReadSessionAbsenceClusterTests"
233324
]
@@ -286,7 +377,6 @@
286377
"kind": "own-clusters",
287378
"classes": [
288379
"GraphLabelUseClusterTests",
289-
"MetamorphicPublicClusterTests",
290380
"QueueWorkerProcessClusterTests",
291381
"RequiredProcessingPolicyClusterTests",
292382
"SqlParserAdmissionClusterWholeFlowTests"
@@ -300,8 +390,6 @@
300390
"AggregateReplayClusterSiloRestartTests",
301391
"EventCurrentReadAuthorityClusterTests",
302392
"EventProjectionClusterTests",
303-
"GraphLabelFrontierPublicTests",
304-
"GraphVertexPublicWholeFlowTests",
305393
"GuideLifecyclePublicTests",
306394
"PartitionQueryPublicSiloRestartTests",
307395
"RecurringDueClusterTests",
@@ -328,8 +416,7 @@
328416
"classes": [
329417
"ModelHeadAuthorityClusterTests",
330418
"RemoteTransferRepairClusterTests",
331-
"RequestBodyLimitClusterTests",
332-
"SubscriptionPhasePublicColdTests"
419+
"RequestBodyLimitClusterTests"
333420
]
334421
},
335422
{
@@ -345,7 +432,6 @@
345432
"LeftFilteredJoinClusterTests",
346433
"QueueDeadLetterPublicClusterTests",
347434
"RecurringScheduleClusterTests",
348-
"SparseCompositePrefixClusterTests",
349435
"SqlModelViewClusterParityTests",
350436
"TimeSeriesCurrentCapabilityClusterTests"
351437
]
@@ -369,7 +455,6 @@
369455
"kind": "own-clusters",
370456
"classes": [
371457
"CompetingInboxPublicTests",
372-
"FunctionalFileOwnershipClusterTests",
373458
"RemoteTransferAttemptClusterTests",
374459
"ResourcePolicyUpdateClusterTests"
375460
]
@@ -431,7 +516,6 @@
431516
"DeclarativeCompositionClusterTests",
432517
"DocumentSessionReadClusterTests",
433518
"GraphPathClusterSiloRestartTests",
434-
"QueueOrderedRetryPublicClusterTests",
435519
"SqlGraphSearchClusterTests",
436520
"StreamCatchupColdClusterTests",
437521
"TimeSeriesAggregateScanBoundClusterTests"

0 commit comments

Comments
 (0)