Skip to content

Commit 816069d

Browse files
committed
test step writes
1 parent efadd45 commit 816069d

3 files changed

Lines changed: 199 additions & 2 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
.last_run.json
1+
.last_run_id
22
*.sqlite3

examples/dbos_durability/.last_run_id

Lines changed: 0 additions & 1 deletion
This file was deleted.

packages/llama-agents-dbos/tests/test_dbos_determinism_subprocess.py

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,3 +732,201 @@ def test_concurrent_workers_stress(test_db_path: Path, iteration: int) -> None:
732732
f"Iteration {iteration} failed.\nstdout: {result.stdout}\nstderr: {result.stderr}"
733733
)
734734
assert_no_determinism_errors(result)
735+
736+
737+
# =============================================================================
738+
# Test 7: Streaming stress test - many write_event_to_stream and send_event calls
739+
# This tests determinism when a step fires many background tasks for streaming
740+
# and internal event sending, then gets interrupted and replayed.
741+
# =============================================================================
742+
743+
STREAMING_STRESS_WORKFLOW_CODE = textwrap.dedent("""
744+
import asyncio
745+
746+
class ProgressEvent(Event):
747+
progress: int = Field(default=0)
748+
749+
class WorkItem(Event):
750+
item_id: int = Field(default=0)
751+
752+
class WorkDone(Event):
753+
item_id: int = Field(default=0)
754+
total_processed: int = Field(default=0)
755+
756+
class FanOutComplete(Event):
757+
pass
758+
759+
class StreamingStressWorkflow(Workflow):
760+
@step
761+
async def fan_out(self, ctx: Context, ev: StartEvent) -> FanOutComplete:
762+
# Fire many stream writes and internal events concurrently
763+
# This creates many background tasks that call DBOS operations
764+
for i in range(15):
765+
ctx.write_event_to_stream(ProgressEvent(progress=i))
766+
ctx.send_event(WorkItem(item_id=i))
767+
print(f"STEP:fan_out:dispatched_15_items", flush=True)
768+
# Write completion signal to stream for interrupt tests
769+
ctx.write_event_to_stream(ProgressEvent(progress=999))
770+
return FanOutComplete()
771+
772+
@step(num_workers=4)
773+
async def process_work(self, ctx: Context, ev: WorkItem) -> WorkDone:
774+
# Each worker also writes to stream, creating more concurrent DBOS ops
775+
await asyncio.sleep(0.01) # Small delay to increase interleaving
776+
ctx.write_event_to_stream(ProgressEvent(progress=100 + ev.item_id))
777+
print(f"STEP:process_work:{ev.item_id}:complete", flush=True)
778+
return WorkDone(item_id=ev.item_id)
779+
780+
@step
781+
async def after_fanout(self, ctx: Context, ev: FanOutComplete) -> None:
782+
# Consume FanOutComplete, don't trigger anything
783+
print("STEP:after_fanout:complete", flush=True)
784+
return None
785+
786+
@step
787+
async def collect(self, ctx: Context, ev: WorkDone) -> StopEvent:
788+
# First WorkDone ends the workflow
789+
print(f"STEP:collect:{ev.item_id}:complete", flush=True)
790+
return StopEvent(result={"first_done": ev.item_id})
791+
""")
792+
793+
794+
def test_streaming_stress_determinism(test_db_path: Path) -> None:
795+
"""Test determinism with many concurrent stream writes and send_event calls."""
796+
run_id = "test-streaming-stress-001"
797+
db_url = f"sqlite+pysqlite:///{test_db_path}?check_same_thread=false"
798+
799+
run_main = textwrap.dedent(f'''
800+
wf = StreamingStressWorkflow(runtime=runtime)
801+
runtime.launch()
802+
try:
803+
ctx = Context(wf)
804+
handler = ctx._workflow_run(wf, StartEvent(), run_id="{run_id}")
805+
result = await handler
806+
print(f"RESULT:{{result}}", flush=True)
807+
print("SUCCESS", flush=True)
808+
except Exception as e:
809+
print(f"ERROR:{{type(e).__name__}}:{{e}}", flush=True)
810+
raise
811+
finally:
812+
runtime.destroy()
813+
''')
814+
815+
print("\n=== Running streaming stress workflow ===")
816+
result = run_workflow_script(
817+
make_script(
818+
STREAMING_STRESS_WORKFLOW_CODE, run_main, db_url, "test-streaming-stress"
819+
)
820+
)
821+
print(f"stdout: {result.stdout}")
822+
print(f"stderr: {result.stderr}")
823+
824+
assert "SUCCESS" in result.stdout, (
825+
f"Should complete successfully.\nstdout: {result.stdout}\nstderr: {result.stderr}"
826+
)
827+
assert_no_determinism_errors(result)
828+
829+
830+
def test_streaming_interrupt_resume(test_db_path: Path) -> None:
831+
"""Test interrupt/resume with many concurrent stream writes in flight."""
832+
run_id = "test-streaming-interrupt-001"
833+
db_url = f"sqlite+pysqlite:///{test_db_path}?check_same_thread=false"
834+
835+
# Start workflow and interrupt after seeing the ProgressEvent with progress=999
836+
# (the one returned by fan_out). This ensures all the send_event and
837+
# write_event_to_stream calls have been made but workers are still running.
838+
start_main = textwrap.dedent(f'''
839+
wf = StreamingStressWorkflow(runtime=runtime)
840+
runtime.launch()
841+
ctx = Context(wf)
842+
handler = ctx._workflow_run(wf, StartEvent(), run_id="{run_id}")
843+
async for event in handler.stream_events():
844+
print(f"EVENT:{{type(event).__name__}}:{{getattr(event, 'progress', getattr(event, 'item_id', ''))}}", flush=True)
845+
# Interrupt after fan_out completes (it emits progress=999)
846+
if isinstance(event, ProgressEvent) and event.progress == 999:
847+
print("INTERRUPTING_AFTER_FANOUT", flush=True)
848+
import os
849+
os._exit(0)
850+
''')
851+
852+
# Resume and complete
853+
resume_main = textwrap.dedent(f'''
854+
wf = StreamingStressWorkflow(runtime=runtime)
855+
runtime.launch()
856+
try:
857+
ctx = Context(wf)
858+
handler = ctx._workflow_run(wf, StartEvent(), run_id="{run_id}")
859+
result = await handler
860+
print(f"RESULT:{{result}}", flush=True)
861+
print("SUCCESS", flush=True)
862+
except Exception as e:
863+
print(f"ERROR:{{type(e).__name__}}:{{e}}", flush=True)
864+
raise
865+
finally:
866+
runtime.destroy()
867+
''')
868+
869+
print("\n=== Starting streaming workflow (will interrupt after fan_out) ===")
870+
result1 = run_workflow_script(
871+
make_script(
872+
STREAMING_STRESS_WORKFLOW_CODE, start_main, db_url, "test-streaming-int"
873+
)
874+
)
875+
print(f"stdout: {result1.stdout}")
876+
print(f"stderr: {result1.stderr}")
877+
878+
assert "STEP:fan_out:dispatched_15_items" in result1.stdout, (
879+
"Fan out should complete"
880+
)
881+
assert "INTERRUPTING_AFTER_FANOUT" in result1.stdout, "Should have interrupted"
882+
883+
print("\n=== Resuming streaming workflow ===")
884+
result2 = run_workflow_script(
885+
make_script(
886+
STREAMING_STRESS_WORKFLOW_CODE, resume_main, db_url, "test-streaming-int"
887+
)
888+
)
889+
print(f"stdout: {result2.stdout}")
890+
print(f"stderr: {result2.stderr}")
891+
892+
assert_no_determinism_errors(result2)
893+
assert "SUCCESS" in result2.stdout or result2.returncode == 0, (
894+
f"Resume should succeed.\nstdout: {result2.stdout}\nstderr: {result2.stderr}"
895+
)
896+
897+
898+
@pytest.mark.parametrize("iteration", range(5))
899+
def test_streaming_stress_repeated(test_db_path: Path, iteration: int) -> None:
900+
"""Stress test streaming - run 5 times to catch timing issues."""
901+
run_id = f"test-streaming-repeated-{iteration}"
902+
db_url = f"sqlite+pysqlite:///{test_db_path}?check_same_thread=false"
903+
904+
run_main = textwrap.dedent(f'''
905+
wf = StreamingStressWorkflow(runtime=runtime)
906+
runtime.launch()
907+
try:
908+
ctx = Context(wf)
909+
handler = ctx._workflow_run(wf, StartEvent(), run_id="{run_id}")
910+
result = await handler
911+
print(f"RESULT:{{result}}", flush=True)
912+
print("SUCCESS", flush=True)
913+
except Exception as e:
914+
print(f"ERROR:{{type(e).__name__}}:{{e}}", flush=True)
915+
raise
916+
finally:
917+
runtime.destroy()
918+
''')
919+
920+
result = run_workflow_script(
921+
make_script(
922+
STREAMING_STRESS_WORKFLOW_CODE,
923+
run_main,
924+
db_url,
925+
f"streaming-stress-{iteration}",
926+
)
927+
)
928+
929+
assert "SUCCESS" in result.stdout, (
930+
f"Iteration {iteration} failed.\nstdout: {result.stdout}\nstderr: {result.stderr}"
931+
)
932+
assert_no_determinism_errors(result)

0 commit comments

Comments
 (0)