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,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Deimos::ActiveRecordConsume::BatchSlicer do
4
+ describe '#slice' do
5
+ let(:batch) do
6
+ [
7
+ Deimos::Message.new({ v: 1 }, nil, key: 'C'),
8
+ Deimos::Message.new({ v: 123 }, nil, key: 'A'),
9
+ Deimos::Message.new({ v: 999 }, nil, key: 'B'),
10
+ Deimos::Message.new({ v: 456 }, nil, key: 'A'),
11
+ Deimos::Message.new({ v: 2 }, nil, key: 'C'),
12
+ Deimos::Message.new({ v: 3 }, nil, key: 'C')
13
+ ]
14
+ end
15
+
16
+ it 'should slice a batch by key' do
17
+ slices = described_class.slice(batch)
18
+
19
+ expect(slices).
20
+ to match([
21
+ match_array([
22
+ Deimos::Message.new({ v: 1 }, nil, key: 'C'),
23
+ Deimos::Message.new({ v: 123 }, nil, key: 'A'),
24
+ Deimos::Message.new({ v: 999 }, nil, key: 'B')
25
+ ]),
26
+ match_array([
27
+ Deimos::Message.new({ v: 456 }, nil, key: 'A'),
28
+ Deimos::Message.new({ v: 2 }, nil, key: 'C')
29
+ ]),
30
+ match_array([
31
+ Deimos::Message.new({ v: 3 }, nil, key: 'C')
32
+ ])
33
+ ])
34
+ end
35
+
36
+ it 'should handle empty batches' do
37
+ slices = described_class.slice([])
38
+
39
+ expect(slices).to be_empty
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'deimos/active_record_consume/schema_model_converter'
4
+ require 'deimos/schema_backends/avro_local'
5
+
6
+ # Wrapped in a module to prevent class leakage
7
+ module SchemaModelConverterTest
8
+ describe Deimos::ActiveRecordConsume::SchemaModelConverter do
9
+ # Create ActiveRecord table and model
10
+ before(:all) do
11
+ ActiveRecord::Base.connection.create_table(:wibbles, force: true) do |t|
12
+ t.integer(:wibble_id)
13
+ t.string(:name)
14
+ t.integer(:bar)
15
+ t.datetime(:birthday_int)
16
+ t.datetime(:birthday_long)
17
+ t.datetime(:birthday_optional)
18
+ t.timestamps
19
+ end
20
+
21
+ # :nodoc:
22
+ class Wibble < ActiveRecord::Base
23
+ end
24
+ Wibble.reset_column_information
25
+ end
26
+
27
+ after(:all) do
28
+ ActiveRecord::Base.connection.drop_table(:wibbles)
29
+ end
30
+
31
+ let(:schema) { Deimos::SchemaBackends::AvroLocal.new(schema: 'Wibble', namespace: 'com.my-namespace') }
32
+ let(:inst) { described_class.new(schema, Wibble) }
33
+
34
+ describe '#convert' do
35
+ it 'should extract attributes from the payload' do
36
+ payload = { 'id' => 123, 'wibble_id' => 456, 'name' => 'wibble' }
37
+
38
+ expect(inst.convert(payload)).to include('id' => 123, 'wibble_id' => 456, 'name' => 'wibble')
39
+ end
40
+
41
+ it 'should ignore payload fields that don\'t exist' do
42
+ payload = { 'foo' => 'abc' }
43
+
44
+ expect(inst.convert(payload)).not_to include('foo')
45
+ end
46
+
47
+ it 'should ignore fields in the schema but not in the model' do
48
+ payload = { 'floop' => 'def' }
49
+
50
+ expect(inst.convert(payload)).not_to include('floop')
51
+ end
52
+
53
+ it 'should ignore model fields not in the schema' do
54
+ payload = { 'bar' => 'xyz' }
55
+
56
+ expect(inst.convert(payload)).not_to include('bar')
57
+ end
58
+
59
+ it 'should handle nils' do
60
+ payload = { 'name' => nil }
61
+
62
+ expect(inst.convert(payload)['name']).to be_nil
63
+ end
64
+
65
+ describe 'timestamps' do
66
+ it 'should ignore AR timestamp fields' do
67
+ payload = { 'updated_at' => '1234567890', 'created_at' => '2345678901' }
68
+
69
+ expect(inst.convert(payload)).not_to include('updated_at', 'created_at')
70
+ end
71
+
72
+ it 'should parse timestamps' do
73
+ first = Time.zone.local(2019, 1, 1, 11, 12, 13)
74
+ second = Time.zone.local(2019, 2, 2, 12, 13, 14)
75
+
76
+ payload = { 'birthday_int' => first.to_i, 'birthday_long' => second.to_i }
77
+
78
+ expect(inst.convert(payload)).to include('birthday_int' => first, 'birthday_long' => second)
79
+ end
80
+
81
+ it 'should parse integer-string timestamps' do
82
+ date = Time.zone.local(2019, 1, 1, 11, 12, 13)
83
+
84
+ payload = { 'birthday_int' => '1546359133' }
85
+
86
+ expect(inst.convert(payload)).to include('birthday_int' => date)
87
+ end
88
+
89
+ it 'should ignore other strings for timestamps' do
90
+ payload = { 'birthday_int' => 'some-other-val' }
91
+
92
+ expect(inst.convert(payload)).to include('birthday_int' => 'some-other-val')
93
+ end
94
+ end
95
+
96
+ it 'should coerce nullable unions' do
97
+ date = Time.zone.local(2019, 1, 1, 11, 12, 13)
98
+
99
+ payload = { 'birthday_optional' => date.to_i }
100
+
101
+ expect(inst.convert(payload)).to include('birthday_optional' => date)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -2,9 +2,9 @@
2
2
 
