Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

adding awareness event when open and close websockets #246

Merged
merged 1 commit into from
Mar 19, 2024

Conversation

jzhang20133
Copy link
Collaborator

@jzhang20133 jzhang20133 commented Mar 12, 2024

Adding awareness event when open and close websockets

Screenshot 2024-03-11 at 5 22 03 PM

Copy link

welcome bot commented Mar 12, 2024

Thanks for submitting your first pull request! You are awesome! 🤗

If you haven't done so already, check out Jupyter's Code of Conduct. Also, please make sure you followed the pull request template, as this will help us review your contribution more quickly.
welcome
You can meet the other Jovyans by joining our Discourse forum. There is also a intro thread there where you can stop by and say Hi! 👋

Welcome to the Jupyter community! 🎉

Copy link
Contributor

Binder 👈 Launch a Binder on branch jzhang20133/jupyter-collaboration/main-2

@jzhang20133 jzhang20133 added the enhancement New feature or request label Mar 12, 2024
jupyter_collaboration/handlers.py Outdated Show resolved Hide resolved
jupyter_collaboration/events/awareness.yaml Outdated Show resolved Hide resolved
jupyter_collaboration/events/awareness.yaml Outdated Show resolved Hide resolved
jupyter_collaboration/utils.py Outdated Show resolved Hide resolved
jupyter_collaboration/events/awareness.yaml Outdated Show resolved Hide resolved
@@ -284,6 +286,8 @@ def on_close(self) -> None:
# keep the document for a while in case someone reconnects
self.log.info("Cleaning room: %s", self._room_id)
self.room.cleaner = asyncio.create_task(self._clean_room())
if isinstance(self.room, DocumentRoom):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not emitting the event for all room types?

Copy link
Collaborator

Choose a reason for hiding this comment

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

+1

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For YdocWebsocketHandler that handle global awareness websocket, its room type will be TransientRoom. I found its room id is fixed, called "JupyterLab:globalAwareness" and its creating and closing does not indicate any user joining or leaving the collaboration on a document. Hence we only emit when a websocket close where its room type is DocumentRoom.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Global awareness being a TransientRoom doesn't mean that clients leaving a document in a TransientRoom should not emit an event. Maybe the event should not be emitted specifically when the room ID is JupyterLab:globalAwareness? This room ID is a bit unfortunate, as it should probably be a standard in the Jupyter ecosystem and not particularly for JupyterLab, so a better would have been Jupyter:globalAwareness.
@Zsailer do you think this room ID should be specified somewhere in the Jupyter schemas, since it's probably a convention we want to rely on?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We would need to rely on RoomId to identity which document that the host has left from and close all the guest websockets associated as clients in that DocumentRoom. For a TransientRoom it seems that it is not tied to a specific document and mainly used by global awareness. We only want to emit an event when open and on_close has been successful as well.

Copy link
Collaborator

Choose a reason for hiding this comment

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

All I'm saying is that you could do something like that:

Suggested change
if isinstance(self.room, DocumentRoom):
if self.room.room_id != "JupyterLab:globalAwareness":

A TransientRoom is not just for global awareness, it could be for any other collaborative document that doesn't need to be persisted, and you probably want to emit join/leave events for them too.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure.

