resque_extensions 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/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format nested
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in resque_extensions.gemspec
4
+ gemspec
5
+
6
+ # this is just for our test project - we need activerecord
7
+ gem "guard-rspec"
8
+ gem "mocha"
9
+ gem "mock_redis"
10
+ gem "rails", "~> 3"
11
+ gem "rspec"
12
+ gem "sqlite3"
data/Guardfile ADDED
@@ -0,0 +1,24 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+
9
+ # Rails example
10
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
11
+ watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
12
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
13
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
14
+ watch('config/routes.rb') { "spec/routing" }
15
+ watch('app/controllers/application_controller.rb') { "spec/controllers" }
16
+
17
+ # Capybara features specs
18
+ watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
19
+
20
+ # Turnip features and steps
21
+ watch(%r{^spec/acceptance/(.+)\.feature$})
22
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
23
+ end
24
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Dan Langevin
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ # ResqueExtensions
2
+
3
+ Adds behavior to Resque to make it a bit more like Delayed::Job
4
+
5
+ To use, call `async` where you would have used `send_later`
6
+
7
+ ActiveRecord Objects are serialized with their class name and ID and pulled
8
+ out of the database when they are run.
9
+
10
+ ## Usage
11
+
12
+ ### Instance Method
13
+ @my_instance = MyClass.find(params[:id])
14
+ @my_instance.async(:expensive_method)
15
+
16
+ ### Class Method
17
+ # This will store just the ID of the MyClass instance passed in and pull
18
+ # it out of the DB when the code is run
19
+ MyClass.async(:other_expensive_method, MyClass.find(1))
20
+
21
+ ### Class Method on a class defined elsewhere
22
+
23
+ If you don't have access to the Class/Module you need (e.g. a Mailer that
24
+ is defined in a different codebase), you can use
25
+ `ResqueExtensions.enqueue_class_method`
26
+
27
+ ResqueExtensions.enqueue_class_method("MissingClass", :my_method, arg...)
28
+
29
+ ### Serializing ActiveRecord objects
30
+
31
+ In Delayed::Job, you can enqueue whole objects or collections of objects,
32
+ which are then serialized by Marshal or YAML. This is problematic for
33
+ several reasons
34
+
35
+ 1. Objects enqueued in one version of Ruby may not be able to be loaded in
36
+ another
37
+ 2. The underlying data may have changed and we can have an invalid version
38
+ of the object when our job is performed
39
+ 3. It is hard to debug jobs and determine exactly what they are doing because
40
+ Marshal and YAML are not particularly readable formats
41
+
42
+ To get around this ResqueExtensions serializes Classes and ActiveRecords in
43
+ a string format and then constantizes them or pulls them from the database
44
+ when the job is performed.
45
+
46
+ This allows us to enqueue jobs in a very flexible way. The following would
47
+ be converted to strings and the objects would be reified when the job is
48
+ performed.
49
+
50
+ my_instance.async(:my_method, other_instance, array_of_instances)
51
+
52
+
53
+ ### Specifying a Queue
54
+
55
+ You can specify a queue to run this job in when you enqueue it as an optional
56
+ last argument
57
+
58
+ MyClass.async(
59
+ :other_expensive_method, MyClass.find(1), :queue => "custom-queue"
60
+ )
61
+
62
+
63
+ ## Installation
64
+
65
+ Add this line to your application's Gemfile:
66
+
67
+ gem 'resque_extensions'
68
+
69
+ And then execute:
70
+
71
+ $ bundle
72
+
73
+ Or install it yourself as:
74
+
75
+ $ gem install resque_extensions
76
+
77
+ ## Usage
78
+
79
+ TODO: Write usage instructions here
80
+
81
+ ## Contributing
82
+
83
+ 1. Fork it
84
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
85
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
86
+ 4. Push to the branch (`git push origin my-new-feature`)
87
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,22 @@
1
+ require 'resque'
2
+
3
+ require "resque_extensions/version"
4
+ require "resque_extensions/async_method"
5
+
6
+ module ResqueExtensions
7
+
8
+ def self.enqueue_class_method(klass, *args)
9
+ klass = "#{AsyncMethod::CLASS_PREFIX}#{klass}"
10
+ AsyncMethod.new(klass, *args).enqueue!
11
+ end
12
+
13
+ module ObjectMethods
14
+ # call this method asynchronously
15
+ def async(*args)
16
+ ResqueExtensions::AsyncMethod.new(self, *args).enqueue!
17
+ end
18
+ end
19
+ end
20
+
21
+ Object.send(:extend, ResqueExtensions::ObjectMethods)
22
+ Object.send(:include, ResqueExtensions::ObjectMethods)
@@ -0,0 +1,124 @@
1
+ module ResqueExtensions
2
+ class AsyncMethod
3
+
4
+ OPTIONAL_SETTINGS = [:queue]
5
+
6
+ ACTIVERECORD_PREFIX = "_ActiveRecord::"
7
+ CLASS_PREFIX = "_Class::"
8
+
9
+
10
+ attr_reader :args
11
+
12
+ def self.perform(*args)
13
+ data = self.reify_data(args)
14
+ caller = data.shift
15
+ # call the method with all the args
16
+ caller.send(*data)
17
+ end
18
+
19
+ # Constructor
20
+ # @param [Object] caller The class or instance that is doing work
21
+ # @param [String, Symbol] method The method we are calling
22
+ # @param args Additional arguments we pass in
23
+ def initialize(caller, method, *args)
24
+ @caller = caller
25
+ @method = method
26
+ # set up our options
27
+ self.set_options(args)
28
+ # leftover args are assigned
29
+ @args = args
30
+ end
31
+
32
+ # Is the caller a class or an instance of
33
+ # a class
34
+ def class_method?
35
+ @caller.is_a?(Class)
36
+ end
37
+
38
+ def enqueue!
39
+ Resque::Job.create(
40
+ self.queue, self.class, *self.data_to_enqueue
41
+ )
42
+ end
43
+
44
+ # Is the caller an instance or a class
45
+ def instance_method?
46
+ !self.class_method?
47
+ end
48
+
49
+ # the queue for this job
50
+ def queue
51
+ @queue ||= "default"
52
+ end
53
+
54
+
55
+ protected
56
+
57
+ def self.reify_data(data)
58
+ # call recursively
59
+ if data.is_a?(Array)
60
+ data = data.collect{|d| self.reify_data(d)}
61
+ # call on values
62
+ elsif data.is_a?(Hash)
63
+ data.each_pair do |k,v|
64
+ data[k] = self.reify_data(v)
65
+ end
66
+ # our special ActiveRecord encoding
67
+ elsif data.to_s =~ /^#{ACTIVERECORD_PREFIX}/
68
+ # get our ActiveRecord back
69
+ data = data.split("::")
70
+ id = data.pop
71
+ class_name = data[1..-1].join("::")
72
+ data = Resque::Job.constantize(class_name).find(id)
73
+ # classes become strings prefixed by _Class
74
+ elsif data.to_s =~ /^#{CLASS_PREFIX}/
75
+ data = Resque::Job.constantize(data.gsub(/^#{CLASS_PREFIX}/,''))
76
+ end
77
+ # return data
78
+ data
79
+ end
80
+
81
+ def data_to_enqueue
82
+ self.prepare_data([@caller, @method, *@args])
83
+ end
84
+
85
+ # prepare our data for Redis
86
+ def prepare_data(data)
87
+ # call recursively
88
+ if data.is_a?(Array)
89
+ data = data.collect{|d| self.prepare_data(d)}
90
+ # call on values
91
+ elsif data.is_a?(Hash)
92
+ data.each_pair do |k,v|
93
+ data[k] = self.prepare_data(v)
94
+ end
95
+ # our special ActiveRecord encoding
96
+ elsif data.is_a?(ActiveRecord::Base)
97
+ data = "_ActiveRecord::#{data.class}::#{data.id}"
98
+ # classes become strings prefixed by _Class
99
+ elsif data.is_a?(Class)
100
+ data = "_Class::#{data.to_s}"
101
+ end
102
+ # return data
103
+ data
104
+ end
105
+
106
+ # set the options passed in as instance variables
107
+ def set_options(args)
108
+ if self.has_options?(args.last)
109
+ args.pop.each_pair do |key, val|
110
+ instance_variable_set("@#{key}", val)
111
+ end
112
+ end
113
+ end
114
+
115
+ # Is the given argument a hash of valid options
116
+ def has_options?(argument)
117
+ return false unless argument.is_a?(Hash)
118
+ # get our keys
119
+ keys = argument.keys.collect(&:to_sym)
120
+ # if we have overlaps, we've been passed options
121
+ return (keys & OPTIONAL_SETTINGS).length > 0
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,3 @@
1
+ module ResqueExtensions
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,22 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'resque_extensions/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "resque_extensions"
8
+ gem.version = ResqueExtensions::VERSION
9
+ gem.authors = ["Dan Langevin"]
10
+ gem.email = ["dan.langevin@lifebooker.com"]
11
+ gem.description = %q{An extension to Resque that makes it act more like Delayed::Job}
12
+ gem.summary = %q{Resque extensions to add .async}
13
+ gem.homepage = "https://github.com/dlangevin/resque_extensions"
14
+
15
+ # works with resque before 2.0
16
+ gem.add_dependency "resque", "~> 1"
17
+
18
+ gem.files = `git ls-files`.split($/)
19
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21
+ gem.require_paths = ["lib"]
22
+ end
@@ -0,0 +1,159 @@
1
+ require 'spec_helper'
2
+
3
+ module ResqueExtensions
4
+
5
+ describe AsyncMethod do
6
+
7
+ context ".perform" do
8
+
9
+ it "deserializes and calls a method on the caller" do
10
+ async_method = AsyncMethod.new(MyClass, :my_class_method, "a")
11
+ async_method.enqueue!
12
+
13
+ # make sure we call the correct
14
+ MyClass.expects(:send).with("my_class_method", "a")
15
+
16
+ job = Resque.reserve("default")
17
+ job.perform
18
+
19
+ end
20
+
21
+ it "deserializes and calls a method on an ActiveRecord" do
22
+
23
+ my_instance = MyClass.create(:name => "Dan")
24
+
25
+ async_method = AsyncMethod.new(my_instance, :my_instance_method)
26
+ async_method.enqueue!
27
+
28
+ MyClass.expects(:find).with(my_instance.id.to_s).returns(my_instance)
29
+ my_instance.expects(:send).with("my_instance_method")
30
+
31
+ job = Resque.reserve("default")
32
+ job.perform
33
+
34
+ end
35
+
36
+ end
37
+
38
+
39
+ context "#initialize" do
40
+
41
+ it "initializes with a class" do
42
+
43
+ async_method = AsyncMethod.new(MyClass, :my_class_method)
44
+ async_method.should be_class_method
45
+
46
+ end
47
+
48
+ it "initializes with an instance" do
49
+
50
+ async_method = AsyncMethod.new(MyClass.new, :my_class_method)
51
+ async_method.should be_instance_method
52
+
53
+ end
54
+
55
+ end
56
+
57
+ context "#args" do
58
+
59
+ it "accepts a variable number of arguments" do
60
+
61
+ async_method = AsyncMethod.new(
62
+ MyClass, :my_class_method, :a, :b, "c"
63
+ )
64
+ async_method.args.should eql([:a, :b, "c"])
65
+
66
+ end
67
+
68
+ it "removes any options from the arguments" do
69
+ async_method = AsyncMethod.new(
70
+ MyClass, :my_class_method, :a, :b, {:queue => "test"}
71
+ )
72
+ async_method.args.should eql([:a, :b])
73
+ end
74
+
75
+ end
76
+
77
+ context "#enqueue" do
78
+
79
+ it "creates a new job for the class or instance" do
80
+ async_method = AsyncMethod.new(MyClass, :my_class_method)
81
+ async_method.enqueue!
82
+
83
+ job = Resque.reserve("default")
84
+
85
+ job.payload.should eql({
86
+ "class" => "ResqueExtensions::AsyncMethod",
87
+ "args" => ["_Class::MyClass", "my_class_method"]
88
+ })
89
+
90
+ end
91
+
92
+ it "serializes ActiveRecords passed in as the caller" do
93
+
94
+ my_instance = MyClass.create(:name => "Dan")
95
+
96
+ async_method = AsyncMethod.new(my_instance, :my_instance_method)
97
+ async_method.enqueue!
98
+
99
+ job = Resque.reserve("default")
100
+
101
+ job.payload.should eql({
102
+ "class" => "ResqueExtensions::AsyncMethod",
103
+ "args" => [
104
+ "_ActiveRecord::MyClass::#{my_instance.id}",
105
+ "my_instance_method"
106
+ ]
107
+ })
108
+
109
+ end
110
+
111
+ it "serializes ActiveRecords passed in as arguments" do
112
+
113
+ my_instance = MyClass.create(:name => "Dan")
114
+
115
+ async_method = AsyncMethod.new(
116
+ MyClass,
117
+ :my_class_method,
118
+ [my_instance],
119
+ my_instance,
120
+ {:a => my_instance}
121
+ )
122
+ async_method.enqueue!
123
+
124
+ instance_string = "_ActiveRecord::MyClass::#{my_instance.id}"
125
+
126
+
127
+ job = Resque.reserve("default")
128
+
129
+ job.payload.should eql({
130
+ "class" => "ResqueExtensions::AsyncMethod",
131
+ "args" => [
132
+ "_Class::MyClass",
133
+ "my_class_method",
134
+ [instance_string],
135
+ instance_string,
136
+ {"a" => instance_string}
137
+ ]
138
+ })
139
+
140
+ end
141
+
142
+ end
143
+
144
+ context "#queue" do
145
+
146
+ it "sets the queue if it is passed as an argument" do
147
+
148
+ async_method = AsyncMethod.new(
149
+ MyClass, :my_class_method, :queue => "test"
150
+ )
151
+ async_method.queue.should eql("test")
152
+
153
+ end
154
+
155
+ end
156
+
157
+ end
158
+
159
+ end
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe ResqueExtensions do
4
+
5
+ context ".enqueue_class_method" do
6
+
7
+ it "enqueues a class method from a string" do
8
+ ResqueExtensions.enqueue_class_method(
9
+ "MyClass", :my_class_method
10
+ )
11
+ MyClass.expects(:send).with("my_class_method")
12
+
13
+ job = Resque.reserve("default")
14
+ job.perform
15
+ end
16
+
17
+ end
18
+
19
+ context "ObjectMethods" do
20
+
21
+ context ".async" do
22
+
23
+ it "enqueues a class method resque" do
24
+
25
+ MyClass.async(:my_class_method, "a", "b", "c")
26
+
27
+ MyClass.expects(:send).with("my_class_method", "a", "b", "c")
28
+
29
+ job = Resque.reserve("default")
30
+ job.perform
31
+ end
32
+
33
+ end
34
+
35
+ context "#async" do
36
+
37
+ it "enqueues an instance method resque" do
38
+
39
+ my_class = MyClass.create(:name => "test")
40
+
41
+ my_class.async(:my_instance_method, {:a => my_class})
42
+
43
+ MyClass.stubs(:find).with(my_class.id.to_s).returns(my_class)
44
+
45
+ my_class.expects(:send)
46
+ .with(
47
+ "my_instance_method",
48
+ has_entries("a" => instance_of(MyClass))
49
+ )
50
+
51
+ job = Resque.reserve("default")
52
+ job.perform
53
+
54
+
55
+ end
56
+
57
+ end
58
+
59
+ end
60
+
61
+ end
@@ -0,0 +1,51 @@
1
+
2
+ require 'resque_extensions'
3
+
4
+ require 'active_record'
5
+ require 'mock_redis'
6
+ require 'resque'
7
+
8
+
9
+ Bundler.setup
10
+
11
+ ActiveRecord::Base.establish_connection(
12
+ :adapter => "sqlite3",
13
+ :database => File.dirname(__FILE__) + "/../tmp/test.sqlite"
14
+ )
15
+
16
+ Resque.redis = MockRedis.new
17
+
18
+ # This code will be run each time you run your specs.
19
+ Dir[File.dirname(__FILE__) + "/support/**/*.rb"].each {|f| require f}
20
+
21
+ RSpec.configure do |config|
22
+ config.treat_symbols_as_metadata_keys_with_true_values = true
23
+ config.run_all_when_everything_filtered = true
24
+ config.mock_with :mocha
25
+
26
+ config.before(:each) do
27
+ Resque.redis = MockRedis.new
28
+ end
29
+
30
+ config.before(:all) do
31
+
32
+ ActiveRecord::Base.connection.create_table(:my_classes, :force => true) do |t|
33
+
34
+ t.string(:name)
35
+ t.timestamps
36
+ end
37
+
38
+ MyClass = Class.new(ActiveRecord::Base) do
39
+ def my_instance_method(*args)
40
+ return args
41
+ end
42
+
43
+ def self.my_class_method(*args)
44
+ return args
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: resque_extensions
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dan Langevin
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-04 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: resque
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1'
30
+ description: An extension to Resque that makes it act more like Delayed::Job
31
+ email:
32
+ - dan.langevin@lifebooker.com
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - .gitignore
38
+ - .rspec
39
+ - Gemfile
40
+ - Guardfile
41
+ - LICENSE.txt
42
+ - README.md
43
+ - Rakefile
44
+ - lib/resque_extensions.rb
45
+ - lib/resque_extensions/async_method.rb
46
+ - lib/resque_extensions/version.rb
47
+ - resque_extensions.gemspec
48
+ - spec/lib/resque_extensions/async_method_spec.rb
49
+ - spec/lib/resque_extensions_spec.rb
50
+ - spec/spec_helper.rb
51
+ homepage: https://github.com/dlangevin/resque_extensions
52
+ licenses: []
53
+ post_install_message:
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubyforge_project:
71
+ rubygems_version: 1.8.24
72
+ signing_key:
73
+ specification_version: 3
74
+ summary: Resque extensions to add .async
75
+ test_files:
76
+ - spec/lib/resque_extensions/async_method_spec.rb
77
+ - spec/lib/resque_extensions_spec.rb
78
+ - spec/spec_helper.rb
79
+ has_rdoc: