feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View file

@ -0,0 +1,181 @@
BEGIN;
-- Transactional producers enqueue pending rows even during provider bursts so
-- business state never rolls back merely because notification capacity is
-- exhausted. Rate limiting therefore belongs to the system dispatch lease,
-- immediately before a worker can read registrations or contact FCM.
CREATE TABLE public.push_dispatch_claim_events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
attempt_id uuid NOT NULL,
dispatch_lease_token uuid NOT NULL UNIQUE,
claimed_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_push_dispatch_claim_events_claimed_at
ON public.push_dispatch_claim_events(claimed_at);
ALTER TABLE public.push_dispatch_claim_events ENABLE ROW LEVEL SECURITY;
REVOKE ALL ON TABLE public.push_dispatch_claim_events
FROM PUBLIC, anon, authenticated;
GRANT ALL ON TABLE public.push_dispatch_claim_events TO service_role;
CREATE OR REPLACE FUNCTION public.enforce_system_push_claim_rate()
RETURNS trigger
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
BEGIN
IF NEW.actor_kind <> 'system'
OR NEW.status <> 'processing'
OR NEW.dispatch_lease_token IS NULL THEN
RETURN NEW;
END IF;
IF TG_OP = 'UPDATE'
AND OLD.dispatch_lease_token IS NOT DISTINCT FROM NEW.dispatch_lease_token THEN
RETURN NEW;
END IF;
-- The same lock is acquired by reserve_system_push_dispatch and the due
-- worker. Reusing it keeps direct dispatch and drain claims in one budget.
PERFORM pg_advisory_xact_lock(hashtextextended('system-push-global', 73050));
DELETE FROM public.push_dispatch_claim_events
WHERE claimed_at < now() - interval '2 days';
IF (
SELECT count(*)
FROM public.push_dispatch_claim_events
WHERE claimed_at > now() - interval '1 minute'
) >= 500 OR (
SELECT count(*)
FROM public.push_dispatch_claim_events
WHERE claimed_at > now() - interval '24 hours'
) >= 10000 THEN
RAISE EXCEPTION 'push_rate_limited' USING ERRCODE = '54000';
END IF;
INSERT INTO public.push_dispatch_claim_events (
attempt_id, dispatch_lease_token
) VALUES (
NEW.id, NEW.dispatch_lease_token
);
RETURN NEW;
END;
$$;
CREATE TRIGGER enforce_system_push_claim_rate_on_insert
BEFORE INSERT ON public.push_dispatch_attempts
FOR EACH ROW EXECUTE FUNCTION public.enforce_system_push_claim_rate();
CREATE TRIGGER enforce_system_push_claim_rate_on_update
BEFORE UPDATE OF status, dispatch_lease_token ON public.push_dispatch_attempts
FOR EACH ROW EXECUTE FUNCTION public.enforce_system_push_claim_rate();
CREATE OR REPLACE FUNCTION public.claim_due_push_dispatches(batch_limit integer DEFAULT 50)
RETURNS TABLE (
attempt_id uuid,
event_type text,
resource_id uuid,
dispatch_lease_token uuid
)
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = ''
AS $$
DECLARE
minute_slots integer;
daily_slots integer;
system_slots integer;
BEGIN
IF auth.role() <> 'service_role' THEN
RAISE EXCEPTION 'service_role_required' USING ERRCODE = '42501';
END IF;
IF batch_limit IS NULL OR batch_limit NOT BETWEEN 1 AND 100 THEN
RAISE EXCEPTION 'invalid_batch_limit' USING ERRCODE = '22023';
END IF;
-- Keep capacity calculation, row claims and claim-ledger inserts in the same
-- transaction. At capacity, system rows are not selected or mutated, so
-- their pending status and next_retry_at remain intact for a later drain.
PERFORM pg_advisory_xact_lock(hashtextextended('system-push-global', 73050));
DELETE FROM public.push_dispatch_claim_events
WHERE claimed_at < now() - interval '2 days';
SELECT greatest(0, 500 - count(*))::integer
INTO minute_slots
FROM public.push_dispatch_claim_events
WHERE claimed_at > now() - interval '1 minute';
SELECT greatest(0, 10000 - count(*))::integer
INTO daily_slots
FROM public.push_dispatch_claim_events
WHERE claimed_at > now() - interval '24 hours';
system_slots := least(batch_limit, minute_slots, daily_slots);
RETURN QUERY
WITH user_due AS (
SELECT
attempt.id,
coalesce(attempt.next_retry_at, attempt.created_at) AS due_at
FROM public.push_dispatch_attempts AS attempt
WHERE attempt.actor_kind = 'user'
AND (
(attempt.status = 'pending'
AND coalesce(attempt.next_retry_at, attempt.created_at) <= now())
OR (attempt.status = 'processing' AND attempt.lease_expires_at <= now())
)
ORDER BY due_at, attempt.id
FOR UPDATE SKIP LOCKED
LIMIT batch_limit
), system_due AS (
SELECT
attempt.id,
coalesce(attempt.next_retry_at, attempt.created_at) AS due_at
FROM public.push_dispatch_attempts AS attempt
WHERE attempt.actor_kind = 'system'
AND (
(attempt.status = 'pending'
AND coalesce(attempt.next_retry_at, attempt.created_at) <= now())
OR (attempt.status = 'processing' AND attempt.lease_expires_at <= now())
)
ORDER BY due_at, attempt.id
FOR UPDATE SKIP LOCKED
LIMIT system_slots
), due AS (
SELECT candidate.id
FROM (
SELECT * FROM user_due
UNION ALL
SELECT * FROM system_due
) AS candidate
ORDER BY candidate.due_at, candidate.id
LIMIT batch_limit
), claimed AS (
UPDATE public.push_dispatch_attempts AS attempt
SET status = 'processing',
attempt_count = attempt.attempt_count + 1,
dispatch_lease_token = gen_random_uuid(),
lease_expires_at = now() + interval '2 minutes',
next_retry_at = NULL,
updated_at = now()
FROM due
WHERE attempt.id = due.id
RETURNING attempt.*
)
SELECT claimed.id, claimed.event_type, claimed.resource_id,
claimed.dispatch_lease_token
FROM claimed
ORDER BY claimed.created_at, claimed.id;
END;
$$;
REVOKE ALL ON FUNCTION public.enforce_system_push_claim_rate()
FROM PUBLIC, anon, authenticated;
REVOKE ALL ON FUNCTION public.claim_due_push_dispatches(integer)
FROM PUBLIC, anon, authenticated;
GRANT EXECUTE ON FUNCTION public.claim_due_push_dispatches(integer)
TO service_role;
COMMIT;