feat(rate-limiting): Implement core rate limiting functionality with configuration, decision-making, metrics, middleware, and service registration
- Add RateLimitConfig for configuration management with YAML binding support. - Introduce RateLimitDecision to encapsulate the result of rate limit checks. - Implement RateLimitMetrics for OpenTelemetry metrics tracking. - Create RateLimitMiddleware for enforcing rate limits on incoming requests. - Develop RateLimitService to orchestrate instance and environment rate limit checks. - Add RateLimitServiceCollectionExtensions for dependency injection registration.
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
-- Notify Schema Migration 011: Partition deliveries Table
|
||||
-- Sprint: SPRINT_3422_0001_0001 - Time-Based Partitioning
|
||||
-- Task: 5.1 - Create partitioned notify.deliveries table
|
||||
-- Category: C (infrastructure change, requires maintenance window)
|
||||
--
|
||||
-- Purpose: Convert notify.deliveries to a partitioned table for improved
|
||||
-- query performance on time-range queries and easier data lifecycle management.
|
||||
--
|
||||
-- Partition strategy: Monthly by created_at
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ============================================================================
|
||||
-- Step 1: Create partitioned deliveries table
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notify.deliveries_partitioned (
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
tenant_id TEXT NOT NULL,
|
||||
channel_id UUID NOT NULL,
|
||||
rule_id UUID,
|
||||
template_id UUID,
|
||||
status notify.delivery_status NOT NULL DEFAULT 'pending',
|
||||
recipient TEXT NOT NULL,
|
||||
subject TEXT,
|
||||
body TEXT,
|
||||
event_type TEXT NOT NULL,
|
||||
event_payload JSONB NOT NULL DEFAULT '{}',
|
||||
attempt INT NOT NULL DEFAULT 0,
|
||||
max_attempts INT NOT NULL DEFAULT 3,
|
||||
next_retry_at TIMESTAMPTZ,
|
||||
error_message TEXT,
|
||||
external_id TEXT,
|
||||
correlation_id TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
queued_at TIMESTAMPTZ,
|
||||
sent_at TIMESTAMPTZ,
|
||||
delivered_at TIMESTAMPTZ,
|
||||
failed_at TIMESTAMPTZ,
|
||||
PRIMARY KEY (id, created_at)
|
||||
) PARTITION BY RANGE (created_at);
|
||||
|
||||
-- Note: Foreign keys cannot reference partitioned tables directly.
|
||||
-- Application-level integrity checks are used instead.
|
||||
|
||||
-- ============================================================================
|
||||
-- Step 2: Create initial partitions (past 3 months + 4 months ahead)
|
||||
-- ============================================================================
|
||||
|
||||
DO $$
|
||||
DECLARE
|
||||
v_start DATE;
|
||||
v_end DATE;
|
||||
v_partition_name TEXT;
|
||||
BEGIN
|
||||
-- Start from 3 months ago (shorter history for high-volume table)
|
||||
v_start := date_trunc('month', NOW() - INTERVAL '3 months')::DATE;
|
||||
|
||||
-- Create partitions until 4 months ahead
|
||||
WHILE v_start <= date_trunc('month', NOW() + INTERVAL '4 months')::DATE LOOP
|
||||
v_end := (v_start + INTERVAL '1 month')::DATE;
|
||||
v_partition_name := 'deliveries_' || to_char(v_start, 'YYYY_MM');
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_class c
|
||||
JOIN pg_namespace n ON c.relnamespace = n.oid
|
||||
WHERE n.nspname = 'notify' AND c.relname = v_partition_name
|
||||
) THEN
|
||||
EXECUTE format(
|
||||
'CREATE TABLE notify.%I PARTITION OF notify.deliveries_partitioned
|
||||
FOR VALUES FROM (%L) TO (%L)',
|
||||
v_partition_name, v_start, v_end
|
||||
);
|
||||
RAISE NOTICE 'Created partition notify.%', v_partition_name;
|
||||
END IF;
|
||||
|
||||
v_start := v_end;
|
||||
END LOOP;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- Create default partition for any data outside defined ranges
|
||||
CREATE TABLE IF NOT EXISTS notify.deliveries_default
|
||||
PARTITION OF notify.deliveries_partitioned DEFAULT;
|
||||
|
||||
-- ============================================================================
|
||||
-- Step 3: Create indexes on partitioned table
|
||||
-- ============================================================================
|
||||
|
||||
-- Tenant index
|
||||
CREATE INDEX IF NOT EXISTS ix_deliveries_part_tenant
|
||||
ON notify.deliveries_partitioned (tenant_id);
|
||||
|
||||
-- Status-based queries (most common for worker processing)
|
||||
CREATE INDEX IF NOT EXISTS ix_deliveries_part_status
|
||||
ON notify.deliveries_partitioned (tenant_id, status);
|
||||
|
||||
-- Pending deliveries for retry processing
|
||||
CREATE INDEX IF NOT EXISTS ix_deliveries_part_pending
|
||||
ON notify.deliveries_partitioned (status, next_retry_at)
|
||||
WHERE status IN ('pending', 'queued');
|
||||
|
||||
-- Channel-based queries
|
||||
CREATE INDEX IF NOT EXISTS ix_deliveries_part_channel
|
||||
ON notify.deliveries_partitioned (channel_id);
|
||||
|
||||
-- Correlation tracking
|
||||
CREATE INDEX IF NOT EXISTS ix_deliveries_part_correlation
|
||||
ON notify.deliveries_partitioned (correlation_id)
|
||||
WHERE correlation_id IS NOT NULL;
|
||||
|
||||
-- Time-range queries (tenant + created_at)
|
||||
CREATE INDEX IF NOT EXISTS ix_deliveries_part_created
|
||||
ON notify.deliveries_partitioned (tenant_id, created_at DESC);
|
||||
|
||||
-- BRIN index for efficient time-range scans
|
||||
CREATE INDEX IF NOT EXISTS ix_deliveries_part_created_brin
|
||||
ON notify.deliveries_partitioned USING BRIN (created_at)
|
||||
WITH (pages_per_range = 32);
|
||||
|
||||
-- External ID lookup (for webhook callbacks)
|
||||
CREATE INDEX IF NOT EXISTS ix_deliveries_part_external_id
|
||||
ON notify.deliveries_partitioned (external_id)
|
||||
WHERE external_id IS NOT NULL;
|
||||
|
||||
-- ============================================================================
|
||||
-- Step 4: Add partition to partition_mgmt tracking (if schema exists)
|
||||
-- ============================================================================
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'partition_mgmt') THEN
|
||||
INSERT INTO partition_mgmt.managed_tables (
|
||||
schema_name,
|
||||
table_name,
|
||||
partition_key,
|
||||
partition_type,
|
||||
retention_months,
|
||||
months_ahead,
|
||||
created_at
|
||||
) VALUES (
|
||||
'notify',
|
||||
'deliveries_partitioned',
|
||||
'created_at',
|
||||
'monthly',
|
||||
12, -- 1 year retention (high volume, short lifecycle)
|
||||
4, -- Create 4 months ahead
|
||||
NOW()
|
||||
) ON CONFLICT (schema_name, table_name) DO NOTHING;
|
||||
END IF;
|
||||
END
|
||||
$$;
|
||||
|
||||
-- ============================================================================
|
||||
-- Migration Notes (for DBA to execute during maintenance window)
|
||||
-- ============================================================================
|
||||
-- After this migration, to complete the table swap:
|
||||
--
|
||||
-- 1. Stop writes to notify.deliveries (pause notification worker)
|
||||
-- 2. Migrate existing data:
|
||||
-- INSERT INTO notify.deliveries_partitioned (
|
||||
-- id, tenant_id, channel_id, rule_id, template_id, status,
|
||||
-- recipient, subject, body, event_type, event_payload,
|
||||
-- attempt, max_attempts, next_retry_at, error_message,
|
||||
-- external_id, correlation_id, created_at, queued_at,
|
||||
-- sent_at, delivered_at, failed_at
|
||||
-- )
|
||||
-- SELECT id, tenant_id, channel_id, rule_id, template_id, status,
|
||||
-- recipient, subject, body, event_type, event_payload,
|
||||
-- attempt, max_attempts, next_retry_at, error_message,
|
||||
-- external_id, correlation_id, created_at, queued_at,
|
||||
-- sent_at, delivered_at, failed_at
|
||||
-- FROM notify.deliveries;
|
||||
-- 3. Rename tables:
|
||||
-- ALTER TABLE notify.deliveries RENAME TO deliveries_old;
|
||||
-- ALTER TABLE notify.deliveries_partitioned RENAME TO deliveries;
|
||||
-- 4. Drop old table after verification:
|
||||
-- DROP TABLE notify.deliveries_old;
|
||||
-- 5. Resume notification worker
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user