action_event 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/action_event.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "action_event"
3
- s.version = "0.0.1"
3
+ s.version = "0.0.2"
4
4
  s.date = "2009-10-06"
5
5
  s.summary = "A framework for asynchronous message processing in a Rails application."
6
6
  s.email = "wkonkel@gmail.com"
@@ -10,4 +10,5 @@ Gem::Specification.new do |s|
10
10
  s.authors = ["Warren Konkel"]
11
11
  s.files = Dir.glob('**/*') - Dir.glob('test/*.rb')
12
12
  s.test_files = Dir.glob('test/*.rb')
13
+ s.require_paths = ["lib"]
13
14
  end
@@ -1,4 +1,239 @@
1
1
  #!/usr/bin/env ruby
2
2
  require File.dirname(__FILE__) + '/../config/boot'
3
- $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), '/../vendor/plugins/action_event/lib'))
4
- require 'action_event/commands/poller'
3
+ require 'fileutils'
4
+ require 'optparse'
5
+
6
+ module ActionEvent
7
+ module Commands
8
+ class Poller
9
+ def initialize
10
+ @options = {
11
+ :id => 1,
12
+ :queues => %W(high medium low),
13
+ :command => 'start',
14
+ :environment => RAILS_ENV,
15
+ :daemon => false,
16
+ :max_load_average => 8,
17
+ :min_instances => 5,
18
+ :max_instances => 200,
19
+ :max_adjustment => 5,
20
+ :min_queue_size => 1000
21
+ }
22
+
23
+ OptionParser.new do |opts|
24
+ opts.banner = "Usage: #{$0} [options] <command>"
25
+ opts.on("-d", "--daemon", "Run as a daemon") { |v| @options[:daemon] = v }
26
+ opts.on("-i", "--id=N", Integer, "Specify ID used in PID file when running as daemon") { |v| @options[:id] = v }
27
+ opts.on("-q", "--queues='high medium low'", "Specify queue names in order") { |v| @options[:queues] = v.split(' ') }
28
+ opts.on("-e", "--environment=development", "Specify which rails environment to run in") { |v| @options[:environment] = v }
29
+ opts.separator ""
30
+ opts.separator "Cluster options:"
31
+ opts.on("-l", "--load-average=8", "Specify what load average to optimize to") { |v| @options[:max_load_average] = v }
32
+ opts.on("-m", "--min-instances=5", "Specify mimimum number of instances") { |v| @options[:min_instances] = v }
33
+ opts.on("-x", "--max-instances=200", "Specify maximum number of instances") { |v| @options[:max_instances] = v }
34
+ opts.on("-a", "--max-adjustment=5", "Specify how many the maximum amount of instances that will be adjusted") { |v| @options[:max_adjustment] = v }
35
+ opts.on("-s", "--min-queue-size=1000", "Specify how many must be in the queue to adjust instances") { |v| @options[:min_queue_size] = v }
36
+ opts.separator ""
37
+ opts.separator "Commands:"
38
+ opts.separator " start - starts up the poller"
39
+ opts.separator " stop - stops a poller currently running as a daemon"
40
+ opts.separator " status - prints the status of the queues"
41
+ opts.separator ""
42
+ opts.separator "Examples:"
43
+ opts.separator " #{$0} start (starts a poller running in the console)"
44
+ opts.separator " #{$0} -d -e production start (starts a poller running as a daemon with ID #1)"
45
+ opts.separator " #{$0} --daemon --id=5 start (starts poller with ID #5)"
46
+ opts.separator " #{$0} --daemon --id=5 stop (stops poller with ID #5)"
47
+ end.parse!
48
+
49
+ @options[:command] = ARGV.pop unless ARGV.empty?
50
+
51
+ case
52
+ when @options[:command] == 'start' && !@options[:daemon] then trap_ctrl_c and load_rails_environment and start_processing_loop
53
+ when @options[:command] == 'start' && @options[:daemon] then trap_term and start_daemon and load_rails_environment and start_processing_loop and remove_pid
54
+ when @options[:command] == 'stop' then stop_daemon
55
+ when @options[:command] == 'cluster' then load_rails_environment and refresh_cluster
56
+ when @options[:command] == 'status' then load_rails_environment and print_status
57
+ end
58
+ end
59
+
60
+ def print_status
61
+ ActionEvent::Message.queue_status(@options[:queues]).to_a.sort { |a,b| a.first <=> b.first }.each do |table,messages_left|
62
+ log "#{table}:\t\t#{messages_left}"
63
+ end
64
+ end
65
+
66
+ def load_rails_environment
67
+ ENV['ACTION_EVENT_USE_POLLER_DB'] = 'true'
68
+ ENV["RAILS_ENV"] = @options[:environment]
69
+ RAILS_ENV.replace(@options[:environment])
70
+ log "Loading #{RAILS_ENV} environment..."
71
+ require "#{RAILS_ROOT}/config/environment"
72
+
73
+ if defined?(NewRelic)
74
+ NewRelic::Control.instance.instance_eval do
75
+ @settings['app_name'] = @settings['app_name'] + ' (Poller)'
76
+ end
77
+ end
78
+
79
+ true
80
+ end
81
+
82
+ # returns the name of the PID file to use for daemons
83
+ def pid_filename
84
+ @pid_filename ||= File.join(RAILS_ROOT, "/log/poller.#{@options[:id]}.pid")
85
+ end
86
+
87
+ # forks from the current process and closes out everything
88
+ def start_daemon
89
+ log "Starting daemon ##{@options[:id]}..."
90
+
91
+ # some process magic
92
+ exit if fork # Parent exits, child continues.
93
+ Process.setsid # Become session leader.
94
+ exit if fork # Zap session leader.
95
+ Dir.chdir "/" # Release old working directory.
96
+ File.umask 0000 # Ensure sensible umask. Adjust as needed.
97
+
98
+ # Free file descriptors and point them somewhere sensible.
99
+ STDIN.reopen "/dev/null"
100
+ STDOUT.reopen File.join(RAILS_ROOT, "log/poller.log"), "a"
101
+ STDERR.reopen STDOUT
102
+
103
+ # don't start up until the previous poller is dead
104
+ while (previous_pid = File.read(pid_filename).to_i rescue nil) do
105
+ break unless File.exists?("/proc/#{previous_pid}")
106
+ log "Waiting for previous poller to finish..."
107
+ Process.kill('TERM', previous_pid)
108
+ sleep(5)
109
+ end
110
+
111
+ # record pid
112
+ File.open(pid_filename, 'w') { |f| f << Process.pid }
113
+ end
114
+
115
+ def trap_ctrl_c
116
+ trap("SIGINT") do
117
+ @stop_processing = true
118
+ log "Sending stop signal..."
119
+ end
120
+ end
121
+
122
+ def trap_term
123
+ trap("SIGTERM") do
124
+ @stop_processing = true
125
+ log "Received stop signal..."
126
+ end
127
+ end
128
+
129
+ def refresh_cluster
130
+ # gather some current stats
131
+ current_load = `uptime`.split(' ')[-3..-3][0].to_f
132
+ current_queue = ActionEvent::Message.queue_status(*@options[:queues]).to_a.map(&:last).sum
133
+
134
+ # remove stale pid files
135
+ current_pids = Dir[File.join(RAILS_ROOT, "log/poller.*.pid")]
136
+ active_pids, stale_pids = current_pids.partition { |f| (File.read("/proc/#{File.read(f).to_i}/cmdline").include?('poller') rescue false) }
137
+ stale_pids.each { |f| File.delete(f) }
138
+
139
+ # compute adjustment based on current load average and queue size
140
+ if active_pids.length > 0
141
+ current_instances = active_pids.length
142
+ needed_instances = ((current_instances*@options[:max_load_average])/current_load).floor
143
+
144
+ if needed_instances > current_instances
145
+ needed_instances = [needed_instances, current_instances + @options[:max_adjustment]].min
146
+ elsif needed_instances < current_instances && current_queue > @options[:min_queue_size]
147
+ needed_instances = [needed_instances, current_instances - @options[:max_adjustment]].max
148
+ end
149
+ else
150
+ current_instances = 0
151
+ needed_instances = @options[:min_instances]
152
+ end
153
+
154
+ needed_instances = @options[:max_instances] if needed_instances > @options[:max_instances]
155
+ needed_instances = @options[:min_instances] if needed_instances < @options[:min_instances]
156
+
157
+
158
+ # remove pids if there's too many or spawn new ones if there's not enough
159
+ if needed_instances < current_instances
160
+ active_pids.last(current_instances - needed_instances).each { |pid_file| puts "delete #{pid_file}" } #File.delete(pid_file) }
161
+ elsif needed_instances > current_instances
162
+ (needed_instances - current_instances).times do
163
+ next_id = (1..needed_instances).to_a.find { |i| !File.exists?(File.join(RAILS_ROOT, "log/poller.#{i}.pid")) }
164
+ puts "start at id #{next_id}"
165
+ # if fork
166
+ #
167
+ # end
168
+ end
169
+ end
170
+ end
171
+
172
+ def should_stop_processing?
173
+ @stop_processing || (@options[:daemon] && (File.read(pid_filename).to_i rescue 0) != Process.pid)
174
+ end
175
+
176
+ # finds the already running daemon and stops it...
177
+ def stop_daemon
178
+ if previous_pid = File.read(pid_filename).to_i rescue nil
179
+ log "Sending stop signal to daemon ##{@options[:id]}..."
180
+ Process.kill('TERM', previous_pid)
181
+ end
182
+ end
183
+
184
+ def remove_pid
185
+ if Process.pid == (File.read(pid_filename).to_i rescue nil)
186
+ log "Cleaning up PID file..."
187
+ FileUtils.rm(pid_filename)
188
+ end
189
+ end
190
+
191
+ # loops until should_stop_processing? set to true... in local mode, this is never set so it will loop forever
192
+ def start_processing_loop
193
+ log "Processing queues: #{@options[:queues].join(',')}"
194
+ next_iteration or sleep(0.5) until should_stop_processing?
195
+ log "Got signal to stop... exiting."
196
+ end
197
+
198
+ # if we can get a message, process it
199
+ def next_iteration
200
+ reload_application if RAILS_ENV == 'development'
201
+ if message = ActionEvent::Message.try_to_get_next_message(@options[:queues])
202
+ begin
203
+ log_text = "#{message[:queue_name]}:#{message[:event]} (#{message[:params].inspect})"
204
+ log "Processing #{log_text}"
205
+ "#{message[:event]}_event".camelize.constantize.process(message[:params])
206
+ log "Finished processing #{log_text}"
207
+ rescue Exception => e
208
+ log "Error processing #{log_text}: #{e} #{e.backtrace.join("\n")}"
209
+ end
210
+ return true
211
+ else
212
+ # return false if we didn't get a message... makes start_processing_loop sleep(1)
213
+ return false
214
+ end
215
+ rescue Exception => e
216
+ log "Error getting next message (#{e})"
217
+ ActionEvent::Message.connection.verify! rescue log("Error verifying DB connection... sleeping 5 seconds. (#{$!})") and sleep(5)
218
+ return true
219
+ end
220
+
221
+ def reload_application
222
+ ActionController::Routing::Routes.reload
223
+ ActionController::Base.view_paths.reload! rescue nil
224
+ ActionView::Helpers::AssetTagHelper::AssetTag::Cache.clear rescue nil
225
+
226
+ ActiveRecord::Base.reset_subclasses
227
+ ActiveSupport::Dependencies.clear
228
+ ActiveRecord::Base.clear_reloadable_connections!
229
+ end
230
+
231
+ def log(message)
232
+ $stdout.puts "[#{"#{@options[:id]}:#{Process.pid} " if @options[:daemon]}#{Time.now}] #{message}"
233
+ $stdout.flush
234
+ end
235
+ end
236
+ end
237
+ end
238
+
239
+ ActionEvent::Commands::Poller.new
data/lib/action_event.rb CHANGED
@@ -2,4 +2,3 @@ require 'action_event/base'
2
2
  require 'action_event/object_extensions'
