Records often belong in different Kafka topics: task events in one, telemetry in another, and alarms in a topic that someone watches. KafkaEgress takes a topic_router function that picks the topic for each record, instead of one event topic and one telemetry topic.
The scripts publish to Kafka, so start a broker with odctl up kafka-lite. Installation shows the one-time odctl install. Tear it down afterwards with odctl down kafka-lite --volumes.
Steps
Write a Router
A topic router is a Python function that receives one record, as a dictionary with its stream_type, key or path_id, task, instance and value, and returns the topic name as a string. KafkaEgress sends every record it receives, so the router must always return a topic. To drop records, give the provider a when predicate on add_egress, which filters them before they reach the router.
This router sends telemetry, alarms and other events to three topics. Every form uses it unchanged, from complex_routing_logic.py:
defcustom_kafka_router(data:Dict[str,Any])->str:"""Picks a topic for each record by its kind and its value."""ifdata.get("stream_type")=="telemetry":return"shop-telemetry"value=data.get("value")ifisinstance(value,dict)and(value.get("status")=="packing_failed"orvalue.get("level")=="ERROR"):return"system-alarms"# Every other event, including a payload that is not a dict.return"shop-events"
defkafka_egress():"""Sends each record to the topic the router picks."""returnKafkaEgress(bootstrap_servers="127.0.0.1:9092",topic_router=custom_kafka_router,)
run passes it to env.setup_egress, as the full file shows.
Run the Shop
The shop packs an order about every 2 seconds, and every tenth order fails, so all three topics receive records. The minimal statistics give shop-telemetry a reading every 10 seconds. The payload comes from packed in complex_routing_logic.py, which returns {"status": "packing_failed"} for every tenth order and {"status": "packed"} for the others:
resources:packers:{current_cap:1, max_cap:1}services:packing:{dist:constant, value:1.5 s}arrivals:orders:{dist:exponential, rate:0.5, spawn:pack_order}tasks:pack_order:service:packingresource:packers# Called as packed(task_id, context): every tenth order failspayload:!pythoncomplex_routing_logic.packedstats:{level:minimal, interval:10}# readings for shop-telemetry
app.add_arrival("orders",dist="exponential",rate=0.5)app.add_service("packing",dist="constant",value=1.5)app.with_stats("minimal",interval=10)# readings for shop-telemetry@app.task(service_id="packing",resource_id="packers")defpack_order(task_id:int):returnpacked(task_id)# every tenth order fails@app.arrival_loop("orders")deforders(context:SimulationContext):whileTrue:order=yieldcontext.wait_for_arrival("orders")context.spawn(pack_order(context.next_instance("pack_order"),entity=order))
With a router, KafkaEgress does not create topics at start, because only the router knows which topics it uses. This function in complex_routing_logic.py creates them:
defcreate_topics()->None:"""With a router, KafkaEgress creates no topics, so create them first."""admin=KafkaAdminConnector(bootstrap_servers="127.0.0.1:9092")admin.create_topics([{"name":topic}fortopicinTOPICS])
if__name__=="__main__":logging.basicConfig(level=logging.INFO,format="%(levelname)s [%(asctime)s] %(message)s")create_topics()# with a router, KafkaEgress creates no topicsapp.run(until="1 min")
if__name__=="__main__":logging.basicConfig(level=logging.INFO,format="%(levelname)s [%(asctime)s] %(message)s")create_topics()# with a router, KafkaEgress creates no topicsrun()
Check the Result
Run desflow run complex_routing.yaml, python complex_routing.py or python complex_routing_low_level.py while kafka-lite is up, beside complex_routing_logic.py. Each runs for one minute. The Kafka UI of kafka-lite, at http://127.0.0.1:8086, shows the records arriving in shop-events, shop-telemetry and system-alarms. Only the orders whose instance ends in 9 reach system-alarms. The three forms use seed 42, so they send the same events.
# The Route Records to Topics guide: a shop whose records go to three Kafka topics.# Needs a broker on 127.0.0.1:9092 (odctl up kafka-lite). Run it with# desflow run complex_routing.yaml, beside complex_routing_logic.pysimulation:sim_id:Shopfactor:1random_seed:42egress:-type:Kafkaconfig:bootstrap_servers:127.0.0.1:9092topic_router:!pythoncomplex_routing_logic.custom_kafka_routerresources:packers:{current_cap:1, max_cap:1}services:packing:{dist:constant, value:1.5 s}arrivals:orders:{dist:exponential, rate:0.5, spawn:pack_order}tasks:pack_order:service:packingresource:packers# Called as packed(task_id, context): every tenth order failspayload:!pythoncomplex_routing_logic.packedstats:{level:minimal, interval:10}# readings for shop-telemetryrun:until:1 min# With a router, KafkaEgress creates no topics, so create them firstbefore:-!pythoncomplex_routing_logic.create_topics
"""The Route Records to Topics guide: the router, the topics and the payload that theblueprint, the builder and the low-level script share."""fromtypingimportAny,DictfromdesflowimportKafkaAdminConnectorTOPICS=["shop-events","shop-telemetry","system-alarms"]defcustom_kafka_router(data:Dict[str,Any])->str:"""Picks a topic for each record by its kind and its value."""ifdata.get("stream_type")=="telemetry":return"shop-telemetry"value=data.get("value")ifisinstance(value,dict)and(value.get("status")=="packing_failed"orvalue.get("level")=="ERROR"):return"system-alarms"# Every other event, including a payload that is not a dict.return"shop-events"defcreate_topics()->None:"""With a router, KafkaEgress creates no topics, so create them first."""admin=KafkaAdminConnector(bootstrap_servers="127.0.0.1:9092")admin.create_topics([{"name":topic}fortopicinTOPICS])defpacked(task_id:int,context=None)->Dict[str,str]:"""Every tenth order fails, for system-alarms."""iftask_id%10==9:return{"status":"packing_failed"}return{"status":"packed"}
"""The Route Records to Topics guide: a shop whose records go to three Kafka topics.It needs a Kafka broker on 127.0.0.1:9092: start one with `odctl up kafka-lite`. Run itwith `python complex_routing.py`, beside complex_routing_logic.py; it runs for oneminute."""importloggingfromcomplex_routing_logicimportcreate_topics,custom_kafka_router,packedfromdesflowimportKafkaEgress,SimulationContextapp=(SimulationContext(sim_id="Shop",factor=1.0,random_seed=42).add_resource("packers",current_cap=1,max_cap=1).add_egress(KafkaEgress(bootstrap_servers="127.0.0.1:9092",topic_router=custom_kafka_router,)))app.add_arrival("orders",dist="exponential",rate=0.5)app.add_service("packing",dist="constant",value=1.5)app.with_stats("minimal",interval=10)# readings for shop-telemetry@app.task(service_id="packing",resource_id="packers")defpack_order(task_id:int):returnpacked(task_id)# every tenth order fails@app.arrival_loop("orders")deforders(context:SimulationContext):whileTrue:order=yieldcontext.wait_for_arrival("orders")context.spawn(pack_order(context.next_instance("pack_order"),entity=order))if__name__=="__main__":logging.basicConfig(level=logging.INFO,format="%(levelname)s [%(asctime)s] %(message)s")create_topics()# with a router, KafkaEgress creates no topicsapp.run(until="1 min")
"""The Route Records to Topics guide, low-level API: a shop whose records go to threeKafka topics.It needs a Kafka broker on 127.0.0.1:9092: start one with `odctl up kafka-lite`. Run itwith `python complex_routing_low_level.py`, beside complex_routing_logic.py; it runsfor one minute."""importitertoolsimportloggingfromcomplex_routing_logicimportcreate_topics,custom_kafka_router,packedfromdesflowimport(CapacityConfig,DistributionConfig,DynamicRealtimeEnvironment,DynamicResource,KafkaEgress,Sampler,SimParameter,)PARAMS=SimParameter(sim_id="Shop",arrival={"orders":DistributionConfig(dist="exponential",rate=0.5)},service={"packing":DistributionConfig(dist="constant",value=1.5)},resources={"packers":CapacityConfig(current_cap=1,max_cap=1)},)defkafka_egress():"""Sends each record to the topic the router picks."""returnKafkaEgress(bootstrap_servers="127.0.0.1:9092",topic_router=custom_kafka_router,)defpack_order(env,packers,sampler,key,task_id):path_id="Shop.service.packing"stats=env.statsenv.publish_event(key,{"path_id":path_id,"status":"queued"},"pack_order",task_id)stats.task_queued("pack_order",task_id)withpackers.request()asunit:yieldunitenv.publish_event(key,{"path_id":path_id,"status":"started"},"pack_order",task_id)stats.task_started("pack_order",task_id)yieldenv.timeout(sampler.sample(env.registry.get_config(path_id)))env.publish_event(key,packed(task_id),"pack_order",task_id)stats.task_finished("pack_order",task_id)deforders(env,packers,sampler):gap=env.registry.get_config("Shop.arrival.orders")fornumberinitertools.count(1):yieldenv.timeout(sampler.sample(gap))env.process(pack_order(env,packers,sampler,f"orders-{number}",number-1))defrun(until=60.0,factor=1.0,egress=None):env=DynamicRealtimeEnvironment(factor=factor)env.registry.register_sim_parameter(PARAMS)env.setup_egress(egressor[kafka_egress()])env.setup_stats("Shop","minimal",interval=10)# readings for shop-telemetrypackers=DynamicResource(env,"Shop","packers")env.process(orders(env,packers,Sampler(seed=42)))try:env.run(until=until)finally:env.teardown()if__name__=="__main__":logging.basicConfig(level=logging.INFO,format="%(levelname)s [%(asctime)s] %(message)s")create_topics()# with a router, KafkaEgress creates no topicsrun()