Source code for dandi_compute_code.queue._process_queue

import logging
import pathlib
import random
import time
from typing import Literal

from ._count_running_aind_ephys_pipeline_jobs import _count_running_aind_ephys_pipeline_jobs
from ._submit_next import _submit_next

_log = logging.getLogger(__name__)


[docs] def process_queue( *, queue_directory: pathlib.Path, processing_directory: pathlib.Path, max_concurrent_aind_jobs: int = 2, jitter_seconds: float = 30.0, test: bool = False, ) -> Literal["submitted", "no-pending", "slots-unavailable"]: """ Submit jobs from ``state.jsonl`` up to ``max_concurrent_aind_jobs`` total running ``AIND-Ephys-Pipeline`` SLURM jobs. If ``state.jsonl`` is absent, a :class:`FileNotFoundError` is raised. If ``state.jsonl`` exists but is empty, a warning is emitted and the invocation returns without submitting jobs. Otherwise ``squeue --me`` is checked for currently running ``AIND-Ephys-Pipeline`` jobs, and up to the difference from ``max_concurrent_aind_jobs`` jobs are submitted. A random delay of up to *jitter_seconds* is applied before any work is done to spread out concurrent invocations and avoid thundering-herd submission bursts. :param queue_directory: Path to the queue root directory. :type queue_directory: pathlib.Path :param processing_directory: Path to the directory used for temporary working trees during job submission. :type processing_directory: pathlib.Path :param max_concurrent_aind_jobs: Maximum number of ``AIND-Ephys-Pipeline`` jobs allowed to be running concurrently before new submissions are skipped. :type max_concurrent_aind_jobs: int :param jitter_seconds: Maximum number of seconds to sleep before proceeding. A uniformly random duration between ``0`` and *jitter_seconds* is chosen each invocation. Set to ``0`` to disable jitter entirely. :type jitter_seconds: float :param test: If ``True``, preserve temporary processing directories on success. :type test: bool :returns: ``"submitted"`` when one or more jobs were submitted, ``"no-pending"`` when no pending jobs were available to submit, and ``"slots-unavailable"`` when submission was skipped because no queue slots were available. :rtype: Literal["submitted", "no-pending", "slots-unavailable"] :raises FileNotFoundError: If ``state.jsonl`` is not found in *queue_directory*. :raises ValueError: If *jitter_seconds* is negative. """ if jitter_seconds < 0: message = "jitter_seconds must be non-negative" raise ValueError(message) if jitter_seconds > 0: delay = random.uniform(0, jitter_seconds) _log.info("Sleeping %.2f seconds (jitter) before processing queue", delay) time.sleep(delay) state_file = queue_directory / "state.jsonl" if not state_file.exists(): message = f"State file not found: {state_file}" raise FileNotFoundError(message) if max_concurrent_aind_jobs < 1: message = "max_concurrent_aind_jobs must be at least 1" raise ValueError(message) if not state_file.read_text().strip(): _log.info(f"No entries in {state_file}") return "no-pending" running_count = _count_running_aind_ephys_pipeline_jobs() available_slots = max(0, max_concurrent_aind_jobs - running_count) if available_slots < 1: return "slots-unavailable" submitted_any = _submit_next( processing_directory=processing_directory, max_submissions=available_slots, test=test, ) return "submitted" if submitted_any else "no-pending"