async_observer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ # async-observer - Rails plugin for asynchronous job execution
2
+
3
+ # Copyright (C) 2007 Philotic Inc.
4
+
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+
19
+ require 'open3'
20
+
21
+ module AsyncObserver; end
22
+ module AsyncObserver::Util
23
+ def log_bracketed(name)
24
+ begin
25
+ RAILS_DEFAULT_LOGGER.info "#!#{name}!begin!#{Time.now.utc.xmlschema(6)}"
26
+ yield()
27
+ ensure
28
+ RAILS_DEFAULT_LOGGER.info "#!#{name}!end!#{Time.now.utc.xmlschema(6)}"
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,260 @@
1
+ # async-observer - Rails plugin for asynchronous job execution
2
+
3
+ # Copyright (C) 2007 Philotic Inc.
4
+
5
+ # This program is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+
19
+ begin
20
+ require 'mysql'
21
+ rescue LoadError
22
+ # Ignore case where we don't have mysql
23
+ end
24
+ require 'async_observer/queue'
25
+ require 'async_observer/util'
26
+
27
+ module AsyncObserver; end
28
+
29
+ class AsyncObserver::Worker
30
+ extend AsyncObserver::Util
31
+ include AsyncObserver::Util
32
+
33
+ SLEEP_TIME = 60 if !defined?(SLEEP_TIME) # rails loads this file twice
34
+
35
+ class << self
36
+ attr_accessor :finish
37
+ attr_accessor :custom_error_handler
38
+ attr_accessor :before_filter
39
+ attr_writer :handle
40
+
41
+ def handle
42
+ @handle or raise 'no custom handler is defined'
43
+ end
44
+
45
+ def error_handler(&block)
46
+ self.custom_error_handler = block
47
+ end
48
+
49
+ def before_reserves
50
+ @before_reserves ||= []
51
+ end
52
+
53
+ def before_reserve(&block)
54
+ before_reserves << block
55
+ end
56
+
57
+ def run_before_reserve
58
+ before_reserves.each {|b| b.call()}
59
+ end
60
+ end
61
+
62
+ def initialize(top_binding)
63
+ @top_binding = top_binding
64
+ @stop = false
65
+ end
66
+
67
+ def main_loop()
68
+ trap('TERM') { @stop = true }
69
+ until @stop do
70
+ safe_dispatch(get_job())
71
+ end
72
+ end
73
+
74
+ def startup()
75
+ log_bracketed('worker-startup') do
76
+ appver = AsyncObserver::Queue.app_version
77
+ RAILS_DEFAULT_LOGGER.info "pid is #{$$}"
78
+ RAILS_DEFAULT_LOGGER.info "app version is #{appver}"
79
+ mark_db_socket_close_on_exec()
80
+ if AsyncObserver::Queue.queue.nil?
81
+ RAILS_DEFAULT_LOGGER.error 'no queue has been configured'
82
+ exit(1)
83
+ end
84
+ AsyncObserver::Queue.queue.watch(appver) if appver
85
+ end
86
+ flush_logger
87
+ end
88
+
89
+ # This prevents us from leaking fds when we exec. Only works for mysql.
90
+ def mark_db_socket_close_on_exec()
91
+ ActiveRecord::Base.active_connections.each(&:set_close_on_exec)
92
+ rescue NoMethodError
93
+ end
94
+
95
+ def shutdown()
96
+ log_bracketed('worker-shutdown') do
97
+ do_all_work()
98
+ end
99
+ end
100
+
101
+ def run()
102
+ startup()
103
+ main_loop()
104
+ rescue Interrupt
105
+ shutdown()
106
+ end
107
+
108
+ def q_hint()
109
+ @q_hint || AsyncObserver::Queue.queue
110
+ end
111
+
112
+ # This heuristic is to help prevent one queue from starving. The idea is that
113
+ # if the connection returns a job right away, it probably has more available.
114
+ # But if it takes time, then it's probably empty. So reuse the same
115
+ # connection as long as it stays fast. Otherwise, have no preference.
116
+ def reserve_and_set_hint()
117
+ t1 = Time.now.utc
118
+ return job = q_hint().reserve()
119
+ ensure
120
+ t2 = Time.now.utc
121
+ @q_hint = if brief?(t1, t2) and job then job.conn else nil end
122
+ end
123
+
124
+ def brief?(t1, t2)
125
+ ((t2 - t1) * 100).to_i.abs < 10
126
+ end
127
+
128
+ def get_job()
129
+ log_bracketed('worker-get-job') do
130
+ loop do
131
+ begin
132
+ AsyncObserver::Queue.queue.connect()
133
+ self.class.run_before_reserve
134
+ return reserve_and_set_hint()
135
+ rescue Interrupt => ex
136
+ raise ex
137
+ rescue SignalException => ex
138
+ raise ex
139
+ rescue Beanstalk::DeadlineSoonError
140
+ # Do nothing; immediately try again, giving the user a chance to
141
+ # clean up in the before_reserve hook.
142
+ RAILS_DEFAULT_LOGGER.info 'Job deadline soon; you should clean up.'
143
+ rescue Exception => ex
144
+ @q_hint = nil # in case there's something wrong with this conn
145
+ RAILS_DEFAULT_LOGGER.info(
146
+ "#{ex.class}: #{ex}\n" + ex.backtrace.join("\n"))
147
+ RAILS_DEFAULT_LOGGER.info 'something is wrong. We failed to get a job.'
148
+ RAILS_DEFAULT_LOGGER.info "sleeping for #{SLEEP_TIME}s..."
149
+ sleep(SLEEP_TIME)
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ def dispatch(job)
156
+ ActiveRecord::Base.verify_active_connections!
157
+ return run_ao_job(job) if async_observer_job?(job)
158
+ return run_other(job)
159
+ end
160
+
161
+ def safe_dispatch(job)
162
+ log_bracketed('worker-dispatch') do
163
+ RAILS_DEFAULT_LOGGER.info "got #{job.inspect}:\n" + job.body
164
+ log_bracketed('job-stats') do
165
+ job.stats.each do |k,v|
166
+ RAILS_DEFAULT_LOGGER.info "#{k}=#{v}"
167
+ end
168
+ end
169
+ begin
170
+ return dispatch(job)
171
+ rescue Interrupt => ex
172
+ begin job.release() rescue :ok end
173
+ raise ex
174
+ rescue Exception => ex
175
+ handle_error(job, ex)
176
+ ensure
177
+ flush_logger
178
+ end
179
+ end
180
+ end
181
+
182
+ def flush_logger
183
+ if defined?(RAILS_DEFAULT_LOGGER) &&
184
+ RAILS_DEFAULT_LOGGER.respond_to?(:flush)
185
+ RAILS_DEFAULT_LOGGER.flush
186
+ end
187
+ end
188
+
189
+ def handle_error(job, ex)
190
+ if self.class.custom_error_handler
191
+ self.class.custom_error_handler.call(job, ex)
192
+ else
193
+ self.class.default_handle_error(job, ex)
194
+ end
195
+ end
196
+
197
+ def self.default_handle_error(job, ex)
198
+ RAILS_DEFAULT_LOGGER.info "Job failed: #{job.server}/#{job.id}"
199
+ RAILS_DEFAULT_LOGGER.info("#{ex.class}: #{ex}\n" + ex.backtrace.join("\n"))
200
+ job.decay()
201
+ rescue Beanstalk::UnexpectedResponse
202
+ end
203
+
204
+ def run_ao_job(job)
205
+ RAILS_DEFAULT_LOGGER.info 'running as async observer job'
206
+ f = self.class.before_filter
207
+ f.call(job) if f
208
+ job.delete if job.ybody[:delete_first]
209
+ run_code(job)
210
+ job.delete()
211
+ rescue ActiveRecord::RecordNotFound => ex
212
+ if job.age > 60
213
+ job.delete() # it's old; this error is most likely permanent
214
+ else
215
+ job.decay() # it could be replication delay so retry quietly
216
+ end
217
+ end
218
+
219
+ def run_code(job)
220
+ eval(job.ybody[:code], @top_binding, "(beanstalk job #{job.id})", 1)
221
+ end
222
+
223
+ def async_observer_job?(job)
224
+ begin job.ybody[:type] == :rails rescue false end
225
+ end
226
+
227
+ def run_other(job)
228
+ RAILS_DEFAULT_LOGGER.info 'trying custom handler'
229
+ self.class.handle.call(job)
230
+ end
231
+
232
+ def do_all_work()
233
+ RAILS_DEFAULT_LOGGER.info 'finishing all running jobs. interrupt again to kill them.'
234
+ f = self.class.finish
235
+ f.call() if f
236
+ end
237
+ end
238
+
239
+ class ActiveRecord::ConnectionAdapters::MysqlAdapter < ActiveRecord::ConnectionAdapters::AbstractAdapter
240
+ def set_close_on_exec()
241
+ @connection.set_close_on_exec()
242
+ end
243
+ end
244
+
245
+ class Mysql
246
+ def set_close_on_exec()
247
+ if @net
248
+ @net.set_close_on_exec()
249
+ else
250
+ # we are in the c mysql binding
251
+ RAILS_DEFAULT_LOGGER.info "Warning: we are using the C mysql binding, can't set close-on-exec"
252
+ end
253
+ end
254
+ end
255
+
256
+ class Mysql::Net
257
+ def set_close_on_exec()
258
+ @sock.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
259
+ end
260
+ end
metadata ADDED
@@ -0,0 +1,68 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: async_observer
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - "Kristj\xC3\xA1n P\xC3\xA9tursson"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-07 00:00:00 -05:00
13
+ default_executable: worker
14
+ dependencies: []
15
+
16
+ description: Async Observer is a Rails plugin that provides deep integration with Beanstalk.
17
+ email: kristjan@causes.com
18
+ executables:
19
+ - worker
20
+ extensions: []
21
+
22
+ extra_rdoc_files:
23
+ - README
24
+ files:
25
+ - .gitignore
26
+ - COPYING
27
+ - README
28
+ - Rakefile
29
+ - VERSION
30
+ - async_observer.gemspec
31
+ - bin/worker
32
+ - init.rb
33
+ - lib/async_observer.rb
34
+ - lib/async_observer/daemonize.rb
35
+ - lib/async_observer/extend.rb
36
+ - lib/async_observer/queue.rb
37
+ - lib/async_observer/util.rb
38
+ - lib/async_observer/worker.rb
39
+ has_rdoc: true
40
+ homepage: http://async-observer.rubyforge.org/
41
+ licenses: []
42
+
43
+ post_install_message:
44
+ rdoc_options:
45
+ - --charset=UTF-8
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - ">="
51
+ - !ruby/object:Gem::Version
52
+ version: "0"
53
+ version:
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: "0"
59
+ version:
60
+ requirements: []
61
+
62
+ rubyforge_project:
63
+ rubygems_version: 1.3.5
64
+ signing_key:
65
+ specification_version: 3
66
+ summary: Async Observer
67
+ test_files: []
68
+