diff --git a/supabase/functions/_shared/retryHelper.ts b/supabase/functions/_shared/retryHelper.ts index 3f256c9d..e0daf56d 100644 --- a/supabase/functions/_shared/retryHelper.ts +++ b/supabase/functions/_shared/retryHelper.ts @@ -30,7 +30,7 @@ export function isRetryableError(error: unknown): boolean { // HTTP status codes that should be retried if (error && typeof error === 'object') { - const httpError = error as { status?: number }; + const httpError = error as { status?: number; code?: string }; // Rate limiting if (httpError.status === 429) return true; @@ -47,6 +47,26 @@ export function isRetryableError(error: unknown): boolean { return false; } +/** + * Check if error is a database deadlock or serialization failure + */ +export function isDeadlockError(error: unknown): boolean { + if (!error || typeof error !== 'object') return false; + + const dbError = error as { code?: string; message?: string }; + + // PostgreSQL deadlock error codes + if (dbError.code === '40P01') return true; // deadlock_detected + if (dbError.code === '40001') return true; // serialization_failure + + // Check message for deadlock indicators + const message = dbError.message?.toLowerCase() || ''; + if (message.includes('deadlock')) return true; + if (message.includes('could not serialize')) return true; + + return false; +} + /** * Calculate exponential backoff delay with optional jitter */ diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index 95a44493..36e3d290 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -4,6 +4,7 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { createErrorResponse } from "../_shared/errorSanitizer.ts"; import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; import { rateLimiters, withRateLimit } from "../_shared/rateLimiter.ts"; +import { withEdgeRetry, isDeadlockError } from "../_shared/retryHelper.ts"; const corsHeaders = { 'Access-Control-Allow-Origin': '*', @@ -1071,28 +1072,31 @@ serve(withRateLimit(async (req) => { }> = []; // Process items in order - for (const item of sortedItems) { - edgeLogger.info('Processing item', { action: 'approval_process_item', itemId: item.id, itemType: item.item_type }); - - // Extract data from relational tables based on item_type (OUTSIDE try-catch) - let itemData: any; - switch (item.item_type) { - case 'park': - itemData = { - ...(item as any).park_submission, - // Merge temp refs for this item - ...(tempRefsByItemId.get(item.id) || {}) - }; - // DEBUG: Log what columns are present - edgeLogger.info('Park item data loaded', { - action: 'approval_park_data_debug', - itemId: item.id, - hasLocationId: !!itemData.location_id, - parkSubmissionId: itemData.id, - parkSubmissionKeys: Object.keys((item as any).park_submission || {}), - requestId: tracking.requestId - }); - break; + // Wrap entire approval loop in deadlock retry logic + await withEdgeRetry( + async () => { + for (const item of sortedItems) { + edgeLogger.info('Processing item', { action: 'approval_process_item', itemId: item.id, itemType: item.item_type }); + + // Extract data from relational tables based on item_type (OUTSIDE try-catch) + let itemData: any; + switch (item.item_type) { + case 'park': + itemData = { + ...(item as any).park_submission, + // Merge temp refs for this item + ...(tempRefsByItemId.get(item.id) || {}) + }; + // DEBUG: Log what columns are present + edgeLogger.info('Park item data loaded', { + action: 'approval_park_data_debug', + itemId: item.id, + hasLocationId: !!itemData.location_id, + parkSubmissionId: itemData.id, + parkSubmissionKeys: Object.keys((item as any).park_submission || {}), + requestId: tracking.requestId + }); + break; case 'ride': itemData = { ...(item as any).ride_submission, @@ -1583,6 +1587,18 @@ serve(withRateLimit(async (req) => { if (updateError) { edgeLogger.error('Failed to update submission status', { action: 'approval_update_status', error: updateError.message, requestId: tracking.requestId }); } + }, + { + maxAttempts: 3, + baseDelay: 500, + maxDelay: 2000, + backoffMultiplier: 2, + jitter: true, + shouldRetry: isDeadlockError + }, + tracking.requestId, + 'approval_transaction' + ); // Log audit trail for submission action try {