Skip to content

Commit 7537687

Browse files
authored
Fix stream simulator (#487)
* Fix stream simulator Signed-off-by: Luke Hinds <lukehinds@gmail.com> * Don't Blocki the event loop in simulator --------- Signed-off-by: Luke Hinds <lukehinds@gmail.com>
1 parent ff7b235 commit 7537687

1 file changed

Lines changed: 25 additions & 7 deletions

File tree

deepfabric/stream_simulator.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
if TYPE_CHECKING:
1717
from .progress import ProgressReporter
1818

19+
# Track current stream task to prevent interleaving (mutable container to avoid global statement)
20+
_stream_state: dict[str, asyncio.Task | None] = {"current_task": None}
21+
1922

2023
class StreamSimulatorConfig(BaseModel):
2124
"""Configuration for buffered stream simulation."""
@@ -37,6 +40,9 @@ def simulate_stream(
3740
Starts simulation in background and returns immediately. This is the
3841
primary interface for stream simulation throughout the codebase.
3942
43+
Cancels any in-flight stream task before starting a new one to prevent
44+
interleaved chunks from multiple generations appearing scrambled in the TUI.
45+
4046
Args:
4147
progress_reporter: ProgressReporter instance or None
4248
content: Text to simulate streaming
@@ -51,6 +57,11 @@ def simulate_stream(
5157
if not progress_reporter or not _config.enabled:
5258
return None
5359

60+
# Cancel any in-flight stream to prevent interleaving
61+
current_task = _stream_state["current_task"]
62+
if current_task is not None and not current_task.done():
63+
current_task.cancel()
64+
5465
async def _simulate_impl() -> None:
5566
"""Internal implementation of chunk emission."""
5667
if not content:
@@ -59,10 +70,17 @@ async def _simulate_impl() -> None:
5970
delay = _config.chunk_delay_ms / 1000.0
6071
chunk_size = _config.chunk_size
6172

62-
for i in range(0, len(content), chunk_size):
63-
chunk = content[i : i + chunk_size]
64-
progress_reporter.emit_chunk(source, chunk, **metadata)
65-
if delay > 0 and i + chunk_size < len(content):
66-
await asyncio.sleep(delay)
67-
68-
return asyncio.create_task(_simulate_impl())
73+
try:
74+
for i in range(0, len(content), chunk_size):
75+
# Await before emitting to ensure cancellation is processed
76+
# and event loop is not blocked when delay is 0
77+
if i > 0:
78+
await asyncio.sleep(delay)
79+
chunk = content[i : i + chunk_size]
80+
progress_reporter.emit_chunk(source, chunk, **metadata)
81+
except asyncio.CancelledError:
82+
# Gracefully handle cancellation
83+
pass
84+
85+
_stream_state["current_task"] = asyncio.create_task(_simulate_impl())
86+
return _stream_state["current_task"]

0 commit comments

Comments
 (0)