pylon 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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
+
@@ -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
@@ -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