rails-pipeline 1.1.1

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 (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
+