Merge pull request #40 from ArnasDon/fix/automations-atomic-execution-counter

fix(automations): atomic execution_count via SQL function (migration 007)
This commit is contained in:
Arnas Donauskas 2026-04-17 21:56:37 +03:00 committed by GitHub
commit 348dbb7d6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 45 additions and 7 deletions

View File

@ -159,13 +159,16 @@ async function executeAutomation(automation: Automation, input: DispatchInput) {
triggerEvent: input.triggerType,
})
await db
.from('automations')
.update({
execution_count: (automation.execution_count ?? 0) + 1,
last_executed_at: new Date().toISOString(),
})
.eq('id', automation.id)
// Atomic counter update via the SQL function from migration 007.
// Doing this with a client-side read-modify-write raced when the
// same automation fired for two contacts simultaneously — both
// would read N and both write N+1, losing one count permanently.
const { error: rpcErr } = await db.rpc('increment_automation_execution_count', {
p_automation_id: automation.id,
})
if (rpcErr) {
console.error('[automations] increment counter failed:', rpcErr)
}
}
interface ExecuteArgs {

View File

@ -0,0 +1,35 @@
-- ============================================================
-- 007_automations_increment_counter.sql
--
-- Atomic increment of automations.execution_count + refresh of
-- last_executed_at. Called via PostgREST RPC from the engine.
--
-- Before this, the engine did a read-modify-write:
-- UPDATE automations SET execution_count = <cached + 1> WHERE id = ...
-- so two concurrent dispatches (e.g. the same automation firing for
-- two different contacts in the same second) could both read N and
-- both write N+1, permanently losing one count.
--
-- Idempotent — safe to re-run.
-- ============================================================
CREATE OR REPLACE FUNCTION increment_automation_execution_count(p_automation_id UUID)
RETURNS VOID
LANGUAGE sql
SECURITY DEFINER
SET search_path = public
AS $$
UPDATE automations
SET
execution_count = execution_count + 1,
last_executed_at = NOW()
WHERE id = p_automation_id;
$$;
-- Only the service role needs to call this (engine uses the
-- service-role client). Explicitly lock anon / authenticated out so
-- an authenticated user can't juice someone else's counter via RPC.
REVOKE ALL ON FUNCTION increment_automation_execution_count(UUID) FROM PUBLIC;
REVOKE ALL ON FUNCTION increment_automation_execution_count(UUID) FROM anon;
REVOKE ALL ON FUNCTION increment_automation_execution_count(UUID) FROM authenticated;
GRANT EXECUTE ON FUNCTION increment_automation_execution_count(UUID) TO service_role;