deimos-ruby 1.6.1 → 1.8.0.pre.beta1

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 (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