Class: SQLiteSweep::SSH::ConnectionManager
- Inherits:
-
Object
- Object
- SQLiteSweep::SSH::ConnectionManager
- Defined in:
- lib/sqlitesweep/ssh/connection_manager.rb
Overview
Manages SSH ControlMaster connections for multiplexed remote access.
Why ControlMaster instead of net-ssh?
- net-ssh is not thread-safe for concurrent operations on a single session
- System ssh with ControlMaster handles multiplexing natively
- SSH agent, config files, and known_hosts all work automatically
How it works:
1. On first query to a host, establishes a background master connection
(ssh -N -f with ControlMaster=yes) that stays alive via ControlPersist
2. Subsequent SSH commands to the same host multiplex over the existing
master via ControlPath — no new TCP/auth handshake needed
3. A semaphore enforces --max-ssh to cap total master connections
4. On shutdown, sends "ssh -O exit" to each master and cleans up sockets
Socket files live in /tmp/sqlitesweep_ssh_<pid>/ and are cleaned up on shutdown.
Instance Method Summary collapse
-
#ensure_master(host_key) ⇒ Object
Ensures a ControlMaster connection exists for the given host.
-
#initialize(config) ⇒ ConnectionManager
constructor
A new instance of ConnectionManager.
-
#run(host_key, remote_command, timeout: nil) ⇒ String
Runs a command on a remote host over the multiplexed SSH connection.
-
#shutdown ⇒ Object
Shuts down all ControlMaster connections and cleans up socket files.
-
#socket_path(host_key) ⇒ String
Returns the Unix socket path for a given host’s ControlMaster.
Constructor Details
#initialize(config) ⇒ ConnectionManager
Returns a new instance of ConnectionManager.
26 27 28 29 30 31 32 33 |
# File 'lib/sqlitesweep/ssh/connection_manager.rb', line 26 def initialize(config) @config = config @socket_dir = File.join("/tmp", "sqlitesweep_ssh_#{Process.pid}") FileUtils.mkdir_p(@socket_dir) @semaphore = Concurrent::Semaphore.new(config.max_ssh) @masters = Concurrent::Map.new # host_key => true (thread-safe set) @mutex = Mutex.new end |
Instance Method Details
#ensure_master(host_key) ⇒ Object
Ensures a ControlMaster connection exists for the given host. If one already exists, returns immediately. Otherwise, acquires a semaphore permit and establishes a new master.
Uses BatchMode=yes to prevent password prompts (fails fast if key-based auth isn’t configured). StrictHostKeyChecking=accept-new auto-accepts new hosts but rejects changed keys.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/sqlitesweep/ssh/connection_manager.rb', line 53 def ensure_master(host_key) return if @masters[host_key] @semaphore.acquire begin # Double-check after acquiring semaphore (another thread may have # established the master while we were waiting) return if @masters[host_key] socket = socket_path(host_key) cmd = [ "ssh", "-o", "ControlMaster=yes", "-o", "ControlPath=#{socket}", "-o", "ControlPersist=120", "-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=#{@config.ssh_timeout}", "-N", "-f", # No command, go to background host_key ] _out, err, status = Open3.capture3(*cmd) unless status.success? @semaphore.release raise SSHError, "Failed to establish SSH master to #{host_key}: #{err.strip}" end @masters[host_key] = true rescue SSHError raise rescue => e @semaphore.release raise SSHError, "SSH connection error to #{host_key}: #{e.}" end end |
#run(host_key, remote_command, timeout: nil) ⇒ String
Runs a command on a remote host over the multiplexed SSH connection. Ensures a ControlMaster exists first, then executes via ControlPath.
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/sqlitesweep/ssh/connection_manager.rb', line 99 def run(host_key, remote_command, timeout: nil) ensure_master(host_key) socket = socket_path(host_key) cmd = [ "ssh", "-o", "ControlPath=#{socket}", "-o", "BatchMode=yes", host_key, remote_command ] timeout ||= @config.query_timeout out, err, status = execute_with_timeout(cmd, timeout) unless status.success? raise QueryError, "Remote command failed on #{host_key}: #{err.strip}" end out end |
#shutdown ⇒ Object
Shuts down all ControlMaster connections and cleans up socket files. Sends “ssh -O exit” to each master to gracefully close it.
123 124 125 126 127 128 129 130 131 |
# File 'lib/sqlitesweep/ssh/connection_manager.rb', line 123 def shutdown @masters.each_key do |host_key| socket = socket_path(host_key) system("ssh", "-o", "ControlPath=#{socket}", "-O", "exit", host_key, out: File::NULL, err: File::NULL) end @masters.clear FileUtils.rm_rf(@socket_dir) if Dir.exist?(@socket_dir) end |
#socket_path(host_key) ⇒ String
Returns the Unix socket path for a given host’s ControlMaster.
39 40 41 |
# File 'lib/sqlitesweep/ssh/connection_manager.rb', line 39 def socket_path(host_key) File.join(@socket_dir, host_key.gsub("/", "_")) end |