deimos-ruby 1.8.2 → 1.8.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 237cb6cd22b6a057003fedae1caeec81c999a3d9b90d837bb8b0a4add43ca4e2
4
- data.tar.gz: fab51933cdcd5c5fd10b8c92bbc67854fe56f23cdbec41660579f18b9075e09c
3
+ metadata.gz: 3c12e06b13f4b1910a93d955ddca456bdfaa0d8c5ee1a925cc1aea5c2d0f4df3
4
+ data.tar.gz: b4ef92834678153a746b585a0c6c5ff36cb32fc65cd3fc94accc8a35e7fd6feb
5
5
  SHA512:
6
- metadata.gz: 193b9b593b4f92edecc6c239eb6106fbe40e0acd1453694508f3fb02525166c39e4f1eba71230362493cd1647ab19e98a63ecf55f5371b844e9d0c38ffa5b2ea
7
- data.tar.gz: e7d42ec7ec2bc864735c9230a925685935678d2dd0f3918426926dc9f358d5eb810dd5b324bcacaf9201c78bdfbbfa58037a4cac4cf4ca66257737440b3d2c97
6
+ metadata.gz: 4c3eaf327c4483302847733e1a5134b2f559f4e0ed2e7b090d893292d0cde80ad7f9677c54b0f3b415d3933e377013c286fc133399705803419f0df6cb446f75
7
+ data.tar.gz: a3bb3aa32da8ed98ee4bb763cd5c123f1289042219e85b219629c9e6b4a3ddd75870e0c7d9ea74b0e7d24a6edc707c5d0af39b3e6847de3da3d271679113040f
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## UNRELEASED
9
9
 
