pylon 0.2.7 → 0.2.8
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/lib/pylon.rb +1 -1
- data/lib/pylon/application.rb +12 -7
- data/lib/pylon/command.rb +133 -0
- data/lib/pylon/command/new_leader.rb +26 -0
- data/lib/pylon/command/ping.rb +26 -0
- data/lib/pylon/command/status.rb +26 -0
- data/lib/pylon/command_handler.rb +30 -0
- data/lib/pylon/config.rb +6 -6
- data/lib/pylon/elector.rb +45 -63
- data/lib/pylon/exceptions.rb +38 -0
- data/lib/pylon/failure_detector.rb +28 -0
- data/lib/pylon/mixin/convert_to_class_name.rb +70 -0
- data/lib/pylon/node.rb +37 -47
- metadata +114 -84
data/lib/pylon.rb
CHANGED
data/lib/pylon/application.rb
CHANGED
@@ -119,6 +119,17 @@ class Pylon
|
|
119
119
|
:description => "Group to set privilege to",
|
120
120
|
:proc => nil
|
121
121
|
|
122
|
+
option :multicast,
|
123
|
+
:short => "-M",
|
124
|
+
:long => "--multicast",
|
125
|
+
:description => "Enable multicast support via encapuslated pragmatic general multicast",
|
126
|
+
:proc => lambda { |m| true }
|
127
|
+
|
128
|
+
option :multicast_interface,
|
129
|
+
:short => "-i INTERFACE",
|
130
|
+
:long => "--multicast-interface INTERFACE",
|
131
|
+
:description => "Interface to use to send multicast over"
|
132
|
+
|
122
133
|
option :multicast_address,
|
123
134
|
:short => "-a ADDRESS",
|
124
135
|
:long => "--multicast-address ADDRESS",
|
@@ -135,16 +146,11 @@ class Pylon
|
|
135
146
|
:description => "Enable multicast over loopback interfaces",
|
136
147
|
:proc => lambda { |loop| true }
|
137
148
|
|
138
|
-
option :interface,
|
139
|
-
:short => "-i INTERFACE",
|
140
|
-
:long => "--interface INTERFACE",
|
141
|
-
:description => "Interface to use to send multicast over"
|
142
|
-
|
143
149
|
option :tcp_address,
|
144
150
|
:short => "-t TCPADDRESS",
|
145
151
|
:long => "--tcp-address TCPADDRESS",
|
146
152
|
:description => "Interface to use to bind request socket to"
|
147
|
-
|
153
|
+
|
148
154
|
option :tcp_port,
|
149
155
|
:short => "-P TCPPORT",
|
150
156
|
:long => "--tcp-port TCPPORT",
|
@@ -155,7 +161,6 @@ class Pylon
|
|
155
161
|
:long => "--minimum-master-nodes NODES",
|
156
162
|
:description => "How many nodes to wait for before starting master election",
|
157
163
|
:proc => lambda { |nodes| nodes.to_i }
|
158
|
-
|
159
164
|
end
|
160
165
|
end
|
161
166
|
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# Author:: AJ Christensen (<aj@junglist.gen.nz>)
|
2
|
+
# Copyright:: Copyright (c) 2011 AJ Christensen
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or#implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
require_relative "../pylon" # For the PYLON_ROOT const
|
18
|
+
require_relative "log"
|
19
|
+
require_relative "exceptions"
|
20
|
+
require_relative "mixin/convert_to_class_name"
|
21
|
+
|
22
|
+
class Pylon
|
23
|
+
class Command
|
24
|
+
|
25
|
+
extend Pylon::Mixin::ConvertToClassName
|
26
|
+
|
27
|
+
attr_accessor :options, :config
|
28
|
+
|
29
|
+
def self.options
|
30
|
+
@options ||= {}
|
31
|
+
@options
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.options=(val)
|
35
|
+
raise(ArgumentError, "Options must recieve a hash") unless val.kind_of?(Hash)
|
36
|
+
@options = val
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(*args)
|
40
|
+
@options = Hash.new
|
41
|
+
@config = Hash.new
|
42
|
+
|
43
|
+
klass_options = self.class.options
|
44
|
+
klass_options.keys.inject(@options) { |memo,key| memo[key] = klass_options[key].dup; memo }
|
45
|
+
|
46
|
+
@options.each do |config_key, config_opts|
|
47
|
+
config[config_key] = config_opts
|
48
|
+
end
|
49
|
+
|
50
|
+
super(*args)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.inherited(subclass)
|
54
|
+
unless subclass.unnamed?
|
55
|
+
commands[subclass.snake_case_name] = subclass
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.commands
|
60
|
+
@@commands ||= {}
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.unnamed?
|
64
|
+
name.nil? or name.empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.snake_case_name
|
68
|
+
convert_to_snake_case(name.split('::').last) unless unnamed?
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.common_name
|
72
|
+
snake_case_name.split('_').join(' ')
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.load_commands
|
76
|
+
command_files.each do |file|
|
77
|
+
Kernel.load file
|
78
|
+
end
|
79
|
+
true
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.command_files
|
83
|
+
@@command_files ||= find_commands.values.flatten.uniq
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.find_commands
|
87
|
+
files = Dir[File.expand_path('../command/*.rb', __FILE__)]
|
88
|
+
command_files = {}
|
89
|
+
files.each do |command_file|
|
90
|
+
rel_path = command_file[/#{Pylon::PYLON_ROOT}#{Regexp.escape(File::SEPARATOR)}(.*)\.rb/,1]
|
91
|
+
command_files[rel_path] = command_file
|
92
|
+
end
|
93
|
+
command_files
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.list_commands
|
97
|
+
load_commands
|
98
|
+
commands.each do |command|
|
99
|
+
Log.info "command loaded: #{command}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.run(command, options={})
|
104
|
+
Log.warn "command: options is not a hash" unless options.is_a? Hash
|
105
|
+
load_commands
|
106
|
+
command_class = command_class_from(command)
|
107
|
+
command_class.options = options.merge!(command_class.options) if options.respond_to? :merge! # just in case
|
108
|
+
instance = command_class.new(command)
|
109
|
+
instance.run
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.command_class_from(args)
|
113
|
+
args = [args].flatten
|
114
|
+
command_words = args.select {|arg| arg =~ /^(([[:alnum:]])[[:alnum:]\_\-]+)$/ }
|
115
|
+
command_class = nil
|
116
|
+
while ( !command_class ) && ( !command_words.empty? )
|
117
|
+
snake_case_class_name = command_words.join("_")
|
118
|
+
unless command_class = commands[snake_case_class_name]
|
119
|
+
command_words.pop
|
120
|
+
end
|
121
|
+
end
|
122
|
+
command_class ||= commands[args.first.gsub('-', '_')]
|
123
|
+
command_class || command_not_found!(args)
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.command_not_found!(args)
|
127
|
+
Log.debug "command not found: #{args.inspect}"
|
128
|
+
raise Pylon::Exceptions::Command::NotFound, args
|
129
|
+
end
|
130
|
+
|
131
|
+
end # Command
|
132
|
+
end # Pylon
|
133
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Author:: AJ Christensen (<aj@junglist.gen.nz>)
|
2
|
+
# Copyright:: Copyright (c) 2011 AJ Christensen
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or#implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
class Pylon
|
18
|
+
class Command
|
19
|
+
class NewLeader < Command
|
20
|
+
def run
|
21
|
+
# raise InvalidOptions unless options.has_key? :new_leader
|
22
|
+
[ "ok", "new_leader" ].to_json
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Author:: AJ Christensen (<aj@junglist.gen.nz>)
|
2
|
+
# Copyright:: Copyright (c) 2011 AJ Christensen
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or#implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
class Pylon
|
18
|
+
class Command
|
19
|
+
class Ping < Command
|
20
|
+
def run
|
21
|
+
# for calculating if timestamp is within time bounds
|
22
|
+
[ "ok", :timestamp => Time.now.to_i ]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Author:: AJ Christensen (<aj@junglist.gen.nz>)
|
2
|
+
# Copyright:: Copyright (c) 2011 AJ Christensen
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or#implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
class Pylon
|
18
|
+
class Command
|
19
|
+
class Status < Command
|
20
|
+
def run
|
21
|
+
raise InvalidOptions unless options.has_key? :node
|
22
|
+
options[:node]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Author:: AJ Christensen (<aj@junglist.gen.nz>)
|
2
|
+
# Copyright:: Copyright (c) 2011 AJ Christensen
|
3
|
+
# License:: Apache License, Version 2.0
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or#implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
#
|
17
|
+
require_relative "log"
|
18
|
+
require_relative "command"
|
19
|
+
|
20
|
+
class Pylon
|
21
|
+
class CommandHandler
|
22
|
+
def initialize
|
23
|
+
Log.debug "command_handler: initialized"
|
24
|
+
end
|
25
|
+
|
26
|
+
def handle_command
|
27
|
+
true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/pylon/config.rb
CHANGED
@@ -22,7 +22,7 @@ class Pylon
|
|
22
22
|
extend Mixlib::Config
|
23
23
|
|
24
24
|
config_file "~/.pylon.rb"
|
25
|
-
log_level :
|
25
|
+
log_level :debug
|
26
26
|
log_location STDOUT
|
27
27
|
daemonize false
|
28
28
|
user nil
|
@@ -30,13 +30,13 @@ class Pylon
|
|
30
30
|
umask 0022
|
31
31
|
|
32
32
|
# Options for the multicast server
|
33
|
-
multicast
|
33
|
+
multicast false
|
34
34
|
multicast_address "225.4.5.6"
|
35
35
|
multicast_port "13336"
|
36
36
|
multicast_ttl 3
|
37
37
|
multicast_listen_address nil
|
38
38
|
multicast_loopback false
|
39
|
-
|
39
|
+
multicast_interface "eth0"
|
40
40
|
|
41
41
|
# TCP settings
|
42
42
|
tcp_address "*"
|
@@ -47,7 +47,7 @@ class Pylon
|
|
47
47
|
# cluster settings
|
48
48
|
maximum_weight 1000
|
49
49
|
cluster_name "pylon"
|
50
|
-
|
50
|
+
seed_unicast_endpoints []
|
51
51
|
master nil
|
52
52
|
minimum_master_nodes 1
|
53
53
|
sleep_after_announce 5
|
@@ -57,5 +57,5 @@ class Pylon
|
|
57
57
|
fd_timeout 30
|
58
58
|
fd_retries 3
|
59
59
|
|
60
|
-
end
|
61
|
-
end
|
60
|
+
end # Config
|
61
|
+
end # Pylon
|
data/lib/pylon/elector.rb
CHANGED
@@ -24,7 +24,7 @@ require_relative "node"
|
|
24
24
|
class Pylon
|
25
25
|
class Elector
|
26
26
|
|
27
|
-
attr_accessor :cluster_name, :context, :multicast_endpoint, :node, :nodes, :multicast_announcer_thread, :multicast_listener_thread, :tcp_listener_thread
|
27
|
+
attr_accessor :cluster_name, :context, :multicast_endpoint, :node, :nodes, :multicast_announcer_thread, :multicast_listener_thread, :tcp_listener_thread
|
28
28
|
|
29
29
|
def initialize
|
30
30
|
@cluster_name = Pylon::Config[:cluster_name]
|
@@ -32,22 +32,21 @@ class Pylon
|
|
32
32
|
|
33
33
|
@node = Pylon::Node.new
|
34
34
|
@nodes = Array(@node)
|
35
|
-
|
35
|
+
node.master = Pylon::Config[:master]
|
36
36
|
|
37
|
+
Pylon::Log.info "#{node} initialized, starting elector"
|
37
38
|
Pylon::Log.info "elector[#{cluster_name}] initialized, starting pub/sub sockets on #{multicast_endpoint} and tcp listener socket on #{node.unicast_endpoint}"
|
38
39
|
|
39
40
|
Thread.abort_on_exception = true
|
40
41
|
|
41
|
-
|
42
|
-
|
42
|
+
@unicast_announcer_thread = node.unicast_announcer
|
43
|
+
if Pylon::Config[:multicast]
|
43
44
|
@multicast_announcer_thread = node.multicast_announcer
|
44
45
|
@multicast_listener_thread = multicast_listener
|
45
|
-
|
46
|
-
@multicast_listener_thread.join
|
47
|
-
@unicast_announcer_thread.join
|
48
46
|
@multicast_announcer_thread.join
|
47
|
+
@multicast_listener_thread.join
|
49
48
|
end
|
50
|
-
|
49
|
+
@unicast_announcer_thread.join
|
51
50
|
|
52
51
|
# join listeners
|
53
52
|
# @unicast_announcer_thread.join
|
@@ -84,42 +83,39 @@ class Pylon
|
|
84
83
|
end
|
85
84
|
|
86
85
|
def multicast_endpoint
|
87
|
-
@multicast_endpoint ||= "epgm://#{Pylon::Config[:
|
86
|
+
@multicast_endpoint ||= "epgm://#{Pylon::Config[:multicast_interface]};#{Pylon::Config[:multicast_address]}:#{Pylon::Config[:multicast_port]}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def failure_detectors
|
90
|
+
nodes.reject{|n| n == node}.map do |node|
|
91
|
+
Thread.new do
|
92
|
+
Log.info "failure_detector: starting failure detection against #{node}"
|
93
|
+
loop do
|
94
|
+
node.ping
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
88
98
|
end
|
89
99
|
|
90
|
-
def
|
91
|
-
nodes << node unless nodes.include? node
|
92
|
-
# fire up a new failure detector
|
100
|
+
def refresh_failure_detectors
|
93
101
|
failure_detectors.each do |thread|
|
94
102
|
thread.join
|
95
103
|
end
|
96
104
|
end
|
105
|
+
|
106
|
+
def add_node node
|
107
|
+
nodes << node unless nodes.include? node
|
108
|
+
allocate_master
|
109
|
+
# refresh_failure_detectors
|
110
|
+
end
|
97
111
|
|
98
|
-
def
|
99
|
-
|
100
|
-
|
101
|
-
begin
|
102
|
-
Timeout::timeout(Pylon::Config[:fd_timeout]) do
|
103
|
-
pong, timestamp = node.send "ping", :attempt => attempt
|
104
|
-
if (timestamp - Time.now.to_i) >= 600
|
105
|
-
Log.warn "failure_detector: received bad timestamp from #{node}, sending 'exit' message"
|
106
|
-
node.send "exit", {"message" => "bad timestamp received after #{Pylon::Config[:fd_retries]}"}
|
107
|
-
nodes.delete node
|
108
|
-
else
|
109
|
-
Log.debug "failure_detector: received good pong with timestamp: #{timestamp}"
|
110
|
-
Thread.pass
|
111
|
-
end
|
112
|
-
end
|
113
|
-
rescue Timeout::Error
|
114
|
-
Log.warn "failure_detector: #{node} timed out, removing"
|
115
|
-
nodes.delete node
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
112
|
+
def remove_node node
|
113
|
+
nodes.delete node if nodes.include? node
|
114
|
+
# refresh_failure_detectors
|
119
115
|
end
|
120
116
|
|
121
117
|
def assert_leadership
|
122
|
-
nodes.
|
118
|
+
nodes.map do |node|
|
123
119
|
Thread.new do
|
124
120
|
status = node.send "status"
|
125
121
|
Log.info "assert_leadership: status of #{node}: #{status}"
|
@@ -128,23 +124,9 @@ class Pylon
|
|
128
124
|
end
|
129
125
|
end.each do |thread|
|
130
126
|
thread.join
|
131
|
-
end if master
|
132
|
-
end
|
133
|
-
|
134
|
-
def failure_detectors
|
135
|
-
nodes.reject{|n| n == node}.map do |node|
|
136
|
-
Thread.new do
|
137
|
-
Log.info "failure_detector: starting failure detection against #{node}"
|
138
|
-
loop do
|
139
|
-
assert_leadership
|
140
|
-
ping_node node
|
141
|
-
allocate_master
|
142
|
-
end
|
143
|
-
Thread.pass
|
144
|
-
end
|
145
|
-
end
|
127
|
+
end if node.master
|
146
128
|
end
|
147
|
-
|
129
|
+
|
148
130
|
def multicast_listener
|
149
131
|
Thread.new do
|
150
132
|
Log.debug "multicast_listener: zeromq sub socket starting up on #{multicast_endpoint}"
|
@@ -155,9 +137,7 @@ class Pylon
|
|
155
137
|
sub_socket.setsockopt ZMQ::MCAST_LOOP, Pylon::Config[:multicast_loopback]
|
156
138
|
sub_socket.connect multicast_endpoint
|
157
139
|
loop do
|
158
|
-
|
159
|
-
Log.debug "multicast_listener: handling announce from #{uuid}"
|
160
|
-
handle_announce sub_socket.recv_string if sub_socket.more_parts?
|
140
|
+
handle_announce sub_socket.recv_string
|
161
141
|
end
|
162
142
|
end
|
163
143
|
end
|
@@ -167,30 +147,32 @@ class Pylon
|
|
167
147
|
Log.debug "allocate_master: node: #{node}"
|
168
148
|
end
|
169
149
|
if node.uuid == nodes.last.uuid
|
170
|
-
|
150
|
+
node.master = true
|
171
151
|
Log.info "allocate_master: master allocated; sending new_leader"
|
172
|
-
nodes.each do |
|
173
|
-
|
152
|
+
nodes.each do |remote_node|
|
153
|
+
remote_node.send "new_leader", :new_leader => node
|
174
154
|
end
|
175
155
|
else
|
176
156
|
Log.info "allocate_master: someone else is the master, getting ready for work"
|
177
|
-
|
157
|
+
node.master = false
|
178
158
|
end
|
179
159
|
end
|
180
160
|
|
181
|
-
def handle_announce recv_string
|
161
|
+
def handle_announce recv_string = ""
|
162
|
+
return false if recv_string.empty?
|
163
|
+
|
182
164
|
Log.info "handle_announce: got string #{recv_string}"
|
183
165
|
new_node = JSON.parse(recv_string)
|
184
166
|
Log.info "handle_anounce: got announce from #{new_node}"
|
185
|
-
if master
|
167
|
+
if node.master
|
186
168
|
Log.info "handle_announce: I am the master: updating #{new_node} of leadership status"
|
187
169
|
if node.weight > new_node.weight
|
188
170
|
Log.info "handle_announce: I'm bigger than you: sending new_leader to #{new_node}"
|
189
|
-
new_node.send "new_leader", node
|
171
|
+
new_node.send "new_leader", :new_leader => node
|
190
172
|
else
|
191
173
|
Log.info "handle_announce: new leader, sending change_leader to all nodes"
|
192
174
|
nodes.each do |n|
|
193
|
-
n.send "change_leader",
|
175
|
+
n.send "change_leader", :new_leader => node
|
194
176
|
end
|
195
177
|
end
|
196
178
|
elsif nodes.length < Pylon::Config[:minimum_master_nodes]
|
@@ -204,10 +186,10 @@ class Pylon
|
|
204
186
|
end
|
205
187
|
end
|
206
188
|
|
207
|
-
|
208
189
|
def connect_node node
|
209
190
|
Log.debug "connect_node: request socket connecting to #{node}"
|
210
|
-
|
191
|
+
# parse a fresh node out of the status
|
192
|
+
new_node = JSON.parse(node.send "status")
|
211
193
|
Log.debug "connect_node: node: #{new_node}"
|
212
194
|
if nodes.include? new_node
|
213
195
|
Log.info "connect_node: skipping node #{new_node}, already in local list, sleeping for 60 secs"
|