Checkpoint Lab — Selenium Grid Architecture, Roles, Routing, and Session Distribution
The checkpoint treats Grid as an operating system for browser sessions. You will predict routing changes, run either a real two-Node local Hub/Node topology or a faithful no-browser simulation, prove placement, inject one failure, preserve evidence, and restore service without masking the root cause.
Learning objectives
- Define a two-lane Chrome/Firefox Grid with explicit one-session Node capacity.
- Predict queue, slot, Session Map, and evidence changes before execution.
- Prove which Node owns each session using GraphQL/status evidence.
- Inject a Node or capability failure and trace the request through queue/matching evidence.
- Restore service and verify deterministic cleanup with no orphan sessions.
1. Checkpoint scenario and safety boundary
The preferred live topology is one local Hub plus two local Nodes: one Chrome lane and one Firefox lane, each capped at one session. This requires both browsers on the same disposable lab host. If one browser is unavailable, use the supplied faithful simulation—the prompt explicitly permits a simulated local Grid. Do not point either path at employer/customer infrastructure or a public Router.
2. Setup and exact assumptions
- Python 3.10+ and
selenium==4.47.0. -
Selenium Server/Grid
4.47.0with Java 11+ for live mode. -
Live mode: Chrome and Firefox installed, Node driver resolution
via
--selenium-manager true. -
Hub/Router:
127.0.0.1:4444; Chrome Node: port 5555; Firefox Node: port 6666. -
AUT: loopback
127.0.0.1:8784because all live Nodes are on the same host. - Each Node max sessions = 1. Queue timeout = 8 seconds for controlled failure.
- No real credentials, cloud accounts, production URL, proxy/TLS override, or public Grid.
The following example makes the Setup and exact assumptions behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from pathlib import Path
ROOT = Path("chapter19_fixture")
SITE = ROOT / "site"
SITE.mkdir(parents=True, exist_ok=True)
(SITE / "index.html").write_text(r"""<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Grid Routing Lab</title><style>body{font-family:system-ui;margin:32px;max-width:800px}.card{border:1px solid #888;border-radius:12px;padding:16px;margin:16px 0}button{padding:9px 13px}</style></head>
<body><h1 data-testid="heading">Grid Routing Lab</h1><div class="card"><p data-testid="state" data-state="ready">ready</p><button data-testid="advance" type="button">Advance state</button></div>
<script>
const state=document.querySelector('[data-testid="state"]');
document.querySelector('[data-testid="advance"]').addEventListener('click',()=>{state.dataset.state='advanced';state.textContent='advanced'});
</script></body></html>""", encoding="utf-8")
(ROOT / "serve.py").write_text(r"""from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
ROOT=Path(__file__).resolve().parent; SITE=ROOT/"site"; HOST="127.0.0.1"; PORT=8784
class H(SimpleHTTPRequestHandler):
def translate_path(self,path):
raw=super().translate_path(path); rel=Path(raw).relative_to(Path.cwd()); return str(SITE/rel)
def log_message(self,fmt,*args): pass
server=ThreadingHTTPServer((HOST,PORT),H); print(f"Grid fixture: http://{HOST}:{PORT}/")
try: server.serve_forever()
except KeyboardInterrupt: pass
finally: server.server_close()
""", encoding="utf-8")
print(ROOT.resolve())
3. Write predictions before running
Record at least these predictions in predictions.md:
- A Chrome request will match the Chrome stereotype and create a Session Map entry pointing to the Chrome Node URI; Firefox behaves analogously.
- After the Firefox Node is stopped, a new Firefox request will enter the queue but cannot be assigned; it will fail after the configured request timeout while a Chrome request can still use the healthy Chrome Node.
-
After each
quit(), that session disappears from Grid state and its slot becomes free.
4. Live path: start Hub and two Nodes
Use three terminals. The Hub packages Router, Distributor, Session Map, Queue, and Event Bus. Two Nodes register through the Hub Event Bus and advertise their browser lanes.
# Terminal A — Hub/control plane
java -jar selenium-server-4.47.0.jar hub --session-request-timeout 8 --session-request-timeout-period 1 --port 4444
# Terminal B — Chrome Node
java -jar selenium-server-4.47.0.jar node --hub http://127.0.0.1:4444 --port 5555 --max-sessions 1 --driver-implementation chrome --selenium-manager true
# Terminal C — Firefox Node
java -jar selenium-server-4.47.0.jar node --hub http://127.0.0.1:4444 --port 6666 --max-sessions 1 --driver-implementation firefox --selenium-manager true
If either Node does not register, stop and diagnose private port/address/browser availability. Do not replace the missing lane silently and still call the run “two-browser live.”
5. Preflight and capture baseline evidence
The following example makes the Preflight and capture baseline evidence behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from urllib.request import urlopen
from pathlib import Path
import json
out = Path("grid-checkpoint-evidence")
out.mkdir(exist_ok=True)
with urlopen("http://127.0.0.1:4444/status", timeout=3) as r:
status = json.load(r)
(out / "status-before.json").write_text(json.dumps(status, indent=2), encoding="utf-8")
print("saved", out / "status-before.json")
Verify two registered Nodes and inspect their stereotypes before creating any session. Save Hub/Node console logs separately if your shell/CI captures them; include timestamps and keep attempt-1 evidence immutable.
6. Create one session per stereotype and prove placement
Run this with sessions sequentially if the host is resource-constrained; the purpose is placement, not load testing.
import json
from urllib.request import Request, urlopen
from selenium import webdriver
GRID = "http://127.0.0.1:4444"
def placement(session_id):
query_text = """{
session(id: "%s") { id nodeId nodeUri slot { id stereotype } }
}""" % session_id
q = {"query": query_text}
req = Request(GRID + "/graphql", data=json.dumps(q).encode(),
headers={"Content-Type":"application/json"})
with urlopen(req, timeout=3) as r:
return json.load(r)
def run(label, options):
driver = webdriver.Remote(GRID, options=options)
try:
driver.get("http://127.0.0.1:8784/")
print(label, driver.session_id, driver.capabilities.get("browserName"))
print(json.dumps(placement(driver.session_id), indent=2))
assert driver.title == "Grid Routing Lab"
finally:
driver.quit()
run("chrome", webdriver.ChromeOptions())
run("firefox", webdriver.FirefoxOptions())
Expected: each session reports the requested browser family, and GraphQL shows the corresponding Node URI and slot stereotype. This is independent proof of routing rather than inference from the client-side browser name alone.
7. Inject one Node failure and preserve first evidence
Stop only the Firefox Node process (Ctrl+C in Terminal C). Do not
stop the Hub or Chrome Node. Save a new
/status snapshot. Then request Firefox once. The
request should be queued and eventually fail under the short lab
timeout. While it waits, capture the queue endpoint from another
terminal.
curl http://127.0.0.1:4444/status > grid-checkpoint-evidence/status-firefox-down.json
curl http://127.0.0.1:4444/se/grid/newsessionqueue/queue > grid-checkpoint-evidence/queue-firefox-down.json
The following example makes the Inject one Node failure and preserve first evidence behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
from selenium import webdriver
from selenium.common.exceptions import WebDriverException
try:
webdriver.Remote("http://127.0.0.1:4444", options=webdriver.FirefoxOptions())
except WebDriverException as exc:
print("expected Firefox creation failure")
print(str(exc)[:1000])
Do not retry automatically. The first failure is the evidence: requested stereotype Firefox + missing/unavailable matching Node + queue entry/timeout.
8. Control experiment: prove healthy capacity still routes
After the Firefox failure, create a Chrome session. If Chrome still succeeds and is placed on the Chrome Node, the incident is lane-specific rather than a total Router/Hub outage. This is a high-value isolation step.
from selenium import webdriver
driver = webdriver.Remote("http://127.0.0.1:4444", options=webdriver.ChromeOptions())
try:
print("healthy lane", driver.session_id, driver.capabilities.get("browserName"))
finally:
driver.quit()
9. Restore the failed lane and verify recovery
Restart the Firefox Node with the exact original command. Wait for
it to appear in /status, then run one Firefox session
again and prove GraphQL placement. Recovery is complete only when
the requested lane is registered, can create a session, and returns
its slot to free after quit().
10. Faithful simulation fallback when live two-browser prerequisites are missing
The simulation uses the same state machine: queue requests, Distributor matching, Node up/down state, slots, and Session Map ownership. It is explicitly labeled simulation and therefore cannot prove browser-driver behavior, but it faithfully exercises the architectural reasoning required by this checkpoint.
from dataclasses import dataclass, field
from collections import deque
from pathlib import Path
import json
@dataclass
class Slot:
browser: str
session_id: str | None = None
@dataclass
class Node:
name: str
uri: str
slots: list[Slot]
up: bool = True
@dataclass
class Request:
request_id: str
browser: str
class GridModel:
def __init__(self, nodes):
self.nodes = nodes
self.queue = deque()
self.session_map = {}
self.events = []
self.counter = 0
def emit(self, event, **data):
row = {"event": event, **data}
self.events.append(row)
print(json.dumps(row))
def submit(self, req):
self.queue.append(req)
self.emit("queue.add", request=req.request_id, browser=req.browser)
def distribute_once(self):
if not self.queue:
return False
req = self.queue[0]
for node in self.nodes:
if not node.up:
continue
for slot in node.slots:
if slot.browser == req.browser and slot.session_id is None:
self.queue.popleft()
self.counter += 1
session_id = f"session-{self.counter:02d}"
slot.session_id = session_id
self.session_map[session_id] = node.uri
self.emit("distributor.match", request=req.request_id, node=node.name, browser=slot.browser)
self.emit("session.created", session=session_id, node=node.name, uri=node.uri)
return True
self.emit("distributor.no_match", request=req.request_id, browser=req.browser)
return False
def release(self, session_id):
node_uri = self.session_map.pop(session_id, None)
for node in self.nodes:
for slot in node.slots:
if slot.session_id == session_id:
slot.session_id = None
self.emit("session.released", session=session_id, node_uri=node_uri)
def status(self):
return {
"queue": [r.__dict__ for r in self.queue],
"session_map": dict(self.session_map),
"nodes": [{"name": n.name, "uri": n.uri, "up": n.up,
"slots": [s.__dict__ for s in n.slots]} for n in self.nodes]
}
nodes = [
Node("node-chrome", "http://127.0.0.1:5555", [Slot("chrome")]),
Node("node-firefox", "http://127.0.0.1:6666", [Slot("firefox")]),
]
grid = GridModel(nodes)
grid.submit(Request("req-chrome", "chrome"))
grid.submit(Request("req-firefox", "firefox"))
grid.distribute_once(); grid.distribute_once()
# Inject one node failure, then submit another Firefox request.
nodes[1].up = False
grid.emit("node.down", node="node-firefox")
grid.submit(Request("req-firefox-2", "firefox"))
grid.distribute_once()
out = Path("grid-simulation-evidence")
out.mkdir(exist_ok=True)
(out / "events.json").write_text(json.dumps(grid.events, indent=2), encoding="utf-8")
(out / "status.json").write_text(json.dumps(grid.status(), indent=2), encoding="utf-8")
print(out.resolve())
The following example makes the Faithful simulation fallback when live two-browser prerequisites are missing behavior concrete. Read it with the stated assumptions, then compare its observable output or state changes with the explanation that follows.
python grid_model_sim.py
# Inspect:
# grid-simulation-evidence/events.json
# grid-simulation-evidence/status.json
Expected event order: both initial requests enter the queue and are
matched to their corresponding Nodes; then the Firefox Node is
marked down; the second Firefox request stays queued and emits
distributor.no_match.
11. Required evidence packet
-
predictions.mdwith predicted session/queue/slot changes. -
status-before.jsonwith Node/stereotype baseline. - For live mode: session IDs, returned capabilities, GraphQL placement for Chrome/Firefox.
-
status-firefox-down.jsonandqueue-firefox-down.jsonor equivalent timestamped observations. - First failure text from the Firefox request; do not overwrite it after recovery.
- Control Chrome success evidence.
- Recovery status/placement evidence.
-
For fallback:
events.jsonandstatus.jsonlabeled simulated. - A short conclusion naming the failing layer and why Router, AUT waits, and retries were not the repair.
12. Verification checklist
- Exactly the intended Node/browser stereotypes were present before live execution.
- Each live session was correlated to a Node URI/slot, not merely a client browser name.
- Stopping Firefox affected only Firefox placement; healthy Chrome remained usable.
- The failed Firefox request produced queue/matching evidence before correction.
- No blanket retry, giant timeout, JavaScript bypass, TLS disablement, or public Grid exposure was used.
- After recovery, Firefox can create one session again.
-
After final
quit(), Grid reports zero lab sessions and all intended slots are free.
13. Cleanup and rollback
Quit any live WebDriver sessions first. Stop the Firefox Node, Chrome Node, Hub, and AUT server with Ctrl+C. Remove only the disposable fixture/evidence/simulation directories after reviewing the evidence. Confirm ports 4444, 5555, 6666, and 8784 are no longer owned by lab processes.
from pathlib import Path
import shutil
for p in [Path("chapter19_fixture"), Path("grid-checkpoint-evidence"), Path("grid-simulation-evidence")]:
if p.exists():
shutil.rmtree(p)
print("Chapter 19 lab artifacts removed")
14. What Chapter 19 adds to the production operating model
Your Selenium platform now has a Grid control-plane model: requests are queued and matched to measured browser capacity; running sessions have explicit Node ownership; health and placement are observable; failures are classified by queue, stereotype, Node, route, or test/AUT layer; and Grid endpoints stay protected. This is the foundation needed to package and scale the same roles safely.
15. Bridge to Chapter 20 — Deploying Grid with Containers, Kubernetes, and Cloud Infrastructure
Chapter 20 moves the logical roles into deployment substrates. Containers and Kubernetes change process/network/storage/resource boundaries, but they do not change the Router/Queue/Distributor/Session Map/Event Bus/Node responsibilities learned here. The main question becomes how to preserve those contracts while adding ephemeral Nodes, images, service discovery, volumes, and orchestration.
Knowledge check
What evidence proves a live browser session was placed on a particular Node?
Use the session ID together with Grid GraphQL/session data showing the owning node ID/URI and slot stereotype; returned browser capabilities alone do not prove the Node URI.
After the Firefox Node is stopped, why should a Chrome control session still be attempted?
It distinguishes a lane-specific Node/capability failure from a total Router/Hub/control-plane outage.
Why is the simulation acceptable but not equivalent to a live Grid run?
It faithfully models queue/distributor/slot/session-map logic, but it cannot prove real Java server networking, browser driver startup, or actual RemoteWebDriver behavior.
What must happen after recovery before declaring the Firefox lane healthy?
The Node must register, advertise the intended stereotype, successfully create a Firefox session, and release its slot cleanly after teardown.
What does Chapter 19 add before containerized Grid deployment?
A precise logical routing/capacity/health model whose responsibilities must remain visible when processes move into containers or orchestrators.
Official references and version notes
- Selenium 4.47 release notes — current stable client and Grid baseline.
- Selenium downloads — Python and Selenium Server/Grid 4.47.0 stable releases.
- Selenium Grid — purpose and current Grid documentation entry point.
- Grid components — Router, Distributor, Session Map, New Session Queue, Node, and Event Bus responsibilities.
- Grid architecture — slots, stereotypes, sessions, and logical relationships.
- Grid getting started — Standalone, Hub/Node, Distributed roles, ports, Java/browser prerequisites, and Selenium Manager option.
- Grid CLI options — current Node/session queue/capacity/heartbeat/BiDi/managed-download options.
- Grid TOML configuration — reviewable configuration examples and Router authentication.
- Grid endpoints — status, Node, session, and New Session Queue endpoints.
- Grid GraphQL support — session placement, Node, queue, and capacity queries.
- External datastore — JDBC/Redis-backed Session Map patterns.
- Grid configuration help — use the pinned server JAR help as implementation-grounded configuration truth.
Version-sensitive behavior was rechecked against Selenium primary
documentation on 2026-08-28. Mandatory examples pin Selenium
Python and Selenium Server/Grid 4.47.0, Python 3.10+, and Java 11+
for the server path. Local Standalone/Hub/Node labs remain on
loopback/private networking and use Selenium Manager on the Grid
Node only through the current documented
--selenium-manager true option. Queue timeouts are
deliberately shortened only for disposable failure exercises. Grid
4 roles are taught as Router, New Session Queue, Distributor,
Session Map, Event Bus, Nodes, slots/stereotypes; Standalone
packages rather than replaces those responsibilities. External
state backends are optional advanced architecture and must be
revalidated against the pinned server/API before production use.
Paid browser clouds, public Grid endpoints, enterprise
identity/proxy infrastructure, and managed Kubernetes/cloud are
not required.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.