massive 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +3 -0
  4. data/.rvmrc +1 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +19 -0
  7. data/Gemfile.lock +141 -0
  8. data/Guardfile +9 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +196 -0
  11. data/Rakefile +8 -0
  12. data/lib/massive.rb +63 -0
  13. data/lib/massive/cancelling.rb +20 -0
  14. data/lib/massive/file.rb +80 -0
  15. data/lib/massive/file_job.rb +9 -0
  16. data/lib/massive/file_process.rb +7 -0
  17. data/lib/massive/file_step.rb +7 -0
  18. data/lib/massive/job.rb +115 -0
  19. data/lib/massive/locking.rb +27 -0
  20. data/lib/massive/memory_consumption.rb +15 -0
  21. data/lib/massive/notifications.rb +40 -0
  22. data/lib/massive/notifiers.rb +6 -0
  23. data/lib/massive/notifiers/base.rb +32 -0
  24. data/lib/massive/notifiers/pusher.rb +17 -0
  25. data/lib/massive/process.rb +69 -0
  26. data/lib/massive/process_serializer.rb +12 -0
  27. data/lib/massive/retry.rb +49 -0
  28. data/lib/massive/status.rb +59 -0
  29. data/lib/massive/step.rb +143 -0
  30. data/lib/massive/step_serializer.rb +12 -0
  31. data/lib/massive/timing_support.rb +10 -0
  32. data/lib/massive/version.rb +3 -0
  33. data/massive.gemspec +23 -0
  34. data/spec/fixtures/custom_job.rb +4 -0
  35. data/spec/fixtures/custom_step.rb +19 -0
  36. data/spec/models/massive/cancelling_spec.rb +83 -0
  37. data/spec/models/massive/file_job_spec.rb +24 -0
  38. data/spec/models/massive/file_spec.rb +209 -0
  39. data/spec/models/massive/file_step_spec.rb +22 -0
  40. data/spec/models/massive/job_spec.rb +319 -0
  41. data/spec/models/massive/locking_spec.rb +52 -0
  42. data/spec/models/massive/memory_consumption_spec.rb +24 -0
  43. data/spec/models/massive/notifications_spec.rb +107 -0
  44. data/spec/models/massive/notifiers/base_spec.rb +48 -0
  45. data/spec/models/massive/notifiers/pusher_spec.rb +49 -0
  46. data/spec/models/massive/process_serializer_spec.rb +38 -0
  47. data/spec/models/massive/process_spec.rb +235 -0
  48. data/spec/models/massive/status_spec.rb +104 -0
  49. data/spec/models/massive/step_serializer_spec.rb +40 -0
  50. data/spec/models/massive/step_spec.rb +490 -0
  51. data/spec/models/massive/timing_support_spec.rb +55 -0
  52. data/spec/shared/step_context.rb +25 -0
  53. data/spec/spec_helper.rb +42 -0
  54. data/spec/support/mongoid.yml +78 -0
  55. metadata +175 -0
