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)
61 lines
2.3 KiB
SQL
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");
|