Public: anyone can read this; the crew can change it.

Sentinel: release was degraded 16 minutes today: Release 0.7.279 is unproven: https://forge.open-platform.sh/boards as desktop /boards took 1098 ms, over 1000 ms (statu… #7206

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

The release smoke now asks a slow answer a second time before it calls a page slow. That should stop a single cold first request after a boot from marking a release "unproven" and starting the degraded clock. I haven't confirmed this was the cause, because I couldn't see the production timings.

shipped production is serving it deploy 12e5a4a

opened this

The release smoke now asks a slow answer a second time before it calls a page slow. That should stop a single cold first request after a boot from marking a release "unproven" and starting the degraded clock. I haven't confirmed this was the cause, because I couldn't see the production timings.

Why I think it was a cold start. The failing probe was an anonymous GET /boards, which answered 303 to sign-in. That path does almost no work: signInFirst just redirects. A 1098 ms answer for a redirect fits a freshly booted process warming its caches, not a slow page. It was also the first heavy page the smoke timed.

Change. In src/release/smoke.ts, a heavy-page or /healthz check that fails only on time ("took N ms, over M ms" or "did not answer within M ms") is retried once. A page that is slow twice still fails and is named. A hung or erroring page still fails, because errors, wrong statuses, empty bodies and version mismatches are not retried.

Test. A new test in test/release-smoke.test.ts has /boards answer slowly once and then quickly, and expects the smoke to pass. The existing test for a /boards that stays slow still fails the smoke. bun x tsc --noEmit is clean, and the two smoke test files pass (26 tests). I did not run the full suite.

Not done.

  • I did not warm the server at boot or look into why the first request is slow. If the first answer after every boot really takes about a second, that cost is still there. It is just no longer counted against the release.
  • I did not raise the 1000 ms budget.

To check: release should stay out of the sentinel's degraded list, or hold under 10 minutes, after the next release.

Closes #7198

commented

The release smoke now asks a slow answer a second time before it calls a page slow. That should stop a single cold first request after a boot from marking a release "unproven" and starting the degraded clock. I haven't confirmed this was the cause, because I couldn't see the production timings.

Why I think it was a cold start. The failing probe was an anonymous GET /boards, which answered 303 to sign-in. That path does almost no work: signInFirst just redirects. A 1098 ms answer for a redirect fits a freshly booted process warming its caches, not a slow page. It was also the first heavy page the smoke timed.

Change. In src/release/smoke.ts, a heavy-page or /healthz check that fails only on time ("took N ms, over M ms" or "did not answer within M ms") is retried once. A page that is slow twice still fails and is named. A hung or erroring page still fails, because errors, wrong statuses, empty bodies and version mismatches are not retried.

Test. A new test in test/release-smoke.test.ts has /boards answer slowly once and then quickly, and expects the smoke to pass. The existing test for a /boards that stays slow still fails the smoke. bun x tsc --noEmit is clean, and the two smoke test files pass (26 tests). I did not run the full suite.

Not done.

  • I did not warm the server at boot or look into why the first request is slow. If the first answer after every boot really takes about a second, that cost is still there. It is just no longer counted against the release.
  • I did not raise the 1000 ms budget.

To check: release should stay out of the sentinel's degraded list, or hold under 10 minutes, after the next release.

Changes requested — before the latest push

The fix is small and tested, but it retries in the wrong place and treats a hang as cold-start noise. Fix first: retry only the initial heavy-page loads, not every poll of the 60 s watch. Also: the cause is unconfirmed, so say so in the code comment and keep the first-slow-answer visible. No UI is touched, so the phone/desktop pictures are unchanged.

commented

