deimos-ruby 1.7.0.pre.beta1 → 1.8.1.pre.beta3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +8 -4
  3. data/CHANGELOG.md +50 -0
  4. data/Gemfile.lock +109 -75
  5. data/README.md +147 -16
  6. data/deimos-ruby.gemspec +4 -2
  7. data/docs/ARCHITECTURE.md +144 -0
  8. data/docs/CONFIGURATION.md +4 -0
  9. data/lib/deimos.rb +8 -7
  10. data/lib/deimos/active_record_consume/batch_consumption.rb +159 -0
  11. data/lib/deimos/active_record_consume/batch_slicer.rb +27 -0
  12. data/lib/deimos/active_record_consume/message_consumption.rb +58 -0
  13. data/lib/deimos/active_record_consume/schema_model_converter.rb +52 -0
  14. data/lib/deimos/active_record_consumer.rb +33 -75
  15. data/lib/deimos/batch_consumer.rb +2 -142
  16. data/lib/deimos/config/configuration.rb +8 -10
  17. data/lib/deimos/consume/batch_consumption.rb +150 -0
  18. data/lib/deimos/consume/message_consumption.rb +94 -0
  19. data/lib/deimos/consumer.rb +79 -72
  20. data/lib/deimos/instrumentation.rb +10 -5
  21. data/lib/deimos/kafka_message.rb +1 -1
  22. data/lib/deimos/kafka_topic_info.rb +21 -2
  23. data/lib/deimos/message.rb +6 -1
  24. data/lib/deimos/schema_backends/avro_base.rb +33 -1
  25. data/lib/deimos/schema_backends/avro_schema_coercer.rb +30 -11
  26. data/lib/deimos/schema_backends/base.rb +21 -2
  27. data/lib/deimos/utils/db_poller.rb +6 -6
  28. data/lib/deimos/utils/db_producer.rb +57 -15
  29. data/lib/deimos/utils/deadlock_retry.rb +68 -0
  30. data/lib/deimos/utils/lag_reporter.rb +19 -26
  31. data/lib/deimos/utils/schema_controller_mixin.rb +111 -0
  32. data/lib/deimos/version.rb +1 -1
  33. data/lib/generators/deimos/active_record/templates/migration.rb.tt +28 -0
  34. data/lib/generators/deimos/active_record/templates/model.rb.tt +5 -0
  35. data/lib/generators/deimos/active_record_generator.rb +79 -0
  36. data/lib/generators/deimos/db_backend/templates/migration +1 -0
  37. data/lib/generators/deimos/db_backend/templates/rails3_migration +1 -0
  38. data/spec/active_record_batch_consumer_spec.rb +481 -0
  39. data/spec/active_record_consume/batch_slicer_spec.rb +42 -0
  40. data/spec/active_record_consume/schema_model_converter_spec.rb +105 -0
  41. data/spec/active_record_consumer_spec.rb +3 -11
  42. data/spec/batch_consumer_spec.rb +24 -7
  43. data/spec/config/configuration_spec.rb +4 -0
  44. data/spec/consumer_spec.rb +6 -6
  45. data/spec/deimos_spec.rb +57 -49
  46. data/spec/generators/active_record_generator_spec.rb +56 -0
  47. data/spec/handlers/my_batch_consumer.rb +6 -1
  48. data/spec/handlers/my_consumer.rb +6 -1
  49. data/spec/kafka_listener_spec.rb +54 -0
  50. data/spec/kafka_topic_info_spec.rb +39 -16
  51. data/spec/message_spec.rb +19 -0
  52. data/spec/producer_spec.rb +34 -0
  53. data/spec/schemas/com/my-namespace/Generated.avsc +71 -0
  54. data/spec/schemas/com/my-namespace/MyNestedSchema.avsc +55 -0
  55. data/spec/schemas/com/my-namespace/MySchemaCompound-key.avsc +18 -0
  56. data/spec/schemas/com/my-namespace/Wibble.avsc +43 -0
  57. data/spec/schemas/com/my-namespace/request/Index.avsc +11 -0
  58. data/spec/schemas/com/my-namespace/request/UpdateRequest.avsc +11 -0
  59. data/spec/schemas/com/my-namespace/response/Index.avsc +11 -0
  60. data/spec/schemas/com/my-namespace/response/UpdateResponse.avsc +11 -0
  61. data/spec/spec_helper.rb +24 -0
  62. data/spec/utils/db_poller_spec.rb +2 -2
  63. data/spec/utils/db_producer_spec.rb +84 -10
  64. data/spec/utils/deadlock_retry_spec.rb +74 -0
  65. data/spec/utils/lag_reporter_spec.rb +29 -22
  66. data/spec/utils/schema_controller_mixin_spec.rb +68 -0
  67. metadata +87 -30
  68. data/lib/deimos/base_consumer.rb +0 -100
  69. data/lib/deimos/utils/executor.rb +0 -124
  70. data/lib/deimos/utils/platform_schema_validation.rb +0 -0
  71. data/lib/deimos/utils/signal_handler.rb +0 -68
  72. data/spec/utils/executor_spec.rb +0 -53
  73. data/spec/utils/signal_handler_spec.rb +0 -16
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'generators/deimos/active_record_generator'
4
+
5
+ RSpec.describe Deimos::Generators::ActiveRecordGenerator do
6
+
7
+ after(:each) do
8
+ FileUtils.rm_rf('db') if File.exist?('db')
9
+ FileUtils.rm_rf('app') if File.exist?('app')
10
+ end
11
+
12
+ it 'should generate a migration' do
13
+ expect(Dir['db/migrate/*.rb']).to be_empty
14
+ expect(Dir['app/models/*.rb']).to be_empty
15
+ described_class.start(['generated_table', 'com.my-namespace.Generated'])
16
+ files = Dir['db/migrate/*.rb']
17
+ expect(files.length).to eq(1)
18
+ results = <<~MIGRATION
19
+ class CreateGeneratedTable < ActiveRecord::Migration[6.0]
20
+ def up
21
+ if table_exists?(:generated_table)
22
+ warn "generated_table already exists, exiting"
23
+ return
24
+ end
25
+ create_table :generated_table do |t|
26
+ t.string :a_string
27
+ t.integer :a_int
28
+ t.bigint :a_long
29
+ t.float :a_float
30
+ t.float :a_double
31
+ t.string :an_enum
32
+ t.json :an_array
33
+ t.json :a_map
34
+ t.json :a_record
35
+ end
36
+
37
+ # TODO add indexes as necessary
38
+ end
39
+
40
+ def down
41
+ return unless table_exists?(:generated_table)
42
+ drop_table :generated_table
43
+ end
44
+
45
+ end
46
+ MIGRATION
47
+ expect(File.read(files[0])).to eq(results)
48
+ model = <<~MODEL
49
+ class GeneratedTable < ApplicationRecord
50
+ enum an_enum: {sym1: 'sym1', sym2: 'sym2'}
51
+ end
52
+ MODEL
53
+ expect(File.read('app/models/generated_table.rb')).to eq(model)
54
+ end
55
+
56
+ end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConsumerTest
4
- class MyBatchConsumer < Deimos::BatchConsumer; end
4
+ # Mock consumer
5
+ class MyBatchConsumer < Deimos::Consumer
6
+ # :no-doc:
7
+ def consume_batch
8
+ end
9
+ end
5
10
  end
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConsumerTest
4
- class MyConsumer < Deimos::Consumer; end
4
+ # Mock consumer
5
+ class MyConsumer < Deimos::Consumer
6
+ # :no-doc:
7
+ def consume
8
+ end
9
+ end
5
10
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Deimos::KafkaListener do
4
+ include_context 'with widgets'
5
+
6
+ prepend_before(:each) do
7
+ producer_class = Class.new(Deimos::Producer) do
8
+ schema 'MySchema'
9
+ namespace 'com.my-namespace'
10
+ topic 'my-topic'
11
+ key_config none: true
12
+ end
13
+ stub_const('MyProducer', producer_class)
14
+ end
15
+
16
+ before(:each) do
17
+ Deimos.configure do |c|
18
+ c.producers.backend = :kafka
19
+ c.schema.backend = :avro_local
20
+ end
21
+ allow_any_instance_of(Kafka::Cluster).to receive(:add_target_topics)
22
+ allow_any_instance_of(Kafka::Cluster).to receive(:partitions_for).
23
+ and_raise(Kafka::Error)
24
+ end
25
+
26
+ describe '.send_produce_error' do
27
+ let(:payloads) do
28
+ [{ 'test_id' => 'foo', 'some_int' => 123 },
29
+ { 'test_id' => 'bar', 'some_int' => 124 }]
30
+ end
31
+
32
+ it 'should listen to publishing errors and republish as Deimos events' do
33
+ Deimos.subscribe('produce_error') do |event|
34
+ expect(event.payload).to include(
35
+ producer: MyProducer,
36
+ topic: 'my-topic',
37
+ payloads: payloads
38
+ )
39
+ end
40
+ expect(Deimos.config.metrics).to receive(:increment).
41
+ with('publish_error', tags: %w(topic:my-topic), by: 2)
42
+ expect { MyProducer.publish_list(payloads) }.to raise_error(Kafka::DeliveryFailed)
43
+ end
44
+
45
+ it 'should not send any notifications when producer is not found' do
46
+ Deimos.subscribe('produce_error') do |_|
47
+ raise 'OH NOES'
48
+ end
49
+ allow(Deimos::Producer).to receive(:descendants).and_return([])
50
+ expect(Deimos.config.metrics).not_to receive(:increment).with('publish_error', anything)
51
+ expect { MyProducer.publish_list(payloads) }.to raise_error(Kafka::DeliveryFailed)
52
+ end
53
+ end
54
+ end
@@ -37,22 +37,45 @@ each_db_config(Deimos::KafkaTopicInfo) do
37
37
  end
