1414
1515from marimo import _loggers
1616from marimo ._messaging .cell_output import CellChannel
17- from marimo ._messaging .console_output_worker import ConsoleMsg , buffered_writer
17+ from marimo ._messaging .console_output_worker import (
18+ ConsoleMsg ,
19+ FlushMarker ,
20+ buffered_writer ,
21+ )
1822from marimo ._messaging .mimetypes import ConsoleMimeType
1923from marimo ._messaging .types import (
2024 KernelMessage ,
3236LOGGER = _loggers .marimo_logger ()
3337
3438
39+ # Maximum time to block waiting for the buffered console writer to flush.
40+ # The flush hook runs on the hot path between cell execution and idle, so
41+ # we bound the wait even though flushes normally complete in <10ms.
42+ _FLUSH_CONSOLE_TIMEOUT_S = 5.0
43+
44+
3545# Byte limits on outputs exist for two reasons
3646#
3747# 1. We use a multiprocessing.Connection object to send outputs from
@@ -106,7 +116,9 @@ def __init__(
106116 if self .redirect_console :
107117 # Console outputs are buffered
108118 self .console_msg_cv = threading .Condition (threading .Lock ())
109- self .console_msg_queue : deque [ConsoleMsg | None ] = deque ()
119+ self .console_msg_queue : deque [ConsoleMsg | FlushMarker | None ] = (
120+ deque ()
121+ )
110122 self .buffered_console_thread = threading .Thread (
111123 target = buffered_writer ,
112124 args = (self .console_msg_queue , self , self .console_msg_cv ),
@@ -133,14 +145,41 @@ def write(self, data: KernelMessage) -> None:
133145 e ,
134146 )
135147
148+ def flush_console (self ) -> None :
149+ """Force the buffered console writer to flush immediately.
150+
151+ Blocks until all pending console messages have been sent to the
152+ frontend, or until a short timeout elapses if the writer thread
153+ is no longer alive. This ensures that stderr/stdout output
154+ produced during cell execution is delivered before the cell is
155+ marked idle.
156+ """
157+ if not self .redirect_console :
158+ return
159+ # If the buffered writer isn't alive (e.g., shutdown in progress),
160+ # enqueuing a marker would never be drained. Bail out early.
161+ if not self .buffered_console_thread .is_alive ():
162+ return
163+ marker = FlushMarker ()
164+ with self .console_msg_cv :
165+ self .console_msg_queue .append (marker )
166+ self .console_msg_cv .notify ()
167+ # Bounded wait: if the writer dies or stalls, don't block the
168+ # caller indefinitely.
169+ if not marker .done .wait (timeout = _FLUSH_CONSOLE_TIMEOUT_S ):
170+ LOGGER .warning (
171+ "Timed out waiting for console flush after %ss" ,
172+ _FLUSH_CONSOLE_TIMEOUT_S ,
173+ )
174+
136175 def stop (self ) -> None :
137176 """Teardown resources created by the stream."""
138177 # Sending `None` through the queue signals the console thread to exit.
139178 # We don't join the thread in case its processing outputs still; don't
140179 # want to block the entire program.
141180 if self .redirect_console :
142- self .console_msg_queue .append (None )
143181 with self .console_msg_cv :
182+ self .console_msg_queue .append (None )
144183 self .console_msg_cv .notify ()
145184
146185
@@ -263,8 +302,7 @@ def seekable(self) -> bool:
263302 return False
264303
265304 def flush (self ) -> None :
266- # TODO(akshayka): maybe force the buffered writer to write
267- return
305+ self ._stream .flush_console ()
268306
269307 def _write_with_mimetype (
270308 self , data : str , mimetype : ConsoleMimeType
@@ -280,15 +318,15 @@ def _write_with_mimetype(
280318 "Warning: marimo truncated a very large console output.\n "
281319 )
282320 data = data [: int (max_bytes )] + " ... "
283- self ._stream .console_msg_queue .append (
284- ConsoleMsg (
285- stream = CellChannel .STDOUT ,
286- cell_id = self ._stream .cell_id ,
287- data = data ,
288- mimetype = mimetype ,
289- )
290- )
291321 with self ._stream .console_msg_cv :
322+ self ._stream .console_msg_queue .append (
323+ ConsoleMsg (
324+ stream = CellChannel .STDOUT ,
325+ cell_id = self ._stream .cell_id ,
326+ data = data ,
327+ mimetype = mimetype ,
328+ )
329+ )
292330 self ._stream .console_msg_cv .notify ()
293331 return len (data )
294332
@@ -338,8 +376,7 @@ def seekable(self) -> bool:
338376 return False
339377
340378 def flush (self ) -> None :
341- # TODO(akshayka): maybe force the buffered writer to write
342- return
379+ self ._stream .flush_console ()
343380
344381 def _write_with_mimetype (
345382 self , data : str , mimetype : ConsoleMimeType
0 commit comments