class SSHKit::Backend::ConnectionPool

The ConnectionPool caches connections and allows them to be reused, so long as the reuse happens within the `idle_timeout` period. Timed out connections are eventually closed, forcing a new connection to be used in that case.

Additionally, a background thread is started to check for abandoned connections that have timed out without any attempt at being reused. These are eventually closed as well and removed from the cache.

If `idle_timeout` set to `false`, `0`, or `nil`, no caching is performed, and a new connection is created and then immediately closed each time. The default timeout is 30 (seconds).

There is a single public method: `with`. Example usage:

pool = SSHKit::Backend::ConnectionPool.new
pool.with(Net::SSH.method(:start), "host", "username") do |connection|
  # do stuff with connection
end

Constants

NilCache

A cache that holds no connections. Any connection provided to this cache is simply closed.

Attributes

caches[R]
idle_timeout[RW]
timed_out_connections[R]

Public Class Methods

new(idle_timeout=30) click to toggle source
# File lib/sshkit/backends/connection_pool.rb, line 44
def initialize(idle_timeout=30)
  @idle_timeout = idle_timeout
  @caches = {}
  @caches.extend(MonitorMixin)
  @timed_out_connections = Queue.new

  # Spin up eviction loop only if caching is enabled
  if cache_enabled?
    Thread.new { run_eviction_loop }
  end
end

Public Instance Methods

close_connections() click to toggle source

Immediately close all cached connections and empty the pool.

# File lib/sshkit/backends/connection_pool.rb, line 80
def close_connections
  caches.synchronize do
    caches.values.each(&:clear)
    caches.clear
    process_deferred_close
  end
end
flush_connections() click to toggle source

Immediately remove all cached connections, without closing them. This only exists for unit test purposes.

# File lib/sshkit/backends/connection_pool.rb, line 75
def flush_connections
  caches.synchronize { caches.clear }
end
with(connection_factory, *args) { |conn| ... } click to toggle source

Creates a new connection or reuses a cached connection (if possible) and yields the connection to the given block. Connections are created by invoking the `connection_factory` proc with the given `args`. The arguments are used to construct a key used for caching.

# File lib/sshkit/backends/connection_pool.rb, line 60
def with(connection_factory, *args)
  cache = find_cache(args)
  conn = cache.pop || begin
    connection_factory.call(*args)
  end
  yield(conn)
ensure
  cache.push(conn) unless conn.nil?
  # Sometimes the args mutate as a result of opening a connection. In this
  # case we need to update the cache key to match the new args.
  update_key_if_args_changed(cache, args)
end

Private Instance Methods

cache_enabled?() click to toggle source
# File lib/sshkit/backends/connection_pool.rb, line 98
def cache_enabled?
  idle_timeout && idle_timeout > 0
end
cache_key_for_connection_args(args) click to toggle source
# File lib/sshkit/backends/connection_pool.rb, line 94
def cache_key_for_connection_args(args)
  args.hash
end
find_cache(args) click to toggle source

Look up a Cache that matches the given connection arguments.

# File lib/sshkit/backends/connection_pool.rb, line 103
def find_cache(args)
  if cache_enabled?
    key = cache_key_for_connection_args(args)
    caches[key] || thread_safe_find_or_create_cache(key)
  else
    NilCache.new(method(:silently_close_connection))
  end
end
process_deferred_close() click to toggle source

Immediately close any connections that are pending closure. rubocop:disable Lint/HandleExceptions

# File lib/sshkit/backends/connection_pool.rb, line 147
def process_deferred_close
  until timed_out_connections.empty?
    connection = timed_out_connections.pop(true)
    silently_close_connection(connection)
  end
rescue ThreadError
  # Queue#pop(true) raises ThreadError if the queue is empty.
  # This could only happen if `close_connections` is called at the same time
  # the background eviction thread has woken up to close connections. In any
  # case, it is not something we need to care about, since an empty queue is
  # perfectly OK.
end
run_eviction_loop() click to toggle source

Loops indefinitely to close connections and to find abandoned connections that need to be closed.

# File lib/sshkit/backends/connection_pool.rb, line 135
def run_eviction_loop
  loop do
    process_deferred_close

    # Periodically sweep all Caches to evict stale connections
    sleep(5)
    caches.values.each(&:evict)
  end
end
silently_close_connection(connection) click to toggle source

Close the given `connection` immediately, assuming it responds to a `close` method. If it doesn't, or if `nil` is provided, it is silently ignored. Any `StandardError` is also silently ignored. Returns `true` if the connection was closed; `false` if it was already closed or could not be closed due to an error.

# File lib/sshkit/backends/connection_pool.rb, line 172
def silently_close_connection(connection)
  return false unless connection.respond_to?(:close)
  return false if connection.respond_to?(:closed?) && connection.closed?
  connection.close
  true
rescue StandardError
  false
end
silently_close_connection_later(connection) click to toggle source

Adds the connection to a queue that is processed asynchronously by a background thread. The connection will eventually be closed.

# File lib/sshkit/backends/connection_pool.rb, line 163
def silently_close_connection_later(connection)
  timed_out_connections << connection
end
thread_safe_find_or_create_cache(key) click to toggle source

Cache creation needs to happen in a mutex, because otherwise a race condition might cause two identical caches to be created for the same key.

# File lib/sshkit/backends/connection_pool.rb, line 114
def thread_safe_find_or_create_cache(key)
  caches.synchronize do
    caches[key] ||= begin
      Cache.new(key, idle_timeout, method(:silently_close_connection_later))
    end
  end
end
update_key_if_args_changed(cache, args) click to toggle source

Update cache key with changed args to prevent cache miss

# File lib/sshkit/backends/connection_pool.rb, line 123
def update_key_if_args_changed(cache, args)
  new_key = cache_key_for_connection_args(args)

  caches.synchronize do
    return if cache.same_key?(new_key)
    caches[new_key] = caches.delete(cache.key)
    cache.key = new_key
  end
end