Hacktoberfest 2026:维护者为十月标记出来的 issue,仍然开放、适合新手。 浏览 Hacktoberfest issue

A waiting request says why it is not placed, on a worker

未关闭
#636 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
5/5
预计耗时
一周以上
新手友好度
12/100
Issue 类型
功能
描述清晰度
基本清楚
活跃度
活跃
技术栈
typescript
领域
api, backend, cli, frontend

调研方向

Start in src/leasing/acquisition-planner.ts at the blocked return (around lines 222-224), which currently drops the capacity reason, then follow the coordinator's #defer path in src/leasing/lease-acquisition-coordinator.ts. The change touches the contract schema in src/contract/schemas.ts (which needs a protocol bump), the worker dispatcher, and the CLI and console, and the spec is part of feature #623, so confirm scope with maintainers first. Done means simlock list --requests shows a reason on queued requests only, and e2e/waiting-requests.test.ts passes with the new lines.

由索引模型根据 Issue 内容生成。

描述

task:ready

Part of #623.

Scope

On a single host (a worker), a request waiting in the queue says why it is not placed yet: in simlock list --requests, GET /v1/lease-requests, status.get waiting, and a "Why" row in the console's Waiting inspector. A worker's NO_CAPACITY answer names the capacity check that refused it, so #637 can show it on a gateway.

In short

Each time the daemon tries to place a waiting request, it keeps the reason the attempt stopped, in memory. A queued request shows the reason from its latest attempt. A starting request shows none: its stage already says the daemon is working on it. The reasons and their texts are feature #623 Shared values, rows "Waiting reason (worker)", "Reason text" and "NO_CAPACITY detail".

flowchart LR
  A[Request arrives or queue wakes] --> P{Plan}
  P -- grant / boot / create / evict --> S[starting]
  P -- blocked --> R["Keep the reason on the waiter (new)"]
  R --> Q[queued]
  Q --> L["list.get requests: reason on the entry (new)"]
  P -- blocked, noWait --> N["NO_CAPACITY with details.capacityReason (new)"]

Words used:

  • Reason: the first check that stopped the request at its latest placement attempt, for example platform-running-limit, printed "Full: running limit for iOS".
  • Capacity reason: one of the four capacity refusals (device-limit, ram-budget, global-running-limit, platform-running-limit).

Before and after:

$ simlock list --requests
Request req_7: agent-b, ios iPhone 16, queued at 1, waiting 12s
Request req_9: agent-c, ios iPhone 16, queued at 2, waiting 3s
Request req_11: agent-d, android Pixel 8, starting, waiting 1s

$ simlock list --requests          # after
Request req_7: agent-b, ios iPhone 16, queued at 1, waiting 12s — full: running limit for iOS
Request req_9: agent-c, ios iPhone 16, queued at 2, waiting 3s — behind an earlier request
Request req_11: agent-d, android Pixel 8, starting, waiting 1s
Situation Today After
iOS running limit reached, nothing idle to shut down queued at 1, waiting 12s ... — full: running limit for iOS
Another request is ahead in the simulator/emulator line queued at 2, ... ... — behind an earlier request
Creating a device failed twice queued at 1, ... ... — creating a device failed twice
The warm pool is booting a device that fits queued at 1, ... ... — a matching device is starting
Physical: every match leased or absent physical ..., queued at 1, ... ... — every matching device is leased or absent
Being placed starting, waiting 1s as today
--no-wait at the RAM budget NO_CAPACITY, no details NO_CAPACITY, details.capacityReason: "ram-budget"

If this goes wrong, a user would see a reason that is stale or wrong, or a reason on a request that is starting.

Technical spec