3
3
  require 'action_event/message'
4
4
  ActiveSupport::Dependencies.load_paths << File.join(RAILS_ROOT, 'app/events')
5
-
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_event
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Warren Konkel
@@ -29,9 +29,7 @@ files:
29
29
  - generators/event/templates/event.rb
30
30
  - generators/event/templates/migration.rb
31
31
  - generators/event/templates/poller
32
- - init.rb
33
32
  - lib/action_event/base.rb
34
- - lib/action_event/commands/poller.rb
35
33
  - lib/action_event/message.rb
36
34
  - lib/action_event/object_extensions.rb
37
35
  - lib/action_event.rb
data/init.rb DELETED
@@ -1 +0,0 @@
1
- require 'action_event'
@@ -1,237 +0,0 @@
1
- require 'fileutils'
2
- require 'optparse'
3
-
4
- module ActionEvent
5
- module Commands
6
- class Poller
7
- def initialize
8
- @options = {
9
- :id => 1,
10
- :queues => %W(high medium low),
11
- :command => 'start',
12
- :environment => RAILS_ENV,
13
- :daemon => false,
14
- :max_load_average => 8,
15
- :min_instances => 5,
16
- :max_instances => 200,
17
- :max_adjustment => 5,
18
- :min_queue_size => 1000
19
- }
20
-
21
- OptionParser.new do |opts|
22
- opts.banner = "Usage: #{$0} [options] <command>"
23
- opts.on("-d", "--daemon", "Run as a daemon") { |v| @options[:daemon] = v }
24
- opts.on("-i", "--id=N", Integer, "Specify ID used in PID file when running as daemon") { |v| @options[:id] = v }
25
- opts.on("-q", "--queues='high medium low'", "Specify queue names in order") { |v| @options[:queues] = v.split(' ') }
26
- opts.on("-e", "--environment=development", "Specify which rails environment to run in") { |v| @options[:environment] = v }
27
- opts.separator ""
28
- opts.separator "Cluster options:"
29
- opts.on("-l", "--load-average=8", "Specify what load average to optimize to") { |v| @options[:max_load_average] = v }
30
- opts.on("-m", "--min-instances=5", "Specify mimimum number of instances") { |v| @options[:min_instances] = v }
31
- opts.on("-x", "--max-instances=200", "Specify maximum number of instances") { |v| @options[:max_instances] = v }
32
- opts.on("-a", "--max-adjustment=5", "Specify how many the maximum amount of instances that will be adjusted") { |v| @options[:max_adjustment] = v }
33
- opts.on("-s", "--min-queue-size=1000", "Specify how many must be in the queue to adjust instances") { |v| @options[:min_queue_size] = v }
34
- opts.separator ""
35
- opts.separator "Commands:"
36
- opts.separator " start - starts up the poller"
37
- opts.separator " stop - stops a poller currently running as a daemon"
38
- opts.separator " status - prints the status of the queues"
39
- opts.separator ""
40
- opts.separator "Examples:"
41
- opts.separator " #{$0} start (starts a poller running in the console)"
42
- opts.separator " #{$0} -d -e production start (starts a poller running as a daemon with ID #1)"
43
- opts.separator " #{$0} --daemon --id=5 start (starts poller with ID #5)"
44
- opts.separator " #{$0} --daemon --id=5 stop (stops poller with ID #5)"
45
- end.parse!
46
-
47
- @options[:command] = ARGV.pop unless ARGV.empty?
48
-
49
- case
50
- when @options[:command] == 'start' && !@options[:daemon] then trap_ctrl_c and load_rails_environment and start_processing_loop
51
- when @options[:command] == 'start' && @options[:daemon] then trap_term and start_daemon and load_rails_environment and start_processing_loop and remove_pid
52
- when @options[:command] == 'stop' then stop_daemon
53
- when @options[:command] == 'cluster' then load_rails_environment and refresh_cluster
54
- when @options[:command] == 'status' then load_rails_environment and print_status
55
- end
56
- end
57
-
58
- def print_status
59
- ActionEvent::Message.queue_status(@options[:queues]).to_a.sort { |a,b| a.first <=> b.first }.each do |table,messages_left|
60
- log "#{table}:\t\t#{messages_left}"
61
- end
62
- end
63
-
64
- def load_rails_environment
65
- ENV['ACTION_EVENT_USE_POLLER_DB'] = 'true'
66
- ENV["RAILS_ENV"] = @options[:environment]
67
- RAILS_ENV.replace(@options[:environment])
68
- log "Loading #{RAILS_ENV} environment..."
69
- require "#{RAILS_ROOT}/config/environment"
70
-
71
- if defined?(NewRelic)
72
- NewRelic::Control.instance.instance_eval do
73
- @settings['app_name'] = @settings['app_name'] + ' (Poller)'
74
- end
75
- end
76
-
77
- true
78
- end
79
-
80
- # returns the name of the PID file to use for daemons
81
- def pid_filename
82
- @pid_filename ||= File.join(RAILS_ROOT, "/log/poller.#{@options[:id]}.pid")
83
- end
84
-
85
- # forks from the current process and closes out everything
86
- def start_daemon
87
- log "Starting daemon ##{@options[:id]}..."
88
-
89
- # some process magic
90
- exit if fork # Parent exits, child continues.
91
- Process.setsid # Become session leader.
92
- exit if fork # Zap session leader.
93
- Dir.chdir "/" # Release old working directory.
94
- File.umask 0000 # Ensure sensible umask. Adjust as needed.
95
-
96
- # Free file descriptors and point them somewhere sensible.
97
- STDIN.reopen "/dev/null"
98
- STDOUT.reopen File.join(RAILS_ROOT, "log/poller.log"), "a"
99
- STDERR.reopen STDOUT
100
-
101
- # don't start up until the previous poller is dead
102
- while (previous_pid = File.read(pid_filename).to_i rescue nil) do
103
- break unless File.exists?("/proc/#{previous_pid}")
104
- log "Waiting for previous poller to finish..."
105
- Process.kill('TERM', previous_pid)
106
- sleep(5)
107
- end
108
-
109
- # record pid
110
- File.open(pid_filename, 'w') { |f| f << Process.pid }
111
- end
112
-
113
- def trap_ctrl_c
114
- trap("SIGINT") do
115
- @stop_processing = true
116
- log "Sending stop signal..."
117
- end
118
- end
119
-
120
- def trap_term
121
- trap("SIGTERM") do
122
- @stop_processing = true
123
- log "Received stop signal..."
124
- end
125
- end
126
-
127
- def refresh_cluster
128
- # gather some current stats
129
- current_load = `uptime`.split(' ')[-3..-3][0].to_f
130
- current_queue = ActionEvent::Message.queue_status(*@options[:queues]).to_a.map(&:last).sum
131
-
132
- # remove stale pid files
133
- current_pids = Dir[File.join(RAILS_ROOT, "log/poller.*.pid")]
134
- active_pids, stale_pids = current_pids.partition { |f| (File.read("/proc/#{File.read(f).to_i}/cmdline").include?('poller') rescue false) }
135
- stale_pids.each { |f| File.delete(f) }
136
-
137
- # compute adjustment based on current load average and queue size
138
- if active_pids.length > 0
139
- current_instances = active_pids.length
140
- needed_instances = ((current_instances*@options[:max_load_average])/current_load).floor
141
-
142
- if needed_instances > current_instances
143
- needed_instances = [needed_instances, current_instances + @options[:max_adjustment]].min
144
- elsif needed_instances < current_instances && current_queue > @options[:min_queue_size]
145
- needed_instances = [needed_instances, current_instances - @options[:max_adjustment]].max
146
- end
147
- else
148
- current_instances = 0
149
- needed_instances = @options[:min_instances]
150
- end
151
-
152
- needed_instances = @options[:max_instances] if needed_instances > @options[:max_instances]
153
- needed_instances = @options[:min_instances] if needed_instances < @options[:min_instances]
154
-
155
-
156
- # remove pids if there's too many or spawn new ones if there's not enough
157
- if needed_instances < current_instances
158
- active_pids.last(current_instances - needed_instances).each { |pid_file| puts "delete #{pid_file}" } #File.delete(pid_file) }
159
- elsif needed_instances > current_instances
160
- (needed_instances - current_instances).times do
161
- next_id = (1..needed_instances).to_a.find { |i| !File.exists?(File.join(RAILS_ROOT, "log/poller.#{i}.pid")) }
162
- puts "start at id #{next_id}"
163
- # if fork
164
- #
165
- # end
166
- end
167
- end
168
- end
169
-
170
- def should_stop_processing?
171
- @stop_processing || (@options[:daemon] && (File.read(pid_filename).to_i rescue 0) != Process.pid)
172
- end
173
-
174
- # finds the already running daemon and stops it...
175
- def stop_daemon
176
- if previous_pid = File.read(pid_filename).to_i rescue nil
177
- log "Sending stop signal to daemon ##{@options[:id]}..."
178
- Process.kill('TERM', previous_pid)
179
- end
180
- end
181
-
182
- def remove_pid
183
- if Process.pid == (File.read(pid_filename).to_i rescue nil)
184
- log "Cleaning up PID file..."
185
- FileUtils.rm(pid_filename)
186
- end
187
- end
188
-
189
- # loops until should_stop_processing? set to true... in local mode, this is never set so it will loop forever
190
- def start_processing_loop
191
- log "Processing queues: #{@options[:queues].join(',')}"
192
- next_iteration or sleep(0.5) until should_stop_processing?
193
- log "Got signal to stop... exiting."
194
- end
195
-
196
- # if we can get a message, process it
197
- def next_iteration
198
- reload_application if RAILS_ENV == 'development'
199
- if message = ActionEvent::Message.try_to_get_next_message(@options[:queues])
200
- begin
201
- log_text = "#{message[:queue_name]}:#{message[:event]} (#{message[:params].inspect})"
202
- log "Processing #{log_text}"
203
- "#{message[:event]}_event".camelize.constantize.process(message[:params])
204
- log "Finished processing #{log_text}"
205
- rescue Exception => e
206
- log "Error processing #{log_text}: #{e} #{e.backtrace.join("\n")}"
207
- end
208
- return true
209
- else
210
- # return false if we didn't get a message... makes start_processing_loop sleep(1)
211
- return false
212
- end
213
- rescue Exception => e
214
- log "Error getting next message (#{e})"
215
- ActionEvent::Message.connection.verify! rescue log("Error verifying DB connection... sleeping 5 seconds. (#{$!})") and sleep(5)
216
- return true
217
- end
218
-
219
- def reload_application
220
- ActionController::Routing::Routes.reload
221
- ActionController::Base.view_paths.reload! rescue nil
222
- ActionView::Helpers::AssetTagHelper::AssetTag::Cache.clear rescue nil
223
-
224
- ActiveRecord::Base.reset_subclasses
225
- ActiveSupport::Dependencies.clear
226
- ActiveRecord::Base.clear_reloadable_connections!
227
- end
228
-
229
- def log(message)
230
- $stdout.puts "[#{"#{@options[:id]}:#{Process.pid} " if @options[:daemon]}#{Time.now}] #{message}"
231
- $stdout.flush
232
- end
233
- end
234
- end
235
- end
236
-
237
- ActionEvent::Commands::Poller.new