dionysus-rb 0.5.0 → 1.0.0

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: 64406a4ca852cfc6d663586730005d9c562121571cb0b876de7584eb63b2c49f
4
- data.tar.gz: c30a51ca9f590795e55021209f274d67a18aa197bd2ebb7be3ed471940ee7f6a
3
+ metadata.gz: 0c0362ffd0074956ac7ace1f3c4a392eaa7b5b09a79ecf113a14f9da8f9f5b22
4
+ data.tar.gz: 44cdab85d128017c09dc0535c1225d9d68515addc5c2d1d4b28b50dbb2fc0150
5
5
  SHA512:
6
- metadata.gz: bd48b03ee34b15dd0481733a9bdd726ef23c871d2a6a19249501ffa5ebb200559a2c301785a0e9449e79059eb98f3166da2c70049d087003dc2684c89ab0a013
7
- data.tar.gz: 13f6d2e1068ea38a8c8954e594fe36382d8d6a061228c5f1f9c5de722651c495363be8569874757ce8f1fe41232495597676532d7e56da1a5a3fe25a4c0a32f1
6
+ metadata.gz: 63905f2eb238cdca13e750e728498449d6238be26f30bbe68c4709c61218c989d33dd8c7da553c4c4ec6e36dc6b05cc84a8cc44067048752b88018338d1809ee
7
+ data.tar.gz: cabf8edf52bd2ba5ffaf26608110a7f110ba4a7c15ff2ead082c74cd780c5018bd92b0b44a15e847edcf65c39a898bdd537f2c9c85c3c191c1a1803c1a657e96
data/.circleci/config.yml CHANGED
@@ -4,7 +4,7 @@ jobs:
4
4
  working_directory: ~/BookingSync/dionysus-rb
5
5
  parallelism: 1
6
6
  docker:
7
- - image: cimg/ruby:2.7.6
7
+ - image: cimg/ruby:3.3.11
8
8
  environment:
9
9
  POSTGRES_URL: "postgres://docker:docker@127.0.0.1:5432"
10
10
  DATABASE_URL: "postgres://docker:docker@127.0.0.1:5432/circle_test"
@@ -6,10 +6,10 @@ jobs:
6
6
  fail-fast: true
7
7
  runs-on: ubuntu-latest
8
8
  steps:
9
- - uses: actions/checkout@v2
9
+ - uses: actions/checkout@v4
10
10
  - uses: ruby/setup-ruby@v1
11
11
  with:
12
- ruby-version: 2.7
12
+ ruby-version: '3.3'
13
13
  bundler-cache: true
14
14
  - run: bundle exec rubocop
15
15
  rspec:
@@ -17,7 +17,7 @@ jobs:
17
17
  strategy:
18
18
  fail-fast: false
19
19
  matrix:
20
- ruby: ['2.7', '3.0.6', '3.1', '3.2']
20
+ ruby: ['3.3', '3.4']
21
21
  runs-on: ubuntu-latest
22
22
  env:
23
23
  DD_PROFILING_NO_EXTENSION: true
@@ -27,23 +27,6 @@ jobs:
27
27
  POSTGRES_USER: postgres
28
28
  POSTGRES_PASSWORD: postgres
29
29
  POSTGRES_DB: dionysus-rb-test
30
- CLUSTER_ID: kafka-docker-cluster-1
31
- KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
32
- KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
33
- KAFKA_PROCESS_ROLES: broker,controller
34
- KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
35
- KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
36
- KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
37
- KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://127.0.0.1:9092
38
- KAFKA_BROKER_ID: 1
39
- KAFKA_CONTROLLER_QUORUM_VOTERS: 1@127.0.0.1:9093
40
- ALLOW_PLAINTEXT_LISTENER: 'yes'
41
- KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
42
- KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
43
- KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
44
- KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: "true"
45
- KAFKA_AUTHORIZER_CLASS_NAME: org.apache.kafka.metadata.authorizer.StandardAuthorizer
46
- KAFKA_CREATE_TOPICS: "v102_rentals:1:1"
47
30
  services:
48
31
  postgres:
49
32
  image: postgres:14
@@ -63,14 +46,34 @@ jobs:
63
46
  --health-interval 10s
64
47
  --health-timeout 5s
65
48
  --health-retries 5
49
+ kafka:
50
+ image: confluentinc/cp-kafka:7.5.2
51
+ env:
52
+ CLUSTER_ID: MkU3OEVBNTcwNTJENDM2Qk
53
+ KAFKA_NODE_ID: 1
54
+ KAFKA_PROCESS_ROLES: broker,controller
55
+ KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093
56
+ KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
57
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
58
+ KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
59
+ KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
60
+ KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
61
+ KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true'
62
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
63
+ KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
64
+ KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
65
+ KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0
66
+ ports:
67
+ - 9092:9092
68
+ options: >-
69
+ --health-cmd "kafka-topics --bootstrap-server localhost:9092 --list"
70
+ --health-interval 10s
71
+ --health-timeout 10s
72
+ --health-retries 12
73
+ --health-start-period 20s
66
74
  steps:
67
- - uses: actions/checkout@v2
68
- - name: Start Kafka with docker-compose
69
- run: |
70
- docker-compose up -d || (sleep 5 && docker-compose up -d)
71
-
75
+ - uses: actions/checkout@v4
72
76
  - uses: ruby/setup-ruby@v1
73
-
74
77
  with:
75
78
  ruby-version: ${{ matrix.ruby }}
76
79
  bundler-cache: true
