beanstalker 0.0.1

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,27 @@
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/.
8
+
9
+
10
+ Worker Options:
11
+ -d : daemonize
12
+ --pid [path to pidfile] : drop a pid file to a path
13
+ -e [test,production,development] : set the rails environment
14
+
15
+ Example Usage:
16
+
17
+ start 3 workers
18
+ ./vendor/plugins/async_observer/bin/worker -d --pid log/worker1.pid -e production
19
+ ./vendor/plugins/async_observer/bin/worker -d --pid log/worker2.pid -e production
20
+ ./vendor/plugins/async_observer/bin/worker -d --pid log/worker3.pid -e production
21
+
22
+ kill one
23
+ kill -s INT `cat log/worker1.pid`
24
+
25
+ Remember kill a worker will cause it to go into a shutdown phase.
26
+ Run the above again to kill immediately, but remember all jobs in
27
+ the workers queue is lost at that point...
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "beanstalker"
8
+ gem.summary = %Q{Beanstalker provides deep integration with Beanstalk. Fork from http://github.com/kristjan/async_observer}
9
+ gem.description = %Q{Beanstalker provides deep integration with Beanstalk. Fork from http://github.com/kristjan/async_observer}
10
+ gem.email = "glebpom@gmail.com"
11
+ gem.homepage = "http://github.com/glebpom/beanstalker"
12
+ gem.authors = ["Gleb Pomykalov"]
13
+ # gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ gem.add_dependency "daemonizer"
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/test_*.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "beanstalker #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/bin/worker ADDED
@@ -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,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,140 @@
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]
90
+
91
+ class << ActiveRecord::Base
92
+ HOOKS.each do |hook|
93
+ code = %Q{def async_#{hook}(*methods, &b) add_async_hook(#{hook.inspect}, *methods, &b) end}
94
+ class_eval(code, __FILE__, __LINE__ - 1)
95
+ end
96
+
97
+ def add_async_hook(hook, *args, &block)
98
+ if args && args.first.is_a?(Symbol)
99
+ method = args.shift
100
+ async_hooks[hook] << lambda{|o| o.send(method)}
101
+ else
102
+ async_hooks[hook] << block
103
+ end
104
+ end
105
+
106
+ def async_hooks
107
+ @async_hooks ||= Hash.new do |hash, hook|
108
+ ahook = :"_async_#{hook}"
109
+
110
+ # This is for the producer's benefit
111
+ send(hook){|o| async_send(ahook, o)}
112
+
113
+ # This is for the worker's benefit
114
+ code = "def #{ahook}(o) run_async_hooks(#{hook.inspect}, o) end"
115
+ instance_eval(code, __FILE__, __LINE__ - 1)
116
+
117
+ hash[hook] = []
118
+ end
119
+ end
120
+
121
+ def run_async_hooks(hook, o)
122
+ async_hooks[hook].each{|b| b.call(o)}
123
+ end
124
+
125
+ def send_to_instance(id, selector, *args)
126
+ x = find_by_id(id)
127
+ x.send(selector, *args) if x
128
+ end
129
+
130
+ def async_each_opts(selector, opts, *args)
131
+ min = opts.fetch(:min, minimum(:id))
132
+ max = opts.fetch(:max, maximum(:id))
133
+
134
+ (min..max).async_each_opts(self, :send_to_instance, opts, selector, *args)
135
+ end
136
+
137
+ def async_each(selector, *args)
138
+ async_each_opts(selector, {}, *args)
139
+ end
140
+ end
@@ -0,0 +1,182 @@
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
+ class Date
174
+ def rrepr() "Date.parse('#{self.inspect}')" end
175
+ end
176
+
177
+ module AsyncObserver::Extensions
178
+ def rrepr()
179
+ method = (respond_to? :get_cache) ? 'get_cache' : 'find'
180
+ "#{self.class.rrepr}.#{method}(#{id.rrepr})"
181
+ end
182
+ end