async_observer 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,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
+