Quickstart

Five minutes from zero to crash recovery. Every command below is runnable verbatim from a checkout.

Set up a scratch project

QS=$(mktemp -d)

All commands use --db $QS/queue.db so nothing touches a real queue.

Add jobs

python3 -m spoolctl add --db $QS/queue.db -- echo "hello from spoolctl"
python3 -m spoolctl add --db $QS/queue.db -- echo "second job"
python3 -m spoolctl add --db $QS/queue.db -- sleep 3
Added job 1
Added job 2
Added job 3

Check status

python3 -m spoolctl status --db $QS/queue.db
canceled 0  dead 0  done 0  failed 0  queued 3  running 0

Three jobs queued, none running yet.

Run a worker

python3 -m spoolctl work --db $QS/queue.db

The worker claims and executes jobs one at a time:

spoolctl: job 1 attempt 1 succeeded (0.0s) -> done
spoolctl: job 2 attempt 1 succeeded (0.0s) -> done
spoolctl: job 3 attempt 1 succeeded (3.0s) -> done

Stop it with Ctrl-C when the queue is empty.

Read output

python3 -m spoolctl output --db $QS/queue.db 1
=== job 1 attempt 1 stdout ===
hello from spoolctl
=== job 1 attempt 1 stderr ===

stdout and stderr are captured per attempt and persist across retries.

Crash recovery

This is what spoolctl is for. Add a long-running job and kill the worker mid-execution:

python3 -m spoolctl add --db $QS/queue.db -- sleep 120
python3 -m spoolctl work --db $QS/queue.db &

Wait a few seconds for the worker to claim the job, then check status:

python3 -m spoolctl status --db $QS/queue.db
canceled 0  dead 0  done 3  failed 0  queued 0  running 1

Now kill the worker with SIGKILL – no cleanup possible:

kill -9 %1

The job still shows as running because no one has checked yet:

python3 -m spoolctl status --db $QS/queue.db
canceled 0  dead 0  done 3  failed 0  queued 0  running 1

Start a new worker. It will detect the dead worker via heartbeat staleness and PID liveness checks, reap the abandoned attempt, and re-queue the job:

python3 -m spoolctl work --db $QS/queue.db &

After roughly 30 seconds (the reap threshold – see Guarantees for why this delay exists and what it protects against), the new worker reaps and re-executes:

spoolctl: reaped job 4 (worker pid <PID> died); now queued

The job is back in the queue with its crash count incremented. The new worker claims and runs it again.

Clean up

rm -rf $QS

Next steps

  • Concepts for the object model and state machine
  • Guarantees for the full reliability story
  • Agent Guide for building scripts and agents on top of spoolctl