@@ -297,6 +301,13 @@ def _emit(self, level: LogLevel, action: str | None = None, msg: str | None = No

self.event_logger.emit(schema_id=JUPYTER_COLLABORATION_EVENTS_URI, data=data)

def _emit_awareness_event(self, user: str, action: str, msg: str | None = None) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Rename user to username?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

will address

Copy link
Collaborator

@dlqqq dlqqq left a comment

Choose a reason for hiding this comment

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

@jzhang20133 Thank you opening this PR! I left a comment below, but David Brochart's review covers the areas of feedback blocking this PR from being merged. 😁

All I would like to add is that it's possible to emit events through a decorator interface. Emitting events imperatively (as done in this PR) is less preferable, since it's really easy for a developer to accidentally delete the line emitting the event. Using a decorator also allows for this feature to be added without any changes to the method definition, which I take as a sign of clean code.

To implement a decorator interface, you would want to define an @emit_awareness_event decorator that looks something like:

def emit_awareness_event(action: str, message: Optional[str] = None):
    def decorator(method):
        async def method_stub(self, *args, **kwargs):
            return_value = await method(self, *args, **kwargs)
            data = { "action": action, ... }
            self.event_logger.emit(..., data=data)
            return return_value
        return method_stub
    return decorator

After doing so, the method declarations would look like:

    @emit_awareness_event("join")
    async def open(self, room_id):
        ...

    @emit_awareness_event("leave")
    def on_close(self) -> None:
        ...

Let me know if this all sounds like a good idea for future work. If so, I can track this in a new issue so as to not block this PR. Thanks! 🤗

@@ -297,6 +301,13 @@ def _emit(self, level: LogLevel, action: str | None = None, msg: str | None = No

self.event_logger.emit(schema_id=JUPYTER_COLLABORATION_EVENTS_URI, data=data)

def _emit_awareness_event(self, user: str, action: str, msg: str | None = None) -> None:
Copy link
Collaborator

Choose a reason for hiding this comment

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

(no action needed) I think we should prefer declaring the type of msg as Optional[str] instead of str | None, since the vertical bar syntax for type unions is only available for Python 3.10+. I thought the convention in Jupyter packages was to support every currently maintained version of Python, the lowest being Python 3.8 at present.

However, I also noticed that the rest of the source code uses the str | None. I've opened an issue so we can discuss this as a team. No action needed on this PR, just a comment.

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's fine to use str | None. Jupyter collaboration supports Python 3.8 and above. Union types as X | Y are allowed for python<3.10 with:

from __future__ import annotations

@@ -284,6 +286,8 @@ def on_close(self) -> None:
# keep the document for a while in case someone reconnects
self.log.info("Cleaning room: %s", self._room_id)
self.room.cleaner = asyncio.create_task(self._clean_room())
if isinstance(self.room, DocumentRoom):
Copy link
Collaborator

Choose a reason for hiding this comment

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

+1

@jzhang20133
Copy link
Collaborator Author

def emit_awareness_event(action: str, message: Optional[str] = None):
    def decorator(method):
        async def method_stub(self, *args, **kwargs):
            return_value = await method(self, *args, **kwargs)
            data = { "action": action, ... }
            self.event_logger.emit(..., data=data)
            return return_value
        return method_stub
    return decorator

I really love this idea! I just tried it out. In this use case, we only emit when websocket serving a DocumentRoom is successfully opened and closed and in open(self, room_id) method, when initialize room fails, it handles certain type exception by closing websocket with error message and swallows other type of exceptions. And for that failure cases, we won't emit this event. We would need to change original logic in open method in order to leverage decorator which defeat the purpose.

Copy link
Collaborator

@davidbrochart davidbrochart left a comment

Choose a reason for hiding this comment

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

Thanks @jzhang20133, I think this is good to go, but it would be great if you could add a test.

@jzhang20133
Copy link
Collaborator Author

Thanks @jzhang20133, I think this is good to go, but it would be great if you could add a test.

@davidbrochart Thank you for reviewing the PR. I have added a test to cover emit awareness event.

Copy link
Member

@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.

This looks good, @jzhang20133!

I think we need to set the minimum version of Jupyter Events to the latest release, v0.10.0.

Copy link
Collaborator

@davidbrochart davidbrochart left a comment

Choose a reason for hiding this comment

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

Thanks @jzhang20133.

@jzhang20133 jzhang20133 merged commit 265df16 into jupyterlab:main Mar 19, 2024
18 of 20 checks passed
Copy link

welcome bot commented Mar 19, 2024

Congrats on your first merged pull request in this project! 🎉
congrats
Thank you for contributing, we are very proud of you! ❤️

@davidbrochart
Copy link
Collaborator

@jzhang20133 Test test_room_handler_doc_client_should_emit_awareness_event fails in the CI, would you mind taking a look? Thanks!

@Zsailer
Copy link
Member

Zsailer commented Mar 20, 2024

@davidbrochart fixed here: #258

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.

None yet

4 participants