data/.rubocop.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  inherit_from: .rubocop_todo.yml
2
2
 
3
- require:
3
+ plugins:
4
4
  - rubocop-performance
5
5
  - rubocop-rspec
6
6
  - rubocop-rake
@@ -11,7 +11,7 @@ inherit_mode:
11
11
 
12
12
  AllCops:
13
13
  NewCops: enable
14
- TargetRubyVersion: 2.7
14
+ TargetRubyVersion: 3.3
15
15
  Exclude:
16
16
  - "db/**/*"
17
17
  - "bin/**/*"
@@ -129,6 +129,21 @@ RSpec/ExampleLength:
129
129
  Naming/VariableNumber:
130
130
  Enabled: false
131
131
 
132
+ # Same rationale as Naming/VariableNumber: the specs deliberately number fixtures
133
+ # (rental_1, metadata_2) when they describe an ordered batch.
134
+ RSpec/IndexedLet:
135
+ Enabled: false
136
+
137
+ # Development dependencies are declared in the gemspec, not in the Gemfile.
138
+ Gemspec/DevelopmentDependencies:
139
+ Enabled: false
140
+
141
+ # `is_expected_block` (spec/support/is_expected_block.rb) is `expect { subject }`,
142
+ # which the cop cannot see through.
143
+ RSpec/NoExpectationExample:
144
+ AllowedPatterns:
145
+ - "^is_expected_block"
146
+
132
147
  Metrics/BlockLength:
133
148
  Max: 30
134
149
  Exclude:
@@ -136,7 +151,7 @@ Metrics/BlockLength:
136
151
  - 'lib/dionysus/support/rspec/outbox_publishable.rb'
137
152
  - 'lib/dionysus/consumer/persistor.rb'
138
153
 
139
- Naming/PredicateName:
154
+ Naming/PredicatePrefix:
140
155
  Enabled: false
141
156
 
142
157
  Metrics/AbcSize:
data/.rubocop_todo.yml CHANGED
@@ -52,3 +52,17 @@ Metrics/CyclomaticComplexity:
52
52
  Metrics/BlockLength:
53
53
  Exclude:
54
54
  - 'lib/dionysus/producer/karafka_responder_generator.rb'
55
+
56
+ # `declared_attributes` and friends are Arrays of [name, options] pairs, not Hashes,
57
+ # so the cop's each_key/each_value suggestion raises NoMethodError.
58
+ Style/HashEachMethods:
59
+ Exclude:
60
+ - 'lib/dionysus/producer/model_serializer.rb'
61
+
62
+ # integration_spec prints progress for a long-running Kafka run; the null provider
63
+ # specs use `puts` as the sentinel proving the block was called.
64
+ RSpec/Output:
65
+ Exclude:
66
+ - 'spec/dionysus/utils/null_retry_provider_spec.rb'
67
+ - 'spec/dionysus/utils/null_transaction_provider_spec.rb'
68
+ - 'spec/integration_spec.rb'
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ ruby-3.3.11
data/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.0.0]
4
+
5
+ Brings `dionysus-rb` back in line with the private gem it was extracted from, which had moved on
6
+ by four releases.
7
+
8
+ ### Breaking changes
9
+
10
+ - Require Ruby >= 3.3 (was >= 2.7).
11
+ - Require karafka `~> 2.4` (was `~> 2.0`). Follow https://karafka.io/docs/Upgrades-2.4/ - most
12
+ notably, `Karafka::Messages::Metadata` no longer carries a single `deserializer`, it carries a
13
+ `deserializers` config with separate `payload` and `key` entries, and `key` is now `raw_key`.
14
+ This only affects code that constructs metadata by hand, such as consumer specs.
15
+ - The instrumenter is now expected to respond to `increment(name, options)` in addition to
16
+ `instrument`. `Dionysus::Utils::NullInstrumenter` implements it; a custom instrumenter that does
17
+ not will raise when a publish produces no message (see below).
18
+
19
+ ### Added
20
+
21
+ - Notify when publishing an outbox record produces no Kafka message at all.
22
+ `Dionysus::Producer::Outbox::Publisher#publish` could complete successfully having sent nothing -
23
+ a model declared only as a `with:` dependency has no top-level responder, so when its parent
24
+ could not be resolved the call returned normally - and `RecordsProcessor` then marked the record
25
+ as published, losing the change in every consumer with no error and no retry. Such a record is
26
+ now logged, counted as `dionysus.publish.nothing_published` and reported through the configured
27
+ `error_handler`. It notifies rather than raises: the producer cannot distinguish a legitimately
28
+ deleted parent from one that is not visible yet.
29
+ - `Dionysus.initialize_application!` accepts `consumer_group_name`, which is used verbatim as the
30
+ consumer group name and takes precedence over `consumer_group_prefix`. `consumer_group_prefix`
31
+ stays supported and unchanged, so the documented migration path from `bookingsync-prometheus`
32
+ keeps working.
33
+
34
+ ### Fixed
35
+
36
+ - Compatibility with the `datadog` gem 2.0, which renamed the `span_type:` keyword of
37
+ `Datadog::Tracing#trace` to `type:`. `Dionysus::Producer::Outbox::DatadogTracer` now picks the
38
+ right keyword depending on whether the pre-2.0 `ddtrace` gem is loaded.
39
+ - Do not publish the `dionysus.consume_batch` event when the batch is empty.
40
+ - `Dionysus::Producer::Outbox::Publisher` no longer trips over a `nil` entry in a parent
41
+ association collection.
42
+
3
43
  ## [0.5.0]
