fix(import): raise statement_timeout on import_sie_journal_entries to 290s (#1101)
A production Fortnox migration failed with "SIE-verifikationer kunde inte importeras atomiskt: canceling statement due to statement timeout": the atomic import RPC (20260712150000) inherits the 8s authenticator statement_timeout, so any real-world multi-year SIE file exceeds it and the whole import is cancelled and rolled back. Same failure class as 20260629160100 (replace_sie_import / undo_sie_import); same fix, a function-scoped statement_timeout of 290s sitting under the calling routes' maxDuration = 300 ceiling. Migration 20260721144311 is already applied to prod (proconfig verified carrying statement_timeout=290s); the committed file is byte-identical under the same version. The new pg-real ratchet pins the config on all three SIE RPCs because CREATE OR REPLACE FUNCTION silently drops ALTER FUNCTION settings. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
3cb5ae7716
commit
aa6d42a167
@@ -245,3 +245,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. 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.
|
||||
|
||||
@@ -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';
|
||||
@@ -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')
|
||||
},
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user