action_event 0.0.1 → 0.0.2

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