4
44
  - Publish consumed messages batch as `dionysus.consume_batch` event.
5
45
 
data/Gemfile.lock CHANGED
@@ -1,13 +1,13 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- dionysus-rb (0.5.0)
4
+ dionysus-rb (1.0.0)
5
5
  activerecord (>= 5)
6
6
  activesupport (>= 3.2)
7
7
  concurrent-ruby
8
8
  dry-monitor
9
9
  file-based-healthcheck
10
- karafka (~> 2.0)
10
+ karafka (~> 2.4)
11
11
  sidekiq
12
12
  sidekiq-cron
13
13
  sigurd
@@ -34,8 +34,8 @@ GEM
34
34
  mutex_m
35
35
  tzinfo (~> 2.0)
36
36
  amq-protocol (2.3.2)
37
- ast (2.4.2)
38
- base64 (0.1.1)
37
+ ast (2.4.3)
38
+ base64 (0.3.0)
39
39
  bigdecimal (3.1.4)
40
40
  bunny (2.22.0)
41
41
  amq-protocol (~> 2.3, >= 2.3.1)
@@ -43,20 +43,20 @@ GEM
43
43
  byebug (11.1.3)
44
44
  carrot-top (0.0.7)
45
45
  json
46
+ cgi (0.5.2)
46
47
  concurrent-ruby (1.2.2)
47
48
  connection_pool (2.4.1)
48
49
  crypt_keeper (0.13.1)
49
50
  activerecord (>= 3.0)
50
51
  activesupport (>= 3.0)
51
- datadog-ci (0.5.1)
52
+ datadog (2.40.0)
53
+ cgi
54
+ datadog-ruby_core_source (~> 3.5, >= 3.5.4)
55
+ libdatadog (~> 36.0.0.1.0)
56
+ libddwaf (~> 1.30.0.0.0)
57
+ logger
52
58
  msgpack
53
- ddtrace (1.18.0)
54
- datadog-ci (~> 0.5.0)
55
- debase-ruby_core_source (= 3.2.3)
56
- libdatadog (~> 5.0.0.1.0)
57
- libddwaf (~> 1.14.0.0.0)
58
- msgpack
59
- debase-ruby_core_source (3.2.3)
59
+ datadog-ruby_core_source (3.5.4)
60
60
  diff-lcs (1.5.0)
61
61
  dogstatsd-ruby (5.5.0)
62
62
  drb (2.1.1)
@@ -118,32 +118,41 @@ GEM
118
118
  i18n (1.14.1)
119
119
  concurrent-ruby (~> 1.0)
120
120
  ice_nine (0.11.2)
121
- json (2.6.2)
122
- karafka (2.2.7)
123
- karafka-core (>= 2.2.2, < 2.3.0)
124
- thor (>= 0.20)
125
- waterdrop (>= 2.6.6, < 3.0.0)
121
+ json (2.21.2)
122
+ karafka (2.5.2)
123
+ base64 (~> 0.2)
124
+ karafka-core (>= 2.5.6, < 2.6.0)
125
+ karafka-rdkafka (>= 0.22.0)
126
+ waterdrop (>= 2.8.9, < 3.0.0)
126
127
  zeitwerk (~> 2.3)
127
- karafka-core (2.2.2)
128
- concurrent-ruby (>= 1.1)
129
- karafka-rdkafka (>= 0.13.1, < 0.14.0)
130
- karafka-rdkafka (0.13.5)
128
+ karafka-core (2.5.13)
129
+ karafka-rdkafka (>= 0.20.0)
130
+ logger (>= 1.6.0)
131
+ karafka-rdkafka (0.22.2)
131
132
  ffi (~> 1.15)
133
+ json (> 2.0)
134
+ logger
132
135
  mini_portile2 (~> 2.6)
133
136
  rake (> 12)
134
- libdatadog (5.0.0.1.0)
135
- libddwaf (1.14.0.0.0)
137
+ language_server-protocol (3.17.0.6)
138
+ libdatadog (36.0.0.1.0)
139
+ libddwaf (1.30.0.0.2)
136
140
  ffi (~> 1.0)
141
+ lint_roller (1.1.0)
142
+ logger (1.7.0)
137
143
  mini_portile2 (2.8.4)
138
144
  minitest (5.20.0)
139
- msgpack (1.7.2)
145
+ msgpack (1.8.4)
140
146
  multi_json (1.15.0)
141
147
  mutex_m (0.1.2)
142
- parallel (1.22.1)
143
- parser (3.1.2.1)
148
+ parallel (2.1.0)
149
+ parser (3.3.12.0)
144
150
  ast (~> 2.4.1)
151
+ racc
145
152
  pg (1.3.5)
153
+ prism (1.9.0)
146
154
  raabro (1.4.0)
155
+ racc (1.8.1)
147
156
  rack (2.2.4)
148
157
  rainbow (3.1.1)
149
158
  rake (13.0.6)
@@ -153,10 +162,9 @@ GEM
153
162
  redis (>= 3.0.4)
154
163
  redlock (1.2.2)
155
164
  redis (>= 3.0.0, < 5.0)
156
- regexp_parser (2.5.0)
165
+ regexp_parser (2.12.0)
157
166
  request_store (1.5.1)
158
167
  rack (>= 1.4)
159
- rexml (3.2.5)
160
168
  rspec (3.12.0)
161
169
  rspec-core (~> 3.12.0)
162
170
  rspec-expectations (~> 3.12.0)
