deimos-temp-fork 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (146) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +83 -0
  3. data/.gitignore +41 -0
  4. data/.gitmodules +0 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +333 -0
  7. data/.ruby-gemset +1 -0
  8. data/.ruby-version +1 -0
  9. data/CHANGELOG.md +349 -0
  10. data/CODE_OF_CONDUCT.md +77 -0
  11. data/Dockerfile +23 -0
  12. data/Gemfile +6 -0
  13. data/Gemfile.lock +286 -0
  14. data/Guardfile +22 -0
  15. data/LICENSE.md +195 -0
  16. data/README.md +1099 -0
  17. data/Rakefile +13 -0
  18. data/bin/deimos +4 -0
  19. data/deimos-ruby.gemspec +44 -0
  20. data/docker-compose.yml +71 -0
  21. data/docs/ARCHITECTURE.md +140 -0
  22. data/docs/CONFIGURATION.md +236 -0
  23. data/docs/DATABASE_BACKEND.md +147 -0
  24. data/docs/INTEGRATION_TESTS.md +52 -0
  25. data/docs/PULL_REQUEST_TEMPLATE.md +35 -0
  26. data/docs/UPGRADING.md +128 -0
  27. data/lib/deimos-temp-fork.rb +95 -0
  28. data/lib/deimos/active_record_consume/batch_consumption.rb +164 -0
  29. data/lib/deimos/active_record_consume/batch_slicer.rb +27 -0
  30. data/lib/deimos/active_record_consume/message_consumption.rb +79 -0
  31. data/lib/deimos/active_record_consume/schema_model_converter.rb +52 -0
  32. data/lib/deimos/active_record_consumer.rb +67 -0
  33. data/lib/deimos/active_record_producer.rb +87 -0
  34. data/lib/deimos/backends/base.rb +32 -0
  35. data/lib/deimos/backends/db.rb +41 -0
  36. data/lib/deimos/backends/kafka.rb +33 -0
  37. data/lib/deimos/backends/kafka_async.rb +33 -0
  38. data/lib/deimos/backends/test.rb +20 -0
  39. data/lib/deimos/batch_consumer.rb +7 -0
  40. data/lib/deimos/config/configuration.rb +381 -0
  41. data/lib/deimos/config/phobos_config.rb +137 -0
  42. data/lib/deimos/consume/batch_consumption.rb +150 -0
  43. data/lib/deimos/consume/message_consumption.rb +94 -0
  44. data/lib/deimos/consumer.rb +104 -0
  45. data/lib/deimos/instrumentation.rb +76 -0
  46. data/lib/deimos/kafka_message.rb +60 -0
  47. data/lib/deimos/kafka_source.rb +128 -0
  48. data/lib/deimos/kafka_topic_info.rb +102 -0
  49. data/lib/deimos/message.rb +79 -0
  50. data/lib/deimos/metrics/datadog.rb +47 -0
  51. data/lib/deimos/metrics/mock.rb +39 -0
  52. data/lib/deimos/metrics/provider.rb +36 -0
  53. data/lib/deimos/monkey_patches/phobos_cli.rb +35 -0
  54. data/lib/deimos/monkey_patches/phobos_producer.rb +51 -0
  55. data/lib/deimos/poll_info.rb +9 -0
  56. data/lib/deimos/producer.rb +224 -0
  57. data/lib/deimos/railtie.rb +8 -0
  58. data/lib/deimos/schema_backends/avro_base.rb +140 -0
  59. data/lib/deimos/schema_backends/avro_local.rb +30 -0
  60. data/lib/deimos/schema_backends/avro_schema_coercer.rb +119 -0
  61. data/lib/deimos/schema_backends/avro_schema_registry.rb +34 -0
  62. data/lib/deimos/schema_backends/avro_validation.rb +21 -0
  63. data/lib/deimos/schema_backends/base.rb +150 -0
  64. data/lib/deimos/schema_backends/mock.rb +42 -0
  65. data/lib/deimos/shared_config.rb +63 -0
  66. data/lib/deimos/test_helpers.rb +360 -0
  67. data/lib/deimos/tracing/datadog.rb +35 -0
  68. data/lib/deimos/tracing/mock.rb +40 -0
  69. data/lib/deimos/tracing/provider.rb +29 -0
  70. data/lib/deimos/utils/db_poller.rb +150 -0
  71. data/lib/deimos/utils/db_producer.rb +243 -0
  72. data/lib/deimos/utils/deadlock_retry.rb +68 -0
  73. data/lib/deimos/utils/inline_consumer.rb +150 -0
  74. data/lib/deimos/utils/lag_reporter.rb +175 -0
  75. data/lib/deimos/utils/schema_controller_mixin.rb +115 -0
  76. data/lib/deimos/version.rb +5 -0
  77. data/lib/generators/deimos/active_record/templates/migration.rb.tt +28 -0
  78. data/lib/generators/deimos/active_record/templates/model.rb.tt +5 -0
  79. data/lib/generators/deimos/active_record_generator.rb +79 -0
  80. data/lib/generators/deimos/db_backend/templates/migration +25 -0
  81. data/lib/generators/deimos/db_backend/templates/rails3_migration +31 -0
  82. data/lib/generators/deimos/db_backend_generator.rb +48 -0
  83. data/lib/generators/deimos/db_poller/templates/migration +11 -0
  84. data/lib/generators/deimos/db_poller/templates/rails3_migration +16 -0
  85. data/lib/generators/deimos/db_poller_generator.rb +48 -0
  86. data/lib/tasks/deimos.rake +34 -0
  87. data/spec/active_record_batch_consumer_spec.rb +481 -0
  88. data/spec/active_record_consume/batch_slicer_spec.rb +42 -0
  89. data/spec/active_record_consume/schema_model_converter_spec.rb +105 -0
  90. data/spec/active_record_consumer_spec.rb +154 -0
  91. data/spec/active_record_producer_spec.rb +85 -0
  92. data/spec/backends/base_spec.rb +10 -0
  93. data/spec/backends/db_spec.rb +54 -0
  94. data/spec/backends/kafka_async_spec.rb +11 -0
  95. data/spec/backends/kafka_spec.rb +11 -0
  96. data/spec/batch_consumer_spec.rb +256 -0
  97. data/spec/config/configuration_spec.rb +248 -0
  98. data/spec/consumer_spec.rb +209 -0
  99. data/spec/deimos_spec.rb +169 -0
  100. data/spec/generators/active_record_generator_spec.rb +56 -0
  101. data/spec/handlers/my_batch_consumer.rb +10 -0
  102. data/spec/handlers/my_consumer.rb +10 -0
  103. data/spec/kafka_listener_spec.rb +55 -0
  104. data/spec/kafka_source_spec.rb +381 -0
  105. data/spec/kafka_topic_info_spec.rb +111 -0
  106. data/spec/message_spec.rb +19 -0
  107. data/spec/phobos.bad_db.yml +73 -0
  108. data/spec/phobos.yml +77 -0
  109. data/spec/producer_spec.rb +498 -0
  110. data/spec/rake_spec.rb +19 -0
  111. data/spec/schema_backends/avro_base_shared.rb +199 -0
  112. data/spec/schema_backends/avro_local_spec.rb +32 -0
  113. data/spec/schema_backends/avro_schema_registry_spec.rb +32 -0
  114. data/spec/schema_backends/avro_validation_spec.rb +24 -0
  115. data/spec/schema_backends/base_spec.rb +33 -0
  116. data/spec/schemas/com/my-namespace/Generated.avsc +71 -0
  117. data/spec/schemas/com/my-namespace/MyNestedSchema.avsc +62 -0
  118. data/spec/schemas/com/my-namespace/MySchema-key.avsc +13 -0
  119. data/spec/schemas/com/my-namespace/MySchema.avsc +18 -0
  120. data/spec/schemas/com/my-namespace/MySchemaCompound-key.avsc +18 -0
  121. data/spec/schemas/com/my-namespace/MySchemaWithBooleans.avsc +18 -0
  122. data/spec/schemas/com/my-namespace/MySchemaWithDateTimes.avsc +33 -0
  123. data/spec/schemas/com/my-namespace/MySchemaWithId.avsc +28 -0
  124. data/spec/schemas/com/my-namespace/MySchemaWithUniqueId.avsc +32 -0
  125. data/spec/schemas/com/my-namespace/Wibble.avsc +43 -0
  126. data/spec/schemas/com/my-namespace/Widget.avsc +27 -0
  127. data/spec/schemas/com/my-namespace/WidgetTheSecond.avsc +27 -0
  128. data/spec/schemas/com/my-namespace/request/CreateTopic.avsc +11 -0
  129. data/spec/schemas/com/my-namespace/request/Index.avsc +11 -0
  130. data/spec/schemas/com/my-namespace/request/UpdateRequest.avsc +11 -0
  131. data/spec/schemas/com/my-namespace/response/CreateTopic.avsc +11 -0
  132. data/spec/schemas/com/my-namespace/response/Index.avsc +11 -0
  133. data/spec/schemas/com/my-namespace/response/UpdateResponse.avsc +11 -0
  134. data/spec/spec_helper.rb +267 -0
  135. data/spec/utils/db_poller_spec.rb +320 -0
  136. data/spec/utils/db_producer_spec.rb +514 -0
  137. data/spec/utils/deadlock_retry_spec.rb +74 -0
  138. data/spec/utils/inline_consumer_spec.rb +31 -0
  139. data/spec/utils/lag_reporter_spec.rb +76 -0
  140. data/spec/utils/platform_schema_validation_spec.rb +0 -0
  141. data/spec/utils/schema_controller_mixin_spec.rb +84 -0
  142. data/support/deimos-solo.png +0 -0
  143. data/support/deimos-with-name-next.png +0 -0
  144. data/support/deimos-with-name.png +0 -0
  145. data/support/flipp-logo.png +0 -0
  146. metadata +551 -0
