Source code for simstack.util.runner_utils

import re
import subprocess
import uuid
from datetime import datetime
from typing import List

from odmantic import ObjectId
from simstack.core.context import context
from simstack.models.parameters import Resource
from simstack.models.slurm_info import SlurmInfo
from simstack.util.git_repository_status import get_git_status
from simstack.util.submit_to_watchdog import submit_to_watchdog

import logging

logger = logging.getLogger("runner_utils")


[docs] def make_git_status_list() -> List[str]: git_status_list = [] git_path_list = [context.config.project_root] for path in git_path_list: result = get_git_status(path) if result["branch"]: value = result["branch"] + "[" + result["short_hash"] + "]" if result["up_to_date"]: value += " (up-to-date)" else: value += " (behind " + str(result["behind"]) + " commits)" git_status_list.append(value) else: git_status_list.append("No branch found") return git_status_list
[docs] def run_squeue_for_job(job_id: str) -> str: result = subprocess.run( f"squeue -j {job_id}", shell=True, capture_output=True, text=True, timeout=30, ) return result.stdout
[docs] def get_job_info( job_id: str, task_id: ObjectId, resource: Resource ) -> SlurmInfo | None: """Get job information from SLURM queue using squeue""" try: stdout = run_squeue_for_job(job_id) # logger.info(f"task_id: {task_id} running squeue for job {job_id}: result: {stdout}") if not stdout or stdout.strip() == "": # after a while slurm will stop returning info for jobs that are no longer running return None lines = stdout.splitlines() # logger.info(f"task_id: {task_id} slurm info for job {job_id}: {lines}") if len(lines) < 2: return None # The first line is the header; the second line is the single info line info_line = lines[1].strip() if not info_line: return None # Split the single line into parts separated by whitespace parts = re.split(r"\s+", info_line) # logger.info(f"task_id: {task_id} slurm info for job {job_id}: {parts}") # Expected default squeue columns: # JOBID PARTITION NAME USER ST TIME NODES NODELIST(REASON) name = parts[2] if len(parts) > 2 else "" user = parts[3] if len(parts) > 3 else "" code = parts[4] if len(parts) > 4 else "" time_str = parts[5] if len(parts) > 5 else "" nodelist_raw = parts[7] if len(parts) > 7 else "" # Split nodelist on commas or whitespace, filter empties nodes = [n for n in re.split(r"[,\s]+", nodelist_raw) if n] slurm_info = SlurmInfo( node_registry=task_id, resource=resource, job_id=job_id, updated=datetime.now(), name=name, user=user, code=code, time=time_str, nodes=nodes, ) return slurm_info except Exception as e: logger.exception(f"Error getting job info for {job_id}: {str(e)}") return None
[docs] async def clean_slurm_info(resource: Resource, user: str | None = None) -> None: """Clean up old slurm info entries""" try: squeue_cmd = "squeue" if user: squeue_cmd += f" -u {user}" if context.config.docker: watchdog_id = f"slurm_{uuid.uuid4()}" queue_dir = context.config.workdir / "queue" result = submit_to_watchdog(squeue_cmd, watchdog_id, queue_dir=queue_dir) else: result = subprocess.run( squeue_cmd, shell=True, capture_output=True, text=True, timeout=30, ) if result.returncode == 0: active_job_ids = set() for line in result.stdout.splitlines(): parts = line.split() if not parts or parts[0] == "JOBID": continue active_job_ids.add(parts[0]) # Find all SLURM info entries for this resource # the user id is truncated on saving running_jobs = await context.db.find( SlurmInfo, SlurmInfo.resource.value == resource.value ) # logger.info(f"Found {running_jobs} slurm info entries for {resource}") # logger.info(f"Active job IDs: {active_job_ids} Slurm info IDs: {[job.job_id for job in running_jobs]}") # logger.info(f"User: {user} resource: {resource} ") # Delete entries for jobs that are no longer running for job in running_jobs: if job.job_id not in active_job_ids: await context.db.delete(job) logger.info(f"Deleted SLURM info for completed job {job.job_id}") except Exception as e: logger.exception(f"Error cleaning slurm info for {resource}: {str(e)}")