deimos-ruby 1.6.1 → 1.8.0.pre.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +9 -0
  3. data/.rubocop.yml +15 -13
  4. data/.ruby-version +1 -1
  5. data/CHANGELOG.md +30 -0
  6. data/Gemfile.lock +87 -80
  7. data/README.md +139 -15
  8. data/Rakefile +1 -1
  9. data/deimos-ruby.gemspec +3 -2
  10. data/docs/ARCHITECTURE.md +144 -0
  11. data/docs/CONFIGURATION.md +27 -0
  12. data/lib/deimos.rb +7 -6
  13. data/lib/deimos/active_record_consume/batch_consumption.rb +159 -0
  14. data/lib/deimos/active_record_consume/batch_slicer.rb +27 -0
  15. data/lib/deimos/active_record_consume/message_consumption.rb +58 -0
  16. data/lib/deimos/active_record_consume/schema_model_converter.rb +52 -0
  17. data/lib/deimos/active_record_consumer.rb +33 -75
  18. data/lib/deimos/active_record_producer.rb +23 -0
  19. data/lib/deimos/batch_consumer.rb +2 -140
  20. data/lib/deimos/config/configuration.rb +28 -10
  21. data/lib/deimos/consume/batch_consumption.rb +148 -0
  22. data/lib/deimos/consume/message_consumption.rb +93 -0
  23. data/lib/deimos/consumer.rb +79 -69
  24. data/lib/deimos/kafka_message.rb +1 -1
  25. data/lib/deimos/kafka_source.rb +29 -23
  26. data/lib/deimos/kafka_topic_info.rb +1 -1
  27. data/lib/deimos/message.rb +6 -1
  28. data/lib/deimos/metrics/provider.rb +0 -2
  29. data/lib/deimos/poll_info.rb +9 -0
  30. data/lib/deimos/tracing/provider.rb +0 -2
  31. data/lib/deimos/utils/db_poller.rb +149 -0
  32. data/lib/deimos/utils/db_producer.rb +8 -3
  33. data/lib/deimos/utils/deadlock_retry.rb +68 -0
  34. data/lib/deimos/utils/lag_reporter.rb +19 -26
  35. data/lib/deimos/version.rb +1 -1
  36. data/lib/generators/deimos/db_poller/templates/migration +11 -0
  37. data/lib/generators/deimos/db_poller/templates/rails3_migration +16 -0
  38. data/lib/generators/deimos/db_poller_generator.rb +48 -0
  39. data/lib/tasks/deimos.rake +7 -0
  40. data/spec/active_record_batch_consumer_spec.rb +481 -0
  41. data/spec/active_record_consume/batch_slicer_spec.rb +42 -0
  42. data/spec/active_record_consume/schema_model_converter_spec.rb +105 -0
  43. data/spec/active_record_consumer_spec.rb +22 -11
  44. data/spec/active_record_producer_spec.rb +66 -88
  45. data/spec/batch_consumer_spec.rb +23 -7
  46. data/spec/config/configuration_spec.rb +4 -0
  47. data/spec/consumer_spec.rb +8 -8
  48. data/spec/deimos_spec.rb +57 -49
  49. data/spec/handlers/my_batch_consumer.rb +6 -1
  50. data/spec/handlers/my_consumer.rb +6 -1
  51. data/spec/kafka_source_spec.rb +53 -0
  52. data/spec/message_spec.rb +19 -0
  53. data/spec/producer_spec.rb +3 -3
  54. data/spec/rake_spec.rb +1 -1
  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/spec_helper.rb +61 -6
  58. data/spec/utils/db_poller_spec.rb +320 -0
  59. data/spec/utils/deadlock_retry_spec.rb +74 -0
  60. data/spec/utils/lag_reporter_spec.rb +29 -22
  61. metadata +61 -20
  62. data/lib/deimos/base_consumer.rb +0 -104
  63. data/lib/deimos/utils/executor.rb +0 -124
  64. data/lib/deimos/utils/platform_schema_validation.rb +0 -0
  65. data/lib/deimos/utils/signal_handler.rb +0 -68
  66. data/spec/utils/executor_spec.rb +0 -53
  67. data/spec/utils/signal_handler_spec.rb +0 -16
@@ -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'
@@ -90,7 +90,7 @@ module ConsumerTest
90
90
  'some_int' => 123 },
91
91
  { skip_expectation: true }
92
92
  ) { raise 'OH NOES' }
93
- } .not_to raise_error
93
+ }.not_to raise_error
94
94
  end
