pylon 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +53 -0
- data/LICENSE +201 -0
- data/NOTICE +8 -0
- data/README.org +3 -0
- data/Rakefile +44 -0
- data/Vagrantfile +19 -0
- data/bin/pylon +33 -0
- data/cookbooks/pylon/README.rdoc +8 -0
- data/cookbooks/pylon/metadata.rb +6 -0
- data/cookbooks/pylon/recipes/default.rb +42 -0
- data/lib/pylon.rb +19 -0
- data/lib/pylon/application.rb +161 -0
- data/lib/pylon/config.rb +61 -0
- data/lib/pylon/daemon.rb +171 -0
- data/lib/pylon/elector.rb +164 -0
- data/lib/pylon/log.rb +24 -0
- data/lib/pylon/node.rb +147 -0
- data/test/helper.rb +19 -0
- data/test/test_boss.rb +7 -0
- data/vendor/cache/archive-tar-minitar-0.5.2.gem +0 -0
- data/vendor/cache/erubis-2.7.0.gem +0 -0
- data/vendor/cache/ffi-1.0.9.gem +0 -0
- data/vendor/cache/ffi-rzmq-0.8.2.gem +0 -0
- data/vendor/cache/git-1.2.5.gem +0 -0
- data/vendor/cache/i18n-0.5.0.gem +0 -0
- data/vendor/cache/jeweler-1.6.4.gem +0 -0
- data/vendor/cache/json-1.5.3.gem +0 -0
- data/vendor/cache/minitest-2.6.0.gem +0 -0
- data/vendor/cache/mixlib-cli-1.2.2.gem +0 -0
- data/vendor/cache/mixlib-config-1.1.2.gem +0 -0
- data/vendor/cache/mixlib-log-1.3.0.gem +0 -0
- data/vendor/cache/net-scp-1.0.4.gem +0 -0
- data/vendor/cache/net-ssh-2.1.4.gem +0 -0
- data/vendor/cache/rake-0.9.2.gem +0 -0
- data/vendor/cache/rcov-0.9.10.gem +0 -0
- data/vendor/cache/thor-0.14.6.gem +0 -0
- data/vendor/cache/uuidtools-2.1.2.gem +0 -0
- data/vendor/cache/vagrant-0.8.2.gem +0 -0
- data/vendor/cache/virtualbox-0.9.1.gem +0 -0
- metadata +227 -0
data/lib/pylon.rb
ADDED
@@ -0,0 +1,19 @@
|
|
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
|
+
VERSION = "0.1.1"
|
19
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
#
|
2
|
+
# Author:: AJ Christensen (<aj@junglist.gen.nz>)
|
3
|
+
# Copyright:: Copyright (c) 2011 AJ Christensen
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or#implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
|
19
|
+
require "mixlib/cli"
|
20
|
+
require_relative "config"
|
21
|
+
require_relative "log"
|
22
|
+
require_relative "daemon"
|
23
|
+
require_relative "elector"
|
24
|
+
|
25
|
+
class Pylon
|
26
|
+
class Application
|
27
|
+
class << self
|
28
|
+
def fatal! message, error_code = -1
|
29
|
+
Pylon::Log.fatal message
|
30
|
+
Process.exit error_code
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
include Mixlib::CLI
|
36
|
+
|
37
|
+
def configure_logging
|
38
|
+
Pylon::Log.init(Pylon::Config[:log_location])
|
39
|
+
if ( Pylon::Config[:log_location] != STDOUT ) && STDOUT.tty? && (!Pylon::Config[:daemonize])
|
40
|
+
stdout_logger = Logger.new(STDOUT)
|
41
|
+
STDOUT.sync = true
|
42
|
+
stdout_logger.formatter = Pylon::Log.logger.formatter
|
43
|
+
Pylon::Log.loggers << stdout_logger
|
44
|
+
end
|
45
|
+
Pylon::Log.level = Pylon::Config[:log_level]
|
46
|
+
end
|
47
|
+
|
48
|
+
def run argv=ARGV
|
49
|
+
parse_options(argv)
|
50
|
+
Pylon::Config.merge!(config)
|
51
|
+
configure_logging
|
52
|
+
Pylon::Log.info "Pylon #{Pylon::VERSION} warming up"
|
53
|
+
|
54
|
+
Pylon::Daemon.change_privilege
|
55
|
+
Pylon::Daemon.daemonize "pylon" if Pylon::Config[:daemonize]
|
56
|
+
|
57
|
+
elector = Pylon::Elector.new
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize
|
61
|
+
super
|
62
|
+
trap("TERM") do
|
63
|
+
Pylon::Application.fatal!("SIGTERM received, stopping", 1)
|
64
|
+
end
|
65
|
+
|
66
|
+
trap("INT") do
|
67
|
+
Pylon::Application.fatal!("SIGINT received, stopping", 2)
|
68
|
+
end
|
69
|
+
|
70
|
+
unless RUBY_PLATFORM =~ /mswin|mingw32|windows/
|
71
|
+
trap("QUIT") do
|
72
|
+
Pylon::Log.info("SIGQUIT received, call stack:\n " + caller.join("\n "))
|
73
|
+
end
|
74
|
+
|
75
|
+
trap("HUP") do
|
76
|
+
Pylon::Log.info("SIGHUP received, reconfiguring")
|
77
|
+
# reconfigure
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
option :config_file,
|
83
|
+
:short => "-c CONFIG",
|
84
|
+
:long => "--config CONFIG",
|
85
|
+
:default => 'config.rb',
|
86
|
+
:description => "The configuration file to use"
|
87
|
+
|
88
|
+
option :log_level,
|
89
|
+
:short => "-l LEVEL",
|
90
|
+
:long => "--log_level LEVEL",
|
91
|
+
:description => "Set the log level (debug, info, warn, error, fatal)",
|
92
|
+
:required => true,
|
93
|
+
:proc => Proc.new { |l| l.to_sym }
|
94
|
+
|
95
|
+
option :help,
|
96
|
+
:short => "-h",
|
97
|
+
:long => "--help",
|
98
|
+
:description => "Show this message",
|
99
|
+
:on => :tail,
|
100
|
+
:boolean => true,
|
101
|
+
:show_options => true,
|
102
|
+
:exit => 0
|
103
|
+
|
104
|
+
option :daemonize,
|
105
|
+
:short => "-d",
|
106
|
+
:long => "--daemonize",
|
107
|
+
:description => "send pylon to the background",
|
108
|
+
:proc => lambda { |p| true }
|
109
|
+
|
110
|
+
option :user,
|
111
|
+
:short => "-u USER",
|
112
|
+
:long => "--user USER",
|
113
|
+
:description => "User to set privilege to",
|
114
|
+
:proc => nil
|
115
|
+
|
116
|
+
option :group,
|
117
|
+
:short => "-g GROUP",
|
118
|
+
:long => "--group GROUP",
|
119
|
+
:description => "Group to set privilege to",
|
120
|
+
:proc => nil
|
121
|
+
|
122
|
+
option :multicast_address,
|
123
|
+
:short => "-a ADDRESS",
|
124
|
+
:long => "--multicast-address ADDRESS",
|
125
|
+
:description => "Address to use for UDP multicast"
|
126
|
+
|
127
|
+
option :multicast_port,
|
128
|
+
:short => "-p PORT",
|
129
|
+
:long => "--multicast-port PORT",
|
130
|
+
:description => "Port to use for UDP multicast"
|
131
|
+
|
132
|
+
option :multicast_loopback,
|
133
|
+
:short => "-L",
|
134
|
+
:long => "--multicast-loopback",
|
135
|
+
:description => "Enable multicast over loopback interfaces",
|
136
|
+
:proc => lambda { |loop| true }
|
137
|
+
|
138
|
+
option :interface,
|
139
|
+
:short => "-i INTERFACE",
|
140
|
+
:long => "--interface INTERFACE",
|
141
|
+
:description => "Interface to use to send multicast over"
|
142
|
+
|
143
|
+
option :tcp_address,
|
144
|
+
:short => "-t TCPADDRESS",
|
145
|
+
:long => "--tcp-address TCPADDRESS",
|
146
|
+
:description => "Interface to use to bind request socket to"
|
147
|
+
|
148
|
+
option :tcp_port,
|
149
|
+
:short => "-P TCPPORT",
|
150
|
+
:long => "--tcp-port TCPPORT",
|
151
|
+
:description => "Port to bind request socket to"
|
152
|
+
|
153
|
+
option :minimum_master_nodes,
|
154
|
+
:short => "-m NODES",
|
155
|
+
:long => "--minimum-master-nodes NODES",
|
156
|
+
:description => "How many nodes to wait for before starting master election",
|
157
|
+
:proc => lambda { |nodes| nodes.to_i }
|
158
|
+
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
data/lib/pylon/config.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
#
|
2
|
+
# Author:: AJ Christensen (<aj@junglist.gen.nz>)
|
3
|
+
# Copyright:: Copyright (c) 2011 AJ Christensen
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or#implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
require "mixlib/config"
|
19
|
+
|
20
|
+
class Pylon
|
21
|
+
class Config
|
22
|
+
extend Mixlib::Config
|
23
|
+
|
24
|
+
config_file "~/.pylon.rb"
|
25
|
+
log_level :info
|
26
|
+
log_location STDOUT
|
27
|
+
daemonize false
|
28
|
+
user nil
|
29
|
+
group nil
|
30
|
+
umask 0022
|
31
|
+
|
32
|
+
# Options for the multicast server
|
33
|
+
multicast true
|
34
|
+
multicast_address "225.4.5.6"
|
35
|
+
multicast_port "13336"
|
36
|
+
multicast_ttl 3
|
37
|
+
multicast_listen_address nil
|
38
|
+
multicast_loopback false
|
39
|
+
interface_address "127.0.0.1"
|
40
|
+
|
41
|
+
# TCP settings
|
42
|
+
tcp_address "*"
|
43
|
+
tcp_port "13335"
|
44
|
+
tcp_retries 10
|
45
|
+
tcp_timeout 5
|
46
|
+
|
47
|
+
# cluster settings
|
48
|
+
maximum_weight 1000
|
49
|
+
cluster_name "pylon"
|
50
|
+
Seed_tcp_endpoints []
|
51
|
+
master nil
|
52
|
+
minimum_master_nodes 1
|
53
|
+
sleep_after_announce 5
|
54
|
+
|
55
|
+
# TODO: not implemented
|
56
|
+
ping_interval 1
|
57
|
+
ping_timeout 30
|
58
|
+
ping_retries 3
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
data/lib/pylon/daemon.rb
ADDED
@@ -0,0 +1,171 @@
|
|
1
|
+
#
|
2
|
+
# Author:: AJ Christensen (<aj@junglist.gen.nz>)
|
3
|
+
# Copyright:: Copyright (c) 2011 AJ Christensen
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
require "etc"
|
19
|
+
require_relative "application"
|
20
|
+
require_relative "config"
|
21
|
+
|
22
|
+
class Pylon
|
23
|
+
class Daemon
|
24
|
+
class << self
|
25
|
+
attr_accessor :name
|
26
|
+
|
27
|
+
# Daemonize the current process, managing pidfiles and process uid/gid
|
28
|
+
#
|
29
|
+
# === Parameters
|
30
|
+
# name<String>:: The name to be used for the pid file
|
31
|
+
#
|
32
|
+
def daemonize(name)
|
33
|
+
@name = name
|
34
|
+
pid = pid_from_file
|
35
|
+
unless running?
|
36
|
+
remove_pid_file()
|
37
|
+
Pylon::Log.info("Daemonizing..")
|
38
|
+
begin
|
39
|
+
exit if fork
|
40
|
+
Process.setsid
|
41
|
+
exit if fork
|
42
|
+
Pylon::Log.info("Forked, in #{Process.pid}. Priveleges: #{Process.euid} #{Process.egid}")
|
43
|
+
File.umask Pylon::Config[:umask]
|
44
|
+
$stdin.reopen("/dev/null")
|
45
|
+
$stdout.reopen("/dev/null", "a")
|
46
|
+
$stderr.reopen($stdout)
|
47
|
+
save_pid_file
|
48
|
+
at_exit { remove_pid_file }
|
49
|
+
rescue NotImplementedError => e
|
50
|
+
Pylon::Application.fatal!("There is no fork: #{e.message}")
|
51
|
+
end
|
52
|
+
else
|
53
|
+
Pylon::Application.fatal!("Pylon is already running pid #{pid}")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Check if Pylon is running based on the pid_file
|
58
|
+
# ==== Returns
|
59
|
+
# Boolean::
|
60
|
+
# True if Pylon is running
|
61
|
+
# False if Pylon is not running
|
62
|
+
#
|
63
|
+
def running?
|
64
|
+
if pid_from_file.nil?
|
65
|
+
false
|
66
|
+
else
|
67
|
+
Process.kill(0, pid_from_file)
|
68
|
+
true
|
69
|
+
end
|
70
|
+
rescue Errno::ESRCH, Errno::ENOENT
|
71
|
+
false
|
72
|
+
rescue Errno::EACCES => e
|
73
|
+
Pylon::Application.fatal!("You don't have access to the PID file at #{pid_file}: #{e.message}")
|
74
|
+
end
|
75
|
+
|
76
|
+
# Gets the pid file for @name
|
77
|
+
# ==== Returns
|
78
|
+
# String::
|
79
|
+
# Location of the pid file for @name
|
80
|
+
def pid_file
|
81
|
+
Pylon::Config[:pid_file] or "/tmp/#{@name}.pid"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Suck the pid out of pid_file
|
85
|
+
# ==== Returns
|
86
|
+
# Integer::
|
87
|
+
# The PID from pid_file
|
88
|
+
# nil::
|
89
|
+
# Returned if the pid_file does not exist.
|
90
|
+
#
|
91
|
+
def pid_from_file
|
92
|
+
File.read(pid_file).chomp.to_i
|
93
|
+
rescue Errno::ENOENT, Errno::EACCES
|
94
|
+
nil
|
95
|
+
end
|
96
|
+
|
97
|
+
# Store the PID on the filesystem
|
98
|
+
# This uses the Pylon::Config[:pid_file] option, or "/tmp/name.pid" otherwise
|
99
|
+
#
|
100
|
+
def save_pid_file
|
101
|
+
file = pid_file
|
102
|
+
begin
|
103
|
+
FileUtils.mkdir_p(File.dirname(file))
|
104
|
+
rescue Errno::EACCES => e
|
105
|
+
Pylon::Application.fatal!("Failed store pid in #{File.dirname(file)}, permission denied: #{e.message}")
|
106
|
+
end
|
107
|
+
|
108
|
+
begin
|
109
|
+
File.open(file, "w") { |f| f.write(Process.pid.to_s) }
|
110
|
+
rescue Errno::EACCES => e
|
111
|
+
Pylon::Application.fatal!("Couldn't write to pidfile #{file}, permission denied: #{e.message}")
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Delete the PID from the filesystem
|
116
|
+
def remove_pid_file
|
117
|
+
FileUtils.rm(pid_file) if File.exists?(pid_file)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Change process user/group to those specified in Pylon::Config
|
121
|
+
#
|
122
|
+
def change_privilege
|
123
|
+
Dir.chdir("/")
|
124
|
+
|
125
|
+
if Pylon::Config[:user] and Pylon::Config[:group]
|
126
|
+
Pylon::Log.info("About to change privilege to #{Pylon::Config[:user]}:#{Pylon::Config[:group]}")
|
127
|
+
_change_privilege(Pylon::Config[:user], Pylon::Config[:group])
|
128
|
+
elsif Pylon::Config[:user]
|
129
|
+
Pylon::Log.info("About to change privilege to #{Pylon::Config[:user]}")
|
130
|
+
_change_privilege(Pylon::Config[:user])
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Change privileges of the process to be the specified user and group
|
135
|
+
#
|
136
|
+
# ==== Parameters
|
137
|
+
# user<String>:: The user to change the process to.
|
138
|
+
# group<String>:: The group to change the process to.
|
139
|
+
#
|
140
|
+
# ==== Alternatives
|
141
|
+
# If group is left out, the user will be used (changing to user:user)
|
142
|
+
#
|
143
|
+
def _change_privilege(user, group=user)
|
144
|
+
uid, gid = Process.euid, Process.egid
|
145
|
+
|
146
|
+
begin
|
147
|
+
target_uid = Etc.getpwnam(user).uid
|
148
|
+
rescue ArgumentError => e
|
149
|
+
Pylon::Application.fatal!("Failed to get UID for user #{user}, does it exist? #{e.message}")
|
150
|
+
return false
|
151
|
+
end
|
152
|
+
|
153
|
+
begin
|
154
|
+
target_gid = Etc.getgrnam(group).gid
|
155
|
+
rescue ArgumentError => e
|
156
|
+
Pylon::Application.fatal!("Failed to get GID for group #{group}, does it exist? #{e.message}")
|
157
|
+
return false
|
158
|
+
end
|
159
|
+
|
160
|
+
if (uid != target_uid) or (gid != target_gid)
|
161
|
+
Process.initgroups(user, target_gid)
|
162
|
+
Process::GID.change_privilege(target_gid)
|
163
|
+
Process::UID.change_privilege(target_uid)
|
164
|
+
end
|
165
|
+
true
|
166
|
+
rescue Errno::EPERM => e
|
167
|
+
Pylon::Application.fatal!("Permission denied when trying to change #{uid}:#{gid} to #{target_uid}:#{target_gid}. #{e.message}")
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
#
|
2
|
+
# Author:: AJ Christensen (<aj@junglist.gen.nz>)
|
3
|
+
# Copyright:: Copyright (c) 2011 AJ Christensen
|
4
|
+
# License:: Apache License, Version 2.0
|
5
|
+
#
|
6
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License.
|
8
|
+
# You may obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
14
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or#implied.
|
15
|
+
# See the License for the specific language governing permissions and
|
16
|
+
# limitations under the License.
|
17
|
+
#
|
18
|
+
require "uuidtools"
|
19
|
+
require "ffi-rzmq"
|
20
|
+
require "thread"
|
21
|
+
require "json"
|
22
|
+
require_relative "node"
|
23
|
+
|
24
|
+
class Pylon
|
25
|
+
class Elector
|
26
|
+
|
27
|
+
attr_accessor :cluster_name, :context, :multicast_endpoint, :node, :nodes, :multicast_announcer_thread, :multicast_listener_thread, :tcp_listener_thread, :master
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@cluster_name = Pylon::Config[:cluster_name]
|
31
|
+
@context = ZMQ::Context.new(1)
|
32
|
+
|
33
|
+
@node = Pylon::Node.new
|
34
|
+
@nodes = Array(@node)
|
35
|
+
@master = Pylon::Config[:master]
|
36
|
+
|
37
|
+
Pylon::Log.info "elector[#{cluster_name}] initialized, starting pub/sub sockets on #{multicast_endpoint} and tcp listener socket on #{node.unicast_endpoint}"
|
38
|
+
|
39
|
+
Thread.abort_on_exception = true
|
40
|
+
|
41
|
+
scheduler = Thread.new do
|
42
|
+
@unicast_announcer_thread = node.unicast_announcer
|
43
|
+
@multicast_announcer_thread = node.multicast_announcer
|
44
|
+
@multicast_listener_thread = multicast_listener
|
45
|
+
|
46
|
+
@multicast_listener_thread.join
|
47
|
+
@unicast_announcer_thread.join
|
48
|
+
@multicast_announcer_thread.join
|
49
|
+
end
|
50
|
+
scheduler.join
|
51
|
+
|
52
|
+
# join listeners
|
53
|
+
# @unicast_announcer_thread.join
|
54
|
+
# @tcp_listener_thread.join
|
55
|
+
# sleep 5
|
56
|
+
# @multicast_announcer_thread.join
|
57
|
+
|
58
|
+
at_exit do
|
59
|
+
Log.debug "cleaning up zeromq context"
|
60
|
+
@context.terminate
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def stop_announcer
|
65
|
+
@multicast_announcer_thread.stop
|
66
|
+
end
|
67
|
+
|
68
|
+
def run_announcer
|
69
|
+
@multicast_announcer.thread.run
|
70
|
+
end
|
71
|
+
|
72
|
+
def pause_listeners
|
73
|
+
@tcp_listener_thread.stop
|
74
|
+
@multicast_listener_thread.stop
|
75
|
+
end
|
76
|
+
|
77
|
+
def run_listeners
|
78
|
+
@tcp_listener_thread.run
|
79
|
+
@multicst_listener_thread.run
|
80
|
+
end
|
81
|
+
|
82
|
+
def unicast_endpoint
|
83
|
+
@unicast_endpoint ||= "tcp://#{Pylon::Config[:tcp_address]}:#{Pylon::Config[:tcp_port]}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def multicast_endpoint
|
87
|
+
@multicast_endpoint ||= "epgm://#{Pylon::Config[:interface]};#{Pylon::Config[:multicast_address]}:#{Pylon::Config[:multicast_port]}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_node node
|
91
|
+
@nodes << node unless @nodes.include? node
|
92
|
+
end
|
93
|
+
|
94
|
+
def multicast_listener
|
95
|
+
Thread.new do
|
96
|
+
Log.debug "multicast_listener: zeromq sub socket starting up on #{@multicast_endpoint}"
|
97
|
+
sub_socket = context.socket ZMQ::SUB
|
98
|
+
sub_socket.setsockopt ZMQ::IDENTITY, "node"
|
99
|
+
sub_socket.setsockopt ZMQ::SUBSCRIBE, ""
|
100
|
+
sub_socket.setsockopt ZMQ::RATE, 1000
|
101
|
+
sub_socket.setsockopt ZMQ::MCAST_LOOP, Pylon::Config[:multicast_loopback]
|
102
|
+
sub_socket.connect multicast_endpoint
|
103
|
+
loop do
|
104
|
+
uuid = sub_socket.recv_string
|
105
|
+
Log.debug "multicast_listener: handling announce from #{uuid}"
|
106
|
+
handle_announce sub_socket.recv_string if sub_socket.more_parts?
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def allocate_master
|
112
|
+
if node.uuid == nodes.sort.first.uuid
|
113
|
+
@master = true
|
114
|
+
Log.info "allocate_master: master allocated"
|
115
|
+
nodes.each do |node|
|
116
|
+
connect_node node.uuid.to_s, node.unicast_endpoint
|
117
|
+
end
|
118
|
+
else
|
119
|
+
Log.info "allocate_master: someone else is the master, getting ready for work"
|
120
|
+
@master = false
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def handle_announce recv_string
|
125
|
+
Log.info "handle_announce: got string #{recv_string}"
|
126
|
+
node = JSON.parse(recv_string)
|
127
|
+
Log.info "handle_anounce: got announce from #{node}"
|
128
|
+
if master
|
129
|
+
Log.info "handle_announce: I am the master: updating #{node} of leadership status"
|
130
|
+
connect_node(node.uuid.to_s, node.unicast_endpoint)
|
131
|
+
elsif nodes.length < Pylon::Config[:minimum_master_nodes]
|
132
|
+
if nodes.include? node
|
133
|
+
Log.info "handle_announce: skipping node #{node}, already known"
|
134
|
+
Log.debug "handle_announce: nodes: #{nodes}"
|
135
|
+
else
|
136
|
+
Log.info "handle_announce: subscribing for #{node} on endpoint: #{node.unicast_endpoint}"
|
137
|
+
connect_node(node.uuid.to_s, node.unicast_endpoint)
|
138
|
+
end
|
139
|
+
else
|
140
|
+
allocate_master
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def connect_node uuid, endpoint
|
145
|
+
Log.debug "connect_node: subscribe socket connecting to #{endpoint}"
|
146
|
+
sub_socket = context.socket ZMQ::SUB
|
147
|
+
sub_socket.setsockopt ZMQ::IDENTITY, uuid
|
148
|
+
sub_socket.setsockopt ZMQ::SUBSCRIBE, uuid
|
149
|
+
sub_socket.connect endpoint
|
150
|
+
uuid = sub_socket.recv_string
|
151
|
+
Log.debug "connect_node: got uuid on sub socket, parsing node"
|
152
|
+
new_node = JSON.parse(sub_socket.recv_string) if sub_socket.more_parts?
|
153
|
+
Log.debug "connect_node: node: #{new_node}"
|
154
|
+
if nodes.include? new_node
|
155
|
+
Log.info "connect_node: skipping node #{new_node}, already in local list, sleeping for 60 secs"
|
156
|
+
Log.debug "connect_node: nodes: #{nodes}"
|
157
|
+
sleep 60
|
158
|
+
else
|
159
|
+
Log.info "connect_node: connected to node #{new_node}, adding to local list"
|
160
|
+
add_node new_node
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|