mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 12:03:59 +00:00
152 lines
6.3 KiB
PL/PgSQL
152 lines
6.3 KiB
PL/PgSQL
-- 07_corrected_functions.sql
|
|
-- Corrected versions of all four utility functions from the change request.
|
|
-- Each fix is annotated with the issue number it addresses (see REVIEW.md).
|
|
|
|
-- ── Fix for cleanup_expired_artifacts ────────────────────────────────────────
|
|
-- Issue 4: Added partition_key to WHERE and IN-list for partition pruning.
|
|
-- Issue 4: Replaced FOR UPDATE with FOR UPDATE SKIP LOCKED to prevent
|
|
-- stacking behind a concurrent cleanup job.
|
|
-- Bonus: RETURNING clause allows callers to log what was actually deleted.
|
|
CREATE OR REPLACE FUNCTION demo.cleanup_expired_artifacts(
|
|
p_batch_size INT DEFAULT 10000
|
|
)
|
|
RETURNS INT AS $$
|
|
DECLARE
|
|
v_deleted INT;
|
|
BEGIN
|
|
DELETE FROM demo.ci_job_artifacts
|
|
WHERE (id, partition_key) IN (
|
|
SELECT id, partition_key
|
|
FROM demo.ci_job_artifacts
|
|
WHERE expire_at < NOW()
|
|
AND locked = FALSE
|
|
AND partition_key < DATE_TRUNC('month', NOW())::DATE -- prune to old partitions only
|
|
ORDER BY expire_at ASC
|
|
LIMIT p_batch_size
|
|
FOR UPDATE SKIP LOCKED
|
|
);
|
|
|
|
GET DIAGNOSTICS v_deleted = ROW_COUNT;
|
|
RAISE NOTICE 'Cleaned up % expired artifacts', v_deleted;
|
|
RETURN v_deleted;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
|
|
-- ── Fix for terminate_idle_sessions ──────────────────────────────────────────
|
|
-- Issue 5: Added filter to exclude PgBouncer pooler connections.
|
|
-- In transaction-mode PgBouncer the backend appears idle between
|
|
-- client transactions. Terminating it breaks the pool and causes
|
|
-- "SSL connection has been closed unexpectedly" for active clients.
|
|
-- Bonus: filter also excludes replication and background workers.
|
|
CREATE OR REPLACE FUNCTION demo.terminate_idle_sessions(
|
|
p_idle_threshold INTERVAL DEFAULT '10 minutes',
|
|
p_max_terminated INT DEFAULT 50,
|
|
p_pgbouncer_app TEXT DEFAULT 'pgbouncer' -- set to your app_name
|
|
)
|
|
RETURNS TABLE(pid INT, username TEXT, idle_duration INTERVAL, query TEXT) AS $$
|
|
BEGIN
|
|
RETURN QUERY
|
|
WITH idle_sessions AS (
|
|
SELECT
|
|
a.pid,
|
|
a.usename::TEXT,
|
|
NOW() - a.state_change AS duration,
|
|
a.query
|
|
FROM pg_stat_activity a
|
|
WHERE a.state = 'idle'
|
|
AND NOW() - a.state_change > p_idle_threshold
|
|
AND a.pid <> pg_backend_pid()
|
|
AND a.backend_type = 'client backend'
|
|
AND a.application_name NOT ILIKE '%' || p_pgbouncer_app || '%'
|
|
AND a.client_addr IS NOT NULL -- exclude unix socket (local tools)
|
|
ORDER BY duration DESC
|
|
LIMIT p_max_terminated
|
|
)
|
|
SELECT s.pid, s.usename, s.duration, s.query
|
|
FROM idle_sessions s
|
|
WHERE pg_terminate_backend(s.pid);
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
|
|
-- ── Fix for calculate_namespace_storage ──────────────────────────────────────
|
|
-- Issue 6: Cache race condition fixed with FOR UPDATE SKIP LOCKED.
|
|
-- Callers that can't acquire the cache row lock return the stale
|
|
-- cached value rather than all recomputing simultaneously.
|
|
-- Issue 7: Recursive CTE depth limit enforced (max 20 levels).
|
|
-- Requires tables: namespaces(id, parent_id), project_storages(namespace_id, storage_bytes),
|
|
-- namespace_storage_cache(namespace_id, total_bytes, calculated_at)
|
|
-- (These don't exist in our demo schema; this is illustrative code only.)
|
|
CREATE OR REPLACE FUNCTION demo.calculate_namespace_storage(
|
|
p_namespace_id BIGINT
|
|
)
|
|
RETURNS BIGINT AS $$
|
|
DECLARE
|
|
v_total_bytes BIGINT := 0;
|
|
v_cached_bytes BIGINT;
|
|
v_cached_at TIMESTAMP;
|
|
BEGIN
|
|
-- Attempt to lock the cache row exclusively.
|
|
-- SKIP LOCKED means concurrent callers return the stale value immediately
|
|
-- instead of piling up and all recomputing the expensive CTE.
|
|
SELECT total_bytes, calculated_at
|
|
INTO v_cached_bytes, v_cached_at
|
|
FROM namespace_storage_cache
|
|
WHERE namespace_id = p_namespace_id
|
|
FOR UPDATE SKIP LOCKED;
|
|
|
|
-- Cache hit: return without recomputing
|
|
IF v_cached_at IS NOT NULL AND v_cached_at > NOW() - INTERVAL '1 hour' THEN
|
|
RETURN v_cached_bytes;
|
|
END IF;
|
|
|
|
-- Cache miss or expired: recompute with depth-limited recursive CTE.
|
|
-- Issue 7 fix: depth column added to prevent infinite recursion on cycles.
|
|
WITH RECURSIVE ns_tree AS (
|
|
SELECT id, 0 AS depth
|
|
FROM namespaces
|
|
WHERE id = p_namespace_id
|
|
|
|
UNION ALL
|
|
|
|
SELECT n.id, t.depth + 1
|
|
FROM namespaces n
|
|
JOIN ns_tree t ON n.parent_id = t.id
|
|
WHERE t.depth < 20 -- hard cap; cyclic namespaces stop here
|
|
)
|
|
SELECT COALESCE(SUM(ps.storage_bytes), 0)
|
|
INTO v_total_bytes
|
|
FROM ns_tree nt
|
|
JOIN project_storages ps ON ps.namespace_id = nt.id;
|
|
|
|
INSERT INTO namespace_storage_cache (namespace_id, total_bytes, calculated_at)
|
|
VALUES (p_namespace_id, v_total_bytes, NOW())
|
|
ON CONFLICT (namespace_id) DO UPDATE
|
|
SET total_bytes = EXCLUDED.total_bytes,
|
|
calculated_at = EXCLUDED.calculated_at;
|
|
|
|
RETURN v_total_bytes;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
|
|
-- ── Fix for database_health view ─────────────────────────────────────────────
|
|
-- Issue 8: NULLIF prevents division by zero if n_live_tup is ever 0.
|
|
-- The original WHERE n_live_tup > 1000 makes the crash unlikely in practice,
|
|
-- but this is fragile to future refactoring that removes the filter.
|
|
CREATE OR REPLACE VIEW demo.database_health AS
|
|
SELECT
|
|
schemaname,
|
|
relname AS table_name,
|
|
n_live_tup AS live_rows,
|
|
n_dead_tup AS dead_rows,
|
|
ROUND(n_dead_tup::NUMERIC / NULLIF(n_live_tup, 0) * 100, 2) AS dead_row_pct,
|
|
last_vacuum,
|
|
last_autovacuum,
|
|
last_analyze,
|
|
pg_size_pretty(pg_total_relation_size(schemaname || '.' || relname)) AS total_size
|
|
FROM pg_stat_user_tables
|
|
WHERE n_live_tup > 1000
|
|
ORDER BY n_dead_tup DESC;
|