feat: add optimistic locking and improve type safety
- 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>
This commit is contained in:
266
OPTIMISTIC_LOCKING_SUMMARY.md
Normal file
266
OPTIMISTIC_LOCKING_SUMMARY.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user