feat(git): implement deployment tracking and enhance lifecycle UX
Introduce a deployment recording system to track dashboard versions across environments and improve the Git management user experience. - Add `Deployment` model and Alembic migration to persist deployment history. - Implement `GitDeploymentRecorder` and `GitFingerprint` plugins to automate deployment logging and content hashing. - Add `get_deployment_status` API endpoint to retrieve real-time environment states. - Refactor `GitLifecycleHeader` to prioritize Call-to-Action (CTA) buttons and improve visual hierarchy. - Update `GitWorkspacePanel` to emphasize version saving and streamline commit workflows. - Enhance `GitEnvironmentTimeline` with deployment status integration, collapsible UI, and improved theme consistency. - Add auto-navigation logic in `GitManagerModel` to guide users to relevant tabs based on recommended actions. - Clean up obsolete documentation and update i18n strings for git visualization features.
This commit is contained in:
@@ -118,6 +118,34 @@ async def lifespan(app: FastAPI):
|
||||
_s.close()
|
||||
except Exception as _e:
|
||||
logger.explore("Failed to clean up stuck validation runs", error=str(_e))
|
||||
|
||||
# General reconciliation of stuck tasks from previous lifetime (idea from queue recovery patterns).
|
||||
# Tasks left in RUNNING when the in-memory asyncio tasks were lost (backend restart/crash).
|
||||
# This generalizes the ValidationRun-specific cleanup above.
|
||||
try:
|
||||
from sqlalchemy.orm import Session as _Ses
|
||||
from src.core.database import TasksSessionLocal as _TasksDb
|
||||
from src.models.task import TaskRecord as _TR
|
||||
from datetime import datetime as _dt, timezone as _tz
|
||||
|
||||
_s: _Ses = _TasksDb()
|
||||
_stuck_tasks = _s.query(_TR).filter(_TR.status == "RUNNING").all()
|
||||
for _t in _stuck_tasks:
|
||||
_t.status = "FAILED"
|
||||
_t.finished_at = _dt.now(_tz.utc)
|
||||
_t.error = "Force-stopped: backend restarted while task was in progress"
|
||||
# result can carry hint
|
||||
if not _t.result:
|
||||
_t.result = {"error": "interrupted_by_restart"}
|
||||
logger.reason(
|
||||
"Force-stopped stuck task",
|
||||
payload={"task_id": _t.id, "type": _t.type},
|
||||
)
|
||||
_s.commit()
|
||||
_s.close()
|
||||
except Exception as _e:
|
||||
logger.explore("Failed to clean up stuck general tasks", error=str(_e))
|
||||
|
||||
logger.reason("Initializing AsyncJobRunner")
|
||||
get_async_job_runner() # Initialize singleton with running event loop BEFORE scheduler starts
|
||||
logger.reason("Starting scheduler")
|
||||
@@ -126,7 +154,31 @@ async def lifespan(app: FastAPI):
|
||||
logger.reflect("Application startup complete")
|
||||
yield
|
||||
# Shutdown
|
||||
# Improved graceful shutdown (inspired by Celery worker drain + async best practices).
|
||||
# 1. Stop accepting new scheduled jobs.
|
||||
# 2. Attempt to let in-flight tasks finish or cancel them within timeout.
|
||||
# 3. Best-effort flush.
|
||||
scheduler.stop()
|
||||
try:
|
||||
from .dependencies import get_config_manager, get_task_manager
|
||||
tm = get_task_manager()
|
||||
# Access internal tracking (tasks are asyncio.Task objects)
|
||||
running = list(getattr(tm, '_async_tasks', {}).values())
|
||||
if running:
|
||||
cm = get_config_manager()
|
||||
graceful = getattr(cm, 'settings', None) if cm else None
|
||||
timeout = 30.0
|
||||
if graceful and hasattr(graceful, 'graceful_shutdown_timeout'):
|
||||
timeout = getattr(graceful, 'graceful_shutdown_timeout', 30.0)
|
||||
logger.reason(f"Draining {len(running)} running task(s) with timeout={timeout}s")
|
||||
done, pending = await asyncio.wait(running, timeout=timeout)
|
||||
for p in pending:
|
||||
p.cancel()
|
||||
if pending:
|
||||
await asyncio.gather(*pending, return_exceptions=True)
|
||||
logger.reason("Task drain complete")
|
||||
except Exception as _e:
|
||||
logger.explore("Graceful task drain during shutdown encountered error", error=str(_e))
|
||||
|
||||
|
||||
# #endregion lifespan
|
||||
|
||||
Reference in New Issue
Block a user