Skip to content

Commit 282f0ad

Browse files
Hotfix/sqlite rescue (#48)
* Add SQLite rescue for Databricks Apps persistence Databricks Apps containers are ephemeral - SQLite databases are lost on container restarts during platform updates. This adds a rescue module that: - Restores DB from Unity Catalog Volume on startup (before migrations) - Backs up DB to Volume on shutdown (SIGTERM/SIGINT handlers) - Periodic backup after N write operations (configurable, default 50) - Exposes rescue status in /health/detailed endpoint Configuration via environment variables: - SQLITE_VOLUME_BACKUP_PATH: Volume path for backup - SQLITE_BACKUP_AFTER_OPS: Write ops before auto-backup Also documents Databricks Apps authentication (service principals, automatic credential injection, resource permissions) in the spec. Co-Authored-By: Claude <noreply@anthropic.com> * Update deploy recipe to use Databricks CLI directly The old deploy.sh script was removed. Updated the `just deploy` recipe to use the Databricks CLI directly for syncing and deploying: - Syncs files to workspace (excluding .git, node_modules, etc.) - Deploys app using `databricks apps deploy` - Uses DATABRICKS_CONFIG_PROFILE and DATABRICKS_APP_NAME from .env.local - Updated setup message to reference `just deploy` Co-Authored-By: Claude <noreply@anthropic.com> * Run npm install in ui-build if dependencies are stale The ui-build recipe now checks if npm install needs to run by comparing package.json modification time against node_modules. This ensures builds don't fail due to missing dependencies while avoiding unnecessary installs. Co-Authored-By: Claude <noreply@anthropic.com> * Auto-create Databricks app if it doesn't exist The deploy recipe now checks if the app exists before deploying. If not, it creates the app first using `databricks apps create`. Co-Authored-By: Claude <noreply@anthropic.com> * Add root package.json for Databricks Apps Node.js build Databricks Apps looks for package.json at the root to detect Node.js projects. Added a root package.json that delegates to client/: - Root package.json with build script that runs client build - Removed npx from client scripts (not available in Databricks env) - Simplified deploy recipe to let Databricks handle the build Co-Authored-By: Claude <noreply@anthropic.com> * Fix SQLite rescue for Unity Catalog volume paths The previous implementation failed because: 1. It tried to create parent directories under /Volumes which is not allowed 2. It didn't validate the UC volume path format 3. The volume must already exist - we can't create it Fixed by: - Added _validate_volume_path() to check path format early - Added _get_volume_root() to extract and verify volume accessibility - Don't call mkdir() for UC volume destinations - Validate configuration at startup and fail fast with clear error - Added path_valid, path_error, volume_accessible to status endpoint Expected path format: /Volumes/<catalog>/<schema>/<volume>/workshop.db Co-Authored-By: Claude <noreply@anthropic.com> * Refactor SQLite rescue to use Databricks SDK Files API Databricks Apps do NOT support FUSE mounts for UC volumes. This refactors the sqlite_rescue module to use the Databricks SDK Files API instead of direct filesystem operations. Changes: - Use WorkspaceClient.files.download/upload instead of shutil.copy - Add _get_workspace_client() for cached SDK client - Add _file_exists_on_volume() using files.get_status() - Support SQLITE_VOLUME_PATH env var (base path, appends /workshop.db) - Improve restore logic: try download directly, handle NotFound gracefully - Add detailed logging to debug file operations - Update app.yaml with valueFrom configuration example - Update BUILD_AND_DEPLOY_SPEC.md documentation Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 05fb6fd commit 282f0ad

8 files changed

Lines changed: 789 additions & 13 deletions

File tree

app.yaml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,35 @@
1-
command:
1+
command:
22
- "gunicorn"
33
- "server.app:app"
44
- "-w"
55
- "2"
66
- "--worker-class"
77
- "uvicorn.workers.UvicornWorker"
88
- "--timeout"
9-
- "1800"
9+
- "1800"
10+
11+
# SQLite Rescue: Backup/restore DB to Unity Catalog Volume for persistence
12+
# across container restarts.
13+
#
14+
# IMPORTANT: Databricks Apps do NOT support FUSE mounts for UC volumes.
15+
# This app uses the Databricks SDK Files API for volume access.
16+
#
17+
# Setup steps:
18+
# 1. Add the Unity Catalog volume as an App resource in the Apps UI
19+
# 2. Grant the app "Can read and write" permission on the volume
20+
# 3. Give it a resource key (e.g., "db_backup_volume")
21+
# 4. Uncomment and configure the env section below
22+
#
23+
# Note: valueFrom provides the volume base path (e.g., /Volumes/catalog/schema/volume).
24+
# The app automatically appends "/workshop.db" to construct the full backup path.
25+
#
26+
# env:
27+
# - name: SQLITE_VOLUME_PATH
28+
# valueFrom: db_backup_volume # Resource key from Apps UI
29+
# - name: SQLITE_BACKUP_AFTER_OPS
30+
# value: "50" # Backup after every 50 write operations (default: 50, 0 to disable)
31+
#
32+
# Alternative: Specify the full path directly (works but less portable):
33+
# env:
34+
# - name: SQLITE_VOLUME_BACKUP_PATH
35+
# value: "/Volumes/your_catalog/your_schema/your_volume/workshop.db"

client/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
"version": "0.1.0",
55
"type": "module",
66
"scripts": {
7-
"start": "npx vite",
8-
"dev": "npx vite",
9-
"build": "npx vite build",
10-
"preview": "npx vite preview",
11-
"test": "npx playwright test",
12-
"test:e2e": "npx playwright test",
7+
"start": "vite",
8+
"dev": "vite",
9+
"build": "vite build",
10+
"preview": "vite preview",
11+
"test": "playwright test",
12+
"test:e2e": "playwright test",
1313
"test:unit": "vitest run",
1414
"test:unit:watch": "vitest",
1515
"test:unit:coverage": "vitest run --coverage",

justfile

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ setup: setup-uv setup-prereqs setup-python setup-client configure test-connectio
3030
@echo ""
3131
@echo "🎯 Virtual environment created at: .venv/"
3232
@echo ""
33-
@echo "Next step: run './deploy.sh' when ready to deploy"
33+
@echo "Next step: run 'just deploy' when ready to deploy"
3434

3535
# Install uv
3636
[group('setup')]
@@ -245,6 +245,15 @@ ui-dev:
245245

246246
[group('dev')]
247247
ui-build:
248+
#!/usr/bin/env bash
249+
set -euo pipefail
250+
251+
# Run npm install if node_modules is missing or package.json is newer
252+
if [ ! -d "{{client-dir}}/node_modules" ] || [ "{{client-dir}}/package.json" -nt "{{client-dir}}/node_modules" ]; then
253+
echo "📦 Installing frontend dependencies..."
254+
npm -C {{client-dir}} install
255+
fi
256+
248257
npm -C {{client-dir}} run build
249258

250259
[group('dev')]
@@ -342,9 +351,37 @@ api port="8000":
342351

343352
[group('app')]
344353
deploy:
345-
just ui-build
346-
just db-bootstrap
347-
SKIP_UI_BUILD=1 ./deploy.sh
354+
#!/usr/bin/env bash
355+
set -euo pipefail
356+
357+
PROFILE="${DATABRICKS_CONFIG_PROFILE:-DEFAULT}"
358+
APP="${DATABRICKS_APP_NAME:?DATABRICKS_APP_NAME is not set (run \`just configure\`)}"
359+
360+
echo "📦 Syncing files to workspace..."
361+
DATABRICKS_USERNAME=$(databricks --profile "$PROFILE" current-user me | jq -r .userName)
362+
WORKSPACE_PATH="/Workspace/Users/$DATABRICKS_USERNAME/$APP"
363+
364+
databricks --profile "$PROFILE" sync . "$WORKSPACE_PATH" \
365+
--exclude ".git" \
366+
--exclude "node_modules" \
367+
--exclude "__pycache__" \
368+
--exclude "*.db" \
369+
--exclude ".venv" \
370+
--exclude ".e2e-*"
371+
372+
# Create app if it doesn't exist
373+
if ! databricks --profile "$PROFILE" apps get "$APP" &>/dev/null; then
374+
echo "📱 Creating app: $APP"
375+
databricks --profile "$PROFILE" apps create "$APP"
376+
fi
377+
378+
echo "🚀 Deploying app: $APP"
379+
echo " Databricks will run: npm install → pip install → npm run build → app.yaml command"
380+
databricks --profile "$PROFILE" apps deploy "$APP" --source-code-path "$WORKSPACE_PATH"
381+
382+
echo ""
383+
echo "✅ Deployment initiated for $APP"
384+
echo " Run 'just app-info' to check deployment status"
348385

349386
[group('dev')]
350387
dev api_port="8000" ui_port="5173":

package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "human-eval-workshop",
3+
"private": true,
4+
"description": "Root package.json to enable Databricks Apps Node.js build flow",
5+
"scripts": {
6+
"build": "npm -C client install && npm -C client run build"
7+
}
8+
}

server/app.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,21 +14,50 @@
1414
from server.config import ServerConfig
1515
from server.db_bootstrap import maybe_bootstrap_db_on_startup
1616
from server.routers import router
17+
from server.sqlite_rescue import (
18+
backup_to_volume,
19+
get_rescue_status,
20+
install_shutdown_handlers,
21+
restore_from_volume,
22+
)
1723

1824

1925
@asynccontextmanager
2026
async def lifespan(app: FastAPI):
2127
"""Manage application lifespan with proper startup and shutdown."""
2228
print("🚀 Application startup - lifespan function called!")
2329

30+
# SQLite Rescue: Restore from Unity Catalog Volume if configured
31+
# This MUST happen before database bootstrap/migrations
32+
rescue_status = get_rescue_status()
33+
if rescue_status["configured"]:
34+
print(f"📦 SQLite rescue configured: {rescue_status['volume_backup_path']}")
35+
if restore_from_volume():
36+
print("✅ Database restored from Unity Catalog Volume")
37+
else:
38+
print("ℹ️ No backup to restore (starting fresh or backup not found)")
39+
40+
# Install signal handlers for graceful shutdown backup
41+
install_shutdown_handlers()
42+
else:
43+
print("⚠️ SQLITE_VOLUME_BACKUP_PATH not configured - database will NOT persist across container restarts")
44+
2445
# NOTE: This is a *fallback* safety net for deployments that don't run `just db-bootstrap`.
2546
# It is designed to be safe under multi-process servers (e.g., gunicorn with multiple
2647
# Uvicorn workers) via an inter-process lock.
2748
maybe_bootstrap_db_on_startup()
2849

2950
print("✅ Application startup complete!")
3051
yield
52+
53+
# SQLite Rescue: Backup to Unity Catalog Volume on shutdown
3154
print("🔄 Application shutting down...")
55+
if rescue_status["configured"]:
56+
print("💾 Backing up database to Unity Catalog Volume...")
57+
if backup_to_volume(force=True):
58+
print("✅ Database backed up successfully")
59+
else:
60+
print("⚠️ Database backup failed or skipped")
3261

3362

3463
# Request timing middleware
@@ -113,7 +142,16 @@ async def detailed_health():
113142
"invalid": getattr(pool, "invalid", lambda: 0)(), # Handle missing invalid method
114143
}
115144

