Repository navigation
Report in the All Messages view incomplete scatter-gather results and server query timeouts #3107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ramonsmits
merged 1 commit into
ramon/audit-new-row-animation
from
ramon/audit-incomplete-results
Sep 10, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { describe, expect, test } from "vitest"; | ||
| import { decodeInstanceId, describeIncompleteReason, describeInstance, parseIncompleteResults } from "@/components/incompleteResults"; | ||
|
|
||
| describe("FEATURE: Incomplete-results header parsing", () => { | ||
| test("EXAMPLE: A missing header means the response is complete", () => { | ||
| expect(parseIncompleteResults(null)).toEqual([]); | ||
| expect(parseIncompleteResults("")).toEqual([]); | ||
| }); | ||
|
|
||
| test("EXAMPLE: Entries carry the instance and why it contributed nothing", () => { | ||
| expect(parseIncompleteResults("audit-2:timeout, audit-3:unavailable, audit-4:error")).toEqual([ | ||
| { instanceId: "audit-2", reason: "timeout" }, | ||
| { instanceId: "audit-3", reason: "unavailable" }, | ||
| { instanceId: "audit-4", reason: "error" }, | ||
| ]); | ||
| }); | ||
|
|
||
| test("EXAMPLE: The reason follows the last colon, so instance ids can contain colons", () => { | ||
| expect(parseIncompleteResults("http://audit-host:44444/api:timeout")).toEqual([{ instanceId: "http://audit-host:44444/api", reason: "timeout" }]); | ||
| }); | ||
|
|
||
| test("EXAMPLE: An unknown reason is treated as an error", () => { | ||
| expect(parseIncompleteResults("audit-2:exploded")).toEqual([{ instanceId: "audit-2", reason: "error" }]); | ||
| expect(parseIncompleteResults("audit-2")).toEqual([{ instanceId: "audit-2", reason: "error" }]); | ||
| }); | ||
|
|
||
| test("EXAMPLE: Reasons read as prose", () => { | ||
| expect(describeIncompleteReason("timeout")).toBe("timed out"); | ||
| expect(describeIncompleteReason("unavailable")).toBe("unreachable"); | ||
| expect(describeIncompleteReason("error")).toBe("returned an error"); | ||
| }); | ||
|
|
||
| // ServiceControl identifies an instance by its API URL, lower-cased and base64 encoded with | ||
| // the URL-safe alphabet: '-' for '+', '_' for '/', '.' for '=' (InstanceIdGenerator) | ||
| describe("RULE: Instance ids are ServiceControl's base64 API URLs and are shown as host and port", () => { | ||
| test("EXAMPLE: An id decodes to the instance's API URL", () => { | ||
| expect(decodeInstanceId("aHR0cDovL2xvY2FsaG9zdDo0NDQ0NC9hcGkv")).toBe("http://localhost:44444/api/"); | ||
| }); | ||
|
|
||
| test("EXAMPLE: URL-safe substitutions and dot padding are reversed before decoding", () => { | ||
| expect(decodeInstanceId("aHR0cDovL3NjLWF1ZGl0OjQ0NDQ0L2FwaQ..")).toBe("http://sc-audit:44444/api"); | ||
| }); | ||
|
|
||
| test("EXAMPLE: Something that is not a base64 URL is left alone", () => { | ||
| expect(decodeInstanceId("audit-2")).toBeNull(); | ||
| expect(decodeInstanceId("")).toBeNull(); | ||
| }); | ||
|
|
||
| test("EXAMPLE: The label is host and port; scheme and path are noise for a reader", () => { | ||
| expect(describeInstance("aHR0cDovL2xvY2FsaG9zdDo0NDQ0NC9hcGkv")).toEqual({ label: "localhost:44444", apiUrl: "http://localhost:44444/api/" }); | ||
| expect(describeInstance("aHR0cDovL2F1ZGl0LTIuaW50ZXJuYWw6MzMzMzMvYXBp")).toEqual({ label: "audit-2.internal:33333", apiUrl: "http://audit-2.internal:33333/api" }); | ||
| }); | ||
|
|
||
| test("EXAMPLE: A default port is omitted from the label", () => { | ||
| expect(describeInstance("aHR0cHM6Ly9hdWRpdC5leGFtcGxlLmNvbS9hcGkv")).toEqual({ label: "audit.example.com", apiUrl: "https://audit.example.com/api/" }); | ||
| }); | ||
|
|
||
| test("EXAMPLE: An id that does not decode is shown as it is, with nothing to hover", () => { | ||
| expect(describeInstance("audit-2")).toEqual({ label: "audit-2", apiUrl: null }); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| // Parsing for the X-Particular-Incomplete-Results response header. | ||
| // | ||
| // ServiceControl's composite (scatter-gather) endpoints return a bare array, | ||
| // so when an instance contributes nothing the response stays 200 with the | ||
| // partial data and this header names what is missing, as comma-separated | ||
| // "instanceId:reason" entries (reasons: timeout, unavailable, error). | ||
| // A response without the header is complete. | ||
|
|
||
| export type IncompleteReason = "timeout" | "unavailable" | "error"; | ||
|
|
||
| export interface IncompleteInstance { | ||
| instanceId: string; | ||
| reason: IncompleteReason; | ||
| } | ||
|
|
||
| export const incompleteResultsHeader = "X-Particular-Incomplete-Results"; | ||
|
|
||
| const knownReasons: ReadonlySet<string> = new Set(["timeout", "unavailable", "error"]); | ||
|
|
||
| export function parseIncompleteResults(header: string | null): IncompleteInstance[] { | ||
| if (!header) return []; | ||
| return header | ||
| .split(",") | ||
| .map((entry) => entry.trim()) | ||
| .filter((entry) => entry !== "") | ||
| .map((entry) => { | ||
| // The reason follows the last colon; instance ids can contain colons (e.g. URLs) | ||
| const separator = entry.lastIndexOf(":"); | ||
| const instanceId = separator > 0 ? entry.slice(0, separator) : entry; | ||
| const rawReason = separator > 0 ? entry.slice(separator + 1).trim() : ""; | ||
| const reason: IncompleteReason = knownReasons.has(rawReason) ? (rawReason as IncompleteReason) : "error"; | ||
| return { instanceId, reason }; | ||
| }); | ||
| } | ||
|
|
||
| export function describeIncompleteReason(reason: IncompleteReason): string { | ||
| switch (reason) { | ||
| case "timeout": | ||
| return "timed out"; | ||
| case "unavailable": | ||
| return "unreachable"; | ||
| default: | ||
| return "returned an error"; | ||
| } | ||
| } | ||
|
|
||
| // ServiceControl identifies an instance by its API URL, lower-cased and base64 encoded with the | ||
| // URL-safe alphabet ('-' for '+', '_' for '/', '.' for '=': InstanceIdGenerator.FromApiUrl). | ||
| // Returns the URL, or null when the id is not one of those. | ||
| export function decodeInstanceId(instanceId: string): string | null { | ||
| if (instanceId === "") return null; | ||
| try { | ||
| const binary = atob(instanceId.replace(/-/g, "+").replace(/_/g, "/").replace(/\./g, "=")); | ||
| const decoded = new TextDecoder("utf-8", { fatal: true }).decode(Uint8Array.from(binary, (c) => c.charCodeAt(0))); | ||
| const url = new URL(decoded); | ||
| return url.protocol === "http:" || url.protocol === "https:" ? decoded : null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| export interface InstanceDescription { | ||
| // What a reader needs to tell instances apart: host and port (default port omitted) | ||
| label: string; | ||
| // The full API URL for a tooltip, when the id decoded to one | ||
| apiUrl: string | null; | ||
| } | ||
|
|
||
| export function describeInstance(instanceId: string): InstanceDescription { | ||
| const apiUrl = decodeInstanceId(instanceId); | ||
| if (apiUrl === null) return { label: instanceId, apiUrl: null }; | ||
| // URL.host already omits the port when it is the scheme's default | ||
| return { label: new URL(apiUrl).host, apiUrl }; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.