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.
data/README ADDED
@@ -0,0 +1,7 @@
1
+ This is Async Observer -- a Rails plugin that provides deep integration with
2
+ Beanstalk.
3
+
4
+ For more information, see http://async-observer.rubyforge.org/.
5
+
6
+ For more information on Beanstalk, see its home page at
7
+ http://xph.us/software/beanstalkd/.
@@ -0,0 +1,13 @@
1
+ begin
2
+ require 'jeweler'
3
+ Jeweler::Tasks.new do |gemspec|
4
+ gemspec.name = "async_observer"
5
+ gemspec.summary = "Async Observer"
6
+ gemspec.description = "Async Observer is a Rails plugin that provides deep integration with Beanstalk."
7
+ gemspec.email = "kristjan@causes.com"
8
+ gemspec.homepage = "http://async-observer.rubyforge.org/"
9
+ gemspec.authors = ["Kristján Pétursson"]
10
+ end
11
+ rescue LoadError
12
+ puts "Jeweler not available. Install it with: gem install jeweler"
13
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,52 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{async_observer}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Kristj\303\241n P\303\251tursson"]
12
+ s.date = %q{2010-01-07}
13
+ s.default_executable = %q{worker}
14
+ s.description = %q{Async Observer is a Rails plugin that provides deep integration with Beanstalk.}
15
+ s.email = %q{kristjan@causes.com}
16
+ s.executables = ["worker"]
17
+ s.extra_rdoc_files = [
18
+ "README"
19
+ ]
20
+ s.files = [
21
+ ".gitignore",
22
+ "COPYING",
23
+ "README",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "async_observer.gemspec",
27
+ "bin/worker",
28
+ "init.rb",
29
+ "lib/async_observer.rb",
30
+ "lib/async_observer/daemonize.rb",
31
+ "lib/async_observer/extend.rb",
32
+ "lib/async_observer/queue.rb",
33
+ "lib/async_observer/util.rb",
34
+ "lib/async_observer/worker.rb"
35
+ ]
36
+ s.homepage = %q{http://async-observer.rubyforge.org/}
37
+ s.rdoc_options = ["--charset=UTF-8"]
38
+ s.require_paths = ["lib"]
39
+ s.rubygems_version = %q{1.3.5}
40
+ s.summary = %q{Async Observer}
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
47
+ else
48
+ end
49
+ else
50
+ end
51
+ end
52
+
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env ruby
2
+ # async-observer - Rails plugin for asynchronous job execution
3
+
4
+ # Copyright (C) 2007 Philotic Inc.
5
+
6
+ # This program is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+
11
+ # This program is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+
20
+ # Use the same pointer (and therefore same buffer) for stdout and stderr.
21
+ $VERBOSE = nil; STDERR = $stderr = STDOUT = $stdout; $VERBOSE = false
22
+
23
+ require 'time'
24
+
25
+ def load_rails_and_run
26
+ # Rails initialization.
27
+ # We do this here instead of using script/runner because script/runner
28
+ # breaks __FILE__, which we use below.
29
+ begin
30
+ puts "#!load-rails!begin!#{Time.now.utc.xmlschema(6)}"
31
+ require File.expand_path(File.dirname(__FILE__) + '/../../../../config/boot')
32
+ puts "RAILS_ROOT=#{RAILS_ROOT.inspect}"
33
+ require RAILS_ROOT + '/config/environment'
34
+ ensure
35
+ puts "#!load-rails!end!#{Time.now.utc.xmlschema(6)}"
36
+ end
37
+ require 'async_observer/worker'
38
+ AsyncObserver::Worker.new(binding).run()
39
+ end
40
+
41
+ # set environment
42
+ if ARGV.include?('-e') # check rails env
43
+ env=nil
44
+ ARGV.each_with_index{|arg,i| env = ARGV[i+1] if arg == '-e' }
45
+ RAILS_ENV=env unless env.nil?
46
+ end
47
+
48
+ if ARGV.include?('-d')
49
+ pidpath = 'log/worker.pid'
50
+ if ARGV.include?('--pid') # look up the pid path
51
+ ARGV.each_with_index{|arg,i| pidpath = ARGV[i+1] if arg == '--pid' }
52
+ STDERR.puts "Missing pid file path!" and exit(1) if pidpath.nil?
53
+ end
54
+ unless File.writable?(File.dirname(pidpath))
55
+ STDERR.puts "#{pidpath} not writable!"
56
+ exit(1)
57
+ end
58
+
59
+ if File.exist?(pidpath)
60
+ STDERR.puts "#{pidpath} exits! Make sure the worker isn't still running and try again after rm #{pidpath}"
61
+ exit(1)
62
+ end
63
+
64
+ require File.dirname(__FILE__) + '/../lib/async_observer/daemonize'
65
+ AsyncObserver::Daemonize.detach(pidpath) do
66
+ load_rails_and_run
67
+ end
68
+ else
69
+ load_rails_and_run
70
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'async_observer/extend'
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/../init.rb'
@@ -0,0 +1,35 @@
1
+ # async-observer - Rails plugin for asynchronous job execution
2
+ # Copyright (C) 2009 Todd A. Fisher.
3
+
4
+ # This program is free software: you can redistribute it and/or modify
5
+ # it under the terms of the GNU General Public License as published by
6
+ # the Free Software Foundation, either version 3 of the License, or
7
+ # (at your option) any later version.
8
+
9
+ # This program is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
+ # GNU General Public License for more details.
13
+
14
+ # You should have received a copy of the GNU General Public License
15
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
16
+ module AsyncObserver; end
17
+
18
+ class AsyncObserver::Daemonize
19
+ def self.detach(pidfile='log/worker.pid',&block)
20
+ # daemonize, create a pipe to send status to the parent process, after the child has successfully started or failed
21
+ fork do
22
+ Process.setsid
23
+ fork do
24
+ Process.setsid
25
+ File.open(pidfile, 'wb') {|f| f << Process.pid}
26
+ at_exit { File.unlink(pidfile) }
27
+ File.umask 0000
28
+ STDIN.reopen "/dev/null"
29
+ STDOUT.reopen "/dev/null", "a"
30
+ STDERR.reopen STDOUT
31
+ block.call
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,135 @@
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
+ require 'async_observer/queue'
19
+
20
+ CLASSES_TO_EXTEND = [
21
+ ActiveRecord::Base,
22
+ Array,
23
+ Hash,
24
+ Module,
25
+ Numeric,
26
+ Range,
27
+ String,
28
+ Symbol,
29
+ ]
30
+
31
+ module AsyncObserver::Extensions
32
+ def async_send(selector, *args)
33
+ async_send_opts(selector, {}, *args)
34
+ end
35
+
36
+ def async_send_opts(selector, opts, *args)
37
+ AsyncObserver::Queue.put_call!(self, selector, opts, args)
38
+ end
39
+ end
40
+
41
+ CLASSES_TO_EXTEND.each do |c|
42
+ c.send :include, AsyncObserver::Extensions
43
+ end
44
+
45
+ class Range
46
+ DEFAULT_FANOUT_FUZZ = 0
47
+ DEFAULT_FANOUT_DEGREE = 1000
48
+
49
+ def split_to(n)
50
+ split_by((size + n - 1) / n) { |group| yield(group) }
51
+ end
52
+
53
+ def split_by(n)
54
+ raise ArgumentError.new('invalid slice size') if n < 1
55
+ n -= 1 if !exclude_end?
56
+ i = first
57
+ while member?(i)
58
+ j = [i + n, last].min
59
+ yield(Range.new(i, j, exclude_end?))
60
+ i = j + (exclude_end? ? 0 : 1)
61
+ end
62
+ end
63
+
64
+ def size
65
+ last - first + (exclude_end? ? 0 : 1)
66
+ end
67
+
68
+ def async_each_opts(rcv, selector, opts, *extra)
69
+ fanout_degree = opts.fetch(:fanout_degree, DEFAULT_FANOUT_DEGREE)
70
+ if size <= fanout_degree
71
+ each {|i| rcv.async_send_opts(selector, opts, i, *extra)}
72
+ else
73
+ fanout_opts = opts.merge(:fuzz => opts.fetch(:fanout_fuzz,
74
+ DEFAULT_FANOUT_FUZZ))
75
+ fanout_opts[:pri] = opts[:fanout_pri] || opts[:pri]
76
+ fanout_opts = fanout_opts.reject_hash{|k,v| nil.equal?(v)}
77
+ split_to(fanout_degree) do |subrange|
78
+ subrange.async_send_opts(:async_each_opts, fanout_opts, rcv, selector,
79
+ opts, *extra)
80
+ end
81
+ end
82
+ end
83
+
84
+ def async_each(rcv, selector, *extra)
85
+ async_each_opts(rcv, selector, {}, *extra)
86
+ end
87
+ end
88
+
89
+ HOOKS = [:after_create, :after_update, :after_save, :after_destroy]
90
+
91
+ class << ActiveRecord::Base
92
+ HOOKS.each do |hook|
93
+ code = %Q{def async_#{hook}(&b) add_async_hook(#{hook.inspect}, b) end}
94
+ class_eval(code, __FILE__, __LINE__ - 1)
95
+ end
96
+
97
+ def add_async_hook(hook, block)
98
+ async_hooks[hook] << block
99
+ end
100
+
101
+ def async_hooks
102
+ @async_hooks ||= Hash.new do |hash, hook|
103
+ ahook = :"_async_#{hook}"
104
+
105
+ # This is for the producer's benefit
106
+ send(hook){|o| async_send(ahook, o)}
107
+
108
+ # This is for the worker's benefit
109
+ code = "def #{ahook}(o) run_async_hooks(#{hook.inspect}, o) end"
110
+ instance_eval(code, __FILE__, __LINE__ - 1)
111
+
112
+ hash[hook] = []
113
+ end
114
+ end
115
+
116
+ def run_async_hooks(hook, o)
117
+ async_hooks[hook].each{|b| b.call(o)}
118
+ end
119
+
120
+ def send_to_instance(id, selector, *args)
121
+ x = find_by_id(id)
122
+ x.send(selector, *args) if x
123
+ end
124
+
125
+ def async_each_opts(selector, opts, *args)
126
+ min = opts.fetch(:min, minimum(:id))
127
+ max = opts.fetch(:max, maximum(:id))
128
+
129
+ (min..max).async_each_opts(self, :send_to_instance, opts, selector, *args)
130
+ end
131
+
132
+ def async_each(selector, *args)
133
+ async_each_opts(selector, {}, *args)
134
+ end
135
+ end
@@ -0,0 +1,178 @@
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 'beanstalk-client'
20
+
21
+ module AsyncObserver; end
22
+
23
+ class AsyncObserver::Queue; end
24
+
25
+ class << AsyncObserver::Queue
26
+ DEFAULT_PRI = 512
27
+ DEFAULT_FUZZ = 0
28
+ DEFAULT_DELAY = 0
29
+ DEFAULT_TTR = 120
30
+ DEFAULT_TUBE = 'default'
31
+
32
+ attr_accessor :queue, :app_version, :after_put
33
+
34
+ # This is a fake worker instance for running jobs synchronously.
35
+ def sync_worker()
36
+ require 'async_observer/worker'
37
+ @sync_worker ||= AsyncObserver::Worker.new(binding)
38
+ end
39
+
40
+ # This runs jobs synchronously; it's used when no queue is configured.
41
+ def sync_run(obj)
42
+ body = YAML.dump(obj)
43
+ job = Beanstalk::Job.new(AsyncObserver::FakeConn.new(), 0, body)
44
+ sync_worker.dispatch(job)
45
+ sync_worker.do_all_work()
46
+ return 0, '0.0.0.0'
47
+ end
48
+
49
+ def put!(obj, pri=DEFAULT_PRI, delay=DEFAULT_DELAY, ttr=DEFAULT_TTR,
50
+ tube=DEFAULT_TUBE)
51
+ return sync_run(obj) if (:direct.equal?(pri) or !queue)
52
+ queue.connect()
53
+ queue.use(tube)
54
+ info = [queue.yput(obj, pri, delay, ttr), queue.last_server]
55
+ f = AsyncObserver::Queue.after_put
56
+ f.call(*info) if f
57
+ return info
58
+ end
59
+
60
+ SUBMIT_OPTS = [:pri, :fuzz, :delay, :ttr, :tube]
61
+
62
+ def put_call!(obj, sel, opts, args=[])
63
+ pri = opts.fetch(:pri, DEFAULT_PRI)
64
+ fuzz = opts.fetch(:fuzz, DEFAULT_FUZZ)
65
+ delay = opts.fetch(:delay, DEFAULT_DELAY)
66
+ ttr = opts.fetch(:ttr, DEFAULT_TTR)
67
+ tube = opts.fetch(:tube, (app_version or DEFAULT_TUBE))
68
+ worker_opts = opts.reject{|k,v| SUBMIT_OPTS.include?(k)}
69
+
70
+ pri = pri + rand(fuzz + 1) if !:direct.equal?(pri)
71
+
72
+ code = gen(obj, sel, args)
73
+ RAILS_DEFAULT_LOGGER.info("put #{pri} #{code}")
74
+ put!(pkg(code, worker_opts), pri, delay, ttr, tube)
75
+ end
76
+
77
+ def pkg(code, opts)
78
+ opts.merge(:type => :rails, :code => code)
79
+ end
80
+
81
+ # Be careful not to pass in a selector that's not valid ruby source.
82
+ def gen(obj, selector, args)
83
+ obj.rrepr + '.' + selector.to_s + '(' + gen_args(args) + ')'
84
+ end
85
+
86
+ def gen_args(args)
87
+ args.rrepr[1...-1]
88
+ end
89
+ end
90
+
91
+ class AsyncObserver::FakeConn
92
+ def delete(x)
93
+ end
94
+
95
+ def release(x, y, z)
96
+ end
97
+
98
+ def bury(x, y)
99
+ end
100
+
101
+ def addr
102
+ '<none>'
103
+ end
104
+
105
+ def job_stats(id)
106
+ {
107
+ 'id' => id,
108
+ 'state' => 'reserved',
109
+ 'age' => 0,
110
+ 'delay' => 0,
111
+ 'time-left' => 5000,
112
+ 'timeouts' => 0,
113
+ 'releases' => 0,
114
+ 'buries' => 0,
115
+ 'kicks' => 0,
116
+ }
117
+ end
118
+ end
119
+
120
+ # This is commented out to workaround (what we think is) a ruby bug in method
121
+ # lookup. Somehow the methods defined here are being used instead of ones in
122
+ # ActiveRecord::Base.
123
+ #class Object
124
+ # def rrepr()
125
+ # raise ArgumentError.new('no consistent external repr for ' + self.inspect)
126
+ # end
127
+ #end
128
+
129
+ class Symbol
130
+ def rrepr() inspect end
131
+ end
132
+
133
+ class Module
134
+ def rrepr() name end
135
+ end
136
+
137
+ class NilClass
138
+ def rrepr() inspect end
139
+ end
140
+
141
+ class FalseClass
142
+ def rrepr() inspect end
143
+ end
144
+
145
+ class TrueClass
146
+ def rrepr() inspect end
147
+ end
148
+
149
+ class Numeric
150
+ def rrepr() inspect end
151
+ end
152
+
153
+ class String
154
+ def rrepr() inspect end
155
+ end
156
+
157
+ class Array
158
+ def rrepr() '[' + map(&:rrepr).join(', ') + ']' end
159
+ end
160
+
161
+ class Hash
162
+ def rrepr() '{' + map{|k,v| k.rrepr + '=>' + v.rrepr}.join(', ') + '}' end
163
+ end
164
+
165
+ class Range
166
+ def rrepr() "(#{first.rrepr}#{exclude_end? ? '...' : '..'}#{last.rrepr})" end
167
+ end
168
+
169
+ class Time
170
+ def rrepr() "Time.parse('#{self.inspect}')" end
171
+ end
172
+
173
+ module AsyncObserver::Extensions
174
+ def rrepr()
175
+ method = (respond_to? :get_cache) ? 'get_cache' : 'find'
176
+ "#{self.class.rrepr}.#{method}(#{id.rrepr})"
177
+ end
178
+ end