116-
return {"status": "healthy", "database": "connected", "connection_pool": pool_info, "timestamp": time.time()}
145+
# Get SQLite rescue status
146+
rescue_status = get_rescue_status()
147+
148+
return {
149+
"status": "healthy",
150+
"database": "connected",
151+
"connection_pool": pool_info,
152+
"sqlite_rescue": rescue_status,
153+
"timestamp": time.time(),
154+
}
117155
except Exception as e:
118156
return {"status": "unhealthy", "database": "disconnected", "error": str(e), "timestamp": time.time()}
119157

server/database.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ def set_sqlite_pragma(dbapi_connection, connection_record):
7070
cursor.execute("PRAGMA synchronous=NORMAL")
7171
cursor.close()
7272

73+
74+
# SQLite Rescue: Track write operations for backup triggering
75+
# This listener fires after successful commits and notifies the rescue module
76+
@event.listens_for(engine, "commit")
77+
def on_commit(conn):
78+
"""Record write operations for SQLite rescue backup triggering."""
79+
try:
80+
from server.sqlite_rescue import record_write_operation
81+
record_write_operation()
82+
except ImportError:
83+
pass # sqlite_rescue not available
84+
7385
# Create session factory with better session management
7486
SessionLocal = sessionmaker(
7587
autocommit=False,

0 commit comments

Comments
 (0)