Fix SSH tunnel not being closed after remote server reboot (#406)#407
Open
debba wants to merge 4 commits into
Open
Fix SSH tunnel not being closed after remote server reboot (#406)#407debba wants to merge 4 commits into
debba wants to merge 4 commits into
Conversation
…oot (#406) SSH tunnels were cached in the global TUNNELS map but never stopped or removed, so a tunnel outlived the connection that owned it. When the remote host rebooted the tunnel died while still holding its local forward port; the next reconnect reused the stale map entry and failed with "port already in use". Only restarting the app cleared the map. - ssh_tunnel: reap the killed system-ssh child in stop(); add is_alive() and remove_tunnel() helpers. - commands: skip and discard dead tunnels on reuse; tear down the tunnel in disconnect_connection. - health_check: tear down the tunnel when a connection exceeds the failure threshold (the path triggered by a server reboot).
…check failure Builds on the existing stale-tunnel eviction: - russh: the forwarding loop now checks handle.is_closed() and shuts itself down when the SSH session dies, releasing the local port; the running flag doubles as liveness state - system ssh: add ServerAliveInterval/CountMax and ExitOnForwardFailure so the process exits when the server goes away instead of holding the port forever - share the tunnel-key construction (ssh_tunnel_key_for) between teardown_ssh_tunnel and the new evict_dead_ssh_tunnel, and use the latter in resolve_connection_params instead of the inline stale check - health check: fail pings fast when the tunnel is dead, and close pools from expanded params without resolving (no tunnel rebuild toward a server that may still be down); dedupe param expansion in expand_params()
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.
Closes #406
What was happening
When a MySQL connection went through an SSH tunnel and the remote server rebooted, Tabularis correctly detected the drop and cleared the connection indicator. But trying to reconnect failed with a "port already in use" error, and the only way out was to restart the app.
The reason is that SSH tunnels are cached in a global map (keyed by
user@host:port:remote->port) and reused across connects, but nothing ever tore them down. A tunnel could outlive the connection it belonged to. Once the remote host rebooted, the tunnel died while its local forward port was still held by the (now defunct) ssh process — or, in the russh case, by a forwarding loop that never noticed the session was gone — so the next reconnect happily picked up the stale map entry, pointed at that dead port, and blew up. Restarting the app "fixed" it only because that wiped the map.What I changed
ssh_tunnel.rshandle.is_closed()on every poll iteration: when the SSH session dies it shuts itself down and releases the local port. Without this a russh tunnel stayed "alive" forever and the eviction below would never trigger for it.ServerAliveInterval=15,ServerAliveCountMax=3andExitOnForwardFailure=yes, so the ssh process exits when the server goes away instead of holding the local port indefinitely.stop()now also reaps the killed system-ssh child (wait()), so it doesn't linger as a zombie still holding the forwarded port.is_alive()to tell whether a cached tunnel is still usable (system-ssh child exited, or russh loop shut down after session death /stop()).remove_tunnel(key)to stop a tunnel and drop it from the map (no-op if it isn't there).commands.rsresolve_connection_paramsevicts a dead one via the newevict_dead_ssh_tunnel()and creates a fresh tunnel instead of reusing the broken port.ssh_tunnel_key_for(), used by bothteardown_ssh_tunnel()andevict_dead_ssh_tunnel().disconnect_connectiontears the tunnel down so it no longer survives the connection.health_check.rsconnection_idthe pool key only depends on driver/id/database, and resolving would have recreated a tunnel toward the dead server.expand_params().Tests
Added unit tests in
ssh_tunnel.rsforis_alive()on both backends (russh flag + system-ssh exited child), forstop()reaping the child, and forremove_tunnel(both the missing-key no-op and real removal), plus tests incommands.rsfor theevict_dead_ssh_tunnel()no-op paths. Full backend suite passes (the only failures are the pre-existing, environment-specificaskpasssocket-path ones).Notes
Tunnels are shared by key, so two connections to the same DB over the same SSH host share one tunnel — tearing it down on disconnect affects both, which is the intended behaviour here since a reboot takes them all down anyway.