3
3
  require 'date'
4
4
 
5
- # :nodoc:
6
- module ActiveRecordProducerTest
7
- describe Deimos::ActiveRecordConsumer do
5
+ # Wrapped in a module to prevent class leakage
6
+ module ActiveRecordConsumerTest
7
+ describe Deimos::ActiveRecordConsumer, 'Message Consumer' do
8
8
 
9
9
  before(:all) do
10
10
  ActiveRecord::Base.connection.create_table(:widgets, force: true) do |t|
@@ -137,13 +137,5 @@ module ActiveRecordProducerTest
137
137
  expect(Widget.find_by_test_id('id1').some_int).to eq(3)
138
138
  expect(Widget.find_by_test_id('id2').some_int).to eq(4)
139
139
  end
140
-
141
- it 'should coerce int values to datetimes' do
142
- column = Widget.columns.find { |c| c.name == 'some_datetime_int' }
143
- expect(MyConsumer.new.send(:_coerce_field, column, 1_579_046_400)).to eq('2020-01-14 19:00:00 -0500')
144
- expect(MyConsumer.new.send(:_coerce_field, column, '1579046400')).to eq('2020-01-14 19:00:00 -0500')
145
- expect(MyConsumer.new.send(:_coerce_field, column, 'some-other-val')).to eq('some-other-val')
146
- end
147
-
148
140
  end
149
141
  end
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'deimos/batch_consumer'
4
-
5
3
  # :nodoc:
6
4
  module ConsumerTest
7
- describe Deimos::BatchConsumer do
5
+ describe Deimos::Consumer, 'Batch Consumer' do
8
6
 
9
7
  prepend_before(:each) do
10
8
  # :nodoc:
11
- consumer_class = Class.new(Deimos::BatchConsumer) do
9
+ consumer_class = Class.new(described_class) do
12
10
  schema 'MySchema'
13
11
  namespace 'com.my-namespace'
14
12
  key_config field: 'test_id'
@@ -32,6 +30,24 @@ module ConsumerTest
32
30
  batch.concat([{ 'invalid' => 'key' }])
33
31
  end
34
32
 
33
+ it 'should provide backwards compatibility for BatchConsumer class' do
34
+ consumer_class = Class.new(Deimos::BatchConsumer) do
35
+ schema 'MySchema'
36
+ namespace 'com.my-namespace'
37
+ key_config field: 'test_id'
38
+
39
+ # :nodoc:
40
+ def consume_batch(_payloads, _metadata)
41
+ raise 'This should not be called unless call_original is set'
42
+ end
43
+ end
44
+ stub_const('ConsumerTest::MyOldBatchConsumer', consumer_class)
45
+
46
+ test_consume_batch(MyOldBatchConsumer, batch) do |received, _metadata|
47
+ expect(received).to eq(batch)
48
+ end
49
+ end
50
+
35
51
  it 'should consume a batch of messages' do
36
52
  test_consume_batch(MyBatchConsumer, batch) do |received, _metadata|
37
53
  expect(received).to eq(batch)
@@ -95,11 +111,12 @@ module ConsumerTest
95
111
  test_consume_batch('my_batch_consume_topic', batch, keys: keys) do |_received, metadata|
96
112
  # Mock decode_key extracts the value of the first field as the key
97
113
  expect(metadata[:keys]).to eq(%w(foo bar))
114
+ expect(metadata[:first_offset]).to eq(1)
98
115
  end
99
116
  end
100
117
 
101
118
  it 'should decode plain keys for all messages in the batch' do
102
- consumer_class = Class.new(Deimos::BatchConsumer) do
119
+ consumer_class = Class.new(described_class) do
103
120
  schema 'MySchema'
104
121
  namespace 'com.my-namespace'
105
122
  key_config plain: true
