deimos-ruby 1.8.0.pre.beta2 → 1.8.1.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +8 -4
- data/CHANGELOG.md +15 -0
- data/Gemfile.lock +93 -73
- data/README.md +23 -0
- data/deimos-ruby.gemspec +2 -2
- data/lib/deimos.rb +3 -3
- data/lib/deimos/kafka_topic_info.rb +21 -2
- data/lib/deimos/schema_backends/avro_base.rb +28 -1
- data/lib/deimos/schema_backends/base.rb +15 -2
- data/lib/deimos/utils/db_producer.rb +57 -19
- data/lib/deimos/version.rb +1 -1
- data/lib/generators/deimos/active_record/templates/migration.rb.tt +28 -0
- data/lib/generators/deimos/active_record/templates/model.rb.tt +5 -0
- data/lib/generators/deimos/active_record_generator.rb +79 -0
- data/lib/generators/deimos/db_backend/templates/migration +1 -0
- data/lib/generators/deimos/db_backend/templates/rails3_migration +1 -0
- data/spec/generators/active_record_generator_spec.rb +56 -0
- data/spec/kafka_topic_info_spec.rb +39 -16
- data/spec/schemas/com/my-namespace/Generated.avsc +71 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/utils/db_producer_spec.rb +84 -10
- metadata +13 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 335328a0ca5fb9147405485a66944ebe0d05af552ec09ec9605d76a7e7da7e9a
|
|
4
|
+
data.tar.gz: 438a9a91bda52be144d63867a4ae42dd1901cc7472f61232044aa934efebb464
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3e01c5c9db93cb6e0f56dafa2f1b301c30b9cc11c397ca35f77b80cfe62bd9600eacb8774516ee1c81337193687944a8c7bec3cba400df6ea8b1c33af7e82c27
|
|
7
|
+
data.tar.gz: 9de8cea35db671c43123d096653a6423ccfcaad8a2b8e9478f787e879bd7fd50a626604b8626f79b5f8654c84130fded58385d6f75fe44e59e1bfb5a284e1096
|
data/.rubocop.yml
CHANGED
|
@@ -5,6 +5,7 @@ AllCops:
|
|
|
5
5
|
Exclude:
|
|
6
6
|
- lib/deimos/monkey_patches/*.rb
|
|
7
7
|
- vendor/**/*
|
|
8
|
+
- Guardfile
|
|
8
9
|
NewCops: enable
|
|
9
10
|
|
|
10
11
|
# class Plumbus
|
|
@@ -36,7 +37,7 @@ Layout/EmptyLinesAroundBlockBody:
|
|
|
36
37
|
Enabled: false
|
|
37
38
|
|
|
38
39
|
Layout/LineLength:
|
|
39
|
-
Max:
|
|
40
|
+
Max: 120
|
|
40
41
|
Severity: refactor
|
|
41
42
|
Exclude:
|
|
42
43
|
- 'spec/**/*'
|
|
@@ -77,13 +78,14 @@ Lint/UnusedMethodArgument:
|
|
|
77
78
|
|
|
78
79
|
Metrics/AbcSize:
|
|
79
80
|
Severity: refactor
|
|
80
|
-
Max:
|
|
81
|
+
Max: 25
|
|
81
82
|
|
|
82
83
|
Metrics/BlockLength:
|
|
83
|
-
|
|
84
|
+
Enabled: false
|
|
84
85
|
|
|
85
86
|
Metrics/ClassLength:
|
|
86
87
|
Severity: refactor
|
|
88
|
+
Max: 500
|
|
87
89
|
|
|
88
90
|
Metrics/CyclomaticComplexity:
|
|
89
91
|
Severity: refactor
|
|
@@ -91,10 +93,11 @@ Metrics/CyclomaticComplexity:
|
|
|
91
93
|
|
|
92
94
|
Metrics/MethodLength:
|
|
93
95
|
Severity: refactor
|
|
94
|
-
Max:
|
|
96
|
+
Max: 50
|
|
95
97
|
|
|
96
98
|
Metrics/ModuleLength:
|
|
97
99
|
Severity: refactor
|
|
100
|
+
Max: 200
|
|
98
101
|
|
|
99
102
|
Metrics/ParameterLists:
|
|
100
103
|
Max: 5
|
|
@@ -102,6 +105,7 @@ Metrics/ParameterLists:
|
|
|
102
105
|
|
|
103
106
|
Metrics/PerceivedComplexity:
|
|
104
107
|
Severity: refactor
|
|
108
|
+
Max: 10
|
|
105
109
|
|
|
106
110
|
# Use alias_method instead of alias
|
|
107
111
|
Style/Alias:
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## UNRELEASED
|
|
9
9
|
|
|
10
|
+
## 1.8.1-beta1 - 2020-07-22
|
|
11
|
+
|
|
12
|
+
### Fixes :wrench:
|
|
13
|
+
- Retry deleting messages without resending the batch if the
|
|
14
|
+
delete fails (fixes [#34](https://github.com/flipp-oss/deimos/issues/34))
|
|
15
|
+
- Delete messages in batches rather than all at once to
|
|
16
|
+
cut down on the chance of a deadlock.
|
|
17
|
+
|
|
18
|
+
### Features :star:
|
|
19
|
+
- Add `last_processed_at` to `kafka_topic_info` to ensure
|
|
20
|
+
that wait metrics are accurate in cases where records
|
|
21
|
+
get created with an old `created_at` time (e.g. for
|
|
22
|
+
long-running transactions).
|
|
23
|
+
- Add generator for ActiveRecord models and migrations (fixes [#6](https://github.com/flipp-oss/deimos/issues/6))
|
|
24
|
+
|
|
10
25
|
## 1.8.0-beta2 - 2020-07-08
|
|
11
26
|
|
|
12
27
|
### Fixes :wrench:
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
deimos-ruby (1.8.
|
|
4
|
+
deimos-ruby (1.8.1.pre.beta1)
|
|
5
5
|
avro_turf (~> 0.11)
|
|
6
6
|
phobos (~> 1.9)
|
|
7
7
|
ruby-kafka (~> 0.7)
|
|
@@ -10,72 +10,86 @@ PATH
|
|
|
10
10
|
GEM
|
|
11
11
|
remote: https://rubygems.org/
|
|
12
12
|
specs:
|
|
13
|
-
actioncable (
|
|
14
|
-
actionpack (=
|
|
13
|
+
actioncable (6.0.3.2)
|
|
14
|
+
actionpack (= 6.0.3.2)
|
|
15
15
|
nio4r (~> 2.0)
|
|
16
16
|
websocket-driver (>= 0.6.1)
|
|
17
|
-
|
|
18
|
-
actionpack (=
|
|
19
|
-
|
|
20
|
-
|
|
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)
|
|
23
|
+
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)
|
|
21
28
|
mail (~> 2.5, >= 2.5.4)
|
|
22
29
|
rails-dom-testing (~> 2.0)
|
|
23
|
-
actionpack (
|
|
24
|
-
actionview (=
|
|
25
|
-
activesupport (=
|
|
30
|
+
actionpack (6.0.3.2)
|
|
31
|
+
actionview (= 6.0.3.2)
|
|
32
|
+
activesupport (= 6.0.3.2)
|
|
26
33
|
rack (~> 2.0, >= 2.0.8)
|
|
27
34
|
rack-test (>= 0.6.3)
|
|
28
35
|
rails-dom-testing (~> 2.0)
|
|
29
|
-
rails-html-sanitizer (~> 1.0, >= 1.0
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
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)
|
|
42
|
+
nokogiri (>= 1.8.5)
|
|
43
|
+
actionview (6.0.3.2)
|
|
44
|
+
activesupport (= 6.0.3.2)
|
|
32
45
|
builder (~> 3.1)
|
|
33
46
|
erubi (~> 1.4)
|
|
34
47
|
rails-dom-testing (~> 2.0)
|
|
35
|
-
rails-html-sanitizer (~> 1.
|
|
36
|
-
activejob (
|
|
37
|
-
activesupport (=
|
|
48
|
+
rails-html-sanitizer (~> 1.1, >= 1.2.0)
|
|
49
|
+
activejob (6.0.3.2)
|
|
50
|
+
activesupport (= 6.0.3.2)
|
|
38
51
|
globalid (>= 0.3.6)
|
|
39
|
-
activemodel (
|
|
40
|
-
activesupport (=
|
|
41
|
-
activerecord (
|
|
42
|
-
activemodel (=
|
|
43
|
-
activesupport (=
|
|
44
|
-
|
|
45
|
-
activerecord-import (1.0.4)
|
|
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)
|
|
46
58
|
activerecord (>= 3.2)
|
|
47
|
-
activestorage (
|
|
48
|
-
actionpack (=
|
|
49
|
-
|
|
59
|
+
activestorage (6.0.3.2)
|
|
60
|
+
actionpack (= 6.0.3.2)
|
|
61
|
+
activejob (= 6.0.3.2)
|
|
62
|
+
activerecord (= 6.0.3.2)
|
|
50
63
|
marcel (~> 0.3.1)
|
|
51
|
-
activesupport (
|
|
64
|
+
activesupport (6.0.3.2)
|
|
52
65
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
53
66
|
i18n (>= 0.7, < 2)
|
|
54
67
|
minitest (~> 5.1)
|
|
55
68
|
tzinfo (~> 1.1)
|
|
56
|
-
|
|
57
|
-
ast (2.4.
|
|
69
|
+
zeitwerk (~> 2.2, >= 2.2.2)
|
|
70
|
+
ast (2.4.1)
|
|
58
71
|
avro (1.9.2)
|
|
59
72
|
multi_json
|
|
60
73
|
avro_turf (0.11.0)
|
|
61
74
|
avro (>= 1.7.7, < 1.10)
|
|
62
75
|
excon (~> 0.45)
|
|
63
76
|
builder (3.2.4)
|
|
64
|
-
coderay (1.1.
|
|
77
|
+
coderay (1.1.3)
|
|
65
78
|
concurrent-ruby (1.1.6)
|
|
66
79
|
concurrent-ruby-ext (1.1.6)
|
|
67
80
|
concurrent-ruby (= 1.1.6)
|
|
68
81
|
crass (1.0.6)
|
|
69
82
|
database_cleaner (1.8.5)
|
|
70
|
-
ddtrace (0.
|
|
83
|
+
ddtrace (0.37.0)
|
|
71
84
|
msgpack
|
|
72
|
-
diff-lcs (1.
|
|
73
|
-
digest-crc (0.
|
|
74
|
-
|
|
85
|
+
diff-lcs (1.4.4)
|
|
86
|
+
digest-crc (0.6.1)
|
|
87
|
+
rake (~> 13.0)
|
|
88
|
+
dogstatsd-ruby (4.8.1)
|
|
75
89
|
erubi (1.9.0)
|
|
76
|
-
excon (0.
|
|
90
|
+
excon (0.75.0)
|
|
77
91
|
exponential-backoff (0.0.4)
|
|
78
|
-
ffi (1.
|
|
92
|
+
ffi (1.13.1)
|
|
79
93
|
formatador (0.2.5)
|
|
80
94
|
globalid (0.4.2)
|
|
81
95
|
activesupport (>= 4.2.0)
|
|
@@ -96,20 +110,19 @@ GEM
|
|
|
96
110
|
guard-rubocop (1.3.0)
|
|
97
111
|
guard (~> 2.0)
|
|
98
112
|
rubocop (~> 0.20)
|
|
99
|
-
i18n (1.8.
|
|
113
|
+
i18n (1.8.4)
|
|
100
114
|
concurrent-ruby (~> 1.0)
|
|
101
|
-
jaro_winkler (1.5.4)
|
|
102
115
|
listen (3.2.1)
|
|
103
116
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
|
104
117
|
rb-inotify (~> 0.9, >= 0.9.10)
|
|
105
118
|
little-plugger (1.1.4)
|
|
106
|
-
logging (2.
|
|
119
|
+
logging (2.3.0)
|
|
107
120
|
little-plugger (~> 1.1)
|
|
108
|
-
multi_json (~> 1.
|
|
109
|
-
loofah (2.
|
|
121
|
+
multi_json (~> 1.14)
|
|
122
|
+
loofah (2.6.0)
|
|
110
123
|
crass (~> 1.0.2)
|
|
111
124
|
nokogiri (>= 1.5.9)
|
|
112
|
-
lumberjack (1.2.
|
|
125
|
+
lumberjack (1.2.6)
|
|
113
126
|
mail (2.7.1)
|
|
114
127
|
mini_mime (>= 0.1.1)
|
|
115
128
|
marcel (0.3.3)
|
|
@@ -118,20 +131,20 @@ GEM
|
|
|
118
131
|
mimemagic (0.3.5)
|
|
119
132
|
mini_mime (1.0.2)
|
|
120
133
|
mini_portile2 (2.4.0)
|
|
121
|
-
minitest (5.14.
|
|
134
|
+
minitest (5.14.1)
|
|
122
135
|
msgpack (1.3.3)
|
|
123
|
-
multi_json (1.
|
|
136
|
+
multi_json (1.15.0)
|
|
124
137
|
mysql2 (0.5.3)
|
|
125
138
|
nenv (0.3.0)
|
|
126
139
|
nio4r (2.5.2)
|
|
127
|
-
nokogiri (1.10.
|
|
140
|
+
nokogiri (1.10.10)
|
|
128
141
|
mini_portile2 (~> 2.4.0)
|
|
129
142
|
notiffany (0.1.3)
|
|
130
143
|
nenv (~> 0.1)
|
|
131
144
|
shellany (~> 0.0)
|
|
132
|
-
parallel (1.19.
|
|
133
|
-
parser (2.7.1.
|
|
134
|
-
ast (~> 2.4.
|
|
145
|
+
parallel (1.19.2)
|
|
146
|
+
parser (2.7.1.4)
|
|
147
|
+
ast (~> 2.4.1)
|
|
135
148
|
pg (1.2.3)
|
|
136
149
|
phobos (1.9.0)
|
|
137
150
|
activesupport (>= 3.0.0)
|
|
@@ -147,35 +160,38 @@ GEM
|
|
|
147
160
|
rack (2.2.3)
|
|
148
161
|
rack-test (1.1.0)
|
|
149
162
|
rack (>= 1.0, < 3)
|
|
150
|
-
rails (
|
|
151
|
-
actioncable (=
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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)
|
|
160
175
|
bundler (>= 1.3.0)
|
|
161
|
-
railties (=
|
|
176
|
+
railties (= 6.0.3.2)
|
|
162
177
|
sprockets-rails (>= 2.0.0)
|
|
163
178
|
rails-dom-testing (2.0.3)
|
|
164
179
|
activesupport (>= 4.2.0)
|
|
165
180
|
nokogiri (>= 1.6)
|
|
166
181
|
rails-html-sanitizer (1.3.0)
|
|
167
182
|
loofah (~> 2.3)
|
|
168
|
-
railties (
|
|
169
|
-
actionpack (=
|
|
170
|
-
activesupport (=
|
|
183
|
+
railties (6.0.3.2)
|
|
184
|
+
actionpack (= 6.0.3.2)
|
|
185
|
+
activesupport (= 6.0.3.2)
|
|
171
186
|
method_source
|
|
172
187
|
rake (>= 0.8.7)
|
|
173
|
-
thor (>= 0.
|
|
188
|
+
thor (>= 0.20.3, < 2.0)
|
|
174
189
|
rainbow (3.0.0)
|
|
175
190
|
rake (13.0.1)
|
|
176
191
|
rb-fsevent (0.10.4)
|
|
177
192
|
rb-inotify (0.10.1)
|
|
178
193
|
ffi (~> 1.0)
|
|
194
|
+
regexp_parser (1.7.1)
|
|
179
195
|
rexml (3.2.4)
|
|
180
196
|
rspec (3.9.0)
|
|
181
197
|
rspec-core (~> 3.9.0)
|
|
@@ -183,7 +199,7 @@ GEM
|
|
|
183
199
|
rspec-mocks (~> 3.9.0)
|
|
184
200
|
rspec-core (3.9.2)
|
|
185
201
|
rspec-support (~> 3.9.3)
|
|
186
|
-
rspec-expectations (3.9.
|
|
202
|
+
rspec-expectations (3.9.2)
|
|
187
203
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
188
204
|
rspec-support (~> 3.9.0)
|
|
189
205
|
rspec-mocks (3.9.1)
|
|
@@ -192,16 +208,19 @@ GEM
|
|
|
192
208
|
rspec-support (3.9.3)
|
|
193
209
|
rspec_junit_formatter (0.4.1)
|
|
194
210
|
rspec-core (>= 2, < 4, != 2.12.0)
|
|
195
|
-
rubocop (0.
|
|
196
|
-
jaro_winkler (~> 1.5.1)
|
|
211
|
+
rubocop (0.88.0)
|
|
197
212
|
parallel (~> 1.10)
|
|
198
|
-
parser (>= 2.7.
|
|
213
|
+
parser (>= 2.7.1.1)
|
|
199
214
|
rainbow (>= 2.2.2, < 4.0)
|
|
215
|
+
regexp_parser (>= 1.7)
|
|
200
216
|
rexml
|
|
217
|
+
rubocop-ast (>= 0.1.0, < 1.0)
|
|
201
218
|
ruby-progressbar (~> 1.7)
|
|
202
219
|
unicode-display_width (>= 1.4.0, < 2.0)
|
|
203
|
-
rubocop-
|
|
204
|
-
|
|
220
|
+
rubocop-ast (0.2.0)
|
|
221
|
+
parser (>= 2.7.0.1)
|
|
222
|
+
rubocop-rspec (1.42.0)
|
|
223
|
+
rubocop (>= 0.87.0)
|
|
205
224
|
ruby-kafka (0.7.10)
|
|
206
225
|
digest-crc
|
|
207
226
|
ruby-progressbar (1.10.1)
|
|
@@ -209,7 +228,7 @@ GEM
|
|
|
209
228
|
sigurd (0.0.1)
|
|
210
229
|
concurrent-ruby (~> 1)
|
|
211
230
|
exponential-backoff
|
|
212
|
-
sprockets (4.0.
|
|
231
|
+
sprockets (4.0.2)
|
|
213
232
|
concurrent-ruby (~> 1.0)
|
|
214
233
|
rack (> 1, < 3)
|
|
215
234
|
sprockets-rails (3.2.1)
|
|
@@ -222,15 +241,16 @@ GEM
|
|
|
222
241
|
tzinfo (1.2.7)
|
|
223
242
|
thread_safe (~> 0.1)
|
|
224
243
|
unicode-display_width (1.7.0)
|
|
225
|
-
websocket-driver (0.7.
|
|
244
|
+
websocket-driver (0.7.3)
|
|
226
245
|
websocket-extensions (>= 0.1.0)
|
|
227
246
|
websocket-extensions (0.1.5)
|
|
247
|
+
zeitwerk (2.4.0)
|
|
228
248
|
|
|
229
249
|
PLATFORMS
|
|
230
250
|
ruby
|
|
231
251
|
|
|
232
252
|
DEPENDENCIES
|
|
233
|
-
activerecord (~>
|
|
253
|
+
activerecord (~> 6)
|
|
234
254
|
activerecord-import
|
|
235
255
|
avro (~> 1.9)
|
|
236
256
|
database_cleaner (~> 1.7)
|
|
@@ -242,7 +262,7 @@ DEPENDENCIES
|
|
|
242
262
|
guard-rubocop (~> 1)
|
|
243
263
|
mysql2 (~> 0.5)
|
|
244
264
|
pg (~> 1.1)
|
|
245
|
-
rails (~>
|
|
265
|
+
rails (~> 6)
|
|
246
266
|
rake (~> 13)
|
|
247
267
|
rspec (~> 3)
|
|
248
268
|
rspec_junit_formatter (~> 0.3)
|
data/README.md
CHANGED
|
@@ -557,6 +557,29 @@ class MyConsumer < Deimos::ActiveRecordConsumer
|
|
|
557
557
|
end
|
|
558
558
|
```
|
|
559
559
|
|
|
560
|
+
#### Generating Tables and Models
|
|
561
|
+
|
|
562
|
+
Deimos provides a generator that takes an existing schema and generates a
|
|
563
|
+
database table based on its fields. By default, any complex sub-types (such as
|
|
564
|
+
records or arrays) are turned into JSON (if supported) or string columns.
|
|
565
|
+
|
|
566
|
+
Before running this migration, you must first copy the schema into your repo
|
|
567
|
+
in the correct path (in the example above, you would need to have a file
|
|
568
|
+
`{SCHEMA_ROOT}/com/my-namespace/MySchema.avsc`).
|
|
569
|
+
|
|
570
|
+
To generate a model and migration, run the following:
|
|
571
|
+
|
|
572
|
+
rails g deimos:active_record TABLE_NAME FULL_SCHEMA_NAME
|
|
573
|
+
|
|
574
|
+
Example:
|
|
575
|
+
|
|
576
|
+
rails g deimos:active_record my_table com.my-namespace.MySchema
|
|
577
|
+
|
|
578
|
+
...would generate:
|
|
579
|
+
|
|
580
|
+
db/migrate/1234_create_my_table.rb
|
|
581
|
+
app/models/my_table.rb
|
|
582
|
+
|
|
560
583
|
#### Batch Consumers
|
|
561
584
|
|
|
562
585
|
Deimos also provides a batch consumption mode for `ActiveRecordConsumer` which
|
data/deimos-ruby.gemspec
CHANGED
|
@@ -23,7 +23,7 @@ Gem::Specification.new do |spec|
|
|
|
23
23
|
spec.add_runtime_dependency('ruby-kafka', '~> 0.7')
|
|
24
24
|
spec.add_runtime_dependency('sigurd', '0.0.1')
|
|
25
25
|
|
|
26
|
-
spec.add_development_dependency('activerecord', '~>
|
|
26
|
+
spec.add_development_dependency('activerecord', '~> 6')
|
|
27
27
|
spec.add_development_dependency('activerecord-import')
|
|
28
28
|
spec.add_development_dependency('avro', '~> 1.9')
|
|
29
29
|
spec.add_development_dependency('database_cleaner', '~> 1.7')
|
|
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
|
|
|
34
34
|
spec.add_development_dependency('guard-rubocop', '~> 1')
|
|
35
35
|
spec.add_development_dependency('mysql2', '~> 0.5')
|
|
36
36
|
spec.add_development_dependency('pg', '~> 1.1')
|
|
37
|
-
spec.add_development_dependency('rails', '~>
|
|
37
|
+
spec.add_development_dependency('rails', '~> 6')
|
|
38
38
|
spec.add_development_dependency('rake', '~> 13')
|
|
39
39
|
spec.add_development_dependency('rspec', '~> 3')
|
|
40
40
|
spec.add_development_dependency('rspec_junit_formatter', '~>0.3')
|
data/lib/deimos.rb
CHANGED
|
@@ -28,9 +28,9 @@ if defined?(ActiveRecord)
|
|
|
28
28
|
require 'deimos/kafka_source'
|
|
29
29
|
require 'deimos/kafka_topic_info'
|
|
30
30
|
require 'deimos/backends/db'
|
|
31
|
-
require 'sigurd/signal_handler
|
|
32
|
-
require 'sigurd/executor
|
|
33
|
-
require 'deimos/utils/db_producer
|
|
31
|
+
require 'sigurd/signal_handler'
|
|
32
|
+
require 'sigurd/executor'
|
|
33
|
+
require 'deimos/utils/db_producer'
|
|
34
34
|
require 'deimos/utils/db_poller'
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -13,7 +13,7 @@ module Deimos
|
|
|
13
13
|
def lock(topic, lock_id)
|
|
14
14
|
# Try to create it - it's fine if it already exists
|
|
15
15
|
begin
|
|
16
|
-
self.create(topic: topic)
|
|
16
|
+
self.create(topic: topic, last_processed_at: Time.zone.now)
|
|
17
17
|
rescue ActiveRecord::RecordNotUnique
|
|
18
18
|
# continue on
|
|
19
19
|
end
|
|
@@ -52,7 +52,26 @@ module Deimos
|
|
|
52
52
|
# @param lock_id [String]
|
|
53
53
|
def clear_lock(topic, lock_id)
|
|
54
54
|
self.where(topic: topic, locked_by: lock_id).
|
|
55
|
-
update_all(locked_by: nil,
|
|
55
|
+
update_all(locked_by: nil,
|
|
56
|
+
locked_at: nil,
|
|
57
|
+
error: false,
|
|
58
|
+
retries: 0,
|
|
59
|
+
last_processed_at: Time.zone.now)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Update all topics that aren't currently locked and have no messages
|
|
63
|
+
# waiting. It's OK if some messages get inserted in the middle of this
|
|
64
|
+
# because the point is that at least within a few milliseconds of each
|
|
65
|
+
# other, it wasn't locked and had no messages, meaning the topic
|
|
66
|
+
# was in a good state.
|
|
67
|
+
# @param except_topics [Array<String>] the list of topics we've just
|
|
68
|
+
# realized had messages in them, meaning all other topics were empty.
|
|
69
|
+
def ping_empty_topics(except_topics)
|
|
70
|
+
records = KafkaTopicInfo.where(locked_by: nil).
|
|
71
|
+
where('topic not in(?)', except_topics)
|
|
72
|
+
records.each do |info|
|
|
73
|
+
info.update_attribute(:last_processed_at, Time.zone.now)
|
|
74
|
+
end
|
|
56
75
|
end
|
|
57
76
|
|
|
58
77
|
# The producer calls this if it gets an error sending messages. This
|
|
@@ -33,6 +33,30 @@ module Deimos
|
|
|
33
33
|
decode(payload, schema: @key_schema['name'])[field_name]
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# :nodoc:
|
|
37
|
+
def sql_type(field)
|
|
38
|
+
type = field.type.type
|
|
39
|
+
return type if %w(array map record).include?(type)
|
|
40
|
+
|
|
41
|
+
if type == :union
|
|
42
|
+
non_null = field.type.schemas.reject { |f| f.type == :null }
|
|
43
|
+
if non_null.size > 1
|
|
44
|
+
warn("WARNING: #{field.name} has more than one non-null type. Picking the first for the SQL type.")
|
|
45
|
+
end
|
|
46
|
+
return non_null.first.type
|
|
47
|
+
end
|
|
48
|
+
return type.to_sym if %w(float boolean).include?(type)
|
|
49
|
+
return :integer if type == 'int'
|
|
50
|
+
return :bigint if type == 'long'
|
|
51
|
+
|
|
52
|
+
if type == 'double'
|
|
53
|
+
warn('Avro `double` type turns into SQL `float` type. Please ensure you have the correct `limit` set.')
|
|
54
|
+
return :float
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
:string
|
|
58
|
+
end
|
|
59
|
+
|
|
36
60
|
# @override
|
|
37
61
|
def coerce_field(field, value)
|
|
38
62
|
AvroSchemaCoercer.new(avro_schema).coerce_type(field.type, value)
|
|
@@ -40,7 +64,10 @@ module Deimos
|
|
|
40
64
|
|
|
41
65
|
# @override
|
|
42
66
|
def schema_fields
|
|
43
|
-
avro_schema.fields.map
|
|
67
|
+
avro_schema.fields.map do |field|
|
|
68
|
+
enum_values = field.type.type == 'enum' ? field.type.symbols : []
|
|
69
|
+
SchemaField.new(field.name, field.type, enum_values)
|
|
70
|
+
end
|
|
44
71
|
end
|
|
45
72
|
|
|
46
73
|
# @override
|
|
@@ -3,13 +3,15 @@
|
|
|
3
3
|
module Deimos
|
|
4
4
|
# Represents a field in the schema.
|
|
5
5
|
class SchemaField
|
|
6
|
-
attr_accessor :name, :type
|
|
6
|
+
attr_accessor :name, :type, :enum_values
|
|
7
7
|
|
|
8
8
|
# @param name [String]
|
|
9
9
|
# @param type [Object]
|
|
10
|
-
|
|
10
|
+
# @param enum_values [Array<String>]
|
|
11
|
+
def initialize(name, type, enum_values=[])
|
|
11
12
|
@name = name
|
|
12
13
|
@type = type
|
|
14
|
+
@enum_values = enum_values
|
|
13
15
|
end
|
|
14
16
|
end
|
|
15
17
|
|
|
@@ -109,6 +111,17 @@ module Deimos
|
|
|
109
111
|
raise NotImplementedError
|
|
110
112
|
end
|
|
111
113
|
|
|
114
|
+
# Given a field definition, return the SQL type that might be used in
|
|
115
|
+
# ActiveRecord table creation - e.g. for Avro, a `long` type would
|
|
116
|
+
# return `:bigint`. There are also special values that need to be returned:
|
|
117
|
+
# `:array`, `:map` and `:record`, for types representing those structures.
|
|
118
|
+
# `:enum` is also recognized.
|
|
119
|
+
# @param field [SchemaField]
|
|
120
|
+
# @return [Symbol]
|
|
121
|
+
def sql_type(field)
|
|
122
|
+
raise NotImplementedError
|
|
123
|
+
end
|
|
124
|
+
|
|
112
125
|
# Encode a message key. To be defined by subclass.
|
|
113
126
|
# @param key [String|Hash] the value to use as the key.
|
|
114
127
|
# @param key_id [Symbol|String] the field name of the key.
|
|
@@ -9,6 +9,8 @@ module Deimos
|
|
|
9
9
|
attr_accessor :id, :current_topic
|
|
10
10
|
|
|
11
11
|
BATCH_SIZE = 1000
|
|
12
|
+
DELETE_BATCH_SIZE = 10
|
|
13
|
+
MAX_DELETE_ATTEMPTS = 3
|
|
12
14
|
|
|
13
15
|
# @param logger [Logger]
|
|
14
16
|
def initialize(logger=Logger.new(STDOUT))
|
|
@@ -48,6 +50,7 @@ module Deimos
|
|
|
48
50
|
topics = retrieve_topics
|
|
49
51
|
@logger.info("Found topics: #{topics}")
|
|
50
52
|
topics.each(&method(:process_topic))
|
|
53
|
+
KafkaTopicInfo.ping_empty_topics(topics)
|
|
51
54
|
sleep(0.5)
|
|
52
55
|
end
|
|
53
56
|
|
|
@@ -87,13 +90,13 @@ module Deimos
|
|
|
87
90
|
begin
|
|
88
91
|
produce_messages(compacted_messages.map(&:phobos_message))
|
|
89
92
|
rescue Kafka::BufferOverflow, Kafka::MessageSizeTooLarge, Kafka::RecordListTooLarge
|
|
93
|
+
delete_messages(messages)
|
|
90
94
|
@logger.error('Message batch too large, deleting...')
|
|
91
95
|
@logger.error(Deimos::KafkaMessage.decoded(messages))
|
|
92
|
-
Deimos::KafkaMessage.where(id: messages.map(&:id)).delete_all
|
|
93
96
|
raise
|
|
94
97
|
end
|
|
95
98
|
end
|
|
96
|
-
|
|
99
|
+
delete_messages(messages)
|
|
97
100
|
Deimos.config.metrics&.increment(
|
|
98
101
|
'db_producer.process',
|
|
99
102
|
tags: %W(topic:#{@current_topic}),
|
|
@@ -106,6 +109,27 @@ module Deimos
|
|
|
106
109
|
true
|
|
107
110
|
end
|
|
108
111
|
|
|
112
|
+
# @param messages [Array<Deimos::KafkaMessage>]
|
|
113
|
+
def delete_messages(messages)
|
|
114
|
+
attempts = 1
|
|
115
|
+
begin
|
|
116
|
+
messages.in_groups_of(DELETE_BATCH_SIZE, false).each do |batch|
|
|
117
|
+
Deimos::KafkaMessage.where(topic: batch.first.topic,
|
|
118
|
+
id: batch.map(&:id)).
|
|
119
|
+
delete_all
|
|
120
|
+
end
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
if (e.message =~ /Lock wait/i || e.message =~ /Lost connection/i) &&
|
|
123
|
+
attempts <= MAX_DELETE_ATTEMPTS
|
|
124
|
+
attempts += 1
|
|
125
|
+
ActiveRecord::Base.connection.verify!
|
|
126
|
+
sleep(1)
|
|
127
|
+
retry
|
|
128
|
+
end
|
|
129
|
+
raise
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
109
133
|
# @return [Array<Deimos::KafkaMessage>]
|
|
110
134
|
def retrieve_messages
|
|
111
135
|
KafkaMessage.where(topic: @current_topic).order(:id).limit(BATCH_SIZE)
|
|
@@ -126,19 +150,33 @@ module Deimos
|
|
|
126
150
|
metrics = Deimos.config.metrics
|
|
127
151
|
return unless metrics
|
|
128
152
|
|
|
153
|
+
topics = KafkaTopicInfo.select(%w(topic last_processed_at))
|
|
129
154
|
messages = Deimos::KafkaMessage.
|
|
130
155
|
select('count(*) as num_messages, min(created_at) as earliest, topic').
|
|
131
|
-
group(:topic)
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
156
|
+
group(:topic).
|
|
157
|
+
index_by(&:topic)
|
|
158
|
+
topics.each do |record|
|
|
159
|
+
message_record = messages[record.topic]
|
|
160
|
+
# We want to record the last time we saw any activity, meaning either
|
|
161
|
+
# the oldest message, or the last time we processed, whichever comes
|
|
162
|
+
# last.
|
|
163
|
+
if message_record
|
|
164
|
+
record_earliest = record.earliest
|
|
165
|
+
# SQLite gives a string here
|
|
166
|
+
if record_earliest.is_a?(String)
|
|
167
|
+
record_earliest = Time.zone.parse(record_earliest)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
earliest = [record.last_processed_at, record_earliest].max
|
|
171
|
+
time_diff = Time.zone.now - earliest
|
|
172
|
+
metrics.gauge('pending_db_messages_max_wait', time_diff,
|
|
173
|
+
tags: ["topic:#{record.topic}"])
|
|
174
|
+
else
|
|
175
|
+
# no messages waiting
|
|
176
|
+
metrics.gauge('pending_db_messages_max_wait', 0,
|
|
177
|
+
tags: ["topic:#{record.topic}"])
|
|
178
|
+
end
|
|
179
|
+
metrics.gauge('pending_db_messages_count', message_record&.num_messages || 0,
|
|
142
180
|
tags: ["topic:#{record.topic}"])
|
|
143
181
|
end
|
|
144
182
|
end
|
|
@@ -174,11 +212,11 @@ module Deimos
|
|
|
174
212
|
end
|
|
175
213
|
|
|
176
214
|
@logger.error("Got error #{e.class.name} when publishing #{batch.size} in groups of #{batch_size}, retrying...")
|
|
177
|
-
if batch_size < 10
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
215
|
+
batch_size = if batch_size < 10
|
|
216
|
+
1
|
|
217
|
+
else
|
|
218
|
+
(batch_size / 10).to_i
|
|
219
|
+
end
|
|
182
220
|
shutdown_producer
|
|
183
221
|
retry
|
|
184
222
|
end
|
|
@@ -187,7 +225,7 @@ module Deimos
|
|
|
187
225
|
# @param batch [Array<Deimos::KafkaMessage>]
|
|
188
226
|
# @return [Array<Deimos::KafkaMessage>]
|
|
189
227
|
def compact_messages(batch)
|
|
190
|
-
return batch
|
|
228
|
+
return batch if batch.first&.key.blank?
|
|
191
229
|
|
|
192
230
|
topic = batch.first.topic
|
|
193
231
|
return batch if config.compact_topics != :all &&
|
data/lib/deimos/version.rb
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
|
|
2
|
+
def up
|
|
3
|
+
if table_exists?(:<%= table_name %>)
|
|
4
|
+
warn "<%= table_name %> already exists, exiting"
|
|
5
|
+
return
|
|
6
|
+
end
|
|
7
|
+
create_table :<%= table_name %> do |t|
|
|
8
|
+
<%- fields.each do |key| -%>
|
|
9
|
+
<%- next if %w(id message_id timestamp).include?(key.name) -%>
|
|
10
|
+
<%- sql_type = schema_base.sql_type(key)
|
|
11
|
+
if %w(record array map).include?(sql_type)
|
|
12
|
+
conn = ActiveRecord::Base.connection
|
|
13
|
+
sql_type = conn.respond_to?(:supports_json?) && conn.supports_json? ? :json : :string
|
|
14
|
+
end
|
|
15
|
+
-%>
|
|
16
|
+
t.<%= sql_type %> :<%= key.name %>
|
|
17
|
+
<%- end -%>
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# TODO add indexes as necessary
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def down
|
|
24
|
+
return unless table_exists?(:<%= table_name %>)
|
|
25
|
+
drop_table :<%= table_name %>
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
require 'rails/generators/active_record/migration'
|
|
5
|
+
require 'rails/version'
|
|
6
|
+
|
|
7
|
+
# Generates a new consumer.
|
|
8
|
+
module Deimos
|
|
9
|
+
module Generators
|
|
10
|
+
# Generator for ActiveRecord model and migration.
|
|
11
|
+
class ActiveRecordGenerator < Rails::Generators::Base
|
|
12
|
+
include Rails::Generators::Migration
|
|
13
|
+
if Rails.version < '4'
|
|
14
|
+
extend(ActiveRecord::Generators::Migration)
|
|
15
|
+
else
|
|
16
|
+
include ActiveRecord::Generators::Migration
|
|
17
|
+
end
|
|
18
|
+
source_root File.expand_path('active_record/templates', __dir__)
|
|
19
|
+
|
|
20
|
+
argument :table_name, desc: 'The table to create.', required: true
|
|
21
|
+
argument :full_schema, desc: 'The fully qualified schema name.', required: true
|
|
22
|
+
|
|
23
|
+
no_commands do
|
|
24
|
+
|
|
25
|
+
# @return [String]
|
|
26
|
+
def db_migrate_path
|
|
27
|
+
if defined?(Rails.application) && Rails.application
|
|
28
|
+
paths = Rails.application.config.paths['db/migrate']
|
|
29
|
+
paths.respond_to?(:to_ary) ? paths.to_ary.first : paths.to_a.first
|
|
30
|
+
else
|
|
31
|
+
'db/migrate'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @return [String]
|
|
36
|
+
def migration_version
|
|
37
|
+
"[#{ActiveRecord::Migration.current_version}]"
|
|
38
|
+
rescue StandardError
|
|
39
|
+
''
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [String]
|
|
43
|
+
def table_class
|
|
44
|
+
self.table_name.classify
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [String]
|
|
48
|
+
def schema
|
|
49
|
+
last_dot = self.full_schema.rindex('.')
|
|
50
|
+
self.full_schema[last_dot + 1..-1]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @return [String]
|
|
54
|
+
def namespace
|
|
55
|
+
last_dot = self.full_schema.rindex('.')
|
|
56
|
+
self.full_schema[0...last_dot]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @return [Deimos::SchemaBackends::Base]
|
|
60
|
+
def schema_base
|
|
61
|
+
@schema_base ||= Deimos.schema_backend_class.new(schema: schema, namespace: namespace)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @return [Array<SchemaField>]
|
|
65
|
+
def fields
|
|
66
|
+
schema_base.schema_fields
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
desc 'Generate migration for a table based on an existing schema.'
|
|
72
|
+
# :nodoc:
|
|
73
|
+
def generate
|
|
74
|
+
migration_template('migration.rb', "db/migrate/create_#{table_name.underscore}.rb")
|
|
75
|
+
template('model.rb', "app/models/#{table_name.underscore}.rb")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -16,6 +16,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
|
|
|
16
16
|
t.datetime :locked_at
|
|
17
17
|
t.boolean :error, null: false, default: false
|
|
18
18
|
t.integer :retries, null: false, default: 0
|
|
19
|
+
t.datetime :last_processed_at
|
|
19
20
|
end
|
|
20
21
|
add_index :kafka_topic_info, :topic, unique: true
|
|
21
22
|
add_index :kafka_topic_info, [:locked_by, :error]
|
|
@@ -16,6 +16,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version
|
|
|
16
16
|
t.datetime :locked_at
|
|
17
17
|
t.boolean :error, null: false, default: false
|
|
18
18
|
t.integer :retries, null: false, default: 0
|
|
19
|
+
t.datetime :last_processed_at
|
|
19
20
|
end
|
|
20
21
|
add_index :kafka_topic_info, :topic, unique: true
|
|
21
22
|
add_index :kafka_topic_info, [:locked_by, :error]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'generators/deimos/active_record_generator'
|
|
4
|
+
|
|
5
|
+
RSpec.describe Deimos::Generators::ActiveRecordGenerator do
|
|
6
|
+
|
|
7
|
+
after(:each) do
|
|
8
|
+
FileUtils.rm_rf('db') if File.exist?('db')
|
|
9
|
+
FileUtils.rm_rf('app') if File.exist?('app')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it 'should generate a migration' do
|
|
13
|
+
expect(Dir['db/migrate/*.rb']).to be_empty
|
|
14
|
+
expect(Dir['app/models/*.rb']).to be_empty
|
|
15
|
+
described_class.start(['generated_table', 'com.my-namespace.Generated'])
|
|
16
|
+
files = Dir['db/migrate/*.rb']
|
|
17
|
+
expect(files.length).to eq(1)
|
|
18
|
+
results = <<~MIGRATION
|
|
19
|
+
class CreateGeneratedTable < ActiveRecord::Migration[6.0]
|
|
20
|
+
def up
|
|
21
|
+
if table_exists?(:generated_table)
|
|
22
|
+
warn "generated_table already exists, exiting"
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
create_table :generated_table do |t|
|
|
26
|
+
t.string :a_string
|
|
27
|
+
t.integer :a_int
|
|
28
|
+
t.bigint :a_long
|
|
29
|
+
t.float :a_float
|
|
30
|
+
t.float :a_double
|
|
31
|
+
t.string :an_enum
|
|
32
|
+
t.json :an_array
|
|
33
|
+
t.json :a_map
|
|
34
|
+
t.json :a_record
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# TODO add indexes as necessary
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def down
|
|
41
|
+
return unless table_exists?(:generated_table)
|
|
42
|
+
drop_table :generated_table
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
end
|
|
46
|
+
MIGRATION
|
|
47
|
+
expect(File.read(files[0])).to eq(results)
|
|
48
|
+
model = <<~MODEL
|
|
49
|
+
class GeneratedTable < ApplicationRecord
|
|
50
|
+
enum an_enum: {sym1: 'sym1', sym2: 'sym2'}
|
|
51
|
+
end
|
|
52
|
+
MODEL
|
|
53
|
+
expect(File.read('app/models/generated_table.rb')).to eq(model)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
@@ -37,22 +37,45 @@ each_db_config(Deimos::KafkaTopicInfo) do
|
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
specify '#clear_lock' do
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
40
|
+
freeze_time do
|
|
41
|
+
Deimos::KafkaTopicInfo.create!(topic: 'my-topic', locked_by: 'abc',
|
|
42
|
+
locked_at: 10.seconds.ago, error: true, retries: 1,
|
|
43
|
+
last_processed_at: 20.seconds.ago)
|
|
44
|
+
Deimos::KafkaTopicInfo.create!(topic: 'my-topic2', locked_by: 'def',
|
|
45
|
+
locked_at: 10.seconds.ago, error: true, retries: 1,
|
|
46
|
+
last_processed_at: 20.seconds.ago)
|
|
47
|
+
Deimos::KafkaTopicInfo.clear_lock('my-topic', 'abc')
|
|
48
|
+
expect(Deimos::KafkaTopicInfo.count).to eq(2)
|
|
49
|
+
record = Deimos::KafkaTopicInfo.first
|
|
50
|
+
expect(record.locked_by).to eq(nil)
|
|
51
|
+
expect(record.locked_at).to eq(nil)
|
|
52
|
+
expect(record.error).to eq(false)
|
|
53
|
+
expect(record.retries).to eq(0)
|
|
54
|
+
expect(record.last_processed_at.to_s).to eq(Time.zone.now.to_s)
|
|
55
|
+
record = Deimos::KafkaTopicInfo.last
|
|
56
|
+
expect(record.locked_by).not_to eq(nil)
|
|
57
|
+
expect(record.locked_at).not_to eq(nil)
|
|
58
|
+
expect(record.error).not_to eq(false)
|
|
59
|
+
expect(record.retries).not_to eq(0)
|
|
60
|
+
expect(record.last_processed_at.to_s).to eq(20.seconds.ago.to_s)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
specify '#ping_empty_topics' do
|
|
65
|
+
freeze_time do
|
|
66
|
+
old_time = 1.hour.ago.to_s
|
|
67
|
+
t1 = Deimos::KafkaTopicInfo.create!(topic: 'topic1', last_processed_at: old_time)
|
|
68
|
+
t2 = Deimos::KafkaTopicInfo.create!(topic: 'topic2', last_processed_at: old_time)
|
|
69
|
+
t3 = Deimos::KafkaTopicInfo.create!(topic: 'topic3', last_processed_at: old_time,
|
|
70
|
+
locked_by: 'me', locked_at: 1.minute.ago)
|
|
71
|
+
|
|
72
|
+
expect(Deimos::KafkaTopicInfo.count).to eq(3)
|
|
73
|
+
Deimos::KafkaTopicInfo.all.each { |t| expect(t.last_processed_at.to_s).to eq(old_time) }
|
|
74
|
+
Deimos::KafkaTopicInfo.ping_empty_topics(%w(topic1))
|
|
75
|
+
expect(t1.reload.last_processed_at.to_s).to eq(old_time) # was passed as an exception
|
|
76
|
+
expect(t2.reload.last_processed_at.to_s).to eq(Time.zone.now.to_s)
|
|
77
|
+
expect(t3.reload.last_processed_at.to_s).to eq(old_time) # is locked
|
|
78
|
+
end
|
|
56
79
|
end
|
|
57
80
|
|
|
58
81
|
specify '#register_error' do
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"namespace": "com.my-namespace",
|
|
3
|
+
"name": "Generated",
|
|
4
|
+
"type": "record",
|
|
5
|
+
"doc": "Test schema",
|
|
6
|
+
"fields": [
|
|
7
|
+
{
|
|
8
|
+
"name": "a_string",
|
|
9
|
+
"type": "string"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"name": "a_int",
|
|
13
|
+
"type": "int"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "a_long",
|
|
17
|
+
"type": "long"
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"name": "a_float",
|
|
21
|
+
"type": "float"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"name": "a_double",
|
|
25
|
+
"type": "double"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "an_enum",
|
|
29
|
+
"type": {
|
|
30
|
+
"type": "enum",
|
|
31
|
+
"name": "AnEnum",
|
|
32
|
+
"symbols": ["sym1", "sym2"]
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "an_array",
|
|
37
|
+
"type": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"items": "int"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "a_map",
|
|
44
|
+
"type": {
|
|
45
|
+
"type": "map",
|
|
46
|
+
"values": "string"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"name": "timestamp",
|
|
51
|
+
"type": "string"
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "message_id",
|
|
55
|
+
"type": "string"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "a_record",
|
|
59
|
+
"type": {
|
|
60
|
+
"type": "record",
|
|
61
|
+
"name": "ARecord",
|
|
62
|
+
"fields": [
|
|
63
|
+
{
|
|
64
|
+
"name": "a_record_field",
|
|
65
|
+
"type": "string"
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
}
|
data/spec/spec_helper.rb
CHANGED
|
@@ -16,11 +16,13 @@ each_db_config(Deimos::Utils::DbProducer) do
|
|
|
16
16
|
|
|
17
17
|
before(:each) do
|
|
18
18
|
stub_const('Deimos::Utils::DbProducer::BATCH_SIZE', 2)
|
|
19
|
+
stub_const('Deimos::Utils::DbProducer::DELETE_BATCH_SIZE', 1)
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
specify '#process_next_messages' do
|
|
22
23
|
expect(producer).to receive(:retrieve_topics).and_return(%w(topic1 topic2))
|
|
23
24
|
expect(producer).to receive(:process_topic).twice
|
|
25
|
+
expect(Deimos::KafkaTopicInfo).to receive(:ping_empty_topics).with(%w(topic1 topic2))
|
|
24
26
|
expect(producer).to receive(:sleep).with(0.5)
|
|
25
27
|
producer.process_next_messages
|
|
26
28
|
end
|
|
@@ -40,6 +42,9 @@ each_db_config(Deimos::Utils::DbProducer) do
|
|
|
40
42
|
Deimos::KafkaMessage.create!(topic: 'topic1',
|
|
41
43
|
message: 'blah',
|
|
42
44
|
key: "key#{i}")
|
|
45
|
+
Deimos::KafkaMessage.create!(topic: 'topic2',
|
|
46
|
+
message: 'blah',
|
|
47
|
+
key: "key#{i}")
|
|
43
48
|
end
|
|
44
49
|
stub_const('Deimos::Utils::DbProducer::BATCH_SIZE', 5)
|
|
45
50
|
producer.current_topic = 'topic1'
|
|
@@ -280,6 +285,12 @@ each_db_config(Deimos::Utils::DbProducer) do
|
|
|
280
285
|
message: "mess#{i}",
|
|
281
286
|
partition_key: "key#{i}"
|
|
282
287
|
)
|
|
288
|
+
Deimos::KafkaMessage.create!(
|
|
289
|
+
id: i,
|
|
290
|
+
topic: 'my-topic2',
|
|
291
|
+
message: "mess#{i}",
|
|
292
|
+
partition_key: "key#{i}"
|
|
293
|
+
)
|
|
283
294
|
end
|
|
284
295
|
|
|
285
296
|
expect(Deimos::KafkaTopicInfo).to receive(:lock).
|
|
@@ -288,9 +299,60 @@ each_db_config(Deimos::Utils::DbProducer) do
|
|
|
288
299
|
expect(producer).to receive(:retrieve_messages).and_return(messages)
|
|
289
300
|
expect(Deimos::KafkaTopicInfo).to receive(:register_error)
|
|
290
301
|
|
|
302
|
+
expect(Deimos::KafkaMessage.count).to eq(8)
|
|
303
|
+
producer.process_topic('my-topic')
|
|
291
304
|
expect(Deimos::KafkaMessage.count).to eq(4)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
it 'should retry deletes and not re-publish' do
|
|
308
|
+
messages = (1..4).map do |i|
|
|
309
|
+
Deimos::KafkaMessage.create!(
|
|
310
|
+
id: i,
|
|
311
|
+
topic: 'my-topic',
|
|
312
|
+
message: "mess#{i}",
|
|
313
|
+
partition_key: "key#{i}"
|
|
314
|
+
)
|
|
315
|
+
end
|
|
316
|
+
(5..8).each do |i|
|
|
317
|
+
Deimos::KafkaMessage.create!(
|
|
318
|
+
id: i,
|
|
319
|
+
topic: 'my-topic2',
|
|
320
|
+
message: "mess#{i}",
|
|
321
|
+
partition_key: "key#{i}"
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
raise_error = true
|
|
326
|
+
expect(Deimos::KafkaMessage).to receive(:where).exactly(5).times.and_wrap_original do |m, *args|
|
|
327
|
+
if raise_error
|
|
328
|
+
raise_error = false
|
|
329
|
+
raise 'Lock wait timeout'
|
|
330
|
+
end
|
|
331
|
+
m.call(*args)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
expect(Deimos::KafkaTopicInfo).to receive(:lock).
|
|
335
|
+
with('my-topic', 'abc').and_return(true)
|
|
336
|
+
expect(producer).to receive(:retrieve_messages).ordered.and_return(messages)
|
|
337
|
+
expect(producer).to receive(:retrieve_messages).ordered.and_return([])
|
|
338
|
+
expect(phobos_producer).to receive(:publish_list).once.with(messages.map(&:phobos_message))
|
|
339
|
+
|
|
340
|
+
expect(Deimos::KafkaMessage.count).to eq(8)
|
|
292
341
|
producer.process_topic('my-topic')
|
|
293
|
-
expect(Deimos::KafkaMessage.count).to eq(
|
|
342
|
+
expect(Deimos::KafkaMessage.count).to eq(4)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
it 'should re-raise misc errors on delete' do
|
|
346
|
+
messages = (1..3).map do |i|
|
|
347
|
+
Deimos::KafkaMessage.create!(
|
|
348
|
+
id: i,
|
|
349
|
+
topic: 'my-topic',
|
|
350
|
+
message: "mess#{i}",
|
|
351
|
+
partition_key: "key#{i}"
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
expect(Deimos::KafkaMessage).to receive(:where).once.and_raise('OH NOES')
|
|
355
|
+
expect { producer.delete_messages(messages) }.to raise_exception('OH NOES')
|
|
294
356
|
end
|
|
295
357
|
|
|
296
358
|
end
|
|
@@ -309,21 +371,34 @@ each_db_config(Deimos::Utils::DbProducer) do
|
|
|
309
371
|
Deimos::KafkaMessage.create!(topic: "topic#{i}", message: nil,
|
|
310
372
|
created_at: (1 + i).minute.ago)
|
|
311
373
|
end
|
|
374
|
+
Deimos::KafkaTopicInfo.create!(topic: 'topic1',
|
|
375
|
+
last_processed_at: 6.minutes.ago)
|
|
376
|
+
Deimos::KafkaTopicInfo.create!(topic: 'topic2',
|
|
377
|
+
last_processed_at: 3.minutes.ago)
|
|
378
|
+
Deimos::KafkaTopicInfo.create!(topic: 'topic3',
|
|
379
|
+
last_processed_at: 5.minutes.ago)
|
|
312
380
|
allow(Deimos.config.metrics).to receive(:gauge)
|
|
313
381
|
producer.send_pending_metrics
|
|
314
|
-
expect(Deimos.config.metrics).to have_received(:gauge).
|
|
382
|
+
expect(Deimos.config.metrics).to have_received(:gauge).exactly(6).times
|
|
383
|
+
# topic1 has the earliest message 4 minutes ago and last processed 6
|
|
384
|
+
# minutes ago, so the most amount of time we've seen nothing is 4 minutes
|
|
315
385
|
expect(Deimos.config.metrics).to have_received(:gauge).
|
|
316
386
|
with('pending_db_messages_max_wait', 4.minutes.to_i, tags: ['topic:topic1'])
|
|
387
|
+
# topic2 has earliest message 5 minutes ago and last processed 3 minutes
|
|
388
|
+
# ago, so we should give it 3 minutes
|
|
389
|
+
expect(Deimos.config.metrics).to have_received(:gauge).
|
|
390
|
+
with('pending_db_messages_max_wait', 3.minutes.to_i, tags: ['topic:topic2'])
|
|
391
|
+
# topic3 has no messages, so should get 0
|
|
317
392
|
expect(Deimos.config.metrics).to have_received(:gauge).
|
|
318
|
-
with('pending_db_messages_max_wait',
|
|
393
|
+
with('pending_db_messages_max_wait', 0, tags: ['topic:topic3'])
|
|
394
|
+
expect(Deimos.config.metrics).to have_received(:gauge).
|
|
395
|
+
with('pending_db_messages_count', 3, tags: ['topic:topic1'])
|
|
396
|
+
expect(Deimos.config.metrics).to have_received(:gauge).
|
|
397
|
+
with('pending_db_messages_count', 3, tags: ['topic:topic2'])
|
|
398
|
+
expect(Deimos.config.metrics).to have_received(:gauge).
|
|
399
|
+
with('pending_db_messages_count', 0, tags: ['topic:topic3'])
|
|
319
400
|
end
|
|
320
401
|
end
|
|
321
|
-
|
|
322
|
-
it 'should send 0 if no messages' do
|
|
323
|
-
expect(Deimos.config.metrics).to receive(:gauge).
|
|
324
|
-
with('pending_db_messages_max_wait', 0)
|
|
325
|
-
producer.send_pending_metrics
|
|
326
|
-
end
|
|
327
402
|
end
|
|
328
403
|
|
|
329
404
|
example 'Full integration test' do
|
|
@@ -403,5 +478,4 @@ each_db_config(Deimos::Utils::DbProducer) do
|
|
|
403
478
|
}
|
|
404
479
|
])
|
|
405
480
|
end
|
|
406
|
-
|
|
407
481
|
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.
|
|
4
|
+
version: 1.8.1.pre.beta1
|
|
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-07-
|
|
11
|
+
date: 2020-07-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: avro_turf
|
|
@@ -72,14 +72,14 @@ dependencies:
|
|
|
72
72
|
requirements:
|
|
73
73
|
- - "~>"
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '
|
|
75
|
+
version: '6'
|
|
76
76
|
type: :development
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
80
|
- - "~>"
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '
|
|
82
|
+
version: '6'
|
|
83
83
|
- !ruby/object:Gem::Dependency
|
|
84
84
|
name: activerecord-import
|
|
85
85
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -226,20 +226,14 @@ dependencies:
|
|
|
226
226
|
requirements:
|
|
227
227
|
- - "~>"
|
|
228
228
|
- !ruby/object:Gem::Version
|
|
229
|
-
version: '
|
|
230
|
-
- - ">="
|
|
231
|
-
- !ruby/object:Gem::Version
|
|
232
|
-
version: 5.2.4.2
|
|
229
|
+
version: '6'
|
|
233
230
|
type: :development
|
|
234
231
|
prerelease: false
|
|
235
232
|
version_requirements: !ruby/object:Gem::Requirement
|
|
236
233
|
requirements:
|
|
237
234
|
- - "~>"
|
|
238
235
|
- !ruby/object:Gem::Version
|
|
239
|
-
version: '
|
|
240
|
-
- - ">="
|
|
241
|
-
- !ruby/object:Gem::Version
|
|
242
|
-
version: 5.2.4.2
|
|
236
|
+
version: '6'
|
|
243
237
|
- !ruby/object:Gem::Dependency
|
|
244
238
|
name: rake
|
|
245
239
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -406,6 +400,9 @@ files:
|
|
|
406
400
|
- lib/deimos/utils/inline_consumer.rb
|
|
407
401
|
- lib/deimos/utils/lag_reporter.rb
|
|
408
402
|
- lib/deimos/version.rb
|
|
403
|
+
- lib/generators/deimos/active_record/templates/migration.rb.tt
|
|
404
|
+
- lib/generators/deimos/active_record/templates/model.rb.tt
|
|
405
|
+
- lib/generators/deimos/active_record_generator.rb
|
|
409
406
|
- lib/generators/deimos/db_backend/templates/migration
|
|
410
407
|
- lib/generators/deimos/db_backend/templates/rails3_migration
|
|
411
408
|
- lib/generators/deimos/db_backend_generator.rb
|
|
@@ -427,6 +424,7 @@ files:
|
|
|
427
424
|
- spec/config/configuration_spec.rb
|
|
428
425
|
- spec/consumer_spec.rb
|
|
429
426
|
- spec/deimos_spec.rb
|
|
427
|
+
- spec/generators/active_record_generator_spec.rb
|
|
430
428
|
- spec/handlers/my_batch_consumer.rb
|
|
431
429
|
- spec/handlers/my_consumer.rb
|
|
432
430
|
- spec/kafka_source_spec.rb
|
|
@@ -441,6 +439,7 @@ files:
|
|
|
441
439
|
- spec/schema_backends/avro_schema_registry_spec.rb
|
|
442
440
|
- spec/schema_backends/avro_validation_spec.rb
|
|
443
441
|
- spec/schema_backends/base_spec.rb
|
|
442
|
+
- spec/schemas/com/my-namespace/Generated.avsc
|
|
444
443
|
- spec/schemas/com/my-namespace/MySchema-key.avsc
|
|
445
444
|
- spec/schemas/com/my-namespace/MySchema.avsc
|
|
446
445
|
- spec/schemas/com/my-namespace/MySchemaCompound-key.avsc
|
|
@@ -499,6 +498,7 @@ test_files:
|
|
|
499
498
|
- spec/config/configuration_spec.rb
|
|
500
499
|
- spec/consumer_spec.rb
|
|
501
500
|
- spec/deimos_spec.rb
|
|
501
|
+
- spec/generators/active_record_generator_spec.rb
|
|
502
502
|
- spec/handlers/my_batch_consumer.rb
|
|
503
503
|
- spec/handlers/my_consumer.rb
|
|
504
504
|
- spec/kafka_source_spec.rb
|
|
@@ -513,6 +513,7 @@ test_files:
|
|
|
513
513
|
- spec/schema_backends/avro_schema_registry_spec.rb
|
|
514
514
|
- spec/schema_backends/avro_validation_spec.rb
|
|
515
515
|
- spec/schema_backends/base_spec.rb
|
|
516
|
+
- spec/schemas/com/my-namespace/Generated.avsc
|
|
516
517
|
- spec/schemas/com/my-namespace/MySchema-key.avsc
|
|
517
518
|
- spec/schemas/com/my-namespace/MySchema.avsc
|
|
518
519
|
- spec/schemas/com/my-namespace/MySchemaCompound-key.avsc
|