- Implement optimistic locking for deposit balance updates - Prevent race conditions in concurrent deposit requests - Add automatic retry with exponential backoff (max 3 attempts) - Add version column to user_deposits table - Improve type safety across codebase - Add explicit types for Namecheap API responses - Add typed function arguments (ManageDepositArgs, etc.) - Remove `any` types from deposit-agent and tool files - Add reconciliation job for balance integrity verification - Compare user_deposits.balance vs SUM(confirmed transactions) - Alert admin on discrepancy detection - Set up test environment with Vitest + Miniflare - Add 50+ test cases for deposit system - Add helper functions for test data creation - Update documentation - Add migration guide for version columns - Document optimistic locking patterns Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
267 lines
7.9 KiB
Markdown
267 lines
7.9 KiB
Markdown
# Optimistic Locking Implementation Summary
|
|
|
|
## Overview
|
|
|
|
Implemented Optimistic Locking pattern to prevent data inconsistencies in deposit operations where D1 `batch()` is not a true transaction.
|
|
|
|
## Files Created
|
|
|
|
### 1. Database Migration
|
|
**File:** `migrations/002_add_version_columns.sql`
|
|
- Adds `version INTEGER NOT NULL DEFAULT 1` column to `user_deposits` table
|
|
- Creates index `idx_deposits_user_version` on `(user_id, version)`
|
|
- Includes rollback instructions in comments
|
|
|
|
### 2. Optimistic Lock Utility
|
|
**File:** `src/utils/optimistic-lock.ts`
|
|
- `OptimisticLockError` class for version conflict detection
|
|
- `executeWithOptimisticLock<T>()` function with:
|
|
- Automatic retry with exponential backoff (100ms, 200ms, 400ms)
|
|
- Max 3 attempts
|
|
- Structured logging for debugging
|
|
- Generic type support
|
|
|
|
### 3. Reconciliation Job
|
|
**File:** `src/utils/reconciliation.ts`
|
|
- `reconcileDeposits()` function to verify data integrity
|
|
- Compares `user_deposits.balance` vs SUM(confirmed transactions)
|
|
- Detects and logs discrepancies
|
|
- `formatReconciliationReport()` for admin notifications
|
|
|
|
## Files Modified
|
|
|
|
### 1. deposit-agent.ts
|
|
**Changes:**
|
|
- Added import of `executeWithOptimisticLock` and `OptimisticLockError`
|
|
- Updated `request_deposit` (auto_matched case) - lines 83-150
|
|
- Wrapped balance update in optimistic lock
|
|
- Version check before UPDATE
|
|
- Automatic retry on version conflict
|
|
- Updated `confirm_deposit` - lines 302-358
|
|
- Same optimistic locking pattern
|
|
- Transaction status update + balance increase
|
|
|
|
**Pattern Used:**
|
|
```typescript
|
|
try {
|
|
await executeWithOptimisticLock(db, async (attempt) => {
|
|
// Get current version
|
|
const current = await db.prepare(
|
|
'SELECT balance, version FROM user_deposits WHERE user_id = ?'
|
|
).bind(userId).first<{ balance: number; version: number }>();
|
|
|
|
// Update with version check
|
|
const result = await db.prepare(
|
|
'UPDATE user_deposits SET balance = balance + ?, version = version + 1 WHERE user_id = ? AND version = ?'
|
|
).bind(amount, userId, current.version).run();
|
|
|
|
if (!result.success || result.meta.changes === 0) {
|
|
throw new OptimisticLockError('Version mismatch');
|
|
}
|
|
|
|
return result;
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof OptimisticLockError) {
|
|
logger.warn('동시성 충돌 감지', { userId, amount });
|
|
throw new Error('처리 중 오류가 발생했습니다. 잠시 후 다시 시도해주세요.');
|
|
}
|
|
throw error;
|
|
}
|
|
```
|
|
|
|
### 2. index.ts
|
|
**Changes:**
|
|
- Added import of `reconcileDeposits` and `formatReconciliationReport`
|
|
- Added reconciliation job to `scheduled()` handler (lines 234-256)
|
|
- Runs after expiry cleanup
|
|
- Sends admin notification if inconsistencies found
|
|
- Graceful error handling (doesn't break cron)
|
|
|
|
### 3. schema.sql
|
|
**Changes:**
|
|
- Added `version INTEGER NOT NULL DEFAULT 1` column to `user_deposits` table
|
|
- Added index `idx_deposits_user_version ON user_deposits(user_id, version)`
|
|
|
|
### 4. CLAUDE.md
|
|
**Changes:**
|
|
- Added "Transaction Isolation & Optimistic Locking" section under "Deposit System"
|
|
- Documents problem, solution, implementation details
|
|
- Includes migration commands and verification steps
|
|
- Provides concurrency scenario example
|
|
|
|
## How It Works
|
|
|
|
### Optimistic Locking Flow
|
|
|
|
```
|
|
1. Request arrives (deposit or confirm)
|
|
↓
|
|
2. Read current balance and version
|
|
↓
|
|
3. Perform operations
|
|
↓
|
|
4. UPDATE balance with version check:
|
|
WHERE user_id = ? AND version = ?
|
|
↓
|
|
5. Check if changes = 0 (version mismatch)
|
|
↓
|
|
YES: Throw OptimisticLockError → Retry with backoff
|
|
NO: Success → Return result
|
|
```
|
|
|
|
### Retry Strategy
|
|
|
|
- **Attempt 1:** Immediate execution
|
|
- **Attempt 2:** Wait 100ms, retry
|
|
- **Attempt 3:** Wait 200ms, retry
|
|
- **Attempt 4:** Wait 400ms, retry (final)
|
|
- **Max retries exhausted:** Return user-friendly error message
|
|
|
|
### Reconciliation (Daily Cron)
|
|
|
|
```
|
|
Every day at KST 00:00:
|
|
1. Compare user_deposits.balance with SUM(confirmed transactions)
|
|
2. Detect discrepancies
|
|
3. Log all inconsistencies with details
|
|
4. Send admin notification if issues found
|
|
5. Generate detailed report
|
|
```
|
|
|
|
## Testing
|
|
|
|
### Local Testing
|
|
|
|
```bash
|
|
# 1. Apply migration to local database
|
|
wrangler d1 execute telegram-conversations --local --file=migrations/002_add_version_columns.sql
|
|
|
|
# 2. Verify version column
|
|
wrangler d1 execute telegram-conversations --local --command "SELECT user_id, balance, version FROM user_deposits LIMIT 5"
|
|
|
|
# 3. Start local dev server
|
|
npm run dev
|
|
|
|
# 4. Test deposit flow
|
|
curl -X POST http://localhost:8787/webhook \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Telegram-Bot-Api-Secret-Token: test-secret" \
|
|
-d '{"message":{"chat":{"id":123},"text":"홍길동 10000원 입금"}}'
|
|
|
|
# 5. Check logs for optimistic lock messages
|
|
npm run tail
|
|
```
|
|
|
|
### Production Deployment
|
|
|
|
```bash
|
|
# 1. BACKUP database first (important!)
|
|
wrangler d1 export telegram-conversations --output=backup-$(date +%Y%m%d).sql
|
|
|
|
# 2. Apply migration
|
|
wrangler d1 execute telegram-conversations --file=migrations/002_add_version_columns.sql
|
|
|
|
# 3. Verify migration
|
|
wrangler d1 execute telegram-conversations --command "PRAGMA table_info(user_deposits)"
|
|
|
|
# 4. Deploy code changes
|
|
npm run deploy
|
|
|
|
# 5. Monitor logs
|
|
npm run tail
|
|
|
|
# 6. Test with actual deposit
|
|
# (Use Telegram bot to test deposit flow)
|
|
|
|
# 7. Wait for next cron run (KST 00:00) to test reconciliation
|
|
# Or manually trigger: wrangler d1 execute --command "SELECT ..."
|
|
```
|
|
|
|
### Concurrency Test Scenario
|
|
|
|
**Simulate concurrent requests:**
|
|
1. User has balance: 10,000원 (version=1)
|
|
2. Send two deposit requests simultaneously:
|
|
- Request A: +5,000원
|
|
- Request B: +3,000원
|
|
3. Expected behavior:
|
|
- Request A: Reads version=1, updates to version=2 ✅
|
|
- Request B: Reads version=1, fails (version mismatch), retries
|
|
- Request B: Reads version=2, updates to version=3 ✅
|
|
4. Final state: 18,000원 (version=3)
|
|
|
|
## Benefits
|
|
|
|
### Data Integrity
|
|
- **Before:** Batch operations could partially fail, causing balance/transaction mismatch
|
|
- **After:** Version conflicts detected and automatically retried, guaranteeing consistency
|
|
|
|
### Concurrency Safety
|
|
- **Before:** Simultaneous deposits could overwrite each other
|
|
- **After:** Version column prevents lost updates through automatic retry
|
|
|
|
### Monitoring
|
|
- **Before:** No automated integrity verification
|
|
- **After:** Daily reconciliation job detects and alerts on any discrepancies
|
|
|
|
### Performance
|
|
- **Impact:** Minimal - version check adds negligible overhead
|
|
- **Index:** `idx_deposits_user_version` ensures fast version lookups
|
|
|
|
## Rollback Plan
|
|
|
|
If issues occur:
|
|
|
|
```bash
|
|
# 1. Revert code changes
|
|
git revert <commit-hash>
|
|
npm run deploy
|
|
|
|
# 2. (Optional) Remove version column
|
|
# Note: SQLite doesn't support DROP COLUMN directly
|
|
# Alternative: Keep column but remove code dependency
|
|
|
|
# 3. Manual data fix (if needed)
|
|
wrangler d1 execute telegram-conversations --command "
|
|
UPDATE user_deposits SET balance = (
|
|
SELECT COALESCE(SUM(
|
|
CASE
|
|
WHEN type = 'deposit' AND status = 'confirmed' THEN amount
|
|
WHEN type IN ('withdrawal', 'refund') AND status = 'confirmed' THEN -amount
|
|
ELSE 0
|
|
END
|
|
), 0)
|
|
FROM deposit_transactions
|
|
WHERE user_id = user_deposits.user_id
|
|
)
|
|
"
|
|
```
|
|
|
|
## Future Enhancements
|
|
|
|
1. **Metrics Dashboard:**
|
|
- Track optimistic lock conflicts per day
|
|
- Monitor retry success rate
|
|
- Alert on high conflict rate (>10% of operations)
|
|
|
|
2. **Compensation Transaction:**
|
|
- Automatic rollback on catastrophic failures
|
|
- Transaction log for audit trail
|
|
|
|
3. **Read Replicas:**
|
|
- Use version for cache invalidation
|
|
- Enable eventual consistency patterns
|
|
|
|
4. **Admin Tools:**
|
|
- Manual reconciliation trigger
|
|
- Balance adjustment with audit log
|
|
- Transaction replay for debugging
|
|
|
|
## References
|
|
|
|
- Migration: `migrations/002_add_version_columns.sql`
|
|
- Utility: `src/utils/optimistic-lock.ts`, `src/utils/reconciliation.ts`
|
|
- Integration: `src/deposit-agent.ts`, `src/index.ts`
|
|
- Documentation: `CLAUDE.md` (Transaction Isolation section)
|