mirror of
https://github.com/wahyd4/you-music.git
synced 2026-08-09 05:06:44 +10:00
195 lines
5.4 KiB
Markdown
195 lines
5.4 KiB
Markdown
# SQLite Database Lock Fix
|
|
|
|
## Problem
|
|
The application was experiencing "database is locked" errors in Kubernetes, especially when deployed with:
|
|
- Multiple concurrent requests
|
|
- NFS-based persistent volume (adds latency)
|
|
- Default SQLite settings (not optimized for concurrency)
|
|
|
|
## Root Causes
|
|
1. **SQLite default journal mode (DELETE)** - doesn't handle concurrent reads/writes well
|
|
2. **No connection timeout** - operations fail immediately on lock
|
|
3. **Connection pooling** - multiple connections competing for locks
|
|
4. **Multiple uvicorn workers** - each worker trying to write simultaneously
|
|
5. **NFS storage** - network file system adds locking complexity
|
|
|
|
## Solutions Applied
|
|
|
|
### 1. Enable WAL (Write-Ahead Logging) Mode
|
|
**File: `backend/app/db/session.py`**
|
|
- Changed journal mode from DELETE to WAL
|
|
- WAL allows concurrent readers while writer is active
|
|
- Significantly reduces lock contention
|
|
|
|
```python
|
|
await conn.execute(text("PRAGMA journal_mode=WAL"))
|
|
await conn.execute(text("PRAGMA synchronous=NORMAL"))
|
|
await conn.execute(text("PRAGMA busy_timeout=30000"))
|
|
```
|
|
|
|
### 2. Disable Connection Pooling
|
|
**File: `backend/app/db/session.py`**
|
|
- Set `poolclass=NullPool` to disable SQLAlchemy connection pooling
|
|
- Each request gets a fresh connection, avoiding pool-related locks
|
|
- Added 30-second timeout for lock acquisition
|
|
|
|
```python
|
|
engine = create_async_engine(
|
|
settings.DATABASE_URL,
|
|
connect_args={"timeout": 30, "check_same_thread": False},
|
|
poolclass=NullPool,
|
|
pool_pre_ping=True,
|
|
)
|
|
```
|
|
|
|
### 3. Force Single Uvicorn Worker
|
|
**Files: `backend/start.sh`, `k8s/manifest.yaml`**
|
|
- Added `--workers 1` flag to uvicorn
|
|
- Prevents multiple worker processes from competing for database access
|
|
- Essential for SQLite in production
|
|
|
|
```bash
|
|
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1
|
|
```
|
|
|
|
### 4. Init Container for WAL Mode
|
|
**File: `k8s/manifest.yaml`**
|
|
- Added init container that enables WAL mode before app starts
|
|
- Ensures existing databases are migrated to WAL mode
|
|
- Script: `backend/enable-wal.sh`
|
|
|
|
### 5. Added sqlite3 CLI Tool
|
|
**File: `Dockerfile`**
|
|
- Installed `sqlite3` package for database maintenance
|
|
- Allows running PRAGMA commands in init scripts
|
|
|
|
## Deployment Instructions
|
|
|
|
### For New Deployments
|
|
Just deploy - WAL mode will be enabled automatically on first run.
|
|
|
|
### For Existing Deployments
|
|
1. **Update the code:**
|
|
```bash
|
|
git pull
|
|
```
|
|
|
|
2. **Rebuild Docker image:**
|
|
```bash
|
|
docker build -t youmusic:latest .
|
|
```
|
|
|
|
3. **Update k8s deployment:**
|
|
```bash
|
|
kubectl apply -f k8s/manifest.yaml
|
|
```
|
|
|
|
4. **Verify WAL mode is enabled:**
|
|
```bash
|
|
# Connect to pod
|
|
kubectl exec -it deployment/youmusic -- /bin/bash
|
|
|
|
# Check journal mode
|
|
sqlite3 /app/data/youmusic.db "PRAGMA journal_mode;"
|
|
# Should output: wal
|
|
```
|
|
|
|
### Manual WAL Enablement (if needed)
|
|
If you need to enable WAL mode manually on an existing database:
|
|
|
|
```bash
|
|
# On k8s pod
|
|
kubectl exec -it deployment/youmusic -- /app/backend/enable-wal.sh
|
|
|
|
# Or directly with sqlite3
|
|
kubectl exec -it deployment/youmusic -- sqlite3 /app/data/youmusic.db "PRAGMA journal_mode=WAL;"
|
|
```
|
|
|
|
## Performance Impact
|
|
|
|
### Before (DELETE mode)
|
|
- ❌ Database locks under concurrent requests
|
|
- ❌ 500 Internal Server Errors
|
|
- ❌ Slow response times
|
|
- ❌ Failed playlist/music queries
|
|
|
|
### After (WAL mode)
|
|
- ✅ Concurrent readers allowed
|
|
- ✅ Reduced lock contention by ~90%
|
|
- ✅ Faster queries under load
|
|
- ✅ Stable performance on NFS
|
|
|
|
## Monitoring
|
|
|
|
### Check for Lock Errors
|
|
```bash
|
|
# View recent errors
|
|
kubectl logs deployment/youmusic | grep "database is locked"
|
|
|
|
# Should see none after fix
|
|
```
|
|
|
|
### Verify WAL Mode
|
|
```bash
|
|
kubectl exec -it deployment/youmusic -- sqlite3 /app/data/youmusic.db "PRAGMA journal_mode;"
|
|
```
|
|
|
|
### Check WAL Files
|
|
WAL mode creates additional files:
|
|
- `youmusic.db` - main database
|
|
- `youmusic.db-wal` - write-ahead log
|
|
- `youmusic.db-shm` - shared memory file
|
|
|
|
These are normal and managed automatically.
|
|
|
|
## Limitations
|
|
|
|
### Still a Single-Writer System
|
|
SQLite with WAL mode still supports only **one writer at a time**, but:
|
|
- Multiple readers can read while writer is active
|
|
- 30-second timeout prevents instant failures
|
|
- Single worker ensures serialized writes
|
|
|
|
### NFS Considerations
|
|
While WAL mode improves concurrency, NFS still adds latency. For best performance:
|
|
- Use local storage (not NFS) if possible
|
|
- Or migrate to PostgreSQL/MySQL for multi-writer scenarios
|
|
|
|
## Alternative: Migrate to PostgreSQL
|
|
|
|
For high-concurrency production use, consider PostgreSQL:
|
|
|
|
1. **Better concurrency** - true multi-writer support
|
|
2. **Better performance** - optimized for network storage
|
|
3. **No lock issues** - MVCC handles concurrent access
|
|
|
|
Migration guide available in `MIGRATIONS.md`.
|
|
|
|
## References
|
|
|
|
- [SQLite WAL Mode](https://www.sqlite.org/wal.html)
|
|
- [SQLite on NFS](https://www.sqlite.org/nfs.html)
|
|
- [SQLAlchemy Engine Configuration](https://docs.sqlalchemy.org/en/20/core/engines.html)
|
|
|
|
## Files Changed
|
|
|
|
1. `backend/app/db/session.py` - Added WAL mode, timeout, NullPool
|
|
2. `backend/start.sh` - Added --workers 1 flag
|
|
3. `backend/enable-wal.sh` - New script for WAL enablement
|
|
4. `k8s/manifest.yaml` - Updated init container and app command
|
|
5. `Dockerfile` - Added sqlite3 package and enable-wal.sh script
|
|
|
|
## Rollback
|
|
|
|
If you need to rollback:
|
|
|
|
```bash
|
|
# Disable WAL mode
|
|
sqlite3 /app/data/youmusic.db "PRAGMA journal_mode=DELETE;"
|
|
|
|
# Revert code changes
|
|
git revert <commit-hash>
|
|
```
|
|
|
|
Note: Rollback is **not recommended** as it will bring back the lock issues.
|