mirror of
https://github.com/stellarshenson/stellars-jupyterhub-ds.git
synced 2026-03-08 06:00:29 +00:00
fix: use separate SQLite database for activity monitor
- ActivityMonitor now uses /data/activity_samples.sqlite instead of JupyterHub's main database to avoid SQLite locking conflicts - Fixes "database is locked" errors that prevented login when both JupyterHub and ActivityMonitor wrote simultaneously - Added "Last Active" column to activity table showing relative time
This commit is contained in:
@@ -189,3 +189,6 @@ This journal tracks substantive work on documents, diagrams, and documentation c
|
|||||||
|
|
||||||
62. **Task - Activity Monitor UI polish and sample cleanup fix**: Improved activity bar design and fixed sample retention enforcement<br>
|
62. **Task - Activity Monitor UI polish and sample cleanup fix**: Improved activity bar design and fixed sample retention enforcement<br>
|
||||||
**Result**: Fixed inconsistent timestamp text (initial "Last updated: --" vs loaded "Measured X ago" - removed initial text so only "Measured X ago" shows). Redesigned activity bar from separate 16x16 boxes to thin continuous bar (80px wide, 8px tall) with subtle 1px dividers between 5 segments - visually distinct from day-based layouts. Fixed critical sample cleanup bug - cleanup only ran when new sample was INSERT-ed, not when existing sample was UPDATE-d, causing old samples to persist indefinitely when refreshes kept updating the last sample. Moved cleanup to run on every record_sample() call regardless of update vs insert, ensuring samples older than RETENTION_DAYS are always pruned
|
**Result**: Fixed inconsistent timestamp text (initial "Last updated: --" vs loaded "Measured X ago" - removed initial text so only "Measured X ago" shows). Redesigned activity bar from separate 16x16 boxes to thin continuous bar (80px wide, 8px tall) with subtle 1px dividers between 5 segments - visually distinct from day-based layouts. Fixed critical sample cleanup bug - cleanup only ran when new sample was INSERT-ed, not when existing sample was UPDATE-d, causing old samples to persist indefinitely when refreshes kept updating the last sample. Moved cleanup to run on every record_sample() call regardless of update vs insert, ensuring samples older than RETENTION_DAYS are always pruned
|
||||||
|
|
||||||
|
63. **Task - Fix SQLite database locking**: Resolved database lock errors preventing login<br>
|
||||||
|
**Result**: ActivityMonitor was using JupyterHub's main SQLite database (`/data/jupyterhub.sqlite`) via separate SQLAlchemy connection, causing `sqlite3.OperationalError: database is locked` when both tried to write simultaneously (login + activity sampling). Fixed by moving ActivityMonitor to separate database file `/data/activity_samples.sqlite`, completely avoiding lock contention. Added "Last Active" column to activity table showing relative time since user's last activity (e.g., "5min", "2h 14min")
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ PROJECT_NAME="stellars-jupyterhub-ds"
|
|||||||
PROJECT_DESCRIPTION="Multi-user JupyterHub 4 deployment platform with data science stack, GPU auto-detection, NativeAuthenticator, and isolated per-user environments spawned via DockerSpawner"
|
PROJECT_DESCRIPTION="Multi-user JupyterHub 4 deployment platform with data science stack, GPU auto-detection, NativeAuthenticator, and isolated per-user environments spawned via DockerSpawner"
|
||||||
|
|
||||||
# Version
|
# Version
|
||||||
VERSION="3.7.3_cuda-13.0.2_jh-5.4.2"
|
VERSION="3.7.5_cuda-13.0.2_jh-5.4.2"
|
||||||
VERSION_COMMENT="Activity Monitor: admin page with 3-state status, activity scoring, reset functionality"
|
VERSION_COMMENT="Activity Monitor: admin page with 3-state status, activity scoring, reset functionality"
|
||||||
RELEASE_TAG="RELEASE_3.2.11"
|
RELEASE_TAG="RELEASE_3.2.11"
|
||||||
RELEASE_DATE="2025-11-09"
|
RELEASE_DATE="2025-11-09"
|
||||||
|
|||||||
@@ -132,11 +132,16 @@ class ActivityMonitor:
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
def _get_db(self):
|
def _get_db(self):
|
||||||
"""Get or create database session"""
|
"""Get or create database session.
|
||||||
|
|
||||||
|
Uses a SEPARATE database file to avoid SQLite locking conflicts
|
||||||
|
with JupyterHub's main database.
|
||||||
|
"""
|
||||||
if self._db_session is not None:
|
if self._db_session is not None:
|
||||||
return self._db_session
|
return self._db_session
|
||||||
|
|
||||||
db_url = os.environ.get('JUPYTERHUB_DB_URL', 'sqlite:////data/jupyterhub.sqlite')
|
# Use separate database file to avoid locking conflicts with JupyterHub
|
||||||
|
db_url = 'sqlite:////data/activity_samples.sqlite'
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._engine = create_engine(db_url)
|
self._engine = create_engine(db_url)
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
<th class="text-center">CPU</th>
|
<th class="text-center">CPU</th>
|
||||||
<th class="text-center">Memory</th>
|
<th class="text-center">Memory</th>
|
||||||
<th class="text-center">Time Left</th>
|
<th class="text-center">Time Left</th>
|
||||||
|
<th class="text-center">Last Active</th>
|
||||||
<th>Activity (7 days)</th>
|
<th>Activity (7 days)</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -166,6 +167,7 @@ require(["jquery"], function($) {
|
|||||||
'<td class="text-center">' + formatCpu(user.cpu_percent) + '</td>' +
|
'<td class="text-center">' + formatCpu(user.cpu_percent) + '</td>' +
|
||||||
'<td class="text-center">' + formatMemory(user.memory_mb, user.memory_percent) + '</td>' +
|
'<td class="text-center">' + formatMemory(user.memory_mb, user.memory_percent) + '</td>' +
|
||||||
'<td class="text-center">' + formatTimeRemaining(user.time_remaining_seconds) + '</td>' +
|
'<td class="text-center">' + formatTimeRemaining(user.time_remaining_seconds) + '</td>' +
|
||||||
|
'<td class="text-center">' + formatLastActive(user.last_activity) + '</td>' +
|
||||||
'<td>' + renderActivityBar(user.activity_score) + '</td>' +
|
'<td>' + renderActivityBar(user.activity_score) + '</td>' +
|
||||||
'</tr>';
|
'</tr>';
|
||||||
tbody.append(row);
|
tbody.append(row);
|
||||||
@@ -229,6 +231,30 @@ require(["jquery"], function($) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatLastActive(isoString) {
|
||||||
|
if (!isoString) {
|
||||||
|
return '<span class="text-muted">--</span>';
|
||||||
|
}
|
||||||
|
var date = new Date(isoString);
|
||||||
|
var now = new Date();
|
||||||
|
var diffMs = now - date;
|
||||||
|
var diffSec = Math.floor(diffMs / 1000);
|
||||||
|
var diffMin = Math.floor(diffSec / 60);
|
||||||
|
var diffHour = Math.floor(diffMin / 60);
|
||||||
|
var remainingMin = diffMin % 60;
|
||||||
|
|
||||||
|
if (diffMin < 1) {
|
||||||
|
return '<span class="text-success">now</span>';
|
||||||
|
} else if (diffMin < 60) {
|
||||||
|
return diffMin + 'min';
|
||||||
|
} else if (diffHour < 24) {
|
||||||
|
return diffHour + 'h ' + remainingMin + 'min';
|
||||||
|
} else {
|
||||||
|
var diffDay = Math.floor(diffHour / 24);
|
||||||
|
return diffDay + 'd ' + (diffHour % 24) + 'h';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderActivityBar(score) {
|
function renderActivityBar(score) {
|
||||||
if (score === null || score === undefined) {
|
if (score === null || score === undefined) {
|
||||||
return '<span class="text-muted small">--</span>';
|
return '<span class="text-muted small">--</span>';
|
||||||
|
|||||||
Reference in New Issue
Block a user