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