Skip to main content

A bounded, fault-tolerant async worker pipeline.

Project description

taskforeman

A bounded, fault-tolerant async worker pipeline for Python 3.10+.

Designed for workloads that require stateful, long-lived workers — browser automation, API clients, database connection pools, or any workload where the worker itself needs lifecycle management. Failures escalate automatically through three levels: retry → error accumulation → circuit breaker.


Installation

pip install taskforeman

Quick start

import asyncio
from taskforeman import BaseWorker, WorkerManager, WorkerPoolSettings, WorkerException

class MyWorker(BaseWorker):
    async def start(self) -> None:
        self.driver = await launch_my_engine()
        self._running = True

    async def stop(self) -> None:
        await self.driver.close()
        self._running = False

async def my_task(worker: MyWorker, url: str) -> None:
    page = await worker.driver.goto(url)
    if page.status == 429:
        raise WorkerException("rate limited", error=True)
    await db.insert(page.content)

async def main():
    settings = WorkerPoolSettings(max_size=4, rpm=60)
    async with WorkerManager(MyWorker, settings) as manager:
        for url in urls:
            await manager.enqueue(my_task, url)
        await manager.join()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass

Construction styles

Managed lifecycle

The manager instantiates and owns all workers. Use this when all workers are identical:

async with WorkerManager(MyWorker, pool_settings) as manager:
    await manager.enqueue(my_task, item)
    await manager.join()

Bring your own workers

Pre-construct workers with individual configuration, then hand them to the manager. Use this when workers need different proxies, credentials, or profiles:

from dataclasses import dataclass
from taskforeman import WorkerSettings

@dataclass
class MyWorkerSettings(WorkerSettings):
    proxy: str = ""
    name: str = "worker"

workers = [
    MyWorker(MyWorkerSettings(proxy="residential-1.example.com", name="worker-0")),
    MyWorker(MyWorkerSettings(proxy="residential-2.example.com", name="worker-1")),
]

async with WorkerManager.from_workers(workers, pool_settings) as manager:
    await manager.enqueue(my_task, item)
    await manager.join()

Worker indices are assigned automatically by the manager. Access them via worker.index and worker.worker_settings inside your task callables.


Settings

Settings can be provided as nested dataclasses (recommended for production) or as flat keyword arguments (convenient for scripts).

Nested

from taskforeman import (
    WorkerPoolSettings,
    PoolSettings,
    RetrySettings,
    ErrorSettings,
    RateLimitSettings,
    CircuitBreakerSettings,
)

settings = WorkerPoolSettings(
    task_timeout    = 30.0,
    queue_maxsize   = 100,
    pool            = PoolSettings(max_size=4, restart_every=50),
    retry           = RetrySettings(max_attempts=3, timeout=5.0, backoff=2.0),
    error           = ErrorSettings(max_accumulated=3),
    rate_limit      = RateLimitSettings(requests_per_minute=120, burst=5),
    circuit_breaker = CircuitBreakerSettings(max_attempts=3, timeout=10.0, backoff=2.0),
)

Flat

settings = WorkerPoolSettings(
    max_size           = 4,
    restart_every      = 50,
    retry_max_attempts = 3,
    retry_timeout      = 5.0,
    retry_backoff      = 2.0,
    max_accumulated    = 3,
    rpm                = 120,
    burst              = 5,
    cb_max_attempts    = 3,
    cb_timeout         = 10.0,
    cb_backoff         = 2.0,
)

Reference

Setting Default Description
task_timeout 30.0 Per-task execution timeout in seconds
queue_maxsize 100 Max tasks held in queue; callers block when full
worker_start_delay 2.0 Seconds between starting successive workers
pool.max_size 4 Number of concurrent workers
pool.restart_every None Restart worker after N completed tasks; None disables
retry.max_attempts 3 Max attempts before escalating to error accumulation
retry.timeout 5.0 Base wait in seconds between retry attempts
retry.backoff 2.0 Multiplier applied to timeout on each attempt
error.max_accumulated 3 Retry exhaustions before a circuit breaker trip fires
rate_limit.requests_per_minute 120.0 Sustained RPM per worker; None disables
rate_limit.burst 1 Token bucket capacity; controls burst after idle periods
circuit_breaker.max_attempts 3 Trips allowed before the program exits
circuit_breaker.timeout 10.0 Base pause in seconds when the breaker opens
circuit_breaker.backoff 2.0 Multiplier applied to timeout on each trip