@@ -0,0 +1,52 @@
1
+ require "spec_helper"
2
+
3
+ shared_examples_for Massive::Locking do
4
+ let(:redis) { Resque.redis }
5
+ let(:key) { :some_key }
6
+
7
+ context "when there is a lock for the given key" do
8
+ let(:lock_key) { subject.send(:lock_key_for, key) }
9
+ before { redis.set(lock_key, 60) }
10
+
11
+ it { should be_locked(key) }
12
+
13
+ it "does not sets the an expiration for the key" do
14
+ redis.should_not_receive(:pexpire)
15
+ subject.locked?(key)
16
+ end
17
+ end
18
+
19
+ context "when there is no lock for the given key" do
20
+ let(:lock_key) { subject.send(:lock_key_for, key) }
21
+
22
+ it { should_not be_locked(key) }
23
+
24
+ context "and an expiration is not given for the locked key" do
25
+ it "sets the expiration to 60 seconds, specifying in miliseconds" do
26
+ redis.should_receive(:pexpire).with(lock_key, 60 * 1000)
27
+ subject.locked?(key)
28
+ end
29
+ end
30
+
31
+ context "and an expiration is given for the locked key" do
32
+ it "sets the expiration to this value" do
33
+ redis.should_receive(:pexpire).with(lock_key, 10)
34
+ subject.locked?(key, 10)
35
+ end
36
+ end
37
+
38
+ context "when pexpire command is not supported" do
39
+ let(:error) { Redis::CommandError.new('not supported') }
40
+ before { redis.stub(:pexpire).and_raise(error) }
41
+
42
+ it "should set expiration using expire command, dividing expiration per 1000 and rounding" do
43
+ redis.should_receive(:expire).with(lock_key, (1500/1000).to_i)
44
+ subject.locked?(key, 1500)
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ describe Massive::Step do
51
+ it_should_behave_like Massive::Locking
52
+ end
@@ -0,0 +1,24 @@
1
+ require "spec_helper"
2
+
3
+ shared_examples_for Massive::MemoryConsumption do
4
+ let(:memory) { 123456 }
5
+ let(:io) { double(IO, gets: " #{memory} ") }
6
+ before { IO.stub(:popen).with("ps -o rss= -p #{Process.pid}").and_yield(io) }
7
+
8
+ its(:current_memory_consumption) { should eq(memory) }
9
+
10
+ context "and an error is raised" do
11
+ let(:error) { StandardError.new('some error') }
12
+ before { IO.stub(:popen).with("ps -o rss= -p #{Process.pid}").and_raise(error) }
13
+
14
+ its(:current_memory_consumption) { should be_zero }
15
+ end
16
+ end
17
+
18
+ describe Massive::Step do
19
+ it_should_behave_like Massive::MemoryConsumption
20
+ end
21
+
22
+ describe Massive::Job do
23
+ it_should_behave_like Massive::MemoryConsumption
24
+ end
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples_for Massive::Notifications do
4
+ its(:notifier_id) { should eq("#{described_class.name.underscore.gsub('/', '-')}-#{notifyable.id}") }
5
+
6
+ after do
7
+ described_class.notifier(:base, {}) # resetting notifier
8
+ end
9
+
10
+ describe "#notify(message)" do
11
+ let(:message) { :some_message }
12
+ let(:serializer) { notifyable.active_model_serializer.new(notifyable) }
13
+
14
+ it "notifies the message" do
15
+ notifyable.notifier.should_receive(:notify).with(message)
16
+ notifyable.notify(message)
17
+ end
18
+
19
+ it "sends a serialized version of itself, after reloading itself, as data" do
20
+ notifyable.save
21
+ notifyable.notify(message)
22
+ notifyable.notifier.last[:data].as_json.should eq(serializer.as_json)
23
+ end
24
+
25
+ context "when there is no serializer for the step" do
26
+ before { notifyable.stub(:active_model_serializer).and_return(nil) }
27
+
28
+ it "does not sends the notification" do
29
+ notifyable.notifier.should_not_receive(:notify)
30
+ notifyable.notify(message)
31
+ end
32
+ end
33
+ end
34
+
35
+ describe "#notifier" do
36
+ let(:options) { { expiration: 200, foo: 'bar', other: 'yup' } }
37
+
38
+ it "returns an instance of the notifier" do
39
+ notifyable.notifier.should be_a(Massive::Notifiers::Base)
40
+ end
41
+
42
+ it "passes notifier options when creating the notifier" do
43
+ described_class.notifier :base, options
44
+ notifyable.notifier.options.should eq(options)
45
+ end
46
+
47
+ it "creates it with the notifier_id" do
48
+ notifyable.notifier.id.should eq(notifyable.notifier_id)
49
+ end
50
+ end
51
+
52
+ describe ".notifier" do
53
+ context "when a parameter is given" do
54
+ context "as a symbol" do
55
+ it "returns a notifier class from Massive::Notifiers::<given_symbol.camelized>" do
56
+ described_class.notifier :pusher
57
+ described_class.notifier_class.should eq(Massive::Notifiers::Pusher)
58
+ end
59
+
60
+ context "and others parameters are given" do
61
+ let(:options) { { expiration: 200, foo: 'bar', other: 'yup' } }
62
+
63
+ it "store these parameters to be used when creating the notifier" do
64
+ described_class.notifier :pusher, options
65
+ described_class.notifier_options.should eq(options)
66
+ end
67
+ end
68
+ end
69
+
70
+ context "as a Class" do
71
+ it "configures the notifier, using the symbol to get the class" do
72
+ described_class.notifier(Massive::Notifiers::Pusher)
73
+ described_class.notifier_class.should eq(Massive::Notifiers::Pusher)
74
+ end
75
+
76
+ context "and others parameters are given" do
77
+ let(:options) { { expiration: 200, foo: 'bar', other: 'yup' } }
78
+
79
+ it "passes these parameters when creating the notifier" do
80
+ described_class.notifier(Massive::Notifiers::Pusher, options)
81
+ described_class.notifier_options.should eq(options)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ describe Massive::Step do
90
+ let(:process) { Massive::Process.new }
91
+ subject(:notifyable) { process.steps.build }
92
+
93
+ it_should_behave_like Massive::Notifications
94
+ end
95
+
96
+ describe Massive::Job do
97
+ let(:process) { Massive::Process.new }
98
+ let(:step) { process.steps.build }
99
+ subject(:job) { step.jobs.build }
100
+
101
+ let(:message) { 'some message' }
102
+
103
+ it "delegates #notify to the step" do
104
+ step.should_receive(:notify).with(message)
105
+ job.notify(message)
106
+ end
107
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe Massive::Notifiers::Base do
4
+ let(:id) { 'some-id' }
5
+ subject(:notifier) { Massive::Notifiers::Base.new(id) }
6
+
7
+ it_should_behave_like Massive::Locking
8
+
9
+ describe "#notify(message, data)" do
10
+ let(:redis) { Resque.redis }
11
+
12
+ let(:message) { :some_message }
13
+ let(:data) { { some: 'data' } }
14
+
15
+ context "when a notification for this message is not locked" do
16
+ it "sends a notification" do
17
+ notifier.notify(message, data)
18
+ notifier.last[:message].should eq(message)
19
+ notifier.last[:data].should eq(data)
20
+ end
21
+
22
+ context "when a block is given" do
23
+ it "sends a notification with the data being the return from the block" do
24
+ notifier.notify(message) { data }
25
+ notifier.last[:message].should eq(message)
26
+ notifier.last[:data].should eq(data)
27
+ end
28
+ end
29
+ end
30
+
31
+ context "when a notification for this message is locked" do
32
+ let(:lock_key) { subject.send(:lock_key_for, message) }
33
+ before { redis.set(lock_key, 60) }
34
+
35
+ it "does not send a notification" do
36
+ notifier.notify(message, data)
37
+ notifier.last[:message].should be_nil
38
+ notifier.last[:data].should be_nil
39
+ end
40
+
41
+ context "when a block is given" do
42
+ it "does not execute the block" do
43
+ notifier.notify(message) { fail('should not execute this block') }
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,49 @@
1
+ require 'spec_helper'
2
+
3
+ describe Massive::Notifiers::Pusher do
4
+ let(:id) { 'pusher_notifier' }
5
+
6
+ let(:client) { double('Pusher') }
7
+ subject(:notifier) { Massive::Notifiers::Pusher.new(id, client: client) }
8
+
9
+ it_should_behave_like Massive::Locking
10
+
11
+ it { should be_a(Massive::Notifiers::Base) }
12
+
13
+ describe "#notify(message, data)" do
14
+ let(:redis) { Resque.redis }
15
+
16
+ let(:message) { :some_message }
17
+ let(:data) { { some: 'data' } }
18
+
19
+ context "when a notification for this message is not locked" do
20
+ it "sends a notification" do
21
+ client.should_receive(:trigger).with(id, message, data)
22
+ notifier.notify(message, data)
23
+ end
24
+
25
+ context "when a block is given" do
26
+ it "sends a notification with the data being the return from the block" do
27
+ client.should_receive(:trigger).with(id, message, data)
28
+ notifier.notify(message) { data }
29
+ end
30
+ end
31
+ end
32
+
33
+ context "when a notification for this message is locked" do
34
+ let(:lock_key) { subject.send(:lock_key_for, message) }
35
+ before { redis.set(lock_key, 60) }
36
+
37
+ it "does not send a notification" do
38
+ client.should_not_receive(:trigger)
39
+ notifier.notify(message, data)
40
+ end
41
+
42
+ context "when a block is given" do
43
+ it "does not execute the block" do
44
+ notifier.notify(message) { fail('should not execute this block') }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,38 @@
1
+ require "spec_helper"
2
+
3
+ describe Massive::ProcessSerializer do
4
+ let(:process) { Massive::Process.new }
5
+ subject(:serialized) { described_class.new(process).as_json(root: false) }
6
+
7
+ it "serializes process id as string" do
8
+ serialized[:id].should eq(process.id.to_s)
9
+ end
10
+
11
+ [:created_at, :updated_at].each do |field|
12
+ it "serializes the #{field}" do
13
+ process[field] = 1.minute.ago
14
+ serialized[field].should eq(process[field])
15
+ end
16
+ end
17
+
18
+ it "serializes the processed percentage" do
19
+ process.stub(:processed_percentage).and_return(12)
20
+ serialized[:processed_percentage].should eq(process.processed_percentage)
21
+ end
22
+
23
+ context "when it is completed" do
24
+ before { process.stub(:completed?).and_return(true) }
25
+
26
+ it "serializes completed" do
27
+ serialized[:completed].should be_true
28
+ end
29
+ end
30
+
31
+ context "when it is not completed" do
32
+ before { process.stub(:completed?).and_return(false) }
33
+
34
+ it "serializes completed" do
35
+ serialized[:completed].should be_false
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,235 @@
1
+ require "spec_helper"
2
+
3
+ describe Massive::Process do
4
+ subject(:process) { Massive::Process.new }
5
+
6
+ describe "#enqueue_next" do
7
+ context "when there are steps" do
8
+ let!(:first_step) { process.steps.build }
9
+ let!(:second_step) { process.steps.build }
10
+ let!(:third_step) { process.steps.build }
11
+
12
+ context "and none of them are completed" do
13
+ it "enqueues the first step" do
14
+ first_step.should_receive(:enqueue)
15
+ process.enqueue_next
16
+ end
17
+
18
+ it "does not enqueue the other steps" do
19
+ second_step.should_not_receive(:enqueue)
20
+ third_step.should_not_receive(:enqueue)
21
+ process.enqueue_next
22
+ end
23
+ end
24
+
25
+ context "and the first one is completed, but the second one is not" do
26
+ before { first_step.finished_at = Time.now }
27
+
28
+ it "does not enqueue the first step" do
29
+ first_step.should_not_receive(:enqueue)
30
+ process.enqueue_next
31
+ end
32
+
33
+ it "enqueues the second step" do
34
+ second_step.should_receive(:enqueue)
35
+ process.enqueue_next
36
+ end
37
+
38
+ it "does not enqueue the third step" do
39
+ third_step.should_not_receive(:enqueue)
40
+ process.enqueue_next
41
+ end
42
+ end
43
+
44
+ context "and the first one is enqueued" do
45
+ before { first_step.stub(:enqueued?).and_return(true) }
46
+
47
+ it "does not enqueue the next step" do
48
+ second_step.should_not_receive(:enqueue)
49
+ process.enqueue_next
50
+ end
51
+ end
52
+
53
+ context "but all of them are completed" do
54
+ before do
55
+ process.steps.each do |step|
56
+ step.finished_at = Time.now
57
+ end
58
+ end
59
+
60
+ it "does not enqueue any of the steps" do
61
+ process.steps.each do |step|
62
+ step.should_not_receive(:enqueue)
63
+ end
64
+
65
+ process.enqueue_next
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ describe "#next_step" do
72
+ let!(:step) { process.steps.build }
73
+
74
+ context "when the step is enqueued" do
75
+ before { step.stub(:enqueued?).and_return(true) }
76
+
77
+ its(:next_step) { should be_nil }
78
+ end
79
+
80
+ context "when the step is not enqueued" do
81
+ its(:next_step) { should eq step }
82
+ end
83
+ end
84
+
85
+ describe ".find_step" do
86
+ let!(:step) { process.steps.build }
87
+
88
+ before { process.save }
89
+
90
+ it "returns the step with id within the process" do
91
+ Massive::Process.find_step(process.id, step.id).should eq(step)
92
+ end
93
+ end
94
+
95
+ describe ".find_job" do
96
+ let!(:step) { process.steps.build }
97
+ let!(:job) { step.jobs.build }
98
+
99
+ before { process.save }
100
+
101
+ it "returns the job with id within the step of the process" do
102
+ Massive::Process.find_job(process.id, step.id, job.id).should eq(job)
103
+ end
104
+ end
105
+
106
+ describe "#processed_percentage" do
107
+ let(:step_1) { process.steps.build(weight: 9) }
108
+ let(:step_2) { process.steps.build }
109
+
110
+ context "when the process have not started" do
111
+ before do
112
+ step_1.stub(:processed_percentage).and_return(0)
113
+ step_2.stub(:processed_percentage).and_return(0)
114
+ end
115
+
116
+ its(:processed_percentage) { should eq 0 }
117
+ end
118
+
119
+ context "when the process have finished" do
120
+ before do
121
+ step_1.stub(:processed_percentage).and_return(1)
122
+ step_2.stub(:processed_percentage).and_return(1)
123
+ end
124
+
125
+ its(:processed_percentage) { should eq 1 }
126
+ end
127
+
128
+ context "when the file export step is finished" do
129
+ before { step_1.stub(:processed_percentage).and_return(1) }
130
+
131
+ context "and the file upload step is not finished" do
132
+ before { step_2.stub(:processed_percentage).and_return(0) }
133
+
134
+ its(:processed_percentage) { should eq 0.9 }
135
+ end
136
+ end
137
+
138
+ context "when the file export step is finished" do
139
+ before { step_1.stub(:processed_percentage).and_return(1) }
140
+
141
+ context "and the file upload step is finished" do
142
+ before { step_2.stub(:processed_percentage).and_return(1) }
143
+
144
+ its(:processed_percentage) { should eq 1 }
145
+ end
146
+ end
147
+
148
+ context "when the file export step is finished" do
149
+ before { step_1.stub(:processed_percentage).and_return(1) }
150
+
151
+ context "and the file upload step is half way to be finished" do
152
+ before { step_2.stub(:processed_percentage).and_return(0.5) }
153
+
154
+ its(:processed_percentage) { should eq 0.95 }
155
+ end
156
+ end
157
+
158
+ context "when the total weight of the steps is zero" do
159
+ let(:step_1) { process.steps.build(weight: 0) }
160
+ let(:step_2) { process.steps.build(weight: 0) }
161
+
162
+ its(:processed_percentage) { should eq 0 }
163
+ end
164
+ end
165
+
166
+ describe "#completed?" do
167
+ let!(:step_1) { process.steps.build }
168
+ let!(:step_2) { process.steps.build }
169
+
170
+ before { process.save }
171
+
172
+ context "when the steps are incompleted steps" do
173
+ its(:completed?) { should be_false }
174
+ end
175
+
176
+ context "when therere are no incompleted steps" do
177
+ before do
178
+ step_1.update_attributes(finished_at: Time.now, failed_at: nil)
179
+ step_2.update_attributes(finished_at: Time.now, failed_at: nil)
180
+ end
181
+
182
+ its(:completed?) { should be_true }
183
+ end
184
+ end
185
+
186
+ describe "#cancel" do
187
+ let!(:now) do
188
+ Time.now.tap do |now|
189
+ Time.stub(:now).and_return(now)
190
+ end
191
+ end
192
+
193
+ it "sets cancelled_at to the current time, persisting it" do
194
+ process.cancel
195
+ process.reload.cancelled_at.to_i.should eq(now.to_i)
196
+ end
197
+
198
+ it "sets a cancelled key in redis with the process id" do
199
+ process.cancel
200
+ Massive.redis.exists("#{process.class.name.underscore}:#{process.id}:cancelled").should be_true
201
+ end
202
+ end
203
+
204
+ describe "#canceled?" do
205
+ context "when it has a cancelled_at" do
206
+ before { process.cancelled_at = Time.now }
207
+
208
+ it { should be_cancelled }
209
+ end
210
+
211
+ context "when it doesn't have a cancelled_at" do
212
+ it { should_not be_cancelled }
213
+
214
+ context "but there is a cancelled key for this process in redis" do
215
+ before { Massive.redis.set("#{process.class.name.underscore}:#{process.id}:cancelled", true) }
216
+
217
+ it { should be_cancelled }
218
+ end
219
+ end
220
+ end
221
+
222
+ describe "#active_model_serializer" do
223
+ its(:active_model_serializer) { should eq Massive::ProcessSerializer }
224
+
225
+ context "when class inherits from Massive::Process and does not have a serializer" do
226
+ class TestProcess < Massive::Process
227
+ end
228
+
229
+ it "returns Massive::ProcessSerializer" do
230
+ process = TestProcess.new
231
+ process.active_model_serializer.should eq Massive::ProcessSerializer
232
+ end
233
+ end
234
+ end
235
+ end