38
38
 
39
39
  specify '#clear_lock' do
40
- described_class.create!(topic: 'my-topic', locked_by: 'abc',
41
- locked_at: 10.seconds.ago, error: true, retries: 1)
42
- described_class.create!(topic: 'my-topic2', locked_by: 'def',
43
- locked_at: 10.seconds.ago, error: true, retries: 1)
44
- described_class.clear_lock('my-topic', 'abc')
45
- expect(described_class.count).to eq(2)
46
- record = described_class.first
47
- expect(record.locked_by).to eq(nil)
48
- expect(record.locked_at).to eq(nil)
49
- expect(record.error).to eq(false)
50
- expect(record.retries).to eq(0)
51
- record = described_class.last
52
- expect(record.locked_by).not_to eq(nil)
53
- expect(record.locked_at).not_to eq(nil)
54
- expect(record.error).not_to eq(false)
55
- expect(record.retries).not_to eq(0)
40
+ freeze_time do
41
+ Deimos::KafkaTopicInfo.create!(topic: 'my-topic', locked_by: 'abc',
42
+ locked_at: 10.seconds.ago, error: true, retries: 1,
43
+ last_processed_at: 20.seconds.ago)
44
+ Deimos::KafkaTopicInfo.create!(topic: 'my-topic2', locked_by: 'def',
45
+ locked_at: 10.seconds.ago, error: true, retries: 1,
46
+ last_processed_at: 20.seconds.ago)
47
+ Deimos::KafkaTopicInfo.clear_lock('my-topic', 'abc')
48
+ expect(Deimos::KafkaTopicInfo.count).to eq(2)
49
+ record = Deimos::KafkaTopicInfo.first
50
+ expect(record.locked_by).to eq(nil)
51
+ expect(record.locked_at).to eq(nil)
52
+ expect(record.error).to eq(false)
53
+ expect(record.retries).to eq(0)
54
+ expect(record.last_processed_at.to_s).to eq(Time.zone.now.to_s)
55
+ record = Deimos::KafkaTopicInfo.last
56
+ expect(record.locked_by).not_to eq(nil)
57
+ expect(record.locked_at).not_to eq(nil)
58
+ expect(record.error).not_to eq(false)
59
+ expect(record.retries).not_to eq(0)
60
+ expect(record.last_processed_at.to_s).to eq(20.seconds.ago.to_s)
61
+ end
62
+ end
63
+
64
+ specify '#ping_empty_topics' do
65
+ freeze_time do
66
+ old_time = 1.hour.ago.to_s
67
+ t1 = Deimos::KafkaTopicInfo.create!(topic: 'topic1', last_processed_at: old_time)
68
+ t2 = Deimos::KafkaTopicInfo.create!(topic: 'topic2', last_processed_at: old_time)
69
+ t3 = Deimos::KafkaTopicInfo.create!(topic: 'topic3', last_processed_at: old_time,
70
+ locked_by: 'me', locked_at: 1.minute.ago)
71
+
72
+ expect(Deimos::KafkaTopicInfo.count).to eq(3)
73
+ Deimos::KafkaTopicInfo.all.each { |t| expect(t.last_processed_at.to_s).to eq(old_time) }
74
+ Deimos::KafkaTopicInfo.ping_empty_topics(%w(topic1))
75
+ expect(t1.reload.last_processed_at.to_s).to eq(old_time) # was passed as an exception
76
+ expect(t2.reload.last_processed_at.to_s).to eq(Time.zone.now.to_s)
77
+ expect(t3.reload.last_processed_at.to_s).to eq(old_time) # is locked
78
+ end
56
79
  end
