pyantz.infrastructure.distributed_queue.sql package#
Submodules#
pyantz.infrastructure.distributed_queue.sql.orm module#
SQLAlchemy ORM models for the pyantz SQLite job queue.
The queue is backed by four tables:
job_queue — ordered list of job IDs waiting to be dequeued.
queue_items — chunked JSON representations of each job config.
dependencies — directed edges of the job dependency graph.
job_status — current lifecycle state of every known job.
Large job configs are split into CHUNKSIZE-character chunks so that
SQLite’s per-column string limit is not exceeded.
- class Base(**kwargs: Any)[source]#
Bases:
DeclarativeBaseThe base class for our ORM.
- Parameters:
kwargs (Any)
- _sa_registry: ClassVar[registry] = <sqlalchemy.orm.decl_api.registry object>#
- CHUNKSIZE: Final[int] = 2000#
Number of characters of a job’s config stored per row; multiple rows might make up one job.
- class DependencyTable(**kwargs)[source]#
Bases:
BaseThe dependency table holds the dependency graph of the jobs.
- _sa_class_manager = {'depends_on': <sqlalchemy.orm.attributes.InstrumentedAttribute object>, 'job_id': <sqlalchemy.orm.attributes.InstrumentedAttribute object>}#
- depends_on: Mapped[str]#
Id of the job it waits for.
- job_id: Mapped[str]#
Id of the job which waits.
- class JobConfigTable(**kwargs)[source]#
Bases:
BaseThe job config table holds the configurations for the jobs in the queue.
Note that a job may span multiple tuples (rows) if the config is very large. In that case, it will have multiple sub-indices.
- _sa_class_manager = {'job_config_content': <sqlalchemy.orm.attributes.InstrumentedAttribute object>, 'job_id': <sqlalchemy.orm.attributes.InstrumentedAttribute object>, 'job_subindex': <sqlalchemy.orm.attributes.InstrumentedAttribute object>}#
- job_config_content: Mapped[str]#
A chunk of the job’s config, as JSON, of at most
CHUNKSIZEcharacters.
- job_id: Mapped[str]#
Id of the job.
- job_subindex: Mapped[int]#
Position of this chunk in the job’s config.
- class JobQueue(**kwargs)[source]#
Bases:
BaseActual queue, holds jobs as they’re added.
- _sa_class_manager = {'job_id': <sqlalchemy.orm.attributes.InstrumentedAttribute object>, 'q_index': <sqlalchemy.orm.attributes.InstrumentedAttribute object>}#
- job_id: Mapped[str]#
Id of the queued job.
- q_index: Mapped[int]#
Position in the queue; jobs are dequeued in increasing order.
- class StatusTable(**kwargs)[source]#
Bases:
BaseThe status table holds the current status of each job.
- _sa_class_manager = {'details': <sqlalchemy.orm.attributes.InstrumentedAttribute object>, 'job_id': <sqlalchemy.orm.attributes.InstrumentedAttribute object>, 'job_status': <sqlalchemy.orm.attributes.InstrumentedAttribute object>, 'worker_id': <sqlalchemy.orm.attributes.InstrumentedAttribute object>}#
- details: Mapped[str | None]#
Why the job is in this status, for example the error message for failures.
- job_id: Mapped[str]#
Id of the job.
- job_status: Mapped[int]#
The job’s
JobStatus.
- worker_id: Mapped[int | None]#
The worker (its process id) which is running the job, while it is RUNNING.
pyantz.infrastructure.distributed_queue.sql.sql_queue module#
Queue running with sql.
SqliteQueue keeps the queue in a SQLite file (see
orm for its tables), so
several worker processes can share it.
- BUSY_TIMEOUT_SECONDS = 30#
How long a connection waits for another process to release the database lock.
- class SqliteQueue(database_file: Path)[source]#
Bases:
QueueInterfaceQueue of jobs stored in a SQLite database file.
Example
from pathlib import Path from pyantz.infrastructure.config import JobWithContext from pyantz.infrastructure.distributed_queue.common import JobStatus from pyantz.infrastructure.distributed_queue.sql import SqliteQueue queue = SqliteQueue(Path("example.db")) job = JobWithContext( job_id="a", function="pyantz.jobs.testing.nop.do_nothing", parameters={} ) queue.add_job(job, None) result = queue.get_job() print(result.payload.type_, result.payload.job.job_id) queue.update_job_status("a", JobStatus.SUCCESS) print(queue.get_job().payload.type_)
job a complete
- Parameters:
database_file (Path)
- _abc_impl = <_abc._abc_data object>#
- static _check_empty_type(sesh: Session)[source]#
Check why the queue is empty (pending? or dead/complete?).
- Parameters:
sesh (
Session) – current sqlalchemy session- Returns:
pending if jobs are running, pending or ready; otherwise complete (every job is ERROR or SUCCESS).
- Return type:
- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
- _create_engine(database_file: Path)[source]#
Create the engine and return it.
Connections use write-ahead logging and wait for locks rather than failing immediately, so many worker processes can share one queue file. Note that WAL needs a local filesystem (not NFS) for the queue file.
- Parameters:
database_file (
Path) – the SQLite file holding the queue.- Returns:
the engine, which sets up each new connection.
- Return type:
Engine- Raises:
None. –
- _engine: Engine#
Engine connecting to
database_file.
- static _generate_jobs_report(sesh: Session)[source]#
Create a report of the current jobs on the queue.
- Parameters:
sesh (
Session) – the current sqlalchemy session.- Returns:
the number of jobs in each status, and in total.
- Return type:
- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
- static _get_job_config(sesh: Session, job_id: str)[source]#
Get the job config by piecing the various chunks together.
- Parameters:
sesh (
Session) – the current sqlalchemy session.job_id (
str) – the job to load.
- Returns:
the job.
- Return type:
- Raises:
RuntimeError – if the job has no stored config.
pydantic.ValidationError – if the stored config isn’t a valid job.
- _insert_job(job_config: JobWithContext, parent_job_id: str | UUID | None)[source]#
Insert all rows for a job in one transaction.
- Parameters:
job_config (
JobWithContext) – the job to insert, as PENDING.parent_job_id (
str|UUID|None) – the job which submitted it, whose dependents will wait for this job too; None for no parent.
- Return type:
None- Returns:
None.
- Raises:
sqlalchemy.exc.IntegrityError – if the job id is already present
- _is_protocol = False#
- _try_dequeue()[source]#
Dequeue the next READY job and mark it RUNNING in one transaction.
- Parameters:
None.
- Returns:
- the result of the dequeue, or None if the job
dequeued had a config which could not be loaded. That job is marked ERROR rather than being lost.
- Return type:
GetJobReturn|None- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
- _update_job_statuses()[source]#
Update statuses of all the jobs in the queue.
Propagates errors and successes down the dependency chain.
Specifically, if a job status is ERROR, set its immediate dependents to ERROR; with each call, this will propagate down the dependency chain. And, once every job a PENDING job depends on is SUCCESS, set it to READY.
- Parameters:
None.
- Return type:
None- Returns:
None.
- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
- add_job(job_config: JobWithContext, parent_job_id: str | UUID | None)[source]#
Add the provided job to the queue.
- Parameters:
job_config (
JobWithContext) – job to enqueueparent_job_id (
str|UUID|None) – jobs added inherit their parent dependency “responsibilities”. So a job submitting new jobs should pass its own id to the function. For example if B depends on A and A adds C then B will depend on C. If, however, the user DOESN’T want that (B should run parallel to C), then they can pass “None” as the parent_job_id.
- Returns:
- True if successfully added; False if unable to add
(for example, a job with the same id already exists)
- Return type:
bool- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
Example
from pathlib import Path from pyantz.infrastructure.config import JobWithContext from pyantz.infrastructure.distributed_queue.sql import SqliteQueue queue = SqliteQueue(Path("add_job.db")) nop = "pyantz.jobs.testing.nop.do_nothing" parent = JobWithContext(job_id="parent", function=nop, parameters={}) after = JobWithContext( job_id="after", depends_on={"parent"}, function=nop, parameters={} ) child = JobWithContext(job_id="child", function=nop, parameters={}) print(queue.add_job(parent, None), queue.add_job(parent, None)) queue.add_job(after, None) # "after" now waits for "child" too, as "parent" submitted it queue.add_job(child, parent_job_id="parent")
True False
- change_config_in_place(job_config: JobWithContext)[source]#
Change the config of a job.
In practice, this is used to restart jobs during error recovery. A worker will update the num_attempted_runs and change the job. Then it will set its status to PENDING.
The queue will then set PENDING to READY on its next check and return the job to be re-run (the job is put back in the queue if a worker had already taken it out).
- Parameters:
job_config (
JobWithContext) – configuration to run- Return type:
None- Returns:
None.
- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
Example
from pathlib import Path from pyantz.infrastructure.config import JobWithContext from pyantz.infrastructure.distributed_queue.common import JobStatus from pyantz.infrastructure.distributed_queue.sql import SqliteQueue queue = SqliteQueue(Path("retry.db")) job = JobWithContext( job_id="a", function="pyantz.jobs.testing.nop.do_nothing", parameters={}, ) queue.add_job(job, None) taken = queue.get_job().payload.job retry = taken.model_copy(update={"num_attempted_runs": 1}) queue.change_config_in_place(retry) queue.update_job_status("a", JobStatus.PENDING) print(queue.get_job().payload.job.num_attempted_runs)
1
- database_file: Path#
The SQLite file holding the queue.
- fail_deadlocked_jobs()[source]#
Mark PENDING jobs as ERROR if nothing is READY or RUNNING.
Callers should confirm the state persists across a few polls: a job that just finished may not have had its dependents promoted yet.
- Parameters:
None.
- Returns:
ids of the jobs which were failed
- Return type:
list[str]- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
Example
from pathlib import Path from pyantz.infrastructure.config import JobWithContext from pyantz.infrastructure.distributed_queue.sql import SqliteQueue queue = SqliteQueue(Path("deadlock.db")) stuck = JobWithContext( job_id="stuck", depends_on={"never_added"}, function="pyantz.jobs.testing.nop.do_nothing", parameters={}, ) queue.add_job(stuck, None) print(queue.fail_deadlocked_jobs())
['stuck']
- fail_jobs_of_worker(worker_id: int)[source]#
Mark the RUNNING jobs of a worker which died as ERROR.
The jobs depending on them fail too.
- Parameters:
worker_id (
int) – the worker (its process id) which died.- Returns:
ids of the jobs which were failed
- Return type:
list[str]- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
Example
from pathlib import Path from pyantz.infrastructure.config import JobWithContext from pyantz.infrastructure.distributed_queue.sql import SqliteQueue queue = SqliteQueue(Path("worker.db")) nop = "pyantz.jobs.testing.nop.do_nothing" job_a = JobWithContext(job_id="a", function=nop, parameters={}) queue.add_job(job_a, None) queue.get_job() # a worker takes the job... queue.set_job_worker("a", 1234) print(queue.fail_jobs_of_worker(1234)) # ...and dies
['a']
- get_failed_jobs()[source]#
Get the status (with the error details) of every job which errored.
- Parameters:
None.
- Returns:
the failed jobs, sorted by job id.
- Return type:
list[JobStatusReport]- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
Example
from pathlib import Path from pyantz.infrastructure.config import JobWithContext from pyantz.infrastructure.distributed_queue.common import JobStatus from pyantz.infrastructure.distributed_queue.sql import SqliteQueue queue = SqliteQueue(Path("failed.db")) nop = "pyantz.jobs.testing.nop.do_nothing" job_a = JobWithContext(job_id="a", function=nop, parameters={}) queue.add_job(job_a, None) queue.add_job( JobWithContext( job_id="b", depends_on={"a"}, function=nop, parameters={} ), None, ) queue.update_job_status("a", JobStatus.ERROR, "it broke") for report in queue.get_failed_jobs(): print(report.job_id, report.status_details)
a it broke b A job this job depends on failed
- get_job()[source]#
Return a job from the queue.
The job is taken out of the queue and marked RUNNING. A job whose config can’t be loaded is marked ERROR and skipped.
- Parameters:
None.
- Returns:
- either
A job to run
Pending to indicate that nothing is ready but to keep waiting
Complete to tell the worker to kill itself
- Return type:
- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
Example
from pathlib import Path from pyantz.infrastructure.config import JobWithContext from pyantz.infrastructure.distributed_queue.sql import SqliteQueue queue = SqliteQueue(Path("get_job.db")) nop = "pyantz.jobs.testing.nop.do_nothing" first = JobWithContext(job_id="first", function=nop, parameters={}) second = JobWithContext( job_id="second", depends_on={"first"}, function=nop, parameters={} ) queue.add_job(first, None) queue.add_job(second, None) print(queue.get_job().payload.job.job_id) print(queue.get_job().payload.type_) # second waits for first
first pending
- get_job_ids_with_status(status: JobStatus)[source]#
Get the ids of the jobs which currently have the status, sorted.
- Parameters:
status (
JobStatus) – the status to look for.- Returns:
the ids of the jobs with the status, sorted.
- Return type:
list[str]- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
Example
from pathlib import Path from pyantz.infrastructure.config import JobWithContext from pyantz.infrastructure.distributed_queue.common import JobStatus from pyantz.infrastructure.distributed_queue.sql import SqliteQueue queue = SqliteQueue(Path("with_status.db")) nop = "pyantz.jobs.testing.nop.do_nothing" for job_id in ("b", "a"): queue.add_job( JobWithContext(job_id=job_id, function=nop, parameters={}), None, ) print(queue.get_job_ids_with_status(JobStatus.READY))
['a', 'b']
- get_job_status(job_id: str | UUID)[source]#
Get the current status of a job, and the details given with it.
- Parameters:
job_id (
str|UUID) – job to look up- Returns:
the job’s status and its details.
- Return type:
- Raises:
KeyError – if there is no such job
Example
from pathlib import Path from pyantz.infrastructure.distributed_queue.sql import SqliteQueue queue = SqliteQueue(Path("status.db")) try: queue.get_job_status("unknown") except KeyError: print("no such job")
no such job
- get_jobs_report()[source]#
Get the jobs report.
Useful for debugging or determining the size of the queue.
- Parameters:
None.
- Returns:
report of the jobs and their states.
- Return type:
- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
Example
from pathlib import Path from pyantz.infrastructure.config import JobWithContext from pyantz.infrastructure.distributed_queue.sql import SqliteQueue queue = SqliteQueue(Path("report.db")) nop = "pyantz.jobs.testing.nop.do_nothing" job_a = JobWithContext(job_id="a", function=nop, parameters={}) queue.add_job(job_a, None) queue.add_job( JobWithContext( job_id="b", depends_on={"a"}, function=nop, parameters={} ), None, ) report = queue.get_jobs_report() print(report.total, report.ready, report.pending)
2 1 1
- set_job_worker(job_id: str | UUID, worker_id: int)[source]#
Record which worker is running the job.
- Parameters:
job_id (
str|UUID) – the job being run.worker_id (
int) – the worker (its process id) running it.
- Return type:
None- Returns:
None.
- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
- update_job_status(job_id: str | UUID, status: JobStatus, details: str | None = None)[source]#
Update the status of the job.
- Parameters:
job_id (
str|UUID) – job to be updatedstatus (
JobStatus) – new state after this updatedetails (
str|None) – additional information about status eg., may be the error message for debugging.
- Return type:
None- Returns:
None.
- Raises:
sqlalchemy.exc.OperationalError – if the database can’t be used.
Example
from pathlib import Path from pyantz.infrastructure.config import JobWithContext from pyantz.infrastructure.distributed_queue.common import JobStatus from pyantz.infrastructure.distributed_queue.sql import SqliteQueue queue = SqliteQueue(Path("update.db")) nop = "pyantz.jobs.testing.nop.do_nothing" job_a = JobWithContext(job_id="a", function=nop, parameters={}) queue.add_job(job_a, None) queue.update_job_status("a", JobStatus.ERROR, "out of memory") report = queue.get_job_status("a") print(report.status.name, report.status_details)
ERROR out of memory
Module contents#
Distributed queue that supports multiple processes accessing it simultaneously.
This version relies on sqlite (mostly sqlite).