95
95
 
96
96
  it 'should not fail when consume fails without reraising errors' do
@@ -101,7 +101,7 @@ module ConsumerTest
101
101
  { 'invalid' => 'key' },
102
102
  { skip_expectation: true }
103
103
  )
104
- } .not_to raise_error
104
+ }.not_to raise_error
105
105
  end
106
106
 
107
107
  it 'should call original' do
@@ -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
@@ -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
@@ -126,6 +126,59 @@ module KafkaSourceSpec
126
126
  }, widgets[2].id)
127
127
  end
128
128
 
129
+ it 'should send events on import with on_duplicate_key_update and existing records' do
130
+ widget1 = Widget.create(widget_id: 1, name: 'Widget 1')
131
+ widget2 = Widget.create(widget_id: 2, name: 'Widget 2')
132
+ widget1.name = 'New Widget 1'
133
+ widget2.name = 'New Widget 2'
134
+ Widget.import([widget1, widget2], on_duplicate_key_update: %i(widget_id name))
135
+
136
+ expect('my-topic').to have_sent({
137
+ widget_id: 1,
138
+ name: 'New Widget 1',
139
+ id: widget1.id,
140
+ created_at: anything,
141
+ updated_at: anything
142
+ }, widget1.id)
143
+ expect('my-topic').to have_sent({
144
+ widget_id: 2,
145
+ name: 'New Widget 2',
146
+ id: widget2.id,
147
+ created_at: anything,
148
+ updated_at: anything
149
+ }, widget2.id)
150
+ end
151
+
152
+ it 'should not fail when mixing existing and new records for import :on_duplicate_key_update' do
153
+ widget1 = Widget.create(widget_id: 1, name: 'Widget 1')
154
+ expect('my-topic').to have_sent({
155
+ widget_id: 1,
156
+ name: 'Widget 1',
157
+ id: widget1.id,
158
+ created_at: anything,
159
+ updated_at: anything
160
+ }, widget1.id)
161
+
162
+ widget2 = Widget.new(widget_id: 2, name: 'Widget 2')
163
+ widget1.name = 'New Widget 1'
164
+ Widget.import([widget1, widget2], on_duplicate_key_update: %i(widget_id))
165
+ widgets = Widget.all
166
+ expect('my-topic').to have_sent({
167
+ widget_id: 1,
168
+ name: 'New Widget 1',
169
+ id: widgets[0].id,
170
+ created_at: anything,
171
+ updated_at: anything
172
+ }, widgets[0].id)
173
+ expect('my-topic').to have_sent({
174
+ widget_id: 2,
175
+ name: 'Widget 2',
176
+ id: widgets[1].id,
177
+ created_at: anything,
178
+ updated_at: anything
179
+ }, widgets[1].id)
180
+ end
181
+
129
182
  it 'should send events even if the save fails' do
130
183
  widget = Widget.create!(widget_id: 1, name: 'widget')
131
184
  expect('my-topic').to have_sent({
@@ -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
@@ -148,7 +148,7 @@ module ProducerTest
148
148
  Deimos.disable_producers do
149
149
  raise 'OH NOES'
150
150
  end
151
- } .to raise_error('OH NOES')
151
+ }.to raise_error('OH NOES')
152
152
  expect(Deimos).not_to be_producers_disabled
153
153
  end
154
154
 
@@ -246,7 +246,7 @@ module ProducerTest
246
246
  MyNonEncodedProducer.publish_list(
247
247
  [{ 'test_id' => 'foo', 'some_int' => 123 }]
248
248
  )
249
- } .to raise_error('No key given but a key is required! Use `key_config none: true` to avoid using keys.')
249
+ }.to raise_error('No key given but a key is required! Use `key_config none: true` to avoid using keys.')
250
250
  end
251
251
 
252
252
  it 'should allow nil keys if none: true is configured' do
@@ -254,7 +254,7 @@ module ProducerTest
254
254
  MyNoKeyProducer.publish_list(
255
255
  [{ 'test_id' => 'foo', 'some_int' => 123 }]
256
256
  )
257
- } .not_to raise_error
257
+ }.not_to raise_error
258
258
  end
259
259
 
260
260
  it 'should use a partition key' do
@@ -9,7 +9,7 @@ if Rake.application.lookup(:environment).nil?
9
9
  Rake::Task.define_task(:environment)
10
10
  end
11
11
 
12
- describe 'Rakefile' do # rubocop:disable RSpec/DescribeClass
12
+ describe 'Rakefile' do
13
13
  it 'should start listeners' do
