This repository was archived by the owner on Jul 10, 2026. It is now read-only.
Fix websocket poison connection and leaks on failed requests - #367
Merged
Conversation
thewhaleking
approved these changes
Jun 27, 2026
thewhaleking
left a comment
Contributor
There was a problem hiding this comment.
Probably fine. Cannot look in depth now
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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.
Problem
When many requests share one
AsyncSubstrateInterface(one websocket), sometimes it start to fail withMax retries exceeded.and never recover by itself. There are 3 bugs in the websocket layer:Poison pill (dead handler on an open socket).
The background task
_send_recv_task(the_handler) can finish and returnTimeoutError("Max retries exceeded.")after it used all retries. But the socket stays inOPENstate. The code restart the handler only when the state is notOPENorCONNECTING, so nobody restart it. After that everyretrievejust re-raise the saved error from the dead task. So the connection is stuck forever and every next call fail instantly with the same error._waiting_for_responsecounter leak.This counter is
+1when we start to wait for a response, but-1is done only on success. Ifretrieveraise, we exit theasync withblock and skip the-1. So the counter stays wrong (bigger than 0). This breaks the normal state of the connection, for example the auto shutdown logic that use this counter.max_subscriptionssemaphore permit leak.sendtake one permit. On successretrievegive it back. But if the request fail, the permit is never returned. After enough failed requests there is no free permit and new requests block.Fix
On enter, if
_send_recv_taskis done (dead) but the socket is stillOPEN, we force a clean reconnect under the lock (fresh socket and fresh handler). This recover the poison pill on the next call instead of failing forever.We wrap the request loop in
try/finally, somark_response_receivedis always called (counter stays balanced), and for every request that did not get a response we call the newdiscard_request.discard_requestgive back the semaphore permit, drop the pending future, and keep the id reserved so a late answer from the node can not go into a wrong request.Tests
e2e test: a small async proxy that can go silent (stop forwarding frames but keep the socket open). It reproduce the real poison pill against a local node, and check that after resume the next call recover. Also checked red and green.
How this is connected to the tao.app relay problem
The tao.app relay service use one shared long living
AsyncSubstrateInterface(a singleton, withws_shutdown_timer=None) for all stake operations that go through MEV plus relay. When this shared websocket hit the poison pill, the background handler was dead but the socket stayedOPEN, so the relay never reconnect and every next stake submit failed instantly. The relay return this to the wallet as500 Internal server error: Max retries exceeded.. Because it is one shared connection for everybody, one poison event break staking for all users until the service is restarted.The two leaks are the reason it can not get out of this state by itself. After a failed request
_waiting_for_responsestays bigger than 0, and the auto shutdown logic only close an idle connection when this counter is<= 0. So the connection recycling thatws_shutdown_timerwould normally do is blocked exactly in the poisoned state. The semaphore permit leak is the other side: after enough failed requests there is no free permit and new requests can not be sent at all. For this reason just changingws_shutdown_timer=Noneto some value in the relay would not fix the problem (the leaked counter blocks the auto close), it would only add reconnect and re-init cost. The real fix has to be in the library, and this PR make the shared connection heal itself on the next call and stop both leaks, so the relay stake operations stop failing withMax retries exceeded..