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.
@@ -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"