|
| 1 | +"""External publisher: a non-Activity process pushes events into a workflow. |
| 2 | +
|
| 3 | +The two earlier scenarios publish from inside the workflow itself |
| 4 | +(``OrderWorkflow``, ``PipelineWorkflow``) or from an Activity it runs |
| 5 | +(``charge_card``). This scenario shows the third shape: a backend |
| 6 | +service, scheduled job, or anything else with a Temporal ``Client`` |
| 7 | +publishing into a *running* workflow it didn't start. Same factory as |
| 8 | +the subscribe path — :py:meth:`WorkflowStreamClient.create` — used for |
| 9 | +publishing instead. |
| 10 | +
|
| 11 | +The script starts a ``HubWorkflow`` (which does no work of its own — |
| 12 | +it exists only to host the stream), then runs a publisher and a |
| 13 | +subscriber concurrently. When the publisher is done it signals |
| 14 | +``HubWorkflow.close``, the workflow's run finishes, and the |
| 15 | +subscriber's iterator exits normally. |
| 16 | +
|
| 17 | +Run the worker first (``uv run workflow_stream/run_worker.py``), then:: |
| 18 | +
|
| 19 | + uv run workflow_stream/run_external_publisher.py |
| 20 | +""" |
| 21 | + |
| 22 | +from __future__ import annotations |
| 23 | + |
| 24 | +import asyncio |
| 25 | +import uuid |
| 26 | + |
| 27 | +from temporalio.client import Client |
| 28 | +from temporalio.contrib.workflow_stream import WorkflowStreamClient |
| 29 | + |
| 30 | +from workflow_stream.shared import ( |
| 31 | + TASK_QUEUE, |
| 32 | + TOPIC_NEWS, |
| 33 | + HubInput, |
| 34 | + NewsEvent, |
| 35 | +) |
| 36 | +from workflow_stream.workflows.hub_workflow import HubWorkflow |
| 37 | + |
| 38 | + |
| 39 | +HEADLINES = [ |
| 40 | + "rates held", |
| 41 | + "merger announced", |
| 42 | + "outage resolved", |
| 43 | + "earnings beat", |
| 44 | + "regulator opens probe", |
| 45 | +] |
| 46 | + |
| 47 | + |
| 48 | +async def main() -> None: |
| 49 | + client = await Client.connect("localhost:7233") |
| 50 | + |
| 51 | + workflow_id = f"workflow-stream-hub-{uuid.uuid4().hex[:8]}" |
| 52 | + handle = await client.start_workflow( |
| 53 | + HubWorkflow.run, |
| 54 | + HubInput(hub_id=workflow_id), |
| 55 | + id=workflow_id, |
| 56 | + task_queue=TASK_QUEUE, |
| 57 | + ) |
| 58 | + |
| 59 | + async def publish_news() -> None: |
| 60 | + # WorkflowStreamClient.create takes a Temporal client and a |
| 61 | + # workflow id — the same factory used elsewhere for subscribing. |
| 62 | + # The async context manager batches publishes and flushes on |
| 63 | + # exit; we additionally call flush() before signaling close so |
| 64 | + # we know the events landed before the workflow shuts down. |
| 65 | + producer = WorkflowStreamClient.create(client, workflow_id) |
| 66 | + async with producer: |
| 67 | + for headline in HEADLINES: |
| 68 | + producer.publish(TOPIC_NEWS, NewsEvent(headline=headline)) |
| 69 | + print(f"[publisher] sent: {headline}") |
| 70 | + await asyncio.sleep(0.5) |
| 71 | + await producer.flush() |
| 72 | + # Tell the hub it can stop. The workflow's run() returns, and |
| 73 | + # any in-flight subscribers see their async-for loop exit. |
| 74 | + await handle.signal(HubWorkflow.close) |
| 75 | + print("[publisher] signaled close") |
| 76 | + |
| 77 | + async def consume_news() -> None: |
| 78 | + consumer = WorkflowStreamClient.create(client, workflow_id) |
| 79 | + async for item in consumer.subscribe( |
| 80 | + [TOPIC_NEWS], result_type=NewsEvent |
| 81 | + ): |
| 82 | + print(f"[subscriber] offset={item.offset}: {item.data.headline}") |
| 83 | + |
| 84 | + await asyncio.gather(publish_news(), consume_news()) |
| 85 | + |
| 86 | + result = await handle.result() |
| 87 | + print(f"\nworkflow result: {result}") |
| 88 | + |
| 89 | + |
| 90 | +if __name__ == "__main__": |
| 91 | + asyncio.run(main()) |
0 commit comments