Chapter 19Lesson 05~285 minutes

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.

Checkpoint labTwo NodesPlacementFailure injectionRunbook

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.0 with 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:8784 because 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:

  1. A Chrome request will match the Chrome stereotype and create a Session Map entry pointing to the Chrome Node URI; Firefox behaves analogously.
  2. 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.
  3. 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.md with predicted session/queue/slot changes.
  • status-before.json with Node/stereotype baseline.
  • For live mode: session IDs, returned capabilities, GraphQL placement for Chrome/Firefox.
  • status-firefox-down.json and queue-firefox-down.json or 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.json and status.json labeled 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?

After the Firefox Node is stopped, why should a Chrome control session still be attempted?

Why is the simulation acceptable but not equivalent to a live Grid run?

What must happen after recovery before declaring the Firefox lane healthy?

What does Chapter 19 add before containerized Grid deployment?

Next chapter

Deploying Grid with Containers, Kubernetes, and Cloud Infrastructure: Core Concepts and Mental Model

Continue with Deploying Grid with Containers, Kubernetes, and Cloud Infrastructure: Core Concepts and Mental Model. It builds directly on the state, evidence, and operating assumptions established here, so carry those constraints forward rather than treating the next page as an isolated topic.

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.