57
80
 
58
81
  specify '#register_error' do
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe(Deimos::Message) do
4
+ it 'should detect tombstones' do
5
+ expect(described_class.new(nil, nil, key: 'key1')).
6
+ to be_tombstone
7
+ expect(described_class.new({ v: 'val1' }, nil, key: 'key1')).
8
+ not_to be_tombstone
9
+ expect(described_class.new({ v: '' }, nil, key: 'key1')).
10
+ not_to be_tombstone
11
+ expect(described_class.new({ v: 'val1' }, nil, key: nil)).
12
+ not_to be_tombstone
13
+ end
14
+
15
+ it 'can support complex keys/values' do
16
+ expect { described_class.new({ a: 1, b: 2 }, nil, key: { c: 3, d: 4 }) }.
17
+ not_to raise_exception
18
+ end
19
+ end
@@ -41,6 +41,14 @@ module ProducerTest
41
41
  end
42
42
  stub_const('MyNoKeyProducer', producer_class)
43
43
 
44
+ producer_class = Class.new(Deimos::Producer) do
45
+ schema 'MyNestedSchema'
46
+ namespace 'com.my-namespace'
47
+ topic 'my-topic'
48
+ key_config field: 'test_id'
49
+ end
50
+ stub_const('MyNestedSchemaProducer', producer_class)
51
+
44
52
  producer_class = Class.new(Deimos::Producer) do
