[Access] Draft design of new WebSockets
User Story: WebSocket Subscription Management
-
Connection Establishment:
The client establishes a single WebSocket connection with the Access Node (AN), e.g., viaws://localhost:8080/ws. This connection is maintained until closed by either the AN or the client. -
Subscription Mechanism:
The client sends a subscription request through the WebSocket to subscribe to topics. The AN responds with either success or failure. Upon success, a unique subscription ID is generated:ws.send(JSON.stringify({ action: 'subscribe', topic: 'events', arguments: {} }));The client can subscribe to multiple topics through the same connection, managing the messages received accordingly.
If needed, the client can pass initial parameters via the
argumentsfield:ws.send(JSON.stringify({ action: 'subscribe', topic: 'events', arguments: { start_height: '123456789' } }));Updates from the AN are received as follows:
ws.onmessage = (event) => { const message = JSON.parse(event.data); /* Example message structure: { id: 'sub123', topic: 'events', data: [...] } */ switch (message.topic) { case 'events': // Handle events break; default: console.log('Received message for unsupported topic:', message.topic); } }; -
Unsubscription:
To unsubscribe from a topic, the client sends the following message:ws.send(JSON.stringify({ action: 'unsubscribe', topic: 'events', id: 'sub123' })); -
List Active Subscriptions:
The client can request the list of active subscriptions:ws.send(JSON.stringify({ action: 'list_subscriptions' })); -
Closing the Connection:
The client can close the connection manually, or the AN may close it. All subscriptions will be lost when the connection closes.
Access Node Implementation Requirements
WebSocketBroker Requirements
The WebSocketBroker is similar to the existing WebSocketController but includes several key improvements:
-
Connection Management:
The broker should establish the WebSocket connection during construction but avoid subscribing to topics immediately. It will handle ping/pong messages for connection tracking and error management. -
Messages Responce The response messages for the client should have the following format:
{
id: 'sub123',
topic: 'events',
data: [...], // optional, will be present when receiving data from node
action: 'subscribe', // optional, will be present when a message is related to the action status
error: { // optional, will be present when a message related to the action fails
code: 123,
str: 'failed to create subscription'
}
}
The list_subscriptions action`s response will be different and should have the following format:
{
action: 'list_subscriptions',
subscriptions: [
{ topic: 'events', id: 'sub123' },
{ topic: 'blocks', id: 'sub456' }
]
}
-
Message Handling: The broker listens for incoming client messages and processes actions like
subscribe,unsubscribe, andlist_subscriptions. Supported topics include:- events
- account_statuses
- blocks
- block_headers
- block_digests
- transaction_statuses
-
Handler Creation:
For each new subscription, the broker creates aSubscriptionHandlerspecific to the topic. The handler formats and sends the appropriate response to the client, using the correct topic and data. -
Unsubscription:
The broker should allow unsubscribing by subscription ID. -
List Subscriptions:
The broker should return a list of all active subscriptions for the client upon request. -
Limitations The broker should implement limits on the maximum number of subscriptions per connection, the maximum number of responses per second, and the send timeout.
-
Connection Handling:
The broker should manage connectivity by handling ping/pong messages and unsubscriptions. If the client fails to respond to ping/pong messages, the broker should gracefully close the connection and clean up all associated subscriptions.
A visual representation of the new REST subscription process:
New Pub/Sub API Description
1. The router.go
A new AddWsPubSubRoute function will configure the route for the new subscription mechanism, using a distinct address such as v1/ws, separate from the current v1/subscribe_events route. There will be one main route and one handler for the pub/sub mechanism. Different topics (akin to REST routes) will be handled by the WebSocketBroker, which reacts to messages from the client.
2. The WebSocketBroker
The WebSocketBroker manages subscriptions within a single connection between the client and the node.
type WebSocketBroker struct {
conn *websocket.Conn
subs map[string]map[string]SubscriptionHandler // First key is the topic, second key is the subscription ID
broadcast chan []byte
/*
Other fields similar to WebSocketController,
except for `api`, `eventFilterConfig`, and `heartbeatInterval`,
which are specific to event streaming.
*/
}
The conn field represents the WebSocket connection for bidirectional communication with the client. It handles incoming messages from the client and broadcasts messages back to the client based on subscribed topics. Additionally, it manages ping/pong messages, error handling, and connectivity issues.
The methods associated with the conn field include:
-
readMessages:
This method runs while the connection is active. It retrieves, validates, and processes client messages. Actions handled includesubscribe,unsubscribe, andlist_subscriptions. Additional actions can be added as needed. -
writeMessages:
This method runs while the connection is active, listening on thebroadcastchannel. It retrieves responses and sends them to the client. -
broadcastMessage: This method will be called by eachSubscriptionHandler, who will receive formatted subscription messages and write them to thebroadcastchannel. -
pingPongHandler: This method periodically checks connection availability using ping/pong messages and will terminate the connection if the client becomes unresponsive.
The methods associated with the subs field include:
-
subscribe:
Triggered by thereadMessagesmethod when theactionissubscribe. It takes the topic from the message’stopicfield, creates the appropriateSubscriptionHandlerfor the topic using a factory functionCreateSubscription, and adds an instance of the new handler to thesubsmap. The client receives a notification confirming the successful subscription along with the specific ID. -
unsubscribe:
It is triggered by thereadMessagesmethod when theactionisunsubscribe. It removes the relevant handler from thesubsmap by callingSubscriptionHandler::CloseSubscriptionand notifying the client of successful unsubscription. -
listSubscriptions:
It is triggered by thereadMessagesmethod when theactionislist_subscriptions. It gathers all active subscriptions for the current connection, formats the response, and sends it back to the client.
3. The SubscriptionHandler
type SubscriptionHandler interface {
Close() error
}
func CreateSubscription(topic string, arguments map[string]interface{}, broadcastMessage fun([]byte) err) (SubscriptionHandler, error)
The SubscriptionHandler interface abstracts the actual subscriptions used by the WebSocketBroker. Concrete SubscriptionHandler implementations will be created during the WebSocketBroker::subscribe call, depending on the topic provided by the client. For example, the topic events will have an EventsSubscriptionHandler implementation managing event subscriptions.
-
New[Concrete]SubscriptionHandler: Each constructor function takes atopic, theargumentsfrom the client’s message, and thebroadcastchannel. It stores these values and creates the correspondingsubscription.Subscriptionon the backend. Each subscription is unique, identified by an ID fromsubscription.Subscriptionand linked to an individual instance ofSubscriptionHandler. This ensures the client can subscribe to the same topic multiple times with different parameters. -
messagesHandler: Each handler includes a method that processes messages received from the backend and formats them for the client. This formatted message is passed to the Broker for further processing by calling thebroadcastMessagecallback. -
Close: The method gracefully shuts down the subscription when called. -
CreateSubscription: A free factory function, part of theSubscriptionHandlermodule that creates concreteSubscriptionHandlerbased on thetopicfrom theWebSocketBrokerand returns a new instance ofSubscriptionHandler.
WebSocketBroker and SubscriptionHandler Relationship
-
Receiving a Subscription Request:
- The client sends a subscription request over the WebSocket, which includes a
subscribeaction and the topic to subscribe to. - The
WebSocketBrokerprocesses this message inside thereadMessages()method. It parses the message, and extracts thetopicandarguments. Then thesubscribe()method is called. - Inside the
subscribe()method, based on the topic from the subscription request, theWebSocketBrokercallsCreateSubscriptionto instantiate the appropriateSubscriptionHandlerfor the topic. - The
CreateSubscriptionfunction is a factory method responsible for returning the correctSubscriptionHandler(e.g.,EventsSubscriptionHandler,BlocksSubscriptionHandler, etc.). - The created
SubscriptionHandleris stored in thesubsmap of theWebSocketBrokerusing the topic and a generated subscription ID. - The broker then sends a confirmation message back to the client with the subscription ID using
broadcastMessage.
- The client sends a subscription request over the WebSocket, which includes a
-
Handling Subscription Data:
- The
SubscriptionHandlerlistens for updates from the backend relevant to its topic. - When new data is received, the
SubscriptionHandlercalls thebroadcastMessage(data)callback function provided by theWebSocketBroker. - The
broadcastMessagemethod then sends this data to thebroadcastchannel, which the broker monitors.
- The
-
Broadcasting Data to the Client:
- The
WebSocketBrokerlistens on thebroadcastchannel using thewriteMessages()method. - When new data is available,
writeMessages()retrieves it from the channel and sends it to the client over the WebSocket connection.
- The
Looks great @Guitarheroua. A few comments:
- I think the subscribe/unsubscribe needs an ID field to uniquely identify a subscription on a topic that could have multiple instances (e.g. events, account statuses)
- re: “The router should maintain both - the new and old WebSocket (WS) connections for backward compatibility”
- I think we should create the new system hosted on a new endpoint, and leave the old system in place with no interoperability between them.
- this would be the simplest and incentivize everyone to move over.
- We may also need a
typefield on responses to differentiate between different response message types. - We should include special response messages from the subscribe and unsubscribe actions returning details about the subscription (like ID)
- let's include a
list_subscriptionsaction that returns a list of all active subscriptions
@peterargue What is described here is the Web Application Messaging Protocol, or simply WAMP Protocol, which has a few implementations in Go. The most popular one is NEXUS, which implements the WAMP protocol and includes the features we need. It's also actively maintained, with a new version released this year. While it seems like a good fit for our requirements, we should first discuss the pros and cons of using it. My main concern is the subscription model on the client side. To me, it adds an extra layer of complexity, and clients might not be happy with that.
@peterargue What is described here is the Web Application Messaging Protocol, or simply WAMP Protocol, which has a few implementations in Go. The most popular one is NEXUS, which implements the WAMP protocol and includes the features we need. It's also actively maintained, with a new version released this year. While it seems like a good fit for our requirements, we should first discuss the pros and cons of using it. My main concern is the subscription model on the client side. To me, it adds an extra layer of complexity, and clients might not be happy with that.
We agreed that the Nexus library offers many useful features, but it also includes a lot of unnecessary functionality that we won't use. Additionally, the WAMP protocol implemented by this library adds an extra layer of complexity, particularly on the client side, making it more challenging to handle.
Thanks for the updates.
I think we can consolidate the endpoints a bit. With grpc, we get some input validation for free. Since we don't get that with websockets, I think it makes sense to group the endpoints and add explicit argument checks, similar to the rest endpoints. e.g.
events
account_statuses
blocks
block_headers
block_digests
transaction_statuses
each have optional args start_height and start_block_id
For SubscriptionHandler, I think this design works, but I would suggest not including the create method in the interface, since that would require that you already have a handler object instantiated. Instead, have a factory function CreateSubscription(topic string, arguments map[string]interface{}, broadcast chan []byte) (string, error) that returns a new SubscriptionHandler. Also, I'd suggest using Close() error so it implements the more common io.Closer interface.
What does the Endpoint to SubscriptionHandler interface look like? You mentioned using a channel to feed messages back to the broker. How is data passed back from the endpoint to the handler, then to the broker? Do you have any thoughts on what the response messages to the client will look like?
What will the endpoint logic look like? Will it be similar to the existing events endpoint where it simply passes the backend subscription object to the SubscriptionHandler? Are there commonalities between the SubscriptionHandler implementations so they could be reused, or do you think we will need separate implementations for each?
Subscription Tracking: The broker should track active subscriptions by topic and subscription ID, ensuring it does not subscribe to the same topic more than once under the same connection.
I don't think we'll want this on all topics, so I don't think it's worth the complexity of adding it for only a subset. It's not harmful to have duplicate streams, and may limit some usecases. Best to use a combination of max subscriptions per connection, max responses per second, and send timeouts.
Unsubscription: The broker should allow unsubscribing from a topic using both the topic name and subscription ID.
I think a client should only need the subscription ID to unsubscribe
Looks good @Guitarheroua.