Two-stage pipeline
A common shape: one stage extracts something from raw input, the next one engineers features from it. Each stage is a consumer group on its own stream, and @broker.publisher connects them by publishing a handler's return value.
outputtext
documents --[extract]--> features --[engineer]--> (your model, a database, ...)pipeline.pypython
import logging
import time
from rillkit import Broker, Stream
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
broker = Broker("redis://localhost:6379/0")
DOCUMENTS = Stream("documents", group="extractors")
FEATURES = Stream("features", group="modelers", maxlen=10_000)
@broker.subscribe(DOCUMENTS, consumer="extractor-1", block_ms=500)
@broker.publisher(FEATURES)
def extract(doc: dict) -> dict:
"""Stage 1: pull raw text out of a document. The return value is published to `features`."""
words = doc["text"].split()
return {"doc_id": doc["id"], "words": words}
@broker.subscribe(FEATURES, consumer="modeler-1", block_ms=500)
def engineer(features: dict):
"""Stage 2: turn the extracted words into model features."""
words = features["words"]
print(f"doc {features['doc_id']}: n_words={len(words)} avg_len={sum(map(len, words)) / len(words):.2f}")
broker.start()
broker.publish(DOCUMENTS, {"id": "a1", "text": "redis streams are a log"})
broker.publish(DOCUMENTS, {"id": "a2", "text": "consumer groups share the work"})
time.sleep(1.5)
broker.stop()Output:
outputtext
INFO consumer started | stream: documents | group: extractors | consumer: extractor-1 | policy: nack_on_error | max retries: 3
INFO consumer started | stream: features | group: modelers | consumer: modeler-1 | policy: nack_on_error | max retries: 3
doc a1: n_words=5 avg_len=3.80
doc a2: n_words=5 avg_len=5.20
INFO consumer stopped | stream: features | consumer: modeler-1
INFO consumer stopped | stream: documents | consumer: extractor-1How it fits together:
extractis subscribed todocumentsand decorated with@broker.publisher(FEATURES). Whatever it returns goes to thefeaturesstream. ReturnNoneto publish nothing for a document.engineeris an ordinary consumer offeatures. It has no idea where its input came from, only the stream contract.- Each stage retries and dead-letters on its own. If
engineerfails,features-dlqfills up whiledocumentskeeps flowing. maxlen=10_000onFEATUREScaps the intermediate stream so it cannot grow forever.- Both stages can live in one process, as here, or in separate processes. Since the stream is the only link, you can split them without changing any code.
- Stack more publishers to fan out: add
@broker.publisher("audit")below the first and both streams get the result.