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

Implement Web Socket Functionality for the Office Hours Queue and Ticket System #634

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

ajaygandecha
Copy link
Member

@ajaygandecha ajaygandecha commented Oct 16, 2024

This major pull request adds web socket functionality to the CSXL site's office hour queue and ticket system. This PR also serves as an exploration, since this marks the first time for us attempting to accompany our API endpoints with web socket endpoints. Since this PR is exploratory, the description will contain notes about the overall architecture, tradeoffs, and areas for open discussion.

This PR is not ready to merge, as extra error handling and considerations need to be added first! We also would like to test this at scale before it launches.

Works like a charm locally with some basic ticket calling and retrieving with side by side browser windows 🥳

Check out /backend/api/office-hours/queue.py to see the code for the web socket endpoints and the manager!

Overview of Web Sockets

Web sockets enables the client side to have a constant connection with the backend, enabling instant data transfer. For the purposes of office hours, this would allow the office hours queue to update instantaneously, at the same time, for all devices in an office hours event / room.

Like with API endpoints, we can define websocket endpoints using FastAPI. These endpoints are defined using the @api.websocket(route) decorator and supply a route. However, to connect to a web socket endpoint, there is a difference in the final route that differentiates it from a traditional HTTP request:

Sample HTTP Endpoint:
https://csxl.unc.edu/api/office-hours/1/queue
Websocket Endpoint:
ws://csxl.unc.edu/ws/office-hours/1/queue

Web sockets do not behave the same way as HTTP. There are no "HTTP request methods" like the traditional GET, POST, etc. We instead have an open connection in which data can be sent from the client to the server, and back from the server to the client. Everything else has to be handled manually.

Another challenge that came up is that the web socket request does not include the same authentication headers that our regular HTTP requests do. So, we cannot access the bearer token directly anymore from the HTTP request with our web sockets. To be able to access the subject (currently registered user), we can pass the user's bearer token as a query parameter to the web socket endpoint directly. This is the current band-aid solution.

Ultimately, in the FastAPI backend, once a web socket is opened, that connection remains open and available until the connection is closed (by the user, server, or an error between).

The Official FastAPI Web Socket Docs shows how to implement web sockets. Here is a brief example of a web socket endpoint from the docs:

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    while True:
        data = await websocket.receive_text()
        await websocket.send_text(f"Message text was: {data}")

The connection kicks off once the async websocket.accept() method call finishes running. Then, since we want to keep the connection open, we put the rest of the body in a while True block. We can wait to receive data from the client, and when we receive it, we can send a textual response back.

Ultimately, this simple example only helps us manage one connection. The official docs recommends that we create a Manager object that helps us to manage many connections.

For the office hours queue feature, we need to manage both the students and staff / TAs across many active office hours events at once. This requires a manager that can store all of these connections.

Designing the Web Socket Architecture for Office Hours

To create the web socket functionality for office hours, it was important to consider that students and staff ultimately request different data - students request their ticket information, staff request the entire queue. The actions that students and staff also send are quite different. So, the solution here creates two web socket endpoints - one for get-help and one for queue - that students and staff can connect to, respectively. These web socket endpoints ultimately accept and broadcast their respective data types.

Since these two separate web socket endpoints will be used for the same purpose, and we want to share data and events amongst both students and staff, I designed the QueueConnectionManager to manage the connections for both endpoints at once:

IMG_05D422E43391-1

The manager is relatively simple - there are two dictionaries, one for active_staff_connections and one for active_student_connections, both of which use office hour event IDs as their keys, and for each item at these indexes ( example: active_staff_connections[oh_event_id]) is a list of all of the active connections. These connections are objects that combine both the User object (for easy permissions checking) and WebSocket connection objects into one.

Every time a new student or staff connects to their respective web socket endpoints, those connections are stored in the manager as shown below:

IMG_27C829F2F108-1

The manager has a broadcast(oh_event_id) method that can send updated data to all students and staff in a specific office hour event. Every time a student or staff performs an action (for example, a ticket being made or a ticket being called), the web socket endpoint can call this broadcast method, ensuring all other users in the same office hour event automatically get refreshed data.

This general process is shown in the image below:

IMG_ADEF00351FDF-1

This enables office hour data to be updated automatically! ✨

We can use relatively simple RxJS to access web sockets on the frontend, doing type conversions between JSON and models where necessary, since web sockets only can send and retrieve raw JSON output.

Some Important Considerations + Discussion Points

  • @KrisJordan, one of the biggest hurdles for implementing web sockets was getting around the custom middleware handling. Since web sockets work a bit differently than HTTP requests, the middleware was erroring out. With some changes, I modified your static middleware class to include some extra handling for when a call is made using web sockets rather than HTTP requests. Things seem to be working right now, but this might be a good thing to look at.
  • The data being passed back and forth between the frontend and backend have to be serialized back and forth between objects and JSON. Also, since the Rx WebSockets implementation does not natively support pushing data and retrieving data of different types, the web socket subjects are set to type<any>, with type conversions taking place. I think that, since this is paired with types signals, it should work fine, but maybe something to look into.
  • Big point: The web sockets factory method for Rx makes you write the entire path, from the ws:// including localhost:1560. This is something that we need to look into before deployment.
  • Also, to the point above, I only got it to work on the 1561 port? Not sure if web sockets need forwarding, am not entirely sure how to do this.

@ajaygandecha ajaygandecha added feature New feature or request exploration Used on branches that may not be new features but explores new implementations. labels Oct 16, 2024
@ajaygandecha ajaygandecha self-assigned this Oct 16, 2024
@ajaygandecha ajaygandecha added the DONT MERGE This label is marked for pull requests that should not be merged yet. label Oct 16, 2024
@ajaygandecha ajaygandecha changed the title [DO NOT MERGE] Implement Web Socket Functionality for the Office Hours Queue and Ticket System Implement Web Socket Functionality for the Office Hours Queue and Ticket System Nov 23, 2024
@ajaygandecha ajaygandecha removed the DONT MERGE This label is marked for pull requests that should not be merged yet. label Nov 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
exploration Used on branches that may not be new features but explores new implementations. feature New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant