rack-app-worker 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,37 @@
1
+ require 'rack/app/worker'
2
+ module Rack::App::Worker::CLI
3
+
4
+ extend(self)
5
+
6
+ def start(options)
7
+ observer = Rack::App::Worker::Observer.new
8
+ daemonizer.daemonize if options[:daemonize]
9
+ daemonizer.subscribe_to_signals
10
+ # daemonizer.on_shutdown{ observer.stop }
11
+ # daemonizer.on_halt{ observer.stop }
12
+ observer.start
13
+ end
14
+
15
+ def stop(options)
16
+ daemonizer.send_signal('HUP')
17
+ end
18
+
19
+ def halt(options)
20
+ daemonizer.send_signal('TERM')
21
+ end
22
+
23
+ def reload(options)
24
+ daemonizer.send_signal('USR1')
25
+ end
26
+
27
+ protected
28
+
29
+ def method_missing(command)
30
+ $stderr.puts("Unknown worker command: #{command}")
31
+ end
32
+
33
+ def daemonizer
34
+ @daemonizer ||= Rack::App::Worker::Daemonizer.new('master')
35
+ end
36
+
37
+ end
@@ -0,0 +1,27 @@
1
+ class Rack::App::Worker::ClientProxy
2
+
3
+ require 'rack/app/worker/client_proxy/wrapper'
4
+
5
+ def initialize(name)
6
+ @name = name
7
+ end
8
+
9
+ def send
10
+ Rack::App::Worker::ClientProxy::Wrapper.new(rabbitmq.send_exchange(@name))
11
+ end
12
+
13
+ alias to_one send
14
+
15
+ def broadcast
16
+ Rack::App::Worker::ClientProxy::Wrapper.new(rabbitmq.broadcast_exchange(@name))
17
+ end
18
+
19
+ alias to_all broadcast
20
+
21
+ protected
22
+
23
+ def rabbitmq
24
+ @rabbitmq ||= Rack::App::Worker::RabbitMQ.new
25
+ end
26
+
27
+ end
@@ -0,0 +1,16 @@
1
+ require 'yaml'
2
+ class Rack::App::Worker::ClientProxy::Wrapper < BasicObject
3
+
4
+ def initialize(exchange)
5
+ @exchange = exchange
6
+ end
7
+
8
+ protected
9
+
10
+ def method_missing(method_name, *args)
11
+ headers = {'method_name' => method_name.to_s}
12
+ @exchange.publish(::YAML.dump(args), :headers => headers)
13
+ nil
14
+ end
15
+
16
+ end
@@ -0,0 +1,76 @@
1
+ require 'yaml'
2
+ # Bunny::Consumer
3
+ class Rack::App::Worker::Consumer
4
+
5
+ def initialize(definition)
6
+ @definition = definition
7
+ @instance = @definition[:class].new
8
+ @subscriptions = []
9
+ @shutdown_requested = false
10
+ end
11
+
12
+ def start
13
+ daemonizer.spawn do |d|
14
+ d.process_title("rack-app-worker/#{@definition[:name]}/#{d.id}")
15
+ start_working
16
+ end
17
+ end
18
+
19
+ def stop
20
+ daemonizer.send_signal('HUP', 1)
21
+ end
22
+
23
+ def stop_all
24
+ daemonizer.send_signal('HUP')
25
+ end
26
+
27
+ protected
28
+
29
+ def start_working
30
+ logger.info "consumer start working for #{@definition[:name]}"
31
+ rabbit = Rack::App::Worker::RabbitMQ.new
32
+ subscribe(rabbit.send_queue(@definition[:name]))
33
+ subscribe(rabbit.create_broadcast_queue(@definition[:name]))
34
+ wait_for_shutdown
35
+ end
36
+
37
+ def wait_for_shutdown
38
+ sleep(1) until @shutdown_requested
39
+ end
40
+
41
+ def handle_message(queue, delivery_info, properties, payload)
42
+ method_name = properties[:headers]['method_name']
43
+ args = YAML.load(payload)
44
+ @instance.public_send(method_name, *args)
45
+ queue.channel.ack(delivery_info.delivery_tag, false)
46
+ rescue Exception
47
+ queue.channel.nack(delivery_info.delivery_tag, false, true)
48
+ end
49
+
50
+ def at_shutdown
51
+ logger.info 'cancel subscriptions'
52
+ @subscriptions.each { |c| c.cancel }
53
+ @shutdown_requested = true
54
+ end
55
+
56
+ def daemonizer
57
+ @daemonizer ||= proc {
58
+ daemonizer_instance = Rack::App::Worker::Daemonizer.new(@definition[:name])
59
+ daemonizer_instance.on_shutdown { at_shutdown }
60
+ daemonizer_instance.on_halt { at_shutdown }
61
+ daemonizer_instance
62
+ }.call
63
+ end
64
+
65
+ def subscribe(queue)
66
+ logger.info "creating subscription for #{queue.name}"
67
+ @subscriptions << queue.subscribe(:manual_ack => true) do |delivery_info, properties, payload|
68
+ handle_message(queue, delivery_info, properties, payload)
69
+ end
70
+ end
71
+
72
+ def logger
73
+ @logger ||= Rack::App::Worker::Logger.new
74
+ end
75
+
76
+ end
@@ -0,0 +1,184 @@
1
+ require 'timeout'
2
+ require 'securerandom'
3
+ class Rack::App::Worker::Daemonizer
4
+ DEFAULT_KILL_SIGNAL = 'HUP'.freeze
5
+
6
+ def initialize(daemon_name)
7
+ @daemon_name = daemon_name.to_s
8
+ @on_shutdown, @on_halt, @on_reload = proc {}, proc {}, proc {}
9
+ end
10
+
11
+ def id
12
+ @id ||= SecureRandom.uuid
13
+ end
14
+
15
+ def spawn(&block)
16
+
17
+ parent_pid = current_pid
18
+ spawn_block = proc do
19
+ subscribe_to_signals
20
+ bind(parent_pid)
21
+ save_current_process_pid
22
+ redirect
23
+ block.call(self)
24
+ end
25
+
26
+ try_fork(&spawn_block)
27
+
28
+ end
29
+
30
+ def daemonize
31
+ case try_fork
32
+
33
+ when NilClass #child
34
+ subscribe_to_signals
35
+ save_current_process_pid
36
+ redirect
37
+
38
+ else #parent
39
+ Kernel.exit
40
+
41
+ end
42
+ end
43
+
44
+ def has_running_process?
45
+ pids.any? { |pid| Rack::App::Worker::Utils.process_alive?(pid) }
46
+ end
47
+
48
+ def process_title(new_title)
49
+ if Process.respond_to?(:setproctitle)
50
+ Process.setproctitle(new_title)
51
+ else
52
+
53
+ $0 = new_title
54
+ end
55
+ end
56
+
57
+ def send_signal(signal, to_amount_of_worker=pids.length)
58
+ pids.take(to_amount_of_worker).each do |pid|
59
+ kill(signal, pid)
60
+ end
61
+ end
62
+
63
+ def bind(to_pid)
64
+ Thread.new do
65
+ sleep(1) while Rack::App::Worker::Utils.process_alive?(to_pid)
66
+
67
+ at_shutdown
68
+ end
69
+ end
70
+
71
+ def on_shutdown(&block)
72
+ raise('block not given!') unless block.is_a?(Proc)
73
+ @on_shutdown = block
74
+ end
75
+
76
+ def on_halt(&block)
77
+ raise('block not given!') unless block.is_a?(Proc)
78
+ @on_halt = block
79
+ end
80
+
81
+ def on_reload(&block)
82
+ raise('block not given!') unless block.is_a?(Proc)
83
+ @on_reload = block
84
+ end
85
+
86
+ def subscribe_to_signals
87
+ ::Signal.trap('INT'){ at_shutdown }
88
+ ::Signal.trap('HUP'){ at_shutdown }
89
+ ::Signal.trap('TERM'){ at_halt }
90
+ ::Signal.trap('USR1'){ at_reload }
91
+ end
92
+
93
+ protected
94
+
95
+ # Try and read the existing pid from the pid file and signal the
96
+ # process. Returns true for a non blocking status.
97
+ def kill(signal, pid)
98
+ ::Process.kill(signal, pid)
99
+ true
100
+ rescue Errno::ESRCH
101
+ $stdout.puts "The process #{pid} did not exist: Errno::ESRCH"
102
+ true
103
+ rescue Errno::EPERM
104
+ $stderr.puts "Lack of privileges to manage the process #{pid}: Errno::EPERM"
105
+ false
106
+ rescue ::Exception => e
107
+ $stderr.puts "While signaling the PID, unexpected #{e.class}: #{e}"
108
+ false
109
+ end
110
+
111
+ def at_shutdown
112
+ @on_shutdown.call
113
+ ensure
114
+ at_stop
115
+ end
116
+
117
+ def at_halt
118
+ Timeout.timeout(10) { @on_halt.call } rescue nil
119
+ ensure
120
+ at_stop
121
+ end
122
+
123
+ def at_reload
124
+ @on_reload.call
125
+ end
126
+
127
+ def at_stop
128
+ File.write('/Users/aluzsi/Works/rack-app/worker/sandbox/out', pid_file_path)
129
+ File.delete(pid_file_path) if File.exist?(pid_file_path)
130
+ ::Kernel.exit
131
+ end
132
+
133
+ def try_fork(&block)
134
+ pid = nil
135
+ Timeout.timeout(15) { (Kernel.sleep(1) while (pid = ::Kernel.fork(&block)) == -1) }
136
+ return pid
137
+ rescue Timeout::Error
138
+ raise('Fork failed!')
139
+ end
140
+
141
+ # Attempts to write the pid of the forked process to the pid file.
142
+ def save_current_process_pid
143
+ File.write(pid_file_path, current_pid)
144
+ rescue ::Exception => e
145
+ $stderr.puts "While writing the PID to file, unexpected #{e.class}: #{e}"
146
+ Kernel.exit
147
+ end
148
+
149
+ def redirect
150
+ Timeout.timeout(5) { try_redirect }
151
+ rescue Timeout::Error
152
+ raise('Cannot redirect standard io channels!')
153
+ end
154
+
155
+ # Send stdout and stderr to log files for the child process
156
+ def try_redirect
157
+ $stdin.reopen(Rack::App::Utils.devnull_path)
158
+ $stdout.reopen(Rack::App::Worker::Environment.stdout)
159
+ $stderr.reopen(Rack::App::Worker::Environment.stderr)
160
+ $stdout.sync = $stderr.sync = true
161
+ rescue Errno::ENOENT
162
+ retry
163
+ end
164
+
165
+ def pids
166
+ sorted_pid_files = Dir.glob(File.join(pids_folder_path, '*')).sort_by { |fp| File.mtime(fp) }
167
+ sorted_pid_files.map { |file_path| File.read(file_path).to_i }
168
+ end
169
+
170
+ def pid_file_path
171
+ File.join(pids_folder_path, id)
172
+ end
173
+
174
+ def pids_folder_path
175
+ path = Rack::App::Utils.pwd('pids', 'workers', @daemon_name)
176
+ FileUtils.mkdir_p(path)
177
+ path
178
+ end
179
+
180
+ def current_pid
181
+ Process.pid rescue $$
182
+ end
183
+
184
+ end
@@ -0,0 +1,4 @@
1
+ module Rack::App::Worker::DSL
2
+ require 'rack/app/worker/dsl/for_class'
3
+ require 'rack/app/worker/dsl/for_endpoints'
4
+ end
@@ -0,0 +1,12 @@
1
+ module Rack::App::Worker::DSL::ForClass
2
+
3
+ def worker(name, &block)
4
+ Rack::App::Worker::Register.add(name,block) unless block.nil?
5
+ end
6
+ alias define_worker worker
7
+
8
+ def workers
9
+ Rack::App::Worker::Register
10
+ end
11
+
12
+ end
@@ -0,0 +1,7 @@
1
+ module Rack::App::Worker::DSL::ForEndpoints
2
+
3
+ def workers
4
+ Rack::App::Worker::Register::Clients
5
+ end
6
+
7
+ end
@@ -0,0 +1,71 @@
1
+ require 'rack/app/worker'
2
+ module Rack::App::Worker::Environment
3
+ extend(self)
4
+
5
+ DEFAULT_QOS = 50
6
+ DEFAULT_WORKER_CLUSTER = 'main'.freeze
7
+ DEFAULT_WORKER_NAMESPACE = 'rack-app-worker'.freeze
8
+ DEFAULT_HEARTBEAT_INTERVAL = 10
9
+ DEFAULT_MESSAGE_COUNT_LIMIT = 50
10
+ DEFAULT_MAX_CONSUMER_NUMBER = Rack::App::Worker::Utils.maximum_allowed_process_number
11
+
12
+ def worker_cluster
13
+ (ENV['WORKER_CLUSTER'] || DEFAULT_WORKER_CLUSTER).to_s
14
+ end
15
+
16
+ def queue_qos
17
+ (ENV['WORKER_QOS'] || DEFAULT_QOS).to_i
18
+ end
19
+
20
+ def namespace
21
+ (ENV['WORKER_NAMESPACE'] || DEFAULT_WORKER_NAMESPACE).to_s
22
+ end
23
+
24
+ def heartbeat_interval
25
+ (ENV['WORKER_HEARTBEAT_INTERVAL'] || DEFAULT_HEARTBEAT_INTERVAL).to_i
26
+ end
27
+
28
+ def message_count_limit
29
+ (ENV['WORKER_MESSAGE_COUNT_LIMIT'] || DEFAULT_MESSAGE_COUNT_LIMIT).to_i
30
+ end
31
+
32
+ def max_consumer_number
33
+ (ENV['WORKER_MAX_CONSUMER_NUMBER'] || DEFAULT_MAX_CONSUMER_NUMBER).to_i
34
+ end
35
+
36
+ def log_level
37
+ case ENV['WORKER_LOG_LEVEL'].to_s.upcase
38
+
39
+ when 'DEBUG', '0'
40
+ 0
41
+
42
+ when 'INFO', '1'
43
+ 1
44
+
45
+ when 'WARN', '2'
46
+ 2
47
+
48
+ when 'ERROR', '3'
49
+ 3
50
+
51
+ when 'FATAL', '4'
52
+ 4
53
+
54
+ when 'UNKNOWN', '5'
55
+ 5
56
+
57
+ else
58
+ 3
59
+
60
+ end
61
+ end
62
+
63
+ def stdout
64
+ (ENV['WORKER_STDOUT'] || Rack::App::Utils.devnull_path).to_s
65
+ end
66
+
67
+ def stderr
68
+ (ENV['WORKER_STDOUT'] || Rack::App::Utils.devnull_path).to_s
69
+ end
70
+
71
+ end