Wait for readiness, not for time
The run must wait when it starts an address before any test uses it. The choice is between a fixed sleep that sometimes loses and a probe that checks the address and records what it waited.
- Register a readiness probe after the piece that publishes the address.
- Read the wait in the run layer of a trace.
- Say what a sleep would leave in the trace instead.
- Move the test clock (lesson 1).
- Nothing installed. The archives are on this site.
The scenario
The browser journey needs a real listener: the page is served over HTTP, so the application cannot run in-process for it. The run starts a loopback instance and publishes its address, and something has to wait until that address answers.
A sleep guesses. On a loaded machine the guess is too short, and on an idle one it wastes the difference. The sample registers a readiness probe instead, and the wait it performs is part of the run's record.
The registration
The probe sits directly after the listener it probes:
1if (run.RunsLocalApplications)2{3// The browser needs a real listener; the page journey follows this instance's address.4builder.AddLoopbackApplication(NorthstarTargets.Web, NorthstarProgram.CreateApp);5builder.AddHttpReadiness(NorthstarTargets.Web, "/health");6}
- Only when the run hosts the application
A run pointed at a deployed address skips the listener and the probe entirely.
- Start the listener
The loopback instance binds a free port and publishes the address it got.
- Wait for it to answer
The probe resolves the application's published address and polls /health until it answers. Registering it after the listener is what gives it an address to resolve.
samples/Northstar.ProtoTest/Setup.cs.The order is the contract. A probe is awaited at the position it is registered, so a probe registered before the piece that publishes the address has nothing to check. The same shape appears wherever a run starts a real process, including after an AppHost.
The wait in the run layer
The run layer holds one readiness entity for the loopback instance. From the archive:
| Attribute | Value |
|---|---|
readiness.url | http://127.0.0.1:54120/health, the port this run bound |
readiness.attempts | 1 |
readiness.waitedMs | 85 |
The address is the run's own, so it changes between runs. The two numbers are the answer a sleep cannot give: the address answered on the first probe, and the wait cost 85 milliseconds. A run that needs several probes adds attempts, and a slow address shows up as a larger number instead of a mystery failure in the first test that used it.
The entity is released with the run, at the same position the listener is released. That release is a resource.release entry in the run layer of the same archive.
What the tests do instead
No test in the sample waits for the application. A test starts, takes its client and calls. The waiting happened once, before the first test, and every test after it reads an address the run already checked. Replace a Task.Delay before a request with a readiness probe.
What would a sleep for the same wait leave in the trace instead of the readiness entity?
Checkpoint
The API journeys run in-process, so they need no address. Why does the run layer still carry a readiness entity?
/health and records its URL, the attempts and the time waited; the entity reads 1 attempt and 85 ms. A sleep would record none of that, only a slower entry in one test's execution span.What you learned
- A run that starts an application registers a readiness probe for its address.
- The probe is registered after the piece that publishes the address, because it resolves that address when it runs.
- Readiness records the attempts and the time waited; a sleep records nothing.