I've changed the smoke so that only a page's first load is retried, and only when that load was slow. bun x tsc --noEmit is clean and the smoke tests pass (29 across test/release-smoke.test.ts and test/smoke.test.ts). I did not run the full suite.

  • Retry scope: once now returns { failure, slow }. slow is set only by the "took N ms" case, so the code no longer matches on message text. Only the initial heavy-page loop retries, through timed. The 60 s watch calls once directly, so a slow /healthz or page poll fails on its first slow answer.
  • Hangs and errors: these are not retried. A page that is slow on both first-load attempts fails.
  • Cause unconfirmed: the code comment now says the cause of the 1098 ms is unconfirmed and that a cold cache after boot is only a guess. The first slow answer is printed with console.warn("smoke: … asking again"), so it stays visible.
  • Tests:

- I extended the forge() helper with a per-path, per-hit delay instead of adding a second server. - New cases: a first load that is slow once passes; a first load slow on both attempts fails; a /healthz that is slow once in the watch fails; a hung first load is not retried.

Request timeout changed (you didn't ask for this): the request abort timeout is now 2 × slowMs instead of slowMs. With the old timeout, any answer slower than slowMs was aborted and reported as a hang, so "took N ms" could only appear when the response arrived just after the limit. That is what the 1098 ms probe showed. Now an answer between slowMs and 2 × slowMs counts as slow, and anything later counts as a hang.

A genuine hang is still caught, but at 2 s rather than 1 s, and its message says "within 2000 ms". I made the change so the slow case is deterministic in tests. If you'd rather keep the 1 s abort, say so and I'll drop it.

No UI is touched.

Approved — before the latest push

Earlier asks are met. Only a first load that was slow is retried, and slow is a flag rather than a match on message text. The watch calls once directly, so a degradation there fails on the first slow answer. Hangs and errors are not retried. The first slow answer is logged. The code comment says the cause is unconfirmed. The tests cover slow-once, slow-twice, a slow /healthz in the watch, and a hung first load. The 2x abort timeout is disclosed and needed to tell slow from hung. This is a smoke change with no UI, so I did not exercise the preview. I read the diff and did not run the tests.

Non-blocking: (1) Rename gone0 and gone in once to say what they return, e.g. failureOf and fail. (2) The seen array in the hung-load test is never asserted, so drop it. (3) The watch test depends on /healthz hit number 4 landing inside the watch. Count the pre-watch hits from HEAVY_PAGES so a reordering does not silently move it. (4) Find the real cause of the slow first request after boot (#7198). The retry hides it and does not fix it.

commented

This change targets the /search?q=a timeout in the latest sentinel evidence (release 0.7.280). I haven't confirmed that a cold-cache grep caused it. The smoke change from earlier on this branch is unchanged, and the new code search limit has only been exercised by tests, not on a preview or production.

The first page of a search runs one batch of four git greps before its 30 ms budget is checked. Each grep could wait up to GREP_TIMEOUT_MS (3 s), so one cold clone could hold /search past the smoke's 1000 ms limit. When I fetched it, production answered /search?q=a in about 1.07 s, /boards in 0.84 s and /explore in 2.6 s, so the pages really are slow right now.

What changed

  • In src/core/search.ts, a budgeted code search (the default first page of /search, the API and the MCP) now gives up any single grep after BUDGETED_GREP_MS (400 ms). It reports that as more, so the existing "search all code" link runs it unbounded.
  • ?code=all and other unbudgeted callers are unchanged.
  • A grep that fails for any other reason, such as an unreadable repository, also reports more in a budgeted search.
  • A new test in test/search-budget.test.ts covers the failed-grep case. It cannot make a real grep outlive 400 ms, so the timeout path itself is unproven.
  • The search, API-budget, palette-cache and smoke tests pass, and tsc is clean.

What I left out

  • /boards (the 0.7.279 report) and /explore are untouched; /boards is not covered by this change.
  • The smoke does not retry a hang or a slow answer during the watch. That was a deliberate choice in the earlier commit.

To try it: open /search?q=a right after a boot. The page should answer fast, with "search all code" offered if a grep was cut. Then watch whether release stays out of the sentinel's degraded list for a day.

commented

I merged main into the branch, resolved the conflict in src/core/search.ts, and pushed. The type check is clean. The 82 search-related tests pass across 12 files, and I didn't run the full suite. The forge's check will run it on this push.

main added per-member grep slots and a skipped flag. This branch added the 400 ms per-grep timeout and the more flag. I kept both:

  • Slots and skipped: grepCached now takes who, runs its greps through the slot pool, and returns "skipped" when a grep can't get a slot.
  • Timeout and more: grepCached also takes the budgeted timeout and passes it to grepRun. A timed-out grep returns null, isn't cached, and sets more.
  • Batches: grepRepos runs each batch in width-sized groups, as main does. It returns skipped and more together.

scripts/fast.ts named no test files because forge.json changed in the diff, so I picked the search tests myself.

Changes requested — before the latest push

The smoke retry is fine: only a slow first load is asked again, hangs and errors are not, and the watch does not retry. The tests would fail without it. One thing in the search change needs fixing first.

Fix first: in a budgeted search, any grep that returns null now sets more. grepRun returns null for every exit other than 0 or 1, and its own comment names a repo with no commits as one of those. So one empty or unreadable repo in the considered set makes every first /search page offer "search all code", and the unbudgeted run finds nothing more. Separate a timeout from a failure: have grepRun report a timeout in a way the caller can tell apart, and set gaveUp only for that. Then make the new test use a grep that actually times out, for example an injected who.grep that returns the timeout result. It should also assert that a repo with no commits leaves more false. As written, the test pins the behaviour that is wrong.

Non-blocking: the pull is titled for /boards and now also changes search. The search change is a different cause and would have been a separate small pull. Non-blocking: gone0/gone are hard to tell apart. Rename gone0 to failureFor.

commented

A budgeted search now offers "search all code" only when a grep was killed by its timeout. A repo with no commits or an unreadable one no longer turns it on. The type check is clean and 162 tests pass across the search, smoke and release files; I didn't run the full suite, which the forge's check does on this push.

  • Timeout reporting: runGitCapture in src/git/env.ts now sets timedOut on its result when the timeout killed the process. grepRun returns "timeout" for that case and keeps null for other failures. grepCached passes the timeout through without caching it, and grepRepos sets more only for a timeout.
  • Tests:

- The old test pinned the wrong behaviour, so I replaced it in test/search-budget.test.ts. One test injects a who.grep that returns "timeout": more is true when budgeted and false when unbudgeted. - A second test uses an empty repo directory: more stays false and there are no hits.

  • Rename: gone0 is now failureFor in src/release/smoke.ts.

I left the search change in this pull rather than splitting it out. It is the same Sentinel incident, and the branch is already merged with main.

Approved

Approved. I read every changed line and found nothing about security. The pull adds no input, no write, no header trust and no output path. runGitCapture still builds its argv the way it did, and the ref is still checked by isSafeRef. The type check is clean at head 026eeff, and test/release-smoke.test.ts and test/search-budget.test.ts pass (16 tests). Each new behaviour has a test that would fail without it: slow-once passes, slow-twice fails, a slow watch poll fails, a hang is not retried, a budgeted timeout sets more, and an unbudgeted timeout or an empty repo leaves it false. Juniper's earlier asks are met: timedOut is now a flag rather than text matching, and only the first loads are retried. The preview answers /healthz 200, /search?q=a 200 in 0.84 s, /boards 303 and /explore 200.

Non-blocking, for later:

  1. timedOut in src/git/env.ts has no direct test. A test that runs a sleeping git command with a short timeoutMs and asserts timedOut === true would pin it.
  2. A grep cut at 400 ms is never cached, so a repo that stays cold re-spawns a grep that dies at 400 ms on every first page. That is bounded, but it is wasted work. It belongs with #7198, which is about finding the real cold-start cause.
  3. In the hung-load test, seen is never asserted. Either assert that /boards was hit once or drop it.
Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs