|
| 1 | +"""Truncating ticker: bounded log + slow vs. fast subscribers. |
| 2 | +
|
| 3 | +The ``TickerWorkflow`` publishes ``count`` events at a fixed interval, |
| 4 | +calling ``self.stream.truncate(...)`` periodically to bound log |
| 5 | +growth. This script subscribes twice — once fast, once slow — and |
| 6 | +prints both side-by-side so the trade is visible: |
| 7 | +
|
| 8 | +* The fast subscriber keeps up and sees every published offset in |
| 9 | + order. |
| 10 | +* The slow subscriber sleeps between iterations. When a truncation |
| 11 | + runs past its position, the iterator silently jumps forward to the |
| 12 | + new base offset — the slow subscriber's offsets jump too, and |
| 13 | + intermediate events are not visible to it. |
| 14 | +
|
| 15 | +This is the bounded-log model: log size is capped, slow consumers may |
| 16 | +miss intermediate events, but they always see the most recent state. |
| 17 | +For long-running workflows pushing high event volumes this is usually |
| 18 | +the right trade — pair with set-semantic events where each event |
| 19 | +carries enough state to make missing the prior ones recoverable. |
| 20 | +
|
| 21 | +Run the worker first (``uv run workflow_stream/run_worker.py``), then:: |
| 22 | +
|
| 23 | + uv run workflow_stream/run_truncating_ticker.py |
| 24 | +""" |
| 25 | + |
| 26 | +from __future__ import annotations |
| 27 | + |
| 28 | +import asyncio |
| 29 | +import uuid |
| 30 | + |
| 31 | +from temporalio.client import Client |
| 32 | +from temporalio.contrib.workflow_stream import WorkflowStreamClient |
| 33 | + |
| 34 | +from workflow_stream.shared import ( |
| 35 | + TASK_QUEUE, |
| 36 | + TOPIC_TICK, |
| 37 | + TickerInput, |
| 38 | + TickEvent, |
| 39 | +) |
| 40 | +from workflow_stream.workflows.ticker_workflow import TickerWorkflow |
| 41 | + |
| 42 | + |
| 43 | +SLOW_SUBSCRIBER_DELAY_S = 1.5 |
| 44 | + |
| 45 | + |
| 46 | +async def main() -> None: |
| 47 | + client = await Client.connect("localhost:7233") |
| 48 | + |
| 49 | + workflow_id = f"workflow-stream-ticker-{uuid.uuid4().hex[:8]}" |
| 50 | + handle = await client.start_workflow( |
| 51 | + TickerWorkflow.run, |
| 52 | + TickerInput( |
| 53 | + count=20, |
| 54 | + keep_last=3, |
| 55 | + truncate_every=5, |
| 56 | + interval_ms=400, |
| 57 | + ), |
| 58 | + id=workflow_id, |
| 59 | + task_queue=TASK_QUEUE, |
| 60 | + ) |
| 61 | + |
| 62 | + stream = WorkflowStreamClient.create(client, workflow_id) |
| 63 | + |
| 64 | + async def fast_subscriber() -> None: |
| 65 | + async for item in stream.subscribe([TOPIC_TICK], result_type=TickEvent): |
| 66 | + print(f"[fast] offset={item.offset:3d} n={item.data.n}") |
| 67 | + |
| 68 | + async def slow_subscriber() -> None: |
| 69 | + async for item in stream.subscribe([TOPIC_TICK], result_type=TickEvent): |
| 70 | + print(f"[SLOW] offset={item.offset:3d} n={item.data.n}") |
| 71 | + await asyncio.sleep(SLOW_SUBSCRIBER_DELAY_S) |
| 72 | + |
| 73 | + # Both iterators exit normally when the workflow completes. No |
| 74 | + # terminal sentinel is needed — see the doc's "When the Workflow |
| 75 | + # run completes" note. |
| 76 | + await asyncio.gather(fast_subscriber(), slow_subscriber()) |
| 77 | + |
| 78 | + result = await handle.result() |
| 79 | + print(f"\nworkflow result: {result}") |
| 80 | + |
| 81 | + |
| 82 | +if __name__ == "__main__": |
| 83 | + asyncio.run(main()) |
0 commit comments