quebert 0.0.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/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ Gemfile.lock
21
+
22
+ ## PROJECT::SPECIFIC
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source :rubygems
2
+
3
+ gem 'json'
4
+ gem 'daemons'
5
+ gem 'optitron'
6
+ gem 'beanstalk-client'
7
+
8
+ group :test do
9
+ gem 'rspec'
10
+ gem 'ZenTest'
11
+ gem 'ruby-debug'
12
+ gem 'activerecord'
13
+ gem 'sqlite3-ruby'
14
+ end
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Brad Gessler
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,113 @@
1
+ = Quebert
2
+
3
+ async_observer is great, but is dated and doesn't really support running jobs outside of the async_send idiom. Quebert is an attempt to mix how jobs are run in other popular worker queue frameworks, like resque and dj, with async_observer so that you can have it both ways.
4
+
5
+ A worker queue framework designed around Beanstalk. Features include:
6
+
7
+ = Features
8
+
9
+ * [x] Multiple back-ends (InProcess, Sync, and Beanstalk)
10
+ * [x] Rails/ActiveRecord integration similar to async_observer
11
+ * [ ] Pluggable exception handling (for Hoptoad integration)
12
+ * [ ] Reliable daemonization with pidfile support
13
+
14
+ [x] = Completed, [ ] = Not Started
15
+
16
+ = How to use
17
+
18
+ There are two ways to enqueue jobs with Quebert: through the Job itself, provided you set a default back-end for the job, or put it on the backend.
19
+
20
+ == Jobs
21
+
22
+ Quebert includes a Job class so you can implement how you want certain types of Jobs performed.
23
+
24
+ Quebert.backend = Quebert::Backend::InProcess.new
25
+
26
+ class WackyMathWizard < Quebert::Job
27
+ def perform(*nums)
28
+ nums.inject(0){|sum, n| sum = sum + n}
29
+ end
30
+ end
31
+
32
+ You can either drop a job in a queue:
33
+
34
+ Quebert.backend.put WackyMathWizard, 1, 2, 3
35
+
36
+ Or drop it in right from the job:
37
+
38
+ WackyMathWizard.enqueue 4, 5, 6
39
+
40
+ Then perform the jobs!
41
+
42
+ Quebert.backend.reserve.perform # => 6
43
+ Quebert.backend.reserve.perform # => 15
44
+
45
+ == Async Sender
46
+
47
+ Take any ol' class and include the Quebert::AsyncSender.
48
+
49
+ Quebert.backend = Quebert::Backend::InProcess.new
50
+
51
+ class Greeter
52
+ include Quebert::AsyncSender
53
+
54
+ def initialize(name)
55
+ @name = name
56
+ end
57
+
58
+ def sleep_and_greet(time_of_day)
59
+ sleep 10000 # Sleeping, get it?
60
+ "Oh! Hi #{name}! Good #{time_of_day}."
61
+ end
62
+
63
+ def self.budweiser_greeting(name)
64
+ "waaazup #{name}!"
65
+ end
66
+ end
67
+
68
+ walmart_greeter = Greeter.new("Brad")
69
+
70
+ Remember the send method in ruby?
71
+
72
+ walmart_greeter.send(:sleep_and_greet, "morning")
73
+ # ... time passes, you wait as greeter snores obnoxiously ...
74
+ # => "Oh! Hi Brad! Good morning."
75
+
76
+ What if the method takes a long time to run and you want to queue it? async_send it!
77
+
78
+ walmart_greeter.async_send(:sleep_and_greet, "morning")
79
+ # ... do some shopping and come back later when the dude wakes up
80
+
81
+ Quebert figures out how to *serialize the class, throw it on a worker queue, re-instantiate it on the other side, and finish up the work.
82
+
83
+ Quebert.backend.reserve.perform # => "Oh! Hi Brad! Good morning."
84
+ # ... Sorry dude! I'm shopping already
85
+
86
+ Does it work on Class methods? Yeah, that was easier than making instance methods work:
87
+
88
+ Quebert.async_send(:budweiser_greeting, "Corey")
89
+ Quebert.backend.reserve.perform # => "waazup Corey!"
90
+
91
+ * Only basic data types are included for serialization. Serializers may be customized to include support for different types.
92
+
93
+ = License
94
+
95
+ Copyright (c) 2010 Brad Gessler
96
+
97
+ Permission is hereby granted, free of charge, to any person obtaining a copy
98
+ of this software and associated documentation files (the "Software"), to deal
99
+ in the Software without restriction, including without limitation the rights
100
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
101
+ copies of the Software, and to permit persons to whom the Software is
102
+ furnished to do so, subject to the following conditions:
103
+
104
+ The above copyright notice and this permission notice shall be included in
105
+ all copies or substantial portions of the Software.
106
+
107
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
108
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
109
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
110
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
111
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
112
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
113
+ THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ require 'rake'
2
+
3
+ begin
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gem|
6
+ gem.name = "quebert"
7
+ gem.summary = %Q{A worker queue framework built around beanstalkd}
8
+ gem.description = %Q{A worker queue framework built around beanstalkd}
9
+ gem.email = "brad@bradgessler.com"
10
+ gem.homepage = "http://github.com/bradgessler/quebert"
11
+ gem.authors = ["Brad Gessler"]
12
+ gem.add_development_dependency "rspec", ">= 1.2.9"
13
+ gem.add_dependency "json"
14
+ gem.add_dependency "daemons"
15
+ gem.add_dependency "beanstalk-client"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'spec/rake/spectask'
24
+ Spec::Rake::SpecTask.new(:spec) do |spec|
25
+ spec.libs << 'lib' << 'spec'
26
+ spec.spec_files = FileList['spec/**/*_spec.rb']
27
+ end
28
+
29
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.pattern = 'spec/**/*_spec.rb'
32
+ spec.rcov = true
33
+ end
34
+
35
+ task :spec => :check_dependencies
36
+
37
+ task :default => :spec
38
+
39
+ require 'rake/rdoctask'
40
+ Rake::RDocTask.new do |rdoc|
41
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "quebert #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.0
data/lib/quebert.rb ADDED
@@ -0,0 +1,29 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), 'quebert')
2
+
3
+ module Quebert
4
+ autoload :Configuration, 'quebert/configuration'
5
+ autoload :Job, 'quebert/job'
6
+ autoload :Consumer, 'quebert/consumer'
7
+ autoload :Backend, 'quebert/backend'
8
+ autoload :Support, 'quebert/support'
9
+ autoload :Worker, 'quebert/worker'
10
+ autoload :Daemonizable, 'quebert/daemonizing'
11
+ autoload :AsyncSender, 'quebert/async_sender'
12
+
13
+ class << self
14
+ def configuration
15
+ @configuration ||= Configuration.new
16
+ end
17
+ alias_method :config, :configuration
18
+
19
+ # Registry for quebert backends
20
+ def backends
21
+ @backends ||= {}
22
+ end
23
+ end
24
+
25
+ # Register built-in Quebert backends
26
+ Backend.register :beanstalk, Backend::Beanstalk
27
+ Backend.register :in_process, Backend::InProcess
28
+ Backend.register :sync, Backend::Sync
29
+ end
@@ -0,0 +1,99 @@
1
+ module Quebert
2
+ module AsyncSender
3
+
4
+ # Perform jobs on Object methods (not instances)
5
+ module Object
6
+ class ObjectJob < Job
7
+ def perform(const, meth, args)
8
+ Support.constantize(const).send(meth, *args)
9
+ end
10
+ end
11
+
12
+ def self.included(base)
13
+ base.send(:extend, ClassMethods)
14
+ end
15
+
16
+ module ClassMethods
17
+ def async_send(meth, *args)
18
+ ObjectJob.enqueue(self.name, meth, args)
19
+ end
20
+ end
21
+ end
22
+
23
+ # Perform jobs on instances of classes
24
+ module Instance
25
+ class InstanceJob < Job
26
+ def perform(klass, init_args, meth, args)
27
+ Support.constantize(klass).new(init_args).send(meth, *args)
28
+ end
29
+ end
30
+
31
+ def self.included(base)
32
+ # Its not as simple as including initialize in a class, we
33
+ # have to do some tricks to make it work so we can put the include
34
+ # before the initialize method as opposed to after. Ah, and thanks PivotalLabs for this.
35
+ base.extend ClassMethods
36
+ base.overwrite_initialize
37
+ base.instance_eval do
38
+ def method_added(name)
39
+ return if name != :initialize
40
+ overwrite_initialize
41
+ end
42
+ end
43
+ end
44
+
45
+ def initialize_with_async_sender(*args)
46
+ initialize_without_async_sender(*(@_init_args = args))
47
+ end
48
+
49
+ module ClassMethods
50
+ def overwrite_initialize
51
+ class_eval do
52
+ unless method_defined?(:initialize_with_async_sender)
53
+ define_method(:initialize_with_async_sender) do
54
+ initialize_without_async_sender
55
+ end
56
+ end
57
+
58
+ if instance_method(:initialize) != instance_method(:initialize_with_async_sender)
59
+ alias_method :initialize_without_async_sender, :initialize
60
+ alias_method :initialize, :initialize_with_async_sender
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ def async_send(meth, *args)
67
+ InstanceJob.enqueue(self.class.name, @_init_args, meth, args)
68
+ end
69
+ end
70
+
71
+ # Extend a class with both the object and instance async_send
72
+ module Class
73
+ def self.included(base)
74
+ base.send(:include, AsyncSender::Object)
75
+ base.send(:include, AsyncSender::Instance)
76
+ end
77
+ end
78
+
79
+ module ActiveRecord
80
+ class RecordJob < Job
81
+ def perform(klass, id, meth, args)
82
+ Support.constantize(klass).find(id).send(meth, *args)
83
+ end
84
+ end
85
+
86
+ def self.included(base)
87
+ base.send :include, InstanceMethods
88
+ base.send :include, AsyncSender::Object
89
+ end
90
+
91
+ module InstanceMethods
92
+ def async_send(meth, *args)
93
+ RecordJob.enqueue(self.class.name, id, meth, args)
94
+ end
95
+ end
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,11 @@
1
+ module Quebert
2
+ module Backend
3
+ autoload :InProcess, 'quebert/backend/in_process.rb'
4
+ autoload :Beanstalk, 'quebert/backend/beanstalk.rb'
5
+ autoload :Sync, 'quebert/backend/sync.rb'
6
+
7
+ def self.register(name, backend)
8
+ Quebert.backends[name.to_sym] = backend
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,35 @@
1
+ require 'beanstalk-client'
2
+
3
+ module Quebert
4
+ module Backend
5
+
6
+ # Manage jobs on a Beanstalk queue out of process
7
+ class Beanstalk < Beanstalk::Pool
8
+ def put(job, *args)
9
+ super Job.to_json(job, *args)
10
+ end
11
+
12
+ def reserve_with_consumer
13
+ Consumer::Beanstalk.new(reserve_without_consumer, self)
14
+ end
15
+ alias :reserve_without_consumer :reserve
16
+ alias :reserve :reserve_with_consumer
17
+
18
+ # For testing purposes... I think there's a better way to do this though.
19
+ def drain!
20
+ while peek_ready do
21
+ reserve_without_consumer.delete
22
+ end
23
+ while job = peek_buried do
24
+ last_conn.kick 1 # what? Why the 1? it kicks them all?
25
+ reserve_without_consumer.delete
26
+ end
27
+ end
28
+
29
+ def self.configure(opts={})
30
+ opts[:host] ||= '127.0.0.1:11300'
31
+ new(opts[:host], opts[:tube])
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,14 @@
1
+ module Quebert
2
+ module Backend
3
+ # Drops jobs on an array in-process.
4
+ class InProcess < Array
5
+ def put(job, *args)
6
+ unshift Job.to_json(job, *args)
7
+ end
8
+
9
+ def reserve
10
+ json = pop and Consumer::Base.new(Job.from_json(json))
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ module Quebert
2
+ module Backend
3
+ # Run the job syncronously. This is typically used in a testing environment
4
+ # or could be used as a fallback if other backends fail to initialize
5
+ class Sync
6
+ def put(job, *args)
7
+ Consumer::Base.new(job.new(args)).perform
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,16 @@
1
+ module Quebert
2
+ class Configuration
3
+ attr_accessor :backend
4
+
5
+ def self.from_hash(hash)
6
+ hash, config = Support.symbolize_keys(hash), new
7
+ # Find out backend from the registry and configure
8
+ if backend = Quebert.backends[hash.delete(:backend).to_sym]
9
+ # If the backend supports configuration, do it!
10
+ p backend
11
+ config.backend = backend.respond_to?(:configure) ? backend.configure(Support.symbolize_keys(hash)) : backend.new
12
+ end
13
+ config
14
+ end
15
+ end
16
+ end