@@ -175,26 +183,32 @@ GEM
175
183
  rspec-core (~> 3.0, >= 3.0.0)
176
184
  sidekiq (>= 2.4.0)
177
185
  rspec-support (3.12.1)
178
- rubocop (1.35.0)
186
+ rubocop (1.89.0)
179
187
  json (~> 2.3)
180
- parallel (~> 1.10)
181
- parser (>= 3.1.2.1)
188
+ language_server-protocol (~> 3.17.0.2)
189
+ lint_roller (~> 1.1.0)
190
+ parallel (>= 1.10)
191
+ parser (>= 3.3.0.2)
182
192
  rainbow (>= 2.2.2, < 4.0)
183
- regexp_parser (>= 1.8, < 3.0)
184
- rexml (>= 3.2.5, < 4.0)
185
- rubocop-ast (>= 1.20.1, < 2.0)
193
+ regexp_parser (>= 2.9.3, < 3.0)
194
+ rubocop-ast (>= 1.49.0, < 2.0)
186
195
  ruby-progressbar (~> 1.7)
187
- unicode-display_width (>= 1.4.0, < 3.0)
188
- rubocop-ast (1.21.0)
189
- parser (>= 3.1.1.0)
190
- rubocop-performance (1.14.3)
191
- rubocop (>= 1.7.0, < 2.0)
192
- rubocop-ast (>= 0.4.0)
193
- rubocop-rake (0.6.0)
194
- rubocop (~> 1.0)
195
- rubocop-rspec (2.12.1)
196
- rubocop (~> 1.31)
197
- ruby-progressbar (1.11.0)
196
+ unicode-display_width (>= 2.4.0, < 4.0)
197
+ rubocop-ast (1.50.0)
198
+ parser (>= 3.3.7.2)
199
+ prism (~> 1.7)
200
+ rubocop-performance (1.26.1)
201
+ lint_roller (~> 1.1)
202
+ rubocop (>= 1.75.0, < 2.0)
203
+ rubocop-ast (>= 1.47.1, < 2.0)
204
+ rubocop-rake (0.7.1)
205
+ lint_roller (~> 1.1)
206
+ rubocop (>= 1.72.1)
207
+ rubocop-rspec (3.10.2)
208
+ lint_roller (~> 1.1)
209
+ regexp_parser (>= 2.0)
210
+ rubocop (~> 1.86, >= 1.86.2)
211
+ ruby-progressbar (1.13.0)
198
212
  ruby2_keywords (0.0.5)
199
213
  sentry-ruby (5.4.1)
200
214
  concurrent-ruby (~> 1.0, >= 1.0.2)
@@ -215,14 +229,16 @@ GEM
215
229
  sorted_set (1.0.3)
216
230
  rbtree
217
231
  set (~> 1.0)
218
- thor (1.2.2)
219
232
  timecop (0.9.5)
220
233
  timeout (0.4.0)
221
234
  tzinfo (2.0.6)
222
235
  concurrent-ruby (~> 1.0)
223
- unicode-display_width (2.2.0)
224
- waterdrop (2.6.7)
225
- karafka-core (>= 2.1.1, < 3.0.0)
236
+ unicode-display_width (3.2.0)
237
+ unicode-emoji (~> 4.1)
238
+ unicode-emoji (4.2.0)
239
+ waterdrop (2.8.13)
240
+ karafka-core (>= 2.4.9, < 3.0.0)
241
+ karafka-rdkafka (>= 0.20.0)
226
242
  zeitwerk (~> 2.3)
227
243
  zeitwerk (2.6.12)
228
244
 
@@ -234,7 +250,7 @@ DEPENDENCIES
234
250
  bundler
235
251
  byebug
236
252
  crypt_keeper
237
- ddtrace
253
+ datadog
238
254
  dionysus-rb!
239
255
  dogstatsd-ruby
240
256
  hermes-rb
data/README.md CHANGED
@@ -62,7 +62,19 @@ Dionysus.initialize_application!(
62
62
  )
63
63
  ```
64
64
 
65
- By default, the name of the consumer grpup will be "NAME_OF_THE_APP_dionysus_consumer_group_for_NAME_OF_THE_APP" where `dionysus_consumer_group_for` is a `consumer_group_prefix`.
65
+ By default, the name of the consumer group will be `"dionysus_consumer_group_for_NAME_OF_THE_APP"`, where `dionysus_consumer_group_for` is the `consumer_group_prefix`.
66
+
67
+ If a prefix is not enough and you need to control the entire name, provide `consumer_group_name`, which is used verbatim and takes precedence over `consumer_group_prefix`:
68
+
69
+ ``` rb
70
+ Dionysus.initialize_application!(
71
+ environment: ENV["RAILS_ENV"],
72
+ seed_brokers: ENV.fetch("DIONYSUS_SEED_BROKER").split(";"),
73
+ client_id: "NAME_OF_THE_APP",
74
+ logger: Rails.logger,
75
+ consumer_group_name: "your_custom_consumer_group_name"
76
+ )
77
+ ```
66
78
 
67
79
 
68
80
  And define `dionysus.rb` initializer with your Kafka topics:
@@ -234,9 +246,16 @@ class Dionysus::Utils::NullInstrumenter
234
246
  def self.instrument(name, payload = {})
235
247
  yield
236
248
  end
249
+
250
+ def self.increment(name, options = {})
251
+ end
237
252
  end
238
253
  ```
239
254
 