45
53
  schema 'MySchema'
46
54
  namespace 'com.my-namespace'
@@ -233,6 +241,32 @@ module ProducerTest
233
241
  )
234
242
  end
235
243
 
244
+ it 'should properly encode and coerce values with a nested record' do
245
+ expect(MyNestedSchemaProducer.encoder).to receive(:encode_key).with('test_id', 'foo', topic: 'my-topic-key')
246
+ MyNestedSchemaProducer.publish(
247
+ 'test_id' => 'foo',
248
+ 'test_float' => BigDecimal('123.456'),
249
+ 'some_nested_record' => {
250
+ 'some_int' => 123,
251
+ 'some_float' => BigDecimal('456.789'),
252
+ 'some_string' => '123',
253
+ 'some_optional_int' => nil
254
+ },
255
+ 'some_optional_record' => nil
256
+ )
257
+ expect(MyNestedSchemaProducer.topic).to have_sent(
258
+ 'test_id' => 'foo',
259
+ 'test_float' => 123.456,
260
+ 'some_nested_record' => {
261
+ 'some_int' => 123,
262
+ 'some_float' => 456.789,
263
+ 'some_string' => '123',
264
+ 'some_optional_int' => nil
265
+ },
266
+ 'some_optional_record' => nil
267
+ )
268
+ end
269
+
236
270
  it 'should error with nothing set' do
