diff --git a/DECISIONS.md b/DECISIONS.md index 90cf4e5a..c30370e3 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -245,3 +245,4 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-07-20] Removed Dependabot entirely (.github/dependabot.yml deleted, open PRs #1083/#1082/#1012 closed) on Emil's request: weekly grouped bumps were noise and the #884 bedrock-sdk incident showed the risk profile. Dependency bumps are now manual/deliberate; the bedrock-sdk 0.29.1 exact pin stays enforced by scripts/checks/no-new-antipatterns.mjs. [2026-07-20] Bulk reject (/pending) reuses the exact bulk-approve selection set: high-risk and locked-period ops stay one-by-one for reject too, keeping one selection model instead of per-action eligibility. Server-side bulk-reject has NO high-risk skip (rejecting posts nothing), so the API stays permissive; the UI is the gate. [2026-07-21] Domain cutover is dual-domain, not full migration: app.gnubok.se stays serving /api + /.well-known forever (MCP connectors, API keys, SKV callback registered in Utvecklarportalen); only page traffic redirects to app.accounted.se, gated on NEXT_PUBLIC_APP_URL so the merge is inert. SKV OAuth callback rewritten cookie-free (state + stored oauth_user_id) because sessions no longer exist on the OAuth host; discovery docs host-reflect (allowlisted) for RFC 8414/9728 self-consistency. +[2026-07-21] Fortnox/SIE atomic import timeout fixed by function-scoped statement_timeout (290s) on import_sie_journal_entries, not by chunking the RPC: chunking would reintroduce the partial-import states the atomic RPC exists to eliminate (20260712150000), and the 20260629160100 delete-path precedent already uses the same bound; pg-real ratchet pins the config on all three SIE RPCs because CREATE OR REPLACE silently drops ALTER FUNCTION settings. diff --git a/supabase/migrations/20260721144311_import_sie_statement_timeout.sql b/supabase/migrations/20260721144311_import_sie_statement_timeout.sql new file mode 100644 index 00000000..8f1d6565 --- /dev/null +++ b/supabase/migrations/20260721144311_import_sie_statement_timeout.sql @@ -0,0 +1,22 @@ +-- Raise statement_timeout inside the atomic SIE import RPC so a large +-- Fortnox/provider migration is not cancelled mid-import. +-- +-- Same failure class and fix as 20260629160100 (replace_sie_import / +-- undo_sie_import): a PostgREST request runs under the authenticator +-- role's 8s statement_timeout, and import_sie_journal_entries commits an +-- entire SIE file in one call (per-voucher loop, per-line inserts, audit +-- triggers). A real-world multi-year migration (thousands of vouchers) +-- exceeds 8s, the statement is cancelled ("canceling statement due to +-- statement timeout"), and the whole import rolls back with +-- "SIE-verifikationer kunde inte importeras atomiskt". +-- +-- A function-scoped SET re-arms the timer for the duration of the call and +-- is restored on exit. 290s sits just under the calling routes' maxDuration +-- = 300 ceiling (/api/extensions/ext/[...path], /api/import/sie/execute, +-- /api/v1/.../imports/sie), so the serverless layer stays the effective +-- bound. Function body is unchanged; only configuration is altered. + +ALTER FUNCTION public.import_sie_journal_entries(uuid, uuid, uuid, jsonb) + SET statement_timeout = '290s'; + +NOTIFY pgrst, 'reload schema'; diff --git a/tests/pg/sie-rpc-statement-timeout.pg.test.ts b/tests/pg/sie-rpc-statement-timeout.pg.test.ts new file mode 100644 index 00000000..f80601c4 --- /dev/null +++ b/tests/pg/sie-rpc-statement-timeout.pg.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' +import { getPool } from './setup' + +/** + * The SIE import/replace/undo RPCs each run an entire file's work in one + * statement, so each carries a function-scoped statement_timeout (migrations + * 20260629160100 and 20260721144311); without it they inherit the 8s + * authenticator timeout and any real-world multi-year migration is cancelled + * with "canceling statement due to statement timeout". + * + * CREATE OR REPLACE FUNCTION silently drops settings attached via + * ALTER FUNCTION ... SET, so a future redefinition of any of these functions + * would revive the bug unless the SET is carried along. This ratchet runs + * after full migration replay in CI and pins the config. + */ + +const TIMEOUT_PROTECTED_FUNCTIONS = [ + 'import_sie_journal_entries', + 'replace_sie_import', + 'undo_sie_import', +] + +describe('SIE RPC statement_timeout config', () => { + it.each(TIMEOUT_PROTECTED_FUNCTIONS)( + '%s carries a function-scoped statement_timeout of 290s', + async (functionName) => { + const { rows } = await getPool().query<{ proconfig: string[] | null }>( + `SELECT p.proconfig + FROM pg_proc p + JOIN pg_namespace n ON n.oid = p.pronamespace + WHERE n.nspname = 'public' AND p.proname = $1`, + [functionName], + ) + expect(rows, `${functionName} should exist exactly once in public`).toHaveLength(1) + expect( + rows[0].proconfig ?? [], + `${functionName} lost its statement_timeout: re-add "SET statement_timeout = '290s'" ` + + 'to the CREATE OR REPLACE FUNCTION statement that redefined it', + ).toContain('statement_timeout=290s') + }, + ) +})