Skip to content

fix: wait for a pending job identity instead of denying the restore - #15

Merged
linyiru merged 1 commit into
mainfrom
fix/cache-wait-pending-identity
Oct 16, 2026
Merged

linyiru merged 1 commit into
mainfrom
fix/cache-wait-pending-identity

Conversation

@linyiru

@linyiru linyiru commented Oct 16, 2026

Copy link
Copy Markdown
Member

Why

#14 grants cache rights only once the daemon has handled JobStarted and resolved the job through the REST API. Until then, the VM's lease is pending, and the cache server denies every request from it immediately.

JobStarted is late. On two production hosts over 24 hours (about 4,300 jobs), the scale-set JobStarted message reached the daemon this long after the guest's own Running job: console line:

host jobs p50 p90 max ≥ 30 s
A 2227 33 s 46 s 52 s 53%
B 2081 29 s 47 s 51 s 50%

A typical workflow's first actions/cache restore runs 20–40 s into the job (checkout, then setup). So under #14 as merged, roughly half of all restores land before the lease resolves and turn into misses. Saves at the end of the job are unaffected, which hides the problem: the cache keeps filling and the hit rate quietly halves.

What

  1. A request from a pending lease waits instead of being denied. The cache server polls the record (every 250 ms) until one of these happens:

    • it resolves, and the request goes on;
    • it becomes unresolved (see 2), and the request is denied at once;
    • its generation changes (slot reuse), and the request is denied;
    • --identity-wait runs out (default 60 s; 0 keeps the old deny-at-once behaviour), and the request is denied;
    • the client goes away.

    Waiting grants nothing new: the same Validate and connection-generation check run on the record it ends with. @actions/http-client allows 3 minutes per request, so a 60 s wait is inside the toolkit's own limit. The HTTP server has no WriteTimeout to trip.

  2. Failed resolution is now terminal and visible to readers. Before this PR, a lease whose resolution gave up looked exactly like one still in flight. With waiting, every request from that job would sit out the full wait. The registry now writes unresolved: true when it gives up, Validate rejects such a record, and Pending excludes it.

  3. Denials are counted by reason.

    • /stats keeps identity_denied (the total) and adds identity_denied_by_reason, identity_waits and identity_wait_seconds.
    • /metrics: firerunner_cache_identity_denied_total gains a reason label (missing, pending, unresolved, expired, invalid, generation), so sum() over it equals the old series.
    • New metrics: firerunner_cache_identity_waits_total and firerunner_cache_identity_wait_seconds_total.
    • A rising pending count means the wait is shorter than the JobStarted lag. unresolved points at App permissions or the REST API.

    Stale-generation denials were not counted before; they now are, as generation.

Tests

  • Go:
    • A restore issued while pending waits, then hits once the lease resolves; the wait is counted.
    • Each way a wait ends: deadline, unresolved, slot reuse, client gone.
    • Missing, unresolved, expired and invalid records deny without waiting.
    • Metrics carry reason labels.
    • The registry marks a failed resolution unresolved, and Pending and Validate both refuse such a record.
  • Real toolkit (test/cache-protocol):
    • an @actions/cache restore started 3 s before its lease resolves hits the default-branch entry;
    • a restore from an unresolved lease misses without waiting.
  • Mutation check: each of the following makes at least one new test fail:
    • not waiting;
    • treating unresolved as pending;
    • the registry never marking unresolved;
    • Validate accepting unresolved;
    • not rechecking the generation while waiting;
    • ignoring client cancellation;
    • ignoring the deadline;
    • not counting waits;
    • collapsing the reasons.

JobStarted reaches the daemon tens of seconds after the guest dequeues
the job (p50 ~30 s, p90 ~47 s on two hosts over ~4,300 jobs), and a
workflow's first actions/cache restore usually runs before that. Denying
a pending lease at once turned about half of all restores into misses.

The cache server now polls a pending lease until it resolves, turns
unresolved, changes generation, the client leaves, or --identity-wait
(default 60s) runs out. The registry marks a lease unresolved when
resolution gives up, so readers stop waiting at once.

Denials are counted by reason (missing, pending, unresolved, expired,
invalid, generation) in /stats and as a label on
firerunner_cache_identity_denied_total, alongside wait count and time.

Co-Authored-By: Claude <noreply@anthropic.com>
@linyiru

linyiru commented Oct 16, 2026

Copy link
Copy Markdown
Member Author

babysit-pr · head c123798
CI: green (3 passed, e2e skipped by design).
Local review (subagent): e69d4cd..c123798. No findings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

No blocking issues were identified; bounded waiting preserves authorization and generation checks with focused regression coverage.

No open issues 🎉

What changed in this PR

Prevents early cache restores from becoming misses while job identity resolution is pending, preserving existing authorization checks.

Changes:

  • Adds configurable waiting for pending identities and terminal unresolved records.
  • Exposes denial reasons and wait metrics.
  • Adds Go and real-toolkit regression coverage.
File Description
test/​cache-protocol/​test.mjs Tests pending restores and unresolved denials.
test/​cache-protocol/​README.md Documents expanded integration coverage.
README.md Explains waiting, terminal failures, and metrics.
internal/​jobidentity/​registry.go Publishes terminal resolution failures.
internal/​jobidentity/​identity.go Defines unresolved and pending identity behavior.
internal/​jobidentity/​identity_test.go Tests terminal failures and pending classification.
internal/​cacheserver/​scope.go Implements bounded waiting and denial classification.
internal/​cacheserver/​scope_test.go Tests waiting, termination paths, and metrics.
internal/​cacheserver/​cacheserver.go Initializes waiting defaults and exports metrics.
cmd/​firerunner/​main.go Adds the --identity-wait option.

Review effort: Balanced

@linyiru
linyiru merged commit 5823b5e into main Oct 16, 2026
5 checks passed
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.

2 participants