255
+ `increment` is used for counters that are not tied to a block, currently only
256
+ `dionysus.publish.nothing_published` (see the Transactional Outbox Pattern section). A custom
257
+ instrumenter that does not implement it will raise when that counter is emitted.
258
+
240
259
 
241
260
  Event Bus is useful if you want to react to some events, with the following interface ((this is the default class)):
242
261
 
@@ -508,6 +527,24 @@ bundle exec outbox_worker_health_check
508
527
 
509
528
  It works for both readiness and liveness checks.
510
529
 
530
+ ##### Publishes that produce no Kafka message
531
+
532
+ `Dionysus::Producer::Outbox::Publisher#publish` can legitimately match no responder at all - a
533
+ model declared only as a `with:` dependency has no top-level responder of its own, so when its
534
+ parent cannot be resolved nothing gets sent. The outbox record would still be stamped as
535
+ published, which means the change is silently lost in every consumer.
536
+
537
+ Such a record is now logged via `Dionysus.logger.error`, reported through the configured
538
+ `error_handler` with `capture_message`, and counted as:
539
+
540
+ ```
541
+ instrumenter.increment("dionysus.publish.nothing_published", tags: ["resource:<class>", "topic:<topic>"])
542
+ ```
543
+
544
+ It notifies rather than raises, because the producer cannot tell a legitimately deleted parent
545
+ from one that is merely not visible yet, and raising on the former would retry forever. Reprocess
546
+ the reported record from the console.
547
+
511
548
  #### Tombstoning records
512
549
 
513
550
  The only way to get rid of messages under a given key from Kafka is to tombstone them. Use `Dionysus::Producer::Outbox::TombstonePublisher` to do it:
@@ -677,7 +714,19 @@ Dionysus.initialize_application!(
677
714
  )
