rack-app-worker 0.1.0
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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +33 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/README.md +60 -0
- data/Rakefile +6 -0
- data/VERSION +1 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/rack/app/worker.rb +46 -0
- data/lib/rack/app/worker/cli.rb +37 -0
- data/lib/rack/app/worker/client_proxy.rb +27 -0
- data/lib/rack/app/worker/client_proxy/wrapper.rb +16 -0
- data/lib/rack/app/worker/consumer.rb +76 -0
- data/lib/rack/app/worker/daemonizer.rb +184 -0
- data/lib/rack/app/worker/dsl.rb +4 -0
- data/lib/rack/app/worker/dsl/for_class.rb +12 -0
- data/lib/rack/app/worker/dsl/for_endpoints.rb +7 -0
- data/lib/rack/app/worker/environment.rb +71 -0
- data/lib/rack/app/worker/logger.rb +18 -0
- data/lib/rack/app/worker/observer.rb +100 -0
- data/lib/rack/app/worker/rabbit_mq.rb +117 -0
- data/lib/rack/app/worker/register.rb +24 -0
- data/lib/rack/app/worker/register/builder.rb +25 -0
- data/lib/rack/app/worker/register/clients.rb +10 -0
- data/lib/rack/app/worker/utils.rb +18 -0
- data/lib/rack/app/worker/version.rb +2 -0
- data/rack-app-worker.gemspec +28 -0
- metadata +144 -0
@@ -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,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
|