Skip to content

Pulsar tests start one heavy broker container per worker process, on an unpinned :latest, and hang silently when Docker runs out of memory #3799

Description

@jeremydmiller

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions