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
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @jajeffries @leoparente @manrodrigues @mfiedorowicz @MicahParks @grant-nbl @paulstuart @marc-barry
* @jajeffries @leoparente @manrodrigues @mfiedorowicz @MicahParks @grant-nbl @paulstuart @marc-barry @davidlanouette
3 changes: 2 additions & 1 deletion .github/workflows/container-scan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ concurrency:
cancel-in-progress: true

env:
GO_VERSION: '1.25'
GO_VERSION: '1.26'

jobs:
build-and-scan:
Expand Down Expand Up @@ -61,6 +61,7 @@ jobs:
cache-to: type=gha,scope=${{ matrix.app }},mode=max
build-args: |
SVC=${{ matrix.app }}
GO_VERSION=${{ env.GO_VERSION }}
tags: diode-${{ matrix.app }}:scan

- name: Scan image for vulnerabilities
Expand Down
2 changes: 1 addition & 1 deletion diode-server/auth/manager_hydra.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"strings"
"time"

hydra "github.com/ory/hydra-client-go/v25"
hydra "github.com/ory/hydra-client-go/v26"
)

const (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- +goose Up

-- Two B-tree indexes on ingestion_logs to support fast ORDER BY on the
-- ingestion_ts and updated_at columns. Without these, queries listing
-- ingestion_logs ordered by either column fall back to a parallel
-- sequential scan + top-N heapsort over the full table; on a tenant
-- with several million rows that runs ~700ms-1.2s hot and times out
-- the 5s client budget under concurrent traffic. Plain (non-partial)
-- B-tree because list-style consumers may filter by any subset of
-- state/object_type/branch in addition to ordering by these columns.
--
-- B-tree indexes are bidirectional, so a single index per column covers
-- both ASC and DESC orderings without a second index.

CREATE INDEX IF NOT EXISTS idx_ingestion_logs_ingestion_ts ON ingestion_logs (ingestion_ts);
CREATE INDEX IF NOT EXISTS idx_ingestion_logs_updated_at ON ingestion_logs (updated_at);

-- +goose Down

DROP INDEX IF EXISTS idx_ingestion_logs_ingestion_ts;
DROP INDEX IF EXISTS idx_ingestion_logs_updated_at;
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
-- +goose Up
-- +goose StatementBegin

-- Counter table for fast SUM-based aggregation over (state, object_type).
-- Read path: list queries that filter by any subset of {state, object_type}
-- can answer their total via a single sub-millisecond index lookup instead
-- of a parallel sequential scan over the whole ingestion_logs table.
-- For tenants with several million rows that drops the count cost from
-- ~600ms per call to <1ms.
CREATE TABLE IF NOT EXISTS ingestion_log_state_object_type_counts (
state INT4 NOT NULL,
object_type TEXT NOT NULL,
n BIGINT NOT NULL,
PRIMARY KEY (state, object_type)
);

-- Initial backfill from the current data. Runs once at migration time; after
-- this point the counter is maintained transactionally by the triggers below.
INSERT INTO ingestion_log_state_object_type_counts (state, object_type, n)
SELECT state, COALESCE(object_type, ''), COUNT(*)
FROM ingestion_logs
WHERE state IS NOT NULL
GROUP BY state, COALESCE(object_type, '')
ON CONFLICT (state, object_type) DO UPDATE SET n = EXCLUDED.n;

-- +goose StatementEnd

-- +goose StatementBegin
-- Statement-level triggers with transition tables. Each fires once per
-- INSERT / UPDATE / DELETE statement against ingestion_logs regardless of
-- how many rows it touches, so a 100-row bulk transition costs one trigger
-- invocation, not a hundred. The trigger body collapses the affected rows
-- into a small aggregated UPSERT.
--
-- Rows with NULL state are skipped: state is only NULL transiently during
-- creation; the existing list queries filter `state = ANY(...)` which also
-- excludes nulls, so counter semantics match.

CREATE OR REPLACE FUNCTION ingestion_log_state_object_type_counts_on_insert()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO ingestion_log_state_object_type_counts (state, object_type, n)
SELECT state, COALESCE(object_type, ''), COUNT(*)
FROM new_rows
WHERE state IS NOT NULL
GROUP BY state, COALESCE(object_type, '')
ON CONFLICT (state, object_type)
DO UPDATE SET n = ingestion_log_state_object_type_counts.n + EXCLUDED.n;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd

-- +goose StatementBegin
CREATE OR REPLACE FUNCTION ingestion_log_state_object_type_counts_on_update()
RETURNS TRIGGER AS $$
BEGIN
-- Aggregate the net delta per (state, object_type) cell. A row that
-- changes state from A to B contributes -1 to (A, *) and +1 to (B, *);
-- a row whose (state, object_type) is unchanged nets to zero and is
-- filtered out by the HAVING clause.
WITH deltas AS (
SELECT state, COALESCE(object_type, '') AS object_type, -COUNT(*) AS delta
FROM old_rows
WHERE state IS NOT NULL
GROUP BY state, COALESCE(object_type, '')
UNION ALL
SELECT state, COALESCE(object_type, ''), COUNT(*)
FROM new_rows
WHERE state IS NOT NULL
GROUP BY state, COALESCE(object_type, '')
),
net AS (
SELECT state, object_type, SUM(delta) AS delta
FROM deltas
GROUP BY state, object_type
HAVING SUM(delta) <> 0
)
INSERT INTO ingestion_log_state_object_type_counts (state, object_type, n)
SELECT state, object_type, delta FROM net
ON CONFLICT (state, object_type)
DO UPDATE SET n = ingestion_log_state_object_type_counts.n + EXCLUDED.n;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd

-- +goose StatementBegin
CREATE OR REPLACE FUNCTION ingestion_log_state_object_type_counts_on_delete()
RETURNS TRIGGER AS $$
BEGIN
WITH deltas AS (
SELECT state, COALESCE(object_type, '') AS object_type, COUNT(*) AS n
FROM old_rows
WHERE state IS NOT NULL
GROUP BY state, COALESCE(object_type, '')
)
UPDATE ingestion_log_state_object_type_counts c
SET n = c.n - d.n
FROM deltas d
WHERE c.state = d.state AND c.object_type = d.object_type;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd

-- +goose StatementBegin
CREATE TRIGGER ingestion_log_state_object_type_counts_after_insert
AFTER INSERT ON ingestion_logs
REFERENCING NEW TABLE AS new_rows
FOR EACH STATEMENT
EXECUTE FUNCTION ingestion_log_state_object_type_counts_on_insert();
-- +goose StatementEnd

-- +goose StatementBegin
CREATE TRIGGER ingestion_log_state_object_type_counts_after_update
AFTER UPDATE ON ingestion_logs
REFERENCING OLD TABLE AS old_rows NEW TABLE AS new_rows
FOR EACH STATEMENT
EXECUTE FUNCTION ingestion_log_state_object_type_counts_on_update();
-- +goose StatementEnd

-- +goose StatementBegin
CREATE TRIGGER ingestion_log_state_object_type_counts_after_delete
AFTER DELETE ON ingestion_logs
REFERENCING OLD TABLE AS old_rows
FOR EACH STATEMENT
EXECUTE FUNCTION ingestion_log_state_object_type_counts_on_delete();
-- +goose StatementEnd

-- +goose StatementBegin
-- Reconciliation function: replace the counter contents with the truth as
-- of NOW(). Called at reconciler startup as a defense-in-depth check against
-- drift (admin SQL that bypassed the trigger, replication anomalies, etc.).
-- Bounded to a single transaction so concurrent readers either see the old
-- value or the new value, not partial state.
CREATE OR REPLACE FUNCTION rebuild_ingestion_log_state_object_type_counts()
RETURNS VOID AS $$
BEGIN
LOCK TABLE ingestion_log_state_object_type_counts IN EXCLUSIVE MODE;
DELETE FROM ingestion_log_state_object_type_counts;
INSERT INTO ingestion_log_state_object_type_counts (state, object_type, n)
SELECT state, COALESCE(object_type, ''), COUNT(*)
FROM ingestion_logs
WHERE state IS NOT NULL
GROUP BY state, COALESCE(object_type, '');
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
DROP TRIGGER IF EXISTS ingestion_log_state_object_type_counts_after_delete ON ingestion_logs;
DROP TRIGGER IF EXISTS ingestion_log_state_object_type_counts_after_update ON ingestion_logs;
DROP TRIGGER IF EXISTS ingestion_log_state_object_type_counts_after_insert ON ingestion_logs;
DROP FUNCTION IF EXISTS rebuild_ingestion_log_state_object_type_counts();
DROP FUNCTION IF EXISTS ingestion_log_state_object_type_counts_on_delete();
DROP FUNCTION IF EXISTS ingestion_log_state_object_type_counts_on_update();
DROP FUNCTION IF EXISTS ingestion_log_state_object_type_counts_on_insert();
DROP TABLE IF EXISTS ingestion_log_state_object_type_counts;
-- +goose StatementEnd
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-- +goose Up

-- Composite indexes to support the common deviation-list COUNT shape:
-- "WHERE state = ANY(...) AND (ingestion_ts | updated_at) BETWEEN ? AND ?".
-- Without these, Postgres has to pick between the single-column state
-- index and the single-column time index and filter by the other axis,
-- which scans many irrelevant rows when the time window is selective.
-- The composite lets it seek to each matching state value and range-scan
-- the time column within.

CREATE INDEX IF NOT EXISTS idx_ingestion_logs_state_ingestion_ts ON ingestion_logs (state, ingestion_ts);
CREATE INDEX IF NOT EXISTS idx_ingestion_logs_state_updated_at ON ingestion_logs (state, updated_at);

-- +goose Down

DROP INDEX IF EXISTS idx_ingestion_logs_state_ingestion_ts;
DROP INDEX IF EXISTS idx_ingestion_logs_state_updated_at;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- +goose Up

-- Composite descending index to support the deviation-list LIST shape:
-- "ORDER BY updated_at DESC, id DESC LIMIT N". Without this index Postgres
-- falls back to Parallel Seq Scan + top-N heapsort over the full
-- ingestion_logs table because updated_at carries many duplicate values
-- from bulk operations, so the existing single-column
-- idx_ingestion_logs_updated_at cannot satisfy the multi-column sort
-- via Incremental Sort. With the composite, the planner does an
-- Index Scan over (updated_at DESC, id DESC) and stops at LIMIT N,
-- bringing top-N list queries from seconds to sub-millisecond on
-- multi-million-row tables.
--
-- Complements idx_ingestion_logs_state_updated_at from migration
-- 20260525000002, which helps state-filtered COUNTs but does not satisfy
-- the DESC sort shape: state = ANY(...) requires merging multiple
-- ascending index streams, which the planner estimates as costlier than
-- a seq scan + sort. The composite below lets the planner satisfy the
-- DESC sort directly and apply state as a cheap post-scan filter.

CREATE INDEX IF NOT EXISTS idx_ingestion_logs_updated_at_id_desc
ON ingestion_logs (updated_at DESC, id DESC);

-- +goose Down

DROP INDEX IF EXISTS idx_ingestion_logs_updated_at_id_desc;
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
-- +goose Up

-- The ingestion_log_state_object_type_counts triggers introduced in
-- 20260525000001 use multi-row `INSERT ... ON CONFLICT DO UPDATE` to
-- maintain the (state, object_type) counter cells. Their SELECT had no
-- ORDER BY, so Postgres acquired row locks on the counter table in the
-- non-deterministic order returned by the hash aggregation. Concurrent
-- statement-level trigger invocations (typically two ClaimQueuedForAutoApply
-- transitions, both 1 -> 8 across overlapping object_types) could then form
-- a lock cycle on the same (state, object_type) cells and deadlock with
-- SQLSTATE 40P01.
--
-- The fix is the documented Postgres mitigation for this anti-pattern:
-- ORDER BY the conflict target so every concurrent invocation acquires
-- counter row locks in the same global order. Output rows are unchanged;
-- only the lock-acquisition order is constrained.
--
-- CREATE OR REPLACE FUNCTION swaps the function body in place; existing
-- triggers pick up the new definition on the next statement, so no
-- ALTER TRIGGER or table change is needed.

-- +goose StatementBegin
CREATE OR REPLACE FUNCTION ingestion_log_state_object_type_counts_on_insert()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO ingestion_log_state_object_type_counts (state, object_type, n)
SELECT state, COALESCE(object_type, '') AS object_type, COUNT(*)
FROM new_rows
WHERE state IS NOT NULL
GROUP BY state, COALESCE(object_type, '')
ORDER BY state, object_type
ON CONFLICT (state, object_type)
DO UPDATE SET n = ingestion_log_state_object_type_counts.n + EXCLUDED.n;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd

-- +goose StatementBegin
CREATE OR REPLACE FUNCTION ingestion_log_state_object_type_counts_on_update()
RETURNS TRIGGER AS $$
BEGIN
WITH deltas AS (
SELECT state, COALESCE(object_type, '') AS object_type, -COUNT(*) AS delta
FROM old_rows
WHERE state IS NOT NULL
GROUP BY state, COALESCE(object_type, '')
UNION ALL
SELECT state, COALESCE(object_type, ''), COUNT(*)
FROM new_rows
WHERE state IS NOT NULL
GROUP BY state, COALESCE(object_type, '')
),
net AS (
SELECT state, object_type, SUM(delta) AS delta
FROM deltas
GROUP BY state, object_type
HAVING SUM(delta) <> 0
)
INSERT INTO ingestion_log_state_object_type_counts (state, object_type, n)
SELECT state, object_type, delta FROM net
ORDER BY state, object_type
ON CONFLICT (state, object_type)
DO UPDATE SET n = ingestion_log_state_object_type_counts.n + EXCLUDED.n;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd

-- +goose Down

-- Restore the original (pre-fix) function bodies from 20260525000001.
-- The Down path reintroduces the deadlock risk; it exists only to make
-- the migration reversible during local testing.

-- +goose StatementBegin
CREATE OR REPLACE FUNCTION ingestion_log_state_object_type_counts_on_insert()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO ingestion_log_state_object_type_counts (state, object_type, n)
SELECT state, COALESCE(object_type, ''), COUNT(*)
FROM new_rows
WHERE state IS NOT NULL
GROUP BY state, COALESCE(object_type, '')
ON CONFLICT (state, object_type)
DO UPDATE SET n = ingestion_log_state_object_type_counts.n + EXCLUDED.n;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd

-- +goose StatementBegin
CREATE OR REPLACE FUNCTION ingestion_log_state_object_type_counts_on_update()
RETURNS TRIGGER AS $$
BEGIN
WITH deltas AS (
SELECT state, COALESCE(object_type, '') AS object_type, -COUNT(*) AS delta
FROM old_rows
WHERE state IS NOT NULL
GROUP BY state, COALESCE(object_type, '')
UNION ALL
SELECT state, COALESCE(object_type, ''), COUNT(*)
FROM new_rows
WHERE state IS NOT NULL
GROUP BY state, COALESCE(object_type, '')
),
net AS (
SELECT state, object_type, SUM(delta) AS delta
FROM deltas
GROUP BY state, object_type
HAVING SUM(delta) <> 0
)
INSERT INTO ingestion_log_state_object_type_counts (state, object_type, n)
SELECT state, object_type, delta FROM net
ON CONFLICT (state, object_type)
DO UPDATE SET n = ingestion_log_state_object_type_counts.n + EXCLUDED.n;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- +goose StatementEnd
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- +goose Up
-- +goose StatementBegin
-- requeued_from_state records the terminal state an ingestion log was in when
-- a duplicate observation requeued it for re-plan (see BulkMarkDuplicates).
-- Consumed at persist time: a log requeued from APPLIED spawns a new
-- deviation on drift and is restored to APPLIED, preserving its history.
-- Cleared on every final state write.
ALTER TABLE ingestion_logs ADD COLUMN requeued_from_state INTEGER;
-- +goose StatementEnd

-- +goose Down
-- +goose StatementBegin
ALTER TABLE ingestion_logs DROP COLUMN requeued_from_state;
-- +goose StatementEnd
Loading
Loading