10
+ ## 1.8.7 - 2021-01-14
11
+
12
+ - ### Roadmap :car:
13
+ - Update Phobos version to allow version 1.9 or 2.x.
14
+
15
+ ## 1.8.6 - 2021-01-14
16
+
17
+ - ### Fixes :wrench:
18
+ - Fix for configuration bug with Ruby 3.0 (** instead of passing hash)
19
+
20
+ ## 1.8.5 - 2021-01-13
21
+
22
+ - ### Fixes :wrench:
23
+ - Fixes for Rails 6.1 (remove usage of `update_attributes!`)
24
+
25
+ ## 1.8.4 - 2020-12-02
26
+
27
+ ### Features :star:
28
+ - Add overridable "process_message?" method to ActiveRecordConsumer to allow for skipping of saving/updating records
29
+
30
+ ### Fixes :wrench:
31
+
32
+ - Do not apply type coercion to `timestamp-millis` and `timestamp-micros` logical types (fixes [#97](https://github.com/flipp-oss/deimos/issues/97))
33
+
34
+ ## 1.8.3 - 2020-11-18
35
+
36
+ ### Fixes :wrench:
37
+ - Do not resend already sent messages when splitting up batches
38
+ (fixes [#24](https://github.com/flipp-oss/deimos/issues/24))
39
+ - KafkaSource crashing on bulk-imports if import hooks are disabled
40
+ (fixes [#73](https://github.com/flipp-oss/deimos/issues/73))
41
+ - #96 Use string-safe encoding for partition keys
42
+ - Retry on offset seek failures in inline consumer
43
+ (fixes [#5](Inline consumer should use retries when seeking))
44
+
10
45
  ## 1.8.2 - 2020-09-25
11
46
 
12
47
  ### Features :star:
@@ -1,72 +1,76 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- deimos-ruby (1.8.2)
4
+ deimos-ruby (1.8.7)
5
5
  avro_turf (~> 0.11)
6
- phobos (~> 1.9)
6
+ phobos (>= 1.9, < 3.0)
7
7
  ruby-kafka (~> 0.7)
8
8
  sigurd (= 0.0.1)
9
9
 
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- actioncable (6.0.3.2)
14
- actionpack (= 6.0.3.2)
13
+ actioncable (6.1.1)
14
+ actionpack (= 6.1.1)
15
+ activesupport (= 6.1.1)
15
16
  nio4r (~> 2.0)
16
17
  websocket-driver (>= 0.6.1)
17
- actionmailbox (6.0.3.2)
18
- actionpack (= 6.0.3.2)
19
- activejob (= 6.0.3.2)
20
- activerecord (= 6.0.3.2)
21
- activestorage (= 6.0.3.2)
22
- activesupport (= 6.0.3.2)
18
+ actionmailbox (6.1.1)
19
+ actionpack (= 6.1.1)
20
+ activejob (= 6.1.1)
21
+ activerecord (= 6.1.1)
22
+ activestorage (= 6.1.1)
23
+ activesupport (= 6.1.1)
23
24
  mail (>= 2.7.1)
24
- actionmailer (6.0.3.2)
25
- actionpack (= 6.0.3.2)
26
- actionview (= 6.0.3.2)
27
- activejob (= 6.0.3.2)
25
+ actionmailer (6.1.1)
26
+ actionpack (= 6.1.1)
27
+ actionview (= 6.1.1)
28
+ activejob (= 6.1.1)
29
+ activesupport (= 6.1.1)
28
30
  mail (~> 2.5, >= 2.5.4)
29
31
  rails-dom-testing (~> 2.0)
30
- actionpack (6.0.3.2)
31
- actionview (= 6.0.3.2)
32
- activesupport (= 6.0.3.2)
33
- rack (~> 2.0, >= 2.0.8)
32
+ actionpack (6.1.1)
33
+ actionview (= 6.1.1)
34
+ activesupport (= 6.1.1)
35
+ rack (~> 2.0, >= 2.0.9)
34
36
  rack-test (>= 0.6.3)
35
37
  rails-dom-testing (~> 2.0)
36
38
  rails-html-sanitizer (~> 1.0, >= 1.2.0)
37
- actiontext (6.0.3.2)
38
- actionpack (= 6.0.3.2)
39
- activerecord (= 6.0.3.2)
40
- activestorage (= 6.0.3.2)
41
- activesupport (= 6.0.3.2)
39
+ actiontext (6.1.1)
40
+ actionpack (= 6.1.1)
41
+ activerecord (= 6.1.1)
42
+ activestorage (= 6.1.1)
43
+ activesupport (= 6.1.1)
42
44
  nokogiri (>= 1.8.5)
43
- actionview (6.0.3.2)
44
- activesupport (= 6.0.3.2)
45
+ actionview (6.1.1)
46
+ activesupport (= 6.1.1)
45
47
  builder (~> 3.1)
46
48
  erubi (~> 1.4)
47
49
  rails-dom-testing (~> 2.0)
48
50
  rails-html-sanitizer (~> 1.1, >= 1.2.0)
49
- activejob (6.0.3.2)
50
- activesupport (= 6.0.3.2)
51
+ activejob (6.1.1)
52
+ activesupport (= 6.1.1)
51
53
  globalid (>= 0.3.6)
52
- activemodel (6.0.3.2)
53
- activesupport (= 6.0.3.2)
54
- activerecord (6.0.3.2)
55
- activemodel (= 6.0.3.2)
56
- activesupport (= 6.0.3.2)
57
- activerecord-import (1.0.5)
54
+ activemodel (6.1.1)
55
+ activesupport (= 6.1.1)
56
+ activerecord (6.1.1)
57
+ activemodel (= 6.1.1)
58
+ activesupport (= 6.1.1)
59
+ activerecord-import (1.0.7)
58
60
  activerecord (>= 3.2)
59
- activestorage (6.0.3.2)
60
- actionpack (= 6.0.3.2)
61
- activejob (= 6.0.3.2)
62
- activerecord (= 6.0.3.2)
61
+ activestorage (6.1.1)
62
+ actionpack (= 6.1.1)
63
+ activejob (= 6.1.1)
64
+ activerecord (= 6.1.1)
65
+ activesupport (= 6.1.1)
63
66
  marcel (~> 0.3.1)
64
- activesupport (6.0.3.2)
67
+ mimemagic (~> 0.3.2)
68
+ activesupport (6.1.1)
65
69
  concurrent-ruby (~> 1.0, >= 1.0.2)
66
- i18n (>= 0.7, < 2)
67
- minitest (~> 5.1)
68
- tzinfo (~> 1.1)
69
- zeitwerk (~> 2.2, >= 2.2.2)
70
+ i18n (>= 1.6, < 2)
71
+ minitest (>= 5.1)
72
+ tzinfo (~> 2.0)
73
+ zeitwerk (~> 2.3)
70
74
  ast (2.4.1)
71
75
  avro (1.9.2)
72
76
  multi_json
@@ -75,19 +79,19 @@ GEM
75
79
  excon (~> 0.45)
76
80
  builder (3.2.4)
77
81
  coderay (1.1.3)
78
- concurrent-ruby (1.1.6)
79
- concurrent-ruby-ext (1.1.6)
80
- concurrent-ruby (= 1.1.6)
82
+ concurrent-ruby (1.1.7)
83
+ concurrent-ruby-ext (1.1.7)
84
+ concurrent-ruby (= 1.1.7)
81
85
  crass (1.0.6)
82
86
  database_cleaner (1.8.5)
83
87
  ddtrace (0.37.0)
84
88
  msgpack
85
89
  diff-lcs (1.4.4)
86
- digest-crc (0.6.1)
87
- rake (~> 13.0)
90
+ digest-crc (0.6.3)
91
+ rake (>= 12.0.0, < 14.0.0)
88
92
  dogstatsd-ruby (4.8.1)
89
- erubi (1.9.0)
90
- excon (0.76.0)
93
+ erubi (1.10.0)
94
+ excon (0.78.1)
91
95
  exponential-backoff (0.0.4)
92
96
  ffi (1.13.1)
93
97
  formatador (0.2.5)
@@ -110,7 +114,7 @@ GEM
110
114
  guard-rubocop (1.3.0)
111
115
  guard (~> 2.0)
112
116
  rubocop (~> 0.20)
113
- i18n (1.8.4)
117
+ i18n (1.8.7)
114
118
  concurrent-ruby (~> 1.0)
115
119
  listen (3.2.1)
116
120
  rb-fsevent (~> 0.10, >= 0.10.3)
@@ -119,7 +123,7 @@ GEM
119
123
  logging (2.3.0)
120
124
  little-plugger (~> 1.1)
121
125
  multi_json (~> 1.14)
122
- loofah (2.6.0)
126
+ loofah (2.8.0)
123
127
  crass (~> 1.0.2)
124
128
  nokogiri (>= 1.5.9)
125
129
  lumberjack (1.2.6)
@@ -130,15 +134,16 @@ GEM
130
134
  method_source (1.0.0)
131
135
  mimemagic (0.3.5)
132
136
  mini_mime (1.0.2)
133
- mini_portile2 (2.4.0)
134
- minitest (5.14.1)
137
+ mini_portile2 (2.5.0)
138
+ minitest (5.14.3)
135
139
  msgpack (1.3.3)
136
140
  multi_json (1.15.0)
137
141
  mysql2 (0.5.3)
138
142
  nenv (0.3.0)
139
- nio4r (2.5.2)
140
- nokogiri (1.10.10)
141
- mini_portile2 (~> 2.4.0)
143
+ nio4r (2.5.4)
144
+ nokogiri (1.11.1)
145
+ mini_portile2 (~> 2.5.0)
146
+ racc (~> 1.4)
142
147
  notiffany (0.1.3)
143
148
  nenv (~> 0.1)
144
149
  shellany (~> 0.0)
@@ -146,7 +151,7 @@ GEM
146
151
  parser (2.7.1.4)
147
152
  ast (~> 2.4.1)
148
153
  pg (1.2.3)
149
- phobos (1.9.0)
154
+ phobos (2.0.1)
150
155
  activesupport (>= 3.0.0)
151
156
  concurrent-ruby (>= 1.0.2)
152
157
  concurrent-ruby-ext (>= 1.0.2)
@@ -157,37 +162,38 @@ GEM
157
162
  pry (0.13.1)
158
163
  coderay (~> 1.1)
159
164
  method_source (~> 1.0)
165
+ racc (1.5.2)
160
166
  rack (2.2.3)
161
167
  rack-test (1.1.0)
162
168
  rack (>= 1.0, < 3)
163
- rails (6.0.3.2)
164
- actioncable (= 6.0.3.2)
165
- actionmailbox (= 6.0.3.2)
166
- actionmailer (= 6.0.3.2)
167
- actionpack (= 6.0.3.2)
168
- actiontext (= 6.0.3.2)
169
- actionview (= 6.0.3.2)
170
- activejob (= 6.0.3.2)
171
- activemodel (= 6.0.3.2)
172
- activerecord (= 6.0.3.2)
173
- activestorage (= 6.0.3.2)
174
- activesupport (= 6.0.3.2)
175
- bundler (>= 1.3.0)
176
- railties (= 6.0.3.2)
169
+ rails (6.1.1)
170
+ actioncable (= 6.1.1)
171
+ actionmailbox (= 6.1.1)
172
+ actionmailer (= 6.1.1)
173
+ actionpack (= 6.1.1)
174
+ actiontext (= 6.1.1)
175
+ actionview (= 6.1.1)
176
+ activejob (= 6.1.1)
177
+ activemodel (= 6.1.1)
178
+ activerecord (= 6.1.1)
179
+ activestorage (= 6.1.1)
180
+ activesupport (= 6.1.1)
181
+ bundler (>= 1.15.0)
182
+ railties (= 6.1.1)
177
183
  sprockets-rails (>= 2.0.0)
178
184
  rails-dom-testing (2.0.3)
179
185
  activesupport (>= 4.2.0)
180
186
  nokogiri (>= 1.6)
181
187
  rails-html-sanitizer (1.3.0)
182
188
  loofah (~> 2.3)
183
- railties (6.0.3.2)
184
- actionpack (= 6.0.3.2)
185
- activesupport (= 6.0.3.2)
189
+ railties (6.1.1)
190
+ actionpack (= 6.1.1)
191
+ activesupport (= 6.1.1)
186
192
  method_source
187
193
  rake (>= 0.8.7)
188
- thor (>= 0.20.3, < 2.0)
194
+ thor (~> 1.0)
189
195
  rainbow (3.0.0)
190
- rake (13.0.1)
196
+ rake (13.0.3)
191
197
  rb-fsevent (0.10.4)
192
198
  rb-inotify (0.10.1)
193
199
  ffi (~> 1.0)
@@ -239,20 +245,19 @@ GEM
239
245
  sprockets (4.0.2)
240
246
  concurrent-ruby (~> 1.0)
241
247
  rack (> 1, < 3)
242
- sprockets-rails (3.2.1)
248
+ sprockets-rails (3.2.2)
243
249
  actionpack (>= 4.0)
244
250
  activesupport (>= 4.0)
245
251
  sprockets (>= 3.0.0)
246
252
  sqlite3 (1.4.2)
247
253
  thor (1.0.1)
248
- thread_safe (0.3.6)
249
- tzinfo (1.2.7)
250
- thread_safe (~> 0.1)
254
+ tzinfo (2.0.4)
255
+ concurrent-ruby (~> 1.0)
251
256
  unicode-display_width (1.7.0)
252
257
  websocket-driver (0.7.3)
253
258
  websocket-extensions (>= 0.1.0)
254
259
  websocket-extensions (0.1.5)
255
- zeitwerk (2.4.0)
260
+ zeitwerk (2.4.2)
256
261
 
257
262
  PLATFORMS
258
263
  ruby
@@ -274,9 +279,9 @@ DEPENDENCIES
274
279
  rspec (~> 3)
275
280
  rspec-rails (~> 4)
276
281
  rspec_junit_formatter (~> 0.3)
277
- rubocop (~> 0.72)
278
- rubocop-rspec (~> 1.27)
282
+ rubocop (= 0.88.0)
283
+ rubocop-rspec (= 1.42.0)
279
284
  sqlite3 (~> 1.3)
280
285
 
281
286
  BUNDLED WITH
282
- 2.1.4
287
+ 2.2.5
data/README.md CHANGED
@@ -42,6 +42,7 @@ Please see the following for further information not covered by this readme:
42
42
  * [Configuration Reference](docs/CONFIGURATION.md)
43
43
  * [Database Backend Feature](docs/DATABASE_BACKEND.md)
44
44
  * [Upgrading Deimos](docs/UPGRADING.md)
45
+ * [Contributing to Integration Tests](docs/INTEGRATION_TESTS.md)
45
46
 
46
47
  # Installation
47
48
 
@@ -625,6 +626,15 @@ class MyConsumer < Deimos::ActiveRecordConsumer
625
626
  def record_key(payload)
626
627
  super
627
628
  end
629
+
630
+ # Optional override, returns true by default.
631
+ # When this method returns true, a record corresponding to the message
632
+ # is created/updated.
633
+ # When this method returns false, message processing is skipped and a
634
+ # corresponding record will NOT be created/updated.
635
+ def process_message?(payload)
636
+ super
637
+ end
628
638
  end
629
639
  ```
630
640
 
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ['lib']
20
20
 
21
21
  spec.add_runtime_dependency('avro_turf', '~> 0.11')
22
- spec.add_runtime_dependency('phobos', '~> 1.9')
22
+ spec.add_runtime_dependency('phobos', '>= 1.9', '< 3.0')
23
23
  spec.add_runtime_dependency('ruby-kafka', '~> 0.7')
24
24
  spec.add_runtime_dependency('sigurd', '0.0.1')
25
25
 
@@ -38,7 +38,7 @@ Gem::Specification.new do |spec|
38
38
  spec.add_development_dependency('rspec', '~> 3')
39
39
  spec.add_development_dependency('rspec_junit_formatter', '~>0.3')
40
40
  spec.add_development_dependency('rspec-rails', '~> 4')
41
- spec.add_development_dependency('rubocop', '~> 0.72')
42
- spec.add_development_dependency('rubocop-rspec', '~> 1.27')
41
+ spec.add_development_dependency('rubocop', '0.88.0')
42
+ spec.add_development_dependency('rubocop-rspec', '1.42.0')
43
43
  spec.add_development_dependency('sqlite3', '~> 1.3')
44
44
  end
@@ -0,0 +1,52 @@
1
+ # Running Integration Tests
2
+
3
+ This repo includes integration tests in the [spec/utils](spec/utils) directory.
4
+ Here, there are tests for more deimos features that include a database integration like
5
+ * [Database Poller](README.md#Database Poller)
6
+ * [Database Backend](docs/DATABASE_BACKEND.md)
7
+ * [Deadlock Retrying](lib/deimos/utils/deadlock_retry.rb)
8
+
9
+ You will need to set up the following databases to develop and create unit tests in these test suites.
10
+ * [SQLite](#SQLite)
11
+ * [MySQL](#MySQL)
12
+ * [PostgreSQL](#PostgreSQL)
13
+
14
+ ### SQLite
15
+ This database is covered through the `sqlite3` gem.
16
+
17
+ ## MySQL
18
+ ### Setting up a local MySQL server (Mac)
19
+ ```bash
20
+ # Download MySQL (Optionally, choose a version you are comfortable with)
21
+ brew install mysql
22
+ # Start automatically after rebooting your machine
23
+ brew services start mysql
24
+
25
+ # Cleanup once you are done with MySQL
26
+ brew services stop mysql
27
+ ```
28
+
29
+ ## PostgreSQL
30
+ ### Setting up a local PostgreSQL server (Mac)
31
+ ```bash
32
+ # Install postgres if it's not already installed
33
+ brew install postgres
34
+
35
+ # Initialize and Start up postgres db
36
+ brew services start postgres
37
+ initdb /usr/local/var/postgres
38
+ # Create the default database and user
39
+ # Use the password "root"
40
+ createuser -s --password postgres
41
+
42
+ # Cleanup once done with Postgres
43
+ killall postgres
44
+ brew services stop postgres
45
+ ```
46
+
47
+ ## Running Integration Tests
48
+ You must specify the tag "integration" when running these these test suites.
49
+ This can be done through the CLI with the `--tag integration` argument.
50
+ ```bash
51
+ rspec spec/utils/ --tag integration
52
+ ```
@@ -28,6 +28,7 @@ Please describe the tests that you ran to verify your changes. Provide instructi
28
28
  - [ ] I have performed a self-review of my own code
29
29
  - [ ] I have commented my code, particularly in hard-to-understand areas
30
30
  - [ ] I have made corresponding changes to the documentation
31
+ - [ ] I have added a line in the CHANGELOG describing this change, under the UNRELEASED heading
31
32
  - [ ] My changes generate no new warnings
32
33
  - [ ] I have added tests that prove my fix is effective or that my feature works
33
34
  - [ ] New and existing unit tests pass locally with my changes
@@ -26,6 +26,15 @@ module Deimos
26
26
 
27
27
  # :nodoc:
28
28
  def consume(payload, metadata)
29
+ unless self.process_message?(payload)
30
+ Deimos.config.logger.debug(
31
+ message: 'Skipping processing of message',
32
+ payload: payload,
33
+ metadata: metadata
34
+ )
35
+ return
36
+ end
37
+
29
38
  key = metadata.with_indifferent_access[:key]
30
39
  klass = self.class.config[:record_class]
31
40
  record = fetch_record(klass, (payload || {}).with_indifferent_access, key)
@@ -55,5 +55,13 @@ module Deimos
55
55
  def record_attributes(payload, _key=nil)
56
56
  @converter.convert(payload)
57
57
  end
58
+
59
+ # Override this message to conditionally save records
60
+ # @param payload [Hash] The kafka message as a hash
61
+ # @return [Boolean] if true, record is created/update.
62
+ # If false, record processing is skipped but message offset is still committed.
63
+ def process_message?(_payload)
64
+ true
65
+ end
58
66
  end
59
67
  end
@@ -14,7 +14,7 @@ module Deimos
14
14
  message = Deimos::KafkaMessage.new(
15
15
  message: m.encoded_payload ? m.encoded_payload.to_s.b : nil,
16
16
  topic: m.topic,
17
- partition_key: m.partition_key || m.key
17
+ partition_key: partition_key_for(m)
18
18
  )
19
19
  message.key = m.encoded_key.to_s.b unless producer_class.config[:no_keys]
20
20
  message
@@ -26,6 +26,15 @@ module Deimos
26
26
  by: records.size
27
27
  )
28
28
  end
29
+
30
+ # @param message [Deimos::Message]
31
+ # @return [String] the partition key to use for this message
32
+ def partition_key_for(message)
33
+ return message.partition_key if message.partition_key.present?
34
+ return message.key unless message.key.is_a?(Hash)
35
+
36
+ message.key.to_yaml
37
+ end
29
38
  end
30
39
  end
31
40
  end
@@ -15,6 +15,11 @@ module Deimos
15
15
  # enabled true
16
16
  # ca_cert_file 'my_file'
17
17
  # end
18
+ # config.kafka do
19
+ # ssl do
20
+ # enabled true
21
+ # end
22
+ # end
18
23
  # end
19
24
  # - Allows for arrays of configurations:
20
25
  # Deimos.configure do |config|
@@ -67,7 +67,7 @@ module Deimos
67
67
  topic(kafka_config.topic) if kafka_config.topic.present? && klass.respond_to?(:topic)
68
68
  schema(kafka_config.schema) if kafka_config.schema.present?
69
69
  namespace(kafka_config.namespace) if kafka_config.namespace.present?
70
- key_config(kafka_config.key_config) if kafka_config.key_config.present?
70
+ key_config(**kafka_config.key_config) if kafka_config.key_config.present?
71
71
  end
72
72
  end
73
73
 
@@ -88,8 +88,9 @@ module Deimos
88
88
  array_of_attributes,
89
89
  options={})
90
90
  results = super
91
- return unless self.kafka_config[:import]
92
- return if array_of_attributes.empty?
91
+ if !self.kafka_config[:import] || array_of_attributes.empty?
92
+ return results
93
+ end
93
94
 
94
95
  # This will contain an array of hashes, where each hash is the actual
95
96
  # attribute hash that created the object.
@@ -85,11 +85,8 @@ module Deimos
85
85
  locked_at: Time.zone.now,
86
86
  error: true,
87
87
  retries: record.retries + 1 }
88
- if ActiveRecord::VERSION::MAJOR >= 4
89
- record.update!(attr_hash)
90
- else
91
- record.update_attributes!(attr_hash)
92
- end
88
+ record.attributes = attr_hash
89
+ record.save!
93
90
  end
94
91
 
95
92
  # Update the locked_at timestamp to indicate that the producer is still
@@ -44,9 +44,11 @@ module Deimos
44
44
 
45
45
  case field_type
46
46
  when :int, :long
47
- if val.is_a?(Integer) ||
48
- _is_integer_string?(val) ||
49
- int_classes.any? { |klass| val.is_a?(klass) }
47
+ if %w(timestamp-millis timestamp-micros).include?(type.logical_type)
48
+ val
49
+ elsif val.is_a?(Integer) ||
50
+ _is_integer_string?(val) ||
51
+ int_classes.any? { |klass| val.is_a?(klass) }
50
52
  val.to_i
51
53
  else
52
54
  val # this will fail
@@ -142,7 +142,8 @@ module Deimos
142
142
  last_id = record.public_send(id_method)
143
143
  last_updated_at = last_updated(record)
144
144
  @producer.send_events(batch)
145
- @info.update_attributes!(last_sent: last_updated_at, last_sent_id: last_id)
145
+ @info.attributes = { last_sent: last_updated_at, last_sent_id: last_id }
146
+ @info.save!
146
147
  end
147
148
  end
148
149
  end
@@ -190,11 +190,14 @@ module Deimos
190
190
  end
191
191
  end
192
192
 
193
+ # Produce messages in batches, reducing the size 1/10 if the batch is too
194
+ # large. Does not retry batches of messages that have already been sent.
193
195
  # @param batch [Array<Hash>]
194
196
  def produce_messages(batch)
195
197
  batch_size = batch.size
198
+ current_index = 0
196
199
  begin
197
- batch.in_groups_of(batch_size, false).each do |group|
200
+ batch[current_index..-1].in_groups_of(batch_size, false).each do |group|
198
201
  @logger.debug("Publishing #{group.size} messages to #{@current_topic}")
199
202
  producer.publish_list(group)
200
203
  Deimos.config.metrics&.increment(
@@ -202,6 +205,7 @@ module Deimos
202
205
  tags: %W(status:success topic:#{@current_topic}),
203
206
  by: group.size
204
207
  )
208
+ current_index += group.size
205
209
  @logger.info("Sent #{group.size} messages to #{@current_topic}")
206
210
  end
207
211
  rescue Kafka::BufferOverflow, Kafka::MessageSizeTooLarge,
@@ -6,6 +6,7 @@ module Deimos
6
6
  module Utils
7
7
  # Listener that can seek to get the last X messages in a topic.
8
8
  class SeekListener < Phobos::Listener
9
+ MAX_SEEK_RETRIES = 3
9
10
  attr_accessor :num_messages
10
11
 
11
12
  # :nodoc:
@@ -13,8 +14,10 @@ module Deimos
13
14
  @num_messages ||= 10
14
15
  @consumer = create_kafka_consumer
15
16
  @consumer.subscribe(topic, @subscribe_opts)
17
+ attempt = 0
16
18
 
17
19
  begin
20
+ attempt += 1
18
21
  last_offset = @kafka_client.last_offset_for(topic, 0)
19
22
  offset = last_offset - num_messages
20
23
  if offset.positive?
@@ -22,7 +25,11 @@ module Deimos
22
25
  @consumer.seek(topic, 0, offset)
23
26
  end
24
27
  rescue StandardError => e
25
- "Could not seek to offset: #{e.message}"
28
+ if attempt < MAX_SEEK_RETRIES
29
+ sleep(1.seconds * attempt)
30
+ retry
31
+ end
32
+ log_error("Could not seek to offset: #{e.message} after #{MAX_SEEK_RETRIES} retries", listener_metadata)
26
33
  end
27
34
 
28
35
  instrument('listener.start_handler', listener_metadata) do
@@ -50,7 +57,6 @@ module Deimos
50
57
 
51
58
  # :nodoc:
52
59
  def consume(payload, metadata)
53
- puts "Got #{payload}"
54
60
  self.class.total_messages << {
55
61
  key: metadata[:key],
56
62
  payload: payload
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Deimos
4
- VERSION = '1.8.2'
4
+ VERSION = '1.8.7'
5
5
  end
@@ -137,5 +137,18 @@ module ActiveRecordConsumerTest
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 not create record of process_message returns false' do
142
+ MyConsumer.any_instance.stub(:process_message?).and_return(false)
143
+ expect(Widget.count).to eq(0)
144
+ test_consume_message(MyConsumer, {
145
+ test_id: 'abc',
146
+ some_int: 3,
147
+ updated_at: 1.day.ago.to_i,
148
+ some_datetime_int: Time.zone.now.to_i,
149
+ timestamp: 2.minutes.ago.to_s
150
+ }, { call_original: true, key: 5 })
151
+ expect(Widget.count).to eq(0)
152
+ end
140
153
  end
141
154
  end
@@ -43,6 +43,12 @@ each_db_config(Deimos::Backends::Db) do
43
43
  described_class.publish(producer_class: MyNoKeyProducer,
44
44
  messages: [messages.first])
45
45
  expect(Deimos::KafkaMessage.count).to eq(4)
46
+ end
46
47
 
48
+ it 'should add messages with Hash keys with JSON encoding' do
49
+ described_class.publish(producer_class: MyProducer,
50
+ messages: [build_message({ foo: 0 }, 'my-topic', { 'test_id' => 0 })])
51
+ expect(Deimos::KafkaMessage.count).to eq(1)
52
+ expect(Deimos::KafkaMessage.last.partition_key).to eq(%(---\ntest_id: 0\n))
47
53
  end
48
54
  end
@@ -16,7 +16,7 @@ RSpec.describe Deimos::Generators::ActiveRecordGenerator do
16
16
  files = Dir['db/migrate/*.rb']
17
17
  expect(files.length).to eq(1)
18
18
  results = <<~MIGRATION
19
- class CreateGeneratedTable < ActiveRecord::Migration[6.0]
19
+ class CreateGeneratedTable < ActiveRecord::Migration[6.1]
20
20
  def up
21
21
  if table_exists?(:generated_table)
22
22
  warn "generated_table already exists, exiting"
@@ -225,5 +225,88 @@ module KafkaSourceSpec
225
225
  expect(Deimos::KafkaMessage.count).to eq(0)
226
226
  end
227
227
  end
228
+
229
+ context 'with import hooks disabled' do
230
+ before(:each) do
231
+ # Dummy class we can include the mixin in. Has a backing table created
232
+ # earlier and has the import hook disabled
233
+ class WidgetNoImportHook < ActiveRecord::Base
234
+ include Deimos::KafkaSource
235
+ self.table_name = 'widgets'
236
+
237
+ # :nodoc:
238
+ def self.kafka_config
239
+ {
240
+ update: true,
241
+ delete: true,
242
+ import: false,
243
+ create: true
244
+ }
245
+ end
246
+
247
+ # :nodoc:
248
+ def self.kafka_producers
249
+ [WidgetProducer]
250
+ end
251
+ end
252
+ WidgetNoImportHook.reset_column_information
253
+ end
254
+
255
+ it 'should not fail when bulk-importing with existing records' do
256
+ widget1 = WidgetNoImportHook.create(widget_id: 1, name: 'Widget 1')
257
+ widget2 = WidgetNoImportHook.create(widget_id: 2, name: 'Widget 2')
258
+ widget1.name = 'New Widget No Import Hook 1'
259
+ widget2.name = 'New Widget No Import Hook 2'
260
+
261
+ expect {
262
+ WidgetNoImportHook.import([widget1, widget2], on_duplicate_key_update: %i(widget_id name))
263
+ }.not_to raise_error
264
+
265
+ expect('my-topic').not_to have_sent({
266
+ widget_id: 1,
267
+ name: 'New Widget No Import Hook 1',
268
+ id: widget1.id,
269
+ created_at: anything,
270
+ updated_at: anything
271
+ }, widget1.id)
272
+ expect('my-topic').not_to have_sent({
273
+ widget_id: 2,
274
+ name: 'New Widget No Import Hook 2',
275
+ id: widget2.id,
276
+ created_at: anything,
277
+ updated_at: anything
278
+ }, widget2.id)
279
+ end
280
+
281
+ it 'should not fail when mixing existing and new records' do
282
+ widget1 = WidgetNoImportHook.create(widget_id: 1, name: 'Widget 1')
283
+ expect('my-topic').to have_sent({
284
+ widget_id: 1,
285
+ name: 'Widget 1',
286
+ id: widget1.id,
287
+ created_at: anything,
288
+ updated_at: anything
289
+ }, widget1.id)
290
+
291
+ widget2 = WidgetNoImportHook.new(widget_id: 2, name: 'Widget 2')
292
+ widget1.name = 'New Widget 1'
293
+ WidgetNoImportHook.import([widget1, widget2], on_duplicate_key_update: %i(widget_id))
294
+ widgets = WidgetNoImportHook.all
295
+ expect('my-topic').not_to have_sent({
296
+ widget_id: 1,
297
+ name: 'New Widget 1',
298
+ id: widgets[0].id,
299
+ created_at: anything,
300
+ updated_at: anything
301
+ }, widgets[0].id)
302
+ expect('my-topic').not_to have_sent({
303
+ widget_id: 2,
304
+ name: 'Widget 2',
305
+ id: widgets[1].id,
306
+ created_at: anything,
307
+ updated_at: anything
308
+ }, widgets[1].id)
309
+ end
310
+ end
228
311
  end
229
312
  end
@@ -51,13 +51,13 @@ each_db_config(Deimos::KafkaTopicInfo) do
51
51
  expect(record.locked_at).to eq(nil)
52
52
  expect(record.error).to eq(false)
53
53
  expect(record.retries).to eq(0)
54
- expect(record.last_processed_at.to_s).to eq(Time.zone.now.to_s)
54
+ expect(record.last_processed_at.in_time_zone.to_s).to eq(Time.zone.now.to_s)
55
55
  record = Deimos::KafkaTopicInfo.last
56
56
  expect(record.locked_by).not_to eq(nil)
57
57
  expect(record.locked_at).not_to eq(nil)
58
58
  expect(record.error).not_to eq(false)
59
59
  expect(record.retries).not_to eq(0)
60
- expect(record.last_processed_at.to_s).to eq(20.seconds.ago.to_s)
60
+ expect(record.last_processed_at.in_time_zone.to_s).to eq(20.seconds.ago.to_s)
61
61
  end
62
62
  end
63
63
 
@@ -70,11 +70,11 @@ each_db_config(Deimos::KafkaTopicInfo) do
70
70
  locked_by: 'me', locked_at: 1.minute.ago)
71
71
 
72
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) }
73
+ Deimos::KafkaTopicInfo.all.each { |t| expect(t.last_processed_at.in_time_zone.to_s).to eq(old_time) }
74
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
75
+ expect(t1.reload.last_processed_at.in_time_zone.to_s).to eq(old_time) # was passed as an exception
76
+ expect(t2.reload.last_processed_at.in_time_zone.to_s).to eq(Time.zone.now.to_s)
77
+ expect(t3.reload.last_processed_at.in_time_zone.to_s).to eq(old_time) # is locked
78
78
  end
79
79
  end
80
80
 
@@ -42,6 +42,20 @@ RSpec.shared_examples_for('an Avro backend') do
42
42
  {
43
43
  'name' => 'union-int-field',
44
44
  'type' => %w(null int)
45
+ },
46
+ {
47
+ 'name' => 'timestamp-millis-field',
48
+ 'type' => {
49
+ 'type' => 'long',
50
+ 'logicalType' => 'timestamp-millis'
51
+ }
52
+ },
53
+ {
54
+ 'name' => 'timestamp-micros-field',
55
+ 'type' => {
56
+ 'type' => 'long',
57
+ 'logicalType' => 'timestamp-micros'
58
+ }
45
59
  }
46
60
  ]
47
61
  }
@@ -95,7 +109,9 @@ RSpec.shared_examples_for('an Avro backend') do
95
109
  'string-field' => 'hi mom',
96
110
  'boolean-field' => true,
97
111
  'union-field' => nil,
98
- 'union-int-field' => nil
112
+ 'union-int-field' => nil,
113
+ 'timestamp-millis-field' => Time.utc(2020, 11, 12, 13, 14, 15, 909_090),
114
+ 'timestamp-micros-field' => Time.utc(2020, 11, 12, 13, 14, 15, 909_090)
99
115
  }
100
116
  end
101
117
 
@@ -169,6 +185,15 @@ RSpec.shared_examples_for('an Avro backend') do
169
185
  expect(result['union-field']).to eq('itsme')
170
186
  end
171
187
 
188
+ it 'should not convert timestamp-millis' do
189
+ result = backend.coerce(payload)
190
+ expect(result['timestamp-millis-field']).to eq(Time.utc(2020, 11, 12, 13, 14, 15, 909_090))
191
+ end
192
+
193
+ it 'should not convert timestamp-micros' do
194
+ result = backend.coerce(payload)
195
+ expect(result['timestamp-micros-field']).to eq(Time.utc(2020, 11, 12, 13, 14, 15, 909_090))
196
+ end
172
197
  end
173
198
 
174
199
  end
@@ -87,7 +87,7 @@ module DbConfigs
87
87
  port: 3306,
88
88
  username: 'root',
89
89
  database: 'test',
90
- host: ENV['MYSQL_HOST'] || 'localhost'
90
+ host: ENV['MYSQL_HOST'] || '127.0.0.1'
91
91
  },
92
92
  {
93
93
  adapter: 'sqlite3',
@@ -96,7 +96,32 @@ each_db_config(Deimos::Utils::DbProducer) do
96
96
  expect(phobos_producer).to have_received(:publish_list).with(['A'] * 100).once
97
97
  expect(phobos_producer).to have_received(:publish_list).with(['A'] * 10).once
98
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
99
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
100
125
  end
101
126
 
102
127
  describe '#compact_messages' do
@@ -289,6 +314,8 @@ each_db_config(Deimos::Utils::DbProducer) do
289
314
  message: "mess#{i}",
290
315
  partition_key: "key#{i}"
291
316
  )
317
+ end
318
+ (5..8).each do |i|
292
319
  Deimos::KafkaMessage.create!(
293
320
  id: i,
294
321
  topic: 'my-topic2',
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Deimos::Utils::SeekListener do
4
+
5
+ describe '#start_listener' do
6
+ let(:consumer) { instance_double(Kafka::Consumer) }
7
+ let(:handler) { class_double(Deimos::Utils::MessageBankHandler) }
8
+
9
+ before(:each) do
10
+ allow(handler).to receive(:start)
11
+ allow(consumer).to receive(:subscribe)
12
+ allow_any_instance_of(Phobos::Listener).to receive(:create_kafka_consumer).and_return(consumer)
13
+ allow_any_instance_of(Kafka::Client).to receive(:last_offset_for).and_return(100)
14
+ stub_const('Deimos::Utils::SeekListener::MAX_SEEK_RETRIES', 2)
15
+ end
16
+
17
+ it 'should seek offset' do
18
+ allow(consumer).to receive(:seek)
19
+ expect(consumer).to receive(:seek).once
20
+ seek_listener = described_class.new({ handler: handler, group_id: 999, topic: 'test_topic' })
21
+ seek_listener.start_listener
22
+ end
23
+
24
+ it 'should retry on errors when seeking offset' do
25
+ allow(consumer).to receive(:seek).and_raise(StandardError)
26
+ expect(consumer).to receive(:seek).twice
27
+ seek_listener = described_class.new({ handler: handler, group_id: 999, topic: 'test_topic' })
28
+ seek_listener.start_listener
29
+ end
30
+ end
31
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deimos-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.2
4
+ version: 1.8.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Orner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-25 00:00:00.000000000 Z
11
+ date: 2021-01-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: avro_turf
@@ -28,16 +28,22 @@ dependencies:
28
28
  name: phobos
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
33
  version: '1.9'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '3.0'
34
37
  type: :runtime
35
38
  prerelease: false
36
39
  version_requirements: !ruby/object:Gem::Requirement
37
40
  requirements:
38
- - - "~>"
41
+ - - ">="
39
42
  - !ruby/object:Gem::Version
40
43
  version: '1.9'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.0'
41
47
  - !ruby/object:Gem::Dependency
42
48
  name: ruby-kafka
43
49
  requirement: !ruby/object:Gem::Requirement
@@ -280,30 +286,30 @@ dependencies:
280
286
  name: rubocop
281
287
  requirement: !ruby/object:Gem::Requirement
282
288
  requirements:
283
- - - "~>"
289
+ - - '='
284
290
  - !ruby/object:Gem::Version
285
- version: '0.72'
291
+ version: 0.88.0
286
292
  type: :development
287
293
  prerelease: false
288
294
  version_requirements: !ruby/object:Gem::Requirement
289
295
  requirements:
290
- - - "~>"
296
+ - - '='
291
297
  - !ruby/object:Gem::Version
292
- version: '0.72'
298
+ version: 0.88.0
293
299
  - !ruby/object:Gem::Dependency
294
300
  name: rubocop-rspec
295
301
  requirement: !ruby/object:Gem::Requirement
296
302
  requirements:
297
- - - "~>"
303
+ - - '='
298
304
  - !ruby/object:Gem::Version
299
- version: '1.27'
305
+ version: 1.42.0
300
306
  type: :development
301
307
  prerelease: false
302
308
  version_requirements: !ruby/object:Gem::Requirement
303
309
  requirements:
304
- - - "~>"
310
+ - - '='
305
311
  - !ruby/object:Gem::Version
306
- version: '1.27'
312
+ version: 1.42.0
307
313
  - !ruby/object:Gem::Dependency
308
314
  name: sqlite3
309
315
  requirement: !ruby/object:Gem::Requirement
@@ -348,6 +354,7 @@ files:
348
354
  - docs/ARCHITECTURE.md
349
355
  - docs/CONFIGURATION.md
350
356
  - docs/DATABASE_BACKEND.md
357
+ - docs/INTEGRATION_TESTS.md
351
358
  - docs/PULL_REQUEST_TEMPLATE.md
352
359
  - docs/UPGRADING.md
353
360
  - lib/deimos.rb
@@ -464,6 +471,7 @@ files:
464
471
  - spec/utils/db_poller_spec.rb
465
472
  - spec/utils/db_producer_spec.rb
466
473
  - spec/utils/deadlock_retry_spec.rb
474
+ - spec/utils/inline_consumer_spec.rb
467
475
  - spec/utils/lag_reporter_spec.rb
468
476
  - spec/utils/platform_schema_validation_spec.rb
469
477
  - spec/utils/schema_controller_mixin_spec.rb
@@ -490,7 +498,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
490
498
  - !ruby/object:Gem::Version
491
499
  version: '0'
492
500
  requirements: []
493
- rubygems_version: 3.1.3
501
+ rubygems_version: 3.0.9
494
502
  signing_key:
495
503
  specification_version: 4
496
504
  summary: Kafka libraries for Ruby.
@@ -547,6 +555,7 @@ test_files:
547
555
  - spec/utils/db_poller_spec.rb
548
556
  - spec/utils/db_producer_spec.rb
549
557
  - spec/utils/deadlock_retry_spec.rb
558
+ - spec/utils/inline_consumer_spec.rb
550
559
  - spec/utils/lag_reporter_spec.rb
551
560
  - spec/utils/platform_schema_validation_spec.rb
552
561
  - spec/utils/schema_controller_mixin_spec.rb