1313class YRoomManager ():
1414 """
1515 A singleton that manages all `YRoom` instances in the server extension. This
16- automatically deletes `YRoom` instances if they have had no connected
17- clients or active kernel for >10 seconds.
18-
19- Because rooms may be deleted due to inactivity, consumers should only store
20- a reference to the room ID and call `get_room(room_id)` each time a
21- reference to the room is needed. See `get_room()` for more details.
16+ automatically stops `YRoom` instances if they have had no connected clients
17+ or active kernel for >10 seconds.
2218 """
2319
2420 _rooms_by_id : dict [str , YRoom ]
2521 """
26- Dictionary of active `YRoom` instances, keyed by room ID.
22+ Dictionary of active `YRoom` instances, keyed by room ID. Rooms are never
23+ deleted from this dictionary, even if stopped due to inactivity.
2724
28- It is guaranteed that if a room is present in the dictionary, the room is
29- not currently stopping. This is ensured by `_handle_yroom_stopping()` .
25+ TODO: Delete a room if its file was deleted in/out-of-band or moved
26+ out-of-band. See #116 .
3027 """
3128
3229 _inactive_rooms : set [str ]
3330 """
3431 Set of room IDs that were marked inactive on the last iteration of
3532 `_watch_rooms()`. If a room is inactive and its ID is present in this set,
36- then the room has been inactive for >10 seconds, and the room should be
37- deleted in `_watch_rooms()` .
33+ then the room the room should be stopped as it has been inactive for >10
34+ seconds .
3835 """
3936
4037 _get_fileid_manager : callable [[], BaseFileIdManager ]
@@ -80,23 +77,16 @@ def get_room(self, room_id: str) -> YRoom | None:
8077 """
8178 Returns the `YRoom` instance for a given room ID. If the instance does
8279 not exist, this method will initialize one and return it. Otherwise,
83- this method returns the instance from its cache, ensuring that this
84- method is fast in almost all cases.
85-
86- Consumers should always call this method each time a reference to the
87- `YRoom` is needed, since rooms may be deleted due to inactivity.
88-
89- This method also ensures that the returned room will be alive for >10
90- seconds. This prevents the room from being deleted shortly after the
91- consumer receives it via this method, even if it was inactive.
80+ this method returns the instance from its cache.
9281 """
9382 # First, ensure this room stays open for >10 seconds by removing it from
9483 # the inactive set of rooms if it is present.
9584 self ._inactive_rooms .discard (room_id )
9685
9786 # If room exists, return the room
98- if room_id in self ._rooms_by_id :
99- return self ._rooms_by_id [room_id ]
87+ yroom = self ._rooms_by_id .get (room_id , None )
88+ if yroom :
89+ return yroom
10090
10191 # Otherwise, create a new room
10292 try :
@@ -107,7 +97,6 @@ def get_room(self, room_id: str) -> YRoom | None:
10797 loop = self .loop ,
10898 fileid_manager = self .fileid_manager ,
10999 contents_manager = self .contents_manager ,
110- on_stopping = lambda : self ._handle_yroom_stopping (room_id ),
111100 event_logger = self .event_logger ,
112101 )
113102 self ._rooms_by_id [room_id ] = yroom
@@ -119,31 +108,21 @@ def get_room(self, room_id: str) -> YRoom | None:
119108 )
120109 return None
121110
111+
122112 def has_room (self , room_id : str ) -> bool :
123113 """
124114 Returns whether a `YRoom` instance with a matching `room_id` already
125115 exists.
126116 """
127117 return room_id in self ._rooms_by_id
128118
129- def _handle_yroom_stopping (self , room_id : str ) -> None :
130- """
131- Callback that is run when the YRoom starts stopping. This callback:
132-
133- - Ensures the room is removed from the dictionary, even if the room was
134- stopped directly without `YRoomManager.delete_room()`.
135119
136- - Prevents future connections to the stopping room and allows its memory
137- to be freed once complete.
138- """
139- self ._rooms_by_id .pop (room_id , None )
140-
141-
142120 def delete_room (self , room_id : str ) -> None :
143121 """
144122 Gracefully deletes a YRoom given a room ID. This stops the YRoom,
145- closing all Websockets, applying remaining updates, and saves the final
146- content of the YDoc in a background task.
123+ closing all Websockets with close code 1001 (server shutting down),
124+ applying remaining updates, and saving the final content of the YDoc in
125+ a background task.
147126
148127 Returns `True` if the room was deleted successfully. Returns `False` if
149128 an exception was raised.
@@ -165,65 +144,68 @@ def delete_room(self, room_id: str) -> None:
165144 async def _watch_rooms (self ) -> None :
166145 """
167146 Background task that checks all `YRoom` instances every 10 seconds,
168- deleting any rooms that have been inactive for >10 seconds.
147+ stopping any rooms that have been inactive for >10 seconds.
169148
170- - For rooms providing notebooks: This task deletes the room if it has no
149+ - For rooms providing notebooks: This task stops the room if it has no
171150 connected clients and its kernel execution status is either 'idle' or
172151 'dead'.
173152
174- - For all other rooms: This task deletes the room if it has no connected
153+ - For all other rooms: This task stops the room if it has no connected
175154 clients.
176155 """
177156 while True :
178157 # Check every 10 seconds
179158 await asyncio .sleep (10 )
180159
181- # Get all room IDs from the room dictionary in advance, as the
182- # dictionary will mutate if rooms are deleted.
160+ # Get all room IDs, except for the global awareness room
183161 room_ids = set (self ._rooms_by_id .keys ())
184-
185- # Remove the global awareness room ID from this set, as that room
186- # should not be stopped until the server extension is stopped.
187162 room_ids .discard ("JupyterLab:globalAwareness" )
188163
189- # Iterate through all rooms. If any rooms are empty, stop the rooms .
164+ # Iterate through all rooms. If any rooms are empty, stop the room .
190165 for room_id in room_ids :
191- # Continue if `room_id` is not in the rooms dictionary. This
192- # happens if the room was stopped by something else while this
193- # `for` loop is still running, so we must check.
194- if room_id not in self ._rooms_by_id :
195- self ._inactive_rooms .discard (room_id )
196- continue
197-
198- # Continue if the room has any connected clients.
199- room = self ._rooms_by_id [room_id ]
200- if room .clients .count != 0 :
201- self ._inactive_rooms .discard (room_id )
202- continue
203-
204- # Continue if the room contains a notebook with kernel execution
205- # state neither 'idle' nor 'dead'.
206- # In this case, the notebook kernel may still be running code
207- # cells, so the room should not be closed.
208- awareness = room .get_awareness ().get_local_state () or {}
209- execution_state = awareness .get ("kernel" , {}).get ("execution_state" , None )
210- if execution_state not in { "idle" , "dead" , None }:
211- self ._inactive_rooms .discard (room_id )
212- continue
213-
214- # The room is inactive if this statement is reached
215- # Delete the room if was marked as inactive in the last
216- # iteration, otherwise mark it as inactive.
217- if room_id in self ._inactive_rooms :
218- self .log .info (
219- f"YRoom '{ room_id } ' has been inactive for >10 seconds. "
220- )
221- self .loop .create_task (self .delete_room (room_id ))
222- self ._inactive_rooms .discard (room_id )
223- else :
224- self ._inactive_rooms .add (room_id )
166+ self ._check_room (room_id )
225167
226168
169+ def _check_room (self , room_id : str ) -> None :
170+ """
171+ Checks a room for inactivity.
172+
173+ - If a room is inactive and not in `_inactive_rooms`, this method adds
174+ the room to `_inactive_rooms`.
175+
176+ - If a room is inactive and is listed in `_inactive_rooms`, this method
177+ stops the room, as it has been inactive for 2 consecutive iterations of
178+ `_watch_rooms()`.
179+ """
180+ # Do nothing if the room has any connected clients.
181+ room = self ._rooms_by_id [room_id ]
182+ if room .clients .count != 0 :
183+ self ._inactive_rooms .discard (room_id )
184+ return
185+
186+ # Do nothing if the room contains a notebook with kernel execution state
187+ # neither 'idle' nor 'dead'.
188+ # In this case, the notebook kernel may still be running code cells, so
189+ # the room should not be closed.
190+ awareness = room .get_awareness ().get_local_state () or {}
191+ execution_state = awareness .get ("kernel" , {}).get ("execution_state" , None )
192+ if execution_state not in { "idle" , "dead" , None }:
193+ self ._inactive_rooms .discard (room_id )
194+ return
195+
196+ # The room is inactive if this line is reached.
197+ # Stop the room if was marked as inactive in the last iteration,
198+ # otherwise mark it as inactive.
199+ if room_id in self ._inactive_rooms :
200+ self .log .info (
201+ f"YRoom '{ room_id } ' has been inactive for >10 seconds. "
202+ )
203+ room .stop ()
204+ self ._inactive_rooms .discard (room_id )
205+ else :
206+ self ._inactive_rooms .add (room_id )
207+
208+
227209 def stop (self ) -> None :
228210 """
229211 Gracefully deletes each `YRoom`. See `delete_room()` for more info.
0 commit comments