skip to content

In mountebank, how do you stop two parallel workers creating marina-berth imposters on the same port?

level: seniorimportance: should knowfreq 52%

answer

  1. free only until someone else binds
  2. check, release, then race to bind
  3. let the binder do the allocating
  4. post the imposter with no port
  5. read port off the created imposter

basics

~20 s

Stop computing the port yourself. Post the imposter to mountebank with no port field, and mountebank binds a free one and returns it in the created imposter. A pre-computed number is only free until someone else binds it.

solid answer

~40 s

The collision is a check-then-bind race. Anything that *reports* a free port — MockServer's `org.mockserver.socket.PortFactory.findFreePort()`, or a hand-rolled equivalent — opens a socket, reads the number the kernel gave it, and closes it again; from that instant the number is a guess, and two workers running milliseconds apart can be handed the same one. The fix is to make the process that binds also allocate. In mountebank, `POST /imposters` with the `port` field omitted: mountebank binds a free port itself and echoes the imposter back with `port` populated, so the worker reads a number that is already bound to it. In WireMock the equivalent is `wireMockConfig().dynamicPort()`, whose value is read back with `getHttpBaseUrl()`. The diagnostic signature of the race is an intermittent bind failure on one worker while the rest of the run passes.

code

json · 18 lines
json
{
  "protocol": "http",
  "name": "marina-berths-worker",
  "stubs": [
    {
      "predicates": [{ "equals": { "method": "GET", "path": "/berths" } }],
      "responses": [
        {
          "is": {
            "statusCode": 200,
            "headers": { "Content-Type": "application/json" },
            "body": { "berths": [{ "berthId": "FAL-A12", "lengthMetres": 12 }] }
          }
        }
      ]
    }
  ]
}

go deeper

for a junior

Be ready to say that a port a helper reported as free is only a guess by the time you use it, and that mountebank will choose one for you if you leave the port field out of the imposter body.

for a middle

Explain the check-then-bind window mechanically: the probe socket is opened, read and closed, and the number is unclaimed from that moment. Say why allocating inside the process that binds removes it entirely.

for a senior

Show how you would diagnose one worker failing to bind while the rest pass, and argue why a retry loop is a worse remedy than moving allocation into mountebank itself.

for a principal

Own the rule across the estate: ports are outputs, never inputs, and any suite that pre-computes one is carrying a race. Say what you would put in a template so teams stop rediscovering this.

## What a parallel run actually needs from a port A parallel run starts several worker processes on one machine, and each one wants its own stub of the marina berth-booking API — its own `GET /berths` listing, its own `POST /berths/{berthId}/bookings` returning a `bookingRef`, its own answers and nobody else's. A stub server is a listening TCP socket, and two sockets cannot hold the same port on the same interface. So every worker needs a number that is unique for the life of its run, and it needs that number *before* it can tell the code under test where to send requests. There are only two ways to get one: guess and check, or ask the thing that binds. The first is where collisions come from. ## Why a free-port helper cannot promise anything A free-port helper does the obvious thing. It opens a socket on port zero, asks the kernel which port it was given, closes the socket, and returns the number. MockServer ships exactly such a helper: `findFreePort()` on the class `org.mockserver.socket.PortFactory`. The number it returns *was* free. It is not free by contract afterwards — it is merely unclaimed, and the helper has just released it. That gap is a **check-then-bind race**, and a parallel run is precisely the environment built to lose it: - Two workers can call the helper in the same millisecond and be handed the same number, because each probe closed its socket before the other opened one. - One worker can hold a number while the harness does other setup, and an unrelated process on the machine takes it in between. - Retrying on failure narrows the window but never closes it, and converts a design fault into intermittent slowness. - The failure surfaces on **one** worker while the rest pass, which reads as flakiness rather than as a race. ## Letting mountebank allocate Mountebank closes the window by making the port an *output* of imposter creation rather than an input to it. The sequence a worker follows is short: 1. Build the imposter body — `protocol`, a `name`, and the `stubs` array whose `predicates` match the marina berth routes and whose `responses` carry the `is` replies. 2. **Omit the `port` field.** Mountebank then binds a free port itself, inside the process that will hold the socket, with no gap between choosing and binding. 3. Read the `port` off the imposter document that `POST /imposters` returns. That number is already bound to this worker. 4. Compose the base URL from it and hand that to the code under test. The important property is not that mountebank is clever. It is that allocation and bind happen in one place, so nothing can take the number in between. ## The same problem, three surfaces This is where attribution matters most on this subject, because the three products solve one problem with names that are easy to swap by mistake. | product | how a free port is obtained | how the value is read back | |---|---|---| | Mountebank | omit `port` from the `POST /imposters` body and mountebank binds one | the `port` field of the returned imposter | | WireMock | `wireMockConfig().dynamicPort()`, or `dynamicHttpsPort()` for TLS | `getHttpBaseUrl()` / `getHttpsBaseUrl()` | | MockServer | `PortFactory.findFreePort()`, then start the server on that number | `getPort()` on `MockServerClient` | `PortFactory` is **MockServer's** class, `org.mockserver.socket.PortFactory`. It is not part of WireMock at all; WireMock's own test suite reaches a free port through `Network.findFreePort()`, and the WireMock DSL's answer to this problem is `dynamicPort()`, which delegates the choice to the server itself. Attributing MockServer's `PortFactory` to WireMock is the commonest way to get this wrong, and because both names are real it survives a careless review. Note what the table implies about the race. Mountebank's and WireMock's routes have none, because the binder allocates. MockServer's helper does, because it is a probe — which is why a MockServer suite running many workers has to treat imposter-style allocation failures as expected and start the server immediately after the probe, not several setup steps later. ## Getting the number to the code under test A resolved port is useless until the application under test uses it. In a parallel run the value differs per worker, so it cannot be a constant: - Pass it as an environment variable or a start-up property the application resolves at boot. - Pass it as a constructor or builder argument to the client the test drives. - Rebuild the base URL per worker rather than assembling it once for the whole suite. ## When it still collides - Check `GET /imposters`: a port you did not create belongs to another run on the same machine. - Check that no earlier step pre-computes a port and passes it into the worker as configuration. - Check that a retried worker creates a fresh imposter rather than reusing the number from its first attempt. - Check that the harness does not create the imposter in one process and start the application in another with a copied literal.

  • Why is a retry loop around imposter creation a worse fix than letting mountebank allocate?
    It narrows the race without closing it: every attempt still probes, releases and re-binds, so two workers can lose the same way twice. It also disguises the fault as slowness, making the suite flakier and slower at once. Omitting `port` from `POST /imposters` moves allocation inside the process that binds, which is the only change that removes the window rather than shrinking it.
  • What must happen to the port a worker read back before the code under test starts?
    It has to reach the application as configuration — an environment variable, a start-up property, a constructor argument — and it has to be read per worker rather than once for the suite. Order matters too: if the application resolves its base URL at boot, the imposter must exist first, or the worker points at nothing.

saying these in an interview costs you the question

  • Says a free-port helper guarantees the port stays free
  • Pre-computes ports in the runner and hands them to workers
  • Calls PortFactory a WireMock class
  • Adds a retry loop instead of removing the race
  • Blames the operating system for intermittent bind failures