This is an automated email from the ASF dual-hosted git repository.

sbp pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git


The following commit(s) were added to refs/heads/main by this push:
     new 5e26ee1  Do not apply state subdirectory migrations when hot reloading
5e26ee1 is described below

commit 5e26ee1770921d5869cc60d45a8b297f9e1cdbb0
Author: Sean B. Palmer <[email protected]>
AuthorDate: Thu Jan 15 21:16:46 2026 +0000

    Do not apply state subdirectory migrations when hot reloading
---
 atr/blueprints/__init__.py |  1 -
 atr/db/__init__.py         |  1 -
 atr/server.py              | 78 +++++++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 77 insertions(+), 3 deletions(-)

diff --git a/atr/blueprints/__init__.py b/atr/blueprints/__init__.py
index e8be74e..e78afbe 100644
--- a/atr/blueprints/__init__.py
+++ b/atr/blueprints/__init__.py
@@ -59,5 +59,4 @@ def _check_blueprint(module: ModuleType, routes: list[str]) 
-> None:
 
 def _export_routes(state_dir: pathlib.Path) -> None:
     routes_file = state_dir / "cache" / "routes.json"
-    routes_file.parent.mkdir(parents=True, exist_ok=True)
     routes_file.write_text(json.dumps(sorted(_all_routes), indent=2))
diff --git a/atr/db/__init__.py b/atr/db/__init__.py
index b491589..050f307 100644
--- a/atr/db/__init__.py
+++ b/atr/db/__init__.py
@@ -772,7 +772,6 @@ class Session(sqlalchemy.ext.asyncio.AsyncSession):
 
 async def create_async_engine(app_config: type[config.AppConfig]) -> 
sqlalchemy.ext.asyncio.AsyncEngine:
     absolute_db_path = os.path.join(app_config.STATE_DIR, 
app_config.SQLITE_DB_PATH)
-    os.makedirs(os.path.dirname(absolute_db_path), exist_ok=True)
     # Three slashes are required before either a relative or absolute path
     sqlite_url = f"sqlite+aiosqlite:///{absolute_db_path}"
     # Use aiosqlite for async SQLite access
diff --git a/atr/server.py b/atr/server.py
index 56590ca..4c68373 100644
--- a/atr/server.py
+++ b/atr/server.py
@@ -299,7 +299,6 @@ def _app_setup_logging(app: base.QuartApp, config_mode: 
config.Mode, app_config:
 
     # Configure dedicated audit logger
     try:
-        
pathlib.Path(app_config.STORAGE_AUDIT_LOG_FILE).parent.mkdir(parents=True, 
exist_ok=True)
         audit_handler = logging.FileHandler(
             app_config.STORAGE_AUDIT_LOG_FILE,
             encoding="utf-8",
@@ -438,6 +437,50 @@ def _create_app(app_config: type[config.AppConfig]) -> 
base.QuartApp:
     return app
 
 
+def _get_parent_process_age() -> float:
+    import datetime
+    import subprocess
+    import time
+
+    ppid = os.getppid()
+
+    try:
+        with open(f"/proc/{ppid}/stat") as f:
+            stat = f.read().split()
+        starttime_ticks = int(stat[21])
+        ticks_per_sec = os.sysconf("SC_CLK_TCK")
+        with open("/proc/stat") as f:
+            for line in f:
+                if line.startswith("btime "):
+                    boot_time = int(line.split()[1])
+                    break
+            else:
+                return 0.0
+        process_start = boot_time + (starttime_ticks / ticks_per_sec)
+        return time.time() - process_start
+    except (FileNotFoundError, IndexError, ValueError, OSError):
+        pass
+
+    try:
+        result = subprocess.run(
+            ["ps", "-o", "lstart=", "-p", str(ppid)],
+            capture_output=True,
+            text=True,
+        )
+        if result.returncode == 0:
+            start_str = result.stdout.strip()
+            for fmt in ["%a %b %d %H:%M:%S %Y", "%a %d %b %H:%M:%S %Y"]:
+                try:
+                    dt = datetime.datetime.strptime(start_str, fmt)
+                    return time.time() - dt.timestamp()
+                except ValueError:
+                    continue
+    except OSError:
+        pass
+
+    return 0.0
+
+
 async def _initialise_test_environment() -> None:
     if not config.get().ALLOW_TESTS:
         return
@@ -546,6 +589,24 @@ def _migrate_file(old_path: pathlib.Path, new_path: 
pathlib.Path) -> None:
 
 def _migrate_state_directory(app_config: type[config.AppConfig]) -> None:
     state_dir = pathlib.Path(app_config.STATE_DIR)
+
+    pending = _pending_migrations(state_dir, app_config)
+    if pending:
+        parent_age = _get_parent_process_age()
+        if parent_age > 10.0:
+            import sys
+
+            print("=" * 70, file=sys.stderr)
+            print("ERROR: State directory migration required but hot reload 
detected", file=sys.stderr)
+            print(f"Parent process age: {parent_age:.1f}s", file=sys.stderr)
+            print("Pending migrations:", file=sys.stderr)
+            for p in pending:
+                print(f"  - {p}", file=sys.stderr)
+            print("", file=sys.stderr)
+            print("Please restart the server, not hot reload, to apply 
migrations", file=sys.stderr)
+            print("=" * 70, file=sys.stderr)
+            sys.exit(1)
+
     runtime_dir = state_dir / "runtime"
     runtime_dir.mkdir(parents=True, exist_ok=True)
     lock_path = runtime_dir / "migration.lock"
@@ -560,6 +621,21 @@ def _migrate_state_directory(app_config: 
type[config.AppConfig]) -> None:
             fcntl.flock(lock_file, fcntl.LOCK_UN)
 
 
+def _pending_migrations(state_dir: pathlib.Path, app_config: 
type[config.AppConfig]) -> list[str]:
+    pending = []
+    if (state_dir / "storage-audit.log").exists():
+        pending.append("storage-audit.log -> audit/storage-audit.log")
+    if (state_dir / "routes.json").exists():
+        pending.append("routes.json -> cache/routes.json")
+    if (state_dir / "user_session_cache.json").exists():
+        pending.append("user_session_cache.json -> 
cache/user_session_cache.json")
+    configured_path = app_config.SQLITE_DB_PATH
+    if configured_path in ("atr.db", "database/atr.db"):
+        if (state_dir / "atr.db").exists():
+            pending.append("atr.db -> database/atr.db")
+    return pending
+
+
 def _register_routes(app: base.QuartApp) -> None:
     # Add a global error handler to show helpful error messages with tracebacks
     @app.errorhandler(Exception)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to