Files
cbd420/RACE_CONDITION_FIX.md
2025-12-21 08:43:43 +01:00

149 lines
5.2 KiB
Markdown

# Race Condition Fix - Implementation Summary
## Problem
When multiple buyers attempt to purchase the last available units simultaneously, a race condition occurs where inventory can be oversold.
## Solution
Implemented a 10-minute reservation system using the `pending_orders` table to temporarily hold inventory during checkout.
## Changes Made
### 1. Database Migration
- **File**: `migrations/add_expires_at_to_pending_orders.sql`
- Added `expires_at` column to `pending_orders` table
- Added index on `expires_at` for efficient cleanup queries
**To apply the migration:**
```sql
-- Run the migration file
SOURCE migrations/add_expires_at_to_pending_orders.sql;
```
### 2. Create Invoice Endpoint (`app/api/payments/create-invoice/route.ts`)
- **Atomic Inventory Reservation**: Uses database transactions to atomically check and reserve inventory
- **Pending Orders Check**: Includes non-expired pending orders when calculating available inventory
- **Expiration Time**: Sets `expires_at` to 10 minutes from creation
- **NOWPayments Integration**: Sets `invoice_timeout` to 600 seconds (10 minutes) when creating invoice
### 3. Active Drop Endpoint (`app/api/drops/active/route.ts`)
- **Fill Calculation**: Includes non-expired pending orders in fill calculation
- **UI Display**: Progress bar now shows total reserved inventory (sales + pending orders)
### 4. Cleanup Endpoint (`app/api/payments/cleanup-expired/route.ts`)
- **Automatic Cleanup**: Removes expired pending orders
- **NOWPayments Status Check**: Verifies payment status before cleanup
- **Monitoring**: GET endpoint to check count of expired orders
### 5. UI Updates (`app/components/Drop.tsx`)
- **Visual Indicator**: Added note that items are held for 10 minutes during checkout
- **Progress Bar**: Automatically reflects reserved inventory (including pending orders)
## Setup Instructions
### 1. Run Database Migration
```bash
mysql -u your_user -p your_database < migrations/add_expires_at_to_pending_orders.sql
```
### 2. Set Up Cleanup Job
The cleanup endpoint should be called periodically (recommended: every 1-2 minutes) to remove expired pending orders.
#### Option A: Cron Job (Linux/Mac)
```bash
# Add to crontab (crontab -e)
*/2 * * * * curl -X POST https://your-domain.com/api/payments/cleanup-expired -H "Authorization: Bearer YOUR_CLEANUP_TOKEN"
```
#### Option B: Vercel Cron (if using Vercel)
Add to `vercel.json`:
```json
{
"crons": [{
"path": "/api/payments/cleanup-expired",
"schedule": "*/2 * * * *"
}]
}
```
#### Option C: Node.js Cron Library
```javascript
const cron = require('node-cron');
const fetch = require('node-fetch');
cron.schedule('*/2 * * * *', async () => {
await fetch('http://localhost:3000/api/payments/cleanup-expired', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.CLEANUP_API_TOKEN}`
}
});
});
```
### 3. Environment Variables
Add to your `.env` file:
```
# Required: URL for external IPN handler service
IPN_CALLBACK_URL=http://your-ipn-service.com/api/payments/ipn-callback
# Optional: Token for cleanup endpoint security
CLEANUP_API_TOKEN=your-secure-random-token
```
**Note**: IPN callbacks are handled by an external service. The `IPN_CALLBACK_URL` must point to your external IPN handler that processes payment confirmations and creates sales. See `IPN_INTEGRATION_README.md` for integration details.
## How It Works
1. **Purchase Initiation**: When a buyer clicks "Join Drop":
- System checks available inventory (sales + non-expired pending orders)
- If available, creates pending order with 10-minute expiration
- Creates NOWPayments invoice with 10-minute timeout
- Inventory is now "on hold" for 10 minutes
2. **During Checkout**:
- Progress bar shows reserved inventory (including pending orders)
- Other buyers see reduced availability
- If buyer completes payment within 10 minutes → sale is created
- If buyer doesn't complete payment within 10 minutes → order expires
3. **Expiration**:
- Cleanup job runs every 1-2 minutes
- Expired pending orders are removed
- Inventory becomes available again
- NOWPayments invoice expires on their end
4. **Payment Confirmation** (handled by external IPN service):
- External IPN handler receives payment notification from NOWPayments
- Checks if pending order is expired
- Validates final inventory availability
- Creates sale record if valid
- Deletes pending order
## Testing
1. **Test Race Condition**:
- Open two browser windows
- Try to purchase the last available unit simultaneously
- Only one should succeed
- The other should see "Not enough inventory remaining"
2. **Test Expiration**:
- Create a pending order
- Wait 10+ minutes
- Run cleanup endpoint manually
- Verify order is removed
3. **Test Payment Flow**:
- Create pending order
- Complete payment within 10 minutes
- Verify sale is created
- Verify pending order is deleted
## Notes
- Pending orders are automatically included in inventory calculations
- The 10-minute hold prevents overselling while giving buyers time to complete payment
- NOWPayments invoices also expire after 10 minutes
- Cleanup job should run frequently (every 1-2 minutes) to free up inventory quickly