Facts
# Fact Source
F1 Only the head of the virtual line plans. Any other virtual waiter gets wait, or no-capacity under noWait, without asking the planner. src/leasing/lease-acquisition-coordinator.ts:891-907
F2 A queue wake drives the head of the virtual line and every pending physical waiter, nobody else. src/leasing/lease-acquisition-coordinator.ts:1171-1181
F3 The planner waits, outside noWait, when the warm pool is booting or creating a fitting device for nobody. src/leasing/acquisition-planner.ts:107-116, :139-143
F4 A refused boot of a shut-down device with no idle device to evict is blocked; so is a boot whose claim is taken. src/leasing/acquisition-planner.ts:145-164
F5 A refused new device is blocked when there is no victim; at device-limit it first tries a managed victim; two failures block a provision that capacity allows. src/leasing/acquisition-planner.ts:166-187
F6 A taken eviction claim is blocked. The two eviction plans carry only a claim and a device, no reason. src/leasing/acquisition-planner.ts:67-76, :189-197
F7 blocked returns wait or no-capacity and drops the capacity reason it was given. src/leasing/acquisition-planner.ts:222-224
F8 A physical plan with no ready, present, free fit is blocked; a fit held for an older queued physical request is skipped. src/leasing/acquisition-planner.ts:128-137, :132; src/leasing/lease-acquisition-coordinator.ts:729-738
F9 The capacity refusal reasons are device-limit, ram-budget, global-running-limit, platform-running-limit. src/core/capacity/strategy.ts:30-34
F10 A blocked plan reaches #defer, which rejects a noWait waiter with NoCapacityError and enqueues any other. src/leasing/lease-acquisition-coordinator.ts:882-886, :1071-1078, :1239-1246
F11 After a first provisioning failure the waiter plans again at once; after a second it is enqueued without a plan, or a probe is declined with NoCapacityError. src/leasing/lease-acquisition-coordinator.ts:842-857
F12 A failed eviction (shutdown or delete threw) defers the waiter with no plan. src/leasing/lease-acquisition-coordinator.ts:948-960, :978-984
F13 A physical waiter with no plan defers; a failed physical grant defers too. src/leasing/lease-acquisition-coordinator.ts:718-721, :774-776
F14 A queued waiter whose spec matches a reclaiming device gets a reclaiming progress push; its plan is untouched. src/leasing/lease-acquisition-coordinator.ts:1080-1101
F15 NoCapacityError carries no fields. src/leasing/lease-acquisition-coordinator.ts:82-87
F16 Waiter state lives in memory only; a daemon start fails every request the previous process left open. src/leasing/AGENTS.md "State and timers", "Rules"
F17 waitingRequests() is the request book's waiting(queue.places()); places() gives a queuePosition only to a queued waiter, and the book maps that to stage queued, else starting. src/leasing/create-leasing.ts:280, src/leasing/wait-queue.ts:169-175, src/leasing/lease-request-book.ts:67-90, :308-313
F18 The gateway builds its own entries from the same book and its own WaitQueue. src/gateway/fleet-coordinator.ts:208-210
F19 waitingRequestSchema has no reason. It types list.get requests, status.get waiting, and a worker view's waiting. src/contract/schemas.ts:311-319, :1158; src/contract/operations.ts:123, :584
F20 The daemon parses each handler result through the output schema; an undeclared field is stripped. src/contract/AGENTS.md "Rules"
F21 The worker answers list.get requests and status.get waiting from queue.waitingRequests(). src/daemon/dispatcher.ts:380, :754
F22 GET /v1/lease-requests returns list.get requests as is; the console reads that route. src/http/app.ts:308-315, ui/src/state/waiting.ts:29
F23 NO_CAPACITY declares no details, and is not in CODES_WITH_DECLARED_DETAILS. src/contract/errors.ts:37, :148-175
F24 The socket sends details only from a DispatchError. HTTP puts them in the body only for a DispatchError whose code is in CODES_WITH_DECLARED_DETAILS. src/daemon/server.ts:843-853, src/http/errors.ts:154-170
F25 The worker dispatcher already turns a leasing error into a DispatchError with details: LEASE_ID_TAKEN with leaseId. src/daemon/dispatcher.ts:493-495
F26 POST /v1/lease-requests with noWait answers a refusal through errorResponse. src/http/app.ts:294-300
F27 fromWireError passes details through unchecked. src/contract/errors.ts:453-466
F28 The CLI parses the list with waitingRequestSchema and prints one line per request ending waiting <n>s. src/cli/index.ts:975-976, :987-1010
F29 The console's Waiting inspector shows cards for Waiting, Place, Stage, then facts Request, Created, Queue. A stage it does not know shows as its raw word. ui/src/screens/waiting/index.tsx:108-142, ui/src/model/waiting.ts:104-124, ui/src/model/tones.ts:44, ui/src/model/waiting.test.ts:92
F30 The console's model/ imports contract types only. ui/AGENTS.md:5, :20
F31 Every contract shape change bumps the protocol by one in the same PR; the range is { min: 39, max: 39 }. docs/internal/agent-rules/toolchain.md:153-156, src/contract/AGENTS.md:58-60, src/contract/protocol.ts:13
F32 A replayed failed request answers its stored code only. src/daemon/error-code.ts:90-94
F34 Cancelling a queued waiter rejects it and wakes nobody; so does enqueuing after a second provisioning failure. A queue timeout kicks the queue. src/leasing/lease-acquisition-coordinator.ts:609-619, :842-857; src/leasing/create-leasing.ts:122-135
F35 The gateway lists a worker's own entries spread from its view, so every field the schema declares is kept. src/gateway/dispatcher.ts:387-397
F36 The e2e test anchors each line at waiting \d+s$: single host at :46, :53; gateway at :91, :96, :98. Its limits are one iOS device, one running. e2e/waiting-requests.test.ts:13, :37-101
F37 Core's index re-exports capacity helpers but not CapacityRefusalReason, which only src/core/capacity/index.ts exports. src/core/index.ts:34-38, src/core/capacity/index.ts:21
F33 Docs that describe the line and the route today. docs/CLI.md:1562-1580, docs/HTTP-API.md:1529-1561, docs/CONSOLE.md:289-313
What changes
  1. The plan says why it is blocked. blocked (F7) takes a reason and returns it on wait and no-capacity. Each path gives:

    Path Reason
    Not the head of the virtual line (F1) behind-earlier-request
    Warm pool booting a fit (F3) waiting-for-device
    Boot refused, no victim (F4) the refusal's capacity reason
    New device refused, no victim (F5) the refusal's capacity reason
    Victim found, its claim taken (F5, F6) the capacity reason that asked for the eviction
    Physical, only fits held for an older request (F8) behind-earlier-request
    Boot claim already taken (F4) waiting-for-device
    Two provisioning failures (F5) provisioning-failed
    Physical, no fit free (F8) no-free-physical

    The evict-running and evict-managed plans also carry the capacity reason that asked for them (F6). The capacity reasons map one to one (F9). The mapping is a Record keyed by core's CapacityRefusalReason, so the compiler fails when the two lists differ.

  2. The waiter keeps it. Every enqueue sets the waiter's reason from the plan that led to it. Each new plan replaces it. It is never written to state.json (F16).

    • Enqueue after a second provisioning failure (F11): provisioning-failed.
    • Failed eviction (F12): the capacity reason that asked for the eviction.
    • Failed physical grant (F13): no-free-physical.
    • A request whose match is being cleaned (F14) keeps the capacity reason its plan gave; the reclaiming push stays as today.
    • The head leaving without a wake (F34): cancelling a queued waiter, and enqueuing after a second provisioning failure, now wake the queue, so the new head plans again and replaces its behind-earlier-request.
  3. The listing shows it. waitingRequests() adds reason to each queued entry that has one, never to a starting one (F17). The gateway's own entries get none (F18; #637 adds workers). A gateway's list keeps the reason on a worker's own entries (F35); the CLI prints them with the same suffix.

  4. NO_CAPACITY names the capacity reason. NoCapacityError carries the plan's reason. When it is a capacity reason, the worker dispatcher throws DispatchError("NO_CAPACITY", message, { capacityReason }), as it does for LEASE_ID_TAKEN (F25). Otherwise no details, as today. So the socket sends it (F24), and HTTP's 503 body carries capacityReason (F24, F26). A replay answers no details, as today (F32).

  5. CLI. A queued line with a reason ends — plus the reason text with only its first letter lower-cased. The text comes from feature #623 Shared values "Reason text"; the platform word is the request's (iOS, Android).

  6. Console. The inspector gains a "Why" fact after Queue, with the reason text, for an entry with a reason. No row otherwise. A reason the console does not know shows as its raw word, as a stage does (F29).

  7. Two text tables, each checked. The CLI keeps its table in src/cli, the console in ui/src/model (F30). Each side has one test that its table covers every reason the contract's enum lists.

