net-ssh-multi 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.rdoc +13 -0
- data/Manifest +23 -0
- data/README.rdoc +87 -0
- data/Rakefile +28 -0
- data/lib/net/ssh/multi.rb +71 -0
- data/lib/net/ssh/multi/channel.rb +216 -0
- data/lib/net/ssh/multi/channel_proxy.rb +50 -0
- data/lib/net/ssh/multi/dynamic_server.rb +71 -0
- data/lib/net/ssh/multi/pending_connection.rb +112 -0
- data/lib/net/ssh/multi/server.rb +229 -0
- data/lib/net/ssh/multi/server_list.rb +80 -0
- data/lib/net/ssh/multi/session.rb +546 -0
- data/lib/net/ssh/multi/session_actions.rb +153 -0
- data/lib/net/ssh/multi/subsession.rb +48 -0
- data/lib/net/ssh/multi/version.rb +21 -0
- data/net-ssh-multi.gemspec +59 -0
- data/setup.rb +1585 -0
- data/test/channel_test.rb +152 -0
- data/test/common.rb +2 -0
- data/test/multi_test.rb +20 -0
- data/test/server_test.rb +256 -0
- data/test/session_actions_test.rb +128 -0
- data/test/session_test.rb +201 -0
- data/test/test_all.rb +3 -0
- metadata +93 -0
@@ -0,0 +1,546 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'net/ssh/gateway'
|
3
|
+
require 'net/ssh/multi/server'
|
4
|
+
require 'net/ssh/multi/dynamic_server'
|
5
|
+
require 'net/ssh/multi/server_list'
|
6
|
+
require 'net/ssh/multi/channel'
|
7
|
+
require 'net/ssh/multi/pending_connection'
|
8
|
+
require 'net/ssh/multi/session_actions'
|
9
|
+
require 'net/ssh/multi/subsession'
|
10
|
+
|
11
|
+
module Net; module SSH; module Multi
|
12
|
+
# Represents a collection of connections to various servers. It provides an
|
13
|
+
# interface for organizing the connections (#group), as well as a way to
|
14
|
+
# scope commands to a subset of all connections (#with). You can also provide
|
15
|
+
# a default gateway connection that servers should use when connecting
|
16
|
+
# (#via). It exposes an interface similar to Net::SSH::Connection::Session
|
17
|
+
# for opening SSH channels and executing commands, allowing for these
|
18
|
+
# operations to be done in parallel across multiple connections.
|
19
|
+
#
|
20
|
+
# Net::SSH::Multi.start do |session|
|
21
|
+
# # access servers via a gateway
|
22
|
+
# session.via 'gateway', 'gateway-user'
|
23
|
+
#
|
24
|
+
# # define the servers we want to use
|
25
|
+
# session.use 'user1@host1'
|
26
|
+
# session.use 'user2@host2'
|
27
|
+
#
|
28
|
+
# # define servers in groups for more granular access
|
29
|
+
# session.group :app do
|
30
|
+
# session.use 'user@app1'
|
31
|
+
# session.use 'user@app2'
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# # execute commands on all servers
|
35
|
+
# session.exec "uptime"
|
36
|
+
#
|
37
|
+
# # execute commands on a subset of servers
|
38
|
+
# session.with(:app).exec "hostname"
|
39
|
+
#
|
40
|
+
# # run the aggregated event loop
|
41
|
+
# session.loop
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# Note that connections are established lazily, as soon as they are needed.
|
45
|
+
# You can force the connections to be opened immediately, though, using the
|
46
|
+
# #connect! method.
|
47
|
+
#
|
48
|
+
# == Concurrent Connection Limiting
|
49
|
+
#
|
50
|
+
# Sometimes you may be dealing with a large number of servers, and if you
|
51
|
+
# try to have connections open to all of them simultaneously you'll run into
|
52
|
+
# open file handle limitations and such. If this happens to you, you can set
|
53
|
+
# the #concurrent_connections property of the session. Net::SSH::Multi will
|
54
|
+
# then ensure that no more than this number of connections are ever open
|
55
|
+
# simultaneously.
|
56
|
+
#
|
57
|
+
# Net::SSH::Multi.start(:concurrent_connections => 5) do |session|
|
58
|
+
# # ...
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# Opening channels and executing commands will still work exactly as before,
|
62
|
+
# but Net::SSH::Multi will transparently close finished connections and open
|
63
|
+
# pending ones.
|
64
|
+
#
|
65
|
+
# == Controlling Connection Errors
|
66
|
+
#
|
67
|
+
# By default, Net::SSH::Multi will raise an exception if a connection error
|
68
|
+
# occurs when connecting to a server. This will typically bubble up and abort
|
69
|
+
# the entire connection process. Sometimes, however, you might wish to ignore
|
70
|
+
# connection errors, for instance when starting a daemon on a large number of
|
71
|
+
# boxes and you know that some of the boxes are going to be unavailable.
|
72
|
+
#
|
73
|
+
# To do this, simply set the #on_error property of the session to :ignore
|
74
|
+
# (or to :warn, if you want a warning message when a connection attempt
|
75
|
+
# fails):
|
76
|
+
#
|
77
|
+
# Net::SSH::Multi.start(:on_error => :ignore) do |session|
|
78
|
+
# # ...
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# The default is :fail, which causes the exception to bubble up. Additionally,
|
82
|
+
# you can specify a Proc object as the value for #on_error, which will be
|
83
|
+
# invoked with the server in question if the connection attempt fails. You
|
84
|
+
# can force the connection attempt to retry by throwing the :go symbol, with
|
85
|
+
# :retry as the payload, or force the exception to be reraised by throwing
|
86
|
+
# :go with :raise as the payload:
|
87
|
+
#
|
88
|
+
# handler = Proc.new do |server|
|
89
|
+
# server[:connection_attempts] ||= 0
|
90
|
+
# if server[:connection_attempts] < 3
|
91
|
+
# server[:connection_attempts] += 1
|
92
|
+
# throw :go, :retry
|
93
|
+
# else
|
94
|
+
# throw :go, :raise
|
95
|
+
# end
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# Net::SSH::Multi.start(:on_error => handler) do |session|
|
99
|
+
# # ...
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# Any other thrown value (or no thrown value at all) will result in the
|
103
|
+
# failure being ignored.
|
104
|
+
#
|
105
|
+
# == Lazily Evaluated Server Definitions
|
106
|
+
#
|
107
|
+
# Sometimes you might be dealing with an environment where you don't know the
|
108
|
+
# names or addresses of the servers until runtime. You can certainly dynamically
|
109
|
+
# build server names and pass them to #use, but if the operation to determine
|
110
|
+
# the server names is expensive, you might want to defer it until the server
|
111
|
+
# is actually needed (especially if the logic of your program is such that
|
112
|
+
# you might not even need to connect to that server every time the program
|
113
|
+
# runs).
|
114
|
+
#
|
115
|
+
# You can do this by passing a block to #use:
|
116
|
+
#
|
117
|
+
# session.use do |opt|
|
118
|
+
# lookup_ip_address_of_remote_host
|
119
|
+
# end
|
120
|
+
#
|
121
|
+
# See #use for more information about this usage.
|
122
|
+
class Session
|
123
|
+
include SessionActions
|
124
|
+
|
125
|
+
# The Net::SSH::Multi::ServerList managed by this session.
|
126
|
+
attr_reader :server_list
|
127
|
+
|
128
|
+
# The default Net::SSH::Gateway instance to use to connect to the servers.
|
129
|
+
# If +nil+, no default gateway will be used.
|
130
|
+
attr_reader :default_gateway
|
131
|
+
|
132
|
+
# The hash of group definitions, mapping each group name to a corresponding
|
133
|
+
# Net::SSH::Multi::ServerList.
|
134
|
+
attr_reader :groups
|
135
|
+
|
136
|
+
# The number of allowed concurrent connections. No more than this number
|
137
|
+
# of sessions will be open at any given time.
|
138
|
+
attr_accessor :concurrent_connections
|
139
|
+
|
140
|
+
# How connection errors should be handled. This defaults to :fail, but
|
141
|
+
# may be set to :ignore if connection errors should be ignored, or
|
142
|
+
# :warn if connection errors should cause a warning.
|
143
|
+
attr_accessor :on_error
|
144
|
+
|
145
|
+
# The default user name to use when connecting to a server. If a user name
|
146
|
+
# is not given for a particular server, this value will be used. It defaults
|
147
|
+
# to ENV['USER'] || ENV['USERNAME'], or "unknown" if neither of those are
|
148
|
+
# set.
|
149
|
+
attr_accessor :default_user
|
150
|
+
|
151
|
+
# The number of connections that are currently open.
|
152
|
+
attr_reader :open_connections #:nodoc:
|
153
|
+
|
154
|
+
# The list of "open" groups, which will receive subsequent server definitions.
|
155
|
+
# See #use and #group.
|
156
|
+
attr_reader :open_groups #:nodoc:
|
157
|
+
|
158
|
+
# Creates a new Net::SSH::Multi::Session instance. Initially, it contains
|
159
|
+
# no server definitions, no group definitions, and no default gateway.
|
160
|
+
#
|
161
|
+
# You can set the #concurrent_connections property in the options. Setting
|
162
|
+
# it to +nil+ (the default) will cause Net::SSH::Multi to ignore any
|
163
|
+
# concurrent connection limit and allow all defined sessions to be open
|
164
|
+
# simultaneously. Setting it to an integer will cause Net::SSH::Multi to
|
165
|
+
# allow no more than that number of concurrently open sessions, opening
|
166
|
+
# subsequent sessions only when other sessions finish and close.
|
167
|
+
#
|
168
|
+
# Net::SSH::Multi.start(:concurrent_connections => 10) do |session|
|
169
|
+
# session.use ...
|
170
|
+
# end
|
171
|
+
def initialize(options={})
|
172
|
+
@server_list = ServerList.new
|
173
|
+
@groups = Hash.new { |h,k| h[k] = ServerList.new }
|
174
|
+
@gateway = nil
|
175
|
+
@open_groups = []
|
176
|
+
@connect_threads = []
|
177
|
+
@on_error = :fail
|
178
|
+
@default_user = ENV['USER'] || ENV['USERNAME'] || "unknown"
|
179
|
+
|
180
|
+
@open_connections = 0
|
181
|
+
@pending_sessions = []
|
182
|
+
@session_mutex = Mutex.new
|
183
|
+
|
184
|
+
options.each { |opt, value| send("#{opt}=", value) }
|
185
|
+
end
|
186
|
+
|
187
|
+
# At its simplest, this associates a named group with a server definition.
|
188
|
+
# It can be used in either of two ways:
|
189
|
+
#
|
190
|
+
# First, you can use it to associate a group (or array of groups) with a
|
191
|
+
# server definition (or array of server definitions). The server definitions
|
192
|
+
# must already exist in the #server_list array (typically by calling #use):
|
193
|
+
#
|
194
|
+
# server1 = session.use('host1', 'user1')
|
195
|
+
# server2 = session.use('host2', 'user2')
|
196
|
+
# session.group :app => server1, :web => server2
|
197
|
+
# session.group :staging => [server1, server2]
|
198
|
+
# session.group %w(xen linux) => server2
|
199
|
+
# session.group %w(rackspace backup) => [server1, server2]
|
200
|
+
#
|
201
|
+
# Secondly, instead of a mapping of groups to servers, you can just
|
202
|
+
# provide a list of group names, and then a block. Inside the block, any
|
203
|
+
# calls to #use will automatically associate the new server definition with
|
204
|
+
# those groups. You can nest #group calls, too, which will aggregate the
|
205
|
+
# group definitions.
|
206
|
+
#
|
207
|
+
# session.group :rackspace, :backup do
|
208
|
+
# session.use 'host1', 'user1'
|
209
|
+
# session.group :xen do
|
210
|
+
# session.use 'host2', 'user2'
|
211
|
+
# end
|
212
|
+
# end
|
213
|
+
def group(*args)
|
214
|
+
mapping = args.last.is_a?(Hash) ? args.pop : {}
|
215
|
+
|
216
|
+
if mapping.any? && block_given?
|
217
|
+
raise ArgumentError, "must provide group mapping OR block, not both"
|
218
|
+
elsif block_given?
|
219
|
+
begin
|
220
|
+
saved_groups = open_groups.dup
|
221
|
+
open_groups.concat(args.map { |a| a.to_sym }).uniq!
|
222
|
+
yield self if block_given?
|
223
|
+
ensure
|
224
|
+
open_groups.replace(saved_groups)
|
225
|
+
end
|
226
|
+
else
|
227
|
+
mapping.each do |key, value|
|
228
|
+
(open_groups + Array(key)).uniq.each do |grp|
|
229
|
+
groups[grp.to_sym].concat(Array(value))
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Sets up a default gateway to use when establishing connections to servers.
|
236
|
+
# Note that any servers defined prior to this invocation will not use the
|
237
|
+
# default gateway; it only affects servers defined subsequently.
|
238
|
+
#
|
239
|
+
# session.via 'gateway.host', 'user'
|
240
|
+
#
|
241
|
+
# You may override the default gateway on a per-server basis by passing the
|
242
|
+
# :via key to the #use method; see #use for details.
|
243
|
+
def via(host, user, options={})
|
244
|
+
@default_gateway = Net::SSH::Gateway.new(host, user, options)
|
245
|
+
self
|
246
|
+
end
|
247
|
+
|
248
|
+
# Defines a new server definition, to be managed by this session. The
|
249
|
+
# server is at the given +host+, and will be connected to as the given
|
250
|
+
# +user+. The other options are passed as-is to the Net::SSH session
|
251
|
+
# constructor.
|
252
|
+
#
|
253
|
+
# If a default gateway has been specified previously (with #via) it will
|
254
|
+
# be passed to the new server definition. You can override this by passing
|
255
|
+
# a different Net::SSH::Gateway instance (or +nil+) with the :via key in
|
256
|
+
# the +options+.
|
257
|
+
#
|
258
|
+
# session.use 'host'
|
259
|
+
# session.use 'user@host2', :via => nil
|
260
|
+
# session.use 'host3', :user => "user3", :via => Net::SSH::Gateway.new('gateway.host', 'user')
|
261
|
+
#
|
262
|
+
# If only a single host is given, the new server instance is returned. You
|
263
|
+
# can give multiple hosts at a time, though, in which case an array of
|
264
|
+
# server instances will be returned.
|
265
|
+
#
|
266
|
+
# server1, server2 = session.use "host1", "host2"
|
267
|
+
#
|
268
|
+
# If given a block, this will save the block as a Net::SSH::Multi::DynamicServer
|
269
|
+
# definition, to be evaluated lazily the first time the server is needed.
|
270
|
+
# The block will recive any options hash given to #use, and should return
|
271
|
+
# +nil+ (if no servers are to be added), a String or an array of Strings
|
272
|
+
# (to be interpreted as a connection specification), or a Server or an
|
273
|
+
# array of Servers.
|
274
|
+
def use(*hosts, &block)
|
275
|
+
options = hosts.last.is_a?(Hash) ? hosts.pop : {}
|
276
|
+
options = { :via => default_gateway }.merge(options)
|
277
|
+
|
278
|
+
results = hosts.map do |host|
|
279
|
+
server_list.add(Server.new(self, host, options))
|
280
|
+
end
|
281
|
+
|
282
|
+
if block
|
283
|
+
results << server_list.add(DynamicServer.new(self, options, block))
|
284
|
+
end
|
285
|
+
|
286
|
+
group [] => results
|
287
|
+
results.length > 1 ? results : results.first
|
288
|
+
end
|
289
|
+
|
290
|
+
# Essentially an alias for #servers_for without any arguments. This is used
|
291
|
+
# primarily to satistfy the expectations of the Net::SSH::Multi::SessionActions
|
292
|
+
# module.
|
293
|
+
def servers
|
294
|
+
servers_for
|
295
|
+
end
|
296
|
+
|
297
|
+
# Returns the set of servers that match the given criteria. It can be used
|
298
|
+
# in any (or all) of three ways.
|
299
|
+
#
|
300
|
+
# First, you can omit any arguments. In this case, the full list of servers
|
301
|
+
# will be returned.
|
302
|
+
#
|
303
|
+
# all = session.servers_for
|
304
|
+
#
|
305
|
+
# Second, you can simply specify a list of group names. All servers in all
|
306
|
+
# named groups will be returned. If a server belongs to multiple matching
|
307
|
+
# groups, then it will appear only once in the list (the resulting list
|
308
|
+
# will contain only unique servers).
|
309
|
+
#
|
310
|
+
# servers = session.servers_for(:app, :db)
|
311
|
+
#
|
312
|
+
# Last, you can specify a hash with group names as keys, and property
|
313
|
+
# constraints as the values. These property constraints are either "only"
|
314
|
+
# constraints (which restrict the set of servers to "only" those that match
|
315
|
+
# the given properties) or "except" constraints (which restrict the set of
|
316
|
+
# servers to those whose properties do _not_ match). Properties are described
|
317
|
+
# when the server is defined (via the :properties key):
|
318
|
+
#
|
319
|
+
# session.group :db do
|
320
|
+
# session.use 'dbmain', 'user', :properties => { :primary => true }
|
321
|
+
# session.use 'dbslave', 'user2'
|
322
|
+
# session.use 'dbslve2', 'user2'
|
323
|
+
# end
|
324
|
+
#
|
325
|
+
# # return ONLY on the servers in the :db group which have the :primary
|
326
|
+
# # property set to true.
|
327
|
+
# primary = session.servers_for(:db => { :only => { :primary => true } })
|
328
|
+
#
|
329
|
+
# You can, naturally, combine these methods:
|
330
|
+
#
|
331
|
+
# # all servers in :app and :web, and all servers in :db with the
|
332
|
+
# # :primary property set to true
|
333
|
+
# servers = session.servers_for(:app, :web, :db => { :only => { :primary => true } })
|
334
|
+
def servers_for(*criteria)
|
335
|
+
if criteria.empty?
|
336
|
+
server_list.flatten
|
337
|
+
else
|
338
|
+
# normalize the criteria list, so that every entry is a key to a
|
339
|
+
# criteria hash (possibly empty).
|
340
|
+
criteria = criteria.inject({}) do |hash, entry|
|
341
|
+
case entry
|
342
|
+
when Hash then hash.merge(entry)
|
343
|
+
else hash.merge(entry => {})
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
list = criteria.inject([]) do |aggregator, (group, properties)|
|
348
|
+
raise ArgumentError, "the value for any group must be a Hash, but got a #{properties.class} for #{group.inspect}" unless properties.is_a?(Hash)
|
349
|
+
bad_keys = properties.keys - [:only, :except]
|
350
|
+
raise ArgumentError, "unknown constraint(s) #{bad_keys.inspect} for #{group.inspect}" unless bad_keys.empty?
|
351
|
+
|
352
|
+
servers = groups[group].select do |server|
|
353
|
+
(properties[:only] || {}).all? { |prop, value| server[prop] == value } &&
|
354
|
+
!(properties[:except] || {}).any? { |prop, value| server[prop] == value }
|
355
|
+
end
|
356
|
+
|
357
|
+
aggregator.concat(servers)
|
358
|
+
end
|
359
|
+
|
360
|
+
list.uniq
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
# Returns a new Net::SSH::Multi::Subsession instance consisting of the
|
365
|
+
# servers that meet the given criteria. If a block is given, the
|
366
|
+
# subsession will be yielded to it. See #servers_for for a discussion of
|
367
|
+
# how these criteria are interpreted.
|
368
|
+
#
|
369
|
+
# session.with(:app).exec('hostname')
|
370
|
+
#
|
371
|
+
# session.with(:app, :db => { :primary => true }) do |s|
|
372
|
+
# s.exec 'date'
|
373
|
+
# s.exec 'uptime'
|
374
|
+
# end
|
375
|
+
def with(*groups)
|
376
|
+
subsession = Subsession.new(self, servers_for(*groups))
|
377
|
+
yield subsession if block_given?
|
378
|
+
subsession
|
379
|
+
end
|
380
|
+
|
381
|
+
# Works as #with, but for specific servers rather than groups. It will
|
382
|
+
# return a new subsession (Net::SSH::Multi::Subsession) consisting of
|
383
|
+
# the given servers. (Note that it requires that the servers in question
|
384
|
+
# have been created via calls to #use on this session object, or things
|
385
|
+
# will not work quite right.) If a block is given, the new subsession
|
386
|
+
# will also be yielded to the block.
|
387
|
+
#
|
388
|
+
# srv1 = session.use('host1', 'user')
|
389
|
+
# srv2 = session.use('host2', 'user')
|
390
|
+
# # ...
|
391
|
+
# session.on(srv1, srv2).exec('hostname')
|
392
|
+
def on(*servers)
|
393
|
+
subsession = Subsession.new(self, servers)
|
394
|
+
yield subsession if block_given?
|
395
|
+
subsession
|
396
|
+
end
|
397
|
+
|
398
|
+
# Closes the multi-session by shutting down all open server sessions, and
|
399
|
+
# the default gateway (if one was specified using #via). Note that other
|
400
|
+
# gateway connections (e.g., those passed to #use directly) will _not_ be
|
401
|
+
# closed by this method, and must be managed externally.
|
402
|
+
def close
|
403
|
+
server_list.each { |server| server.close_channels }
|
404
|
+
loop(0) { busy?(true) }
|
405
|
+
server_list.each { |server| server.close }
|
406
|
+
default_gateway.shutdown! if default_gateway
|
407
|
+
end
|
408
|
+
|
409
|
+
alias :loop_forever :loop
|
410
|
+
|
411
|
+
# Run the aggregated event loop for all open server sessions, until the given
|
412
|
+
# block returns +false+. If no block is given, the loop will run for as
|
413
|
+
# long as #busy? returns +true+ (in other words, for as long as there are
|
414
|
+
# any (non-invisible) channels open).
|
415
|
+
def loop(wait=nil, &block)
|
416
|
+
running = block || Proc.new { |c| busy? }
|
417
|
+
loop_forever { break unless process(wait, &running) }
|
418
|
+
end
|
419
|
+
|
420
|
+
# Run a single iteration of the aggregated event loop for all open server
|
421
|
+
# sessions. The +wait+ parameter indicates how long to wait for an event
|
422
|
+
# to appear on any of the different sessions; +nil+ (the default) means
|
423
|
+
# "wait forever". If the block is given, then it will be used to determine
|
424
|
+
# whether #process returns +true+ (the block did not return +false+), or
|
425
|
+
# +false+ (the block returned +false+).
|
426
|
+
def process(wait=nil, &block)
|
427
|
+
realize_pending_connections!
|
428
|
+
wait = @connect_threads.any? ? 0 : wait
|
429
|
+
|
430
|
+
return false unless preprocess(&block)
|
431
|
+
|
432
|
+
readers = server_list.map { |s| s.readers }.flatten
|
433
|
+
writers = server_list.map { |s| s.writers }.flatten
|
434
|
+
|
435
|
+
readers, writers, = IO.select(readers, writers, nil, wait)
|
436
|
+
|
437
|
+
if readers
|
438
|
+
return postprocess(readers, writers)
|
439
|
+
else
|
440
|
+
return true
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# Runs the preprocess stage on all servers. Returns false if the block
|
445
|
+
# returns false, and true if there either is no block, or it returns true.
|
446
|
+
# This is called as part of the #process method.
|
447
|
+
def preprocess(&block) #:nodoc:
|
448
|
+
return false if block && !block[self]
|
449
|
+
server_list.each { |server| server.preprocess }
|
450
|
+
block.nil? || block[self]
|
451
|
+
end
|
452
|
+
|
453
|
+
# Runs the postprocess stage on all servers. Always returns true. This is
|
454
|
+
# called as part of the #process method.
|
455
|
+
def postprocess(readers, writers) #:nodoc:
|
456
|
+
server_list.each { |server| server.postprocess(readers, writers) }
|
457
|
+
true
|
458
|
+
end
|
459
|
+
|
460
|
+
# Takes the #concurrent_connections property into account, and tries to
|
461
|
+
# return a new session for the given server. If the concurrent connections
|
462
|
+
# limit has been reached, then a Net::SSH::Multi::PendingConnection instance
|
463
|
+
# will be returned instead, which will be realized into an actual session
|
464
|
+
# as soon as a slot opens up.
|
465
|
+
#
|
466
|
+
# If +force+ is true, the concurrent_connections check is skipped and a real
|
467
|
+
# connection is always returned.
|
468
|
+
def next_session(server, force=false) #:nodoc:
|
469
|
+
# don't retry a failed attempt
|
470
|
+
return nil if server.failed?
|
471
|
+
|
472
|
+
@session_mutex.synchronize do
|
473
|
+
if !force && concurrent_connections && concurrent_connections <= open_connections
|
474
|
+
connection = PendingConnection.new(server)
|
475
|
+
@pending_sessions << connection
|
476
|
+
return connection
|
477
|
+
end
|
478
|
+
|
479
|
+
@open_connections += 1
|
480
|
+
end
|
481
|
+
|
482
|
+
begin
|
483
|
+
server.new_session
|
484
|
+
rescue Exception => e
|
485
|
+
server.fail!
|
486
|
+
@session_mutex.synchronize { @open_connections -= 1 }
|
487
|
+
|
488
|
+
case on_error
|
489
|
+
when :ignore then
|
490
|
+
# do nothing
|
491
|
+
when :warn then
|
492
|
+
warn("error connecting to #{server}: #{e.class} (#{e.message})")
|
493
|
+
when Proc then
|
494
|
+
go = catch(:go) { on_error.call(server); nil }
|
495
|
+
case go
|
496
|
+
when nil, :ignore then # nothing
|
497
|
+
when :retry then retry
|
498
|
+
when :raise then raise
|
499
|
+
else warn "unknown 'go' command: #{go.inspect}"
|
500
|
+
end
|
501
|
+
else
|
502
|
+
raise
|
503
|
+
end
|
504
|
+
|
505
|
+
return nil
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
# Tells the session that the given server has closed its connection. The
|
510
|
+
# session indicates that a new connection slot is available, which may be
|
511
|
+
# filled by the next pending connection on the next event loop iteration.
|
512
|
+
def server_closed(server) #:nodoc:
|
513
|
+
@session_mutex.synchronize do
|
514
|
+
unless @pending_sessions.delete(server.session)
|
515
|
+
@open_connections -= 1
|
516
|
+
end
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
# Invoked by the event loop. If there is a concurrent_connections limit in
|
521
|
+
# effect, this will close any non-busy sessions and try to open as many
|
522
|
+
# new sessions as it can. It does this in threads, so that existing processing
|
523
|
+
# can continue.
|
524
|
+
#
|
525
|
+
# If there is no concurrent_connections limit in effect, then this method
|
526
|
+
# does nothing.
|
527
|
+
def realize_pending_connections! #:nodoc:
|
528
|
+
return unless concurrent_connections
|
529
|
+
|
530
|
+
server_list.each do |server|
|
531
|
+
server.close if !server.busy?(true)
|
532
|
+
server.update_session!
|
533
|
+
end
|
534
|
+
|
535
|
+
@connect_threads.delete_if { |t| !t.alive? }
|
536
|
+
|
537
|
+
count = concurrent_connections ? (concurrent_connections - open_connections) : @pending_sessions.length
|
538
|
+
count.times do
|
539
|
+
session = @pending_sessions.pop or break
|
540
|
+
@connect_threads << Thread.new do
|
541
|
+
session.replace_with(next_session(session.server, true))
|
542
|
+
end
|
543
|
+
end
|
544
|
+
end
|
545
|
+
end
|
546
|
+
end; end; end
|