ceo-api/drizzle/0007_saga_manager.sql
valentinbvro 765ec37c3a feat: Saga Manager (Sprint 4-6)
Motor generic pentru procese cu mai multi pasi care nu incap intr-o
tranzactie. Garantia nu e atomicitatea, ci compensarea in ordine inversa.

- saga-registry: definitii versionate in cod, backoff exponential plafonat
- saga-runner: corelator outbox->instanta, step runner, timeout, retry,
  compensare; revendicare cu FOR UPDATE SKIP LOCKED ca doua procese sa nu
  avanseze aceeasi saga simultan
- idempotenta la pornire prin index unic (saga, versiune, trigger_event),
  nu prin SELECT-apoi-INSERT care ar avea race
- compensarea are propriul retry: o compensare esuata lasa sistemul mai rau
  decat esecul original
- monitor /v1/sagas cu instante blocate si retry manual care NU sare pasi
- prima saga reala: imbogatire research brief, fara pas AI (un apel AI
  automat per brief ar schimba profilul de cost)
2026-07-29 15:02:56 +02:00

61 lines
2.3 KiB
SQL

-- Saga Manager (Sprint 4-6). Coordoneaza procese cu mai multi pasi care nu
-- incap intr-o tranzactie. Garantia nu e atomicitatea, ci compensarea in
-- ordine inversa a pasilor deja executati.
DO $$ BEGIN
CREATE TYPE "saga_status" AS ENUM
('RUNNING','COMPLETED','COMPENSATING','COMPENSATED','FAILED','TIMED_OUT');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
DO $$ BEGIN
CREATE TYPE "saga_step_status" AS ENUM
('PENDING','RUNNING','COMPLETED','FAILED','COMPENSATED','SKIPPED');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
CREATE TABLE IF NOT EXISTS "saga_instances" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"tenant_id" uuid NOT NULL,
"workspace_id" uuid,
"saga_name" text NOT NULL,
"saga_version" integer NOT NULL,
"status" "saga_status" DEFAULT 'RUNNING' NOT NULL,
"current_step" integer DEFAULT 0 NOT NULL,
"trigger_event_id" uuid NOT NULL,
"correlation_id" uuid,
"context" jsonb DEFAULT '{}'::jsonb NOT NULL,
"attempt" integer DEFAULT 0 NOT NULL,
"next_attempt_at" timestamp DEFAULT now() NOT NULL,
"timeout_at" timestamp,
"last_error" text,
"locked_until" timestamp,
"created_at" timestamp DEFAULT now() NOT NULL,
"updated_at" timestamp DEFAULT now() NOT NULL,
"completed_at" timestamp
);
-- Un eveniment porneste o singura instanta dintr-o saga data. Aceasta
-- constrangere e ce face pornirea idempotenta, nu o verificare in cod.
CREATE UNIQUE INDEX IF NOT EXISTS "saga_trigger_uq"
ON "saga_instances" ("saga_name","saga_version","trigger_event_id");
CREATE INDEX IF NOT EXISTS "saga_due_idx"
ON "saga_instances" ("status","next_attempt_at");
CREATE INDEX IF NOT EXISTS "saga_tenant_idx"
ON "saga_instances" ("tenant_id","created_at");
CREATE TABLE IF NOT EXISTS "saga_steps" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"saga_instance_id" uuid NOT NULL,
"step_name" text NOT NULL,
"sequence" integer NOT NULL,
"status" "saga_step_status" DEFAULT 'PENDING' NOT NULL,
"attempts" integer DEFAULT 0 NOT NULL,
"output" jsonb,
"last_error" text,
"started_at" timestamp,
"completed_at" timestamp,
"compensated_at" timestamp
);
-- Un pas apare o singura data per instanta: baza idempotentei la avans.
CREATE UNIQUE INDEX IF NOT EXISTS "saga_step_uq"
ON "saga_steps" ("saga_instance_id","sequence");