Modules touched

Searches: waitingRequestSchema, WaitingRequest, places(, blocked(, NoCapacityError, "NO_CAPACITY", CapacityRefusalReason, formatWaitingRequests, WaitingRow.

  • src/contract/schemas.ts, src/contract/errors.ts, src/contract/protocol.ts
  • src/core/index.ts (exports CapacityRefusalReason)
  • src/leasing/acquisition-planner.ts, src/leasing/lease-acquisition-coordinator.ts, src/leasing/lease-request-book.ts, src/leasing/wait-queue.ts, src/leasing/create-leasing.ts
  • src/daemon/dispatcher.ts
  • src/cli/index.ts
  • ui/src/model/waiting.ts, ui/src/screens/waiting/index.tsx
  • e2e/waiting-requests.test.ts: its five line patterns (F36) gain the reason suffix, and the single-host test pins the limits below
  • Docs: docs/CLI.md, docs/HTTP-API.md, docs/CONSOLE.md, docs/CLIENT.md; manuals src/leasing/AGENTS.md, src/contract/AGENTS.md
flowchart LR
  cli["CLI (changed)"] --> d["worker dispatcher (changed)"]
  http[HTTP] --> d
  ui["console Waiting (changed)"] --> http
  d --> lease["leasing: coordinator, book (changed)"]
  lease --> plan["planner (changed)"]
  plan --> cap[capacity]
  d --> contract["contract (changed)"]
sequenceDiagram
  participant G as Gateway or client
  participant D as Worker dispatcher
  participant C as Coordinator
  participant P as Planner
  G->>D: lease.request noWait
  D->>C: request
  C->>P: plan
  P-->>C: no-capacity, ram-budget
  C-->>D: NoCapacityError(ram-budget)
  D-->>G: NO_CAPACITY, details.capacityReason ram-budget
  G->>D: list.get requests
  D-->>G: [{ stage: queued, reason: platform-running-limit, ... }]
Contract and event changes
  • waitingRequestSchema gains reason?: enum of the eight values in feature #623 Shared values "Waiting reason (worker)". A value outside the enum fails the parse, and so does reason on a starting entry.
  • ErrorDetailsMap.NO_CAPACITY becomes { capacityReason?: enum of the four }; NO_CAPACITY joins CODES_WITH_DECLARED_DETAILS.
  • This PR bumps the protocol by one, min with it (F31; feature #623 Shared values "NO_CAPACITY detail").
  • Events: none.
Other code on the same state

Searches: readers of queue.pending(), queue.list(), .places() outside tests (4 hits in src/leasing, 5 in src/gateway); writers of waiter fields in lease-acquisition-coordinator.ts.

  • Warm pool waitingDemand (lease-acquisition-coordinator.ts:621-635) and #heldByOlder (:729-738): read spec and state, not the reason. No conflict.
  • cancelPending (:609-619): rejects a queued waiter; it leaves the list (F17), and now wakes the queue (F34). A wake only plans the head and physical waiters under the lock, as every wake does.
  • The gateway (F18): shares the book and queue code. Its own entries carry no reason; a worker's entries keep theirs (F35).
  • #637: adds workers to the same schema and reads details.capacityReason. #637 depends on this task.
  • #629, #631, #635 each bump the protocol. The PR that merges second bumps again.
  • #633: adds the wait p95 to the Waiting page. Other lines of the same screen; whichever merges second takes the base branch.
Failure modes
Call Outcome What happens Limit, counted from
list.get requests ok Each queued entry with its latest reason as today
list.get requests client on another protocol PROTOCOL_VERSION_UNSUPPORTED at hello, as today; existing hello tests cover it as today
lease.request noWait capacity refusal NO_CAPACITY with details.capacityReason none
lease.request noWait other refusal (F1, F11, F8) NO_CAPACITY, no details none
lease.request noWait replayed by idempotency key stored code, no details (F32) none
Console GET /v1/lease-requests unknown reason word raw word in the Why row none
  • Restart mid-step: the reason is in memory; the restart fails the request (F16). Nothing left behind; no test needed.
  • Two callers at once: a listing reads the waiter's reason as the last plan left it. One thread; no torn read; no test needed.
Rules in play
  • architecture.md 10: "A rule is enforced in exactly one place." The reason comes from the plan; each text table is checked against the contract's enum by a test; the capacity list is checked by the compiler.
  • architecture.md 16: "Policy is a pure function". The planner returns the reason; it stores nothing.
  • architecture.md 8: "The daemon owns all state; the CLI is a thin client." The CLI only prints the field.
  • safety.md 10: "Anything that arrives over the wire is a claim". reason and capacityReason are closed enums in the contract.
  • src/contract/AGENTS.md: "Every change to a contract shape bumps it by one in the same PR."
  • testing.md 6, 7: through createLeasing, and the planner's own test for its many cases.
  • documentation.md 3: no repo path in what the CLI prints.
Tests

Seam: createLeasing through createTestEngine (src/leasing/testing.ts) and its waitingRequests(), which list.get requests returns (F21); model on src/leasing/create-leasing.test.ts:3757. The planner's cases in src/leasing/acquisition-planner.test.ts.

  • Planner: each blocked path returns its reason: not tried by the planner; warm-pool boot waiting-for-device; boot refused ram-budget, global-running-limit, platform-running-limit; provision refused device-limit, ram-budget, global-running-limit, platform-running-limit; victim claim taken at device-limit and at platform-running-limit, with the eviction plans carrying it; physical fits held only for an older request behind-earlier-request; boot claim taken waiting-for-device; two failures provisioning-failed; physical no-free-physical. Extend "waits or rejects when policy cannot reserve capacity" (acquisition-planner.test.ts:211).
  • The same reasons come back on no-capacity under noWait.
  • A queued request at the iOS running limit lists reason: "platform-running-limit"; a second one behind it lists behind-earlier-request; the stored request record has no reason.
  • The reason follows the latest attempt: when the head is granted, the request behind it becomes head and lists its own new reason, not behind-earlier-request.
  • A request re-queued after its second provisioning failure lists provisioning-failed, and the request behind it, now head, plans again and lists its own reason.
  • Cancelling the queued head makes the request behind it plan again and list its own reason, not behind-earlier-request.
  • A request while the warm pool boots a fitting device lists waiting-for-device.
  • A failed eviction lists the capacity reason that asked for it; a failed physical grant lists no-free-physical; a request whose only match is reclaiming lists the planner's capacity reason.
  • A waiting physical request with every fit leased lists no-free-physical (physical-leasing.test.ts:582).
  • A starting request lists no reason: one being provisioned, and one held at its first plan.
  • A gateway's own queued entry lists no reason: extend src/gateway/fleet-coordinator.test.ts:4309. A gateway's list.get requests keeps a worker's own entry's reason: extend src/gateway/dispatcher.test.ts:1436.
  • Dispatcher: a noWait lease.request refused at the RAM budget, at the device limit and at the platform running limit rejects NO_CAPACITY with details.capacityReason of each; one behind an earlier request rejects NO_CAPACITY with no details; a replay of the RAM-budget request by its idempotency key rejects NO_CAPACITY with no details. Model on src/daemon/dispatcher.test.ts:473.
  • HTTP: POST /v1/lease-requests with noWait at the running limit answers 503 with capacityReason in the error body (src/http/app.test.ts).
  • Contract: waitingRequestSchema accepts each of the eight reasons on a queued entry, and refuses "bogus" and any reason on a starting entry; NO_CAPACITY is in CODES_WITH_DECLARED_DETAILS (src/contract/schemas.test.ts, errors.test.ts).
  • CLI: a queued request prints — <text> for every reason, platform-running-limit once for iOS and once for Android, only the first letter lower-cased; a queued request with no reason and a starting one print as today. Extend src/cli/index.test.ts:6284.
  • CLI and console tables: each side's table has a text for every reason the contract's enum lists (one test in src/cli, one in ui/src/model).
  • Console model: the chosen row's Why is the text for every reason, the raw word for an unknown one, and absent with no reason (ui/src/model/waiting.test.ts).
  • Console screen: the inspector shows a Why fact after Queue with the text, and no Why fact for a starting request (ui/src/screens/waiting/waiting.test.tsx:76).
  • E2E, single host (e2e/waiting-requests.test.ts:37-55), limits { ios: { maxDevices: 2, maxRunning: 1 }, maxRunning: 2 }, one lease held: agent-b's line ends — full: running limit for iOS, agent-c's — behind an earlier request.
  • E2E, gateway (:57-101): the worker's local-b line ends — full: device limit, on the worker and on the gateway; the fleet line is unchanged.

Done when

  • The tests above pass.
  • On the fake driver, with ios.maxDevices 2, ios.maxRunning 1, global maxRunning 2 and one iOS lease held, a second simlock lease waits; simlock list --requests prints its line ending — full: running limit for iOS (e2e/waiting-requests.test.ts:37-55), and the console's Waiting inspector shows the same text under Why.
  • With the same limits, one iOS lease held and no request waiting, lease.request with noWait: true over the socket is the head, and answers NO_CAPACITY with details.capacityReason: "platform-running-limit".
  • docs/CLI.md, docs/HTTP-API.md, docs/CONSOLE.md and docs/CLIENT.md describe the reason and the detail.

Out of scope

  • A gateway's own entries and per-worker reasons (#637).
  • A reason on a starting request.
  • Keeping reasons across a restart, or in events.
  • Waiting only behind same-platform requests (#387).
  • The gateway's own NO_CAPACITY carrying a capacity reason.

Depends on

  • #635

Written by an agent.

主要语言
TypeScript
星标
19
派生
1
平均合并
7 小时 49 分钟
30 天内合并 PR
228

环境准备

这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

callstackincubator/simlock 的其他 Issue

查看 callstackincubator/simlock 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。