678
715
  ```
679
716
 
680
- By default, the name of the consumer grpup will be "NAME_OF_THE_APP_dionysus_consumer_group_for_NAME_OF_THE_APP" where `dionysus_consumer_group_for` is a `consumer_group_prefix`.
717
+ By default, the name of the consumer group will be `"dionysus_consumer_group_for_NAME_OF_THE_APP"`, where `dionysus_consumer_group_for` is the `consumer_group_prefix`.
718
+
719
+ If a prefix is not enough and you need to control the entire name, provide `consumer_group_name`, which is used verbatim and takes precedence over `consumer_group_prefix`:
720
+
721
+ ``` rb
722
+ Dionysus.initialize_application!(
723
+ environment: ENV["RAILS_ENV"],
724
+ seed_brokers: ENV.fetch("DIONYSUS_SEED_BROKER").split(";"),
725
+ client_id: "NAME_OF_THE_APP",
726
+ logger: Rails.logger,
727
+ consumer_group_name: "your_custom_consumer_group_name"
728
+ )
729
+ ```
681
730
 
682
731
 
683
732
  And define `dionysus.rb` initializer:
data/dionysus-rb.gemspec CHANGED
@@ -30,7 +30,7 @@ Gem::Specification.new do |spec|
30
30
  spec.add_development_dependency "bundler"
31
31
  spec.add_development_dependency "byebug"
32
32
  spec.add_development_dependency "crypt_keeper"
33
- spec.add_development_dependency "ddtrace"
33
+ spec.add_development_dependency "datadog"
34
34
  spec.add_development_dependency "dogstatsd-ruby"
35
35
  spec.add_development_dependency "hermes-rb"
36
36
  spec.add_development_dependency "pg"
@@ -52,13 +52,13 @@ Gem::Specification.new do |spec|
52
52
  spec.add_dependency "concurrent-ruby"
53
53
  spec.add_dependency "dry-monitor"
54
54
  spec.add_dependency "file-based-healthcheck"
55
- spec.add_dependency "karafka", "~> 2.0"
55
+ spec.add_dependency "karafka", "~> 2.4"
56
56
  spec.add_dependency "sidekiq"
57
57
  spec.add_dependency "sidekiq-cron"
58
58
  spec.add_dependency "sigurd"
59
59
  spec.add_dependency "waterdrop", "~> 2.0"
60
60
  spec.add_dependency "zeitwerk"
61
61
 
62
- spec.required_ruby_version = ">= 2.7"
62
+ spec.required_ruby_version = ">= 3.3"
63
63
  spec.metadata["rubygems_mfa_required"] = "true"
64
64
  end
@@ -30,7 +30,7 @@ class Dionysus::Consumer::BatchEventsPublisher
30
30
  end
31
31
 
32
32
  def publish_events_batch_via_event_bus(events_batch_data)
33
- config.event_bus.publish("dionysus.consume_batch", events_batch_data)
33
+ config.event_bus.publish("dionysus.consume_batch", events_batch_data) if events_batch_data.any?
34
34
  events_batch_data
35
35
  end
36
36
 
@@ -57,13 +57,13 @@ class Dionysus::Consumer::ParamsBatchProcessor
57
57
  message_filters.find { |f| f.ignore_message?(topic: topic, message: message, transformed_data: transformed_data) }
58
58
  end
59
59
 
60
- def instrument(label, options = {}, &block)
61
- config.instrumenter.instrument(label, options, &block)
60
+ def instrument(label, options = {}, &)
61
+ config.instrumenter.instrument(label, options, &)
62
62
  end
63
63
 
64
- def with_mutex(metadata, config, &block)
64
+ def with_mutex(metadata, config, &)
65
65
  message_key = metadata.key || SecureRandom.uuid
66
66
  config.processing_mutex_provider.send(config.processing_mutex_method_name, "Dionysus-#{message_key}",
67
- &block)
67
+ &)
68
68
  end
69
69
  end
@@ -151,7 +151,7 @@ class Dionysus::Consumer::Persistor
151
151
  end
152
152
  end
153
153
 
154
- def instrument(label, options = {}, &block)
155
- config.instrumenter.instrument(label, options, &block)
154
+ def instrument(label, options = {}, &)
155
+ config.instrumenter.instrument(label, options, &)
156
156
  end
157
157
  end
@@ -8,9 +8,9 @@ class Dionysus::Consumer::Registry
8
8
  @container = {}
9
9
  end
10
10
 
11
- def namespace(namespace, &block)
11
+ def namespace(namespace, &)
12
12
  registration = Registration.new(namespace)
13
- registration.instance_eval(&block)
13
+ registration.instance_eval(&)
14
14
  container[namespace] = registration
15
15
  end
16
16
 
@@ -32,8 +32,8 @@ class Dionysus::Consumer::Registry
32
32
  @deserializer_klass = deserializer_klass
33
33
  end
34
34
 
35
- def topic(name, options = {}, &block)
36
- new_topic = Topic.new(namespace, name, deserializer_klass, options, &block)
35
+ def topic(name, options = {}, &)
36
+ new_topic = Topic.new(namespace, name, deserializer_klass, options, &)
37
37
  consumer = Dionysus::Consumer::KarafkaConsumerGenerator.new.generate(
38
38
  Dionysus::Consumer.configuration, new_topic
39
39
  )
@@ -27,7 +27,7 @@ class Dionysus::Consumer::SynchronizableModel < SimpleDelegator
27
27
 
28
28
  def assign_attributes_from_dionysus(attributes)
29
29
  public_send("#{synced_data_attribute}=", attributes)
30
- reverse_mapping = config.attributes_mapping_for_model(model.model_name).to_a.map(&:reverse).to_h
30
+ reverse_mapping = config.attributes_mapping_for_model(model.model_name).to_a.to_h(&:reverse)
31
31
 
32
32
  assignable_attributes = extract_assignable_attributes(attributes)
33
33
  .map { |key, value| apply_mapping(reverse_mapping, key, value) }
@@ -13,9 +13,9 @@ class Dionysus::Consumer
13
13
  configuration.registry
14
14
  end
15
15
 
16
- def self.declare(&config)
16
+ def self.declare(&)
17
17
  registry = Dionysus::Consumer::Registry.new
18
- registry.instance_eval(&config)
18
+ registry.instance_eval(&)
19
19
 
20
20
  Dionysus.inject_routing!(registry)
21
21
 
@@ -7,7 +7,7 @@ class Dionysus::Producer::BaseResponder
7
7
 
8
8
  def topic(topic_name)
9
9
  self.topics ||= {}
10
- self.topics[topic_name] = topic_name.to_s
10
+ topics[topic_name] = topic_name.to_s
11
11
  end
12
12
 
13
13
  def call(*data)
@@ -9,7 +9,8 @@ class Dionysus::Producer::Outbox::DatadogTracer
9
9
  end
10
10
 
11
11
  def trace(event_name, topic)
12
- tracer.trace(event_name, span_type: "worker", service: self.class.service_name, on_error: error_handler) do |span|
12
+ tracer.trace(event_name, span_type_key => "worker", service: self.class.service_name,
13
+ on_error: error_handler) do |span|
13
14
  span.set_tag("topic", topic)
14
15
 
15
16
  yield
@@ -26,6 +27,16 @@ class Dionysus::Producer::Outbox::DatadogTracer
26
27
  end
27
28
  end
28
29
 
30
+ # datadog 2.0 renamed the `span_type:` keyword of Datadog::Tracing#trace to `type:`.
31
+ # DDTrace is only defined by the pre-2.0 ddtrace gem.
32
+ def span_type_key
33
+ if defined?(DDTrace)
34
+ :span_type
35
+ else
36
+ :type
37
+ end
38
+ end
39
+
29
40
  def error_handler
30
41
  ->(span, error) { span.set_error(error) }
31
42
  end
@@ -78,7 +78,7 @@ module Dionysus::Producer::Outbox::Model
78
78
  end
79
79
 
80
80
  def error
81
- if error_class_arity == 1 || error_class_arity == -1
81
+ if [1, -1].include?(error_class_arity)
82
82
  error_class.constantize.new(error_message)
83
83
  else
84
84
  StandardError.new("#{error_class_constant}: #{error_message}")
@@ -74,8 +74,7 @@ class Dionysus::Producer::Outbox::Publishable < SimpleDelegator
74
74
  .map(&:primary_topic)
75
75
  topics_from_dependencies = Dionysus::Producer
76
76
  .responders_for_dependency_parent(model.class)
77
- .map(&:last)
78
- .map(&:primary_topic)
77
+ .map { |dependency| dependency.last.primary_topic }
79
78
 
80
79
  [top_level_topics, topics_from_dependencies]
81
80
  .flatten
@@ -33,8 +33,10 @@ class Dionysus::Producer::Outbox::Publisher
33
33
  return
34
34
  end
35
35
 
36
- publish_for_top_level_resource(outbox_record, resource, event_name, topic, options)
37
- publish_for_dependency(resource, topic, options)
36
+ published_count = publish_for_top_level_resource(outbox_record, resource, event_name, topic, options) +
37
+ publish_for_dependency(resource, topic, options)
38
+ handle_nothing_published(resource, event_name, topic) if published_count.zero?
39
+ published_count
38
40
  end
39
41
  end
40
42
 
@@ -67,19 +69,20 @@ class Dionysus::Producer::Outbox::Publisher
67
69
  delegate :logger, to: Dionysus
68
70
 
69
71
  def publish_for_top_level_resource(outbox_record, resource, event_name, topic, options)
70
- Dionysus::Producer.responders_for_model_for_topic(resource.class, topic).each do |responder|
72
+ Dionysus::Producer.responders_for_model_for_topic(resource.class, topic).sum do |responder|
71
73
  partition_key = outbox_record.partition_key.presence || Dionysus::Producer::PartitionKey.new(
72
74
  resource
73
75
  ).to_key(responder: responder)
74
76
  key = Dionysus::Producer::Key.new(resource).to_key
75
77
 
76
78
  responder.call(generate_message(event_name, resource), options.merge(partition_key: partition_key, key: key))
79
+ 1
77
80
  end
78
81
  end
79
82
 
80
83
  def publish_for_dependency(resource, topic, options)
81
84
  Dionysus::Producer.responders_for_dependency_parent_for_topic(resource.class,
82
- topic).each do |parent_klass, responder|
85
+ topic).sum do |parent_klass, responder|
83
86
  parent_event_name = Dionysus::Producer::Outbox::EventName.new(
84
87
  parent_klass.model_name.singular
85
88
  ).updated
@@ -89,10 +92,11 @@ class Dionysus::Producer::Outbox::Publisher
89
92
  elsif resource.class.reflect_on_association(parent_klass.model_name.plural)
90
93
  parent_records = resource.public_send(parent_klass.model_name.plural)
91
94
  else
92
- next
95
+ next 0
93
96
  end
94
97
 
95
- example_parent_record = parent_records.first or next
98
+ parent_records = parent_records.to_a.compact
99
+ example_parent_record = parent_records.first or next 0
96
100
  partition_key = Dionysus::Producer::PartitionKey.new(example_parent_record, config: config)
97
101
  .to_key(responder: responder)
98
102
  key = Dionysus::Producer::Key.new(example_parent_record).to_key
@@ -101,9 +105,22 @@ class Dionysus::Producer::Outbox::Publisher
101
105
  responder.call(generate_message(parent_event_name, parent_record),
102
106
  options.merge(partition_key: partition_key, key: key))
103
107
  end
108
+ parent_records.size
104
109
  end
105
110
  end
106
111
 
112
+ # Deliberately does not raise: a legitimately deleted parent is indistinguishable from
113
+ # one that is not visible yet, and raising on the former would retry forever.
114
+ def handle_nothing_published(resource, event_name, topic)
115
+ message = "Published nothing for #{resource.class}, id: #{resource.id}, event: #{event_name}, " \
116
+ "topic: #{topic} - no responder matched and no parent could be resolved. " \
117
+ "Make sure it's processed later (e.g. by directly doing it from console)."
118
+ logger.error(message)
119
+ instrumenter.increment("dionysus.publish.nothing_published",
120
+ tags: ["resource:#{resource.class}", "topic:#{topic}"])
121
+ error_handler.capture_message(message)
122
+ end
123
+
107
124
  def publish_observer(observer_record, responder)
108
125
  publishable = Dionysus::Producer::Outbox::Publishable.new(observer_record)
109
126
  partition_key = Dionysus::Producer::PartitionKey.new(publishable).to_key(responder: responder)
@@ -8,9 +8,9 @@ class Dionysus::Producer::Registry
8
8
  @container = {}
9
9
  end
10
10
 
11
- def namespace(namespace, &block)
11
+ def namespace(namespace, &)
12
12
  registration = Registration.new(namespace)
13
- registration.instance_eval(&block)
13
+ registration.instance_eval(&)
14
14
  container[namespace] = registration
15
15
  end
16
16
 
@@ -32,9 +32,9 @@ class Dionysus::Producer::Registry
32
32
  @serializer_klass = serializer_klass
33
33
  end
34
34
 
35
- def topic(name, options = {}, &block)
35
+ def topic(name, options = {}, &)
36
36
  new_topic = Topic.new(namespace, name, serializer_klass, options)
37
- new_topic.instance_eval(&block)
37
+ new_topic.instance_eval(&)
38
38
  producer = Dionysus::Producer::KarafkaResponderGenerator.new.generate(
39
39
  Dionysus::Producer.configuration, new_topic
40
40
  )
@@ -119,7 +119,7 @@ class Dionysus::Producer::Registry
119
119
  association_name = observer_config.fetch(:association_name)
120
120
  _attributes = observer_config.fetch(:attributes)
121
121
 
122
- if association_name.is_a?(Symbol) && !model.instance_methods.include?(association_name.to_sym)
122
+ if association_name.is_a?(Symbol) && !model.method_defined?(association_name.to_sym)
123
123
  raise ArgumentError.new("association name :#{association_name} does not exist on model #{model}")
124
124
  end
125
125
  end
@@ -156,7 +156,7 @@ class Dionysus::Producer::Registry
156
156
  config_model_name = observer_config.fetch(:model).to_s
157
157
  config_attributes = observer_config.fetch(:attributes).to_a.map(&:to_sym)
158
158
 
159
- config_model_name == resource_model_name && (config_attributes & changeset_attributes).any?
159
+ config_model_name == resource_model_name && config_attributes.intersect?(changeset_attributes)
160
160
  end
161
161
  end
162
162
  end
@@ -13,10 +13,10 @@ class Dionysus::Producer
13
13
  configuration.registry
14
14
  end
15
15
 
16
- def self.declare(&config)
16
+ def self.declare(&)
17
17
  registry = Dionysus::Producer::Registry.new
18
18
 
19
- registry.instance_eval(&config)
19
+ registry.instance_eval(&)
20
20
  configure do |configuration|
21
21
  configuration.registry = registry
22
22
  end
@@ -4,4 +4,6 @@ class Dionysus::Utils::NullInstrumenter
4
4
  def self.instrument(_name, _payload = {})
5
5
  yield
6
6
  end
7
+
8
+ def self.increment(_name, _options = {}); end
7
9
  end
@@ -3,5 +3,5 @@
3
3
  module Dionysus
4
4
  module Version
5
5
  end
6
- VERSION = "0.5.0"
6
+ VERSION = "1.0.0"
7
7
  end
data/lib/dionysus.rb CHANGED
@@ -20,7 +20,7 @@ module Dionysus
20
20
  end
21
21
  end
22
22
 
23
- def self.initialize_application!(environment:, seed_brokers:, client_id:, logger:, draw_routing: true, consumer_group_prefix: CONSUMER_GROUP_PREFIX)
23
+ def self.initialize_application!(environment:, seed_brokers:, client_id:, logger:, draw_routing: true, consumer_group_prefix: CONSUMER_GROUP_PREFIX, consumer_group_name: nil)
24
24
  ENV["KARAFKA_ENV"] = environment
25
25
 
26
26
  karafka_app = Class.new(Karafka::App) do
@@ -37,7 +37,10 @@ module Dionysus
37
37
 
38
38
  Object.const_set(:KarafkaApp, karafka_app)
39
39
  self.karafka_application = karafka_app
40
- evaluate_routing(consumer_group_prefix: consumer_group_prefix) if consumer_registry.present? && draw_routing
40
+ return unless consumer_registry.present? && draw_routing
41
+
42
+ consumer_group_name ||= "#{consumer_group_prefix}_#{karafka_application.config.client_id}"
43
+ evaluate_routing(consumer_group_name: consumer_group_name)
41
44
  end
42
45
 
43
46
  def self.karafka_application=(karafka_app)
@@ -110,12 +113,11 @@ module Dionysus
110
113
  @monitor ||= Dionysus::Monitor.new
111
114
  end
112
115
 
113
- def self.evaluate_routing(consumer_group_prefix:)
114
- consumer_group_name = "#{consumer_group_prefix}_#{karafka_application.config.client_id}"
116
+ def self.evaluate_routing(consumer_group_name:)
115
117
  karafka_application.instance_exec(consumer_registry) do |registry|
116
118
  consumer_groups.draw do
117
119
  consumer_group consumer_group_name do
118
- registry.registrations.each do |_, registration|
120
+ registry.registrations.each_value do |registration|
119
121
  registration.topics.each do |topic|
120
122
  topic topic.to_s do
121
123
  consumer topic.consumer
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dionysus-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karol Galanciak
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-03-15 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activerecord
@@ -67,7 +66,7 @@ dependencies:
67
66
  - !ruby/object:Gem::Version
68
67
  version: '0'
69
68
  - !ruby/object:Gem::Dependency
70
- name: ddtrace
69
+ name: datadog
71
70
  requirement: !ruby/object:Gem::Requirement
72
71
  requirements:
73
72
  - - ">="
@@ -366,14 +365,14 @@ dependencies:
366
365
  requirements:
367
366
  - - "~>"
368
367
  - !ruby/object:Gem::Version
369
- version: '2.0'
368
+ version: '2.4'
370
369
  type: :runtime
371
370
  prerelease: false
372
371
  version_requirements: !ruby/object:Gem::Requirement
373
372
  requirements:
374
373
  - - "~>"
375
374
  - !ruby/object:Gem::Version
376
- version: '2.0'
375
+ version: '2.4'
377
376
  - !ruby/object:Gem::Dependency
378
377
  name: sidekiq
379
378
  requirement: !ruby/object:Gem::Requirement
@@ -461,6 +460,7 @@ files:
461
460
  - ".rspec"
462
461
  - ".rubocop.yml"
463
462
  - ".rubocop_todo.yml"
463
+ - ".ruby-version"
464
464
  - CHANGELOG.md
465
465
  - Gemfile
466
466
  - Gemfile.lock
@@ -563,7 +563,6 @@ metadata:
563
563
  source_code_uri: https://github.com/BookingSync/dionysus-rb
564
564
  changelog_uri: https://github.com/BookingSync/dionysus-rb/blob/master/CHANGELOG.md
565
565
  rubygems_mfa_required: 'true'
566
- post_install_message:
567
566
  rdoc_options: []
568
567
  require_paths:
569
568
  - lib
@@ -571,15 +570,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
571
570
  requirements:
572
571
  - - ">="
573
572
  - !ruby/object:Gem::Version
574
- version: '2.7'
573
+ version: '3.3'
575
574
  required_rubygems_version: !ruby/object:Gem::Requirement
576
575
  requirements:
577
576
  - - ">="
578
577
  - !ruby/object:Gem::Version
579
578
  version: '0'
580
579
  requirements: []
581
- rubygems_version: 3.4.10
582
- signing_key:
580
+ rubygems_version: 4.0.18
583
581
  specification_version: 4
584
582
  summary: A framework on top of Karafka for Change Data Capture on the model level.
585
583
  test_files: []