@@ -115,7 +132,7 @@ module ConsumerTest
115
132
  describe 'timestamps' do
116
133
  before(:each) do
117
134
  # :nodoc:
118
- consumer_class = Class.new(Deimos::BatchConsumer) do
135
+ consumer_class = Class.new(described_class) do
119
136
  schema 'MySchemaWithDateTimes'
120
137
  namespace 'com.my-namespace'
121
138
  key_config plain: true
@@ -197,7 +214,7 @@ module ConsumerTest
197
214
  describe 'logging' do
198
215
  before(:each) do
199
216
  # :nodoc:
200
- consumer_class = Class.new(Deimos::BatchConsumer) do
217
+ consumer_class = Class.new(described_class) do
201
218
  schema 'MySchemaWithUniqueId'
202
219
  namespace 'com.my-namespace'
203
220
  key_config plain: true
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Mock consumer
3
4
  class MyConfigConsumer < Deimos::Consumer
5
+ # :no-doc:
6
+ def consume
7
+ end
4
8
  end
5
9
  describe Deimos, 'configuration' do
6
10
  it 'should configure with deprecated fields' do
@@ -2,11 +2,11 @@
2
2
 
3
3
  # :nodoc:
4
4
  module ConsumerTest
5
- describe Deimos::Consumer do
5
+ describe Deimos::Consumer, 'Message Consumer' do
6
6
 
7
7
  prepend_before(:each) do
8
8
  # :nodoc:
9
- consumer_class = Class.new(Deimos::Consumer) do
9
+ consumer_class = Class.new(described_class) do
10
10
  schema 'MySchema'
11
11
  namespace 'com.my-namespace'
12
12
  key_config field: 'test_id'
@@ -121,7 +121,7 @@ module ConsumerTest
121
121
  end
122
122
 
123
123
  it 'should use the key schema if set' do
124
- consumer_class = Class.new(Deimos::Consumer) do
124
+ consumer_class = Class.new(described_class) do
125
125
  schema 'MySchema'
126
126
  namespace 'com.my-namespace'
127
127
  key_config schema: 'MySchema_key'
@@ -132,7 +132,7 @@ module ConsumerTest
132
132
  end
133
133
 
134
134
  it 'should not decode if plain is set' do
135
- consumer_class = Class.new(Deimos::Consumer) do
135
+ consumer_class = Class.new(described_class) do
136
136
  schema 'MySchema'
137
137
  namespace 'com.my-namespace'
138
138
  key_config plain: true
@@ -142,7 +142,7 @@ module ConsumerTest
142
142
  end
143
143
 
144
144
  it 'should error with nothing set' do
145
- consumer_class = Class.new(Deimos::Consumer) do
145
+ consumer_class = Class.new(described_class) do
146
146
  schema 'MySchema'
147
147
  namespace 'com.my-namespace'
148
148
  end
@@ -156,7 +156,7 @@ module ConsumerTest
156
156
  describe 'timestamps' do
157
157
  before(:each) do
158
158
  # :nodoc:
159
- consumer_class = Class.new(Deimos::Consumer) do
159
+ consumer_class = Class.new(described_class) do
160
160
  schema 'MySchemaWithDateTimes'
161
161
  namespace 'com.my-namespace'
162
162
  key_config plain: true
@@ -68,11 +68,11 @@ describe Deimos do
68
68
 
69
69
  describe '#start_db_backend!' do
70
70
  it 'should start if backend is db and thread_count is > 0' do
71
- signal_handler = instance_double(Deimos::Utils::SignalHandler)
71
+ signal_handler = instance_double(Sigurd::SignalHandler)
72
72
  allow(signal_handler).to receive(:run!)
73
- expect(Deimos::Utils::Executor).to receive(:new).
73
+ expect(Sigurd::Executor).to receive(:new).
74
74
  with(anything, sleep_seconds: 5, logger: anything).and_call_original
75
- expect(Deimos::Utils::SignalHandler).to receive(:new) do |executor|
75
+ expect(Sigurd::SignalHandler).to receive(:new) do |executor|
76
76
  expect(executor.runners.size).to eq(2)
77
77
  signal_handler
78
78
  end
@@ -83,7 +83,7 @@ describe Deimos do
83
83
  end
84
84
 
85
85
  it 'should not start if backend is not db' do
86
- expect(Deimos::Utils::SignalHandler).not_to receive(:new)
86
+ expect(Sigurd::SignalHandler).not_to receive(:new)
87
87
  described_class.configure do |config|
88
88
  config.producers.backend = :kafka
89
89
  end
@@ -92,7 +92,7 @@ describe Deimos do
92
92
  end
93
93
 
94
94
  it 'should not start if thread_count is nil' do
95
- expect(Deimos::Utils::SignalHandler).not_to receive(:new)
95
+ expect(Sigurd::SignalHandler).not_to receive(:new)
96
96
  described_class.configure do |config|
