Skip to content

Warm up container before parallel test dispatch - #10204

Merged
Alexander Holstrup (aholstrup1) merged 4 commits into
mainfrom
aholstrup1-gdiplus-warmup-mitigation
Aug 19, 2026
Merged

Warm up container before parallel test dispatch#10204
Alexander Holstrup (aholstrup1) merged 4 commits into
mainfrom
aholstrup1-gdiplus-warmup-mitigation

Conversation

@aholstrup1

@aholstrup1 Alexander Holstrup (aholstrup1) commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What & why

The parallel test dispatcher fans out one background job per app/tenant. Concurrent per-tenant company-opens on a fresh container can race on the first use of the container's process-wide state, causing the first test run to fail intermittently (a masked 500 opening page 130455). This runs the first test app alone and awaits it before the parallel fan-out, so that first open happens single-threaded and the rest fan out safely. The change reuses the existing dispatch/wait machinery, is a no-op for single-app or single-tenant runs, and leaves the existing transient-retry path in place. TestConfiguration.json now lists the smallest app first in each bucket so the serial step is cheap, with the remainder still longest-first for scheduling.

Linked work

AB#646706

How I validated this

  • I read the full diff and it contains only changes I intended.
  • I ran the change in Business Central and confirmed it behaves as expected.
  • I added or updated tests for the new behavior, or explained below why none are needed.

What I tested and the outcome

Unit tests: build/scripts/tests/ParallelTestExecution.Test.ps1 passes 10/10, including new tests that the first app is dispatched and awaited alone before fan-out, and that the step is skipped for single-tenant/single-app runs.

End-to-end (this PR's CI run): all 113 Unit Tests container jobs succeeded, the warmup ran in every one, and none showed the failure signature or triggered the transient-race retry.

Risk & compatibility

Low. Only the first dispatch is reordered (one app runs alone, then the rest fan out as before); retry classification, result merging, and tenant/company provisioning are unchanged. Trade-off: the serial app adds its duration to the critical path, which is why each bucket now leads with its smallest app.

On a fresh container the first test run intermittently fails with a masked client 500 on page 130455. The real root cause is a platform GDI+/System.Drawing first-touch race: the image-encoder registry initializes lazily once per NST process and is not thread-safe, so concurrent per-tenant company-opens (media import during InitCompany) hit ArgumentNullException('encoder') and terminate the session.

A fixed 5s inter-dispatch sleep already exists and is insufficient (it staggers dispatch start, not the async GDI+ moment). Instead, run the first app alone and await its completion so GDI+ warms single-threaded before the parallel fan-out. Complementary to the existing transient-race one-retry path, which is unchanged. No-op for single-app/single-tenant runs.

Pipeline-level workaround; remove once the platform guards its first-touch GDI+ init.
@github-actions github-actions Bot added Build: scripts & configs Build scripts and configuration files Other GitHub request for other area than SCM, Finance or Integration Ownership: Needs Review Ownership is Other, low confidence, or needs manual correction labels Aug 12, 2026
… app

Shorten the warmup function/description comments, rename Invoke-GdiPlusWarmupDispatch to Invoke-WarmupDispatch, and reorder each legacy bucket so the smallest app runs first. Since the first app now runs serially before the parallel fan-out, leading with the smallest keeps that serial step cheap while the remaining apps stay longest-first for LPT scheduling.
@aholstrup1 Alexander Holstrup (aholstrup1) changed the title Serialize first test dispatch to mitigate GDI+ first-touch race Warm up container before parallel test dispatch Aug 13, 2026
@aholstrup1
Alexander Holstrup (aholstrup1) marked this pull request as ready for review August 13, 2026 11:21
@github-actions github-actions Bot added this to the Version 29.0 milestone Aug 13, 2026
Timing analysis of the PR build showed Tests-Local runs ~17-19 min, so using it as the serial warmup app added that much to Bucket1's critical path. Lead with Tests-Resource (~3 min) instead and move Tests-Local back into its longest-first position. Bucket2's warmup (Tests-Upgrade, ~1.6 min) was already small.
Comment thread build/scripts/ParallelTestExecution.psm1
Add unit tests for the remaining-apps return value, the unresolved-appId skip branch, and the transient warmup failure landing in State.transient for the caller to re-queue.
Merged via the queue into main with commit 2b38ea0 Aug 19, 2026
336 of 339 checks passed
@aholstrup1
Alexander Holstrup (aholstrup1) deleted the aholstrup1-gdiplus-warmup-mitigation branch August 19, 2026 08:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Build: scripts & configs Build scripts and configuration files Other GitHub request for other area than SCM, Finance or Integration Ownership: Needs Review Ownership is Other, low confidence, or needs manual correction

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants