Repository navigation
ui: keep the sandbox list's search, lifecycle filter and Next working in more cases - #1397
Conversation
… in more cases Follow-ups from the review of #1388. - On a paging server, a search of only hex digits and dashes (other than a dashed ID start) is still matched anywhere in the IDs and names of the loaded page, so the ID column's label finds its row, and is now also sent as `prefix` in a request of its own. Names such as `e2e-1`, `cafe` or `1234`, and IDs starting with the text, are then found beyond the page and listed after it. The search help and notices say which match is made where. - Editing the lifecycle control no longer drops the statuses listed under "Not applied" from the filter and the URL. - A failed load keeps Next when the rows on screen are kept, so a retryable error on a background poll no longer disables Next on a page whose rows and next_cursor are still valid. - A page answering a sampled list switches it to paging and rereads the first page with every filter, instead of stopping the list for the rest of the page view. A bare array answering a paged list still stops it with the reload notice.
|
Thanks for opening this pull request! A SPACE maintainer will take a look. CONTRIBUTING.md explains how we review outside contributions. |
… lacks An older prefix response could overwrite a newer one: a slow request overtaken by a refresh's newer one still passed the key check, and searching A, B, A accepted the first A's answer. Each request now has an AbortController and a ticket; starting one, or an effect cleanup, aborts the previous, and an answer applies only if its ticket is the newest. The supplemental section also repeated rows already in the main table. It now excludes them, counts what remains, and reads "More matches beyond this page".
Review — 4 findingsMedium
Low
Verified against I traced the two mode-switch paths in finding 1 through |
the-gigi-pplx
left a comment
There was a problem hiding this comment.
Approved. 4 non-blocking findings in the review comment above.
…wers During a rolling deploy or its rollback, replicas with and without paging answer behind one load balancer, in either order. A paged view whose poll reached an old replica stopped for the rest of the page view, and a sampled view that met a paging replica switched to paging before its first page answered, so a failed or bare-array reread left it with no rows. The list now never joins or appends rows of two shapes, and keeps the rows on screen through a mismatch: - A bare array answering a paged request fails that load. The page on screen keeps its rows and its Next, and a notice says the API answered in an older format and the list is retrying. - A sampled view that gets a page back reads page 1 with every filter, and switches to paging only when that page answers, replacing the sample with it. Until then the sample stays. - The list stops with the reload notice only after three bare arrays in a row answer paged requests. Any other answer resets the count. The prefix search's request counts toward the same count, and a bare array fails a refresh of its section, which keeps its rows.
When every match of the prefix request was already in the main table and the server had more, the section read "(0+)" over an empty table. Once an answer leaves no row to show, and no error to report, the section is left out, and the "+" appears only next to rows.
…ist's switch A sampled list whose poll answers a page reads page 1 with every filter before it switches to paging. The sample request sends only the org, so choosing another template while that read is in flight leaves the selected request unchanged and invalidates nothing. The page for the earlier filters then passed the ticket check and was shown under the new filter, without Next, until the next poll. After that read answers, the list now checks that it is still the first page for the filters chosen, and otherwise reads it again for them. The load is never skipped there, since the page may be waiting on it with its spinner on.
A failed refresh is retried on the next one-second check, so three bare arrays in a row came within about three seconds. With a fraction f of old replicas, each refresh cycle stopped a paged view with probability about f cubed, and a long rollout with half its replicas old still ended most open lists on the reload notice. The list now stops only when every answer for a minute (`STOP_AFTER_MISMATCH_MS`) has been a bare array answering a paged request, with at least three of them. Any page, for the list or the prefix search, starts the minute over. The clock is injected so the tests can run interleaved answers over ten minutes, and old answers alone through the window, without waiting.
…its shape The page assumes paging until the first list answer arrives, and sent the prefix request before then. On a server without paging, each bare array answering it counted as a mismatch: with a slow first load it could show the older-format notice before the sample loaded, and under the earlier count rule three of them stopped the list for good. The prefix request now waits for `ListSelection.shapeDecided`, and `noteAnswer` neither counts nor starts over before then (`undecided`); the page treats such an answer as a failed refresh of its section.
…ell as a minute The stop window ran from the first bare array of a run and kept running while nothing was sent: a hidden tab does not refresh, and a sleeping laptop or a clock jump skips ahead. One bare array, a minute in a hidden tab, then two more on return stopped the list after three answers. The list now stops only when bare arrays alone have answered for a minute and numbered at least twenty in a row. Twenty is about a third of the retries a minute of one-second checks sends, so a pause cannot stand in for them and a real downgrade still stops the list after about a minute. A count is kept rather than restarting the run after a gap, since a slow request lengthens the gap between retries without meaning a pause.
|
Thanks. All four are addressed; the head is now
Could you take another look? |
Review (re-review at
|
Summary
Follows up the review of #1388. It fixes low-severity problems in the console's sandbox list, mostly on a server that pages and during a rolling deploy.
e2e-1,cafeor1234could not be found beyond that page. Such a search still matches anywhere in the IDs and names of the page on screen, so the ID column's label still finds its row. It is now also sent asprefixin a request of its own (fragmentSearchScopeinui/src/lib/sandbox-list.ts). The names and IDs that start with it, across the whole list, are listed in a "More matches beyond this page" section after the page. That section leaves out rows the main table already shows, and is left out itself once an answer leaves nothing to show. At most one of these requests is in flight (PrefixRequestsinui/src/lib/prefix-search.ts): a new one aborts the one before, and only the newest answer for the search now typed applies. The search help, the notice above the table and the section say which match is made where.onChangereplaced the whole filter. Editing it silently dropped the statuses listed under "Not applied", from state and from the URL. Those statuses are now kept (chooseAppliedStatusesinui/src/lib/sandbox-lifecycle.ts); their own tag still removes them.next_cursorwere still valid. The cursor is now cleared only when the rows on screen are dropped.ListSelectioninui/src/lib/sandbox-list.ts) never joins or appends rows of two shapes, and keeps the rows on screen through a mismatch:STOP_AFTER_MISMATCH_MS) has been a bare array answering a paged request, with at least twenty of them in a row (STOP_AFTER_MISMATCHES): a real downgrade. Any page starts both over. A failed refresh is retried on the next one-second check, so a count alone would have been reached within seconds while some replicas are old. Time alone also passes while nothing is sent: a hidden tab does not refresh, and a sleeping laptop or a clock jump skips ahead. With time alone, one bare array, a minute in a hidden tab, and two more on return would stop the list. Twenty is about a third of the retries a minute sends, so a pause cannot stand in for them, and a downgrade still stops the list after about a minute. The rule keeps a count rather than starting the run over after a gap, because a slow request also lengthens the gap between retries and is not a pause.ListSelection.shapeDecided), and an answer before then neither counts nor starts the minute over.Validation
At
74c037c177c4, inui/:npm ci,npm test(216 tests pass),npx tsc --noEmit -p .andnpm run buildall pass.Each item has unit tests:
sandbox-id.test.mjs: "on a paging server, hex digits and dashes are also sent as a prefix, so such names are found beyond the page".prefix-search.test.mjs: the single-flight and newest-ticket rules, and the matches left out of the section.sandbox-lifecycle.test.mjs: "changing the lifecycle control keeps the statuses a paged list does not apply".sandbox-list.test.mjs: "a failed refresh of the page on screen keeps its rows and its Next". For mixed versions: "a bare array on a refresh of a paged list fails it, keeping its rows and Next", "a paged list stops only once bare arrays alone have answered for the window", "a pause after a bare array, as in a hidden tab, does not count toward stopping the list", "interleaved old and new answers over several minutes never stop a paged list", "a bare array answering the prefix search counts toward stopping the list", "a prefix answer before the first answer of the list neither counts nor starts over", "a page answering a sampled list switches it to paging at the first page, with every filter", "a sampled list keeps its sample until a first page answers, through interleaved shapes", and "a filter changed while a sampled list reads its first page is read again before it switches". The stopping tests inject the clock intoListSelection, so ten minutes of answers run without waiting.Fail-before:
sandbox-list.test.mjsimports exports that the earlier libraries lack, so against either one, unchanged, the whole file fails to load. To see which tests fail, the file at this head was run against a copy of each earliersandbox-list.ts, with the missing exports added as stubs (OlderFormatAnsweras a bareErrorsubclass,STOP_AFTER_MISMATCHES = 3, andSTOP_AFTER_MISMATCH_MS = 60_000) and nothing else changed:6ae6726a, which stopped after a minute and three bare arrays in a row: 1 of 41 fails, the pause test above. The other 40 pass. (This head's library withSTOP_AFTER_MISMATCHESset back to 3, which matches6ae6726a's apart from comments.)5169e750, which stopped after three bare arrays in a row: 5 of 41 fail. They are the two stopping-window tests, the pause test, the prefix-before-first-answer test and the filter-change test above. The other 36 pass.3f03059c, which stopped on the first mismatch: 11 of 41 fail, the mixed-version tests above and "a bare array after a filter change fails the load and clears the other scope's rows" and "a bare array answering Next fails it and clears the previous page". The other 30 pass.In a browser, the previous head's (
6ae6726a) builtui/was served withnext startand driven with Playwright 1.64.1. Every API request was answered by route interception with synthetic data: a signed-in operator, an org, and five sandboxes. The list endpoint answered a page or a bare array as each scenario chose. Each scenario sampled the main table's rows every 100 ms:cafe) on a server without paging whose first answer took 4 s: no prefix request was sent, before or after, and no older-format notice showed.At the previous head (
5169e750), on a local Kind stack from this branch with Playwright interception of the list requests and synthetic fixtures seeded and deleted afterwards:These paths are unchanged since, apart from the stopping rule and the prefix request's timing, which the browser check above covers at
6ae6726a. Kind was not rerun at that head or at this one.Not validated: a hidden tab or a sleeping laptop in a browser; the pause test covers it with the injected clock. Next's cursor in a browser. The Kind stack had a single page, so Next was disabled throughout; keeping the cursor through a mismatch is covered by the unit tests. The filter change during a sampled view's switch to paging is covered only by its unit test, since its timing is hard to force in a browser.
Severity and risk
Severity: low. Each problem hides rows, freezes the list, or disables a control in the operator console; no data is changed.
Risk: a hex-only search on a paging server sends one extra list request (first page, at the page size, with the selected filters and
prefix). That request is debounced and refreshed with the list. Statuses that are not applied now persist until their tag is closed. During a rolling deploy, a list can show rows that are a few polls old under the older-format notice. A sampled view switches to paging, and to page 1, once a page 1 answers. A failed refresh is retried on the next one-second check, so while old replicas answer, an open list can send about one list request a second. The list stops with the reload notice only after a minute in which every answer was a bare array, with at least twenty of them in a row. While any replica that pages still answers, a page soon arrives and starts both over. A tab that is hidden, or a laptop that sleeps, sends nothing, so that time adds no bare arrays: a rollout stops an open list only if at least twenty retries in a row, spanning a minute, all reach old replicas. At one retry a second that is usually about sixty, and never fewer than twenty, which is rare even when most replicas are old. After a real downgrade, a list shows its last rows under the older-format notice for about a minute before the reload notice, or longer when each request takes more than about three seconds. To roll back, revert these commits.Rollout
This change is about how the console behaves while old and new API versions run together; see the last item of the summary. It needs no ordering with the API.
AI assistance: Claude Code drafted the code, the tests and this description.