deimos-temp-fork 0.0.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 (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