- 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>
7.9 KiB
7.9 KiB
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 1column touser_depositstable - Creates index
idx_deposits_user_versionon(user_id, version) - Includes rollback instructions in comments
2. Optimistic Lock Utility
File: src/utils/optimistic-lock.ts
OptimisticLockErrorclass for version conflict detectionexecuteWithOptimisticLock<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.balancevs SUM(confirmed transactions) - Detects and logs discrepancies
formatReconciliationReport()for admin notifications
Files Modified
1. deposit-agent.ts
Changes:
- Added import of
executeWithOptimisticLockandOptimisticLockError - 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:
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
reconcileDepositsandformatReconciliationReport - 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 1column touser_depositstable - 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
# 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
# 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:
- User has balance: 10,000원 (version=1)
- Send two deposit requests simultaneously:
- Request A: +5,000원
- Request B: +3,000원
- 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 ✅
- 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_versionensures fast version lookups
Rollback Plan
If issues occur:
# 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
-
Metrics Dashboard:
- Track optimistic lock conflicts per day
- Monitor retry success rate
- Alert on high conflict rate (>10% of operations)
-
Compensation Transaction:
- Automatic rollback on catastrophic failures
- Transaction log for audit trail
-
Read Replicas:
- Use version for cache invalidation
- Enable eventual consistency patterns
-
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)