Skip to content

The warm pool is its own module: targets, a reserve, and an off switch #359

Description

@V3RON

Request: none

Related: #350 (closed by this feature), #76 (its warm-target half is replaced by this feature; the host activation handshake stays there).

Problem

A lease is fast only when a clean device of the right kind is already running. Today that happens by accident: a device is warm only because someone leased it and released it, and capacity happened to allow it to keep running. The first lease of the day, and the first lease of each new model, always waits for a full boot. On a fresh identity machine every lease waits for one.

The operator has no say in it. There is no way to ask Simlock to keep two iPhone 17 ready before the agents arrive, no way to keep a slot free for a model nobody predicted, and no way to turn warmth off on a machine that needs its RAM back after every lease.

The rule that decides whether a released device stays warm is also wrong in one case. At the running cap it keeps the device only when it is exactly the model the oldest waiting request would create. A released iPhone 15 that fits a waiting --class phone request is shut down and an iPhone 17 booted for it (#350).

Who it is for

The operator of a worker, on a CI runner or a shared developer Mac, who knows which devices the agents on it ask for and wants the first lease to be as fast as the tenth. Also the agent whose request is waiting while a device that would do is being shut down.

Outcome

  • The operator can name targets: a kind of device and a count that Simlock keeps booted, clean and unleased, ahead of demand.
  • The operator can keep a number of running slots free of warm devices, so a request for an unpredicted model boots at once instead of shutting a warm device down first.
  • The operator can turn warmth off, so no idle device ever runs.
  • A released device that fits a waiting request stays warm and is granted to it.
  • A request for a device the pool is already booting waits for that boot instead of starting another.
  • simlock status shows each target, how many are ready and booting, and why it is short. simlock doctor reports a target that can never be met as configured.

How it works for the user

flowchart LR
  A[daemon starts] --> B[pool pass: boot targets, one at a time]
  B --> C{lease request}
  C -->|a warm device fits| D[granted in under a second]
  C -->|one is booting for a target| E[waits, granted when the boot ends]
  C -->|nothing fits| F[boots or creates its own, as today]
  D --> G[pool below target: refill in background]
  H[lease released] --> I[device purged]
  I -->|fits a waiting request, or room under the reserve| J[kept warm]
  I -->|no room, or pool off| K[shut down]
Loading

Before, on a machine that has just started:

$ simlock lease --model "iPhone 17" --os 26.0
{"state":"booting","estimatedReadyMs":30000}
... 30 s ...
{"leaseId":"lse_1","device":{"model":"iPhone 17","mode":"full"}}

After, with a target of two iPhone 17:

$ simlock config set warmPool.targets '[{"platform":"ios","model":"iPhone 17","osVersion":"26.0","count":2}]'
$ simlock daemon restart
$ simlock lease --model "iPhone 17" --os 26.0
{"leaseId":"lse_1","device":{"model":"iPhone 17","mode":"full"}}
$ simlock status
warm pool: enabled, reserve ios 0 android 0
  iPhone 17 / 26.0 / full   wanted 2  ready 1  booting 1

A target the machine cannot fill:

$ simlock status
warm pool: enabled, reserve ios 1 android 0
  iPhone 17 / 26.0 / full   wanted 4  ready 2  booting 0  short: reserve
$ simlock doctor
warm-pool-target-unreachable  iPhone 17 / 26.0 / full wants 4; the iOS running limit is 3 and the reserve is 1

A target for a runtime that is not installed:

$ simlock doctor
warm-pool-target-unreachable  iPhone 17 / 27.0 / full: iOS 27.0 is not installed; run simlock component install ios 27.0

Turning warmth off:

$ simlock config set warmPool.enabled false
$ simlock daemon restart
$ simlock release lse_1
$ simlock status
warm pool: off
devices: sim_a  iPhone 17  shutdown

Examples

Config keys. Today every key under warmPool except quarantine.* is unknown: a warning at start, then ignored.

Input Today After
warmPool.enabled: false ignored with a warning no idle device runs
warmPool.reserveRunning: { "ios": 1 } ignored one iOS running slot never holds a warm device
warmPool.reserveRunning: { "ios": 3 } on a limit of 3 ignored accepted; no iOS device is ever warm, status says so
warmPool.targets: [{ "platform": "ios", "model": "iPhone 17", "count": 2 }] ignored two iPhone 17 on the newest installed runtime, in the default mode
target with "mode": "slim" ignored two slim devices; a full request never gets one
target with "osVersion": "27.0", not installed ignored accepted; nothing booted; doctor finding; never a download
target with "count": 0 ignored rejected at load, config error
target naming a model the catalog does not list ignored accepted; doctor finding
target with "class": "phone" ignored rejected at load, unknown key
warmPool.maxConcurrentBoots: 2 ignored two pool boots or creations may run at once; default 1

What happens to a released device, at the running cap, with one request waiting.

Released device Waiting request Today After
iPhone 17 / 26.0 --model "iPhone 17" --os 26.0 kept, granted kept, granted
iPhone 15 / 26.0 --class phone shut down, iPhone 17 booted (#350) kept, granted
iPhone 15 / 26.0, slim --class phone --mode full shut down shut down; a full device is booted
Pixel 8 --model "iPhone 17" shut down shut down; the iPhone boots into the freed slot
iPhone 17 / 26.0 none; pool off kept shut down
Pixel 8 none; pool off kept running after the snapshot restore restored from the snapshot, then shut down

What a target does when the pool is short.

Situation After
A shut-down device of the target's kind exists it is booted
None exists and the limits allow one more one is created and booted
The running limit minus the reserve is full of leased or warm devices nothing; status says short, nothing is evicted
The RAM budget has no room for a full-size boot nothing; status says short
A request is waiting for capacity the request goes first; the target waits
Identity is fresh a never-leased device is created; it serves one lease and is deleted; the next is created

What could go wrong

  • A target holds RAM and disk on a quiet machine for as long as it is configured. The operator sees it in status and can lower the count.
  • A target whose device keeps failing to boot is retried with a growing delay, up to ten minutes between tries, and shows as short with the reason while it waits.
  • A target that does not fit the limits looks like a bug to someone who does not read status. Doctor names the limit that stops it.
  • A restart with several targets boots them one after another, so the pool takes a couple of minutes to fill. Requests in that window are served as today.
  • A request for a device the pool is booting waits up to a boot's length. It would have booted its own for the same time.
  • With the pool off, every lease pays a boot, including on Android. That is what off means.

Words used

  • warm device: a device that is running, clean, and not leased. A request it fits is granted in under a second.
  • target: a standing order to keep a number of warm devices of one kind, e.g. two iPhone 17 on iOS 26.0 in the default mode. A floor, not a cap, and never filled by evicting anything.
  • kept device: a released device left running because a request it fits is waiting or there is room under the reserve. Kept devices follow the idle timers; targeted devices do not.
  • reserve: running slots per platform that warm devices may not take, so a request for an unpredicted model boots at once. Default none.
  • mode: slim or full. A slim device has simulator daemons disabled. A full request never gets a slim device and a slim request never gets a full one.
  • pool pass: one run of the warm pool over the machine, after a start, a grant, a release or a cleanup. It shuts down what is over, boots what is under, one boot at a time.
  • short: a target with fewer ready devices than wanted at the end of a pass that could do nothing for it, with one reason, the first that applies: pool disabled, no driver for the platform, runtime not installed, unknown model, request could not be resolved, last boot failed (waiting to retry), device limit, running limit, reserve, RAM budget. A target that is still filling, or waiting behind a request, is not short.
  • purge: cleaning a released device. iOS erases it, which leaves it shut down. Android restores its golden snapshot, which leaves it running. Purge is not warmth: with the pool off, Android is restored and then shut down.
  • fresh identity: lease.identity: fresh. Every lease gets a device that never served another and is deleted after. A target then pre-creates never-leased devices.

Non-goals

  • Targets on a gateway, or any fleet-wide warmth. A gateway keeps routing to the worker that has a warm device.
  • Changing targets while the daemon runs. Config is read at start.
  • A target naming a class instead of a model.
  • Evicting a leased or kept device to make room for a target.
  • Downloading a runtime a target names.
  • The host activation handshake and the removing/removed identity status of Host activation handshake and removal status for warm identities #76.
  • Any change to the capacity limits, the RAM budget, or the idle timers for devices that are not targeted.

Completion conditions

  • With warmPool.targets naming two iPhone 17 and no leases, simlock status shows the target with ready 2 within two boots of daemon start, and the first simlock lease for that model is granted without a booting progress line.
  • With a target of two and both granted, status shows ready 0 booting 1, and after the boot ready 1.
  • With reserveRunning.ios: 1 on a running limit of 3 and two warm iPhones, a request for an iPhone 15 is granted after one boot and no warm device is shut down for it.
  • At the running cap with a --class phone request waiting, releasing an iPhone 15 that fits grants it that device; no other device boots. This is the A released device that fits a waiting class request is shut down and another booted #350 case and closes it.
  • A request for a model the pool is booting for a target is granted when that boot ends, and device.provisioned does not fire for it.
  • A targeted warm device idle for longer than idle.shutdownAfterMs is still running; a kept device of another model is shut down at that time.
  • With lease.identity.ios: fresh and a target of one, status shows ready 1 before any lease, a lease is granted without booting, the released device is deleted, and status returns to ready 1 after one boot.
  • With warmPool.enabled: false, a released iOS device is shutdown after its purge, a released Android device is shutdown after its snapshot restore, and the next lease of each boots.
  • A target whose count exceeds the running limit minus the reserve shows short with the limit named in status, and simlock doctor reports it; nothing is shut down or evicted for it.
  • A target naming a runtime that is not installed shows short with the runtime named, doctor reports it with the install command, and no download starts under any downloads.policy.
  • With maxConcurrentBoots at its default and three targets of one each, simlock events shows each device.provisioned after the previous device's device.ready.
  • A count of 0, or a target with a class key, fails config load with an error naming the key.
  • On a Mac with an iOS runtime and an Android system image: the slow lane (e2e/slow-warm-pool.test.ts) proves a target boots a real simulator and a real emulator ahead of a lease, and that off shuts both down after a release.

Open questions

none

Decisions

  • ADR 0017 — The warm pool is a module beside the lease transaction, not a step in it (docs(adr): 0017 — the warm pool is a module beside the lease transaction, not a step in it #361, Accepted — not yet implemented): where warmth lives and how it relates to the lease path. It fixes the two components (reclaim and warm pool), the module's surface and pure policy, the dependency direction, the derived budget with the reserve, what a pass does, the boot-claim wait, and the status, doctor and event surface.

Tasks

flowchart LR
  T373[#373 reclaim coordinator] --> T368[#368 warm pool module]
  T368 --> T369[#369 reserve and wait]
  T369 --> T370[#370 targets]
  T369 --> T371[#371 status, doctor, event]
  T370 --> T371
  T371 --> T372[#372 slow lane]
Loading

Written by an agent.

Activity

  1. V3RON commented on Oct 5, 2026

    @V3RON
    ContributorAuthor

    Spec updated: Decisions section added, naming ADR 0017 (#361, Proposed). The feature stays feature:spec until that ADR is accepted.

    Written by an agent.

  2. added
    feature:readyNo sub-issues; one PR delivers the whole feature.
    and removed
    feature:specBusiness or technical spec in progress.
    on Oct 5, 2026
  3. added
    feature:plannedSplit into tasks. Never picked up itself.
    and removed
    feature:readyNo sub-issues; one PR delivers the whole feature.
    on Oct 5, 2026
  4. V3RON commented on Oct 5, 2026

    @V3RON
    ContributorAuthor

    Spec updated: split into six tasks (#373, #368, #369, #370, #371, #372), Tasks section added, Decisions names ADR 0017 as accepted, the example and the hardware line corrected after the spec check.

    Written by an agent.

  5. V3RON commented on Oct 5, 2026

    @V3RON
    ContributorAuthor

    Delivery run

    Merged: none
    Parked: #373 — PR #375 has one blocking finding (stale docs) still open after review round 3
    Waiting: #368 — on #373; #369, #370, #371, #372 — on #368 and later tasks

    Written by an agent.

  6. V3RON commented on Oct 5, 2026

    @V3RON
    ContributorAuthor

    Delivery run

    Merged: #375 (#373)
    Parked: #368 — the spec must say which waiting requests the pool boots for (queue head only, or every waiter with idle-shutdown skipping the booted device); round 2 also left one behaviour defect and stale docs on PR #386
    Waiting: #369 — on #368; #370, #371, #372 — on #369 and later tasks

    Written by an agent.

  7. V3RON commented on Oct 6, 2026

    @V3RON
    ContributorAuthor

    Delivery run

    Merged: #386 (#368)
    Parked: #369 — Done when line 1 names which released device stays down; the code only guarantees the count (PR #394, draft)
    Waiting: #370, #371, #372 — on #369

    Written by an agent.

  8. V3RON commented on Oct 6, 2026

    @V3RON
    ContributorAuthor

    Delivery run

    Merged: #386 (#368)
    Parked: #369 — run stopped by the maintainer before review round 3 (PR #394, draft)
    Waiting: #370, #371, #372 — on #369

    Written by an agent.

  9. V3RON commented on Oct 6, 2026

    @V3RON
    ContributorAuthor

    Delivery run

    Merged: none
    Parked: #369 — two blocking test findings open after review round 3 (PR #394)
    Waiting: #370 — on #369; #371 — on #369, #370; #372 — on #371

    Written by an agent.

  10. V3RON commented on Oct 6, 2026

    @V3RON
    ContributorAuthor

    Delivery run

    Merged: #394 (#369)
    Parked: #370 — the session hit its token budget during the round 4 fix. Edits are uncommitted in worktree task-370; see the handoff on #370.
    Waiting: #371 — on #370; #372 — on #371

    Written by an agent.

  11. github-actions commented on Oct 10, 2026

    @github-actions

    All 6 sub-issues are closed. If the completion conditions hold on main, close this feature and flip its ADRs to Accepted.

  12. V3RON commented on Oct 10, 2026

    @V3RON
    ContributorAuthor

    All sub-issues (#368, #369, #370, #371, #372, #373) are closed; the last, #372, passed its slow-lane run on real devices. Closing. PR #419 (the slow-lane test for #372) is still a draft and has to be merged on its own. Written by an agent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    feature:plannedSplit into tasks. Never picked up itself.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions