Skip to content

feat: a lease window holds work to set hours - #290

Merged
T-Crypt merged 8 commits into
nextfrom
T-Crypt/lease-window
Nov 2, 2026
Merged

T-Crypt merged 8 commits into
nextfrom
T-Crypt/lease-window

Conversation

@T-Crypt

@T-Crypt T-Crypt commented Nov 2, 2026 •

Copy link
Copy Markdown
Owner

A user asked for jobs that run only when they are not working, or only when electricity is cheapest. Berth already runs batch work in the gaps (--priority batch, background = true), but it had no idea of time of day. This adds one.

What changes for users

  • window = "23:00-07:00" on an [[engine]] entry, or berth run --window 23:00-07:00, holds the lease to those hours on the node's clock. A window may cross midnight.
  • A run that asks outside its window waits for it. It has no deadline unless --wait sets one, and it does not block other waiters. Its reason reads outside its window 23:00-07:00; opens in 3h12m.
  • When the window closes, berthd drains the holder at the usual boundary (a slot, a ComfyUI job), so a request in flight finishes. A parked engine is evicted instead.
  • The gateway does not start a background engine outside its window.
  • --window is refused with --pinned and with --priority hold, because a window close is a drain and those leases are never drained.
  • docs/leasing.md has a new section, "Run work when you're away or power is cheap", with a timer recipe for Linux and Windows.

How it works

The window is a lease field in the broker, next to min_turn, so berth status shows the wait and a re-attach after a berthd restart keeps it. The type lives in internal/proto, because the broker must not import config. In schedule, waiters outside their window step out of the queue before it runs. Tick drains or evicts holders whose window closed, once per lease.

Proof

  • Build, gofmt, vet, go test ./..., the Windows build and vet, and golangci-lint (0 issues) all pass.
  • Every new branch was removed once, and a test failed each time (23 mutations).
  • Two bugs from review have tests that failed before the fix: a windowed waiter at the head of the queue kept a later batch job off a free card, and berthd's 120 s default wait refused a run started at noon for a night window.
  • Live run on a throwaway berthd built from this branch, with sleep jobs that never touch the card. A run with window 08:03-08:05 read opens in 2m, waited past the daemon's --wait 10s, got the card at 08:03:01 and drained at 08:05:01 with its window 08:03-08:05 closed. A later batch run with no window got the card at once while the first one waited.
  • The first CI run failed on a test that acquired its lease at 12:00 UTC, which falls outside the night window on UTC runners. The test now pins its clock inside the window, and the window tests pass under UTC, America/Los_Angeles, Asia/Tokyo and Pacific/Auckland.

A user who wants work on the card only while power is cheap writes
window = "23:00-07:00" on an engine entry. The value parses from
"HH:MM-HH:MM", may cross midnight, and keeps that spelling in TOML,
JSON and YAML through the text codec; the zero value names no window, so
an entry that says nothing is held to no hours.

The type is proto's rather than config's: berthd decides grants by the
window, and config reaches the broker only through claims, so a type in
config would close an import cycle. The engine entry, the socket message
and the lease all read the same value.
The two-digit check is its own rule, and every bad string the table had so
far was already caught by the length check. "1O:00" is five characters with
a colon in the right place, so it is the one that says whether the code
looks at the digits at all.
A lease carries its window from the request that asked for the card. Outside
its hours a waiter does not fit and drains nothing: it reads "outside its
window 23:00-07:00; opens in 3h12m" and keeps its place in the queue. The
tick after hours close drains the holder with "its window 23:00-07:00 closed"
and evicts a parked one, once each, and a lease with no window is untouched.
A window close is a drain, so the boundary a drain names comes first and a
request in flight is never cut off.

The hours ride the way min_turn does: the entry's window or berth run
--window, through runner.Options and the socket into the lease, and back out
on a re-attach so a daemon restart does not lose them. A window goes with
neither --pinned nor --priority hold, since nothing drains either, and a
window the socket cannot read is refused there rather than read as none.

Drain and evict come out of schedule and evictParked, so a closed window asks
for the card back the same way a waiter does.
An engine outside its hours stays down: started, it would only sit in the
queue for a lease berthd cannot grant yet. The clock it is measured by,
freeSince, is the card's and not the engine's, so it keeps running while the
window is shut and the engine starts on the tick its window opens with.
Dropping the key here would reset the idle wait of every other engine on that
card.
leasing.md gets the section: a batch lease yields to any request, a
background engine waits for a free card, and a window holds either to set
hours, with the entry and the berth run line. A close is a drain at the
boundary, so work that is not a server saves checkpoints, and a timer can
start it at the open. gateway.md gets one sentence and one TOML line in its
background section, and the CHANGELOG one entry.
…r it

A batch run waiting for its night window sat at the head of the queue, so
a later batch job with no window waited all day on a free card and read
the windowed run's reason as its own. A waiter outside its window now
steps out of line until the window opens, and reads its own reason.

berthd's default wait (120 s) also refused a run started at noon for a
night window. A windowed request that names no --wait now waits for its
window with no deadline.
@T-Crypt
T-Crypt merged commit 8d416c7 into next Nov 2, 2026
4 checks passed
@T-Crypt
T-Crypt deleted the T-Crypt/lease-window branch November 2, 2026 15:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant