bd7a423b86
* fix(sandbox): fit the nightly cleanup inside PostgREST's 8s session cap Follow-up to #1449. Profiling the repaired teardown on prod puts one sandbox user at ~3s (the auth.users delete fans out over ~250 FK triggers; FK indexes were tried inside an aborted transaction and do not help), while every PostgREST session inherits authenticator's statement_timeout = 8s. The nightly RPC call therefore times out and ROLLS BACK wholesale: a second silent-failure mode for the same cron. - cleanup_expired_sandbox_users gets a function-local statement_timeout of 290s (same sanctioned pattern as undo_sie_import, 20260702154500) via migration 20260807150000. - The cron route bounds each night to 60 users (~180s), exports maxDuration = 300, and the backlog drains over a few nights. - Tests: route asserts the bounded rpc call and the maxDuration budget; the pg suite pins proconfig containing statement_timeout=290s. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(db): index journal_entry_lines.account_id (chart-account cascade seq-scans 730k rows) Caught live during the backlog purge: DELETE FROM auth.users cascades chart_of_accounts deletion, whose ON DELETE SET NULL fires an unindexed UPDATE over journal_entry_lines per account (~37 per sandbox company). This is the bulk of the ~3s per-user teardown cost. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
97 lines
3.2 KiB
PL/PgSQL
97 lines
3.2 KiB
PL/PgSQL
-- The nightly sandbox cleanup cannot finish inside PostgREST's session cap.
|
|
--
|
|
-- All PostgREST sessions log in as authenticator, which carries
|
|
-- statement_timeout = 8s at the role level, and profiling the repaired
|
|
-- teardown (migration 20260807130000) on prod puts one sandbox user at
|
|
-- ~3s: the auth.users delete fans out over ~250 FK triggers. FK indexes do
|
|
-- not help (verified by replaying with them inside an aborted transaction);
|
|
-- the cost is the trigger fan-out itself. One nightly RPC call therefore
|
|
-- cleans at most two users before the whole batch times out and ROLLS BACK,
|
|
-- which is a second silent-failure mode for the cron this migration chain
|
|
-- exists to fix.
|
|
--
|
|
-- Fix mirrors undo_sie_import (20260702154500, pinned by
|
|
-- sie-import.replace.pg.test.ts): a function-local statement_timeout raises
|
|
-- the cap for this statement only. CREATE OR REPLACE resets proconfig, so
|
|
-- the timeout must be restated if this function is ever replaced again.
|
|
-- The cron route passes p_limit sized so a run finishes inside both this
|
|
-- 290s cap and the route's maxDuration.
|
|
|
|
DROP FUNCTION IF EXISTS public.cleanup_expired_sandbox_users(int, int);
|
|
|
|
CREATE FUNCTION public.cleanup_expired_sandbox_users(
|
|
p_max_age_hours int DEFAULT 24,
|
|
p_limit int DEFAULT NULL
|
|
)
|
|
RETURNS jsonb
|
|
LANGUAGE plpgsql
|
|
SECURITY DEFINER
|
|
SET search_path = public
|
|
SET statement_timeout TO '290s'
|
|
AS $$
|
|
DECLARE
|
|
v_user_id uuid;
|
|
v_cleaned integer := 0;
|
|
v_failed integer := 0;
|
|
v_orphans integer := 0;
|
|
BEGIN
|
|
FOR v_user_id IN
|
|
SELECT cs.user_id
|
|
FROM public.company_settings cs
|
|
WHERE cs.is_sandbox = true
|
|
AND cs.created_at < now() - interval '1 hour' * p_max_age_hours
|
|
ORDER BY cs.created_at
|
|
LIMIT p_limit
|
|
LOOP
|
|
BEGIN
|
|
PERFORM public.cleanup_sandbox_user(v_user_id);
|
|
v_cleaned := v_cleaned + 1;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
v_failed := v_failed + 1;
|
|
RAISE WARNING 'Failed to clean up sandbox user %: %', v_user_id, SQLERRM;
|
|
END;
|
|
END LOOP;
|
|
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'auth' AND table_name = 'users'
|
|
AND column_name = 'is_anonymous'
|
|
) THEN
|
|
FOR v_user_id IN
|
|
SELECT u.id
|
|
FROM auth.users u
|
|
WHERE u.is_anonymous = true
|
|
AND u.created_at < now() - interval '1 hour' * p_max_age_hours
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM public.company_settings cs WHERE cs.user_id = u.id
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM public.companies c WHERE c.created_by = u.id
|
|
)
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM public.company_members cm WHERE cm.user_id = u.id
|
|
)
|
|
ORDER BY u.created_at
|
|
LIMIT p_limit
|
|
LOOP
|
|
BEGIN
|
|
DELETE FROM auth.users WHERE id = v_user_id;
|
|
v_orphans := v_orphans + 1;
|
|
EXCEPTION WHEN OTHERS THEN
|
|
v_failed := v_failed + 1;
|
|
RAISE WARNING 'Failed to clean up orphaned anonymous user %: %', v_user_id, SQLERRM;
|
|
END;
|
|
END LOOP;
|
|
END IF;
|
|
|
|
RETURN jsonb_build_object(
|
|
'cleaned', v_cleaned,
|
|
'failed', v_failed,
|
|
'orphans_removed', v_orphans
|
|
);
|
|
END;
|
|
$$;
|
|
|
|
REVOKE ALL ON FUNCTION public.cleanup_expired_sandbox_users(int, int) FROM PUBLIC, anon, authenticated;
|
|
GRANT EXECUTE ON FUNCTION public.cleanup_expired_sandbox_users(int, int) TO service_role;
|