Skip to content

yggdrasil.concurrent

Bounded job execution primitives for large or unbounded streams of work — Job, AsyncJob, ThreadJob, and JobPoolExecutor with backpressure.

One-liner

from yggdrasil.concurrent import Job, JobPoolExecutor

with JobPoolExecutor(max_workers=4) as pool:
    for result in pool.as_completed(Job.make(fn, arg) for arg in items):
        print(result.value)

Job types

from yggdrasil.concurrent import Job, AsyncJob, ThreadJob

# Synchronous callable (runs in thread pool)
job = Job.make(lambda: 42)

# Async coroutine
import asyncio
async def fetch(url): ...
job = AsyncJob.make(fetch, "https://example.com")

# Explicit thread job
job = ThreadJob.make(heavy_io_fn, arg1, kwarg=val)

JobPoolExecutor

from yggdrasil.concurrent import Job, JobPoolExecutor

jobs = [Job.make(lambda x=x: x * x) for x in range(100)]

# Basic: collect results as they finish
with JobPoolExecutor(max_workers=4) as pool:
    for result in pool.as_completed(jobs):
        print(result.value)

# With backpressure: at most 10 jobs queued at once (protects memory)
with JobPoolExecutor(max_workers=4, max_in_flight=10) as pool:
    for result in pool.as_completed(iter(jobs)):
        print(result.value)

JobResult

from yggdrasil.concurrent import Job, JobPoolExecutor, JobResult

jobs = [Job.make(int, s) for s in ["1", "bad", "3"]]

with JobPoolExecutor(max_workers=2) as pool:
    for result in pool.as_completed(jobs):
        if result.exception is not None:
            print("Error:", result.exception)
        else:
            print("OK:", result.value)

Parallelize with yggdrasil.pyutils

For simpler use cases (list of callables, collect all results) use parallelize:

from yggdrasil.pyutils import parallelize

results = parallelize(
    [(fn, (arg,), {}) for arg in items],
    max_workers=8,
)

Fan-out HTTP requests

from yggdrasil.concurrent import Job, JobPoolExecutor
from yggdrasil.http_ import HTTPSession

http = HTTPSession()

def fetch_page(page: int) -> dict:
    return http.get("https://api.example.com/items", params={"page": page}).json()

jobs = [Job.make(fetch_page, p) for p in range(1, 21)]

pages = []
with JobPoolExecutor(max_workers=5, max_in_flight=10) as pool:
    for result in pool.as_completed(jobs):
        result.raise_for_exception()   # re-raises if the job failed
        pages.extend(result.value["items"])

print(f"Fetched {len(pages)} items total")

Retry within a job

Combine with yggdrasil.pyutils.retry for resilient concurrent work:

from yggdrasil.concurrent import Job, JobPoolExecutor
from yggdrasil.pyutils import retry

@retry(max_attempts=3, backoff=2.0)
def fetch_with_retry(url: str) -> bytes:
    from yggdrasil.http_ import HTTPSession
    return HTTPSession().get(url).content

jobs = [Job.make(fetch_with_retry, url) for url in urls]

with JobPoolExecutor(max_workers=8) as pool:
    for result in pool.as_completed(jobs):
        process(result.value)