Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

A gateway updates a chosen worker, incompatible ones included, and routes around it meanwhile

Open
#838 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
5/5
Estimated time
Over a week
Newbie friendliness
35/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Active
Tech stack
node.js, typescript
Domain
api, backend, cli, devtools

Research direction

Start with src/gateway/dispatcher.test.ts and the drain tests at lines 322–331, then trace worker.drain through src/gateway/dispatcher.ts, src/gateway/worker-registry.ts, and src/gateway/routing/stages/takes-requests.ts. Read the contract and CLI entry points listed in the scope, then run pnpm test:changed. Done means the specified worker and gateway update flows, routing and drain behavior, CLI output, HTTP routes, and docs are covered and CI passes.

Written by the indexing model from the issue text.

Description

task:ready

Part of #825.

Scope

From a gateway, an operator updates one chosen worker, including one that shows incompatible. The ways in are simlock worker update <id> [--version X] [--force] [--detach], POST /v1/workers/{id}/update, and the client's updateWorker. The gateway routes no leases to an updating worker and never touches the drain set. The worker list shows each worker's version, its latest version and its update stage. The gateway updates itself with simlock update --gateway, with no lease wait.

Shared values: worker.update, the worker view fields, HTTP routes, WORKER_NOT_CONNECTED, the routing reason updating, the "newer" rule (read), events (read), the protocol version.

In short

simlock worker update 3f81a2c4 follows the update on that worker. It ends at mac-studio-2 (3f81a2c4) is running 0.4.0, or at Update failed (<stage>): <message>. Meanwhile, simlock worker list adds -- simlock 0.3.0 (0.4.0 available) -- updating to 0.4.0: waiting for 2 leases to that worker's line.

sequenceDiagram
  participant O as simlock worker update
  participant G as gateway
  participant W as worker (old)
  O->>G: worker.update {workerId}
  alt disconnected
    G-->>O: WORKER_NOT_CONNECTED
  else connected or incompatible
    G->>W: daemon.update (frozen; passes the degraded client)
    W-->>G: { started, update: waiting }
    G->>G: view.update set, routing skips it
    G-->>O: answer
    loop 30 s tick, and on daemon.update-* events
      G->>W: status.get, or daemon.update {query} when incompatible
    end
    Note over O: O polls worker.list every 2 s
    Note over G: worker restarts and dials back
    G-->>O: view version 0.4.0, update null: running 0.4.0
  end
Input Today After
simlock worker update 3f81a2c4 Unknown worker command: update follows to … is running <v>
simlock worker update nope — UNKNOWN_WORKER
simlock worker update <disconnected id> — WORKER_NOT_CONNECTED: worker <id> is not connected
simlock worker update <id> --detach — Updating mac-studio-2 (3f81a2c4) 0.3.0 → 0.4.0. Follow it with simlock worker list.
simlock worker update <id>, already latest — mac-studio-2 (3f81a2c4) is already on the latest (0.4.0)
simlock worker update on a worker daemon — UNSUPPORTED_IN_WORKER_MODE
simlock update --gateway — installs and restarts at once; a request in that gap fails as in any gateway restart

If this task goes wrong, a user would notice a worker kept out of rotation after its update, leases sent to an updating worker, or an operator's drain undone.

Technical spec