14
14
  runner = instance_double(Phobos::CLI::Runner)
15
15
  expect(Phobos::CLI::Runner).to receive(:new).and_return(runner)
@@ -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
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
4
4
  require 'active_record'
5
+ require 'database_cleaner'
5
6
  require 'deimos'
6
7
  require 'deimos/metrics/mock'
7
8
  require 'deimos/tracing/mock'
@@ -100,9 +101,8 @@ module DbConfigs
100
101
  end
101
102
  end
102
103
 
103
- # Set up the given database.
104
- def setup_db(options)
105
- ActiveRecord::Base.establish_connection(options)
104
+ # :nodoc:
105
+ def run_db_backend_migration
106
106
  migration_class_name = 'DbBackendMigration'
107
107
  migration_version = '[5.2]'
108
108
  migration = ERB.new(
@@ -110,6 +110,24 @@ module DbConfigs
110
110
  ).result(binding)
111
111
  eval(migration) # rubocop:disable Security/Eval
112
112
  ActiveRecord::Migration.new.run(DbBackendMigration, direction: :up)
113
+ end
114
+
115
+ # :nodoc:
116
+ def run_db_poller_migration
117
+ migration_class_name = 'DbPollerMigration'
118
+ migration_version = '[5.2]'
119
+ migration = ERB.new(
120
+ File.read('lib/generators/deimos/db_poller/templates/migration')
121
+ ).result(binding)
122
+ eval(migration) # rubocop:disable Security/Eval
123
+ ActiveRecord::Migration.new.run(DbPollerMigration, direction: :up)
124
+ end
125
+
126
+ # Set up the given database.
127
+ def setup_db(options)
128
+ ActiveRecord::Base.establish_connection(options)
129
+ run_db_backend_migration
130
+ run_db_poller_migration
113
131
 
114
132
  ActiveRecord::Base.descendants.each do |klass|
115
133
  klass.reset_sequence_name if klass.respond_to?(:reset_sequence_name)
@@ -130,8 +148,11 @@ RSpec.configure do |config|
130
148
  # true by default for RSpec 4.0
131
149
  config.shared_context_metadata_behavior = :apply_to_host_groups
132
150
 
151
+ config.filter_run(focus: true)
152
+ config.run_all_when_everything_filtered = true
153
+
133
154
  config.before(:all) do
134
- Time.zone = 'EST'
155
+ Time.zone = 'Eastern Time (US & Canada)'
135
156
  ActiveRecord::Base.logger = Logger.new('/dev/null')
136
157
  ActiveRecord::Base.establish_connection(
137
158
  'adapter' => 'sqlite3',
@@ -141,9 +162,10 @@ RSpec.configure do |config|
141
162
  config.include Deimos::TestHelpers
142
163
  config.include ActiveSupport::Testing::TimeHelpers
143
164
  config.before(:suite) do
144
- Time.zone = 'EST'
145
- ActiveRecord::Base.logger = Logger.new('/dev/null')
146
165
  setup_db(DbConfigs::DB_OPTIONS.last)
166
+
167
+ DatabaseCleaner.strategy = :transaction
168
+ DatabaseCleaner.clean_with(:truncation)
147
169
  end
148
170
 
149
171
  config.mock_with(:rspec) do |mocks|
@@ -164,6 +186,39 @@ RSpec.configure do |config|
164
186
  deimos_config.schema.backend = :avro_validation
165
187
  end
166
188
  end
189
+
190
+ config.around(:each) do |example|
191
+ use_cleaner = !example.metadata[:integration]
192
+
193
+ DatabaseCleaner.start if use_cleaner
194
+
195
+ example.run
196
+
197
+ DatabaseCleaner.clean if use_cleaner
198
+ end
199
+ end
200
+
201
+ RSpec.shared_context('with widgets') do
202
+ before(:all) do
203
+ ActiveRecord::Base.connection.create_table(:widgets, force: true) do |t|
204
+ t.string(:test_id)
205
+ t.integer(:some_int)
206
+ t.boolean(:some_bool)
207
+ t.timestamps
208
+ end
209
+
210
+ # :nodoc:
211
+ class Widget < ActiveRecord::Base
212
+ # @return [String]
213
+ def generated_id
214
+ 'generated_id'
215
+ end
216
+ end
217
+ end
218
+
219
+ after(:all) do
220
+ ActiveRecord::Base.connection.drop_table(:widgets)
221
+ end
167
222
  end
168
223
 
169
224
  RSpec.shared_context('with DB') do