sbmt-outbox 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +440 -0
  3. data/Rakefile +3 -0
  4. data/app/interactors/sbmt/outbox/base_create_item.rb +55 -0
  5. data/app/interactors/sbmt/outbox/create_inbox_item.rb +10 -0
  6. data/app/interactors/sbmt/outbox/create_outbox_item.rb +10 -0
  7. data/app/interactors/sbmt/outbox/dry_interactor.rb +16 -0
  8. data/app/interactors/sbmt/outbox/partition_strategies/hash_partitioning.rb +20 -0
  9. data/app/interactors/sbmt/outbox/partition_strategies/number_partitioning.rb +26 -0
  10. data/app/interactors/sbmt/outbox/process_item.rb +269 -0
  11. data/app/interactors/sbmt/outbox/retry_strategies/compacted_log.rb +41 -0
  12. data/app/interactors/sbmt/outbox/retry_strategies/exponential_backoff.rb +34 -0
  13. data/app/jobs/sbmt/outbox/base_delete_stale_items_job.rb +78 -0
  14. data/app/jobs/sbmt/outbox/delete_stale_inbox_items_job.rb +15 -0
  15. data/app/jobs/sbmt/outbox/delete_stale_outbox_items_job.rb +15 -0
  16. data/app/models/sbmt/outbox/base_item.rb +165 -0
  17. data/app/models/sbmt/outbox/base_item_config.rb +106 -0
  18. data/app/models/sbmt/outbox/inbox_item.rb +38 -0
  19. data/app/models/sbmt/outbox/inbox_item_config.rb +13 -0
  20. data/app/models/sbmt/outbox/outbox_item.rb +52 -0
  21. data/app/models/sbmt/outbox/outbox_item_config.rb +13 -0
  22. data/config/initializers/schked.rb +9 -0
  23. data/config/initializers/yabeda.rb +71 -0
  24. data/config/schedule.rb +9 -0
  25. data/exe/outbox +16 -0
  26. data/lib/generators/helpers/config.rb +46 -0
  27. data/lib/generators/helpers/initializer.rb +41 -0
  28. data/lib/generators/helpers/items.rb +17 -0
  29. data/lib/generators/helpers/migration.rb +73 -0
  30. data/lib/generators/helpers/paas.rb +17 -0
  31. data/lib/generators/helpers/values.rb +49 -0
  32. data/lib/generators/helpers.rb +8 -0
  33. data/lib/generators/outbox/install/USAGE +10 -0
  34. data/lib/generators/outbox/install/install_generator.rb +33 -0
  35. data/lib/generators/outbox/install/templates/Outboxfile +3 -0
  36. data/lib/generators/outbox/install/templates/outbox.rb +32 -0
  37. data/lib/generators/outbox/install/templates/outbox.yml +51 -0
  38. data/lib/generators/outbox/item/USAGE +12 -0
  39. data/lib/generators/outbox/item/item_generator.rb +94 -0
  40. data/lib/generators/outbox/item/templates/inbox_item.rb.tt +7 -0
  41. data/lib/generators/outbox/item/templates/outbox_item.rb.tt +16 -0
  42. data/lib/generators/outbox/transport/USAGE +19 -0
  43. data/lib/generators/outbox/transport/templates/inbox_transport.yml.erb +9 -0
  44. data/lib/generators/outbox/transport/templates/outbox_transport.yml.erb +10 -0
  45. data/lib/generators/outbox/transport/transport_generator.rb +60 -0
  46. data/lib/generators/outbox.rb +23 -0
  47. data/lib/sbmt/outbox/ascii_art.rb +62 -0
  48. data/lib/sbmt/outbox/cli.rb +100 -0
  49. data/lib/sbmt/outbox/database_switcher.rb +15 -0
  50. data/lib/sbmt/outbox/engine.rb +45 -0
  51. data/lib/sbmt/outbox/error_tracker.rb +26 -0
  52. data/lib/sbmt/outbox/errors.rb +14 -0
  53. data/lib/sbmt/outbox/instrumentation/open_telemetry_loader.rb +34 -0
  54. data/lib/sbmt/outbox/logger.rb +35 -0
  55. data/lib/sbmt/outbox/middleware/builder.rb +23 -0
  56. data/lib/sbmt/outbox/middleware/open_telemetry/tracing_create_item_middleware.rb +42 -0
  57. data/lib/sbmt/outbox/middleware/open_telemetry/tracing_item_process_middleware.rb +49 -0
  58. data/lib/sbmt/outbox/middleware/runner.rb +29 -0
  59. data/lib/sbmt/outbox/middleware/sentry/tracing_batch_process_middleware.rb +48 -0
  60. data/lib/sbmt/outbox/middleware/sentry/tracing_item_process_middleware.rb +65 -0
  61. data/lib/sbmt/outbox/middleware/sentry/transaction.rb +28 -0
  62. data/lib/sbmt/outbox/probes/probe.rb +38 -0
  63. data/lib/sbmt/outbox/redis_client_factory.rb +36 -0
  64. data/lib/sbmt/outbox/tasks/delete_failed_items.rake +17 -0
  65. data/lib/sbmt/outbox/tasks/retry_failed_items.rake +20 -0
  66. data/lib/sbmt/outbox/thread_pool.rb +108 -0
  67. data/lib/sbmt/outbox/throttler.rb +52 -0
  68. data/lib/sbmt/outbox/version.rb +7 -0
  69. data/lib/sbmt/outbox/worker.rb +233 -0
  70. data/lib/sbmt/outbox.rb +136 -0
  71. data/lib/sbmt-outbox.rb +3 -0
  72. metadata +594 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ade6bbd7b09d15191e05854d0f9b6ffbb9c3d13270b70c417f4064059bd4c986
