In mountebank, why is a base-port-plus-worker-index scheme worse than letting mb assign the port?
answer
- arithmetic is not a reservation
- assumed free, never actually checked
- collisions across suites, not just workers
- the silent case beats the loud one
- make the port an output
basics
~20 sNothing on the machine promises that block is yours. A second suite, a leftover process or another service can already hold one, and the worst outcome is not a bind failure but a worker quietly reaching another run's imposter.
solid answer
~50 sA base port plus a worker index is arithmetic, not an allocation: nothing on the machine agreed to leave that block alone. Anything else already listening — a second suite, a leftover `mb` process from a crashed run, an unrelated service — takes one of those numbers, and your imposter cannot bind. Worse is the case that does *not* fail: two runs using the same base, where worker three of each reaches whichever imposter took the number first, receives a plausible marina-berth reply from the wrong stub set, and passes or fails for a reason unrelated to the code. The scheme also breaks silently whenever the worker count, the base, or a retried worker's identity changes. Letting mountebank allocate — `POST /imposters` with no `port` field — makes the number an output you read rather than an input you assert.
go deeper
Be able to say why a fixed port per worker is an assumption rather than a reservation, and what happens when something else on the machine already holds that number.
Explain both failure modes and rank them: a bind error is loud and quickly fixed, while reaching another run's imposter is silent and produces a plausible wrong answer.
Argue the case for making the port an output, and describe how you would spot a suite that has quietly been talking to the wrong imposter for weeks.
Decide when a declared port is still acceptable — a serial run, a component that cannot be re-pointed — and state what you require of any suite that keeps one.
## What the scheme assumes, and who agreed to it The scheme is appealing because it is arithmetic you can do in your head: pick a base, add the worker's index, and every worker has a port nobody else in the suite will use. It is genuinely collision-free *within* the suite. The problem is everything outside it. A port is not owned by whoever wrote it down. It is held by whichever process bound it first. A base-port block is therefore a declaration of intent with no mechanism behind it — nothing on the machine has agreed to leave those numbers alone, and nothing will tell you when something else takes one. The suite is asserting a fact about a shared resource it does not control. ## Two failure modes, and the one that hurts When the assumption breaks, it breaks in one of two ways, and they are not equally bad. - **The loud one.** Something already holds the number, so creating the imposter fails outright. The worker never starts, the error names the port, and someone fixes it within the hour. This is the *good* outcome. - **The quiet one.** Another run's imposter is already listening on that number. Your worker's request to `GET /berths` is answered — by somebody else's stub set. The reply is well-formed JSON, plausibly shaped, and wrong. The test passes or fails on data the run never configured, and nothing in the output points at the port. The quiet one is what makes this pattern worth arguing about. A bind error is a bug report. A wrong-imposter answer is a suite that has been lying for weeks, and the only tell is that `GET /imposters` lists an imposter this run did not create. ## Why the arithmetic stops working Even inside the suite, the scheme is fragile in ways that are hard to see in review: - Change the worker count and the block's width changes with it; the base may now overlap something that used to sit safely above it. - Run two jobs on one machine and both compute the same numbers from the same base, so the blocks overlap exactly. - Retry a failed worker in a fresh process and it recomputes the same number, which its own predecessor may still be holding. - Move the suite to a different machine and any assumption about which numbers are quiet there moves with it, untested. - Let the operating system hand one of those numbers to an unrelated socket first, and the block is punctured with no warning at all. None of these is exotic. Each is a normal week in CI. ## What changes when the port becomes an output Mountebank's answer is to make the number something the run *learns* rather than something it *asserts*. Post the imposter with no `port` field; mountebank binds a free one inside the process that will hold the socket and returns it on the created imposter. From that point: - No two workers can be handed the same number, because the binder is the allocator. - A leftover imposter from a crashed worker costs a socket, not a run. - A second suite on the same machine is invisible to yours; there is no shared block to overlap. - The worker count can change freely, because nothing computes anything. - The only remaining discipline is passing the resolved number into the code under test, which is a real requirement but a much smaller one. The other two products in this space reach the same place by different names, and the names are easy to swap: WireMock's route is `wireMockConfig().dynamicPort()` with the value read back through `getHttpBaseUrl()`, while `PortFactory.findFreePort()` is MockServer's helper and belongs to no other product. ## When a declared port is still defensible The pattern is not universally wrong, and an interviewer will respect a candidate who can name the exceptions: - A strictly serial run, where there is only ever one imposter at a time and the number is a convenience. - A component whose base URL genuinely cannot be re-pointed at run time — an image with a baked-in address, a configuration file you do not own. - A local development loop where a stable, memorable port is worth more than robustness. In each case the right move is to declare the port *explicitly and visibly*, treat a bind failure as an expected and informative error, and keep the arrangement out of any suite that runs workers in parallel. ## The rule worth writing down Ports are outputs. A suite that computes one has taken on a race and a silent-crosstalk risk in exchange for saving one read from a response body — which is never a trade worth making once more than one worker is involved.
- When is a declared imposter port still the right choice?When the run is serial and something downstream genuinely cannot be re-pointed — an image with a baked-in base URL, a configuration file you do not own, a local loop where a memorable number helps. Declare it explicitly, treat a bind failure as informative rather than flaky, and keep the arrangement out of any suite that runs workers in parallel.
- How do you tell a bind failure from a worker reaching the wrong imposter?A bind failure is loud and early: `POST /imposters` fails and the worker never starts. The wrong-imposter case is quiet — the call succeeds and the body is plausible but not the one you stubbed. `GET /imposters` listing an imposter this run did not create is the tell that separates them.
A base-port block is a parking bay you reserved by painting a number on the tarmac yourself. Letting mountebank allocate is taking a ticket from the machine that checks a bay is actually empty before it prints one.
saying these in an interview costs you the question
- Says a per-worker offset guarantees uniqueness
- Assumes this suite is the only thing on the machine
- Treats bind failures as flakiness and retries the job
- Widens the port block instead of dropping the assumption
- Cannot describe the wrong-imposter failure at all