pylon 0.2.7 → 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,5 +16,5 @@
16
16
 
17
17
  class Pylon
18
18
  PYLON_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__)))
19
- VERSION = "0.2.7"
19
+ VERSION = "0.2.8"
20
20
  end
@@ -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
@@ -22,7 +22,7 @@ class Pylon
22
22
  extend Mixlib::Config
23
23
 
24
24
  config_file "~/.pylon.rb"
25
- log_level :info
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 true
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
- interface_address "127.0.0.1"
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
- Seed_tcp_endpoints []
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
@@ -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, :master
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
- @master = Pylon::Config[:master]
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
- scheduler = Thread.new do
42
- @unicast_announcer_thread = node.unicast_announcer
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
- scheduler.join
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[:interface]};#{Pylon::Config[:multicast_address]}:#{Pylon::Config[:multicast_port]}"
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 add_node node
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 ping_node node
99
- begin
100
- Pylon::Config[:fd_retries].times do |attempt|
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.each do |node|
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
- uuid = sub_socket.recv_string
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
- @master = true
150
+ node.master = true
171
151
  Log.info "allocate_master: master allocated; sending new_leader"
172
- nodes.each do |node|
173
- node.send "new_leader", node
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
- @master = false
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", new_node
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
- new_node = node.send "status"
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"