@@ -30,6 +30,14 @@ class YRoomManager():
3030 not currently stopping. This is ensured by `_handle_yroom_stopping()`.
3131 """
3232
33+ _inactive_rooms : set [str ]
34+ """
35+ Set of room IDs that were marked inactive on the last iteration of
36+ `_watch_rooms()`. If a room is inactive and its ID is present in this set,
37+ then the room has been inactive for >10 seconds, and the room should be
38+ deleted in `_watch_rooms()`.
39+ """
40+
3341 _get_fileid_manager : callable [[], BaseFileIdManager ]
3442 contents_manager : AsyncContentsManager | ContentsManager
3543 event_logger : EventLogger
@@ -56,6 +64,9 @@ def __init__(
5664 # Initialize dictionary of YRooms, keyed by room ID
5765 self ._rooms_by_id = {}
5866
67+ # Initialize set of inactive rooms tracked by `self._watch_rooms()`
68+ self ._inactive_rooms = set ()
69+
5970 # Start `self._watch_rooms()` background task to automatically stop
6071 # empty rooms
6172 self ._watch_rooms_task = self .loop .create_task (self ._watch_rooms ())
@@ -70,7 +81,14 @@ def get_room(self, room_id: str) -> YRoom | None:
7081 """
7182 Retrieves a YRoom given a room ID. If the YRoom does not exist, this
7283 method will initialize a new YRoom.
84+
85+ This method ensures that the returned room will be alive for >10
86+ seconds. This prevents the room from being deleted shortly after the
87+ consumer receives it via this method, even if it is inactive.
7388 """
89+ # First, ensure this room stays open for >10 seconds by removing it from
90+ # the inactive set of rooms if it is present.
91+ self ._inactive_rooms .discard (room_id )
7492
7593 # If room exists, return the room
7694 if room_id in self ._rooms_by_id :
@@ -142,7 +160,7 @@ async def delete_room(self, room_id: str) -> None:
142160 async def _watch_rooms (self ) -> None :
143161 """
144162 Background task that checks all `YRoom` instances every 10 seconds,
145- deleting any rooms that are totally inactive.
163+ deleting any rooms that have been inactive for >10 seconds .
146164
147165 - For rooms providing notebooks: This task deletes the room if it has no
148166 connected clients and its kernel execution status is either 'idle' or
@@ -169,11 +187,13 @@ async def _watch_rooms(self) -> None:
169187 # happens if the room was stopped by something else while this
170188 # `for` loop is still running, so we must check.
171189 if room_id not in self ._rooms_by_id :
190+ self ._inactive_rooms .discard (room_id )
172191 continue
173192
174193 # Continue if the room has any connected clients.
175194 room = self ._rooms_by_id [room_id ]
176195 if room .clients .count != 0 :
196+ self ._inactive_rooms .discard (room_id )
177197 continue
178198
179199 # Continue if the room contains a notebook with kernel execution
@@ -183,11 +203,20 @@ async def _watch_rooms(self) -> None:
183203 awareness = room .get_awareness ().get_local_state () or {}
184204 execution_state = awareness .get ("kernel" , {}).get ("execution_state" , None )
185205 if execution_state not in { "idle" , "dead" , None }:
206+ self ._inactive_rooms .discard (room_id )
186207 continue
187208
188- # Otherwise, delete the room
189- self .log .info (f"Found empty YRoom '{ room_id } '." )
190- self .loop .create_task (self .delete_room (room_id ))
209+ # The room is inactive if this statement is reached
210+ # Delete the room if was marked as inactive in the last
211+ # iteration, otherwise mark it as inactive.
212+ if room_id in self ._inactive_rooms :
213+ self .log .info (
214+ f"YRoom '{ room_id } ' has been inactive for >10 seconds. "
215+ )
216+ self .loop .create_task (self .delete_room (room_id ))
217+ self ._inactive_rooms .discard (room_id )
218+ else :
219+ self ._inactive_rooms .add (room_id )
191220
192221
193222 async def stop (self ) -> None :
0 commit comments