97
97
  config.producers.backend = :db
98
98
  end
@@ -101,61 +101,69 @@ describe Deimos do
101
101
  end
102
102
 
103
103
  it 'should not start if thread_count is 0' do
104
- expect(Deimos::Utils::SignalHandler).not_to receive(:new)
104
+ expect(Sigurd::SignalHandler).not_to receive(:new)
105
105
  described_class.configure do |config|
106
106
  config.producers.backend = :db
107
107
  end
108
108
  expect { described_class.start_db_backend!(thread_count: 0) }.
109
109
  to raise_error('Thread count is not given or set to zero, exiting')
110
110
  end
111
+ end
111
112
 
112
- describe 'delivery configuration' do
113
- before(:each) do
114
- allow(YAML).to receive(:load).and_return(phobos_configuration)
115
- end
116
-
117
- it 'should not raise an error with properly configured handlers' do
118
- path = config_path # for scope issues in the block below
119
- # Add explicit consumers
120
- phobos_configuration['listeners'] << { 'handler' => 'ConsumerTest::MyConsumer',
121
- 'delivery' => 'message' }
122
- phobos_configuration['listeners'] << { 'handler' => 'ConsumerTest::MyConsumer',
123
- 'delivery' => 'batch' }
124
-
125
- expect {
126
- described_class.configure { |c| c.phobos_config_file = path }
127
- }.not_to raise_error
128
- end
129
-
130
- it 'should raise an error if BatchConsumers do not have inline_batch delivery' do
131
- path = config_path # for scope issues in the block below
132
- phobos_configuration['listeners'] = [{ 'handler' => 'ConsumerTest::MyBatchConsumer',
133
- 'delivery' => 'message' }]
134
-
135
- expect {
136
- described_class.configure { |c| c.phobos_config_file = path }
137
- }.to raise_error('BatchConsumer ConsumerTest::MyBatchConsumer must have delivery set to `inline_batch`')
138
- end
139
-
140
- it 'should raise an error if Consumers do not have message or batch delivery' do
141
- path = config_path # for scope issues in the block below
142
- phobos_configuration['listeners'] = [{ 'handler' => 'ConsumerTest::MyConsumer',
143
- 'delivery' => 'inline_batch' }]
113
+ describe 'delivery configuration' do
114
+ before(:each) do
115
+ allow(YAML).to receive(:load).and_return(phobos_configuration)
116
+ end
144
117
 
145
- expect {
146
- described_class.configure { |c| c.phobos_config_file = path }
147
- }.to raise_error('Non-batch Consumer ConsumerTest::MyConsumer must have delivery set to `message` or `batch`')
148
- end
118
+ it 'should not raise an error with properly configured handlers' do
119
+ expect {
120
+ described_class.configure do
121
+ consumer do
122
+ class_name 'ConsumerTest::MyConsumer'
123
+ delivery :message
124
+ end
125
+ consumer do
126
+ class_name 'ConsumerTest::MyConsumer'
127
+ delivery :batch
128
+ end
129
+ consumer do
130
+ class_name 'ConsumerTest::MyBatchConsumer'
131
+ delivery :inline_batch
132
+ end
133
+ end
134
+ }.not_to raise_error
135
+ end
149
136
 
150
- it 'should treat nil as `batch`' do
151
- path = config_path # for scope issues in the block below
152
- phobos_configuration['listeners'] = [{ 'handler' => 'ConsumerTest::MyConsumer' }]
137
+ it 'should raise an error if inline_batch listeners do not implement consume_batch' do
138
+ expect {
139
+ described_class.configure do
140
+ consumer do
141
+ class_name 'ConsumerTest::MyConsumer'
142
+ delivery :inline_batch
143
+ end
144
+ end
145
+ }.to raise_error('BatchConsumer ConsumerTest::MyConsumer does not implement `consume_batch`')
146
+ end
153
147
 
154
- expect {
155
- described_class.configure { |c| c.phobos_config_file = path }
156
- }.not_to raise_error
157
- end
148
+ it 'should raise an error if Consumers do not have message or batch delivery' do
149
+ expect {
150
+ described_class.configure do
151
+ consumer do
152
+ class_name 'ConsumerTest::MyBatchConsumer'
153
+ delivery :message
154
+ end
155
+ end
156
+ }.to raise_error('Non-batch Consumer ConsumerTest::MyBatchConsumer does not implement `consume`')
157
+ end
158
158
 
159
+ it 'should treat nil as `batch`' do
160
+ expect {
161
+ described_class.configure do
162
+ consumer do
163
+ class_name 'ConsumerTest::MyConsumer'
164
+ end
165
+ end
166
+ }.not_to raise_error
159
167
  end
160
168
  end
161
169
  end