massive 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.
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