Follow-up to #3763 / #3798, where all three Pulsar compliance classes were restored to CI. Working on that suite surfaced three separate problems with how the tests get a broker. None of them is the transport's fault; all of them cost real time.
1. One container per worker process
PulsarContainerFixture starts a Pulsar container from a [ModuleInitializer]:
[ModuleInitializer]
internal static void Initialize()
{
...
_container = new PulsarBuilder().WithImage("apachepulsar/pulsar:latest")...Build();
_container.StartAsync().GetAwaiter().GetResult();
}
A [ModuleInitializer] runs once per process. The supervisor partitions a project across worker processes — I saw runs at 2, 3, 6 and 24 workers depending on machine load — and the retry harness spawns another fresh process per retried test. So a single CIPulsar run can ask Docker for many concurrent Pulsar brokers, and Pulsar standalone is not a small image.
The fixture's own comment already anticipates the leak half of this ("a single run can strand several containers that then live forever"), but the cost while the run is live is the bigger one.
2. Docker starvation produces a silent hang, not an error
With nine unrelated containers holding 4.7 GiB of a 7.7 GiB Docker VM, CIPulsar sat at:
[INF] Filter kept 224 of 224 discovered test(s)
[INF] 224 test(s): 224 batched, 0 isolated
…and never progressed. No Pulsar container was running (docker ps showed none), no timeout fired, nothing in the output pointed at Docker. A compose-started Pulsar in the same conditions was OOM-killed with exit 137. Stopping the unrelated containers fixed it immediately.
Worth a startup precheck, or at minimum a bounded wait with a message naming Docker.
3. The image tag is unpinned, and disagrees with docker-compose
| Where |
Image |
PulsarContainerFixture |
apachepulsar/pulsar:**latest** |
docker-compose.yml |
apachepulsar/pulsar:**4.0.3** |
We have already been bitten by exactly this once: an unpinned :latest on the Azure Service Bus emulator moved 2.0.0 → 2.0.1 underneath us and made a CI-only failure look like a code regression (#3783). A pinned tag is also what makes a bisect trustworthy.
A cheap fix for all three
docker-compose.yml already defines a pulsar service, pinned at 4.0.3 — and nothing uses it. CIPulsar starts only postgresql:
StartDockerServices("postgresql");
Meanwhile the fixture already has the escape hatch: set WOLVERINE_PULSAR (and optionally WOLVERINE_PULSAR_HTTP) and it skips the container start entirely — "An already-running Pulsar wins", same shape as Servers.cs. That env var is currently set nowhere.
So the obvious direction is to start the pinned compose Pulsar once per job and point every worker process at it:
StartDockerServices("pulsar", "postgresql");
// + export WOLVERINE_PULSAR for the test processes
One broker per job instead of one per process, a pinned version, and the same code path developers already use for Postgres and SQL Server. The Testcontainers path stays as the fallback for anyone without compose.
Worth confirming first that a shared broker doesn't reintroduce cross-process topic collisions — the RabbitMQ pass (#3795) found exactly that shape where test resource names were not unique per process.
🤖 Generated with Claude Code
https://claude.ai/code/session_0116vfBcKwcjWn8msM4ZjkuA
Follow-up to #3763 / #3798, where all three Pulsar compliance classes were restored to CI. Working on that suite surfaced three separate problems with how the tests get a broker. None of them is the transport's fault; all of them cost real time.
1. One container per worker process
PulsarContainerFixturestarts a Pulsar container from a[ModuleInitializer]:A
[ModuleInitializer]runs once per process. The supervisor partitions a project across worker processes — I saw runs at 2, 3, 6 and 24 workers depending on machine load — and the retry harness spawns another fresh process per retried test. So a singleCIPulsarrun can ask Docker for many concurrent Pulsar brokers, and Pulsar standalone is not a small image.The fixture's own comment already anticipates the leak half of this ("a single run can strand several containers that then live forever"), but the cost while the run is live is the bigger one.
2. Docker starvation produces a silent hang, not an error
With nine unrelated containers holding 4.7 GiB of a 7.7 GiB Docker VM,
CIPulsarsat at:…and never progressed. No Pulsar container was running (
docker psshowed none), no timeout fired, nothing in the output pointed at Docker. A compose-started Pulsar in the same conditions was OOM-killed with exit 137. Stopping the unrelated containers fixed it immediately.Worth a startup precheck, or at minimum a bounded wait with a message naming Docker.
3. The image tag is unpinned, and disagrees with docker-compose
PulsarContainerFixtureapachepulsar/pulsar:**latest**docker-compose.ymlapachepulsar/pulsar:**4.0.3**We have already been bitten by exactly this once: an unpinned
:lateston the Azure Service Bus emulator moved 2.0.0 → 2.0.1 underneath us and made a CI-only failure look like a code regression (#3783). A pinned tag is also what makes a bisect trustworthy.A cheap fix for all three
docker-compose.ymlalready defines apulsarservice, pinned at 4.0.3 — and nothing uses it.CIPulsarstarts onlypostgresql:Meanwhile the fixture already has the escape hatch: set
WOLVERINE_PULSAR(and optionallyWOLVERINE_PULSAR_HTTP) and it skips the container start entirely — "An already-running Pulsar wins", same shape asServers.cs. That env var is currently set nowhere.So the obvious direction is to start the pinned compose Pulsar once per job and point every worker process at it:
One broker per job instead of one per process, a pinned version, and the same code path developers already use for Postgres and SQL Server. The Testcontainers path stays as the fallback for anyone without compose.
Worth confirming first that a shared broker doesn't reintroduce cross-process topic collisions — the RabbitMQ pass (#3795) found exactly that shape where test resource names were not unique per process.
🤖 Generated with Claude Code
https://claude.ai/code/session_0116vfBcKwcjWn8msM4ZjkuA