-
Notifications
You must be signed in to change notification settings - Fork 339
websocket: Make unsubscribe helpers accept the request ID returned by subscribe #701
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
Changes from 1 commit
9033bde
b8a3e79
a3dabda
5f8c1fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -89,6 +89,7 @@ def __init__(self, *args, **kwargs): | |
| self.subscriptions: Dict[int, Body] = {} | ||
| self.sent_subscriptions: Dict[int, Body] = {} | ||
| self.failed_subscriptions = {} | ||
| self.request_ids_to_subscriptions: Dict[int, int] = {} | ||
| self.request_counter = itertools.count() | ||
|
|
||
| def increment_counter_and_get_id(self) -> int: | ||
|
|
@@ -154,12 +155,11 @@ async def account_unsubscribe( | |
| """Unsubscribe from account notifications. | ||
|
|
||
| Args: | ||
| subscription: ID of subscription to cancel. | ||
| subscription: The request ID returned by the corresponding ``subscribe`` method. | ||
| """ | ||
| req_id = self.increment_counter_and_get_id() | ||
| req = AccountUnsubscribe(subscription, req_id) | ||
| req = AccountUnsubscribe(self._pop_server_subscription(subscription), req_id) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Local bookkeeping is now torn down before the request is sent.
If Same pattern at lines 192, 238, 294, 325, 345, 365, 385 and 405. |
||
| await self.send_request(req) | ||
| del self.subscriptions[subscription] | ||
|
|
||
| async def logs_subscribe( | ||
| self, | ||
|
|
@@ -186,12 +186,11 @@ async def logs_unsubscribe( | |
| """Unsubscribe from transaction logging. | ||
|
|
||
| Args: | ||
| subscription: ID of subscription to cancel. | ||
| subscription: The request ID returned by the corresponding ``subscribe`` method. | ||
| """ | ||
| req_id = self.increment_counter_and_get_id() | ||
| req = LogsUnsubscribe(subscription, req_id) | ||
| req = LogsUnsubscribe(self._pop_server_subscription(subscription), req_id) | ||
| await self.send_request(req) | ||
| del self.subscriptions[subscription] | ||
|
|
||
| async def block_subscribe( | ||
| self, | ||
|
|
@@ -233,12 +232,11 @@ async def block_unsubscribe( | |
| """Unsubscribe from blocks. | ||
|
|
||
| Args: | ||
| subscription: ID of subscription to cancel. | ||
| subscription: The request ID returned by the corresponding ``subscribe`` method. | ||
| """ | ||
| req_id = self.increment_counter_and_get_id() | ||
| req = BlockUnsubscribe(subscription, req_id) | ||
| req = BlockUnsubscribe(self._pop_server_subscription(subscription), req_id) | ||
| await self.send_request(req) | ||
| del self.subscriptions[subscription] | ||
|
|
||
| async def program_subscribe( # pylint: disable=too-many-arguments | ||
| self, | ||
|
|
@@ -290,12 +288,11 @@ async def program_unsubscribe( | |
| """Unsubscribe from program account notifications. | ||
|
|
||
| Args: | ||
| subscription: ID of subscription to cancel. | ||
| subscription: The request ID returned by the corresponding ``subscribe`` method. | ||
| """ | ||
| req_id = self.increment_counter_and_get_id() | ||
| req = ProgramUnsubscribe(subscription, req_id) | ||
| req = ProgramUnsubscribe(self._pop_server_subscription(subscription), req_id) | ||
| await self.send_request(req) | ||
| del self.subscriptions[subscription] | ||
|
|
||
| async def signature_subscribe( | ||
| self, | ||
|
|
@@ -322,12 +319,11 @@ async def signature_unsubscribe( | |
| """Unsubscribe from signature notifications. | ||
|
|
||
| Args: | ||
| subscription: ID of subscription to cancel. | ||
| subscription: The request ID returned by the corresponding ``subscribe`` method. | ||
| """ | ||
| req_id = self.increment_counter_and_get_id() | ||
| req = SignatureUnsubscribe(subscription, req_id) | ||
| req = SignatureUnsubscribe(self._pop_server_subscription(subscription), req_id) | ||
| await self.send_request(req) | ||
| del self.subscriptions[subscription] | ||
|
|
||
| async def slot_subscribe(self) -> int: | ||
| """Subscribe to receive notification anytime a slot is processed by the validator.""" | ||
|
|
@@ -343,12 +339,11 @@ async def slot_unsubscribe( | |
| """Unsubscribe from slot notifications. | ||
|
|
||
| Args: | ||
| subscription: ID of subscription to cancel. | ||
| subscription: The request ID returned by the corresponding ``subscribe`` method. | ||
| """ | ||
| req_id = self.increment_counter_and_get_id() | ||
| req = SlotUnsubscribe(subscription, req_id) | ||
| req = SlotUnsubscribe(self._pop_server_subscription(subscription), req_id) | ||
| await self.send_request(req) | ||
| del self.subscriptions[subscription] | ||
|
|
||
| async def slots_updates_subscribe(self) -> int: | ||
| """Subscribe to receive a notification from the validator on a variety of updates on every slot.""" | ||
|
|
@@ -364,12 +359,11 @@ async def slots_updates_unsubscribe( | |
| """Unsubscribe from slot update notifications. | ||
|
|
||
| Args: | ||
| subscription: ID of subscription to cancel. | ||
| subscription: The request ID returned by the corresponding ``subscribe`` method. | ||
| """ | ||
| req_id = self.increment_counter_and_get_id() | ||
| req = SlotsUpdatesUnsubscribe(subscription, req_id) | ||
| req = SlotsUpdatesUnsubscribe(self._pop_server_subscription(subscription), req_id) | ||
| await self.send_request(req) | ||
| del self.subscriptions[subscription] | ||
|
|
||
| async def root_subscribe(self) -> int: | ||
| """Subscribe to receive notification anytime a new root is set by the validator.""" | ||
|
|
@@ -385,12 +379,11 @@ async def root_unsubscribe( | |
| """Unsubscribe from root notifications. | ||
|
|
||
| Args: | ||
| subscription: ID of subscription to cancel. | ||
| subscription: The request ID returned by the corresponding ``subscribe`` method. | ||
| """ | ||
| req_id = self.increment_counter_and_get_id() | ||
| req = RootUnsubscribe(subscription, req_id) | ||
| req = RootUnsubscribe(self._pop_server_subscription(subscription), req_id) | ||
| await self.send_request(req) | ||
| del self.subscriptions[subscription] | ||
|
|
||
| async def vote_subscribe(self) -> int: | ||
| """Subscribe to receive notification anytime a new vote is observed in gossip.""" | ||
|
|
@@ -406,12 +399,11 @@ async def vote_unsubscribe( | |
| """Unsubscribe from vote notifications. | ||
|
|
||
| Args: | ||
| subscription: ID of subscription to cancel. | ||
| subscription: The request ID returned by the corresponding ``subscribe`` method. | ||
| """ | ||
| req_id = self.increment_counter_and_get_id() | ||
| req = VoteUnsubscribe(subscription, req_id) | ||
| req = VoteUnsubscribe(self._pop_server_subscription(subscription), req_id) | ||
| await self.send_request(req) | ||
| del self.subscriptions[subscription] | ||
|
|
||
| def _process_rpc_response(self, raw: str) -> List[Union[Notification, SubscriptionResult]]: | ||
| parsed = parse_websocket_message(raw) | ||
|
|
@@ -422,8 +414,25 @@ def _process_rpc_response(self, raw: str) -> List[Union[Notification, Subscripti | |
| raise SubscriptionError(item, subscription) | ||
| if isinstance(item, SubscriptionResult): | ||
| self.subscriptions[item.result] = self.sent_subscriptions[item.id] | ||
| self.request_ids_to_subscriptions[item.id] = item.result | ||
|
michaelhly marked this conversation as resolved.
|
||
| return cast(List[Union[Notification, SubscriptionResult]], parsed) | ||
|
|
||
| def _pop_server_subscription(self, subscription: int) -> int: | ||
| """Translate a subscribe request ID into its server-assigned subscription ID. | ||
|
|
||
| The RPC server assigns subscription IDs independently of the request IDs returned by the | ||
| ``subscribe`` helpers, so an unconfirmed request ID is sent as-is. | ||
|
|
||
| Args: | ||
| subscription: The value returned by a ``subscribe`` helper, or a server-assigned ID. | ||
|
|
||
| Returns: | ||
| The server-assigned subscription ID, if known, else the input unchanged. | ||
| """ | ||
| server_subscription = self.request_ids_to_subscriptions.pop(subscription, subscription) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Request IDs and server subscription IDs collide here, cancelling the wrong subscription. Both spaces are plain The PR description says callers that track server-assigned IDs "behave exactly as before"; that only holds while no live request ID equals the server ID being passed. Suggestion: disambiguate instead of guessing. Checking if subscription in self.subscriptions: # already a server-assigned ID
self.request_ids_to_subscriptions = {k: v for k, v in self.request_ids_to_subscriptions.items() if v != subscription}
self.subscriptions.pop(subscription, None)
return subscriptionOr, better long term, expose the mapping through a distinct return type/API so the two ID spaces are never interchangeable |
||
| self.subscriptions.pop(server_subscription, None) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: swapping |
||
| return server_subscription | ||
|
|
||
|
|
||
| @asynccontextmanager | ||
| async def connect(uri: str = "ws://localhost:8900", **kwargs: Any) -> AsyncIterator[SolanaWsClientProtocol]: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These docstrings (all nine) now document the request-ID form, but
README.md:94and the docs page mirroring it still passfirst_resp[0].result— the server-assigned ID — and so do all the integration fixtures. Since the two forms are silently interchangeable and can collide, the public contract ends up documented one way and exercised another. Worth updating README/docs to whichever form is authoritative.