mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 09:31:13 -05:00
Implement critical fixes
Approve and implement Phase 1 critical fixes including CORS, RPC rollback, idempotency, timeouts, and deadlock retry.
This commit is contained in:
@@ -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'
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user