Implement Web Socket Functionality for the Office Hours Queue and Ticket System #634
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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:
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 awhile 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 forqueue
- 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:The manager is relatively simple - there are two dictionaries, one for
active_staff_connections
and one foractive_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 theUser
object (for easy permissions checking) andWebSocket
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:
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:
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
<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.ws://
includinglocalhost:1560
. This is something that we need to look into before deployment.1561
port? Not sure if web sockets need forwarding, am not entirely sure how to do this.