How would you allocate Appium session ports across Android and Apple lanes on a shared host?
answer
- allocation, not configuration
- base number plus worker index
- one band per capability family
- two lanes need two maps
basics
~20 sDerive every port from the worker index inside a band reserved per capability family and per platform, pin the scheme in one place, log each resolved value with the session id, and reclaim leaked ports before a run starts.
solid answer
~40 sTreat it as allocation, not configuration. Reserve a band per capability family — one for `appium:systemPort`, one for `appium:chromedriverPort`, one for `appium:mjpegServerPort`, one for `appium:wdaLocalPort` — and compute each worker's value as base plus index, so two workers cannot arrive at the same number and no lane wanders into another's range. Keep the Android and Apple maps separate, because the two platforms share almost no capability names and only `appium:mjpegServerPort` appears on both. Put the scheme in one module the harness calls, never in per-worker config files that drift. Log every resolved value beside its session id so a failed run stays readable afterwards. And plan for a shared host: leftover forwards from a killed run, and other tenants, mean the bands are checked before a run rather than assumed.
code
python · 24 linesSYSTEM_PORT_BASE = 20000
CHROMEDRIVER_PORT_BASE = 21000
MJPEG_PORT_BASE = 22000
WDA_PORT_BASE = 23000
def ports_for(platform_name, worker_index, needs_webview=False):
if platform_name == "Android":
caps = {
"appium:systemPort": SYSTEM_PORT_BASE + worker_index,
"appium:mjpegServerPort": MJPEG_PORT_BASE + worker_index,
}
if needs_webview:
caps["appium:chromedriverPort"] = CHROMEDRIVER_PORT_BASE + worker_index
return caps
return {
"appium:wdaLocalPort": WDA_PORT_BASE + worker_index,
"appium:mjpegServerPort": MJPEG_PORT_BASE + worker_index,
"appium:derivedDataPath": f"/tmp/call-sheet/derived-{worker_index}",
}
print(ports_for("Android", 3, needs_webview=True))
print(ports_for("iOS", 3))go deeper
Understand that the numbers are handed out rather than typed in, and that a worker's ports come from its index so two workers cannot end up with the same value.
Explain band-per-family arithmetic and why it beats a free-port search: no race between checking and claiming, and values that can be reproduced from a run log.
Cover what the scheme must survive on a real host — leaked forwards from killed runs, other tenants, uneven lanes — and the startup assertions that turn a silent misroute into a loud failure.
Own the boundaries: which ranges the fleet claims on shared machines, how band sizes are validated as worker counts grow, and where the single allocator lives so the port story stays reviewable.
## The problem is allocation, not configuration A fleet running Appium sessions side by side has to hand out several numbers per session, and the numbers have to be unique across everything alive on that host at that moment. Written as configuration — a value typed into each worker's file — it drifts the first time somebody adds a worker. Written as allocation — a function from a worker index to a set of values — it cannot drift, because the uniqueness is arithmetic rather than discipline. That reframing is most of the design. The rest is deciding what the function has to know and what it has to survive. ## Bands, not a pool Give each capability family its own reserved range, and give each worker its offset inside it: - One band for the Android agent channel, `appium:systemPort`. - One band for the Android web-view process, `appium:chromedriverPort`. - One band for the screenshot stream, `appium:mjpegServerPort`, allocated separately per lane. - One band for the Apple agent channel, `appium:wdaLocalPort`. - One directory scheme, not a port band, for `appium:derivedDataPath`. Bands beat a free-port search for three reasons. The search races: two workers can each see the same port as free and then both claim it. The search is invisible: a run that failed yesterday cannot be replayed with the values it used. And the search is unbounded, so an operator has no way to know which numbers the fleet expects to own. ## Two lanes, two maps The Android per-session set and the Apple per-session set were named independently. `appium:systemPort` and `appium:wdaLocalPort` do the same job under names that share nothing; `appium:chromedriverPort` has no Apple counterpart; `appium:derivedDataPath` has no Android counterpart; only `appium:mjpegServerPort` is spelled the same, and even there the mechanism differs. So the allocator branches on `platformName` and returns a different shape per lane. A single map of capability names applied to both platforms will either set names a driver ignores or omit names it needs — and both mistakes are silent. ## What the scheme has to survive 1. **Killed runs.** A run terminated rather than closed can leave a forward holding a host port. The next run claiming that number either fails to start or reaches a stale agent, so the fleet needs a reclaim step before a run, not an incident afterwards. 2. **A shared host.** On a machine that also serves other jobs, the reserved bands are a claim on somebody else's resource. Choose ranges deliberately, write them where an operator will find them, and check availability at run start. 3. **Growth.** Bands sized for four workers are a trap when someone tries twelve. Size them with headroom and make the sizes explicit rather than implied by the arithmetic. 4. **Uneven lanes.** A native-only Android lane allocates no Chromedriver port; an Apple lane allocates a directory as well as a port. The allocator should return only what that lane actually uses. ## Where the scheme lives One module, called by the harness at session creation, that takes the platform and the worker index and returns the capability map. Not per-worker YAML, not environment variables set by hand in a runner, not a comment in a wiki. Concentrating it makes the fleet's port story reviewable, and it means adding a worker is an argument change rather than an edit in five files. Alongside it, one log line per session: the session id, the target device, and every resolved value. That line is what makes a failure diagnosable by whoever reads the run tomorrow. ## Knowing it works The scheme is healthy when a run that goes wrong can be explained without reproducing it. Two signals are worth building in: - Assert at session start that the session is driving the device it asked for, so a misrouted channel fails loudly instead of masquerading as flake. - Compare the count of live sessions with the count of devices actually doing work; an idle device beside a busy one is the classic shape of a shared channel. With those two in place the port question stops being a recurring investigation. It becomes a property of the harness that either holds or reports itself broken, which is the only stable end state for a fleet that keeps growing.
- Why prefer reserved bands over asking the operating system for any free port?A free-port search races — two workers can see the same port as free before either binds it — and it leaves no record, so a failed run cannot be replayed with the values it used. Bands are deterministic, reproducible from the worker index, and legible to an operator who needs to know which numbers the fleet claims on a shared host.
- How do you keep the scheme honest as the fleet grows past its original band sizes?Make the band sizes explicit constants rather than an implication of the arithmetic, and validate at startup that the requested worker count fits. A run that would overflow a band should refuse to start with a clear message. Silent wraparound into a neighbouring band is the failure that shows up as flake weeks later.
- What belongs in the run log for the port scheme to be diagnosable after the fact?One line per session carrying the session id, the target device identifier, and every resolved value — the agent port for that platform, any web-view or stream port, and the Apple lane's derived-data directory. Two lines showing the same port with different devices identify the fault in seconds, without reproducing the run.
saying these in an interview costs you the question
- Hardcodes port numbers in each worker's own configuration file
- Uses one port map for both the Android and Apple lanes
- Assumes a killed run always releases its forwarded ports
- Picks random free ports and never logs the chosen values
- Treats a shared CI host as if the suite owned every port