Skip to content

Adding handshake websocket#114

Closed
marcosfrenkel wants to merge 11 commits into
PublicQuantumNetwork:masterfrom
marcosfrenkel:basic_websocket
Closed

Adding handshake websocket#114
marcosfrenkel wants to merge 11 commits into
PublicQuantumNetwork:masterfrom
marcosfrenkel:basic_websocket

Conversation

@marcosfrenkel

Copy link
Copy Markdown
Collaborator

Adding a new routes file called "coordination" in charge of handling the coordination between nodes. At the moment this is what it does:

  • reset_coordination_state: New call that the frontend should call when loading the main page. Makes sure all the following and leading state are reset and clean for a new interaction (if frontend is in the main page, it is not following or leading anyone).
  • collect_follower: Leader node call to make it look for a follower in a separate node. Not much use for this right now, but that is how it would like. Mostly have been used for development.
  • follow_requested: follower node called by a leader requesting that it follows it. Checks if the frontend is connected and ready to accept a follow request (this can only happen in the main page right now). This is done by a flag (state.client_listening_for_follower_requests) that changes when the websocket gets connected or not. If the client is ready to accept a follow, a second flag (state.following_requested = True) changes and an event gets triggered that makes the websocket send the message to the front end.
  • follow_requested_alert: Websocket that detects when the frontend connects, this changes the flag ``state.client_listening_for_follower_requeststoTrue` to indicate that the client is ready to follow. Sends a message to the client when a follow request comes, receives the response from the client and detects when the websocket gets disconnected and changes the flag back to `False`

@Benjamin-Nussbaum Benjamin-Nussbaum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Change the global state object to be an injected dependency with Depends. Maybe we can think about better endpoint locations/naming/etc, but generally it seems ok.

Comment thread src/pqnstack/app/core/config.py Outdated
Comment on lines +76 to +88
# Leader's state
leading: bool = False
followers_address: str | None = None

# Follower's state
following: bool = False
# Other node requested this node to follow it.
following_requested: bool = False
# User's response to the follow request. None if no response yet, True if accepted, False if rejected.
following_requested_user_response: bool | None = None
# The address of the leader this node is following. None if not following anyone.
leaders_address: str | None = None
leaders_name: str | None = None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Remove "or none" fields. If we want to account content differences in requests/responses, we can add extra models.

Suggested change
# Leader's state
leading: bool = False
followers_address: str | None = None
# Follower's state
following: bool = False
# Other node requested this node to follow it.
following_requested: bool = False
# User's response to the follow request. None if no response yet, True if accepted, False if rejected.
following_requested_user_response: bool | None = None
# The address of the leader this node is following. None if not following anyone.
leaders_address: str | None = None
leaders_name: str | None = None
# Leader's state
leading: bool = False
followers_address: str = ""
# Follower's state
following: bool = False
# Other node requested this node to follow it.
following_requested: bool = False
# User's response to the follow request. None if no response yet, True if accepted, False if rejected.
following_requested_user_response: bool = False
# The address of the leader this node is following. None if not following anyone.
leaders_address: str = ""
leaders_name: str = ""

@marcosfrenkel

Copy link
Copy Markdown
Collaborator Author

I moved all the coordination flags into its own model, and made it a dep. I only left None for following_requested_user_response Since None indicate no reply from the user, True indicates accept and False means reject the request. Could you give it another look @Benjamin-Nussbaum

@Benjamin-Nussbaum Benjamin-Nussbaum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

A few comments, otherwise looks fine modulo general cleanup.

Comment on lines +80 to +113
# Check if the client is ready to accept a follower request and that node is not already following someone.
if coord.client_listening_for_follower_requests and not coord.following:
coord.following_requested = True
coord.leaders_name = leaders_name
coord.leaders_address = leaders_address
# Trigger the state change to get the websocket to send question to user
ask_user_for_follow_event.set()

logger.debug("Asking user to accept follow request from %s (%s)", leaders_name, leaders_address)
await user_replied_event.wait() # Wait for a state change event to see if user accepted
user_replied_event.clear() # Reset the event for the next change
if coord.following_requested_user_response:
logger.debug("Follow request from %s accepted.", leaders_address)
coord.following = True
coord.leaders_name = leaders_name
coord.leaders_address = leaders_address
ask_user_for_follow_event.set()
return True

logger.debug("Follow request from %s rejected.", leaders_address)
# Clean up the state if user rejected
coord.leaders_address = ""
coord.leaders_name = ""
coord.following_requested = False

else:
logger.info(
"Request rejected because %s",
(
"client is not listening for requests"
if coord.client_listening_for_follower_requests
else "this node is already following someone"
),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I suggest inverting the logic here to reduce the indent level and keep the error handling close to the condition.

Comment thread src/pqnstack/app/api/deps.py Outdated
Comment on lines +19 to +20
async def get_coordination_state() -> AsyncGenerator[CoordinationState, None]:
yield state.coordination_state

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some suggestions:

  • Put this function in the same module as the class definition
  • doesn't need to be a generator
  • probably doesn't need to be async
  • rather than a whole separate dependency for a sub-component of the overall state (potentially opening the door for additional complexity via more separate things for other sub-components), just depend on the overall state object, and use the component - getting the whole thing isn't that much more overhead, and it keeps the style consistent across endpoints.

qkd_request_bit_list: list[int] = []


state = NodeState()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This state object should be returned in a get_state() function in this file as mentioned in my other comment.

  • check it isn't imported directly for use in any endpoints

Comment on lines +47 to +58
ret = await http_client.post(f"http://{address}/coordination/follow_requested?leaders_name={settings.node_name}")
if ret.status_code != status.HTTP_200_OK:
raise HTTPException(status_code=ret.status_code, detail=ret.text)

if ret.json() is True:
coord.leading = True
coord.followers_address = address
logger.info("Successfully collected follower")
return True
if ret.json() is False:
logger.info("Follower rejected follow request")
return False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Also, this section could be clearer if follow_requested returned a structured response rather than a raw bool. (either a small dict[str,bool] with a meaningful key, or a more robust ResponseModel

@marcosfrenkel

Copy link
Copy Markdown
Collaborator Author

@Benjamin-Nussbaum, I implemented all the changes let me know if anything else needs changing. I would hold on the merge though since. I wasn't able to test the code properly

@Benjamin-Nussbaum Benjamin-Nussbaum left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks fine - we can discuss later about other possible changes

Comment on lines +145 to +153
try:
await websocket.send_text(
f"Do you want to accept a connection from {state.leaders_name} ({state.leaders_address})?"
)
except RuntimeError as e:
if "websocket.close" in str(e) or "response already completed" in str(e):
logger.debug("WebSocket already closed, cannot send message")
break
raise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Multiple nested try blocks adds confusion. Rather than just trying to send text and handling an exception if the socket is closed, I suggest checking if the socket is closed before attempting to send.

@marcosfrenkel

Copy link
Copy Markdown
Collaborator Author

New changes incoming in different PR

@marcosfrenkel marcosfrenkel deleted the basic_websocket branch December 15, 2025 19:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants