Skip to content

Commit 864127c

Browse files
authored
feat: configurable TTL for marimo run sessions (#3344)
1 parent b6834a9 commit 864127c

File tree

9 files changed

+43
-9
lines changed

9 files changed

+43
-9
lines changed

marimo/_cli/cli.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,7 @@ def edit(
375375
base_url=base_url,
376376
allow_origins=allow_origins,
377377
redirect_console_to_browser=True,
378+
ttl_seconds=None,
378379
)
379380

380381

@@ -469,6 +470,7 @@ def new(
469470
auth_token=_resolve_token(token, token_password),
470471
base_url=base_url,
471472
redirect_console_to_browser=True,
473+
ttl_seconds=None,
472474
)
473475

474476

@@ -533,6 +535,15 @@ def new(
533535
type=bool,
534536
help="Include notebook code in the app.",
535537
)
538+
@click.option(
539+
"--session-ttl",
540+
default=120,
541+
show_default=True,
542+
type=int,
543+
help=(
544+
"Seconds to wait before closing a session on " "websocket disconnect."
545+
),
546+
)
536547
@click.option(
537548
"--watch",
538549
is_flag=True,
@@ -585,6 +596,7 @@ def run(
585596
token: bool,
586597
token_password: Optional[str],
587598
include_code: bool,
599+
session_ttl: int,
588600
watch: bool,
589601
base_url: str,
590602
allow_origins: tuple[str, ...],
@@ -633,6 +645,7 @@ def run(
633645
headless=headless,
634646
mode=SessionMode.RUN,
635647
include_code=include_code,
648+
ttl_seconds=session_ttl,
636649
watch=watch,
637650
base_url=base_url,
638651
allow_origins=allow_origins,
@@ -742,6 +755,7 @@ def tutorial(
742755
cli_args={},
743756
auth_token=_resolve_token(token, token_password),
744757
redirect_console_to_browser=False,
758+
ttl_seconds=None,
745759
)
746760

747761

marimo/_server/api/endpoints/ws.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,9 @@ def _on_disconnect(
315315
session.disconnect_consumer(self)
316316

317317
if self.manager.mode == SessionMode.RUN:
318-
# When the websocket is closed, we wait TTL_SECONDS before
319-
# closing the session. This is to prevent the session from
320-
# being closed if the during an intermittent network issue.
318+
# When the websocket is closed, we wait session.ttl_seconds before
319+
# closing the session. This is to prevent the session from being
320+
# closed if the during an intermittent network issue.
321321
def _close() -> None:
322322
if self.status != ConnectionState.OPEN:
323323
LOGGER.debug(
@@ -330,11 +330,13 @@ def _close() -> None:
330330
self.manager.close_session(self.session_id)
331331

332332
session = self.manager.get_session(self.session_id)
333-
cancellation_handle = asyncio.get_event_loop().call_later(
334-
Session.TTL_SECONDS, _close
335-
)
336333
if session is not None:
334+
cancellation_handle = asyncio.get_event_loop().call_later(
335+
session.ttl_seconds, _close
336+
)
337337
self.cancel_close_handle = cancellation_handle
338+
else:
339+
_close()
338340
else:
339341
cleanup_fn()
340342

marimo/_server/asgi.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ def _create_app_for_file(base_url: str, file_path: str) -> ASGIApp:
462462
cli_args={},
463463
auth_token=auth_token,
464464
redirect_console_to_browser=False,
465+
ttl_seconds=None,
465466
)
466467
app = create_starlette_app(
467468
base_url="",

marimo/_server/export/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ def connection_state(self) -> ConnectionState:
278278
user_config_manager=config_manager,
279279
virtual_files_supported=False,
280280
redirect_console_to_browser=False,
281+
ttl_seconds=None,
281282
)
282283

283284
# Run the notebook to completion once

marimo/_server/sessions.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@
7777
from marimo._utils.typed_connection import TypedConnection
7878

7979
LOGGER = _loggers.marimo_logger()
80-
SESSION_MANAGER: Optional["SessionManager"] = None
8180

8281

8382
class QueueManager:
@@ -396,15 +395,16 @@ def close(self) -> None:
396395
self.main_consumer = None
397396

398397

398+
_DEFAULT_TTL_SECONDS = 120
399+
400+
399401
class Session:
400402
"""A client session.
401403
402404
Each session has its own Python kernel, for editing and running the app,
403405
and its own websocket, for sending messages to the client.
404406
"""
405407

406-
TTL_SECONDS = 120
407-
408408
@classmethod
409409
def create(
410410
cls,
@@ -416,6 +416,7 @@ def create(
416416
user_config_manager: MarimoConfigReader,
417417
virtual_files_supported: bool,
418418
redirect_console_to_browser: bool,
419+
ttl_seconds: Optional[int],
419420
) -> Session:
420421
"""
421422
Create a new session.
@@ -438,6 +439,7 @@ def create(
438439
queue_manager,
439440
kernel_manager,
440441
app_file_manager,
442+
ttl_seconds,
441443
)
442444

443445
def __init__(
@@ -447,6 +449,7 @@ def __init__(
447449
queue_manager: QueueManager,
448450
kernel_manager: KernelManager,
449451
app_file_manager: AppFileManager,
452+
ttl_seconds: Optional[int],
450453
) -> None:
451454
"""Initialize kernel and client connection to it."""
452455
# This is some unique ID that we can use to identify the session
@@ -457,6 +460,9 @@ def __init__(
457460
self.room = Room()
458461
self._queue_manager = queue_manager
459462
self.kernel_manager = kernel_manager
463+
self.ttl_seconds = (
464+
ttl_seconds if ttl_seconds is not None else _DEFAULT_TTL_SECONDS
465+
)
460466
self.session_view = SessionView()
461467

462468
self.kernel_manager.start_kernel()
@@ -673,13 +679,15 @@ def __init__(
673679
cli_args: SerializedCLIArgs,
674680
auth_token: Optional[AuthToken],
675681
redirect_console_to_browser: bool,
682+
ttl_seconds: Optional[int],
676683
) -> None:
677684
self.file_router = file_router
678685
self.mode = mode
679686
self.development_mode = development_mode
680687
self.quiet = quiet
681688
self.sessions: dict[SessionId, Session] = {}
682689
self.include_code = include_code
690+
self.ttl_seconds = ttl_seconds
683691
self.lsp_server = lsp_server
684692
self.watcher: Optional[FileWatcher] = None
685693
self.recents = RecentFilesManager()
@@ -753,6 +761,7 @@ def create_session(
753761
user_config_manager=self.user_config_manager,
754762
virtual_files_supported=True,
755763
redirect_console_to_browser=self.redirect_console_to_browser,
764+
ttl_seconds=self.ttl_seconds,
756765
)
757766
return self.sessions[session_id]
758767

marimo/_server/start.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def start(
7171
development_mode: bool,
7272
quiet: bool,
7373
include_code: bool,
74+
ttl_seconds: Optional[int],
7475
headless: bool,
7576
port: Optional[int],
7677
host: str,
@@ -108,6 +109,7 @@ def start(
108109
development_mode=development_mode,
109110
quiet=quiet,
110111
include_code=include_code,
112+
ttl_seconds=ttl_seconds,
111113
lsp_server=LspServer(lsp_port),
112114
user_config_manager=config_reader,
113115
cli_args=cli_args,

tests/_server/mocks.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def __():
5757
cli_args={},
5858
auth_token=AuthToken("fake-token"),
5959
redirect_console_to_browser=False,
60+
ttl_seconds=None,
6061
)
6162
sm.skew_protection_token = SkewProtectionToken("skew-id-1")
6263
return sm

tests/_server/test_session_manager.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def session_manager():
4545
cli_args={},
4646
auth_token=None,
4747
redirect_console_to_browser=False,
48+
ttl_seconds=None,
4849
)
4950

5051

tests/_server/test_sessions.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,7 @@ def test_session() -> None:
237237
queue_manager,
238238
kernel_manager,
239239
AppFileManager.from_app(InternalApp(App())),
240+
ttl_seconds=None,
240241
)
241242

242243
# Assert startup
@@ -282,6 +283,7 @@ def test_session_disconnect_reconnect() -> None:
282283
queue_manager,
283284
kernel_manager,
284285
AppFileManager.from_app(InternalApp(App())),
286+
ttl_seconds=None,
285287
)
286288

287289
# Assert startup
@@ -338,6 +340,7 @@ def test_session_with_kiosk_consumers() -> None:
338340
queue_manager,
339341
kernel_manager,
340342
AppFileManager.from_app(InternalApp(App())),
343+
ttl_seconds=None,
341344
)
342345

343346
# Assert startup

0 commit comments

Comments
 (0)