rails-pipeline 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +227 -0
  4. data/Rakefile +27 -0
  5. data/bin/pipeline +138 -0
  6. data/bin/redis-to-ironmq.rb +20 -0
  7. data/lib/rails-pipeline.rb +34 -0
  8. data/lib/rails-pipeline/emitter.rb +121 -0
  9. data/lib/rails-pipeline/handlers/activerecord_crud.rb +35 -0
  10. data/lib/rails-pipeline/handlers/base_handler.rb +19 -0
  11. data/lib/rails-pipeline/handlers/logger.rb +13 -0
  12. data/lib/rails-pipeline/ironmq_publisher.rb +37 -0
  13. data/lib/rails-pipeline/ironmq_pulling_subscriber.rb +96 -0
  14. data/lib/rails-pipeline/ironmq_subscriber.rb +21 -0
  15. data/lib/rails-pipeline/pipeline_version.rb +40 -0
  16. data/lib/rails-pipeline/protobuf/encrypted_message.pb.rb +37 -0
  17. data/lib/rails-pipeline/protobuf/encrypted_message.proto +18 -0
  18. data/lib/rails-pipeline/redis_forwarder.rb +207 -0
  19. data/lib/rails-pipeline/redis_ironmq_forwarder.rb +12 -0
  20. data/lib/rails-pipeline/redis_publisher.rb +71 -0
  21. data/lib/rails-pipeline/sns_publisher.rb +62 -0
  22. data/lib/rails-pipeline/subscriber.rb +185 -0
  23. data/lib/rails-pipeline/symmetric_encryptor.rb +127 -0
  24. data/lib/rails-pipeline/version.rb +3 -0
  25. data/lib/tasks/rails-pipeline_tasks.rake +4 -0
  26. data/spec/emitter_spec.rb +141 -0
  27. data/spec/handlers/activerecord_crud_spec.rb +100 -0
  28. data/spec/handlers/logger_spec.rb +42 -0
  29. data/spec/ironmp_pulling_subscriber_spec.rb +98 -0
  30. data/spec/ironmq_publisher_spec.rb +37 -0
  31. data/spec/pipeline_version_spec.rb +35 -0
  32. data/spec/redis_forwarder_spec.rb +99 -0
  33. data/spec/redis_publisher_spec.rb +36 -0
  34. data/spec/sns_publisher_spec.rb +28 -0
  35. data/spec/subscriber_spec.rb +278 -0
  36. data/spec/symmetric_encryptor_spec.rb +21 -0
  37. metadata +175 -0
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require_relative 'pipeline_helper'
3
+
4
+ describe RailsPipeline::IronmqPublisher do
5
+ before do
6
+ @default_emitter = DefaultIronmqEmitter.new({foo: "baz"}, without_protection: true)
7
+ end
8
+
9
+ it "should call post for IronMQ" do
10
+ expect_any_instance_of(IronMQ::Queue).to receive(:post) { |instance, serialized_encrypted_data|
11
+ base64_decoded_data = Base64.strict_decode64(JSON.parse(serialized_encrypted_data)['payload'])
12
+
13
+ encrypted_data = RailsPipeline::EncryptedMessage.parse(base64_decoded_data)
14
+
15
+ serialized_payload = DefaultEmitter.decrypt(encrypted_data)
16
+ data = DefaultEmitter_1_0.parse(serialized_payload)
17
+
18
+ expect(instance.name).to eq("harrys-#{Rails.env}-v1-default_emitters")
19
+ expect(encrypted_data.type_info).to eq(DefaultEmitter_1_0.to_s)
20
+ expect(data.foo).to eq("baz")
21
+ }.once
22
+ @default_emitter.emit
23
+ end
24
+
25
+ it "should actually publish message to IronMQ" do
26
+ @default_emitter.emit
27
+ @default_emitter.emit
28
+ @default_emitter.emit
29
+ @default_emitter.emit
30
+ @default_emitter.emit
31
+ @default_emitter.emit
32
+ @default_emitter.emit
33
+ @default_emitter.emit
34
+ @default_emitter.emit
35
+ end
36
+
37
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe RailsPipeline::PipelineVersion do
4
+ context 'major' do
5
+ it "parses the string correctly" do
6
+ expect(RailsPipeline::PipelineVersion.new("1_0").major).to eq(1)
7
+ end
8
+ end
9
+
10
+ context 'minor' do
11
+ it "parses the string correctly" do
12
+ expect(RailsPipeline::PipelineVersion.new("1_3").minor).to eq(3)
13
+ end
14
+ end
15
+
16
+ describe 'comparison' do
17
+ let(:v1_0) { RailsPipeline::PipelineVersion.new('1_0') }
18
+ let(:v2_0) { RailsPipeline::PipelineVersion.new('2_0') }
19
+ let(:v1_3) { RailsPipeline::PipelineVersion.new('1_3') }
20
+ let(:v1_10) { RailsPipeline::PipelineVersion.new('1_10') }
21
+ let(:v1_10bis) { RailsPipeline::PipelineVersion.new('1_10') }
22
+
23
+ it { expect(v1_0).to be < v2_0 }
24
+ it { expect(v1_3).to be > v1_0 }
25
+ it { expect(v1_10).to be < v2_0 }
26
+ it { expect(v1_10).to be > v1_3 }
27
+ it { expect(v1_10).to eq(v1_10bis) }
28
+ end
29
+
30
+ describe '#to_s' do
31
+ it "renders correctly" do
32
+ expect("#{RailsPipeline::PipelineVersion.new('2_1')}").to eq("2_1")
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,99 @@
1
+
2
+ require 'spec_helper'
3
+ require 'pipeline_helper'
4
+ require 'forwarder_helper'
5
+
6
+ describe RailsPipeline::RedisForwarder do
7
+ before do
8
+ @emitter = DefaultRedisEmitter.new({foo: "baz"}, without_protection: true)
9
+ @redis_queue = @emitter._key
10
+ @forwarder = DummyRedisForwarder.new(@redis_queue)
11
+ @in_progress_queue = @forwarder._in_progress_queue
12
+ @redis = @emitter._redis
13
+ end
14
+
15
+
16
+ context "having one message on the queue" do
17
+ before do
18
+ @redis.del(@redis_queue)
19
+ @redis.del(@in_progress_queue)
20
+ expect(@redis.llen(@redis_queue)).to eq 0
21
+ @emitter.emit # emit a message
22
+
23
+ expect(@redis.llen(@redis_queue)).to eq 1
24
+ expect(@forwarder._processed).to eq 0
25
+ end
26
+
27
+ it "should re-publish messages off the queue" do
28
+
29
+ # Spy on the publish method
30
+ expect(@forwarder).to receive(:publish).once { |topic, data|
31
+ expect(topic).to eq "harrys-#{Rails.env}-v1-default_emitters"
32
+ expect(data).to include "DefaultEmitter_1_0"
33
+ }
34
+
35
+ @forwarder.process_queue
36
+
37
+ # We should have processed the one message
38
+ expect(@redis.llen(@redis_queue)).to eq 0
39
+ expect(@forwarder._processed).to eq 1
40
+
41
+ # Just check that we can handle an empty queue OK
42
+ @forwarder.process_queue
43
+ expect(@redis.llen(@redis_queue)).to eq 0
44
+ expect(@forwarder._processed).to eq 1
45
+ end
46
+
47
+ it "should have an in_progress message temporarily" do
48
+ # Inside the publish method, let's inspect the in_progress queue
49
+ expect(@forwarder).to receive(:publish).once { |topic, data|
50
+ expect(@redis.llen(@in_progress_queue)).to eq 1
51
+ expect(@redis.lrange(@in_progress_queue, 0, 1)[0]).to eq data
52
+ }
53
+ @forwarder.process_queue
54
+ expect(@redis.llen(@in_progress_queue)).to eq 0
55
+ end
56
+
57
+ it "should re-queue failed messages" do
58
+ # Check what happens when publish() raises an exception...
59
+ expect(@forwarder).to receive(:publish).once.and_raise("dummy publishing error")
60
+ @forwarder.process_queue # will fail and put it back on the queue
61
+ expect(@redis.llen(@redis_queue)).to eq 1
62
+ expect(@redis.llen(@in_progress_queue)).to eq 0
63
+ expect(@forwarder._processed).to eq 0
64
+
65
+ # Now process again with no errors...
66
+ expect(@forwarder).to receive(:publish).once
67
+ @forwarder.process_queue # will fail and put it back on the queue
68
+ expect(@redis.llen(@redis_queue)).to eq 0
69
+ expect(@forwarder._processed).to eq 1
70
+ end
71
+
72
+ context "with an abandoned message" do
73
+ before do
74
+ @redis.rpoplpush(@redis_queue, @in_progress_queue)
75
+ expect(@redis.llen(@redis_queue)).to eq 0
76
+ expect(@redis.llen(@in_progress_queue)).to eq 1
77
+ end
78
+
79
+ it "should re-queue timed-out in-progress messages" do
80
+ @forwarder.check_for_failures
81
+ expect(@redis.llen(@redis_queue)).to eq 1
82
+ expect(@redis.llen(@in_progress_queue)).to eq 0
83
+
84
+ end
85
+ end
86
+
87
+ it "should check the in-progress queue at the right times" do
88
+ now = Time.now
89
+ expect(@forwarder).to receive(:check_for_failures).once
90
+ expect(@forwarder).to receive(:process_queue).once.and_call_original
91
+ @forwarder.run
92
+ Timecop.freeze(now + 1.second) {
93
+ expect(@forwarder).not_to receive(:check_for_failures)
94
+ expect(@forwarder).to receive(:process_queue).once.and_call_original
95
+ @forwarder.run
96
+ }
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,36 @@
1
+
2
+ require 'spec_helper'
3
+ require_relative 'pipeline_helper'
4
+
5
+ describe RailsPipeline::RedisPublisher do
6
+ before do
7
+ @test_emitter = TestRedisEmitter.new({foo: "bar"}, without_protection: true)
8
+ @default_emitter = DefaultRedisEmitter.new({foo: "baz"}, without_protection: true)
9
+ end
10
+
11
+ it "should publish message to Redis" do
12
+ Redis.any_instance.should_receive(:lpush).once { |instance, key, serialized_encrypted_data|
13
+ key.should eql RailsPipeline::RedisPublisher.namespace
14
+ encrypted_data = RailsPipeline::EncryptedMessage.parse(serialized_encrypted_data)
15
+ expect(encrypted_data.type_info).to eq(DefaultEmitter_1_0.to_s)
16
+ serialized_payload = DefaultEmitter.decrypt(encrypted_data)
17
+ data = DefaultEmitter_1_0.parse(serialized_payload)
18
+ expect(data.foo).to eq("baz")
19
+ # message is encrypted, but we tested that in pipeline_emitter_spec
20
+ }
21
+ @default_emitter.emit
22
+ end
23
+
24
+ it "just print some timings" do
25
+ @default_emitter.emit
26
+ @default_emitter.emit
27
+ @default_emitter.emit
28
+ @default_emitter.emit
29
+ @default_emitter.emit
30
+ @default_emitter.emit
31
+ @default_emitter.emit
32
+ @default_emitter.emit
33
+ end
34
+
35
+
36
+ end
@@ -0,0 +1,28 @@
1
+ require 'spec_helper'
2
+ require_relative 'pipeline_helper'
3
+
4
+ describe RailsPipeline::SnsPublisher do
5
+ before do
6
+ @default_emitter = DefaultSnsEmitter.new({foo: "baz"}, without_protection: true)
7
+ end
8
+
9
+ it "should publish message to SNS" do
10
+ allow_any_instance_of(AWS::SNS::Topic).to receive(:publish) { |instance, message, options|
11
+ options[:subject].should eql "DefaultSnsEmitter-"
12
+ options[:sqs].should eql message
13
+ encrypted_data = RailsPipeline::EncryptedMessage.parse(message)
14
+ expect(encrypted_data.type_info).to eq(DefaultEmitter_1_0.to_s)
15
+ serialized_payload = DefaultEmitter.decrypt(encrypted_data)
16
+ data = DefaultEmitter_1_0.parse(serialized_payload)
17
+ expect(data.foo).to eq("baz")
18
+ # message is encrypted, but we tested that in pipeline_emitter_spec
19
+ }
20
+ @default_emitter.emit
21
+ end
22
+
23
+ # Skipped since I can't be bothered to set permissions in circle
24
+ skip "should actually send to sns" do
25
+ @default_emitter.emit
26
+ end
27
+
28
+ end
@@ -0,0 +1,278 @@
1
+
2
+ require 'spec_helper'
3
+ require 'pipeline_helper'
4
+
5
+ describe RailsPipeline::Subscriber do
6
+ before do
7
+ @test_emitter = TestEmitter.new({foo: "bar"}, without_protection: true)
8
+
9
+ @test_message = @test_emitter.create_message("2_0", RailsPipeline::EncryptedMessage::EventType::CREATED)
10
+ @subscriber = TestSubscriber.new
11
+ TestSubscriber.handler_method_cache = {}
12
+ OtherSubscriber.handler_method_cache = {}
13
+ end
14
+
15
+
16
+ context "when there is no compatible version registered for the message" do
17
+ before do
18
+ RailsPipeline::Subscriber.register(TestEmitter_2_0, TestModel)
19
+ end
20
+
21
+ it "should handle correct messages" do
22
+ expect(@subscriber).to receive(:handle_payload).once
23
+ @subscriber.handle_envelope(@test_message)
24
+ end
25
+ end
26
+
27
+ context "when there is a compatible version registered for the message" do
28
+ before do
29
+ @test_emitter = TestEmitter.new({foo: "bar"}, without_protection: true)
30
+ RailsPipeline::Subscriber.stub(:registered_handlers){{}}
31
+ end
32
+
33
+ it "should log the inability to process the message" do
34
+ expect(RailsPipeline.logger).to receive(:info).once
35
+ @subscriber.handle_envelope(@test_message)
36
+ end
37
+ end
38
+
39
+ it "should raise exception on malformed messages" do
40
+ @test_message = RailsPipeline::EncryptedMessage.new(salt: "jhkjehd", iv: "khdkjehdkejhdkjehdkjhed")
41
+ expect(@subscriber).not_to receive(:handle_payload)
42
+ expect{@subscriber.handle_envelope(@test_message)}.to raise_error
43
+ end
44
+
45
+ describe 'api_key' do
46
+ before do
47
+ @test_message = @test_emitter.create_message("2_0", RailsPipeline::EncryptedMessage::EventType::CREATED)
48
+ end
49
+
50
+ context 'with wrong api key' do
51
+ it "should drop messages" do
52
+ @test_message.api_key = '123XYZ'
53
+ expect{@subscriber.handle_envelope(@test_message)}.to raise_error(RailsPipeline::Subscriber::WrongApiKeyError)
54
+ end
55
+ end
56
+
57
+ context 'with no api key' do
58
+ it "should drop messages" do
59
+ @test_message.api_key = nil
60
+ expect{@subscriber.handle_envelope(@test_message)}.to raise_error(RailsPipeline::Subscriber::NoApiKeyError)
61
+ end
62
+ end
63
+
64
+ context 'with a correct api key' do
65
+ it "should accept the envelope with correct api_key" do
66
+ stub_const('ENV', {'PIPELINE_API_KEYS' => '123XYZ'})
67
+ @test_message.api_key = '123XYZ'
68
+ expect{@subscriber.handle_envelope(@test_message)}.not_to raise_error
69
+ end
70
+
71
+ it "should accept the envelope with any correct api_key" do
72
+ stub_const('ENV', {'PIPELINE_API_KEYS' => '123XYZ,456UVW'})
73
+ @test_message.api_key = '456UVW'
74
+ expect{@subscriber.handle_envelope(@test_message)}.not_to raise_error
75
+ end
76
+ end
77
+ end
78
+
79
+ context "with decrypted payload" do
80
+ before do
81
+ @payload_str = @subscriber.class.decrypt(@test_message)
82
+ clazz = Object.const_get(@test_message.type_info)
83
+ @payload = clazz.parse(@payload_str)
84
+ end
85
+
86
+ it "should get the version right" do
87
+ expect(@payload.class.name).to eq "TestEmitter_2_0"
88
+ version = @subscriber._version(@payload)
89
+ expect(version).to eq RailsPipeline::PipelineVersion.new("2_0")
90
+ end
91
+
92
+ context "with registered target class" do
93
+ before do
94
+ RailsPipeline::Subscriber.register(TestEmitter_2_0, TestModel)
95
+ end
96
+
97
+ context 'without handler' do
98
+ it "should map to the right target" do
99
+ expect(@subscriber.target_class(@payload)).to eq TestModel
100
+ end
101
+
102
+ it "should instantiate a target" do
103
+ expect(TestModel).to receive(:new).once.and_call_original
104
+ expect(TestModel).to receive(:from_pipeline_2_0).once.and_call_original
105
+ allow_any_instance_of(TestModel).to receive(:save!)
106
+ target = @subscriber.handle_payload(@payload, @test_message)
107
+ expect(target.foo).to eq @payload.foo
108
+ end
109
+ end
110
+
111
+ context 'with a handler' do
112
+ before do
113
+ RailsPipeline::Subscriber.register(
114
+ TestEmitter_2_0, TestModel, RailsPipeline::SubscriberHandler::ActiveRecordCRUD)
115
+ end
116
+
117
+ it 'should map to the correct handler' do
118
+ expect(@subscriber.target_handler(@payload)).to eq(RailsPipeline::SubscriberHandler::ActiveRecordCRUD)
119
+ end
120
+
121
+ it 'should call the correct handler' do
122
+ expect_any_instance_of(RailsPipeline::SubscriberHandler::ActiveRecordCRUD).to receive(:handle_payload).once
123
+ target = @subscriber.handle_payload(@payload, @test_message)
124
+ end
125
+ end
126
+ end
127
+
128
+ context "with a registered target Proc" do
129
+ before do
130
+ @called = false
131
+ RailsPipeline::Subscriber.register(TestEmitter_2_0, Proc.new {
132
+ @called = true
133
+ })
134
+ end
135
+
136
+ it "should map to the right target" do
137
+ expect(@subscriber.target_class(@payload).is_a?(Proc)).to eq true
138
+ end
139
+
140
+ it "should run the proc" do
141
+ @subscriber.handle_payload(@payload, @test_message)
142
+ expect(@called).to eq true
143
+ end
144
+ end
145
+
146
+
147
+ context "without registered target" do
148
+ before do
149
+ RailsPipeline::Subscriber.register(TestEmitter_2_0, nil)
150
+ end
151
+
152
+ it "should not instantiate a target" do
153
+ @subscriber.handle_payload(@payload, @test_message)
154
+ end
155
+ end
156
+ end
157
+
158
+ describe '#most_suitable_method' do
159
+ let(:subscriber) { TestSubscriber.new }
160
+ let(:fake_class) { Class.new }
161
+ before do
162
+ stub_const("TestClass", fake_class)
163
+ # Clear TestSubscriber cache
164
+ TestSubscriber.handler_method_cache = {}
165
+ end
166
+
167
+ context 'when receiver class has the correct version method' do
168
+ before do
169
+ TestClass.define_singleton_method(:from_pipeline_1_1) { }
170
+ end
171
+ let(:version) { RailsPipeline::PipelineVersion.new('1_1') }
172
+ it 'picks the method' do
173
+ subscriber.most_suitable_handler_method_name(version, TestClass).should eq(:from_pipeline_1_1)
174
+ end
175
+ end
176
+
177
+ context 'when receiver class has a handler with same major and lower minor handler method' do
178
+ before do
179
+ TestClass.define_singleton_method(:from_pipeline_1_0) { }
180
+ end
181
+ let(:version) { RailsPipeline::PipelineVersion.new('1_1') }
182
+ it 'picks the method' do
183
+ subscriber.most_suitable_handler_method_name(version, TestClass).should eq(:from_pipeline_1_0)
184
+ end
185
+ end
186
+
187
+ context 'when receiver has multiple methods defined' do
188
+ before do
189
+ TestClass.define_singleton_method(:from_pipeline_1_0) { }
190
+ TestClass.define_singleton_method(:from_pipeline_1_5) { }
191
+ TestClass.define_singleton_method(:from_pipeline_1_2) { }
192
+ TestClass.define_singleton_method(:from_pipeline_1_1) { }
193
+ end
194
+ let(:version) { RailsPipeline::PipelineVersion.new('1_4') }
195
+ it 'picks the closest lower method' do
196
+ subscriber.most_suitable_handler_method_name(version, TestClass).should eq(:from_pipeline_1_2)
197
+ end
198
+ end
199
+
200
+ context 'when receiver has multiple methods defined' do
201
+ before do
202
+ TestClass.define_singleton_method(:from_pipeline_1_2) { }
203
+ TestClass.define_singleton_method(:from_pipeline_1_0) { }
204
+ TestClass.define_singleton_method(:from_pipeline_1_4) { }
205
+ TestClass.define_singleton_method(:from_pipeline_1_5) { }
206
+ end
207
+ let(:version) { RailsPipeline::PipelineVersion.new('1_4') }
208
+ it 'picks the closest lower method' do
209
+ subscriber.most_suitable_handler_method_name(version, TestClass).should eq(:from_pipeline_1_4)
210
+ end
211
+ end
212
+
213
+ context 'when receiver class does not have a handler method' do
214
+ let(:version) { RailsPipeline::PipelineVersion.new('1_1') }
215
+ it 'returns nil' do
216
+ subscriber.most_suitable_handler_method_name(version, TestClass).should be_nil
217
+ end
218
+ end
219
+ end
220
+
221
+ context 'methods cache' do
222
+ let(:subscriber) { TestSubscriber.new }
223
+ let(:other_subscriber) { OtherSubscriber.new }
224
+ let(:version) { RailsPipeline::PipelineVersion.new('1_1') }
225
+ let(:fake_class) { Class.new }
226
+ before do
227
+ stub_const("TestClass", fake_class)
228
+ TestClass.define_singleton_method(:from_pipeline_1_0) { }
229
+ end
230
+
231
+ it 'exists' do
232
+ TestSubscriber.handler_method_cache.should_not be_nil
233
+ end
234
+
235
+ context 'with empty cache' do
236
+
237
+ it 'caches method handler for version' do
238
+ subscriber.most_suitable_handler_method_name(version, TestClass)
239
+ TestSubscriber.handler_method_cache[version].should eq(:from_pipeline_1_0)
240
+ end
241
+ end
242
+
243
+ context 'with non empty cache' do
244
+ before do
245
+ # warms the cache
246
+ subscriber.most_suitable_handler_method_name(version, TestClass)
247
+ end
248
+
249
+ it "reads value from cache" do
250
+ TestClass.should_not_receive(:methods)
251
+ TestSubscriber.handler_method_cache.should_receive(:[]).with(version).once.and_call_original
252
+ subscriber.most_suitable_handler_method_name(version, TestClass)
253
+ end
254
+ end
255
+
256
+ context 'cache is attached to each class' do
257
+ let(:fake_class2) { Class.new }
258
+ before do
259
+ stub_const("TestClass2", fake_class2)
260
+ TestClass2.define_singleton_method(:from_pipeline_1_1) { }
261
+ TestClass2.define_singleton_method(:from_pipeline_2_0) { }
262
+ end
263
+ let(:v1_1) { RailsPipeline::PipelineVersion.new('1_1') }
264
+ let(:v2_0) { RailsPipeline::PipelineVersion.new('2_0') }
265
+
266
+ it 'caches methods in separate buckets' do
267
+ subscriber.most_suitable_handler_method_name(v1_1, TestClass)
268
+ other_subscriber.most_suitable_handler_method_name(v1_1, TestClass2)
269
+ other_subscriber.most_suitable_handler_method_name(v2_0, TestClass2)
270
+ TestSubscriber.handler_method_cache != OtherSubscriber.handler_method_cache
271
+ TestSubscriber.handler_method_cache.length.should eq(1)
272
+ OtherSubscriber.handler_method_cache.length.should eq(2)
273
+ end
274
+ end
275
+ end
276
+
277
+ end
278
+