Skip to content

Commit 62f698b

Browse files
authored
feat(examples): reduce streaming example (#291)
Signed-off-by: Vigith Maurice <vigith@gmail.com>
1 parent 0e2bafe commit 62f698b

5 files changed

Lines changed: 238 additions & 0 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
FROM python:3.11-slim-bullseye AS builder
2+
3+
ENV PYTHONFAULTHANDLER=1 \
4+
PYTHONUNBUFFERED=1 \
5+
PYTHONHASHSEED=random \
6+
PIP_NO_CACHE_DIR=on \
7+
PIP_DISABLE_PIP_VERSION_CHECK=on \
8+
PIP_DEFAULT_TIMEOUT=100 \
9+
POETRY_HOME="/opt/poetry" \
10+
POETRY_VIRTUALENVS_IN_PROJECT=true \
11+
POETRY_NO_INTERACTION=1 \
12+
PYSETUP_PATH="/opt/pysetup"
13+
14+
ENV PATH="$POETRY_HOME/bin:$PATH"
15+
16+
RUN apt-get update \
17+
&& apt-get install --no-install-recommends -y \
18+
curl \
19+
wget \
20+
# deps for building python deps
21+
build-essential \
22+
&& apt-get install -y git \
23+
&& apt-get clean && rm -rf /var/lib/apt/lists/* \
24+
&& curl -sSL https://install.python-poetry.org | python3 -
25+
26+
FROM builder AS udf
27+
28+
WORKDIR $PYSETUP_PATH
29+
COPY ./ ./
30+
31+
# NOTE: place the built wheel in this directory before building the image
32+
RUN pip install $PYSETUP_PATH/pynumaflow_lite-0.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
33+
34+
RUN poetry lock
35+
RUN poetry install --no-cache --no-root && \
36+
rm -rf ~/.cache/pypoetry/
37+
38+
CMD ["python", "reducestream_counter.py"]
39+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
To create the `wheel` file, refer [root](../../README.md)
2+
3+
## HOWTO build Image
4+
5+
```bash
6+
docker build . -t quay.io/numaio/numaflow/pynumaflow-lite-reducestream-counter:v1 --load
7+
```
8+
9+
Load it now to `k3d`
10+
11+
### `k3d`
12+
13+
```bash
14+
k3d image import quay.io/numaio/numaflow/pynumaflow-lite-reducestream-counter:v1
15+
```
16+
17+
### Minikube
18+
19+
```bash
20+
minikube image load quay.io/numaio/numaflow/pynumaflow-lite-reducestream-counter:v1
21+
```
22+
23+
#### Delete image from minikube
24+
25+
`minikube` doesn't like pushing the same image over, delete and load if you are using
26+
the same tag.
27+
28+
```bash
29+
minikube image rm quay.io/numaio/numaflow/pynumaflow-lite-reducestream-counter:v1
30+
```
31+
32+
## Run the pipeline
33+
34+
```bash
35+
kubectl apply -f pipeline.yaml
36+
```
37+
38+
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
apiVersion: numaflow.numaproj.io/v1alpha1
2+
kind: Pipeline
3+
metadata:
4+
name: reducestream-counter
5+
spec:
6+
vertices:
7+
- name: in
8+
source:
9+
# A self data generating source
10+
generator:
11+
rpu: 10
12+
duration: 1s
13+
- name: reducestream
14+
partitions: 1
15+
udf:
16+
container:
17+
image: quay.io/numaio/numaflow/pynumaflow-lite-reducestream-counter:v1
18+
imagePullPolicy: Never
19+
groupBy:
20+
window:
21+
fixed:
22+
length: 10s
23+
streaming: true
24+
keyed: true
25+
storage:
26+
emptyDir: { }
27+
- name: sink
28+
scale:
29+
min: 1
30+
sink:
31+
log: { }
32+
edges:
33+
- from: in
34+
to: reducestream
35+
- from: reducestream
36+
to: sink
37+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[project]
2+
name = "reducestream-counter"
3+
version = "0.1.0"
4+
description = "Reduce streaming counter example using pynumaflow-lite"
5+
authors = [
6+
{ name = "Vigith Maurice", email = "vigith@gmail.com" }
7+
]
8+
readme = "README.md"
9+
requires-python = ">=3.11"
10+
dependencies = [
11+
]
12+
13+
[build-system]
14+
requires = ["poetry-core>=2.0.0,<3.0.0"]
15+
build-backend = "poetry.core.masonry.api"
16+
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""
2+
Reduce Streaming Counter Example
3+
4+
This example demonstrates how to use ReduceStreamer to emit intermediate results
5+
as data arrives, rather than waiting until all data is received.
6+
7+
The counter increments for each datum and emits a message every 10 items,
8+
plus a final message at the end.
9+
"""
10+
import asyncio
11+
import signal
12+
from collections.abc import AsyncIterable, AsyncIterator
13+
14+
from pynumaflow_lite import reducestreamer
15+
16+
17+
class ReduceCounter(reducestreamer.ReduceStreamer):
18+
"""
19+
A reduce streaming counter that emits intermediate results.
20+
21+
This demonstrates the key difference from regular Reducer:
22+
- Regular Reducer: waits for all data, then returns Messages
23+
- ReduceStreamer: yields Message objects incrementally as an async iterator
24+
"""
25+
26+
def __init__(self, initial: int = 0) -> None:
27+
self.counter = initial
28+
29+
async def handler(
30+
self,
31+
keys: list[str],
32+
datums: AsyncIterable[reducestreamer.Datum],
33+
md: reducestreamer.Metadata,
34+
) -> AsyncIterator[reducestreamer.Message]:
35+
"""
36+
Process datums and yield messages incrementally.
37+
38+
Args:
39+
keys: List of keys for this window
40+
datums: Async iterable of incoming data
41+
md: Metadata containing window information
42+
43+
Yields:
44+
Message objects to send to the next vertex
45+
"""
46+
iw = md.interval_window
47+
print(f"Handler started for keys={keys}, window=[{iw.start}, {iw.end}]")
48+
49+
async for _ in datums:
50+
self.counter += 1
51+
52+
# Emit intermediate result every 10 items
53+
if self.counter % 10 == 0:
54+
msg = (
55+
f"counter:{self.counter} "
56+
f"interval_window_start:{iw.start} "
57+
f"interval_window_end:{iw.end}"
58+
).encode()
59+
print(f"Yielding intermediate result: counter={self.counter}")
60+
# Early release of data - this is the key feature of reduce streaming!
61+
yield reducestreamer.Message(msg, keys=keys)
62+
63+
# Emit final result
64+
msg = (
65+
f"counter:{self.counter} (FINAL) "
66+
f"interval_window_start:{iw.start} "
67+
f"interval_window_end:{iw.end}"
68+
).encode()
69+
print(f"Yielding final result: counter={self.counter}")
70+
yield reducestreamer.Message(msg, keys=keys)
71+
72+
73+
# Optional: ensure default signal handlers are in place so asyncio.run can handle them cleanly.
74+
signal.signal(signal.SIGINT, signal.default_int_handler)
75+
try:
76+
signal.signal(signal.SIGTERM, signal.SIG_DFL)
77+
except AttributeError:
78+
pass
79+
80+
81+
async def start(creator: type, init_args: tuple):
82+
"""Start the reduce stream server."""
83+
sock_file = "/var/run/numaflow/reducestream.sock"
84+
server_info_file = "/var/run/numaflow/reducestreamer-server-info"
85+
server = reducestreamer.ReduceStreamAsyncServer(sock_file, server_info_file)
86+
87+
loop = asyncio.get_running_loop()
88+
try:
89+
loop.add_signal_handler(signal.SIGINT, lambda: server.stop())
90+
loop.add_signal_handler(signal.SIGTERM, lambda: server.stop())
91+
except (NotImplementedError, RuntimeError):
92+
pass
93+
94+
try:
95+
print("Starting Reduce Stream Counter Server...")
96+
await server.start(creator, init_args)
97+
print("Shutting down gracefully...")
98+
except asyncio.CancelledError:
99+
try:
100+
server.stop()
101+
except Exception:
102+
pass
103+
return
104+
105+
106+
if __name__ == "__main__":
107+
asyncio.run(start(ReduceCounter, (0,)))
108+

0 commit comments

Comments
 (0)