@@ -0,0 +1,320 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @param seconds [Integer]
4
+ # @return [Time]
5
+ def time_value(secs: 0, mins: 0)
6
+ Time.local(2015, 5, 5, 1, 0, 0) + (secs + (mins * 60))
7
+ end
8
+
9
+ each_db_config(Deimos::Utils::DbPoller) do
10
+
11
+ before(:each) do
12
+ Deimos::PollInfo.delete_all
13
+ end
14
+
15
+ describe '#start!' do
16
+
17
+ before(:each) do
18
+ producer_class = Class.new(Deimos::Producer) do
19
+ schema 'MySchema'
20
+ namespace 'com.my-namespace'
21
+ topic 'my-topic'
22
+ key_config field: 'test_id'
23
+ end
24
+ stub_const('MyProducer', producer_class)
25
+
26
+ producer_class = Class.new(Deimos::Producer) do
27
+ schema 'MySchemaWithId'
28
+ namespace 'com.my-namespace'
29
+ topic 'my-topic'
30
+ key_config plain: true
31
+ end
32
+ stub_const('MyProducerWithID', producer_class)
33
+ end
34
+
35
+ it 'should raise an error if no pollers configured' do
36
+ Deimos.configure {}
37
+ expect { described_class.start! }.to raise_error('No pollers configured!')
38
+ end
39
+
40
+ it 'should start pollers as configured' do
41
+ Deimos.configure do
42
+ db_poller do
43
+ producer_class 'MyProducer'
44
+ end
45
+ db_poller do
46
+ producer_class 'MyProducerWithID'
47
+ end
48
+ end
49
+
50
+ allow(Deimos::Utils::DbPoller).to receive(:new)
51
+ signal_double = instance_double(Sigurd::SignalHandler, run!: nil)
52
+ allow(Sigurd::SignalHandler).to receive(:new).and_return(signal_double)
53
+ described_class.start!
54
+ expect(Deimos::Utils::DbPoller).to have_received(:new).twice
55
+ expect(Deimos::Utils::DbPoller).to have_received(:new).
56
+ with(Deimos.config.db_poller_objects[0])
57
+ expect(Deimos::Utils::DbPoller).to have_received(:new).
58
+ with(Deimos.config.db_poller_objects[1])
59
+ end
60
+ end
61
+
62
+ describe 'pollers' do
63
+ include_context 'with widgets'
64
+
65
+ let(:poller) do
66
+ poller = described_class.new(config)
67
+ allow(poller).to receive(:sleep)
68
+ poller
69
+ end
70
+
71
+ let(:config) { Deimos.config.db_poller_objects.first.dup }
72
+
73
+ before(:each) do
74
+ Widget.delete_all
75
+ producer_class = Class.new(Deimos::ActiveRecordProducer) do
76
+ schema 'MySchemaWithId'
77
+ namespace 'com.my-namespace'
78
+ topic 'my-topic-with-id'
79
+ key_config none: true
80
+ record_class Widget
81
+
82
+ # :nodoc:
83
+ def self.generate_payload(attrs, widget)
84
+ super.merge(message_id: widget.generated_id)
85
+ end
86
+ end
87
+ stub_const('MyProducer', producer_class)
88
+
89
+ Deimos.configure do
90
+ db_poller do
91
+ producer_class 'MyProducer'
92
+ run_every 1.minute
93
+ end
94
+ end
95
+ end
96
+
97
+ after(:each) do
98
+ travel_back
99
+ end
100
+
101
+ it 'should crash if initialized with an invalid producer' do
102
+ config.producer_class = 'NoProducer'
103
+ expect { described_class.new(config) }.to raise_error('Class NoProducer not found!')
104
+ end
105
+
106
+ describe '#retrieve_poll_info' do
107
+
108
+ it 'should start from beginning when configured' do
109
+ poller.retrieve_poll_info
110
+ expect(Deimos::PollInfo.count).to eq(1)
111
+ info = Deimos::PollInfo.last
112
+ expect(info.producer).to eq('MyProducer')
113
+ expect(info.last_sent).to eq(Time.new(0))
114
+ expect(info.last_sent_id).to eq(0)
115
+ end
116
+
117
+ it 'should start from now when configured' do
118
+ travel_to time_value
119
+ config.start_from_beginning = false
120
+ poller.retrieve_poll_info
121
+ expect(Deimos::PollInfo.count).to eq(1)
122
+ info = Deimos::PollInfo.last
123
+ expect(info.producer).to eq('MyProducer')
124
+ expect(info.last_sent).to eq(time_value)
125
+ expect(info.last_sent_id).to eq(0)
126
+ end
127
+
128
+ end
129
+
130
+ specify '#start' do
131
+ i = 0
132
+ expect(poller).to receive(:process_updates).twice do
133
+ i += 1
134
+ poller.stop if i == 2
135
+ end
136
+ poller.start
137
+ end
138
+
139
+ specify '#should_run?' do
140
+ Deimos::PollInfo.create!(producer: 'MyProducer',
141
+ last_sent: time_value)
142
+ poller.retrieve_poll_info
143
+
144
+ # run_every is set to 1 minute
145
+ travel_to time_value(secs: 62)
146
+ expect(poller.should_run?).to eq(true)
147
+
148
+ travel_to time_value(secs: 30)
149
+ expect(poller.should_run?).to eq(false)
150
+
151
+ travel_to time_value(mins: -1) # this shouldn't be possible but meh
152
+ expect(poller.should_run?).to eq(false)
153
+
154
+ # take the 2 seconds of delay_time into account
155
+ travel_to time_value(secs: 60)
156
+ expect(poller.should_run?).to eq(false)
157
+ end
158
+
159
+ specify '#process_batch' do
160
+ travel_to time_value
161
+ widgets = (1..3).map { Widget.create!(test_id: 'some_id', some_int: 4) }
162
+ widgets.last.update_attribute(:updated_at, time_value(mins: -30))
163
+ expect(MyProducer).to receive(:send_events).with(widgets)
164
+ poller.retrieve_poll_info
165
+ poller.process_batch(widgets)
166
+ info = Deimos::PollInfo.last
167
+ expect(info.last_sent.in_time_zone).to eq(time_value(mins: -30))
168
+ expect(info.last_sent_id).to eq(widgets.last.id)
169
+ end
170
+
171
+ describe '#process_updates' do
172
+ before(:each) do
173
+ Deimos::PollInfo.create!(producer: 'MyProducer',
174
+ last_sent: time_value(mins: -61),
175
+ last_sent_id: 0)
176
+ poller.retrieve_poll_info
177
+ travel_to time_value
178
+ stub_const('Deimos::Utils::DbPoller::BATCH_SIZE', 3)
179
+ end
180
+
181
+ let!(:old_widget) do
182
+ # old widget, earlier than window
183
+ Widget.create!(test_id: 'some_id', some_int: 40,
184
+ updated_at: time_value(mins: -200))
185
+ end
186
+
187
+ let!(:last_widget) do
188
+ # new widget, before delay
189
+ Widget.create!(test_id: 'some_id', some_int: 10,
190
+ updated_at: time_value(secs: -1))
191
+ end
192
+
193
+ let!(:widgets) do
194
+ (1..7).map do |i|
195
+ Widget.create!(test_id: 'some_id', some_int: i,
196
+ updated_at: time_value(mins: -61, secs: 30 + i))
197
+ end
198
+ end
199
+
200
+ it 'should update the full table' do
201
+ info = Deimos::PollInfo.last
202
+ config.full_table = true
203
+ expect(MyProducer).to receive(:poll_query).at_least(:once).and_call_original
204
+ expect(poller).to receive(:process_batch).ordered.
205
+ with([old_widget, widgets[0], widgets[1]]).and_wrap_original do |m, *args|
206
+ m.call(*args)
207
+ expect(info.reload.last_sent.in_time_zone).to eq(time_value(mins: -61, secs: 32))
208
+ expect(info.last_sent_id).to eq(widgets[1].id)
209
+ end
210
+ expect(poller).to receive(:process_batch).ordered.
211
+ with([widgets[2], widgets[3], widgets[4]]).and_call_original
212
+ expect(poller).to receive(:process_batch).ordered.
213
+ with([widgets[5], widgets[6]]).and_call_original
214
+ poller.process_updates
215
+
216
+ # this is the updated_at of widgets[6]
217
+ expect(info.reload.last_sent.in_time_zone).to eq(time_value(mins: -61, secs: 37))
218
+ expect(info.last_sent_id).to eq(widgets[6].id)
219
+
220
+ last_widget.update_attribute(:updated_at, time_value(mins: -250))
221
+
222
+ travel 61.seconds
223
+ # should reprocess the table
224
+ expect(poller).to receive(:process_batch).ordered.
225
+ with([last_widget, old_widget, widgets[0]]).and_call_original
226
+ expect(poller).to receive(:process_batch).ordered.
227
+ with([widgets[1], widgets[2], widgets[3]]).and_call_original
228
+ expect(poller).to receive(:process_batch).ordered.
229
+ with([widgets[4], widgets[5], widgets[6]]).and_call_original
230
+ poller.process_updates
231
+
232
+ expect(info.reload.last_sent.in_time_zone).to eq(time_value(mins: -61, secs: 37))
233
+ expect(info.last_sent_id).to eq(widgets[6].id)
234
+ end
235
+
236
+ it 'should send events across multiple batches' do
237
+ allow(MyProducer).to receive(:poll_query).and_call_original
238
+ expect(poller).to receive(:process_batch).ordered.
239
+ with([widgets[0], widgets[1], widgets[2]]).and_call_original
240
+ expect(poller).to receive(:process_batch).ordered.
241
+ with([widgets[3], widgets[4], widgets[5]]).and_call_original
242
+ expect(poller).to receive(:process_batch).ordered.
243
+ with([widgets[6]]).and_call_original
244
+ poller.process_updates
245
+
246
+ expect(MyProducer).to have_received(:poll_query).
247
+ with(time_from: time_value(mins: -61),
248
+ time_to: time_value(secs: -2),
249
+ column_name: :updated_at,
250
+ min_id: 0)
251
+
252
+ travel 61.seconds
253
+ # process the last widget which came in during the delay
254
+ expect(poller).to receive(:process_batch).with([last_widget]).
255
+ and_call_original
256
+ poller.process_updates
257
+
258
+ # widgets[6] updated_at value
259
+ expect(MyProducer).to have_received(:poll_query).
260
+ with(time_from: time_value(mins: -61, secs: 37),
261
+ time_to: time_value(secs: 59), # plus 61 seconds minus 2 seconds for delay
262
+ column_name: :updated_at,
263
+ min_id: widgets[6].id)
264
+
265
+ travel 61.seconds
266
+ # nothing else to process
267
+ expect(poller).not_to receive(:process_batch)
268
+ poller.process_updates
269
+ poller.process_updates
270
+
271
+ expect(MyProducer).to have_received(:poll_query).twice.
272
+ with(time_from: time_value(secs: -1),
273
+ time_to: time_value(secs: 120), # plus 122 seconds minus 2 seconds
274
+ column_name: :updated_at,
275
+ min_id: last_widget.id)
276
+ end
277
+
278
+ it 'should recover correctly with errors and save the right ID' do
279
+ widgets.each do |w|
280
+ w.update_attribute(:updated_at, time_value(mins: -61, secs: 30))
281
+ end
282
+ allow(MyProducer).to receive(:poll_query).and_call_original
283
+ expect(poller).to receive(:process_batch).ordered.
284
+ with([widgets[0], widgets[1], widgets[2]]).and_call_original
285
+ expect(poller).to receive(:process_batch).ordered.
286
+ with([widgets[3], widgets[4], widgets[5]]).and_raise('OH NOES')
287
+
288
+ expect { poller.process_updates }.to raise_exception('OH NOES')
289
+
290
+ expect(MyProducer).to have_received(:poll_query).
291
+ with(time_from: time_value(mins: -61),
292
+ time_to: time_value(secs: -2),
293
+ column_name: :updated_at,
294
+ min_id: 0)
295
+
296
+ info = Deimos::PollInfo.last
297
+ expect(info.last_sent.in_time_zone).to eq(time_value(mins: -61, secs: 30))
298
+ expect(info.last_sent_id).to eq(widgets[2].id)
299
+
300
+ travel 61.seconds
301
+ # process the last widget which came in during the delay
302
+ expect(poller).to receive(:process_batch).ordered.
303
+ with([widgets[3], widgets[4], widgets[5]]).and_call_original
304
+ expect(poller).to receive(:process_batch).with([widgets[6], last_widget]).
305
+ and_call_original
306
+ poller.process_updates
307
+ expect(MyProducer).to have_received(:poll_query).
308
+ with(time_from: time_value(mins: -61, secs: 30),
309
+ time_to: time_value(secs: 59),
310
+ column_name: :updated_at,
311
+ min_id: widgets[2].id)
312
+
313
+ expect(info.reload.last_sent.in_time_zone).to eq(time_value(secs: -1))
314
+ expect(info.last_sent_id).to eq(last_widget.id)
315
+ end
316
+
317
+ end
318
+
319
+ end
320
+ end
@@ -0,0 +1,514 @@
1
+ # frozen_string_literal: true
2
+
3
+ each_db_config(Deimos::Utils::DbProducer) do
4
+ let(:producer) do
5
+ producer = described_class.new
6
+ allow(producer).to receive(:sleep)
7
+ allow(producer).to receive(:producer).and_return(phobos_producer)
8
+ producer
9
+ end
10
+
11
+ let(:phobos_producer) do
12
+ pp = instance_double(Phobos::Producer::PublicAPI)
13
+ allow(pp).to receive(:publish_list)
14
+ pp
15
+ end
16
+
17
+ before(:each) do
18
+ stub_const('Deimos::Utils::DbProducer::BATCH_SIZE', 2)
19
+ stub_const('Deimos::Utils::DbProducer::DELETE_BATCH_SIZE', 1)
20
+ end
21
+
22
+ specify '#process_next_messages' do
23
+ expect(producer).to receive(:retrieve_topics).and_return(%w(topic1 topic2))
24
+ expect(producer).to receive(:process_topic).twice
25
+ expect(Deimos::KafkaTopicInfo).to receive(:ping_empty_topics).with(%w(topic1 topic2))
26
+ expect(producer).to receive(:sleep).with(0.5)
27
+ producer.process_next_messages
28
+ end
29
+
30
+ specify '#retrieve_topics' do
31
+ (1..3).each do |i|
32
+ Deimos::KafkaMessage.create!(topic: "topic#{i}",
33
+ key: 'blergkey',
34
+ message: 'blerg')
35
+ end
36
+ expect(producer.retrieve_topics).
37
+ to contain_exactly('topic1', 'topic2', 'topic3')
38
+ end
39
+
40
+ specify '#retrieve_messages' do
41
+ (1..3).each do |i|
42
+ Deimos::KafkaMessage.create!(topic: 'topic1',
43
+ message: 'blah',
44
+ key: "key#{i}")
45
+ Deimos::KafkaMessage.create!(topic: 'topic2',
46
+ message: 'blah',
47
+ key: "key#{i}")
48
+ end
49
+ stub_const('Deimos::Utils::DbProducer::BATCH_SIZE', 5)
50
+ producer.current_topic = 'topic1'
51
+ messages = producer.retrieve_messages
52
+ expect(messages.size).to eq(3)
53
+ expect(messages).to all(be_a_kind_of(Deimos::KafkaMessage))
54
+ end
55
+
56
+ describe '#produce_messages' do
57
+
58
+ it 'should produce normally' do
59
+ batch = ['A'] * 1000
60
+ expect(phobos_producer).to receive(:publish_list).with(batch).once
61
+ expect(Deimos.config.metrics).to receive(:increment).with('publish',
62
+ tags: %w(status:success topic:),
63
+ by: 1000).once
64
+ producer.produce_messages(batch)
65
+ end
66
+
67
+ it 'should split the batch size on buffer overflow' do
68
+ class_producer = double(Phobos::Producer::ClassMethods::PublicAPI, # rubocop:disable RSpec/VerifiedDoubles
69
+ sync_producer_shutdown: nil)
70
+ allow(producer.class).to receive(:producer).and_return(class_producer)
71
+ expect(class_producer).to receive(:sync_producer_shutdown).twice
72
+ count = 0
73
+ allow(phobos_producer).to receive(:publish_list) do
74
+ count += 1
75
+ raise Kafka::BufferOverflow if count < 3
76
+ end
77
+ allow(Deimos.config.metrics).to receive(:increment)
78
+ batch = ['A'] * 1000
79
+ producer.produce_messages(batch)
80
+ expect(phobos_producer).to have_received(:publish_list).with(batch)
81
+ expect(phobos_producer).to have_received(:publish_list).with(['A'] * 100)
82
+ expect(phobos_producer).to have_received(:publish_list).with(['A'] * 10).exactly(100).times
83
+ expect(Deimos.config.metrics).to have_received(:increment).with('publish',
84
+ tags: %w(status:success topic:),
85
+ by: 10).exactly(100).times
86
+ end
87
+
88
+ it "should raise an error if it can't split any more" do
89
+ allow(phobos_producer).to receive(:publish_list) do
90
+ raise Kafka::BufferOverflow
91
+ end
92
+ expect(Deimos.config.metrics).not_to receive(:increment)
93
+ batch = ['A'] * 1000
94
+ expect { producer.produce_messages(batch) }.to raise_error(Kafka::BufferOverflow)
95
+ expect(phobos_producer).to have_received(:publish_list).with(batch)
96
+ expect(phobos_producer).to have_received(:publish_list).with(['A'] * 100).once
97
+ expect(phobos_producer).to have_received(:publish_list).with(['A'] * 10).once
98
+ expect(phobos_producer).to have_received(:publish_list).with(['A']).once
99
+ end
100
+
101
+ it 'should not resend batches of sent messages' do
102
+ allow(phobos_producer).to receive(:publish_list) do |group|
103
+ raise Kafka::BufferOverflow if group.any?('A') && group.size >= 1000
104
+ raise Kafka::BufferOverflow if group.any?('BIG') && group.size >= 10
105
+ end
106
+ allow(Deimos.config.metrics).to receive(:increment)
107
+ batch = ['A'] * 450 + ['BIG'] * 550
108
+ producer.produce_messages(batch)
109
+
110
+ expect(phobos_producer).to have_received(:publish_list).with(batch)
111
+ expect(phobos_producer).to have_received(:publish_list).with(['A'] * 100).exactly(4).times
112
+ expect(phobos_producer).to have_received(:publish_list).with(['A'] * 50 + ['BIG'] * 50)
113
+ expect(phobos_producer).to have_received(:publish_list).with(['A'] * 10).exactly(5).times
114
+ expect(phobos_producer).to have_received(:publish_list).with(['BIG'] * 1).exactly(550).times
115
+
116
+ expect(Deimos.config.metrics).to have_received(:increment).with('publish',
117
+ tags: %w(status:success topic:),
118
+ by: 100).exactly(4).times
119
+ expect(Deimos.config.metrics).to have_received(:increment).with('publish',
120
+ tags: %w(status:success topic:),
121
+ by: 10).exactly(5).times
122
+ expect(Deimos.config.metrics).to have_received(:increment).with('publish',
123
+ tags: %w(status:success topic:),
124
+ by: 1).exactly(550).times
125
+ end
126
+
127
+ describe '#compact_messages' do
128
+ let(:batch) do
129
+ [
130
+ {
131
+ key: 1,
132
+ topic: 'my-topic',
133
+ message: 'AAA'
134
+ },
135
+ {
136
+ key: 2,
137
+ topic: 'my-topic',
138
+ message: 'BBB'
139
+ },
140
+ {
141
+ key: 1,
142
+ topic: 'my-topic',
143
+ message: 'CCC'
144
+ }
145
+ ].map { |h| Deimos::KafkaMessage.create!(h) }
146
+ end
147
+
148
+ let(:deduped_batch) { batch[1..2] }
149
+
150
+ it 'should dedupe messages when :all is set' do
151
+ Deimos.configure { |c| c.db_producer.compact_topics = :all }
152
+ expect(producer.compact_messages(batch)).to eq(deduped_batch)
153
+ end
154
+
155
+ it 'should dedupe messages when topic is included' do
156
+ Deimos.configure { |c| c.db_producer.compact_topics = %w(my-topic my-topic2) }
157
+ expect(producer.compact_messages(batch)).to eq(deduped_batch)
158
+ end
159
+
160
+ it 'should not dedupe messages when topic is not included' do
161
+ Deimos.configure { |c| c.db_producer.compact_topics = %w(my-topic3 my-topic2) }
162
+ expect(producer.compact_messages(batch)).to eq(batch)
163
+ end
164
+
165
+ it 'should not dedupe messages without keys' do
166
+ unkeyed_batch = [
167
+ {
168
+ key: nil,
169
+ topic: 'my-topic',
170
+ message: 'AAA'
171
+ },
172
+ {
173
+ key: nil,
174
+ topic: 'my-topic',
175
+ message: 'BBB'
176
+ }
177
+ ].map { |h| Deimos::KafkaMessage.create!(h) }
178
+ Deimos.configure { |c| c.db_producer.compact_topics = :all }
179
+ expect(producer.compact_messages(unkeyed_batch)).to eq(unkeyed_batch)
180
+ Deimos.configure { |c| c.db_producer.compact_topics = [] }
181
+ end
182
+
183
+ it 'should compact messages when all messages are unique' do
184
+ Deimos.configure { |c| c.db_producer.compact_topics = %w(my-topic my-topic2) }
185
+ expect(producer.compact_messages(deduped_batch)).to eq(deduped_batch)
186
+ end
187
+ end
188
+ end
189
+
190
+ describe '#process_topic' do
191
+ before(:each) do
192
+ producer.id = 'abc'
193
+ end
194
+
195
+ it 'should do nothing if lock fails' do
196
+ expect(Deimos::KafkaTopicInfo).to receive(:lock).
197
+ with('my-topic', 'abc').and_return(false)
198
+ expect(producer).not_to receive(:retrieve_messages)
199
+ producer.process_topic('my-topic')
200
+ end
201
+
202
+ it 'should complete successfully' do
203
+ messages = (1..4).map do |i|
204
+ Deimos::KafkaMessage.new(
205
+ topic: 'my-topic',
206
+ message: "mess#{i}",
207
+ partition_key: "key#{i}"
208
+ )
209
+ end
210
+ expect(Deimos::KafkaTopicInfo).to receive(:lock).
211
+ with('my-topic', 'abc').and_return(true)
212
+ expect(producer).to receive(:retrieve_messages).ordered.
213
+ and_return(messages[0..1])
214
+ expect(producer).to receive(:send_pending_metrics).twice
215
+ expect(producer).to receive(:produce_messages).ordered.with([
216
+ {
217
+ payload: 'mess1',
218
+ key: nil,
219
+ partition_key: 'key1',
220
+ topic: 'my-topic'
221
+ },
222
+ {
223
+ payload: 'mess2',
224
+ key: nil,
225
+ partition_key: 'key2',
226
+ topic: 'my-topic'
227
+ }
228
+ ])
229
+ expect(Deimos.config.metrics).to receive(:increment).ordered.with(
230
+ 'db_producer.process',
231
+ tags: %w(topic:my-topic),
232
+ by: 2
233
+ )
234
+ expect(producer).to receive(:retrieve_messages).ordered.
235
+ and_return(messages[2..3])
236
+ expect(producer).to receive(:produce_messages).ordered.with([
237
+ {
238
+ payload: 'mess3',
239
+ partition_key: 'key3',
240
+ key: nil,
241
+ topic: 'my-topic'
242
+ },
243
+ {
244
+ payload: 'mess4',
245
+ partition_key: 'key4',
246
+ key: nil,
247
+ topic: 'my-topic'
248
+ }
249
+ ])
250
+ expect(Deimos.config.metrics).to receive(:increment).ordered.with(
251
+ 'db_producer.process',
252
+ tags: %w(topic:my-topic),
253
+ by: 2
254
+ )
255
+ expect(producer).to receive(:retrieve_messages).ordered.
256
+ and_return([])
257
+ expect(Deimos::KafkaTopicInfo).to receive(:heartbeat).
258
+ with('my-topic', 'abc').twice
259
+ expect(Deimos::KafkaTopicInfo).to receive(:clear_lock).
260
+ with('my-topic', 'abc').once
261
+ producer.process_topic('my-topic')
262
+ end
263
+
264
+ it 'should register an error if it gets an error' do
265
+ allow(producer).to receive(:shutdown_producer)
266
+ expect(producer).to receive(:retrieve_messages).and_raise('OH NOES')
267
+ expect(Deimos::KafkaTopicInfo).to receive(:register_error).
268
+ with('my-topic', 'abc')
269
+ expect(producer).not_to receive(:produce_messages)
270
+ producer.process_topic('my-topic')
271
+ expect(producer).to have_received(:shutdown_producer)
272
+ end
273
+
274
+ it 'should move on if it gets a partial batch' do
275
+ expect(producer).to receive(:retrieve_messages).ordered.
276
+ and_return([Deimos::KafkaMessage.new(
277
+ topic: 'my-topic',
278
+ message: 'mess1'
279
+ )])
280
+ expect(producer).to receive(:produce_messages).once
281
+ producer.process_topic('my-topic')
282
+ end
283
+
284
+ it 'should notify on error' do
285
+ messages = (1..4).map do |i|
286
+ Deimos::KafkaMessage.create!(
287
+ id: i,
288
+ topic: 'my-topic',
289
+ message: "mess#{i}",
290
+ partition_key: "key#{i}"
291
+ )
292
+ end
293
+
294
+ expect(Deimos::KafkaTopicInfo).to receive(:lock).
295
+ with('my-topic', 'abc').and_return(true)
296
+ expect(producer).to receive(:produce_messages).and_raise('OH NOES')
297
+ expect(producer).to receive(:retrieve_messages).and_return(messages)
298
+ expect(Deimos::KafkaTopicInfo).to receive(:register_error)
299
+
300
+ expect(Deimos::KafkaMessage.count).to eq(4)
301
+ subscriber = Deimos.subscribe('db_producer.produce') do |event|
302
+ expect(event.payload[:exception_object].message).to eq('OH NOES')
303
+ expect(event.payload[:messages]).to eq(messages)
304
+ end
305
+ producer.process_topic('my-topic')
306
+ # don't delete for regular errors
307
+ expect(Deimos::KafkaMessage.count).to eq(4)
308
+ Deimos.unsubscribe(subscriber)
309
+ end
310
+
311
+ it 'should delete messages on buffer overflow' do
312
+ messages = (1..4).map do |i|
313
+ Deimos::KafkaMessage.create!(
314
+ id: i,
315
+ topic: 'my-topic',
316
+ message: "mess#{i}",
317
+ partition_key: "key#{i}"
318
+ )
319
+ end
320
+ (5..8).each do |i|
321
+ Deimos::KafkaMessage.create!(
322
+ id: i,
323
+ topic: 'my-topic2',
324
+ message: "mess#{i}",
325
+ partition_key: "key#{i}"
326
+ )
327
+ end
328
+
329
+ expect(Deimos::KafkaTopicInfo).to receive(:lock).
330
+ with('my-topic', 'abc').and_return(true)
331
+ expect(producer).to receive(:produce_messages).and_raise(Kafka::BufferOverflow)
332
+ expect(producer).to receive(:retrieve_messages).and_return(messages)
333
+ expect(Deimos::KafkaTopicInfo).to receive(:register_error)
334
+
335
+ expect(Deimos::KafkaMessage.count).to eq(8)
336
+ producer.process_topic('my-topic')
337
+ expect(Deimos::KafkaMessage.count).to eq(4)
338
+ end
339
+
340
+ it 'should retry deletes and not re-publish' do
341
+ messages = (1..4).map do |i|
342
+ Deimos::KafkaMessage.create!(
343
+ id: i,
344
+ topic: 'my-topic',
345
+ message: "mess#{i}",
346
+ partition_key: "key#{i}"
347
+ )
348
+ end
349
+ (5..8).each do |i|
350
+ Deimos::KafkaMessage.create!(
351
+ id: i,
352
+ topic: 'my-topic2',
353
+ message: "mess#{i}",
354
+ partition_key: "key#{i}"
355
+ )
356
+ end
357
+
358
+ raise_error = true
359
+ expect(Deimos::KafkaMessage).to receive(:where).exactly(5).times.and_wrap_original do |m, *args|
360
+ if raise_error
361
+ raise_error = false
362
+ raise 'Lock wait timeout'
363
+ end
364
+ m.call(*args)
365
+ end
366
+
367
+ expect(Deimos::KafkaTopicInfo).to receive(:lock).
368
+ with('my-topic', 'abc').and_return(true)
369
+ expect(producer).to receive(:retrieve_messages).ordered.and_return(messages)
370
+ expect(producer).to receive(:retrieve_messages).ordered.and_return([])
371
+ expect(phobos_producer).to receive(:publish_list).once.with(messages.map(&:phobos_message))
372
+
373
+ expect(Deimos::KafkaMessage.count).to eq(8)
374
+ producer.process_topic('my-topic')
375
+ expect(Deimos::KafkaMessage.count).to eq(4)
376
+ end
377
+
378
+ it 'should re-raise misc errors on delete' do
379
+ messages = (1..3).map do |i|
380
+ Deimos::KafkaMessage.create!(
381
+ id: i,
382
+ topic: 'my-topic',
383
+ message: "mess#{i}",
384
+ partition_key: "key#{i}"
385
+ )
386
+ end
387
+ expect(Deimos::KafkaMessage).to receive(:where).once.and_raise('OH NOES')
388
+ expect { producer.delete_messages(messages) }.to raise_exception('OH NOES')
389
+ end
390
+
391
+ end
392
+
393
+ describe '#send_pending_metrics' do
394
+ it 'should use the first created_at for each topic' do |example|
395
+ # sqlite date-time strings don't work correctly
396
+ next if example.metadata[:db_config][:adapter] == 'sqlite3'
397
+
398
+ freeze_time do
399
+ (1..2).each do |i|
400
+ Deimos::KafkaMessage.create!(topic: "topic#{i}", message: nil,
401
+ created_at: (3 + i).minutes.ago)
402
+ Deimos::KafkaMessage.create!(topic: "topic#{i}", message: nil,
403
+ created_at: (2 + i).minutes.ago)
404
+ Deimos::KafkaMessage.create!(topic: "topic#{i}", message: nil,
405
+ created_at: (1 + i).minute.ago)
406
+ end
407
+ Deimos::KafkaTopicInfo.create!(topic: 'topic1',
408
+ last_processed_at: 6.minutes.ago)
409
+ Deimos::KafkaTopicInfo.create!(topic: 'topic2',
410
+ last_processed_at: 3.minutes.ago)
411
+ Deimos::KafkaTopicInfo.create!(topic: 'topic3',
412
+ last_processed_at: 5.minutes.ago)
413
+ allow(Deimos.config.metrics).to receive(:gauge)
414
+ producer.send_pending_metrics
415
+ expect(Deimos.config.metrics).to have_received(:gauge).exactly(6).times
416
+ # topic1 has the earliest message 4 minutes ago and last processed 6
417
+ # minutes ago, so the most amount of time we've seen nothing is 4 minutes
418
+ expect(Deimos.config.metrics).to have_received(:gauge).
419
+ with('pending_db_messages_max_wait', 4.minutes.to_i, tags: ['topic:topic1'])
420
+ # topic2 has earliest message 5 minutes ago and last processed 3 minutes
421
+ # ago, so we should give it 3 minutes
422
+ expect(Deimos.config.metrics).to have_received(:gauge).
423
+ with('pending_db_messages_max_wait', 3.minutes.to_i, tags: ['topic:topic2'])
424
+ # topic3 has no messages, so should get 0
425
+ expect(Deimos.config.metrics).to have_received(:gauge).
426
+ with('pending_db_messages_max_wait', 0, tags: ['topic:topic3'])
427
+ expect(Deimos.config.metrics).to have_received(:gauge).
428
+ with('pending_db_messages_count', 3, tags: ['topic:topic1'])
429
+ expect(Deimos.config.metrics).to have_received(:gauge).
430
+ with('pending_db_messages_count', 3, tags: ['topic:topic2'])
431
+ expect(Deimos.config.metrics).to have_received(:gauge).
432
+ with('pending_db_messages_count', 0, tags: ['topic:topic3'])
433
+ end
434
+ end
435
+ end
436
+
437
+ example 'Full integration test' do
438
+ (1..4).each do |i|
439
+ (1..2).each do |j|
440
+ Deimos::KafkaMessage.create!(topic: "topic#{j}",
441
+ message: "mess#{i}",
442
+ partition_key: "key#{i}")
443
+ Deimos::KafkaMessage.create!(topic: "topic#{j + 2}",
444
+ key: "key#{i}",
445
+ partition_key: "key#{i}",
446
+ message: "mess#{i}")
447
+ end
448
+ end
449
+ allow(producer).to receive(:produce_messages)
450
+
451
+ producer.process_next_messages
452
+ expect(Deimos::KafkaTopicInfo.count).to eq(4)
453
+ topics = Deimos::KafkaTopicInfo.select('distinct topic').map(&:topic)
454
+ expect(topics).to contain_exactly('topic1', 'topic2', 'topic3', 'topic4')
455
+ expect(Deimos::KafkaMessage.count).to eq(0)
456
+
457
+ expect(producer).to have_received(:produce_messages).with([
458
+ {
459
+ payload: 'mess1',
460
+ partition_key: 'key1',
461
+ key: nil,
462
+ topic: 'topic1'
463
+ },
464
+ {
465
+ payload: 'mess2',
466
+ key: nil,
467
+ partition_key: 'key2',
468
+ topic: 'topic1'
469
+ }
470
+ ])
471
+ expect(producer).to have_received(:produce_messages).with([
472
+ {
473
+ payload: 'mess3',
474
+ key: nil,
475
+ partition_key: 'key3',
476
+ topic: 'topic1'
477
+ },
478
+ {
479
+ payload: 'mess4',
480
+ key: nil,
481
+ partition_key: 'key4',
482
+ topic: 'topic1'
483
+ }
484
+ ])
485
+ expect(producer).to have_received(:produce_messages).with([
486
+ {
487
+ payload: 'mess1',
488
+ key: 'key1',
489
+ partition_key: 'key1',
490
+ topic: 'topic3'
491
+ },
492
+ {
493
+ payload: 'mess2',
494
+ partition_key: 'key2',
495
+ key: 'key2',
496
+ topic: 'topic3'
497
+ }
498
+ ])
499
+ expect(producer).to have_received(:produce_messages).with([
500
+ {
501
+ payload: 'mess3',
502
+ key: 'key3',
503
+ partition_key: 'key3',
504
+ topic: 'topic3'
505
+ },
506
+ {
507
+ payload: 'mess4',
508
+ partition_key: 'key4',
509
+ key: 'key4',
510
+ topic: 'topic3'
511
+ }
512
+ ])
513
+ end
514
+ end