Skip to content

Conversation

@dlqqq
Copy link
Collaborator

@dlqqq dlqqq commented Jun 14, 2025

Description

This PR is a re-implementation of #114 that restarts empty rooms instead of deleting them. This frees the memory occupied by its YDoc history while preserving the references to YRoom.

UPDATE: the _watch_rooms() background task has been disabled. This PR is now just refactors the YRoom and YRoomFileAPI to allow them to be restarted, while also adding support for multiple notebooks sharing a kernel. We should not enable the _watch_rooms() task until #120 is addressed.

Technical details

YRoomManager

The YRoomManager now has a _watch_rooms() background task that runs every 10 seconds, checking each room. This task has been disabled due to #120.

When the room is restarted depends on whether it provides a notebook:

  • For notebook rooms: the room is restarted when there are no connected clients AND when the kernel is either 'idle' or 'dead' for >10 seconds (2 iterations of inactivity).

  • For all other rooms: the room is restarted when there are no connected clients for >10 seconds.

Rooms are also not restarted if they have no updates in the YDoc history. Therefore, empty rooms will only be restarted once, not every 10 seconds they remain inactive.

YRoom & YRoomFileAPI

The main difference is that these classes is that both provide stop() and restart() methods that are fully synchronous.

  • YRoom.stop() now saves the final content of the YDoc in a background task through the YRoomFileAPI.
  • To allow YRoom to use its methods concurrently, YRoomFileAPI now uses a _contents_lock to prevent overlapping ContentsManager calls to the same file.

Guidance on testing this PR

Non-notebook rooms

Open a text file, make a change, then close the text file. Watch the server logs, and you should see statements similar to the ones below printed after 10-20 seconds:

[I 2025-06-16 08:48:48.147 ServerDocsApp] Room 'text:file:75fb094c-57a9-43c0-893b-eea22396462b' has been inactive for >10 seconds. Restarting the room to free memory occupied by its history.
[I 2025-06-16 08:48:48.147 ServerDocsApp] Stopping YRoom 'text:file:75fb094c-57a9-43c0-893b-eea22396462b'.
[I 2025-06-16 08:48:48.147 ServerDocsApp] Restarted FileAPI for room 'text:file:75fb094c-57a9-43c0-893b-eea22396462b'.
[I 2025-06-16 08:48:48.147 ServerDocsApp] Restarted YRoom 'text:file:75fb094c-57a9-43c0-893b-eea22396462b'.
[I 2025-06-16 08:48:48.149 ServerDocsApp] Loading content for room ID 'text:file:75fb094c-57a9-43c0-893b-eea22396462b', found at path: 'renamed.txt'.
[I 2025-06-16 08:48:48.151 ServerDocsApp] Reseting last_modified to 2025-06-16 15:48:48.150390+00:00
[I 2025-06-16 08:48:48.154 ServerDocsApp] Loaded content for room ID 'text:file:75fb094c-57a9-43c0-893b-eea22396462b'.

This asserts that non-notebook rooms are restarted after they have no connected clients for >10 seconds.

Then, you can try opening a text file, not make changes, then close it. You should see that the room is not restarted. This asserts that the room is restarted only if an update was made to its YDoc history, i.e. only if there is memory to be freed.

Notebook rooms

Create a new notebook with this code cell:

import asyncio
await asyncio.sleep(21) # < can be any value greater than 2x poll interval
print("hello")

Run this cell and close the notebook. Watch the logs and verify that it takes more than 20 seconds before the room is restarted. After the room is restarted (~40 seconds), open the notebook, and you should see that "hello" is correctly printed in the output. This asserts that the room manager does not restart a notebook room while its kernel is still running (i.e. its execution state is not "idle" or "dead").

You can use the kernel again after re-opening the notebook to verify that everything still works as expected after the room is restarted.

Follow-up issues

@dlqqq dlqqq force-pushed the restart-inactive-rooms branch from 9f0b13e to aff427f Compare June 16, 2025 16:13
@dlqqq dlqqq marked this pull request as ready for review June 16, 2025 16:13
@dlqqq dlqqq changed the title Automatically restart empty rooms Automatically restart empty rooms to free memory Jun 16, 2025
@dlqqq dlqqq added the enhancement New feature or request label Jun 16, 2025
@dlqqq
Copy link
Collaborator Author

dlqqq commented Jun 16, 2025

@ellisonbg I've added two new methods to YRoom:

  • observe_jupyter_ydoc()
  • unobserve_jupyter_ydoc()

The observers added by this method are automatically migrated when the YDoc is reset.

The old API for adding an observer to the JupyterYDoc (shown below) should not be used anyways, because the default implementation only allows exactly 1 observer per YDoc.

# old API that should be avoided
jupyter_ydoc = await room.get_jupyter_ydoc()
jupyter_ydoc.observe(...)
# ^ this call actually removes any observers previously added via `jupyter_ydoc.observe()`

The only major change for consumers is that consumers will have to use the top-level observer, and not add an observer to a shared type within the YDoc. This can always be worked around because the top-level observer fires every time a shared type within itself is updated. Here is an example of how a consumer can migrate their observer to use the new API:

def observer(event: pycrdt.ArrayEvent):
   ...

# old API that should be avoided
ychat = await yroom.get_jupyter_ydoc()
ychat.messages.observe(observer)

# new API equivalent
yroom.observe_jupyter_ydoc(
    lambda updated_key, event: observer(event) if updated_key == "messages" else None
)

Since JupyterYDoc is just a custom API wrapper around the YDoc, we don't need a corresponding set of methods for self._ydoc. We only need one for self._awareness. I can open an issue for that.

@ellisonbg
Copy link
Collaborator

We probably need more time to discuss/explore how to handle stale references to YDoc's in the server. I am a bit surprised that Ydocs only allow a single observer. How can that be? I have already seen a demo of multiple AI personas watching the same doc with their own observers.

@ellisonbg
Copy link
Collaborator

I also don't think there is any way we can tell people to not use the existing YDoc APIs (such as observers).

@dlqqq dlqqq changed the title Automatically restart empty rooms to free memory Allow YRoom to be restarted and allow multi-room kernels Jun 18, 2025
@dlqqq
Copy link
Collaborator Author

dlqqq commented Jun 18, 2025

Discussed this with @ellisonbg at standup. We should not auto-restart rooms until #120 is addressed. I've disabled the _watch_rooms() background task in the latest commit.

We agreed that this PR should still be merged since it brings API improvements, fixes #111, and lays out the foundation for fixing #60.

@dlqqq dlqqq requested a review from Zsailer June 18, 2025 17:41
Comment on lines +33 to +40
_room_ids: dict[str, str]
"""
Dictionary of room IDs, keyed by session ID.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._room_ids = {}
Copy link
Collaborator

@Zsailer Zsailer Jun 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: A more traitlets-y way to write this logic 🙂

Suggested change
_room_ids: dict[str, str]
"""
Dictionary of room IDs, keyed by session ID.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._room_ids = {}
# Dictionary of Room IDs, keyed by session ID.
_room_ids = Dict({})

Copy link
Collaborator

@Zsailer Zsailer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, @dlqqq!

Great work here. Left a minor comment.

@dlqqq
Copy link
Collaborator Author

dlqqq commented Jun 19, 2025

@Zsailer Thank you for giving this a look Zach! Brian had already asked that I migrate all of these classes to use traitlets, so I'll include that suggestion in my next PR. 🤗

Merging now so I can start my next PR without merge conflicts. 👍

@dlqqq dlqqq merged commit bb7ba74 into jupyter-ai-contrib:main Jun 19, 2025
3 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cannot attach >1 notebooks to same kernel

4 participants