owpengram-server/deploy/migrations/20260901000005_suggested_post_lifecycle_retry.up.sql
2026-09-01 12:06:31 +03:00

94 lines
4.4 KiB
PL/PgSQL

-- Isolate poisoned suggested-post lifecycle rows from the global due queue.
-- A failed aggregate remains durable and retryable, but receives a bounded
-- backoff so it cannot monopolize every one-second dispatcher pass.
ALTER TABLE public.suggested_post_approvals
ADD COLUMN lifecycle_attempts integer NOT NULL DEFAULT 0,
ADD COLUMN next_attempt_at integer NOT NULL DEFAULT 0,
ADD COLUMN last_lifecycle_error text NOT NULL DEFAULT '';
ALTER TABLE public.suggested_post_approvals
ADD CONSTRAINT suggested_post_approvals_retry_shape_check CHECK (
lifecycle_attempts BETWEEN 0 AND 1000000 AND
next_attempt_at >= 0 AND
octet_length(last_lifecycle_error) <= 4096
) NOT VALID;
ALTER TABLE public.suggested_post_approvals
VALIDATE CONSTRAINT suggested_post_approvals_retry_shape_check;
-- next_attempt_at is the queue's next eligible time, not merely a retry flag.
-- Healthy active rows point at their publish/settlement deadline; only an
-- explicit message-deletion wakeup or an already-due row is <= worker now.
UPDATE public.suggested_post_approvals
SET next_attempt_at = CASE state
WHEN 'scheduled' THEN schedule_date
WHEN 'published' THEN settlement_due
ELSE 0
END
WHERE state IN ('scheduled','published');
CREATE INDEX suggested_post_approvals_retry_idx
ON public.suggested_post_approvals(next_attempt_at,monoforum_id,suggestion_message_id)
WHERE state IN ('scheduled','published');
CREATE INDEX suggested_post_approvals_published_message_idx
ON public.suggested_post_approvals(parent_channel_id,published_message_id)
INCLUDE (monoforum_id,suggestion_message_id,next_attempt_at)
WHERE state='published';
-- A separate wakeup table avoids a message-row -> approval-row trigger lock
-- that would invert the worker's approval-row -> message-row order. It only
-- contains active suggested-post aggregates and is drained by atomic claim.
CREATE TABLE public.suggested_post_lifecycle_wakeups (
monoforum_id bigint NOT NULL,
suggestion_message_id integer NOT NULL,
created_at integer NOT NULL,
CONSTRAINT suggested_post_lifecycle_wakeups_pkey
PRIMARY KEY (monoforum_id,suggestion_message_id),
CONSTRAINT suggested_post_lifecycle_wakeups_shape_check
CHECK (monoforum_id>0 AND suggestion_message_id>0 AND created_at>=0)
);
CREATE INDEX suggested_post_lifecycle_wakeups_due_idx
ON public.suggested_post_lifecycle_wakeups(created_at,monoforum_id,suggestion_message_id);
-- Preserve deletions that predate this migration without making the recurring
-- worker rescan global channel tombstone history.
INSERT INTO public.suggested_post_lifecycle_wakeups(monoforum_id,suggestion_message_id,created_at)
SELECT a.monoforum_id,a.suggestion_message_id,0
FROM public.suggested_post_approvals a
WHERE (a.state='scheduled' AND EXISTS (
SELECT 1 FROM public.channel_messages m
WHERE m.channel_id=a.monoforum_id AND m.id=a.suggestion_message_id AND m.deleted))
OR (a.state='published' AND EXISTS (
SELECT 1 FROM public.channel_messages m
WHERE m.channel_id=a.parent_channel_id AND m.id=a.published_message_id AND m.deleted))
ON CONFLICT DO NOTHING;
-- Deletion is a durable lifecycle wakeup. Drive the lookup from the exact
-- message being tombstoned and only insert a small queue fact; never rescan
-- global channel tombstones from the one-second worker.
CREATE OR REPLACE FUNCTION public.telesrv_wake_suggested_post_lifecycle_on_delete()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
INSERT INTO public.suggested_post_lifecycle_wakeups(monoforum_id,suggestion_message_id,created_at)
SELECT monoforum_id,suggestion_message_id,EXTRACT(EPOCH FROM clock_timestamp())::integer
FROM public.suggested_post_approvals
WHERE state='scheduled' AND monoforum_id=NEW.channel_id AND suggestion_message_id=NEW.id
ON CONFLICT DO NOTHING;
INSERT INTO public.suggested_post_lifecycle_wakeups(monoforum_id,suggestion_message_id,created_at)
SELECT monoforum_id,suggestion_message_id,EXTRACT(EPOCH FROM clock_timestamp())::integer
FROM public.suggested_post_approvals
WHERE state='published' AND parent_channel_id=NEW.channel_id AND published_message_id=NEW.id
ON CONFLICT DO NOTHING;
RETURN NEW;
END
$$;
CREATE TRIGGER channel_messages_suggested_post_lifecycle_wakeup
AFTER UPDATE OF deleted ON public.channel_messages
FOR EACH ROW
WHEN (NEW.deleted AND NOT OLD.deleted)
EXECUTE FUNCTION public.telesrv_wake_suggested_post_lifecycle_on_delete();