pylon 0.2.7 → 0.2.8
Sign up to get free protection for your applications and to get access to all the features.
- 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"
|