Facts
# Fact Source
F1 The gateway keeps the worker's client even when status.get answers PROTOCOL_VERSION_UNSUPPORTED, and marks the link incompatible src/gateway/worker-link.ts:165-167, 198, 210-221
F2 The degraded client lets daemon.update through (#836) #836 Tests
F3 The 30 s tick refreshes every link, incompatible ones included; WORKER_CALL_TIMEOUT_MS (10 s) bounds view reads only src/gateway/service.ts:287; src/gateway/worker-link.ts:44, 184
F4 A refresh merges into the old view; an absent key keeps its value (the new fields are never absent) src/gateway/worker-registry.ts:345-365
F5 The routing filter skips a worker not connected, then a drained one; reasons in COULD_TAKE_LATER keep a request waiting src/gateway/routing/stages/takes-requests.ts:14-15; src/gateway/fleet-coordinator.ts:117-122
F6 A worker answer other than NO_CAPACITY, PROVISION_FAILED or a cannot-serve code ends a fleet request worker-failed src/gateway/fleet-coordinator.ts:1053-1096
F7 worker.drain/remove handlers are the models; their tests are in src/gateway/dispatcher.test.ts:322-331 and src/daemon/gateway-fleet.e2e.test.ts; a worker answers worker.* writes with UNSUPPORTED_IN_WORKER_MODE src/gateway/dispatcher.ts:228-231, 375-397; src/daemon/dispatcher.ts:327-329
F8 worker.* inputs use workerId: z.string().min(1); UNKNOWN_WORKER exists, WORKER_NOT_CONNECTED doesn't src/contract/operations.ts:753; src/contract/errors.ts:120-124
F9 Waiting-reason texts are Records over the reason enum: GATEWAY_REASON_TEXT in the CLI and WORKER_WHY_TEXT in the console src/cli/commands/list.ts:62-76; ui/src/model/waiting.ts:96-108
F10 The worker list line is id (label): state -- capacity, leases[…] src/cli/format.ts:5-41
F11 The worker routes are at app.ts:661-691, declared by #821 src/http/app.ts:661-691; #821
F12 The gateway's own daemon object is built in aggregate.ts and the gateway dispatcher src/gateway/aggregate.ts:67; src/gateway/dispatcher.ts:317
F13 isNewerVersion is #837's #837
Modules touched

Searches: rg "drained" in src/gateway, rg "GatewayWaitingReason" (contract, cli list, ui waiting, pipeline), rg "formatWorkers", rg "\"worker\\.drain\"".

Files: src/contract/{operations,schemas,errors,worker-view,protocol}.ts, operations.test.ts; src/gateway/{dispatcher,worker-link,worker-registry,service,aggregate,test-support}.ts, src/gateway/routing/{pipeline.ts,stages/takes-requests.ts}, src/gateway/AGENTS.md; src/daemon/{main,dispatcher}.ts (the gateway role of daemon.update, wired through main.ts); src/simlock-client/{client,types}.ts, src/admin/index.ts; src/cli/commands/{worker,list}.ts, src/cli/format.ts; ui/src/model/waiting.ts (the reason text only); src/http/app.ts; docs CLI.md, HTTP-API.md, CLIENT.md.

Contract and event changes

This PR bumps the protocol by one.

  • worker.update: admin, write. Input { workerId: z.string().min(1), version?, force? } with #836's bounds. The answer is #836's answer plus workerId. On the uplink it's bounded at 60 s. At the limit, the caller gets WORKER_NOT_CONNECTED with worker <id> did not answer within 60 s; check simlock worker list, and the view catches up on the next read.
  • For an incompatible worker, daemon.update {query} runs on the 30 s tick and right after worker.update. A failed query keeps the last view.
  • Worker view: update, lastUpdateFailure, latestVersion, each null when there's nothing to show.
  • Waiting reason updating. Its text is Updating in both Records. It's in COULD_TAKE_LATER, since the worker comes back by itself.
  • WORKER_NOT_CONNECTED (domain, 409, exit 2): worker <id> is not connected.
  • A status re-read also runs on daemon.update-started and daemon.update-failed (beside WORKER_VIEW_CATALOG_EVENTS) and right after worker.update answers.
  • daemon.update on a gateway: no lease wait, straight to installing; fleet admission is not changed.
Other code on the same state

Searches: rg "setDrained" (registry, dispatcher), rg "takesRequests" (routing 3).

  • The drain set and update are separate. An update never calls setDrained, and a drain or undrain during an update changes only the drain set.
  • A lease-event refresh re-reads status.get, and its update: null clears the view.
Failure modes
Call Outcome What happens Limit, counted from
daemon.update over the uplink ok view updated from the answer 60 s
same error reply passed to the caller (UPDATE_*) —
same uplink closed / timeout WORKER_NOT_CONNECTED / the timeout error 60 s
simlock worker update follow view version = to, update null, connected or incompatible … is running <to>, exit 0 none; Ctrl-C stops following
same lastUpdateFailure.to = to Update failed (<stage>): <message>, exit 1 —
gateway's own install fails lastUpdateFailure in its status.get, it keeps serving npm's 10 min
gateway's new version doesn't start #835 rolls back; the gateway's start reads <root>/gateway/update-result.json (#836's readResultFile, wired in startGatewayDaemon) and reports it 2 min (#835)

A gateway restart mid-update rebuilds each view from the worker, which still reports update.

Rules in play
  • ADR 0042 §2: "worker.update … sends daemon.update over that worker's uplink".
  • ADR 0042 §3 and ADR 0005 §9: the update never touches the drain set, and "only undrain ends it".
  • ADR 0005 §31: an incompatible worker "is never dispatched to". Only the frozen call goes out.
Tests

Seams: src/gateway/dispatcher.test.ts and src/daemon/gateway-fleet.e2e.test.ts with ScriptedWorkerClient (src/gateway/test-support.ts:271, given a pass-through for daemon.update when degraded), modelled on the drain tests. Routes go through src/http/app.test.ts, the gateway's own update through src/daemon/main.test.ts, and the CLI through src/cli/index.test.ts.

  • Operator calls worker.update on the gateway: connected (worker gets daemon.update, view update set); incompatible (frozen call sent, view filled from query); disconnected (WORKER_NOT_CONNECTED); unknown id (UNKNOWN_WORKER); POST /v1/workers/{id}/update (202, 200, 403 for an agent token).
  • Gateway routes a lease while a worker is updating: an updating worker is skipped (updating, the request waits); the update ends (update: null clears the view, the worker is picked again); an operator-drained worker stays drained after its update.
  • Operator updates the gateway itself: no wait with fleet leases held; a failed install shows in its status.get; a rolled-back gateway reports the failure at start.
  • Operator runs simlock worker update and simlock worker list: follows to … is running <v>; failure (exit 1); already latest; --detach; on a worker daemon (UNSUPPORTED_IN_WORKER_MODE); the list line shows simlock <v> (<latest> available) only when isNewerVersion, and updating to <v>: <stage>.

Done when

  • pnpm test:changed passes (it runs the unit tests under src/gateway src/daemon src/cli src/http src/contract, src/daemon/gateway-fleet.e2e.test.ts included).
  • CI's check job passes.
  • docs/CLI.md documents simlock worker update and the new list fields; docs/HTTP-API.md documents the routes.

Out of scope

  • Updating every worker in one call. Queuing an update for a disconnected worker. The console (#839).

Depends on

  • #837
  • #821

Written by an agent.

Dominant language
TypeScript
Stars
20
Forks
2
Avg merge
8h
Merged PRs (30d)
316

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from callstackincubator/simlock

All issues in callstackincubator/simlock

Similar issues

More TypeScript issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.