Implement critical fixes

Approve and implement Phase 1 critical fixes including CORS, RPC rollback, idempotency, timeouts, and deadlock retry.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-06 21:51:39 +00:00
parent 7cc4e4ff17
commit 24dbf5bbba
4 changed files with 756 additions and 19 deletions

View File

@@ -1,5 +1,6 @@
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.57.4';
import { corsHeaders } from './cors.ts';
const SUPABASE_URL = Deno.env.get('SUPABASE_URL') || 'https://api.thrillwiki.com';
const SUPABASE_ANON_KEY = Deno.env.get('SUPABASE_ANON_KEY')!;
@@ -11,6 +12,14 @@ interface ApprovalRequest {
}
serve(async (req) => {
// Handle CORS preflight requests
if (req.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: corsHeaders
});
}
// Generate request ID for tracking
const requestId = crypto.randomUUID();
@@ -20,7 +29,13 @@ serve(async (req) => {
if (!authHeader) {
return new Response(
JSON.stringify({ error: 'Missing Authorization header' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
{
status: 401,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
}
);
}
@@ -32,7 +47,13 @@ serve(async (req) => {
if (authError || !user) {
return new Response(
JSON.stringify({ error: 'Unauthorized' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
{
status: 401,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
}
);
}
@@ -45,7 +66,13 @@ serve(async (req) => {
if (!submissionId || !itemIds || itemIds.length === 0) {
return new Response(
JSON.stringify({ error: 'Missing required fields: submissionId, itemIds' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
{
status: 400,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
}
);
}
@@ -63,6 +90,7 @@ serve(async (req) => {
{
status: 200,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Cache-Status': 'HIT'
}
@@ -81,7 +109,13 @@ serve(async (req) => {
console.error(`[${requestId}] Submission not found:`, submissionError);
return new Response(
JSON.stringify({ error: 'Submission not found' }),
{ status: 404, headers: { 'Content-Type': 'application/json' } }
{
status: 404,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
}
);
}
@@ -90,7 +124,13 @@ serve(async (req) => {
console.error(`[${requestId}] Submission locked by another moderator`);
return new Response(
JSON.stringify({ error: 'Submission is locked by another moderator' }),
{ status: 409, headers: { 'Content-Type': 'application/json' } }
{
status: 409,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
}
);
}
@@ -98,7 +138,13 @@ serve(async (req) => {
console.error(`[${requestId}] Invalid submission status: ${submission.status}`);
return new Response(
JSON.stringify({ error: 'Submission already processed' }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
{
status: 400,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
}
);
}
@@ -115,18 +161,51 @@ serve(async (req) => {
console.log(`[${requestId}] Calling process_approval_transaction RPC`);
// ============================================================================
// STEP 7: Call RPC function - entire approval in single atomic transaction
// STEP 7: Call RPC function with deadlock retry logic
// ============================================================================
const { data: result, error: rpcError } = await supabase.rpc(
'process_approval_transaction',
{
p_submission_id: submissionId,
p_item_ids: itemIds,
p_moderator_id: user.id,
p_submitter_id: submission.user_id,
p_request_id: requestId
let retryCount = 0;
const MAX_DEADLOCK_RETRIES = 3;
let result: any = null;
let rpcError: any = null;
while (retryCount <= MAX_DEADLOCK_RETRIES) {
const { data, error } = await supabase.rpc(
'process_approval_transaction',
{
p_submission_id: submissionId,
p_item_ids: itemIds,
p_moderator_id: user.id,
p_submitter_id: submission.user_id,
p_request_id: requestId,
p_idempotency_key: idempotencyKey
}
);
result = data;
rpcError = error;
if (!rpcError) {
// Success!
break;
}
);
// Check for deadlock (40P01) or serialization failure (40001)
if (rpcError.code === '40P01' || rpcError.code === '40001') {
retryCount++;
if (retryCount > MAX_DEADLOCK_RETRIES) {
console.error(`[${requestId}] Max deadlock retries exceeded`);
break;
}
const backoffMs = 100 * Math.pow(2, retryCount);
console.log(`[${requestId}] Deadlock detected, retrying in ${backoffMs}ms (attempt ${retryCount}/${MAX_DEADLOCK_RETRIES})`);
await new Promise(r => setTimeout(r, backoffMs));
continue;
}
// Non-retryable error, break immediately
break;
}
if (rpcError) {
// Transaction failed - EVERYTHING rolled back automatically by PostgreSQL
@@ -146,9 +225,16 @@ serve(async (req) => {
JSON.stringify({
error: 'Approval transaction failed',
message: rpcError.message,
details: rpcError.details
details: rpcError.details,
retries: retryCount
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
{
status: 500,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
}
);
}
@@ -169,6 +255,7 @@ serve(async (req) => {
{
status: 200,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-Id': requestId
}
@@ -182,7 +269,13 @@ serve(async (req) => {
error: 'Internal server error',
message: error instanceof Error ? error.message : 'Unknown error'
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
{
status: 500,
headers: {
...corsHeaders,
'Content-Type': 'application/json'
}
}
);
}
});