Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/verification/grid-worker-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is an opt-in hosted experiment. Run **Grid Worker Recovery Drill** with GitHub Actions `workflow_dispatch`; it is not part of the regular browser matrix and it does not target a shared or remote Grid. The manual workflow creates a unique Compose project from the pinned Selenium Hub and Chrome image digests, uses one Chrome slot, checks the effective 2 GiB memory and shared-memory limits, and has an eight-minute job timeout with project-scoped teardown.

The drill first validates the Grid `/status` slot inventory and confirms it starts empty. It sends one `quality-drill-unsupported` request with the hub's five-second session queue timeout and one-second retry interval. Only a structured session-creation failure plus a validated inventory without that browser is `CAPABILITY_UNAVAILABLE`; connection trouble is `GRID_UNREACHABLE`.
The drill first validates the Grid `/status` slot inventory and confirms it starts empty. Grid's `ready` flag describes available capacity: a full one-slot Grid reports `ready: false` while its owned session is running. The harness requires readiness during initial startup, then keeps reading the validated inventory even while that slot is occupied. It sends one `quality-drill-unsupported` request with the hub's five-second session queue timeout and one-second retry interval. Only a structured session-creation failure plus a validated inventory without that browser is `CAPABILITY_UNAVAILABLE`; connection trouble is `GRID_UNREACHABLE`.

For the interruption, the TestNG worker creates one Chrome session through `WebDriverFactory`, visits a harmless local `data:` page, and atomically writes a private receipt before printing its readiness marker. The receipt directory is mode `0700`; the receipt is mode `0600`, and neither is uploaded. The Node harness independently matches the receipt's exact session in the validated Grid inventory, then sends `SIGKILL` only to the detached process group it spawned. It requires the orphan to remain visible before issuing one W3C DELETE for that receipt-authorized session. It rejects stale, mismatched, foreign, or inventory-unavailable receipts and never uses a blanket session delete.

Expand Down
5 changes: 3 additions & 2 deletions scripts/grid-recovery-drill.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function validatePrivateGridUrl(value) {

export function parseGridInventory(payload) {
const value = payload?.value;
if (!value || value.ready !== true || !Array.isArray(value.nodes)) {
if (!value || typeof value.ready !== 'boolean' || !Array.isArray(value.nodes)) {
throw failure('GRID_INVENTORY_SCHEMA_UNAVAILABLE', 'UNKNOWN', 'inventory');
}
const sessions = [];
Expand Down Expand Up @@ -90,7 +90,7 @@ export function parseGridInventory(payload) {
}
}
return Object.freeze({
ready: true,
ready: value.ready,
nodeCount: value.nodes.length,
sessions: Object.freeze(sessions),
stereotypes: Object.freeze(stereotypes),
Expand Down Expand Up @@ -362,6 +362,7 @@ async function waitForGrid(grid, fetchImpl = fetch) {
try {
const inventory = await readGridInventory(grid, fetchImpl);
if (
inventory.ready &&
inventory.nodeCount > 0 &&
inventory.stereotypes.includes('chrome')
) {
Expand Down
16 changes: 13 additions & 3 deletions scripts/grid-recovery.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ const ownedId = 'owned:session-1';
const origin = 'http://127.0.0.1:4444';
const grid = { origin, endpoint: `${origin}/wd/hub` };

function statusPayload(sessions = [], browserName = 'chrome') {
function statusPayload(sessions = [], browserName = 'chrome', ready = true) {
return {
value: {
ready: true,
ready,
nodes: [{
id: 'node-test',
slots: [{
Expand Down Expand Up @@ -84,7 +84,17 @@ test('capability classification follows the validated slot inventory and structu
test('missing or changed Grid inventory schemas fail closed', () => {
assert.throws(() => parseGridInventory({ value: { ready: true } }), /GRID_INVENTORY_SCHEMA_UNAVAILABLE/);
assert.throws(() => parseGridInventory({ value: { ready: true, nodes: [{ slots: [{}] }] } }), /GRID_INVENTORY_SCHEMA_UNAVAILABLE/);
assert.throws(() => parseGridInventory({ value: { ready: false, nodes: [] } }), /GRID_INVENTORY_SCHEMA_UNAVAILABLE/);
assert.throws(() => parseGridInventory({ value: { nodes: [] } }), /GRID_INVENTORY_SCHEMA_UNAVAILABLE/);
});

test('a full single-slot Grid can be not ready and still expose its live session', () => {
const inventory = parseGridInventory(
statusPayload([{ sessionId: ownedId }], 'chrome', false),
);

assert.equal(inventory.ready, false);
assert.equal(inventory.sessions.length, 1);
assert.equal(inventory.sessions[0].sessionId, ownedId);
});

test('private Grid URL rejects remote hosts, credentials, query strings and fragments', () => {
Expand Down
Loading