dmChatbotBackend / src /utils /run_manager.py
github-actions
Auto deploy from GitHub
cb505ff
Raw History Blame Contribute Delete
7.45 kB
import json
import os
import re
from datetime import datetime, timezone
from typing import Dict, Any, List, Optional
_CURRENT_RUN_NUMBER: Optional[int] = None
DEFAULT_MAX_RUNS = 10
def _get_default_logs_dir() -> str:
"""Return the absolute path to the backend/logs directory."""
utils_dir = os.path.dirname(os.path.abspath(__file__))
backend_dir = os.path.dirname(os.path.dirname(utils_dir))
return os.path.join(backend_dir, "logs")
def _read_counter(counter_file: str) -> Optional[int]:
"""Read the last run number from the counter file if it exists."""
if not os.path.exists(counter_file):
return None
try:
with open(counter_file, "r", encoding="utf-8") as f:
content = f.read().strip()
if content.isdigit():
return int(content)
except Exception:
pass
return None
def _find_max_existing_run(logs_dir: str) -> int:
"""Scan existing run_*.log files to find the highest run number."""
if not os.path.exists(logs_dir):
return 0
pattern = re.compile(r"^run_(\d+)\.log$")
max_num = 0
for name in os.listdir(logs_dir):
match = pattern.match(name)
if match:
max_num = max(max_num, int(match.group(1)))
return max_num
def prune_old_runs(logs_dir: str, max_runs: int = DEFAULT_MAX_RUNS) -> List[str]:
"""
Ensure only the latest `max_runs` run files are kept.
Deletes the oldest run_*.log files exceeding the limit.
Returns the list of deleted filenames.
"""
if not os.path.exists(logs_dir) or max_runs <= 0:
return []
pattern = re.compile(r"^run_(\d+)\.log$")
run_files = []
for name in os.listdir(logs_dir):
match = pattern.match(name)
if match:
run_files.append((int(match.group(1)), name))
# Sort ascending by run number
run_files.sort(key=lambda x: x[0])
deleted = []
if len(run_files) > max_runs:
excess_count = len(run_files) - max_runs
to_delete = run_files[:excess_count]
for _, name in to_delete:
file_path = os.path.join(logs_dir, name)
try:
os.remove(file_path)
deleted.append(name)
except Exception:
pass
_prune_history_file(logs_dir, max_runs)
return deleted
def _prune_history_file(logs_dir: str, max_runs: int):
"""Keep only the last `max_runs` entries in run_history.json."""
history_file = os.path.join(logs_dir, "run_history.json")
if not os.path.exists(history_file):
return
try:
with open(history_file, "r", encoding="utf-8") as f:
history = json.load(f)
if isinstance(history, list) and len(history) > max_runs:
history = history[-max_runs:]
with open(history_file, "w", encoding="utf-8") as f:
json.dump(history, f, indent=2)
except Exception:
pass
def _record_run_history(logs_dir: str, run_number: int, log_file: str, max_runs: int):
"""Append a record for this run in run_history.json and prune older entries."""
history_file = os.path.join(logs_dir, "run_history.json")
history = []
if os.path.exists(history_file):
try:
with open(history_file, "r", encoding="utf-8") as f:
loaded = json.load(f)
if isinstance(loaded, list):
history = loaded
except Exception:
history = []
history.append({
"run_number": run_number,
"timestamp": datetime.now(timezone.utc).isoformat(),
"log_file": os.path.basename(log_file),
})
if len(history) > max_runs:
history = history[-max_runs:]
try:
with open(history_file, "w", encoding="utf-8") as f:
json.dump(history, f, indent=2)
except Exception:
pass
def initialize_run(
logs_dir: Optional[str] = None,
max_runs: Optional[int] = None,
force_new: bool = False,
) -> int:
"""
Initialize the run for this backend execution:
- Increments the persistent run counter by 1 (or starts at 1).
- Prunes older runs so only the last `max_runs` (default 10) are kept.
- Creates/touches the log file for this run.
- Caches the current run number for the process.
"""
global _CURRENT_RUN_NUMBER
if _CURRENT_RUN_NUMBER is not None and not force_new:
return _CURRENT_RUN_NUMBER
if logs_dir is None:
logs_dir = _get_default_logs_dir()
if max_runs is None:
env_max = os.getenv("MAX_LOG_RUNS", "").strip()
max_runs = int(env_max) if env_max.isdigit() else DEFAULT_MAX_RUNS
os.makedirs(logs_dir, exist_ok=True)
counter_file = os.path.join(logs_dir, "run_counter.txt")
last_run = _read_counter(counter_file)
if last_run is None:
last_run = _find_max_existing_run(logs_dir)
new_run = last_run + 1
# Persist the new run number
with open(counter_file, "w", encoding="utf-8") as f:
f.write(str(new_run))
_CURRENT_RUN_NUMBER = new_run
# Create run log file
run_log_file = os.path.join(logs_dir, f"run_{new_run}.log")
if not os.path.exists(run_log_file):
with open(run_log_file, "a", encoding="utf-8"):
pass
# Prune older runs
prune_old_runs(logs_dir, max_runs=max_runs)
# Record in history
_record_run_history(logs_dir, new_run, run_log_file, max_runs)
return new_run
def get_current_run_number(logs_dir: Optional[str] = None) -> int:
"""Return the current cached run number, or initialize one if not yet done."""
global _CURRENT_RUN_NUMBER
if _CURRENT_RUN_NUMBER is None:
return initialize_run(logs_dir=logs_dir)
return _CURRENT_RUN_NUMBER
def get_current_run_log_file(logs_dir: Optional[str] = None) -> str:
"""Return the absolute path to the current run's log file."""
if logs_dir is None:
logs_dir = _get_default_logs_dir()
run_num = get_current_run_number(logs_dir=logs_dir)
return os.path.join(logs_dir, f"run_{run_num}.log")
def get_run_info(logs_dir: Optional[str] = None, max_runs: Optional[int] = None) -> Dict[str, Any]:
"""Return structured metadata about the current and recent runs."""
if logs_dir is None:
logs_dir = _get_default_logs_dir()
if max_runs is None:
env_max = os.getenv("MAX_LOG_RUNS", "").strip()
max_runs = int(env_max) if env_max.isdigit() else DEFAULT_MAX_RUNS
current_run = get_current_run_number(logs_dir=logs_dir)
# Inspect present run files
pattern = re.compile(r"^run_(\d+)\.log$")
present_runs = []
if os.path.exists(logs_dir):
for name in os.listdir(logs_dir):
match = pattern.match(name)
if match:
full_path = os.path.join(logs_dir, name)
stat = os.stat(full_path)
present_runs.append({
"run_number": int(match.group(1)),
"filename": name,
"size_bytes": stat.st_size,
"modified_at": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(),
})
present_runs.sort(key=lambda x: x["run_number"])
return {
"run_number": current_run,
"max_runs": max_runs,
"current_log_file": os.path.basename(get_current_run_log_file(logs_dir=logs_dir)),
"available_runs": present_runs,
}