candygram 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.markdown +103 -0
- data/Rakefile +47 -0
- data/VERSION +1 -0
- data/lib/candygram.rb +10 -0
- data/lib/candygram/connection.rb +64 -0
- data/lib/candygram/delivery.rb +37 -0
- data/lib/candygram/dispatch.rb +136 -0
- data/lib/candygram/utility.rb +47 -0
- data/lib/candygram/wrapper.rb +116 -0
- data/spec/candygram/connection_spec.rb +52 -0
- data/spec/candygram/delivery_spec.rb +52 -0
- data/spec/candygram/dispatch_spec.rb +93 -0
- data/spec/candygram/wrapper_spec.rb +178 -0
- data/spec/candygram_spec.rb +2 -0
- data/spec/spec.opts +1 -0
- data/spec/spec.watchr +12 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/explosive.rb +25 -0
- data/spec/support/missile.rb +10 -0
- metadata +123 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Stephen Eley
|
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.markdown
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
# Candygram
|
2
|
+
|
3
|
+
__"Candygram for Mongo!"__ — _Blazing Saddles_
|
4
|
+
|
5
|
+
Candygram is a job queueing system for the MongoDB database. It is loosely based on the **delayed_job** gem for ActiveRecord and the **Resque** gem for Redis, with the following interesting distinctions:
|
6
|
+
|
7
|
+
* Delayed running can be added to any class with `include Candygram::Delivery`.
|
8
|
+
* Objects with the Delivery module included get magic _*\_later_ variants on every instance method to enqueue the method call. (_*\_in_ and _*\_at_ variants coming soon to specify a time of execution.)
|
9
|
+
* Object states and method arguments are serialized as BSON to take best advantage of Mongo's efficiency.
|
10
|
+
* A centralized dispatcher forks runners to handle each job, with maximum limits defined per class.
|
11
|
+
* The job queue is a capped collection; because jobs are never deleted, recent history can be analyzed and failure states reported.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
Come on, you've done this before:
|
16
|
+
|
17
|
+
$ sudo gem install candygram
|
18
|
+
|
19
|
+
Candygram requires the **mongo** gem, and you'll probably be much happier if you install the **mongo\_ext** gem as well. The author uses only Ruby 1.9, but it _should_ work in Ruby 1.8. If it doesn't, please report a bug in Github's issue tracking system.
|
20
|
+
|
21
|
+
## Configuration
|
22
|
+
|
23
|
+
Both the Delivery and the Dispatcher modules take some configuration
|
24
|
+
parameters to connect to the proper Mongo collection:
|
25
|
+
|
26
|
+
# Makes a default connection to 'localhost' if you don't override it
|
27
|
+
Candygram.connection = Mongo::Connection.new(_params_)
|
28
|
+
|
29
|
+
# Creates a default 'candygram' database if you don't override it
|
30
|
+
Candygram.database = 'my_database'
|
31
|
+
|
32
|
+
# Creates a default 'candygram_queue' collection if you don't -- you get the picture.
|
33
|
+
Candygram.queue = Candygram.database.collection('my_queue')
|
34
|
+
# Or, to make a brand new queue with the proper indexes:
|
35
|
+
Candygram.create_queue('my_queue', 1048576) # 1MB capped collection
|
36
|
+
|
37
|
+
## Creating Jobs
|
38
|
+
|
39
|
+
You can set up any Ruby class to delay method executions by including the Delivery module:
|
40
|
+
|
41
|
+
require 'candygram'
|
42
|
+
|
43
|
+
class Explosive
|
44
|
+
include Candygram::Delivery
|
45
|
+
CANDYGRAM_MAX = 5 # Optional; limits simultaneous runners per dispatcher
|
46
|
+
|
47
|
+
def kaboom(planet)
|
48
|
+
"A #{planet}-shattering kaboom!"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
You can continue to use the class as you normally would, of course. If you want to queue a method to run later, just add _\_later_ to the method name:
|
53
|
+
|
54
|
+
e = Explosive.new
|
55
|
+
e.kaboom_later('Mars')
|
56
|
+
|
57
|
+
This will serialize the object _e_ (including any instance variables) into a Mongo document, along with the method name and the argument. The Candygram dispatcher will find it the next time it looks for jobs to run. It will fork a separate process to unpack the object, call the `kaboom` method, and save the return value in the job document for later reference.
|
58
|
+
|
59
|
+
## Dispatching
|
60
|
+
|
61
|
+
Nice Rake tasks and Rails generators and such are still pending. In the meantime, you can easily make your own dispatch script and call it with Rake or cron or trained beagle or what-have-you:
|
62
|
+
|
63
|
+
require 'candygram'
|
64
|
+
require 'my_environment' # Whatever else you need to make your classes visible
|
65
|
+
|
66
|
+
# Config parameters can be passed as a hash to `new` or by setting attributes.
|
67
|
+
d = Candygram::Dispatch.new
|
68
|
+
d.frequency = 10 # Check for jobs every 10 seconds (default 5)
|
69
|
+
d.max_per_class = 20 # Simultaneous runners per class (default 10)
|
70
|
+
d.quiet = true # Don't announce work on standard output (default false)
|
71
|
+
|
72
|
+
# This is the central method that loops and checks for work...
|
73
|
+
d.run
|
74
|
+
|
75
|
+
# You can kill the loop with CTRL-C or a 'kill' of course, or by
|
76
|
+
# calling 'd.finish' (but to make that work, you'd need to access
|
77
|
+
# it from a separate thread).
|
78
|
+
|
79
|
+
The dispatcher forks a separate process for each job in the queue, constrained by the per-class limits set by the CANDYGRAM_MAX constant in the class or by the **max\_per\_class** configuration variable. It keeps track of its child PIDs and will wait for them to finish if shut down gracefully. Jobs are locked so that dispatchers on multiple servers can be run against the same queue.
|
80
|
+
|
81
|
+
Job runners push status information onto the document to indicate time of running and completion. Future enhancements will likely include some reporting on this data, detection and rerunning on exception or timeout, and (possibly) optimization based on average run times.
|
82
|
+
|
83
|
+
## Limitations
|
84
|
+
|
85
|
+
* You cannot pass blocks or procs to delayed methods. There's just no robust way to capture and serialize the things.
|
86
|
+
* Objects with singleton methods or module extensions outside their class will lose them.
|
87
|
+
* Object classes must accept the `.new` method without any parameters. It's probably a good idea not to pass objects that have complex initialization.
|
88
|
+
* In general, objects that maintain state in any way cleverer than their instance variables will become stupid and probably unpredictable.
|
89
|
+
* Circular object graphs will probably cause explosions.
|
90
|
+
* Because it uses `fork`, this won't run on Windows. (A limitation which bothers me not one iota.)
|
91
|
+
|
92
|
+
## Big Important Disclaimer
|
93
|
+
|
94
|
+
This is still very very alpha software. I needed this for a couple of my own projects, but I'm pushing it into the wild _before_ proving it on those projects; if I don't, I'll probably lose energy and forget to do all the gem bundling and such. It's not nearly as robust yet as I hope to make it: there's a lot more I want to do for handling error cases and making the dispatcher easy to keep running.
|
95
|
+
|
96
|
+
I welcome your suggestions and bug reports using the Github issue tracker. I'm happy to take pull requests as well. If you use this for any interesting projects, please drop me a line and let me know about it -- eventually I may compile a list.
|
97
|
+
|
98
|
+
Have Fun.
|
99
|
+
|
100
|
+
|
101
|
+
## Copyright
|
102
|
+
|
103
|
+
Copyright (c) 2009 Stephen Eley. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "candygram"
|
8
|
+
gem.summary = %Q{Delayed job queueing for MongoMapper}
|
9
|
+
gem.description = %Q{Candygram provides a job queue for the MongoDB document-oriented database, inspired by delayed_job and Resque. It uses the MongoMapper ORM and allows you to queue method calls for execution ASAP or at any time in the future.}
|
10
|
+
gem.email = "sfeley@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/SFEley/candygram"
|
12
|
+
gem.authors = ["Stephen Eley"]
|
13
|
+
gem.add_dependency "mongo", ">= 0.18"
|
14
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
15
|
+
gem.add_development_dependency "yard", ">= 0.5.2"
|
16
|
+
gem.add_development_dependency "mocha", ">= 0.9.7"
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
require 'spec/rake/spectask'
|
25
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
26
|
+
spec.libs << 'lib' << 'spec'
|
27
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
28
|
+
end
|
29
|
+
|
30
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
31
|
+
spec.libs << 'lib' << 'spec'
|
32
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
33
|
+
spec.rcov = true
|
34
|
+
end
|
35
|
+
|
36
|
+
task :spec => :check_dependencies
|
37
|
+
|
38
|
+
task :default => :spec
|
39
|
+
|
40
|
+
begin
|
41
|
+
require 'yard'
|
42
|
+
YARD::Rake::YardocTask.new
|
43
|
+
rescue LoadError
|
44
|
+
task :yardoc do
|
45
|
+
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
46
|
+
end
|
47
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/candygram.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Candygram
|
2
|
+
|
3
|
+
# The MongoDB connection object. Creates a default connection to localhost if not explicitly told otherwise.
|
4
|
+
def self.connection
|
5
|
+
@connection ||= Mongo::Connection.new
|
6
|
+
end
|
7
|
+
|
8
|
+
# Accepts a new MongoDB connection, closing any current ones
|
9
|
+
def self.connection=(val)
|
10
|
+
@connection.close if @connection
|
11
|
+
@connection = val
|
12
|
+
end
|
13
|
+
|
14
|
+
# The Mongo database object. If you just want the name, use #database instead.
|
15
|
+
def self.db
|
16
|
+
@db ||= Mongo::DB.new(DEFAULT_DATABASE, connection)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Sets the Mongo database object. Unless you want to pass specific options or bypass the
|
20
|
+
# Candygram connection object completely, it's probably easier to use the #database= method
|
21
|
+
# and give it the name.
|
22
|
+
def self.db=(val)
|
23
|
+
@db = val
|
24
|
+
end
|
25
|
+
|
26
|
+
# The name of the Mongo database object.
|
27
|
+
def self.database
|
28
|
+
db.name
|
29
|
+
end
|
30
|
+
|
31
|
+
# Creates a Mongo database object with the given name and default options.
|
32
|
+
def self.database=(val)
|
33
|
+
self.db = Mongo::DB.new(val, connection)
|
34
|
+
end
|
35
|
+
|
36
|
+
# The delivery queue collection. If not set, creates a capped collection with a default
|
37
|
+
# name of 'candygram_queue' and a default cap size of 100MB.
|
38
|
+
def self.queue
|
39
|
+
@queue or begin
|
40
|
+
if db.collection_names.include?(DEFAULT_QUEUE)
|
41
|
+
@queue = db[DEFAULT_QUEUE]
|
42
|
+
else
|
43
|
+
@queue = create_queue
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Sets the delivery queue to an existing collection. Assumes you know what you're doing
|
49
|
+
# and have made all the proper indexes and such. If not, use the #create_queue method instead.
|
50
|
+
def self.queue=(val)
|
51
|
+
@queue = val
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creates a new capped collection with the given name and cap size, and sets the indexes needed
|
55
|
+
# for efficient Candygram delivery.
|
56
|
+
def self.create_queue(name=DEFAULT_QUEUE, size=DEFAULT_QUEUE_SIZE)
|
57
|
+
@queue = db.create_collection(name, :capped => true, :size => size)
|
58
|
+
# Make indexes here...
|
59
|
+
@queue.create_index('deliver_at')
|
60
|
+
@queue.create_index('locked')
|
61
|
+
@queue.create_index('result')
|
62
|
+
@queue
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'candygram/connection'
|
2
|
+
require 'candygram/wrapper'
|
3
|
+
|
4
|
+
module Candygram
|
5
|
+
# The special sauce that allows an object to place its method calls into the job queue.
|
6
|
+
module Delivery
|
7
|
+
|
8
|
+
# Lazily adds magic Candygram delivery methods to the class.
|
9
|
+
def method_missing(name, *args)
|
10
|
+
if name =~ /(\S+)_later$/
|
11
|
+
self.class.class_eval <<-LATER
|
12
|
+
def #{name}(*args)
|
13
|
+
send_candygram("#{$1}", *args)
|
14
|
+
end
|
15
|
+
LATER
|
16
|
+
send(name, *args)
|
17
|
+
else
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
# Does the tricky work of adding the method call to the MongoDB queue. Dollars to donuts that
|
24
|
+
# this method name doesn't conflict with anything you're already using...
|
25
|
+
def send_candygram(method, *args)
|
26
|
+
gram = {
|
27
|
+
:class => self.class.name,
|
28
|
+
:package => Wrapper.wrap_object(self),
|
29
|
+
:method => method,
|
30
|
+
:arguments => Wrapper.wrap_array(args),
|
31
|
+
:created_at => Time.now.utc,
|
32
|
+
:deliver_at => Time.now.utc
|
33
|
+
}
|
34
|
+
Candygram.queue << gram
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'candygram/connection'
|
2
|
+
require 'candygram/wrapper'
|
3
|
+
require 'candygram/utility'
|
4
|
+
|
5
|
+
module Candygram
|
6
|
+
# Pays attention to the Candygram work queue and forks runners to do the work as needed.
|
7
|
+
class Dispatch
|
8
|
+
include Utility
|
9
|
+
|
10
|
+
attr_accessor :frequency, :quiet, :max_per_class
|
11
|
+
attr_reader :runners
|
12
|
+
|
13
|
+
# Returns a Dispatch object that will keep checking the Candygram work queue and forking runners.
|
14
|
+
# @option options [Integer] :frequency How often to check the queue (in seconds). Defaults to 5.
|
15
|
+
def initialize(options={})
|
16
|
+
@frequency = options.delete(:frequency) || 5
|
17
|
+
@max_per_class = options.delete(:max_per_class) || 10
|
18
|
+
@quiet = options.delete(:quiet)
|
19
|
+
@runners = {}
|
20
|
+
@index = {}
|
21
|
+
end
|
22
|
+
|
23
|
+
# Loops over the work queue. You can stop it any time with the #finish method if running in a
|
24
|
+
# separate thread.
|
25
|
+
def run
|
26
|
+
Kernel.trap("CLD") do
|
27
|
+
pid = Process.wait
|
28
|
+
remove_runner(pid)
|
29
|
+
end
|
30
|
+
|
31
|
+
until @finish
|
32
|
+
deliveries = check_queue
|
33
|
+
deliveries.each do |del|
|
34
|
+
if slot_open?(del) && lock_delivery(del)
|
35
|
+
puts "Delivering #{del["class"]}\##{del["method"]} at #{Time.now}" unless quiet
|
36
|
+
# Close our connection so that we don't get too many weird copies
|
37
|
+
Candygram.connection = nil
|
38
|
+
child = fork do
|
39
|
+
# We're the runner
|
40
|
+
set_status(del, 'running')
|
41
|
+
package = Wrapper.unwrap(del["package"])
|
42
|
+
args = Wrapper.unwrap(del["arguments"])
|
43
|
+
result = package.send(del["method"].to_sym, *args)
|
44
|
+
finish_delivery(del, result)
|
45
|
+
Candygram.connection = nil
|
46
|
+
exit
|
47
|
+
end
|
48
|
+
# We're the parent
|
49
|
+
add_runner del["class"], child
|
50
|
+
sleep(0.2) # Give connections time to wrap up
|
51
|
+
end
|
52
|
+
end
|
53
|
+
sleep frequency
|
54
|
+
end
|
55
|
+
until @index.empty?
|
56
|
+
sleep(0.1) # We trust our trap
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Tells the #run method to stop running. It's a simple loop condition, not preemptive, so if the
|
61
|
+
# dispatcher is sleeping you may have to wait up to _frequency_ seconds before it really ends.
|
62
|
+
def finish
|
63
|
+
@finish = true
|
64
|
+
end
|
65
|
+
|
66
|
+
# Pushes a new PID onto the 'runners' hash.
|
67
|
+
def add_runner(klass, pid)
|
68
|
+
@runners[klass] ||= []
|
69
|
+
@runners[klass] << pid
|
70
|
+
@index[pid] = klass
|
71
|
+
end
|
72
|
+
|
73
|
+
# Takes a PID off of the 'runners' hash.
|
74
|
+
def remove_runner(pid)
|
75
|
+
klass = @index.delete(pid)
|
76
|
+
@runners[klass].delete(pid)
|
77
|
+
end
|
78
|
+
|
79
|
+
protected
|
80
|
+
# Looks for new work to do
|
81
|
+
def check_queue
|
82
|
+
# The interesting options hash for our new work query
|
83
|
+
check = {
|
84
|
+
:deliver_at => {'$lte' => Time.now.utc},
|
85
|
+
:result => {'$exists' => false},
|
86
|
+
:locked => {'$exists' => false}
|
87
|
+
}
|
88
|
+
Candygram.queue.find(check).to_a
|
89
|
+
end
|
90
|
+
|
91
|
+
# Sets the 'locked' value of the job to prevent anyone else from taking it.
|
92
|
+
# Returns true on success, false on failure.
|
93
|
+
def lock_delivery(del)
|
94
|
+
r = Candygram.queue.update({'_id' => del['_id'], 'locked' => {'$exists' => false}}, # query
|
95
|
+
{'$set' => {'locked' => dispatch_id}}, # update
|
96
|
+
:safe => true)
|
97
|
+
update_succeeded?(r)
|
98
|
+
rescue Mongo::OperationFailure
|
99
|
+
false
|
100
|
+
end
|
101
|
+
|
102
|
+
# Removes the 'locked' value of the job.
|
103
|
+
def unlock_delivery(del)
|
104
|
+
Candygram.queue.update({'_id' => del['_id']}, {'$set' => {'locked' => nil}})
|
105
|
+
end
|
106
|
+
|
107
|
+
# Logs the result of the method call to the delivery. If the result is nil it will be logged
|
108
|
+
# as the word "nil".
|
109
|
+
def finish_delivery(del, result)
|
110
|
+
Candygram.queue.update({'_id' => del['_id']}, {'$set' => {'result' => (result.nil? ? 'nil' : Wrapper.wrap(result))}})
|
111
|
+
unlock_delivery(del)
|
112
|
+
set_status(del,'completed')
|
113
|
+
end
|
114
|
+
|
115
|
+
# Checks whether we've hit the maximum number of simultaneous runners for this particular class.
|
116
|
+
# Limits are determined by (in order of precedence):
|
117
|
+
# 1. The value of a CANDYGRAM_MAX constant set in the class being delivered;
|
118
|
+
# 2. The max_per_class attribute of the Dispatch object;
|
119
|
+
# 3. The generic default of 10.
|
120
|
+
def slot_open?(del)
|
121
|
+
klass = Kernel.const_get(del["class"])
|
122
|
+
if klass.const_defined?(:CANDYGRAM_MAX)
|
123
|
+
limit = klass::CANDYGRAM_MAX
|
124
|
+
else
|
125
|
+
limit = max_per_class
|
126
|
+
end
|
127
|
+
(@runners[del["class"]] ? @runners[del["class"]].length < limit : true)
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
# A unique identifier for this dispatcher. Includes local IP address and PID.
|
132
|
+
def dispatch_id
|
133
|
+
local_ip + '/' + Process.pid.to_s
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'socket'
|
2
|
+
|
3
|
+
module Candygram
|
4
|
+
|
5
|
+
# Various methods that may be useful to both delivery and dispatch.
|
6
|
+
module Utility
|
7
|
+
|
8
|
+
# Returns the IP address of this machine relative to the MongoDB server.
|
9
|
+
# From: http://coderrr.wordpress.com/2008/05/28/get-your-local-ip-address/
|
10
|
+
def local_ip
|
11
|
+
@local_ip or begin
|
12
|
+
orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true # turn off reverse DNS resolution temporarily
|
13
|
+
|
14
|
+
@local_ip = UDPSocket.open do |s|
|
15
|
+
s.connect Candygram.connection.host, 1
|
16
|
+
s.addr.last
|
17
|
+
end
|
18
|
+
ensure
|
19
|
+
Socket.do_not_reverse_lookup = orig
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Parses Mongo's somewhat inscrutable results from Collection.update when :safe => true
|
24
|
+
# and returns whether or not the value was updated. The basic output looks like this:
|
25
|
+
# [[{"err"=>nil, "updatedExisting"=>false, "n"=>0, "ok"=>1.0}], 1, 0]
|
26
|
+
def update_succeeded?(result)
|
27
|
+
result[0][0]["updatedExisting"]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Pushes a new status message onto the job. Includes an identifier for this process and a timestamp.
|
31
|
+
def set_status(del, state)
|
32
|
+
Candygram.queue.update({'_id' => del['_id']}, {'$push' => {'status' => status_hash(state)}})
|
33
|
+
end
|
34
|
+
|
35
|
+
protected
|
36
|
+
# Returns an embedded document suitable for pushing onto a delivery's status array.
|
37
|
+
def status_hash(state)
|
38
|
+
message = {
|
39
|
+
'ip' => local_ip,
|
40
|
+
'pid' => Process.pid,
|
41
|
+
'state' => state,
|
42
|
+
'at' => Time.now.utc
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'date' # Only so we know what one is. Argh.
|
2
|
+
|
3
|
+
module Candygram
|
4
|
+
# Utility methods to serialize and unserialize objects into BSON
|
5
|
+
module Wrapper
|
6
|
+
|
7
|
+
BSON_SAFE = [String,
|
8
|
+
NilClass,
|
9
|
+
TrueClass,
|
10
|
+
FalseClass,
|
11
|
+
Fixnum,
|
12
|
+
Float,
|
13
|
+
Time,
|
14
|
+
Regexp,
|
15
|
+
ByteBuffer,
|
16
|
+
Mongo::ObjectID,
|
17
|
+
Mongo::Code,
|
18
|
+
Mongo::DBRef]
|
19
|
+
|
20
|
+
# Makes an object safe for the sharp pointy edges of MongoDB. Types properly serialized
|
21
|
+
# by the BSON.serialize call get passed through unmolested; others are unpacked and their
|
22
|
+
# pieces individually shrink-wrapped.
|
23
|
+
def self.wrap(thing)
|
24
|
+
# Pass the simple cases through
|
25
|
+
return thing if BSON_SAFE.include?(thing.class)
|
26
|
+
case thing
|
27
|
+
when Symbol
|
28
|
+
wrap_symbol(thing)
|
29
|
+
when Array
|
30
|
+
wrap_array(thing)
|
31
|
+
when Hash
|
32
|
+
wrap_hash(thing)
|
33
|
+
when Numeric # The most obvious are in BSON_SAFE, but not all
|
34
|
+
thing
|
35
|
+
when Date
|
36
|
+
thing.to_time
|
37
|
+
else
|
38
|
+
wrap_object(thing) # Our catchall machinery
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Takes an array and returns the same array with unsafe objects wrapped
|
43
|
+
def self.wrap_array(array)
|
44
|
+
array.map {|element| wrap(element)}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Takes a hash and returns it with both keys and values wrapped
|
48
|
+
def self.wrap_hash(hash)
|
49
|
+
wrapped = {}
|
50
|
+
hash.each {|k, v| wrapped[wrap(k)] = wrap(v)}
|
51
|
+
wrapped
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns a string that's distinctive enough for us to unwrap later and produce the same symbol
|
55
|
+
def self.wrap_symbol(symbol)
|
56
|
+
"__sym_" + symbol.to_s
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns a nested hash containing the class and instance variables of the object. It's not the
|
60
|
+
# deepest we could ever go (it doesn't handle singleton methods, etc.) but it's a start.
|
61
|
+
def self.wrap_object(object)
|
62
|
+
wrapped = {"class" => object.class.name}
|
63
|
+
ivars = {}
|
64
|
+
object.instance_variables.each do |ivar|
|
65
|
+
# Different Ruby versions spit different things out for instance_variables. Annoying.
|
66
|
+
ivar_name = '@' + ivar.to_s.sub(/^@/,'')
|
67
|
+
ivars[ivar_name] = wrap(object.instance_variable_get(ivar_name))
|
68
|
+
end
|
69
|
+
wrapped["ivars"] = ivars unless ivars.empty?
|
70
|
+
{"__object_" => wrapped}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Undoes any complicated magic from the Wrapper.wrap method. Almost everything falls through
|
74
|
+
# untouched except for symbol strings and hashed objects.
|
75
|
+
def self.unwrap(thing)
|
76
|
+
case thing
|
77
|
+
when Hash
|
78
|
+
if thing["__object_"]
|
79
|
+
unwrap_object(thing)
|
80
|
+
else
|
81
|
+
unwrap_hash(thing)
|
82
|
+
end
|
83
|
+
when Array
|
84
|
+
thing.map {|element| unwrap(element)}
|
85
|
+
when /^__sym_(.+)/
|
86
|
+
$1.to_sym
|
87
|
+
else
|
88
|
+
thing
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Traverses the hash, unwrapping both keys and values. Returns the hash that results.
|
93
|
+
def self.unwrap_hash(hash)
|
94
|
+
unwrapped = {}
|
95
|
+
hash.each {|k,v| unwrapped[unwrap(k)] = unwrap(v)}
|
96
|
+
unwrapped
|
97
|
+
end
|
98
|
+
|
99
|
+
# Turns a hashed object back into an object of the stated class, setting any captured instance
|
100
|
+
# variables. The main limitation is that the object's class *must* respond to Class.new without
|
101
|
+
# any parameters; we will not attempt to guess at any complex initialization behavior.
|
102
|
+
def self.unwrap_object(hash)
|
103
|
+
if innards = hash["__object_"]
|
104
|
+
klass = Kernel.const_get(innards["class"])
|
105
|
+
object = klass.new
|
106
|
+
if innards["ivars"]
|
107
|
+
innards["ivars"].each do |name, value|
|
108
|
+
object.instance_variable_set(name, unwrap(value))
|
109
|
+
end
|
110
|
+
end
|
111
|
+
object
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Candygram do
|
4
|
+
it "creates a default connection" do
|
5
|
+
Candygram.connection.host.should == 'localhost'
|
6
|
+
Candygram.connection.port.should == 27017
|
7
|
+
end
|
8
|
+
|
9
|
+
it "accepts an explicit connection" do
|
10
|
+
this = Mongo::Connection.new 'db.mongohq.com' # Works as of time of writing
|
11
|
+
Candygram.connection = this
|
12
|
+
Candygram.connection.host.should == 'db.mongohq.com'
|
13
|
+
end
|
14
|
+
|
15
|
+
it "has a default database" do
|
16
|
+
Candygram.database.should == 'candygram_test'
|
17
|
+
end
|
18
|
+
|
19
|
+
it "accepts a new database name" do
|
20
|
+
Candygram.database = 'candygram_foo'
|
21
|
+
Candygram.db.name.should == 'candygram_foo'
|
22
|
+
end
|
23
|
+
|
24
|
+
it "accepts an actual DB object" do
|
25
|
+
d = Mongo::DB.new('candygram_bar', Candygram.connection)
|
26
|
+
Candygram.db = d
|
27
|
+
Candygram.database.should == 'candygram_bar'
|
28
|
+
end
|
29
|
+
|
30
|
+
it "creates a default queue" do
|
31
|
+
Candygram.queue.name.should == "candygram_queue"
|
32
|
+
Candygram.queue.options['capped'].should be_true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "accepts another collection for the queue" do
|
36
|
+
Candygram.queue = Candygram.db.create_collection('foo')
|
37
|
+
Candygram.queue.name.should == 'foo'
|
38
|
+
end
|
39
|
+
|
40
|
+
it "can create a collection for the queue" do
|
41
|
+
q = Candygram.create_queue('bar')
|
42
|
+
q.should be_a_kind_of(Mongo::Collection)
|
43
|
+
q.options['capped'].should be_true
|
44
|
+
end
|
45
|
+
|
46
|
+
after(:each) do # Clear state from our tests
|
47
|
+
Candygram.queue = nil
|
48
|
+
Candygram.db = nil
|
49
|
+
Candygram.connection = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Candygram::Delivery do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@this = Explosive.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "adds the method to the delivery queue" do
|
10
|
+
@this.kaboom_later
|
11
|
+
Candygram.queue.find(:class => /Explosive/, :method => "kaboom").count.should == 1
|
12
|
+
end
|
13
|
+
|
14
|
+
it "captures the arguments passed to the method" do
|
15
|
+
id = @this.repeated_kaboom_later('Venus', 15)
|
16
|
+
doc = Candygram.queue.find_one(id)
|
17
|
+
doc['arguments'].should == ['Venus', 15]
|
18
|
+
end
|
19
|
+
|
20
|
+
it "takes an object as an argument" do
|
21
|
+
m = Missile.new
|
22
|
+
id = @this.object_kaboom_later('Pluto', 6, m)
|
23
|
+
doc = Candygram.queue.find_one(id)
|
24
|
+
doc['arguments'][2].should be_a_kind_of(Hash)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "sets the time it was created" do
|
28
|
+
id = @this.kaboom_later
|
29
|
+
(Time.now.utc - Candygram.queue.find_one(id)['created_at']).should < 2
|
30
|
+
end
|
31
|
+
|
32
|
+
it "wraps itself up as its own package" do
|
33
|
+
@this.weight = 15
|
34
|
+
id = @this.kaboom_later
|
35
|
+
unwrap = Candygram::Wrapper.unwrap(Candygram.queue.find_one(id)['package'])
|
36
|
+
unwrap.should be_an(Explosive)
|
37
|
+
unwrap.weight.should == 15
|
38
|
+
end
|
39
|
+
|
40
|
+
describe "for _later" do
|
41
|
+
it "can queue a method call using _later" do
|
42
|
+
@this.kaboom_later.should_not be_nil
|
43
|
+
end
|
44
|
+
|
45
|
+
it "sets the time for delivery to now" do
|
46
|
+
id = @this.kaboom_later
|
47
|
+
(Time.now.utc - Candygram.queue.find_one(id)['deliver_at']).should < 2
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
module Candygram
|
4
|
+
describe Candygram::Dispatch do
|
5
|
+
before(:each) do
|
6
|
+
c = Mongo::Connection.new
|
7
|
+
d = Mongo::DB.new(DEFAULT_DATABASE, c)
|
8
|
+
@queue = d.collection('candygram_queue')
|
9
|
+
@dispatch = Dispatch.new(:frequency => 1)
|
10
|
+
@exp = Explosive.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def run_dispatch(cycles=1)
|
14
|
+
t = Thread.new do
|
15
|
+
@dispatch.run
|
16
|
+
end
|
17
|
+
sleep(cycles)
|
18
|
+
yield if block_given?
|
19
|
+
@dispatch.finish
|
20
|
+
t.join(20) or raise "Dispatch never completed!"
|
21
|
+
end
|
22
|
+
|
23
|
+
it "knows how often to check the database" do
|
24
|
+
d = Dispatch.new(:frequency => 5)
|
25
|
+
d.frequency.should == 5
|
26
|
+
end
|
27
|
+
|
28
|
+
it "runs until aborted" do
|
29
|
+
run_dispatch.should_not be_nil
|
30
|
+
end
|
31
|
+
|
32
|
+
it "checks the queue for new work on each cycle" do
|
33
|
+
Candygram.queue.expects(:find).times(2..3)
|
34
|
+
run_dispatch(2.5)
|
35
|
+
end
|
36
|
+
|
37
|
+
it "locks any jobs it finds" do
|
38
|
+
@exp.slow_kaboom_later
|
39
|
+
run_dispatch(2) do
|
40
|
+
@queue.find_one(:locked => {"$exists" => true}).should_not be_nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
it "forks a runner for its work" do
|
45
|
+
pid = Process.pid
|
46
|
+
@exp.slow_kaboom_later
|
47
|
+
run_dispatch(2) do
|
48
|
+
j = @queue.find_one(:locked => {"$exists" => true})
|
49
|
+
j["status"][0]["pid"].should_not == pid
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
it "keeps track of its runners" do
|
54
|
+
3.times { @exp.slow_kaboom_later }
|
55
|
+
run_dispatch(4) do
|
56
|
+
@dispatch.runners['Explosive'].length.should == 3
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it "runs the method" do
|
61
|
+
@exp.kaboom_later
|
62
|
+
run_dispatch
|
63
|
+
j = @queue.find_one('result' => {'$exists' => true})
|
64
|
+
j["result"].should == "An earth-shattering kaboom!"
|
65
|
+
end
|
66
|
+
|
67
|
+
it "unlocks the delivery upon completion" do
|
68
|
+
@exp.kaboom_later
|
69
|
+
run_dispatch
|
70
|
+
j = @queue.find_one('result' => {'$exists' => true})
|
71
|
+
j["locked"].should be_nil
|
72
|
+
end
|
73
|
+
|
74
|
+
it "clears the runner record when it's done working" do
|
75
|
+
3.times { @exp.kaboom_later }
|
76
|
+
run_dispatch
|
77
|
+
sleep(1)
|
78
|
+
@dispatch.runners['Explosive'].should be_empty
|
79
|
+
end
|
80
|
+
|
81
|
+
it "keeps track of maximum instances for each class" do
|
82
|
+
10.times { @exp.slow_kaboom_later }
|
83
|
+
run_dispatch(3) do
|
84
|
+
a = @queue.find(:locked => {"$exists" => false})
|
85
|
+
a.count.should == 5
|
86
|
+
sleep(10)
|
87
|
+
a = @queue.find(:locked => {"$exists" => false})
|
88
|
+
a.count.should == 0
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
module Candygram
|
4
|
+
describe Candygram::Wrapper do
|
5
|
+
describe "wrapping" do
|
6
|
+
|
7
|
+
it "can wrap an array of simple arguments" do
|
8
|
+
a = ["Hi", 1, nil, 17.536]
|
9
|
+
Wrapper.wrap_array(a).should == a
|
10
|
+
end
|
11
|
+
|
12
|
+
it "can wrap a string" do
|
13
|
+
Wrapper.wrap("Hi").should == "Hi"
|
14
|
+
end
|
15
|
+
|
16
|
+
it "can wrap nil" do
|
17
|
+
Wrapper.wrap(nil).should == nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it "can wrap true" do
|
21
|
+
Wrapper.wrap(true).should be_true
|
22
|
+
end
|
23
|
+
|
24
|
+
it "can wrap false" do
|
25
|
+
Wrapper.wrap(false).should be_false
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can wrap an integer" do
|
29
|
+
Wrapper.wrap(5).should == 5
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can wrap a float" do
|
33
|
+
Wrapper.wrap(17.950).should == 17.950
|
34
|
+
end
|
35
|
+
|
36
|
+
it "can wrap an already serialized bytestream" do
|
37
|
+
b = BSON.serialize(:foo => 'bar')
|
38
|
+
Wrapper.wrap(b).should == b
|
39
|
+
end
|
40
|
+
|
41
|
+
it "can wrap an ObjectID" do
|
42
|
+
i = Mongo::ObjectID.new
|
43
|
+
Wrapper.wrap(i).should == i
|
44
|
+
end
|
45
|
+
|
46
|
+
it "can wrap the time" do
|
47
|
+
t = Time.now
|
48
|
+
Wrapper.wrap(t).should == t
|
49
|
+
end
|
50
|
+
|
51
|
+
it "can wrap a regular expression" do
|
52
|
+
r = /ha(l+)eluja(h?)/i
|
53
|
+
Wrapper.wrap(r).should == r
|
54
|
+
end
|
55
|
+
|
56
|
+
it "can wrap a Mongo code object (if we ever need to)" do
|
57
|
+
c = Mongo::Code.new('5')
|
58
|
+
Wrapper.wrap(c).should == c
|
59
|
+
end
|
60
|
+
|
61
|
+
it "can wrap a Mongo DBRef (if we ever need to)" do
|
62
|
+
d = Mongo::DBRef.new('foo', Mongo::ObjectID.new)
|
63
|
+
Wrapper.wrap(d).should == d
|
64
|
+
end
|
65
|
+
|
66
|
+
it "can wrap a date as a time" do
|
67
|
+
d = Date.today
|
68
|
+
Wrapper.wrap(d).should == Date.today.to_time
|
69
|
+
end
|
70
|
+
|
71
|
+
it "can wrap other numeric types (which might throw exceptions later but oh well)" do
|
72
|
+
c = Complex(2, 5)
|
73
|
+
Wrapper.wrap(c).should == c
|
74
|
+
end
|
75
|
+
|
76
|
+
it "can wrap a symbol in a way that preserves its symbolic nature" do
|
77
|
+
Wrapper.wrap(:oldglory).should == "__sym_oldglory"
|
78
|
+
end
|
79
|
+
|
80
|
+
it "wraps an array recursively" do
|
81
|
+
a = [5, 'hi', [':symbol', 0], nil]
|
82
|
+
Wrapper.wrap(a).should == a
|
83
|
+
end
|
84
|
+
|
85
|
+
it "wraps a hash's keys" do
|
86
|
+
h = {"foo" => "bar", :yoo => "yar"}
|
87
|
+
Wrapper.wrap(h).keys.should == ["foo", "__sym_yoo"]
|
88
|
+
end
|
89
|
+
|
90
|
+
it "wraps a hash's values" do
|
91
|
+
h = {:foo => :bar, :yoo => [:yar, 5]}
|
92
|
+
Wrapper.wrap(h).values.should == ["__sym_bar", ["__sym_yar", 5]]
|
93
|
+
end
|
94
|
+
|
95
|
+
it "rejects procs"
|
96
|
+
|
97
|
+
describe "objects" do
|
98
|
+
before(:each) do
|
99
|
+
@missile = Missile.new
|
100
|
+
@missile.payload = "15 megatons"
|
101
|
+
@missile.rocket = [2, Object.new]
|
102
|
+
@this = Wrapper.wrap(@missile)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "returns a hash" do
|
106
|
+
@this.should be_a(Hash)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "keys the hash to be an object" do
|
110
|
+
@this.keys.should == ["__object_"]
|
111
|
+
end
|
112
|
+
|
113
|
+
it "knows the object's class" do
|
114
|
+
@this["__object_"]["class"].should == "Missile"
|
115
|
+
end
|
116
|
+
|
117
|
+
it "captures all the instance variables" do
|
118
|
+
ivars = @this["__object_"]["ivars"]
|
119
|
+
ivars.should have(2).elements
|
120
|
+
ivars["@payload"].should == "15 megatons"
|
121
|
+
ivars["@rocket"][1]["__object_"]["class"].should == "Object"
|
122
|
+
end
|
123
|
+
|
124
|
+
it "avoids circular dependencies"
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe "unwrapping" do
|
130
|
+
before(:each) do
|
131
|
+
@wrapped = {"__object_" => {
|
132
|
+
"class" => "Missile",
|
133
|
+
"ivars" => {
|
134
|
+
"@payload" => "6 kilotons",
|
135
|
+
"@rocket" => [1, {"__object_" => {
|
136
|
+
"class" => "Object"
|
137
|
+
}}]
|
138
|
+
}
|
139
|
+
}}
|
140
|
+
end
|
141
|
+
it "passes most things through untouched" do
|
142
|
+
Wrapper.unwrap(5).should == 5
|
143
|
+
end
|
144
|
+
|
145
|
+
it "turns symbolized strings back into symbols" do
|
146
|
+
Wrapper.unwrap("__sym_blah").should == :blah
|
147
|
+
end
|
148
|
+
|
149
|
+
it "turns hashed objects back into objects" do
|
150
|
+
obj = Wrapper.unwrap(@wrapped)
|
151
|
+
obj.should be_a(Missile)
|
152
|
+
obj.payload.should == "6 kilotons"
|
153
|
+
obj.rocket[0].should == 1
|
154
|
+
obj.rocket[1].should be_an(Object)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "traverses a hash and unwraps whatever it needs to" do
|
158
|
+
hash = {"__sym_foo" => "__sym_bar", "missile" => @wrapped}
|
159
|
+
unwrapped = Wrapper.unwrap(hash)
|
160
|
+
unwrapped[:foo].should == :bar
|
161
|
+
unwrapped["missile"].should be_a(Missile)
|
162
|
+
end
|
163
|
+
|
164
|
+
it "traverses an array and unwraps whatever it needs to" do
|
165
|
+
array = ["__sym_foo", 5, @wrapped, nil, "hi"]
|
166
|
+
unwrapped = Wrapper.unwrap(array)
|
167
|
+
unwrapped[0].should == :foo
|
168
|
+
unwrapped[1].should == 5
|
169
|
+
unwrapped[2].should be_a(Missile)
|
170
|
+
unwrapped[3].should be_nil
|
171
|
+
unwrapped[4].should == "hi"
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
|
177
|
+
end
|
178
|
+
end
|
data/spec/spec.opts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/spec/spec.watchr
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
# See: http://github.com/mynyml/watchr/
|
2
|
+
|
3
|
+
require 'redgreen'
|
4
|
+
require 'autowatchr'
|
5
|
+
|
6
|
+
Autowatchr.new(self) do |config|
|
7
|
+
config.test_dir = 'spec'
|
8
|
+
config.test_re = "^#{config.test_dir}/(.*)_spec\.rb$"
|
9
|
+
config.test_file = '%s_spec.rb'
|
10
|
+
end
|
11
|
+
# watch ( 'spec/.*_spec\.rb' ) { |spec| system("ruby #{spec[0]}")}
|
12
|
+
# watch ( 'lib/(.*).rb' ) { |lib| system("ruby spec/#{spec[1]}_spec.rb")}
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
require 'candygram'
|
4
|
+
require 'mocha'
|
5
|
+
require 'spec'
|
6
|
+
require 'spec/autorun'
|
7
|
+
|
8
|
+
# Override the default database so that we don't clobber any production queues by chance
|
9
|
+
Candygram.const_set(:DEFAULT_DATABASE, "candygram_test")
|
10
|
+
|
11
|
+
# Requires supporting files with custom matchers and macros, etc,
|
12
|
+
# in ./support/ and its subdirectories.
|
13
|
+
Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}
|
14
|
+
|
15
|
+
Spec::Runner.configure do |config|
|
16
|
+
config.mock_with :mocha
|
17
|
+
|
18
|
+
# "I say we take off and nuke the place from orbit. It's the only way to be sure."
|
19
|
+
config.after(:each) {Candygram.connection.drop_database(Candygram::DEFAULT_DATABASE)}
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# A test class that we can make deliveries with and easily check results.
|
2
|
+
class Explosive
|
3
|
+
include Candygram::Delivery
|
4
|
+
CANDYGRAM_MAX = 5
|
5
|
+
|
6
|
+
attr_accessor :weight
|
7
|
+
|
8
|
+
def kaboom
|
9
|
+
"An earth-shattering kaboom!"
|
10
|
+
end
|
11
|
+
|
12
|
+
def repeated_kaboom(planet, repeat)
|
13
|
+
"A #{planet}-shattering kaboom #{repeat} times!"
|
14
|
+
end
|
15
|
+
|
16
|
+
def object_kaboom(planet, repeat, object)
|
17
|
+
"A #{planet}-shattering kaboom #{repeat} times using a #{object.class.name}!"
|
18
|
+
end
|
19
|
+
|
20
|
+
# To test that locking is performed properly
|
21
|
+
def slow_kaboom
|
22
|
+
sleep(5)
|
23
|
+
"Waited 5 seconds... NOW we kaboom."
|
24
|
+
end
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: candygram
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stephen Eley
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-31 00:00:00 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: mongo
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0.18"
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: rspec
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.2.9
|
34
|
+
version:
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: yard
|
37
|
+
type: :development
|
38
|
+
version_requirement:
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 0.5.2
|
44
|
+
version:
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: mocha
|
47
|
+
type: :development
|
48
|
+
version_requirement:
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.9.7
|
54
|
+
version:
|
55
|
+
description: Candygram provides a job queue for the MongoDB document-oriented database, inspired by delayed_job and Resque. It uses the MongoMapper ORM and allows you to queue method calls for execution ASAP or at any time in the future.
|
56
|
+
email: sfeley@gmail.com
|
57
|
+
executables: []
|
58
|
+
|
59
|
+
extensions: []
|
60
|
+
|
61
|
+
extra_rdoc_files:
|
62
|
+
- LICENSE
|
63
|
+
- README.markdown
|
64
|
+
files:
|
65
|
+
- .document
|
66
|
+
- .gitignore
|
67
|
+
- LICENSE
|
68
|
+
- README.markdown
|
69
|
+
- Rakefile
|
70
|
+
- VERSION
|
71
|
+
- lib/candygram.rb
|
72
|
+
- lib/candygram/connection.rb
|
73
|
+
- lib/candygram/delivery.rb
|
74
|
+
- lib/candygram/dispatch.rb
|
75
|
+
- lib/candygram/utility.rb
|
76
|
+
- lib/candygram/wrapper.rb
|
77
|
+
- spec/candygram/connection_spec.rb
|
78
|
+
- spec/candygram/delivery_spec.rb
|
79
|
+
- spec/candygram/dispatch_spec.rb
|
80
|
+
- spec/candygram/wrapper_spec.rb
|
81
|
+
- spec/candygram_spec.rb
|
82
|
+
- spec/spec.opts
|
83
|
+
- spec/spec.watchr
|
84
|
+
- spec/spec_helper.rb
|
85
|
+
- spec/support/explosive.rb
|
86
|
+
- spec/support/missile.rb
|
87
|
+
has_rdoc: true
|
88
|
+
homepage: http://github.com/SFEley/candygram
|
89
|
+
licenses: []
|
90
|
+
|
91
|
+
post_install_message:
|
92
|
+
rdoc_options:
|
93
|
+
- --charset=UTF-8
|
94
|
+
require_paths:
|
95
|
+
- lib
|
96
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: "0"
|
101
|
+
version:
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: "0"
|
107
|
+
version:
|
108
|
+
requirements: []
|
109
|
+
|
110
|
+
rubyforge_project:
|
111
|
+
rubygems_version: 1.3.5
|
112
|
+
signing_key:
|
113
|
+
specification_version: 3
|
114
|
+
summary: Delayed job queueing for MongoMapper
|
115
|
+
test_files:
|
116
|
+
- spec/candygram/connection_spec.rb
|
117
|
+
- spec/candygram/delivery_spec.rb
|
118
|
+
- spec/candygram/dispatch_spec.rb
|
119
|
+
- spec/candygram/wrapper_spec.rb
|
120
|
+
- spec/candygram_spec.rb
|
121
|
+
- spec/spec_helper.rb
|
122
|
+
- spec/support/explosive.rb
|
123
|
+
- spec/support/missile.rb
|