Source code for gestaltdb.sampling.feeder
"""Async prefetch utilities for sampler batches."""
from __future__ import annotations
from queue import Full, Queue
from threading import Event, Thread
from typing import Callable, Generic, TypeVar
T = TypeVar("T")
[docs]
class AsyncBatchFeeder(Generic[T]):
"""Background producer that keeps a bounded queue of sampled batches."""
_STOP = object()
def __init__(self, producer: Callable[[], T], *, max_prefetch: int = 2):
if max_prefetch < 1:
raise ValueError("max_prefetch must be at least 1")
self.producer = producer
self.queue: Queue[object] = Queue(max_prefetch)
self.stop_event = Event()
self.thread: Thread | None = None
[docs]
def start(self) -> "AsyncBatchFeeder[T]":
"""Start the background producer thread."""
if self.thread is not None and self.thread.is_alive():
return self
self.stop_event.clear()
self.thread = Thread(target=self._run, daemon=True)
self.thread.start()
return self
[docs]
def stop(self) -> None:
"""Signal the producer to stop and unblock waiting consumers."""
self.stop_event.set()
try:
self.queue.put_nowait(self._STOP)
except Exception:
pass
if self.thread is not None:
self.thread.join(timeout=5)
[docs]
def get(self, timeout: float | None = None) -> T:
"""Return the next prefetched batch, raising producer errors inline."""
item = self.queue.get(timeout=timeout)
if item is self._STOP:
raise RuntimeError("async batch feeder stopped")
if isinstance(item, BaseException):
raise item
return item
def __enter__(self) -> "AsyncBatchFeeder[T]":
return self.start()
def __exit__(self, exc_type, exc, tb) -> None:
self.stop()
def _run(self) -> None:
while not self.stop_event.is_set():
try:
item = self.producer()
except BaseException as exc: # propagate producer failures to consumer
self._put_until_stopped(exc)
self.stop_event.set()
return
self._put_until_stopped(item)
def _put_until_stopped(self, item: object) -> None:
while not self.stop_event.is_set():
try:
self.queue.put(item, timeout=0.1)
return
except Full:
continue