Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions controlplane/admin/trino_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ type TrinoQuery struct {
ResourceGroup string `json:"resource_group"`
// Query is the redacted SQL text.
Query string `json:"query"`
// Instance is the shared-pool instance whose coordinator runs the query,
// set by the pool observer. Empty for a cell with a single coordinator.
// It is what shows one instance saturated while the others are idle.
Instance string `json:"instance,omitempty"`

Created time.Time `json:"created,omitempty"`
ElapsedMS int64 `json:"elapsed_ms"`
Expand Down
25 changes: 18 additions & 7 deletions controlplane/admin/ui/src/components/TrinoCellPicker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,28 @@ function mount(path = "/trino/queries?active=true") {
render(<MemoryRouter initialEntries={[path]}><TrinoCellPicker /><Location /></MemoryRouter>);
}
beforeEach(() => hooks.useTrinoCells.mockReturnValue({ data: { cells: [{ id: "cell-test" }] } }));
it("requires explicit selection even when only one registered cell exists", () => {
it("defaults to the only registered cell when the URL names none", () => {
mount();
expect(screen.getByLabelText("Trino cell")).toHaveValue("");
expect(screen.getByLabelText("Location")).toHaveTextContent("?active=true");
fireEvent.change(screen.getByLabelText("Trino cell"), { target: { value: "cell-test" } });
expect(screen.getByLabelText("Trino cell")).toHaveValue("cell-test");
expect(screen.getByLabelText("Location")).toHaveTextContent("active=true&cell=cell-test");
});
it("does not infer a default from a cell name", () => {
hooks.useTrinoCells.mockReturnValue({ data: { cells: [{ id: "legacy" }, { id: "cell-test" }] } });
it("defaults to the first registered cell when several exist", () => {
hooks.useTrinoCells.mockReturnValue({ data: { cells: [{ id: "cell-001" }, { id: "cell-002" }] } });
mount();
expect(screen.getByLabelText("Trino cell")).toHaveValue("");
expect(screen.getByLabelText("Location")).toHaveTextContent("cell=cell-001");
fireEvent.change(screen.getByLabelText("Trino cell"), { target: { value: "cell-002" } });
expect(screen.getByLabelText("Location")).toHaveTextContent("cell=cell-002");
});
it("keeps an explicit cell", () => {
hooks.useTrinoCells.mockReturnValue({ data: { cells: [{ id: "cell-001" }, { id: "cell-002" }] } });
mount("/trino/queries?cell=cell-002");
expect(screen.getByLabelText("Trino cell")).toHaveValue("cell-002");
});
it("does not navigate before the cell list loads", () => {
hooks.useTrinoCells.mockReturnValue({ data: undefined });
mount();
expect(screen.getByLabelText("Location")).toHaveTextContent("?active=true");
expect(screen.getByLabelText("Location")).not.toHaveTextContent("cell=");
});
it("does not silently replace an unknown explicit cell", () => {
mount("/trino/queries?cell=removed");
Expand Down
24 changes: 22 additions & 2 deletions controlplane/admin/ui/src/components/TrinoCellPicker.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect } from "react";
import { useSearchParams } from "react-router-dom";
import { useTrinoCells } from "@/hooks/useApi";

Expand All @@ -6,19 +7,38 @@ export function TrinoCellPicker() {
const [params, setParams] = useSearchParams();
const entries = cells.data?.cells ?? [];
const selected = params.get("cell") ?? "";
const fallback = entries[0]?.id ?? "";

// Landing on a Trino page with no cell used to render an empty view until
// the operator picked one. Default to the first registered cell instead.
// This only fills an ABSENT parameter: an explicit cell in the URL, even
// one the server no longer lists, is never replaced, so a shared link keeps
// pointing where it was sent. replace keeps the defaulting out of history.
useEffect(() => {
if (selected || !fallback) return;
setParams(
(previous) => {
if (previous.get("cell")) return previous;
const next = new URLSearchParams(previous);
next.set("cell", fallback);
return next;
},
{ replace: true },
);
}, [selected, fallback, setParams]);

if (!entries.length) return null;
return (
<select
aria-label="Trino cell"
className="h-8 rounded border bg-background px-2 text-sm"
value={selected}
value={selected || fallback}
onChange={(event) => setParams((previous) => {
const next = new URLSearchParams(previous);
next.set("cell", event.target.value);
return next;
})}
>
<option value="" disabled>Choose a cell</option>
{selected && !entries.some((cell) => cell.id === selected) && <option value={selected} disabled>Unknown cell: {selected}</option>}
{entries.map((cell) => <option key={cell.id} value={cell.id}>{cell.id}</option>)}
</select>
Expand Down
95 changes: 94 additions & 1 deletion controlplane/admin/ui/src/lib/trino.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { describe, expect, it } from "vitest";
import {
groupTrinoQueries,
isWaitingTrinoQuery,
sortTrinoQueries,
trinoSqlSummary,
trinoStateBucket,
TRINO_DEGRADED_FAILURE_RATIO,
TRINO_LONG_RUNNING_MS,
isActiveTrinoQuery,
Expand Down Expand Up @@ -330,7 +335,8 @@ describe("trinoStateVariant", () => {
});

it("falls back rather than throwing on a state it does not know", () => {
expect(trinoStateVariant("WAITING_FOR_RESOURCES")).toBe("outline");
expect(trinoStateVariant("SOME_FUTURE_STATE")).toBe("outline");
expect(trinoStateVariant("WAITING_FOR_RESOURCES")).toBe("warning");
});
});

Expand Down Expand Up @@ -433,3 +439,90 @@ describe("summarizeTrinoNodes from system.runtime.nodes", () => {
expect(h.degraded).toBe(0);
});
});

describe("waiting states", () => {
it("counts every pre-execution state as queued, not only QUEUED", () => {
const states = ["QUEUED", "WAITING_FOR_RESOURCES", "DISPATCHING", "PLANNING", "STARTING", "RUNNING", "FINISHING"];
const s = summarizeTrinoQueries(states.map((state, i) => query({ query_id: `q${i}`, state })));
expect(s.queued).toBe(5);
expect(s.running).toBe(2);
});
it("flags a query waiting for resources", () => {
expect(isWaitingTrinoQuery(query({ state: "WAITING_FOR_RESOURCES" }))).toBe(true);
expect(trinoQueryFlag(query({ state: "WAITING_FOR_RESOURCES" }))).toBe("queued");
expect(trinoStateBucket(query({ state: "PLANNING" }))).toBe("waiting");
expect(trinoStateBucket(query({ state: "FINISHING" }))).toBe("running");
expect(trinoStateBucket(query({ state: "FAILED" }))).toBe("failed");
});
});

describe("trinoSqlSummary", () => {
it("strips the SQLMesh plan comment and names the target", () => {
const s = trinoSqlSummary(
'/* SQLMESH_PLAN: 4b1d */ INSERT INTO "org_portola"."sqlmesh_trino"."events__123" ("a") SELECT 1',
);
expect(s).toMatchObject({
verb: "INSERT INTO",
target: "org_portola.sqlmesh_trino.events__123",
tag: "SQLMESH_PLAN: 4b1d",
});
expect(s.text.startsWith("INSERT INTO")).toBe(true);
});
it("reports DDL kind and skips IF NOT EXISTS", () => {
expect(trinoSqlSummary("CREATE TABLE IF NOT EXISTS a.b AS SELECT 1")).toMatchObject({
verb: "CREATE TABLE",
target: "a.b",
});
expect(trinoSqlSummary("create or replace view x.y as select 1")).toMatchObject({
verb: "CREATE VIEW",
target: "x.y",
});
expect(trinoSqlSummary("DROP TABLE IF EXISTS s.t")).toMatchObject({ verb: "DROP TABLE", target: "s.t" });
});
it("treats WITH as a read and finds its first relation", () => {
expect(trinoSqlSummary("-- note\nWITH c AS (SELECT * FROM s.events) SELECT * FROM c")).toMatchObject({
verb: "SELECT",
target: "s.events",
tag: "note",
});
});
it("handles empty and unknown text", () => {
expect(trinoSqlSummary("")).toMatchObject({ verb: "", target: "", tag: "" });
expect(trinoSqlSummary("SHOW CATALOGS")).toMatchObject({ verb: "SHOW", target: "" });
});
});

describe("groupTrinoQueries", () => {
it("rolls up per key, busiest first", () => {
const groups = groupTrinoQueries(
[
query({ query_id: "1", org: "a", cpu_ms: 10 }),
query({ query_id: "2", org: "b", state: "QUEUED" }),
query({ query_id: "3", org: "b", cpu_ms: 5, physical_input_bytes: 7 }),
query({ query_id: "4", org: "b", state: "FINISHED", elapsed_ms: 99_999 }),
],
(q) => q.org,
);
expect(groups.map((g) => g.key)).toEqual(["b", "a"]);
expect(groups[0]).toMatchObject({ total: 3, running: 1, queued: 1, cpuMs: 5, scannedBytes: 7, longestMs: 1_000 });
});
});

describe("sortTrinoQueries", () => {
const rows = [
query({ query_id: "a", elapsed_ms: 5, cpu_ms: 3, state: "QUEUED" }),
query({ query_id: "b", elapsed_ms: 9, cpu_ms: 1 }),
query({ query_id: "c", elapsed_ms: 1, cpu_ms: 2, progress_percentage: 50 }),
];
it("sorts by a numeric key in both directions without mutating", () => {
expect(sortTrinoQueries(rows, "cpu", "desc").map((q) => q.query_id)).toEqual(["a", "c", "b"]);
expect(sortTrinoQueries(rows, "cpu", "asc").map((q) => q.query_id)).toEqual(["b", "c", "a"]);
expect(rows.map((q) => q.query_id)).toEqual(["a", "b", "c"]);
});
it("puts running before waiting and breaks ties by elapsed", () => {
expect(sortTrinoQueries(rows, "state", "asc").map((q) => q.query_id)).toEqual(["b", "c", "a"]);
});
it("sorts unknown progress below zero", () => {
expect(sortTrinoQueries(rows, "progress", "desc")[0].query_id).toBe("c");
});
});
Loading
Loading