Fix: teleop freeze from portal signal-pipe leak in minimum_gello - #79
Open
ronaldnetawat wants to merge 1 commit into
Open
Fix: teleop freeze from portal signal-pipe leak in minimum_gello#79ronaldnetawat wants to merge 1 commit into
ronaldnetawat wants to merge 1 commit into
Conversation
The leader's foreground io loop drove portal RPC unpaced (~7 kHz) and fired command_joint_pos fire-and-forget immediately followed by get_joint_pos. portal's client wakes its socket thread by writing one byte to a signal pipe per request but drains it at most once per send-queue-emptying, so two requests merged into one send burst leak one pipe byte per cycle. After ~2-3 minutes of synced teleop the 64 KiB pipe fills and send() blocks forever in os.write: the leader silently stops forwarding commands while its control worker keeps running, so the follower freezes rigid at its last commanded pose and the desync button appears dead. Confirmed twice with py-spy (MainThread stuck at portal client_socket.py send/os.write), reproducibly minutes after enabling sync and never while idle (one request per cycle happens to be drain-balanced). Fixes: - Pace _run_leader_io_loop and _rpc_polling_worker with _WORKER_LOOP_PERIOD_S like every other loop in this file (these two were missed; 500 Hz is well above the ~270 Hz hardware rate). - Call .result() on ClientRobot.command_joint_pos/command_joint_state. The wait is load-bearing: completing each request before issuing the next keeps every request in its own send burst, so pipe writes and drains stay exactly 1:1 balanced indefinitely. Verified on real hardware (YAM leader + YAM/linear_4310 follower over CAN): 10+ minutes of continuous synced teleop with zero io stalls, versus a reliable freeze within ~3 minutes before the fix.
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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
Leader-follower teleop with
examples/minimum_gello/minimum_gello.pyfreezes reliably after ~2–3 minutes of synced driving: the follower stops tracking and holds its last commanded pose rigidly, the teaching-handle sync button appears dead, and neither process logs any error — both keep printing healthy rate reports. Idle (unsynced) sessions never freeze, even overnight.Root cause
portal's client (3.7.3) wakes its socket thread by writing one byte to an internal signal pipe per request (
client_socket.pysend()), but the socket loop drains that pipe at most one byte per send-queue-emptying — theif not writing:guard skips the drain while messages are queued, and the post-drainwriting = Truepersists until the next queue-emptying._run_leader_io_loopissuescommand_joint_posfire-and-forget and immediately follows it withget_joint_pos. Both requests land in one send burst: two signal bytes written, one drained — a leak of one pipe byte per loop cycle while synced. At a few hundred cycles/sec the 64 KiB pipe fills in ~2–3 minutes, after whichos.writeon the full blocking pipe blocks forever insidesend(). The io loop dies silently; the leader's control worker keeps running at full rate, so no commands reach the follower (rigid at last pose) and desync toggles never get forwarded (button "dead"). Unsynced sessions issue one request per cycle, which happens to be exactly drain-balanced — which is why the freeze only appears after enabling sync.py-spy dumps of two independent freezes show the main thread stuck at the same line (once via
get_joint_pos, once viacommand_joint_pos):Fix
_run_leader_io_loopand_rpc_polling_workerwith_WORKER_LOOP_PERIOD_S, matching every other loop in the file (unpaced they run ~7 kHz of RPC against a ~270 Hz robot)..result()onClientRobot.command_joint_pos/command_joint_state. The wait is load-bearing, not just error propagation: completing each request before issuing the next keeps every request in its own send burst, so signal-pipe writes and drains stay exactly 1:1 balanced indefinitely. Comments in the code document the mechanism.A deeper fix (draining all pending signal bytes) belongs upstream in portal; this change makes the example robust with portal as-is.
Verification
Real hardware — YAM leader (
yam_teaching_handle) + YAM follower (linear_4310), separate CANable buses,--bilateral-kp 0.2: 10+ minutes of continuous synced teleop with zero io stalls (857 consecutive healthy 1 s rate reports), versus a reliable silent freeze within ~3 minutes before the change. The io loop runs at ~365–415 Hz after the fix.