cyclop 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +7 -0
- data/.rspec +2 -0
- data/Gemfile +3 -0
- data/README.md +88 -0
- data/Rakefile +59 -0
- data/bin/cyclop +81 -0
- data/cyclop.gemspec +29 -0
- data/lib/cyclop/action.rb +34 -0
- data/lib/cyclop/job.rb +211 -0
- data/lib/cyclop/version.rb +3 -0
- data/lib/cyclop/worker.rb +128 -0
- data/lib/cyclop.rb +131 -0
- data/spec/cyclop/action_spec.rb +46 -0
- data/spec/cyclop/job_spec.rb +273 -0
- data/spec/cyclop/worker_spec.rb +78 -0
- data/spec/cyclop_spec.rb +39 -0
- data/spec/fixtures/actions/email.rb +14 -0
- data/spec/spec_helper.rb +15 -0
- metadata +141 -0
data/lib/cyclop.rb
ADDED
@@ -0,0 +1,131 @@
|
|
1
|
+
require "mongo"
|
2
|
+
require "logger"
|
3
|
+
require "socket"
|
4
|
+
|
5
|
+
require "cyclop/job"
|
6
|
+
require "cyclop/worker"
|
7
|
+
require "cyclop/action"
|
8
|
+
require "cyclop/version"
|
9
|
+
|
10
|
+
module Cyclop
|
11
|
+
extend self
|
12
|
+
|
13
|
+
# Raised if db not set or connection error
|
14
|
+
class DatabaseNotAvailable < StandardError; end
|
15
|
+
# Raised if two actions share the same queue
|
16
|
+
class ActionQueueClash < StandardError; end
|
17
|
+
# Raised if no action has been found
|
18
|
+
class NoActionFound < StandardError; end
|
19
|
+
|
20
|
+
# Set which `Mongo::DB` to use
|
21
|
+
def db=(db)
|
22
|
+
@db = db
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get `Mongo::DB` to use
|
26
|
+
def db
|
27
|
+
@db
|
28
|
+
end
|
29
|
+
|
30
|
+
# Get memoized host
|
31
|
+
def host
|
32
|
+
@host ||= Socket.gethostname
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get a unique identifier for current process
|
36
|
+
def master_id
|
37
|
+
@master_id ||= "#{host}-#{Process.pid}-#{Thread.current}"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Queues a new job
|
41
|
+
#
|
42
|
+
# Minimum usage:
|
43
|
+
#
|
44
|
+
# Cyclop.push queue: "refresh_cache"
|
45
|
+
#
|
46
|
+
# With `:job_params`:
|
47
|
+
#
|
48
|
+
# # with an `Array`
|
49
|
+
# Cyclop.push queue: "email", job_params: ["1", :welcome]
|
50
|
+
#
|
51
|
+
# # with a `Hash`
|
52
|
+
# Cyclop.push queue: "email", job_params: {user_id: "1",
|
53
|
+
# type: "welcome"}
|
54
|
+
#
|
55
|
+
# With `:delay`:
|
56
|
+
#
|
57
|
+
# # Will not perform the task before a delay of 60 seconds
|
58
|
+
# Cyclop.push queue: "email", delay: 60
|
59
|
+
#
|
60
|
+
# With `:retries` and `:splay`:
|
61
|
+
#
|
62
|
+
# # Will mark the task as failed only after 3 retries
|
63
|
+
# Cyclop.push queue: "email", retries: 3
|
64
|
+
#
|
65
|
+
# # Will mark the task as failed only after 2 retries
|
66
|
+
# # and 30 seconds between each retry
|
67
|
+
# Cyclop.push queue: "email", retries: 2, splay: 30
|
68
|
+
#
|
69
|
+
# Parameters:
|
70
|
+
#
|
71
|
+
# * (Hash) opts (defaults to: {}) - a customizable set of options. The minimum required is :queue.
|
72
|
+
#
|
73
|
+
# Options Hash (opts):
|
74
|
+
#
|
75
|
+
# * (Symbol, String) :queue - name of the queue (required).
|
76
|
+
# * (Array, Hash) :job_params (nil) - parameters to send to the `Cyclop::Job#perform` method.
|
77
|
+
# * (Integer) :delay (0) - time to wait in `seconds` before the task should be performed.
|
78
|
+
# * (Integer) :retries (0) - number of retries before the `Cyclop::Job` is marked as failed.
|
79
|
+
# * (Integer) :splay (60) - time to wait in `seconds` between retry.
|
80
|
+
# * (String) :host (Cyclop.host) - host under which the `Cyclop::Job` should be added.
|
81
|
+
#
|
82
|
+
# Returns a `Cyclop::Job`
|
83
|
+
#
|
84
|
+
def push(opts={})
|
85
|
+
Cyclop::Job.create opts
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get a `Cyclop::Job` to process
|
89
|
+
#
|
90
|
+
# Parameters:
|
91
|
+
#
|
92
|
+
# * (Symbol, String) queues - list of queues to get a `Cyclop::Job` from. Defaults to all.
|
93
|
+
# * (Hash) opts (defaults to: {}) - a customizable set of options.
|
94
|
+
#
|
95
|
+
# Options Hash (opts):
|
96
|
+
#
|
97
|
+
# * (String) :host - limit to `Cyclop::Job`s queued by this host.
|
98
|
+
#
|
99
|
+
# Returns a `Cyclop::Job` or `nil` if nothing to process
|
100
|
+
#
|
101
|
+
def next(*args)
|
102
|
+
opts = extract_opts! args
|
103
|
+
Cyclop::Job.next({queues: args, locked_by: master_id}.merge opts)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Get failed `Cyclop::Job`s
|
107
|
+
#
|
108
|
+
# Parameters:
|
109
|
+
#
|
110
|
+
# * (Symbol, String) queues - list of queues to get a `Cyclop::Job` from. Defaults to all.
|
111
|
+
# * (Hash) opts (defaults to: {}) - a customizable set of options.
|
112
|
+
#
|
113
|
+
# Options Hash (opts):
|
114
|
+
#
|
115
|
+
# * (String) :host - limit to `Cyclop::Job`s queued by this host.
|
116
|
+
# * (Integer) :skip (0) - number of `Cyclop::Job`s to skip.
|
117
|
+
# * (Integer) :limit (nil) - maximum number of `Cyclop::Job`s to return.
|
118
|
+
#
|
119
|
+
# Returns an `Array` of failed `Cyclop::Job`
|
120
|
+
#
|
121
|
+
def failed(*args)
|
122
|
+
opts = extract_opts! args
|
123
|
+
Cyclop::Job.failed({queues: args}.merge opts)
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def extract_opts!(args)
|
129
|
+
(args.pop if args.last.is_a?(Hash)) || {}
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Cyclop::Action do
|
4
|
+
describe ".find_by_queue(queue)" do
|
5
|
+
context "with no subclass" do
|
6
|
+
it "raises an Cyclop::NoActionFound" do
|
7
|
+
lambda {
|
8
|
+
Cyclop::Action.find_by_queue("email")
|
9
|
+
}.should raise_error Cyclop::NoActionFound, "No action defined"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
context "with subclasses" do
|
13
|
+
before :all do
|
14
|
+
class EmailAction < Cyclop::Action
|
15
|
+
def self.queues
|
16
|
+
["email", "email_welcome"]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
class CacheAction < Cyclop::Action
|
20
|
+
def self.queues
|
21
|
+
["cache"]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
it "returns only action matching queue" do
|
26
|
+
Cyclop::Action.find_by_queue("email").should be EmailAction
|
27
|
+
end
|
28
|
+
it "returns nil if no action match queue" do
|
29
|
+
lambda {
|
30
|
+
Cyclop::Action.find_by_queue("compress")
|
31
|
+
}.should raise_error Cyclop::NoActionFound, 'No action found for "compress" queue. Valid queues: "email", "email_welcome", "cache"'
|
32
|
+
end
|
33
|
+
it "raises an error if two actions share the same queue" do
|
34
|
+
class EmailCacheAction < Cyclop::Action
|
35
|
+
def self.queues
|
36
|
+
["email", "cache"]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
lambda {
|
40
|
+
Cyclop::Action.find_by_queue "email"
|
41
|
+
}.should raise_error Cyclop::ActionQueueClash, '"email" queue belongs to multiple actions: EmailAction, EmailCacheAction'
|
42
|
+
Object.send :remove_const, :EmailCacheAction
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,273 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Cyclop::Job do
|
4
|
+
subject { Cyclop::Job.new queue: "demo" }
|
5
|
+
|
6
|
+
its(:queue){ should == "demo" }
|
7
|
+
its(:job_params){ should be_nil }
|
8
|
+
its(:delay){ should == 0 }
|
9
|
+
its(:delayed_until){ should be_nil }
|
10
|
+
its(:retries){ should == 0 }
|
11
|
+
its(:splay){ should == 60 }
|
12
|
+
its(:created_by){ should == Cyclop.host }
|
13
|
+
its(:created_at){ should be_nil }
|
14
|
+
its(:updated_at){ should be_nil }
|
15
|
+
its(:locked_by){ should be_nil }
|
16
|
+
its(:locked_at){ should be_nil }
|
17
|
+
its(:failed){ should be false }
|
18
|
+
its(:attempts){ should == 0 }
|
19
|
+
its(:errors){ should == [] }
|
20
|
+
|
21
|
+
describe "#save" do
|
22
|
+
it "raises a Cyclop::DatabaseNotAvailable if no db defined" do
|
23
|
+
old, Cyclop.db = Cyclop.db, nil
|
24
|
+
lambda {
|
25
|
+
Cyclop::Job.new(queue: "demo").save
|
26
|
+
}.should raise_error Cyclop::DatabaseNotAvailable
|
27
|
+
Cyclop.db = old
|
28
|
+
end
|
29
|
+
context "on create" do
|
30
|
+
it "saves the job" do
|
31
|
+
subject.save.should be_true
|
32
|
+
subject.should be_persisted
|
33
|
+
subject.reload.queue.should == "demo"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe ".create(opts={})" do
|
39
|
+
it do
|
40
|
+
lambda {
|
41
|
+
Cyclop::Job.create
|
42
|
+
}.should raise_error ArgumentError, ":queue is required"
|
43
|
+
end
|
44
|
+
it "returns a persisted job on success" do
|
45
|
+
job = Cyclop::Job.create queue: "demo"
|
46
|
+
job.should be_persisted
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe ".next(opts={})" do
|
51
|
+
it do
|
52
|
+
lambda {
|
53
|
+
Cyclop::Job.next
|
54
|
+
}.should raise_error ArgumentError, "locked_by is required"
|
55
|
+
end
|
56
|
+
context "with no job in queue" do
|
57
|
+
it "returns nil" do
|
58
|
+
Cyclop::Job.next(locked_by: "myid").should be_nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
context "with jobs in queue" do
|
62
|
+
let(:email_job_delayed) { Cyclop::Job.create queue: "email", delay: 3600 }
|
63
|
+
let(:email_job_failed) { Cyclop::Job.create queue: "email", failed: true }
|
64
|
+
let(:email_job_no_retry) { Cyclop::Job.create queue: "email", attempts: 1 }
|
65
|
+
let(:email_job_locked) { Cyclop::Job.create queue: "email", locked_at: Time.now.utc-10 }
|
66
|
+
let(:email_job_next) { Cyclop::Job.create queue: "email" }
|
67
|
+
let(:email_job_other_host) { Cyclop::Job.create queue: "email", created_by: "demo.local" }
|
68
|
+
let(:snail_mail_job_stalling) { Cyclop::Job.create queue: "snail mail", locked_at: Time.now.utc-3000 }
|
69
|
+
|
70
|
+
before do
|
71
|
+
email_job_delayed
|
72
|
+
email_job_failed
|
73
|
+
email_job_no_retry
|
74
|
+
email_job_next
|
75
|
+
email_job_other_host
|
76
|
+
snail_mail_job_stalling
|
77
|
+
end
|
78
|
+
|
79
|
+
it "returns the first job to process when called without queue" do
|
80
|
+
Cyclop::Job.next(locked_by: "myid").should == email_job_next
|
81
|
+
end
|
82
|
+
|
83
|
+
it "increments attempts" do
|
84
|
+
Cyclop::Job.next(locked_by: "myid").attempts.should == 1
|
85
|
+
end
|
86
|
+
|
87
|
+
it "locks to given id" do
|
88
|
+
Cyclop::Job.next(locked_by: "myid").locked_by.should == "myid"
|
89
|
+
end
|
90
|
+
|
91
|
+
it "sets locked_at" do
|
92
|
+
Cyclop::Job.next(locked_by: "myid").locked_at.should_not be_nil
|
93
|
+
end
|
94
|
+
|
95
|
+
it "returns the first job to process when called with :email queue" do
|
96
|
+
Cyclop::Job.next(queues: [:email], locked_by: "myid").should == email_job_next
|
97
|
+
end
|
98
|
+
|
99
|
+
it "returns the first job to process for a given host" do
|
100
|
+
Cyclop::Job.next(queues: [:email], locked_by: "myid", host: "demo.local").should == email_job_other_host
|
101
|
+
end
|
102
|
+
|
103
|
+
it "returns nil when called with :cache queue" do
|
104
|
+
Cyclop::Job.next(queues: [:cache], locked_by: "myid").should be_nil
|
105
|
+
end
|
106
|
+
|
107
|
+
it "unlock and returns job stalled for a long time" do
|
108
|
+
Cyclop::Job.next(queues: ["snail mail"], locked_by: "myid").should == snail_mail_job_stalling
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
describe ".failed(opts={})" do
|
113
|
+
context "with no job in queue" do
|
114
|
+
it "returns an empty array" do
|
115
|
+
Cyclop::Job.failed.should == []
|
116
|
+
end
|
117
|
+
end
|
118
|
+
context "with jobs in queue" do
|
119
|
+
let(:email_job_failed) { Cyclop::Job.create queue: "email", failed: true }
|
120
|
+
let(:cache_job_no_retry) { Cyclop::Job.create queue: "cache", attempts: 1 }
|
121
|
+
let(:cache_job_failed) { Cyclop::Job.create queue: "cache", failed: true }
|
122
|
+
let(:email_job_next) { Cyclop::Job.create queue: "email" }
|
123
|
+
|
124
|
+
before do
|
125
|
+
email_job_failed
|
126
|
+
cache_job_no_retry
|
127
|
+
cache_job_failed
|
128
|
+
email_job_next
|
129
|
+
end
|
130
|
+
|
131
|
+
it "returns all failed jobs when called without queue" do
|
132
|
+
Cyclop::Job.failed.should == [email_job_failed, cache_job_no_retry, cache_job_failed]
|
133
|
+
end
|
134
|
+
|
135
|
+
it "returns all failed job to process when called with :email queue" do
|
136
|
+
Cyclop::Job.failed(queues: [:email]).should == [email_job_failed]
|
137
|
+
end
|
138
|
+
|
139
|
+
it "returns empty array when called with :snail_mail queue" do
|
140
|
+
Cyclop::Job.failed(queues: [:snail_mail]).should == []
|
141
|
+
end
|
142
|
+
|
143
|
+
it "respect :skip and :limit options" do
|
144
|
+
Cyclop::Job.failed(skip: 1, limit: 1).should == [cache_job_no_retry]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
describe "#complete!" do
|
149
|
+
context "when locked by the same process" do
|
150
|
+
let(:email_job) { Cyclop::Job.create queue: "email", locked_by: Cyclop.master_id, locked_at: Time.now.utc }
|
151
|
+
it "removes the job" do
|
152
|
+
email_job.complete!
|
153
|
+
Cyclop::Job.find(email_job._id).should be_nil
|
154
|
+
end
|
155
|
+
end
|
156
|
+
context "when locked by another process" do
|
157
|
+
let(:email_job) { Cyclop::Job.create queue: "email", locked_by: "anotherid", locked_at: Time.now.utc }
|
158
|
+
it "keeps the job" do
|
159
|
+
email_job.complete!
|
160
|
+
Cyclop::Job.find(email_job._id).should == email_job
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
describe "#release!" do
|
165
|
+
context "without exception" do
|
166
|
+
context "when locked by the same process and no more retries to do" do
|
167
|
+
let(:email_job) { Cyclop::Job.create queue: "email", locked_by: Cyclop.master_id, locked_at: ::Time.at(Time.now.to_i).utc, attempts: 1 }
|
168
|
+
before :all do
|
169
|
+
email_job.release!
|
170
|
+
@reload = Cyclop::Job.find email_job._id
|
171
|
+
end
|
172
|
+
it "marks it as failed" do
|
173
|
+
@reload.failed.should be_true
|
174
|
+
end
|
175
|
+
it "keeps locked_at" do
|
176
|
+
@reload.locked_at.should == email_job.locked_at
|
177
|
+
end
|
178
|
+
it "keeps locked_by" do
|
179
|
+
@reload.locked_by.should == email_job.locked_by
|
180
|
+
end
|
181
|
+
end
|
182
|
+
context "when locked by the same process and more retries to do" do
|
183
|
+
let(:email_job) { Cyclop::Job.create queue: "email", locked_by: Cyclop.master_id, locked_at: ::Time.at(Time.now.to_i).utc, attempts: 1, retries: 1, splay: 1 }
|
184
|
+
before :all do
|
185
|
+
email_job.release!
|
186
|
+
@reload = Cyclop::Job.find email_job._id
|
187
|
+
end
|
188
|
+
it "marks it as failed" do
|
189
|
+
@reload.failed.should be_false
|
190
|
+
end
|
191
|
+
it "clears locked_at" do
|
192
|
+
@reload.locked_at.should be_nil
|
193
|
+
end
|
194
|
+
it "clears locked_by" do
|
195
|
+
@reload.locked_by.should be_nil
|
196
|
+
end
|
197
|
+
it "sets delayed_until based on splay" do
|
198
|
+
@reload.delayed_until.should == email_job.locked_at+1
|
199
|
+
end
|
200
|
+
end
|
201
|
+
context "when locked by another process and no more retries to do" do
|
202
|
+
let(:email_job) { Cyclop::Job.create queue: "email", locked_by: "anotherid", locked_at: Time.now.utc, attempts: 1 }
|
203
|
+
it "doesn't mark it as failed" do
|
204
|
+
email_job.release!
|
205
|
+
Cyclop::Job.find(email_job._id).failed.should be_false
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
context "with exception" do
|
210
|
+
let(:exception) { mock :class => Exception, :message => "Soft fail", :backtrace => "backtrace" }
|
211
|
+
before do
|
212
|
+
email_job.release! exception
|
213
|
+
@reload = Cyclop::Job.find email_job._id
|
214
|
+
end
|
215
|
+
context "when locked by the same process and no more retries to do" do
|
216
|
+
let(:email_job) { Cyclop::Job.create queue: "email", locked_by: Cyclop.master_id, locked_at: ::Time.at(Time.now.to_i).utc, attempts: 1 }
|
217
|
+
it "marks it as failed" do
|
218
|
+
@reload.failed.should be_true
|
219
|
+
end
|
220
|
+
it "keeps locked_at" do
|
221
|
+
@reload.locked_at.should == email_job.locked_at
|
222
|
+
end
|
223
|
+
it "keeps locked_by" do
|
224
|
+
@reload.locked_by.should == email_job.locked_by
|
225
|
+
end
|
226
|
+
it "has recorded the error" do
|
227
|
+
@reload.errors.should have(1).item
|
228
|
+
error = @reload.errors.first
|
229
|
+
error["locked_by"].should == email_job.locked_by
|
230
|
+
error["locked_at"].should == email_job.locked_at
|
231
|
+
error["class"].should == "Exception"
|
232
|
+
error["message"].should == "Soft fail"
|
233
|
+
error["backtrace"].should == "backtrace"
|
234
|
+
error["created_at"].should_not be_nil
|
235
|
+
end
|
236
|
+
end
|
237
|
+
context "when locked by the same process and more retries to do" do
|
238
|
+
let(:email_job) { Cyclop::Job.create queue: "email", locked_by: Cyclop.master_id, locked_at: ::Time.at(Time.now.to_i).utc, attempts: 1, retries: 2, splay: 1 }
|
239
|
+
it "marks it as failed" do
|
240
|
+
@reload.failed.should be_false
|
241
|
+
end
|
242
|
+
it "clears locked_at" do
|
243
|
+
@reload.locked_at.should be_nil
|
244
|
+
end
|
245
|
+
it "clears locked_by" do
|
246
|
+
@reload.locked_by.should be_nil
|
247
|
+
end
|
248
|
+
it "sets delayed_until based on splay" do
|
249
|
+
@reload.delayed_until.should == email_job.locked_at+1
|
250
|
+
end
|
251
|
+
it "has recorded the error" do
|
252
|
+
@reload.errors.should have(1).item
|
253
|
+
error = @reload.errors.first
|
254
|
+
error["locked_by"].should == email_job.locked_by
|
255
|
+
error["locked_at"].should == email_job.locked_at
|
256
|
+
error["class"].should == "Exception"
|
257
|
+
error["message"].should == "Soft fail"
|
258
|
+
error["backtrace"].should == "backtrace"
|
259
|
+
error["created_at"].should_not be_nil
|
260
|
+
end
|
261
|
+
end
|
262
|
+
context "when locked by another process and no more retries to do" do
|
263
|
+
let(:email_job) { Cyclop::Job.create queue: "email", locked_by: "anotherid", locked_at: Time.now.utc, attempts: 1 }
|
264
|
+
it "doesn't mark it as failed" do
|
265
|
+
@reload.failed.should be_false
|
266
|
+
end
|
267
|
+
it "doesn't add error" do
|
268
|
+
@reload.errors.should be_empty
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Cyclop::Worker do
|
4
|
+
subject { Cyclop::Worker.new({"mongo" => {"database" => "cyclop_test"}}) }
|
5
|
+
|
6
|
+
its(:queues){ should be_empty }
|
7
|
+
its(:logger){ should_not be_nil }
|
8
|
+
its(:sleep_interval){ should == 1 }
|
9
|
+
its(:actions){ should == "./actions" }
|
10
|
+
|
11
|
+
it "raise ArgumentError without mongo['database']" do
|
12
|
+
lambda {
|
13
|
+
Cyclop::Worker.new
|
14
|
+
}.should raise_error ArgumentError, 'mongo["database"] is required'
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "#perform" do
|
18
|
+
let(:worker) do
|
19
|
+
Cyclop::Worker.new({
|
20
|
+
"mongo" => {"database" => "cyclop_test"},
|
21
|
+
"actions" => File.expand_path("../../fixtures/actions", __FILE__),
|
22
|
+
})
|
23
|
+
end
|
24
|
+
let(:job) { Cyclop.push queue: "slow", job_params: ["tony@starkenterprises.com", :welcome] }
|
25
|
+
it "loads actions files" do
|
26
|
+
lambda { Cyclop::Spec::Action::Email }.should raise_error NameError
|
27
|
+
worker.perform job
|
28
|
+
lambda { Cyclop::Spec::Action::Email }.should_not raise_error
|
29
|
+
end
|
30
|
+
it "calls perform on action class with specified params" do
|
31
|
+
require File.expand_path("../../fixtures/actions/email", __FILE__)
|
32
|
+
Cyclop::Spec::Action::Email.should_receive(:perform).with("tony@starkenterprises.com", :welcome)
|
33
|
+
worker.perform job
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#run" do
|
38
|
+
let(:worker) do
|
39
|
+
Cyclop::Worker.new({
|
40
|
+
"log_file" => File.expand_path("../../../test.log", __FILE__),
|
41
|
+
"mongo" => {"database" => "cyclop_test"},
|
42
|
+
"actions" => File.expand_path("../../fixtures/actions", __FILE__),
|
43
|
+
})
|
44
|
+
end
|
45
|
+
context "with successful action" do
|
46
|
+
it "remove the job" do
|
47
|
+
job = Cyclop.push queue: "slow", job_params: ["tony@starkenterprises.com", :welcome]
|
48
|
+
t = Thread.new { worker.run }
|
49
|
+
sleep 1
|
50
|
+
worker.stop
|
51
|
+
t.join
|
52
|
+
Cyclop::Job.find(job._id).should be_nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "with failing action" do
|
57
|
+
it "mark the job as failed" do
|
58
|
+
job = Cyclop.push queue: "slow", job_params: ["tony@starkenterprises.com"]
|
59
|
+
t = Thread.new { worker.run }
|
60
|
+
sleep 1
|
61
|
+
worker.stop
|
62
|
+
t.join
|
63
|
+
job.reload.failed.should be_true
|
64
|
+
end
|
65
|
+
|
66
|
+
it "mark the job as failed after retry" do
|
67
|
+
job = Cyclop.push queue: "slow", job_params: ["tony@starkenterprises.com"], retries: 1, splay: 0
|
68
|
+
t = Thread.new { worker.run }
|
69
|
+
sleep 1
|
70
|
+
worker.stop
|
71
|
+
t.join
|
72
|
+
job.reload
|
73
|
+
job.failed.should be_true
|
74
|
+
job.attempts.should == 2
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/spec/cyclop_spec.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Cyclop do
|
4
|
+
describe ".push(opts={})" do
|
5
|
+
it "creates a new Cyclop::Job initialized with opts" do
|
6
|
+
opts = {}
|
7
|
+
Cyclop::Job.should_receive :create, with: opts
|
8
|
+
Cyclop.push opts
|
9
|
+
end
|
10
|
+
end
|
11
|
+
describe ".next(*args)" do
|
12
|
+
it "extracts queues from args" do
|
13
|
+
Cyclop::Job.should_receive(:next).with(queues: [:email, "cache"], locked_by: Cyclop.send(:master_id))
|
14
|
+
Cyclop.next :email, "cache"
|
15
|
+
end
|
16
|
+
it "extracts options from args" do
|
17
|
+
Cyclop::Job.should_receive(:next).with(queues: [:email, "cache"], locked_by: "myid")
|
18
|
+
Cyclop.next :email, "cache", locked_by: "myid"
|
19
|
+
end
|
20
|
+
it "extracts options from args even without queues" do
|
21
|
+
Cyclop::Job.should_receive(:next).with(queues: [], locked_by: "myid")
|
22
|
+
Cyclop.next locked_by: "myid"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
describe ".failed(opts={})" do
|
26
|
+
it "extracts queues from args" do
|
27
|
+
Cyclop::Job.should_receive(:failed).with(queues: [:email, "cache"])
|
28
|
+
Cyclop.failed :email, "cache"
|
29
|
+
end
|
30
|
+
it "extracts options from args" do
|
31
|
+
Cyclop::Job.should_receive(:failed).with(queues: [:email, "cache"], limit: 10)
|
32
|
+
Cyclop.failed :email, "cache", limit: 10
|
33
|
+
end
|
34
|
+
it "extracts options from args even without queues" do
|
35
|
+
Cyclop::Job.should_receive(:failed).with(queues: [], limit: 10)
|
36
|
+
Cyclop.failed limit: 10
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "logger"
|
3
|
+
require "cyclop"
|
4
|
+
|
5
|
+
Dir[File.expand_path("../support/**/*.rb", __FILE__)].each{|f| require f}
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.before(:all) do
|
9
|
+
logger = Logger.new "test.log"
|
10
|
+
Cyclop.db = Mongo::Connection.new("localhost", nil, :logger => logger).db "cyclop_test"
|
11
|
+
end
|
12
|
+
config.before do
|
13
|
+
Cyclop.db.collections.each(&:remove)
|
14
|
+
end
|
15
|
+
end
|