Skip to content

Runners

BaseCommandRunner

Base class for command runners. Subclass and implement run().

Set supports_max_retries to True if the runner's task backend supports per-task retry control (e.g. Celery's max_retries). When False, a warning is logged if a command has max_retries > 0.

stop

stop(execution: CommandExecution) -> None

Request a graceful stop of a RUNNING execution.

The base implementation sets the cooperative stop_requested flag (checked on the worker's output-flush heartbeat); backends with a way to signal the worker process override this to also send a SIGTERM-equivalent.

Source code in src/django_admin_runner/runners/__init__.py
def stop(self, execution: CommandExecution) -> None:
    """Request a graceful stop of a RUNNING *execution*.

    The base implementation sets the cooperative ``stop_requested``
    flag (checked on the worker's output-flush heartbeat); backends
    with a way to signal the worker process override this to also
    send a SIGTERM-equivalent.
    """
    from django_admin_runner.models import CommandExecution

    CommandExecution.objects.filter(
        pk=execution.pk, status=CommandExecution.Status.RUNNING
    ).update(stop_requested=True)

force_stop

force_stop(execution: CommandExecution) -> bool

Hard-kill the process running execution.

Returns True when a kill was performed. The base implementation is a no-op (unsupported backend); runners whose backend runs each task in its own process override this.

Source code in src/django_admin_runner/runners/__init__.py
def force_stop(self, execution: CommandExecution) -> bool:
    """Hard-kill the process running *execution*.

    Returns ``True`` when a kill was performed. The base
    implementation is a no-op (unsupported backend); runners whose
    backend runs each task in its own process override this.
    """
    return False

finalize_stale

finalize_stale(
    execution: CommandExecution,
) -> tuple[str, str] | None

Attribute why a dead worker's execution ended (best effort).

Called by the stale-run sweeper for RUNNING executions whose worker_pid no longer exists. Returns (status, note) — e.g. (TIMEOUT, "Killed by django-q2 timeout") — or None when the backend has nothing to say (the sweeper then marks the execution FAILED with a generic "worker lost" note). Must never raise; wrap backend lookups defensively.

Source code in src/django_admin_runner/runners/__init__.py
def finalize_stale(self, execution: CommandExecution) -> tuple[str, str] | None:
    """Attribute why a dead worker's execution ended (best effort).

    Called by the stale-run sweeper for RUNNING executions whose
    ``worker_pid`` no longer exists. Returns ``(status, note)`` —
    e.g. ``(TIMEOUT, "Killed by django-q2 timeout")`` — or ``None``
    when the backend has nothing to say (the sweeper then marks the
    execution FAILED with a generic "worker lost" note). Must never
    raise; wrap backend lookups defensively.
    """
    return None

create_schedule

create_schedule(schedule: ScheduledCommand) -> str

Materialize schedule into a native backend object.

Returns the backend reference to store in ScheduledCommand.backend_schedule_key.

Source code in src/django_admin_runner/runners/__init__.py
def create_schedule(self, schedule: ScheduledCommand) -> str:
    """Materialize *schedule* into a native backend object.

    Returns the backend reference to store in
    ``ScheduledCommand.backend_schedule_key``.
    """
    self._check_schedule_kind(schedule)
    raise ScheduleNotSupportedError(
        f"Backend {self.backend or type(self).__name__!r} does not support schedules."
    )

update_schedule

update_schedule(schedule: ScheduledCommand) -> str

Update the native object referenced by backend_schedule_key in place.

Returns the (possibly new) backend reference.

Source code in src/django_admin_runner/runners/__init__.py
def update_schedule(self, schedule: ScheduledCommand) -> str:
    """Update the native object referenced by ``backend_schedule_key`` in place.

    Returns the (possibly new) backend reference.
    """
    self._check_schedule_kind(schedule)
    raise ScheduleNotSupportedError(
        f"Backend {self.backend or type(self).__name__!r} does not support schedules."
    )

delete_schedule

delete_schedule(schedule: ScheduledCommand) -> None

Remove the native object referenced by backend_schedule_key.

Source code in src/django_admin_runner/runners/__init__.py
def delete_schedule(self, schedule: ScheduledCommand) -> None:
    """Remove the native object referenced by ``backend_schedule_key``."""
    self._check_schedule_kind(schedule)
    raise ScheduleNotSupportedError(
        f"Backend {self.backend or type(self).__name__!r} does not support schedules."
    )

schedule_next_run

schedule_next_run(schedule: ScheduledCommand)

Next run datetime of the native object, or None when unknown.

Source code in src/django_admin_runner/runners/__init__.py
def schedule_next_run(self, schedule: ScheduledCommand):
    """Next run datetime of the native object, or ``None`` when unknown."""
    return None

RunResult dataclass

RunResult(
    execution: CommandExecution,
    redirect_url: str,
    is_async: bool,
    backend: str,
    task_id: str = "",
)

Returned by every BaseCommandRunner.run() implementation.

SyncCommandRunner

Bases: BaseCommandRunner

Runs management commands inline in the current thread (no async).

DjangoTaskRunner

Bases: BaseCommandRunner

Enqueues commands using Django 6.0's built-in task system (django.tasks).

CeleryCommandRunner

Bases: BaseCommandRunner

Enqueues commands as Celery tasks.

Supports per-task retry control via Celery's autoretry_for and max_retries on the shared task.

stop

stop(execution) -> None

Graceful stop: stop flag (heartbeat) + revoke with SIGTERM.

Source code in src/django_admin_runner/runners/celery.py
def stop(self, execution) -> None:
    """Graceful stop: stop flag (heartbeat) + revoke with SIGTERM."""
    super().stop(execution)
    self._revoke(execution, "SIGTERM")

force_stop

force_stop(execution) -> bool

Hard kill: revoke with SIGKILL.

Source code in src/django_admin_runner/runners/celery.py
def force_stop(self, execution) -> bool:
    """Hard kill: revoke with SIGKILL."""
    if not execution.task_id:
        return False
    self._revoke(execution, "SIGKILL")
    return True

finalize_stale

finalize_stale(execution)

Attribute a swept execution via the Celery result backend.

REVOKED tasks (e.g. killed by a hard time_limit revoke) are reported as TIMEOUT; FAILURE as FAILED. Best effort — an unreachable broker yields None (generic FAILED).

Source code in src/django_admin_runner/runners/celery.py
def finalize_stale(self, execution):
    """Attribute a swept execution via the Celery result backend.

    REVOKED tasks (e.g. killed by a hard ``time_limit`` revoke) are
    reported as TIMEOUT; FAILURE as FAILED. Best effort — an
    unreachable broker yields ``None`` (generic FAILED).
    """
    if not execution.task_id:
        return None
    try:
        from celery.result import AsyncResult

        state = AsyncResult(execution.task_id).state
    except Exception:  # noqa: BLE001 - attribution must never break the sweep
        return None
    from django_admin_runner.models import CommandExecution

    if state == "REVOKED":
        return (
            CommandExecution.Status.TIMEOUT,
            "Revoked/killed by Celery (hard time limit or revoke).",
        )
    if state == "FAILURE":
        return (
            CommandExecution.Status.FAILED,
            "Worker died; Celery recorded a task failure.",
        )
    return None

DjangoQ2CommandRunner

Bases: BaseCommandRunner

Enqueues commands as django-q2 tasks.

django-q2 does not support per-task retry control — max_retries is ignored. Configure retries globally via Q_CLUSTER["max_attempts"].

stop

stop(execution) -> None

Graceful stop: stop flag (heartbeat) + SIGTERM to the worker PID.

Source code in src/django_admin_runner/runners/django_q2.py
def stop(self, execution) -> None:
    """Graceful stop: stop flag (heartbeat) + SIGTERM to the worker PID."""
    super().stop(execution)
    import signal

    self._signal_worker(execution, signal.SIGTERM)

force_stop

force_stop(execution) -> bool

Hard kill: SIGKILL to the (verified) worker PID.

Source code in src/django_admin_runner/runners/django_q2.py
def force_stop(self, execution) -> bool:
    """Hard kill: SIGKILL to the (verified) worker PID."""
    import signal

    return self._signal_worker(execution, signal.SIGKILL)

finalize_stale

finalize_stale(execution)

Attribute a swept execution via django-q2's Task records.

The q2 sentinel writes a failure record for tasks it killed (e.g. for exceeding Q_CLUSTER["timeout"]). Best effort — a missing/unreachable record yields None (generic FAILED).

Source code in src/django_admin_runner/runners/django_q2.py
def finalize_stale(self, execution):
    """Attribute a swept execution via django-q2's Task records.

    The q2 sentinel writes a failure record for tasks it killed
    (e.g. for exceeding ``Q_CLUSTER["timeout"]``). Best effort — a
    missing/unreachable record yields ``None`` (generic FAILED).
    """
    if not execution.task_id:
        return None
    from django_admin_runner.models import CommandExecution

    try:
        from django_q.models import Task

        task = Task.objects.filter(name=execution.task_id).first()
    except Exception:  # noqa: BLE001 - attribution must never break the sweep
        return None
    if task is None or task.success or not task.stopped:
        return None
    result = str(task.result or "")
    if "time" in result.lower():
        return (
            CommandExecution.Status.TIMEOUT,
            f"Killed by django-q2 timeout: {result}",
        )
    return (
        CommandExecution.Status.FAILED,
        f"Worker killed; django-q2 recorded failure: {result}",
    )

get_runner

get_runner() -> BaseCommandRunner

Instantiate and return the runner configured by ADMIN_RUNNER_BACKEND.

Possible values: - "django" (default) — :class:~django_admin_runner.runners.django_tasks.DjangoTaskRunner - "sync" — :class:~django_admin_runner.runners.sync.SyncCommandRunner - "celery" — :class:~django_admin_runner.runners.celery.CeleryCommandRunner - "django-q2" — :class:~django_admin_runner.runners.django_q2.DjangoQ2CommandRunner - dotted path — any :class:BaseCommandRunner subclass

Source code in src/django_admin_runner/runners/__init__.py
def get_runner() -> BaseCommandRunner:
    """Instantiate and return the runner configured by ``ADMIN_RUNNER_BACKEND``.

    Possible values:
    - ``"django"`` (default) — :class:`~django_admin_runner.runners.django_tasks.DjangoTaskRunner`
    - ``"sync"`` — :class:`~django_admin_runner.runners.sync.SyncCommandRunner`
    - ``"celery"`` — :class:`~django_admin_runner.runners.celery.CeleryCommandRunner`
    - ``"django-q2"`` — :class:`~django_admin_runner.runners.django_q2.DjangoQ2CommandRunner`
    - dotted path — any :class:`BaseCommandRunner` subclass
    """
    from django.conf import settings

    backend = getattr(settings, "ADMIN_RUNNER_BACKEND", "django")

    if backend == "sync":
        from .sync import SyncCommandRunner

        return SyncCommandRunner()
    if backend == "celery":
        from .celery import CeleryCommandRunner

        return CeleryCommandRunner()
    if backend == "django-q2":
        from .django_q2 import DjangoQ2CommandRunner

        return DjangoQ2CommandRunner()
    if backend == "django":
        from .django_tasks import DjangoTaskRunner

        return DjangoTaskRunner()

    # Dotted import path to a custom runner class
    import importlib

    module_path, class_name = backend.rsplit(".", 1)
    module = importlib.import_module(module_path)
    runner_class = getattr(module, class_name)
    return runner_class()