Flow control

Raise WorkerException from inside any task callable to control how the worker handles the outcome.

from taskforeman import WorkerException

# Retry the task (counts against retry budget)
raise WorkerException("busy", retry=True)

# Restart the worker, then retry
raise WorkerException("session expired", restart=True, retry=True)

# Discard the task silently (no error accumulation)
raise WorkerException("not found", skip=True)

# Force one error accumulation, discard task
raise WorkerException("rate limited", error=True)

# Force an immediate circuit breaker trip, discard task
raise WorkerException("proxy dead", circuit_breaker=True)

# Exit the program immediately
raise WorkerException("unrecoverable", quit=True)

Flag priority

circuit_breaker > error > quit > skip > retry

restart is honoured alongside any primary flag.

Subclassing for readability

class SessionExpired(WorkerException):
    def __init__(self, message=None):
        super().__init__(message, restart=True, retry=True)

class RateLimited(WorkerException):
    def __init__(self, message=None):
        super().__init__(message, error=True)

class ProxyDead(WorkerException):
    def __init__(self, message=None):
        super().__init__(message, circuit_breaker=True)

Escalation model

retry → error accumulation → circuit breaker
  1. Retry — the task is retried up to retry.max_attempts times with exponential backoff between attempts.

  2. Error accumulation — once the retry budget is exhausted, the failure is accumulated. When error.max_accumulated failures accumulate, a circuit breaker trip fires and the accumulator resets.

  3. Circuit breaker — all workers pause for circuit_breaker.timeout seconds (with exponential backoff on each trip) and the task is re-enqueued. After circuit_breaker.max_attempts trips, the program exits.


Rate limiting

Each worker maintains its own independent token bucket. Tokens refill at requests_per_minute / 60 per second up to a maximum of burst. The first task always fires immediately. Subsequent tasks are throttled to the configured rate. If a worker sits idle, tokens accumulate up to burst, allowing a short burst of back-to-back tasks when work resumes.

Set rpm=None to disable rate limiting entirely.


Keyboard interrupt

Wrap your entry point to handle Ctrl+C cleanly:

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass

asyncio.run() cancels all tasks and calls __aexit__ on the manager before raising KeyboardInterrupt, closing all workers cleanly.


Project structure

src/
  taskforeman/
    __init__.py
    config/
      settings.py
    core/
      exceptions.py
      manager.py
      worker.py
tests/
  conftest.py
  test_manager.py
  test_settings.py
  test_worker.py
examples/
  basic_usage.py
pyproject.toml
README.md

Running tests

pip install -e ".[dev]"
pytest

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

taskforeman-0.1.1.tar.gz (14.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

taskforeman-0.1.1-py3-none-any.whl (14.8 kB view details)

Uploaded Python 3

File details

Details for the file taskforeman-0.1.1.tar.gz.

File metadata

  • Download URL: taskforeman-0.1.1.tar.gz
  • Upload date:
  • Size: 14.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.11

File hashes

Hashes for taskforeman-0.1.1.tar.gz
Algorithm Hash digest
SHA256 863243d4695b316ee5c82e1f4eb822687f461b861f5eb0df41954e0f6990042f
MD5 12ddde91c58b969764b31866085ee488
BLAKE2b-256 aa865a94240cae69b5a7213b5183630e96ba2ae9e578a5bad704e50d8cb605bd

See more details on using hashes here.

File details

Details for the file taskforeman-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: taskforeman-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 14.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.11

File hashes

Hashes for taskforeman-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 f842d2433cbcdaf4cd54451c7ef67d587b90708124e2077884fefd5dc56e0c84
MD5 bbfad82e5f674165e70158fb9ced35e7
BLAKE2b-256 66ae2871ec0d157f26eef68c731258cdf9962ea6dd16071a86527ef70a3ad628

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page