4
+ data.tar.gz: cb315c2a3bf2d42c7836358bb848a58ef8aecd2e5c58a23e216ad849c9bbb705
5
+ SHA512:
6
+ metadata.gz: 8584326986eb5be82b5d8a64ae9c6f3acd1a9702d91f2fffa980f5da4a8a8a344c6c184f24ab9fa9ad96f480d2d7c35f9cae615de6e64a5231697e928b5f6a97
7
+ data.tar.gz: ac0fc97998938b1410bd6df209baea1f387129e5b368e785115e690e2cc7899980595787ae0908dc7e8d6ee1a1a16f201c18e299b7156d32c849495f71fa9b1c
data/README.md ADDED
@@ -0,0 +1,440 @@
1
+ [![Gem Version](https://badge.fury.io/rb/sbmt-outbox.svg)](https://badge.fury.io/rb/sbmt-outbox)
2
+ [![Build Status](https://github.com/SberMarket-Tech/sbmt-outbox/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/SberMarket-Tech/sbmt-outbox/actions?query=branch%3Amaster)
3
+
4
+ # Sbmt-Outbox
5
+
6
+ Microservices often publish messages after a transaction has been committed. Writing to the database and publishing a message are two separate transactions, so they must be atomic. A failed publication of a message could lead to a critical failure of the business process.
7
+
8
+ The Outbox pattern provides a reliable solution for message publishing. The idea behind this approach is to have an "outgoing message table" in the service's database. Before the main transaction completes, a new message row is added to this table. As a result, two actions take place as part of a single transaction. An asynchronous process retrieves new rows from the database table and, if they exist, publishes the messages to the broker.
9
+
10
+ Read more about the Outbox pattern at https://microservices.io/patterns/data/transactional-outbox.html
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem "sbmt-outbox"
18
+ ```
19
+
20
+ And then execute:
21
+
22
+ ```shell
23
+ bundle install
24
+ ```
25
+
26
+ ## Auto configuration
27
+
28
+ We recommend going through the configuration and files creation process using the following Rails generators:
29
+
30
+ Each generator can be run by using the `--help` option to learn more about the available arguments.
31
+
32
+ ### Initial configuration
33
+
34
+ If you plug the gem into your application for the first time, you can generate the initial configuration:
35
+
36
+ ```shell
37
+ rails g outbox:install
38
+ ```
39
+
40
+ ### Outbox/inbox items creation
41
+
42
+ An ActiveRecord model can be generated for the outbox/ inbox item like this:
43
+
44
+ ```shell
45
+ rails g outbox:item MaybeNamespaced::SomeOutboxItem --kind outbox
46
+ rails g outbox:item MaybeNamespaced::SomeInboxItem --kind inbox
47
+ ```
48
+
49
+ As the result, a migration and a model will be created and the `outbox.yml` file configured.
50
+
51
+ ### Transport creation
52
+
53
+ A transport is a class that is invoked while processing a specific outbox or inbox item. The transport must return either a boolean value or a dry monad result.
54
+
55
+ ```shell
56
+ rails g outbox:transport MaybeNamespaced::SomeOutboxItem some/transport/name --kind outbox
57
+ rails g outbox:transport MaybeNamespaced::SomeInboxItem some/transport/name --kind inbox
58
+ ```
59
+
60
+ ## Usage
61
+
62
+ To create an Outbox item, you should call the Interactor with the Item Model Class and `event_key` as arguments. The latter will be the Partitioning Key.
63
+
64
+ ```ruby
65
+ transaction do
66
+ some_record.save!
67
+
68
+ result = Sbmt::Outbox::CreateOutboxItem.call(
69
+ MyOutboxItem,
70
+ event_key: some_record.id,
71
+ attributes: {
72
+ payload: some_record.generate_payload,
73
+ options: {
74
+ key: some_record.id, # optional, may be used when producing to a Kafka topic
75
+ headers: {'FOO_BAR' => 'baz'} # optional, you can add custom headers
76
+ }
77
+ }
78
+ )
79
+
80
+ raise result.failure unless result.success?
81
+ end
82
+ ```
83
+
84
+ ## Monitoring
85
+
86
+ We use [Yabeda](https://github.com/yabeda-rb/yabeda) to collect [all kind of metrics](./config/initializers/yabeda.rb).
87
+
88
+ Example of a Grafana dashboard that you can import [from a file](./examples/grafana-dashboard.json):
89
+
90
+ ![Grafana Dashboard](./examples/outbox-grafana-preview.png)
91
+
92
+ [Full picture](./examples/outbox-grafana.png)
93
+
94
+ ## Manual configuration
95
+
96
+ ### Outbox pattern
97
+
98
+ You should create a database table in order for the process to view your outgoing messages.
99
+
100
+ ```ruby
101
+ create_table :my_outbox_items do |t|
102
+ t.uuid :uuid, null: false
103
+ t.string :event_name, null: false # optional, use it when you have several events per one outbox table
104
+ t.string :event_key, null: false
105
+ t.integer :bucket, null: false
106
+ t.integer :status, null: false, default: 0
107
+ t.jsonb :options
108
+ t.binary :payload, null: false # when using mysql the column type should be mediumblob
109
+ t.integer :errors_count, null: false, default: 0
110
+ t.text :error_log
111
+ t.timestamp :processed_at
112
+ t.timestamps
113
+ end
114
+
115
+ add_index :my_outbox_items, :uuid, unique: true
116
+ add_index :my_outbox_items, [:status, :bucket]
117
+ add_index :my_outbox_items, [:event_name, :event_key]
118
+ add_index :my_outbox_items, :created_at
119
+ ```
120
+
121
+ You can combine various types of messages within a single table. To do this, you should include an `event_name` field in the table. However, this approach is only justified if it is assumed that there won't be many events, and those events will follow the same retention and retry policy.
122
+
123
+ ```ruby
124
+ # app/models/my_outbox_item.rb
125
+ class MyOutboxItem < Sbmt::Outbox::OutboxItem
126
+ validates :event_name, presence: true # optional
127
+ end
128
+ ```
129
+
130
+ #### outbox.yml
131
+ The `outbox.yml` configuration file is the main configuration for the gem, where parameters for each outbox/inbox item are located.
132
+
133
+ ```yaml
134
+ # config/outbox.yml
135
+ default: &default
136
+ owner: foo-team # optional, used in Yabeda metrics
137
+ bucket_size: 16 # optional, default 16, see into about the buckets at the #Concurrency section
138
+ probes:
139
+ port: 5555 # default, used for Kubernetes probes
140
+
141
+ outbox_items: # outbox items section
142
+ my_outbox_item: # underscored model class name
143
+ owner: my_outbox_item_team # optional, used in Yabeda metrics
144
+ retention: P1W # retention period, https://en.wikipedia.org/wiki/ISO_8601#Durations
145
+ partition_size: 2 # default 1, partitions count
146
+ max_retries: 3 # default 0, the number of retries before the item will be marked as failed
147
+ transports: # transports section
148
+ produce_message: # underscored transport class name
149
+ topic: "my-topic-name" # default transport arguments
150
+
151
+ development:
152
+ <<: *default
153
+
154
+ test:
155
+ <<: *default
156
+ bucket_size: 2
157
+
158
+ production:
159
+ <<: *default
160
+ bucket_size: 256
161
+ ```
162
+
163
+ ```ruby
164
+ # app/services/import_order.rb
165
+ class ProduceMessage
166
+ def initialize(topic:)
167
+ @topic = topic
168
+ end
169
+
170
+ def call(outbox_item, payload)
171
+ # send message to topic
172
+ true # mark message as processed
173
+ end
174
+ end
175
+ ```
176
+
177
+ **If you use Kafka as a transport, it is recommended that you use the [`sbmt-kafka_producer`](https://github.com/SberMarket-Tech/sbmt-kafka_producer) gem for this purpose.**
178
+
179
+ Transports are defined as follows when `event_name` is used:
180
+
181
+ ```yaml
182
+ outbox_items:
183
+ my_outbox_item:
184
+ transports:
185
+ - class: produce_message
186
+ event_name: "order_created" # event name marker
187
+ topic: "order_created_topic" # some transport default argument
188
+ - class: produce_message
189
+ event_name: "orders_completed"
190
+ topic: "orders_completed_topic"
191
+ ```
192
+
193
+ #### outbox.rb
194
+
195
+ The `outbox.rb` file contains the overall general configuration.
196
+
197
+ ```ruby
198
+ # config/initializers/outbox.rb
199
+
200
+ Rails.application.config.outbox.tap do |config|
201
+ config.redis = {url: ENV.fetch("REDIS_URL")} # Redis is used as a coordinator service
202
+ config.paths << Rails.root.join("config/outbox.yml").to_s # optional; configuration file paths, deep merged at the application start, useful with Rails engines
203
+
204
+ # optional
205
+ config.process_items.tap do |x|
206
+ # maximum processing time of the batch, after which the batch will be considered hung and processing will be aborted
207
+ x[:general_timeout] = 180
208
+ # maximum batch processing time, after which the processing of the batch will be aborted in the current thread,
209
+ # and the next thread that picks up the batch will start processing from the same place
210
+ x[:cutoff_timeout] = 60
211
+ # batch size
212
+ x[:batch_size] = 200
213
+ end
214
+
215
+ # optional
216
+ config.worker.tap do |worker|
217
+ # number of batches that one thread will process per rate interval
218
+ worker[:rate_limit] = 10
219
+ # rate interval in seconds
220
+ worker[:rate_interval] = 60
221
+ end
222
+ end
223
+ ```
224
+
225
+ ### Inbox pattern
226
+
227
+ The database migration will be the same as described in the Outbox pattern.
228
+
229
+ ```ruby
230
+ # app/models/my_inbox_item.rb
231
+ class MyInboxItem < Sbmt::Outbox::InboxItem
232
+ end
233
+ ```
234
+
235
+ ```yaml
236
+ # config/outbox.yml
237
+ # see main configuration at the Outbox pattern
238
+ inbox_items: # inbox items section
239
+ my_inbox_item: # underscored model class name
240
+ owner: my_inbox_item_team # optional, used in Yabeda metrics
241
+ retention: P1W # retention period, https://en.wikipedia.org/wiki/ISO_8601#Durations
242
+ partition_size: 2 # default 1, partitions count
243
+ max_retries: 3 # default 0, the number of retries before the item will be marked as failed
244
+ transports: # transports section
245
+ import_order: # underscored transport class name
246
+ source: "kafka" # default transport arguments
247
+ ```
248
+
249
+ ```ruby
250
+ # app/services/import_order.rb
251
+ class ImportOrder
252
+ def initialize(source:)
253
+ @source = source
254
+ end
255
+
256
+ def call(outbox_item, payload)
257
+ # some work to create order in the database
258
+ true # mark message as processed
259
+ end
260
+ end
261
+ ```
262
+
263
+ **If you use Kafka, it is recommended that you use the [`sbmt-kafka_consumer`](https://github.com/SberMarket-Tech/sbmt-kafka_consumer) gem for this purpose.**
264
+
265
+ ### Retry strategies
266
+
267
+ The gem uses several types of retry strategies to repeat message processing if an error occurs. These strategies can be combined and will be executed one after the other. Each retry strategy takes one of three actions: to process the message, to skip processing the message or to skip processing and mark the message as "skipped" for future processing.
268
+
269
+ #### Exponential backoff
270
+
271
+ This strategy periodically attempts to resend failed messages, with increasing delays in between each attempt.
272
+
273
+ ```yaml
274
+ # config/outbox.yml
275
+ outbox_items:
276
+ my_outbox_item:
277
+ ...
278
+ minimal_retry_interval: 10 # default: 10
279
+ maximal_retry_interval: 600 # default: 600
280
+ multiplier_retry_interval: 2 # default: 2
281
+ retry_strategies:
282
+ - exponential_backoff
283
+ ```
284
+
285
+ #### Compacted log
286
+
287
+ This strategy ensures idempotency. In short, if a message fails and a later message with the same event_key has already been delivered, then you most likely do not want to re-deliver the first one when it is retried.
288
+
289
+ ```yaml
290
+ # config/outbox.yml
291
+ outbox_items:
292
+ my_outbox_item:
293
+ ...
294
+ retry_strategies:
295
+ - exponential_backoff
296
+ - compacted_log
297
+ ```
298
+
299
+ The exponential backoff strategy should be used in conjunction with the compact log strategy, and it should come last to minimize the number of database queries.
300
+
301
+ ### Partition strategies
302
+
303
+ Depending on which type of data is used in the `event_key`, it is necessary to choose the right partitioning strategy.
304
+
305
+ #### Number partitioning
306
+
307
+ This strategy should be used when the `event_key` field contains a number. For example, it could be `52523`, or `some-chars-123`. Any characters that aren't numbers will be removed, and only the numbers will remain. This strategy is used as a default.
308
+
309
+ ```yaml
310
+ # config/outbox.yml
311
+ outbox_items:
312
+ my_outbox_item:
313
+ ...
314
+ partition_strategy: number
315
+ ```
316
+
317
+ #### Hash partitioning
318
+
319
+ This strategy should be used when the `event_key` is a string or uuid.
320
+
321
+ ```yaml
322
+ # config/outbox.yml
323
+ outbox_items:
324
+ my_outbox_item:
325
+ ...
326
+ partition_strategy: hash
327
+ ```
328
+
329
+ ## Concurrency
330
+
331
+ The Outbox executable CLI uses Ruby threads for concurrent processing of messages pulled from the database table. The number of threads is configured using the `--concurrency` option. By default, it's 10 unless a parameter is provided. You can run multiple daemons at once. The number of partitions for each outbox item class is set by the `partition_size` configuration option. Each partition is processed one at a time by a daemon. Each batch of partitions serves several buckets. A bucket is a number in a row in the `bucket` column generated by the partitioning strategy based on the `event_key` column when a message was committed to the database within the range of zero to `bucket_size`. Therefore, each outbox table has many partitions that contain several buckets. Note that you must not have `partition_size` greater than `bucket_size.` This architecture was designed to allow the daemons to scale without stopping the entire system in order to avoid mixing messages chronologically.So, if you need more partitions, just stop the daemons, set `partition_size` correctly, and start them again.
332
+
333
+ **Example:** Suppose you have a Kafka topic with 18 partitions. The `bucket_size` is 256. If we expect a slow payload generation, we can set `partition_size` to 16. Therefore, we should run 4 daemons with 4 threads each in order to maximize utilization of the partitions.
334
+
335
+ ### Middlewares
336
+
337
+ You can wrap item processing within middlewares. There are three types:
338
+ - client middlewares – triggered outside of a daemon; executed alongside an item is created
339
+ - server middlewares – triggered inside a daemon; divided into two types:
340
+ - batch middlewares – executed alongside a batch being fetched from the database
341
+ - item middlewares – execute alongside an item during processing
342
+
343
+ The order of execution depends on the order specified in the outbox configuration:
344
+
345
+ ```ruby
346
+ # config/initializers/outbox.rb
347
+ Rails.application.config.outbox.tap do |config|
348
+ config.item_process_middlewares.push(
349
+ 'MyFirstItemMiddleware', # goes first
350
+ 'MySecondItemMiddleware' # goes second
351
+ )
352
+ end
353
+ ```
354
+
355
+ #### Client middlewares
356
+
357
+ ```ruby
358
+ # config/initializers/outbox.rb
359
+ Rails.application.config.outbox.tap do |config|
360
+ config.create_item_middlewares.push(
361
+ 'MyCreateItemMiddleware'
362
+ )
363
+ end
364
+
365
+ # my_create_item_middleware.rb
366
+ class MyCreateItemMiddleware
367
+ def call(item_class, item_attributes)
368
+ # your code
369
+ yield
370
+ # your code
371
+ end
372
+ end
373
+ ```
374
+
375
+ #### Server middlewares
376
+
377
+ Example of a batch middleware:
378
+
379
+ ```ruby
380
+ # config/initializers/outbox.rb
381
+ Rails.application.config.outbox.tap do |config|
382
+ config.batch_process_middlewares.push(
383
+ 'MyBatchMiddleware'
384
+ )
385
+ end
386
+
387
+ # my_batch_middleware.rb
388
+ class MyBatchMiddleware
389
+ def call(job)
390
+ # your code
391
+ yield
392
+ # your code
393
+ end
394
+ end
395
+ ```
396
+
397
+ Example of an item middleware:
398
+
399
+ ```ruby
400
+ # config/initializers/outbox.rb
401
+ Rails.application.config.outbox.tap do |config|
402
+ config.item_process_middlewares.push(
403
+ 'MyItemMiddleware'
404
+ )
405
+ end
406
+
407
+ # my_create_item_middleware.rb
408
+ class MyItemMiddleware
409
+ def call(item)
410
+ # your code
411
+ yield
412
+ # your code
413
+ end
414
+ end
415
+ ```
416
+
417
+ ## Tracing
418
+
419
+ The gem is optionally integrated with OpenTelemetry. If your main application has `opentelemetry-*` gems, the tracing will be configured automatically.
420
+
421
+ ## CLI Arguments
422
+
423
+ | Key | Description |
424
+ |-----------------------|---------------------------------------------------------------------------|
425
+ | `--boxes or -b` | Outbox/Inbox processors to start` |
426
+ | `--concurrency or -c` | Number of threads. Default 10. |
427
+
428
+ ## Development & Test
429
+
430
+ ### Installation
431
+
432
+ - Install [Dip](https://github.com/bibendi/dip)
433
+ - Run `dip provision`
434
+
435
+ ### Usage
436
+
437
+ - Run `dip setup`
438
+ - Run `dip test`
439
+
440
+ See more commands at [dip.yml](./dip.yml).
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Outbox
5
+ class BaseCreateItem < Outbox::DryInteractor
6
+ param :item_class, reader: :private
7
+ option :attributes, reader: :private
8
+ option :event_key, reader: :private, optional: true, default: -> { attributes[:event_key] }
9
+ option :partition_by, reader: :private, optional: true, default: -> { attributes[:event_key] }
10
+
11
+ delegate :box_type, :box_name, :owner, to: :item_class
12
+ delegate :create_item_middlewares, to: "Sbmt::Outbox"
13
+
14
+ def call
15
+ middlewares = Middleware::Builder.new(create_item_middlewares)
16
+ middlewares.call(item_class, attributes) do
17
+ record = item_class.new(attributes)
18
+
19
+ return Failure(:missing_event_key) unless event_key
20
+ return Failure(:missing_partition_by) unless partition_by
21
+
22
+ res = item_class.config.partition_strategy
23
+ .new(partition_by, item_class.config.bucket_size)
24
+ .call
25
+ record.bucket = res.value! if res.success?
26
+
27
+ if record.save
28
+ track_last_stored_id(record.id, record.partition)
29
+ track_counter(record.partition)
30
+
31
+ Success(record)
32
+ else
33
+ Failure(record.errors)
34
+ end
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def track_last_stored_id(item_id, partition)
41
+ Yabeda
42
+ .outbox
43
+ .last_stored_event_id
44
+ .set({type: box_type, name: box_name, owner: owner, partition: partition}, item_id)
45
+ end
46
+
47
+ def track_counter(partition)
48
+ Yabeda
49
+ .outbox
50
+ .created_counter
51
+ .increment({type: box_type, name: box_name, owner: owner, partition: partition}, by: 1)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Outbox
5
+ # Classes are the same now, but they may differ,
6
+ # hence we should have separate API entrypoints
7
+ class CreateInboxItem < Sbmt::Outbox::BaseCreateItem
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Outbox
5
+ # Classes are the same now, but they may differ,
6
+ # hence we should have separate API entrypoints
7
+ class CreateOutboxItem < Sbmt::Outbox::BaseCreateItem
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Outbox
5
+ class DryInteractor
6
+ extend Dry::Initializer
7
+ include Dry::Monads[:result, :do, :maybe, :list, :try]
8
+
9
+ class << self
10
+ def call(*args, **kwargs)
11
+ new(*args, **kwargs).call
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/sha1"
4
+
5
+ module Sbmt
6
+ module Outbox
7
+ module PartitionStrategies
8
+ class HashPartitioning < Outbox::DryInteractor
9
+ param :key
10
+ param :bucket_size
11
+
12
+ def call
13
+ Success(
14
+ Digest::SHA1.hexdigest(key.to_s).to_i(16) % bucket_size
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Outbox
5
+ module PartitionStrategies
6
+ class NumberPartitioning < Outbox::DryInteractor
7
+ param :key
8
+ param :bucket_size
9
+
10
+ def call
11
+ parsed_key =
12
+ case key
13
+ when Integer
14
+ key
15
+ else
16
+ key.delete("^0-9").to_i
17
+ end
18
+
19
+ Success(
20
+ parsed_key % bucket_size
21
+ )
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end