The shop of Changing Demand, without the rush, packs orders for an hour: one packer, 1.5 seconds of packing and orders at 0.4 a second. A team downstream wants a file of the finished orders. This page asks: which records reach the team's files, how does the team find every record of one order, and when are the files complete? The decision is what the file sink receives and when the team's job may read the folder. The measures are the records each sink receives, the files the file sink writes and the rows in each. The boundary is the hour. The assumptions are an unpaced run (factor: 0) and the default batch of 500 records.
What You Add
Two sinks: ConsoleEgress, which prints every record to the terminal, and JsonlStorageEgress, which writes JSON Lines files, with a when predicate that passes only the finished orders. Egress, Batching and Delivery describes every sink.
Choose What Each Sink Gets
Every sink receives every event and every telemetry record, unless its when says otherwise. when is a function that takes one record and returns True to send it to that sink. This one passes the events whose value says order_packed:
defpacked_only(record:dict)->bool:"""Keeps the finished orders: events whose value says order_packed."""return(record["stream_type"]=="event"andrecord["value"].get("event_type")=="order_packed")
A blueprint names the function with !python module.function, and the module sits beside the blueprint:
egress:# Every event and every telemetry record-type:Console# The finished orders only, one flat row each-type:Jsonlconfig:{default_path:packed/orders.jsonl}when:!pythonstreaming_out_logic.packed_only
defpacked_only(record:dict)->bool:"""Keeps the finished orders: events whose value says order_packed."""return(record["stream_type"]=="event"andrecord["value"].get("event_type")=="order_packed")
# Every event and every telemetry record.add_egress(ConsoleEgress())# The finished orders only, one flat row each.add_egress(JsonlStorageEgress(default_path=f"{out}/packed/orders.jsonl"),when=packed_only,)
env.setup_egress([# Every event and every telemetry recordConsoleEgress(),# The finished orders only, one flat row eachJsonlStorageEgress(default_path=f"{out}/packed/orders.jsonl"),],predicates=[None,packed_only],)
predicates takes one entry per sink, in the order of the list, and None passes every record. packed_only is the same function as in the Builder tab.
The predicate sees the record as published, so an event's payload is still under value. It checks stream_type first, because a telemetry record's value is a single number. A record that no sink passes is written nowhere.
Find One Order's Records
Every event carries three names. key names the entity, here the order: the arrival calls them orders-1, orders-2, and so on. task names the task, and instance names this run of it. The terminal shows the three events of orders-12, each with the same three names:
A reader that joins the files with another source uses key, and one that follows a task through its states uses task and instance. Records and Telemetry lists every field, and What Each Sink Writes how each sink lays them out.
Read the Files
The file sink writes one file per batch, named after default_path with 8 random hexadecimal digits, such as packed/orders_1a2b3c4d.jsonl. A file, once written, is never changed:
defread_files(folder:Path)->dict:"""The rows of each file the sink wrote to folder, by file name."""return{path.name:[json.loads(line)forlineinpath.read_text().splitlines()]forpathinsorted(folder.glob("*.jsonl"))}
The names are random, so the order of the files is not the order of the records: sort by sim_ts when the order matters. A second run in the same folder adds new files beside the old ones, so the scripts empty packed/ before they run.
Know When the Files Are Complete
The model and the sinks finish at different times. The model stops at until. A sink receives records in batches: at factor: 0 a batch is handed over only when it holds 500 records, and the rest at teardown, after the model has stopped. Teardown hands over what each sink still holds, then waits until every sink has written it:
desflow run tears the run down when it reaches run.until, and returns once the sinks have finished.
app.run(until="1 h") tears the run down when it reaches the end, and returns once the sinks have finished.
try:env.run(until=3600)# the model stops at 1 hourfinally:env.teardown()# the sinks receive the rest and finish writing
env.run runs SimPy's event loop and stops at until; env.teardown() is desflow's step that hands over the rest and waits for the sinks, as SimPy and desflow describes.
So a job that reads the folder while the run is going sees only the full batches. The terminal can show the same thing: on a fast machine the model can be done before the console sink prints its first line, and then every line appears after the log says Tearing down egress connectors. Teardown waits for the sinks to finish writing, and gives up only when they have made no progress for drain_stall_seconds, as Egress describes. Records not written by then are lost, with a warning.
Result
Which records reach the team's files? The terminal receives every record: 4,299 events (1,434 orders queued, 1,433 started and 1,432 packed) and 30 telemetry records, five statistics every 10 minutes. The file sink receives only the 1,432 packed orders, one row each, in 3 files of 500, 500 and 432 rows. The two files of 500 were handed over while the model ran; the file of 432 was handed over at teardown, after the model stopped, so the team's job should start when the run has returned. One order's records share its key: orders-12 was queued at 26.97 seconds, started at 28.202 and packed at 29.702.
A record goes from publish_event to each sink's test, then into that sink's buffer, then to a queue that a separate thread writes from. The model only fills buffers, so it can finish while the threads are still writing, and teardown is the step that waits for them. Writing records on background threads is one of the things desflow adds to SimPy, as What desflow Changes sets out. Egress, Batching and Delivery shows each stage of that path.
# The Streaming Out tutorial: the shop's records, to the terminal and to a file.# Run it with: desflow run streaming_out.yaml# Every record is printed; the finished orders are written to packed/.simulation:sim_id:Shopfactor:0random_seed:42resources:packers:{current_cap:1, max_cap:1}services:packing:{dist:constant, value:1.5 s}arrivals:orders:{dist:exponential, rate:0.4, spawn:pack_order}tasks:pack_order:service:packingresource:packerspayload:{event_type:order_packed}# The packers' and the packing task's readings every 10 minutes, as telemetrystats:{level:minimal, interval:10 min}egress:# Every event and every telemetry record-type:Console# The finished orders only, one flat row each-type:Jsonlconfig:{default_path:packed/orders.jsonl}when:!pythonstreaming_out_logic.packed_onlyrun:until:1 h
"""Python for the Streaming Out blueprint beside it: the file sink's predicate."""defpacked_only(record:dict)->bool:"""Keeps the finished orders: events whose value says order_packed."""return(record["stream_type"]=="event"andrecord["value"].get("event_type")=="order_packed")
"""The Streaming Out tutorial: the shop's records, to the terminal and to a file, builder.One packer and 1.5 seconds of packing; orders arrive at 0.4 a second for an hour.Every record is printed to the terminal, the finished orders are written to packed/,and the script prints the files the sink wrote and one order's row:python streaming_out.py 2>/dev/null shows only those lines."""importjsonimportloggingimportshutilfrompathlibimportPathfromdesflowimportConsoleEgress,JsonlStorageEgress,SimulationContextlogging.basicConfig(level=logging.INFO,format="%(levelname)s [%(asctime)s] %(message)s")defpacked_only(record:dict)->bool:"""Keeps the finished orders: events whose value says order_packed."""return(record["stream_type"]=="event"andrecord["value"].get("event_type")=="order_packed")defshop(out:Path)->SimulationContext:"""The shop of Changing Demand, without the rush, writing under out."""app=(SimulationContext(sim_id="Shop",factor=0,random_seed=42).add_resource("packers",current_cap=1,max_cap=1).add_service("packing",dist="constant",value=1.5).add_arrival("orders",dist="exponential",rate=0.4)# The packers' and the packing task's readings every 10 minutes, as telemetry.with_stats("minimal",interval=600)# Every event and every telemetry record.add_egress(ConsoleEgress())# The finished orders only, one flat row each.add_egress(JsonlStorageEgress(default_path=f"{out}/packed/orders.jsonl"),when=packed_only,))@app.task(service_id="packing",resource_id="packers")defpack_order(task_id:int):return{"event_type":"order_packed"}@app.arrival_loop("orders")deforder_generator(context:SimulationContext):task_id=0whileTrue:order=yieldcontext.wait_for_arrival("orders")context.spawn(pack_order(task_id,entity=order))task_id+=1returnappdefread_files(folder:Path)->dict:"""The rows of each file the sink wrote to folder, by file name."""return{path.name:[json.loads(line)forlineinpath.read_text().splitlines()]forpathinsorted(folder.glob("*.jsonl"))}defoutcomes(out:Path)->dict:"""Runs the hour; returns the rows in each file and the row of orders-12."""shutil.rmtree(out/"packed",ignore_errors=True)# a run adds files; start emptyshop(out).run(until="1 h")files=read_files(out/"packed")rows=[rowforfile_rowsinfiles.values()forrowinfile_rows]return{"file_rows":sorted(len(file_rows)forfile_rowsinfiles.values()),"row":next(rowforrowinrowsifrow["key"]=="orders-12"),}if__name__=="__main__":r=outcomes(Path("streaming_out_output"))print(f"{len(r['file_rows'])} files, with {r['file_rows']} rows")row={k:vfork,vinr["row"].items()ifk!="timestamp"}print(f"orders-12: {row}")
"""The Streaming Out tutorial: the shop's records, to the terminal and to a file, low-level.One packer and 1.5 seconds of packing; orders arrive at 0.4 a second for an hour.Every record is printed to the terminal, the finished orders are written to packed/,and the script prints the files the sink wrote and one order's row:python streaming_out_low_level.py 2>/dev/null shows only those lines."""importitertoolsimportjsonimportloggingimportshutilfrompathlibimportPathfromdesflowimport(CapacityConfig,ConsoleEgress,DistributionConfig,DynamicRealtimeEnvironment,DynamicResource,JsonlStorageEgress,Sampler,SimParameter,)logging.basicConfig(level=logging.INFO,format="%(levelname)s [%(asctime)s] %(message)s")PARAMS=SimParameter(sim_id="Shop",arrival={"orders":DistributionConfig(dist="exponential",rate=0.4)},service={"packing":DistributionConfig(dist="constant",value=1.5)},resources={"packers":CapacityConfig(current_cap=1,max_cap=1)},)defpacked_only(record:dict)->bool:"""Keeps the finished orders: events whose value says order_packed."""return(record["stream_type"]=="event"andrecord["value"].get("event_type")=="order_packed")defpack_order(env,sampler,packers,key,task_id):path_id="Shop.service.packing"defpublish(value):# The key names the order; task and instance name this run of the taskenv.publish_event(key,value,task="pack_order",instance=task_id)publish({"path_id":path_id,"status":"queued"})env.stats.task_queued("pack_order",task_id)withpackers.request()asrequest:yieldrequestpublish({"path_id":path_id,"status":"started"})env.stats.task_started("pack_order",task_id)yieldenv.timeout(sampler.sample(env.registry.get_config(path_id)))publish({"event_type":"order_packed"})env.stats.task_finished("pack_order",task_id)deforder_generator(env,sampler,packers):arrival=env.registry.get_config("Shop.arrival.orders")fortask_idinitertools.count():yieldenv.timeout(sampler.sample(arrival))key=f"orders-{task_id+1}"env.process(pack_order(env,sampler,packers,key,task_id))defrun(out:Path):env=DynamicRealtimeEnvironment(factor=0)env.registry.register_sim_parameter(PARAMS)env.setup_egress([# Every event and every telemetry recordConsoleEgress(),# The finished orders only, one flat row eachJsonlStorageEgress(default_path=f"{out}/packed/orders.jsonl"),],predicates=[None,packed_only],)# The packers' and the packing task's readings every 10 minutes, as telemetryenv.setup_stats("Shop","minimal",interval=600)packers=DynamicResource(env,"Shop","packers")env.process(order_generator(env,Sampler(seed=42),packers))try:env.run(until=3600)# the model stops at 1 hourfinally:env.teardown()# the sinks receive the rest and finish writingreturnenvdefread_files(folder:Path)->dict:"""The rows of each file the sink wrote to folder, by file name."""return{path.name:[json.loads(line)forlineinpath.read_text().splitlines()]forpathinsorted(folder.glob("*.jsonl"))}defoutcomes(out:Path)->dict:"""Runs the hour; returns the rows in each file and the row of orders-12."""shutil.rmtree(out/"packed",ignore_errors=True)# a run adds files; start emptyrun(out)files=read_files(out/"packed")rows=[rowforfile_rowsinfiles.values()forrowinfile_rows]return{"file_rows":sorted(len(file_rows)forfile_rowsinfiles.values()),"row":next(rowforrowinrowsifrow["key"]=="orders-12"),}if__name__=="__main__":r=outcomes(Path("streaming_out_output"))print(f"{len(r['file_rows'])} files, with {r['file_rows']} rows")row={k:vfork,vinr["row"].items()ifk!="timestamp"}print(f"orders-12: {row}")
This is the last page of the tutorial for now. Experiments describes every setting of an experiment, its files and its report, and the rest of the User Guide describes each part of desflow in full. The Kafka Digital Twin example takes live changes from Kafka and sends its records to Kafka.