2323from app .services .executor_base import (
2424 SESSION_APP_LABEL ,
2525 SESSION_COMPONENT_LABEL ,
26+ SESSION_EXPIRES_AT_KEY ,
2627 SESSION_NAME_PREFIX ,
2728 BaseExecutor ,
2829 EntryKind ,
3839
3940logger = logging .getLogger (__name__ )
4041
41- # Sessions keep their idle container alive for at most this many seconds; a
42- # follow-up PR replaces this with a per-session TTL plus a reaper.
43- SESSION_MAX_LIFETIME_SECONDS = 24 * 60 * 60
44-
4542
4643@dataclass
4744class _ExecContext :
@@ -405,20 +402,23 @@ def _run_in_container(
405402 def create_session (
406403 self ,
407404 * ,
405+ ttl_seconds : int ,
408406 files : Sequence [tuple [str , bytes ]] | None = None ,
409407 cpu_time_limit_sec : int | None = None ,
410408 memory_limit_mb : int | None = None ,
411409 ) -> SessionInfo :
412410 container_name = f"{ SESSION_NAME_PREFIX } { uuid .uuid4 ().hex } "
411+ expires_at = time .time () + ttl_seconds
413412
414413 cmd = self ._build_run_command (
415414 container_name = container_name ,
416415 cpu_time_limit_sec = cpu_time_limit_sec ,
417416 memory_limit_mb = memory_limit_mb ,
418- sleep_seconds = SESSION_MAX_LIFETIME_SECONDS ,
417+ sleep_seconds = ttl_seconds ,
419418 labels = {
420419 "app" : SESSION_APP_LABEL ,
421420 "component" : SESSION_COMPONENT_LABEL ,
421+ SESSION_EXPIRES_AT_KEY : str (expires_at ),
422422 },
423423 )
424424 start_proc = subprocess .run (cmd , capture_output = True , text = True ) # nosec B603
@@ -433,8 +433,8 @@ def create_session(
433433 self ._kill_container (container_name )
434434 raise
435435
436- logger .info ("Created session container %s" , container_name )
437- return SessionInfo (session_id = container_name )
436+ logger .info ("Created session container %s (expires at %s) " , container_name , expires_at )
437+ return SessionInfo (session_id = container_name , expires_at = expires_at )
438438
439439 def delete_session (self , session_id : str ) -> bool :
440440 if not session_id .startswith (SESSION_NAME_PREFIX ):
@@ -444,15 +444,64 @@ def delete_session(self, session_id: str) -> bool:
444444 capture_output = True ,
445445 text = True ,
446446 )
447- # `docker rm -f <missing>` exits 0 on modern Docker, so check stderr
448- # for the "not found" message regardless of exit code.
447+ if result .returncode == 0 :
448+ return True
449+ # docker rm -f exits non-zero only when the container does not exist.
449450 stderr = (result .stderr or "" ).lower ()
450451 if "no such container" in stderr or "not found" in stderr :
451452 return False
452- if result .returncode == 0 :
453- return True
454453 raise RuntimeError (f"Failed to delete session { session_id } : { result .stderr } " )
455454
455+ def reap_expired_sessions (self ) -> int :
456+ list_cmd = [
457+ self .docker_binary ,
458+ "ps" ,
459+ "-a" ,
460+ "--filter" ,
461+ f"label=app={ SESSION_APP_LABEL } " ,
462+ "--filter" ,
463+ f"label=component={ SESSION_COMPONENT_LABEL } " ,
464+ "--format" ,
465+ f'{{{{.Names}}}}\t {{{{.Label "{ SESSION_EXPIRES_AT_KEY } "}}}}' ,
466+ ]
467+ try :
468+ list_result = subprocess .run ( # nosec B603
469+ list_cmd , capture_output = True , text = True , timeout = 10
470+ )
471+ except subprocess .TimeoutExpired :
472+ logger .warning ("Timed out listing session containers for reap" )
473+ return 0
474+
475+ if list_result .returncode != 0 :
476+ logger .warning ("Failed to list session containers: %s" , list_result .stderr )
477+ return 0
478+
479+ now = time .time ()
480+ reaped = 0
481+ for line in list_result .stdout .splitlines ():
482+ name , _ , expires_str = line .partition ("\t " )
483+ name = name .strip ()
484+ expires_str = expires_str .strip ()
485+ if not name or not expires_str :
486+ continue
487+ try :
488+ expires_at = float (expires_str )
489+ except ValueError :
490+ continue
491+ if expires_at >= now :
492+ continue
493+ rm_result = subprocess .run ( # nosec B603
494+ [self .docker_binary , "rm" , "-f" , name ],
495+ capture_output = True ,
496+ text = True ,
497+ )
498+ if rm_result .returncode == 0 :
499+ reaped += 1
500+ logger .info ("Reaped expired session container %s" , name )
501+ else :
502+ logger .warning ("Failed to reap session container %s: %s" , name , rm_result .stderr )
503+ return reaped
504+
456505 def execute_python (
457506 self ,
458507 * ,
0 commit comments