237
271
  expect {
238
272
  MyErrorProducer.publish_list(
@@ -0,0 +1,71 @@
1
+ {
2
+ "namespace": "com.my-namespace",
3
+ "name": "Generated",
4
+ "type": "record",
5
+ "doc": "Test schema",
6
+ "fields": [
7
+ {
8
+ "name": "a_string",
9
+ "type": "string"
10
+ },
11
+ {
12
+ "name": "a_int",
13
+ "type": "int"
14
+ },
15
+ {
16
+ "name": "a_long",
17
+ "type": "long"
18
+ },
19
+ {
20
+ "name": "a_float",
21
+ "type": "float"
22
+ },
23
+ {
24
+ "name": "a_double",
25
+ "type": "double"
26
+ },
27
+ {
28
+ "name": "an_enum",
29
+ "type": {
30
+ "type": "enum",
31
+ "name": "AnEnum",
32
+ "symbols": ["sym1", "sym2"]
33
+ }
34
+ },
35
+ {
36
+ "name": "an_array",
37
+ "type": {
38
+ "type": "array",
39
+ "items": "int"
40
+ }
41
+ },
42
+ {
43
+ "name": "a_map",
44
+ "type": {
45
+ "type": "map",
46
+ "values": "string"
47
+ }
48
+ },
49
+ {
50
+ "name": "timestamp",
51
+ "type": "string"
52
+ },
53
+ {
54
+ "name": "message_id",
55
+ "type": "string"
56
+ },
57
+ {
58
+ "name": "a_record",
59
+ "type": {
60
+ "type": "record",
61
+ "name": "ARecord",
62
+ "fields": [
63
+ {
64
+ "name": "a_record_field",
65
+ "type": "string"
66
+ }
67
+ ]
68
+ }
69
+ }
70
+ ]
71
+ }
@@ -0,0 +1,55 @@
1
+ {
2
+ "namespace": "com.my-namespace",
3
+ "name": "MyNestedSchema",
4
+ "type": "record",
5
+ "doc": "Test schema",
6
+ "fields": [
7
+ {
8
+ "name": "test_id",
9
+ "type": "string",
10
+ "doc": "test string"
11
+ },
12
+ {
13
+ "name": "test_float",
14
+ "type": "float",
15
+ "doc": "test float"
16
+ },
17
+ {
18
+ "name": "some_nested_record",
19
+ "doc": "some nested record",
20
+ "type": {
21
+ "name": "MyNestedRecord",
22
+ "type": "record",
23
+ "fields": [
24
+ {
25
+ "name": "some_int",
26
+ "type": "int",
27
+ "doc": "some int"
28
+ },
29
+ {
30
+ "name": "some_float",
31
+ "type": "float",
32
+ "doc": "some float"
33
+ },
34
+ {
35
+ "name": "some_string",
36
+ "type": "string",
37
+ "doc": "some string"
38
+ },
39
+ {
40
+ "name": "some_optional_int",
41
+ "type": [ "null", "int" ],
42
+ "doc": "some optional int",
43
+ "default": null
44
+ }
45
+ ]
46
+ }
47
+ },
48
+ {
49
+ "name": "some_optional_record",
50
+ "doc": "some optional record",
51
+ "type": [ "null", "MyNestedRecord" ],
52
+ "default": null
53
+ }
54
+ ]
55
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "namespace": "com.my-namespace",
3
+ "name": "MySchemaCompound-key",
4
+ "type": "record",
5
+ "doc": "Test schema",
6
+ "fields": [
7
+ {
8
+ "name": "part_one",
9
+ "type": "string",
10
+ "doc": "test string one"
11
+ },
12
+ {
13
+ "name": "part_two",
14
+ "type": "string",
15
+ "doc": "test string two"
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "namespace": "com.my-namespace",
3
+ "name": "Wibble",
4
+ "type": "record",
5
+ "fields": [
6
+ {
7
+ "name": "id",
8
+ "type": "long"
9
+ },
10
+ {
11
+ "name": "wibble_id",
12
+ "type": "long"
13
+ },
14
+ {
15
+ "name": "name",
16
+ "type": "string"
17
+ },
18
+ {
19
+ "name": "floop",
20
+ "type": "string"
21
+ },
22
+ {
23
+ "name": "birthday_int",
24
+ "type": "int"
25
+ },
26
+ {
27
+ "name": "birthday_long",
28
+ "type": "long"
29
+ },
30
+ {
31
+ "name": "birthday_optional",
32
+ "type": ["null", "int"]
33
+ },
34
+ {
35
+ "name": "updated_at",
36
+ "type": "long"
37
+ },
38
+ {
39
+ "name": "created_at",
40
+ "type": "long"
41
+ }
42
+ ]
43
+ }