Spaces:
Running
Running
chore: rebuild with updated GHCR images (93d7a331a31a22d0e1699e24e9fe200a0c089737)
Browse files- Dockerfile +1 -1
- README.md +1 -1
- session_sync_daemon.py +70 -5
Dockerfile
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
# Copies pre-built services from GHCR images into single runtime
|
| 3 |
|
| 4 |
# Build arg for image tag (defaults to latest, overridden by deploy workflow with SHA)
|
| 5 |
-
ARG IMAGE_TAG=main-sha-
|
| 6 |
|
| 7 |
# Stage 1: Extract Next.js standalone build
|
| 8 |
FROM ghcr.io/glutamatt/neural-runner/neural-runner-app:${IMAGE_TAG} AS nextjs-build
|
|
|
|
| 2 |
# Copies pre-built services from GHCR images into single runtime
|
| 3 |
|
| 4 |
# Build arg for image tag (defaults to latest, overridden by deploy workflow with SHA)
|
| 5 |
+
ARG IMAGE_TAG=main-sha-93d7a33
|
| 6 |
|
| 7 |
# Stage 1: Extract Next.js standalone build
|
| 8 |
FROM ghcr.io/glutamatt/neural-runner/neural-runner-app:${IMAGE_TAG} AS nextjs-build
|
README.md
CHANGED
|
@@ -68,4 +68,4 @@ Create a **private** Dataset: `{your-username}/neural-runner-sessions`
|
|
| 68 |
|
| 69 |
Built with [Claude Code](https://claude.com/claude-code) 🤖
|
| 70 |
|
| 71 |
-
# Rebuild: Wed Feb 4 23:
|
|
|
|
| 68 |
|
| 69 |
Built with [Claude Code](https://claude.com/claude-code) 🤖
|
| 70 |
|
| 71 |
+
# Rebuild: Wed Feb 4 23:43:00 UTC 2026 - Updated GHCR images
|
session_sync_daemon.py
CHANGED
|
@@ -4,11 +4,16 @@ Session sync daemon - Periodically backs up sessions to HF Dataset.
|
|
| 4 |
|
| 5 |
Runs as a background process managed by process-compose.
|
| 6 |
Syncs session files every 5 minutes to ensure persistence.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
"""
|
| 8 |
|
| 9 |
import os
|
| 10 |
import sys
|
| 11 |
import time
|
|
|
|
| 12 |
import logging
|
| 13 |
from pathlib import Path
|
| 14 |
|
|
@@ -27,29 +32,86 @@ logger = logging.getLogger(__name__)
|
|
| 27 |
# Configuration
|
| 28 |
SYNC_INTERVAL_SECONDS = 120 # 2 minutes (increased from 5 for faster persistence)
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
def main():
|
| 32 |
"""Run periodic session sync."""
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
logger.info("=" * 60)
|
| 35 |
logger.info("Session Sync Daemon - Started")
|
| 36 |
logger.info("=" * 60)
|
|
|
|
| 37 |
|
| 38 |
# Check if HF_TOKEN is set
|
| 39 |
if not os.environ.get("HF_TOKEN"):
|
| 40 |
logger.warning("HF_TOKEN not set - daemon will not sync sessions")
|
| 41 |
logger.info("Set HF_TOKEN in Space settings to enable persistence")
|
| 42 |
logger.info("Daemon will sleep indefinitely (required by process-compose)")
|
| 43 |
-
# Keep daemon running but do nothing
|
| 44 |
-
while
|
| 45 |
time.sleep(3600)
|
| 46 |
-
return
|
| 47 |
|
| 48 |
logger.info(f"Syncing sessions every {SYNC_INTERVAL_SECONDS} seconds")
|
| 49 |
|
| 50 |
-
while
|
| 51 |
try:
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
logger.info("Starting session backup...")
|
| 55 |
backup_sessions_to_dataset()
|
|
@@ -64,6 +126,9 @@ def main():
|
|
| 64 |
logger.warning(f"Session sync failed: {e}")
|
| 65 |
logger.info("Will retry on next interval...")
|
| 66 |
|
|
|
|
|
|
|
|
|
|
| 67 |
|
| 68 |
if __name__ == "__main__":
|
| 69 |
main()
|
|
|
|
| 4 |
|
| 5 |
Runs as a background process managed by process-compose.
|
| 6 |
Syncs session files every 5 minutes to ensure persistence.
|
| 7 |
+
|
| 8 |
+
Implements graceful shutdown with signal handling:
|
| 9 |
+
- SIGTERM: Docker/process-compose shutdown (triggers final backup)
|
| 10 |
+
- SIGINT: Ctrl+C / manual interrupt (triggers final backup)
|
| 11 |
"""
|
| 12 |
|
| 13 |
import os
|
| 14 |
import sys
|
| 15 |
import time
|
| 16 |
+
import signal
|
| 17 |
import logging
|
| 18 |
from pathlib import Path
|
| 19 |
|
|
|
|
| 32 |
# Configuration
|
| 33 |
SYNC_INTERVAL_SECONDS = 120 # 2 minutes (increased from 5 for faster persistence)
|
| 34 |
|
| 35 |
+
# Global flag for graceful shutdown
|
| 36 |
+
shutdown_requested = False
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def signal_handler(signum, frame):
|
| 40 |
+
"""
|
| 41 |
+
Handle shutdown signals (SIGTERM, SIGINT).
|
| 42 |
+
|
| 43 |
+
Triggers immediate backup before exit to preserve session state.
|
| 44 |
+
|
| 45 |
+
Signal sources:
|
| 46 |
+
- SIGTERM: Docker stop, process-compose shutdown, HF Space restart
|
| 47 |
+
- SIGINT: Ctrl+C, manual interrupt
|
| 48 |
+
"""
|
| 49 |
+
global shutdown_requested
|
| 50 |
+
|
| 51 |
+
signal_name = signal.Signals(signum).name
|
| 52 |
+
logger.info("=" * 60)
|
| 53 |
+
logger.info(f"Received {signal_name} signal - initiating graceful shutdown")
|
| 54 |
+
logger.info("=" * 60)
|
| 55 |
+
|
| 56 |
+
shutdown_requested = True
|
| 57 |
+
|
| 58 |
+
# Perform final backup if HF_TOKEN is available
|
| 59 |
+
if os.environ.get("HF_TOKEN"):
|
| 60 |
+
try:
|
| 61 |
+
logger.info("Triggering final session backup...")
|
| 62 |
+
backup_sessions_to_dataset()
|
| 63 |
+
logger.info("✓ Final backup complete")
|
| 64 |
+
except SessionSyncError as e:
|
| 65 |
+
logger.error(f"🚨 SECURITY ERROR during shutdown: {e}")
|
| 66 |
+
# Don't exit(1) here - we're already shutting down
|
| 67 |
+
except Exception as e:
|
| 68 |
+
logger.warning(f"Final backup failed: {e}")
|
| 69 |
+
else:
|
| 70 |
+
logger.info("HF_TOKEN not set - skipping final backup")
|
| 71 |
+
|
| 72 |
+
logger.info("=" * 60)
|
| 73 |
+
logger.info("Session Sync Daemon - Stopped")
|
| 74 |
+
logger.info("=" * 60)
|
| 75 |
+
sys.exit(0)
|
| 76 |
+
|
| 77 |
|
| 78 |
def main():
|
| 79 |
"""Run periodic session sync."""
|
| 80 |
|
| 81 |
+
# Register signal handlers BEFORE starting loop
|
| 82 |
+
# SIGTERM: Docker/process-compose shutdown
|
| 83 |
+
# SIGINT: Ctrl+C / manual interrupt
|
| 84 |
+
signal.signal(signal.SIGTERM, signal_handler)
|
| 85 |
+
signal.signal(signal.SIGINT, signal_handler)
|
| 86 |
+
|
| 87 |
logger.info("=" * 60)
|
| 88 |
logger.info("Session Sync Daemon - Started")
|
| 89 |
logger.info("=" * 60)
|
| 90 |
+
logger.info("Signal handlers registered (SIGTERM, SIGINT)")
|
| 91 |
|
| 92 |
# Check if HF_TOKEN is set
|
| 93 |
if not os.environ.get("HF_TOKEN"):
|
| 94 |
logger.warning("HF_TOKEN not set - daemon will not sync sessions")
|
| 95 |
logger.info("Set HF_TOKEN in Space settings to enable persistence")
|
| 96 |
logger.info("Daemon will sleep indefinitely (required by process-compose)")
|
| 97 |
+
# Keep daemon running but do nothing (still responsive to signals)
|
| 98 |
+
while not shutdown_requested:
|
| 99 |
time.sleep(3600)
|
| 100 |
+
return
|
| 101 |
|
| 102 |
logger.info(f"Syncing sessions every {SYNC_INTERVAL_SECONDS} seconds")
|
| 103 |
|
| 104 |
+
while not shutdown_requested:
|
| 105 |
try:
|
| 106 |
+
# Sleep in small intervals to be responsive to shutdown signals
|
| 107 |
+
# Split SYNC_INTERVAL into 1-second chunks for faster signal response
|
| 108 |
+
for _ in range(SYNC_INTERVAL_SECONDS):
|
| 109 |
+
if shutdown_requested:
|
| 110 |
+
break
|
| 111 |
+
time.sleep(1)
|
| 112 |
+
|
| 113 |
+
if shutdown_requested:
|
| 114 |
+
break
|
| 115 |
|
| 116 |
logger.info("Starting session backup...")
|
| 117 |
backup_sessions_to_dataset()
|
|
|
|
| 126 |
logger.warning(f"Session sync failed: {e}")
|
| 127 |
logger.info("Will retry on next interval...")
|
| 128 |
|
| 129 |
+
# Graceful exit (signal handler already performed final backup)
|
| 130 |
+
logger.info("Main loop exited gracefully")
|
| 131 |
+
|
| 132 |
|
| 133 |
if __name__ == "__main__":
|
| 134 |
main()
|