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