sbmt-outbox 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +440 -0
- data/Rakefile +3 -0
- data/app/interactors/sbmt/outbox/base_create_item.rb +55 -0
- data/app/interactors/sbmt/outbox/create_inbox_item.rb +10 -0
- data/app/interactors/sbmt/outbox/create_outbox_item.rb +10 -0
- data/app/interactors/sbmt/outbox/dry_interactor.rb +16 -0
- data/app/interactors/sbmt/outbox/partition_strategies/hash_partitioning.rb +20 -0
- data/app/interactors/sbmt/outbox/partition_strategies/number_partitioning.rb +26 -0
- data/app/interactors/sbmt/outbox/process_item.rb +269 -0
- data/app/interactors/sbmt/outbox/retry_strategies/compacted_log.rb +41 -0
- data/app/interactors/sbmt/outbox/retry_strategies/exponential_backoff.rb +34 -0
- data/app/jobs/sbmt/outbox/base_delete_stale_items_job.rb +78 -0
- data/app/jobs/sbmt/outbox/delete_stale_inbox_items_job.rb +15 -0
- data/app/jobs/sbmt/outbox/delete_stale_outbox_items_job.rb +15 -0
- data/app/models/sbmt/outbox/base_item.rb +165 -0
- data/app/models/sbmt/outbox/base_item_config.rb +106 -0
- data/app/models/sbmt/outbox/inbox_item.rb +38 -0
- data/app/models/sbmt/outbox/inbox_item_config.rb +13 -0
- data/app/models/sbmt/outbox/outbox_item.rb +52 -0
- data/app/models/sbmt/outbox/outbox_item_config.rb +13 -0
- data/config/initializers/schked.rb +9 -0
- data/config/initializers/yabeda.rb +71 -0
- data/config/schedule.rb +9 -0
- data/exe/outbox +16 -0
- data/lib/generators/helpers/config.rb +46 -0
- data/lib/generators/helpers/initializer.rb +41 -0
- data/lib/generators/helpers/items.rb +17 -0
- data/lib/generators/helpers/migration.rb +73 -0
- data/lib/generators/helpers/paas.rb +17 -0
- data/lib/generators/helpers/values.rb +49 -0
- data/lib/generators/helpers.rb +8 -0
- data/lib/generators/outbox/install/USAGE +10 -0
- data/lib/generators/outbox/install/install_generator.rb +33 -0
- data/lib/generators/outbox/install/templates/Outboxfile +3 -0
- data/lib/generators/outbox/install/templates/outbox.rb +32 -0
- data/lib/generators/outbox/install/templates/outbox.yml +51 -0
- data/lib/generators/outbox/item/USAGE +12 -0
- data/lib/generators/outbox/item/item_generator.rb +94 -0
- data/lib/generators/outbox/item/templates/inbox_item.rb.tt +7 -0
- data/lib/generators/outbox/item/templates/outbox_item.rb.tt +16 -0
- data/lib/generators/outbox/transport/USAGE +19 -0
- data/lib/generators/outbox/transport/templates/inbox_transport.yml.erb +9 -0
- data/lib/generators/outbox/transport/templates/outbox_transport.yml.erb +10 -0
- data/lib/generators/outbox/transport/transport_generator.rb +60 -0
- data/lib/generators/outbox.rb +23 -0
- data/lib/sbmt/outbox/ascii_art.rb +62 -0
- data/lib/sbmt/outbox/cli.rb +100 -0
- data/lib/sbmt/outbox/database_switcher.rb +15 -0
- data/lib/sbmt/outbox/engine.rb +45 -0
- data/lib/sbmt/outbox/error_tracker.rb +26 -0
- data/lib/sbmt/outbox/errors.rb +14 -0
- data/lib/sbmt/outbox/instrumentation/open_telemetry_loader.rb +34 -0
- data/lib/sbmt/outbox/logger.rb +35 -0
- data/lib/sbmt/outbox/middleware/builder.rb +23 -0
- data/lib/sbmt/outbox/middleware/open_telemetry/tracing_create_item_middleware.rb +42 -0
- data/lib/sbmt/outbox/middleware/open_telemetry/tracing_item_process_middleware.rb +49 -0
- data/lib/sbmt/outbox/middleware/runner.rb +29 -0
- data/lib/sbmt/outbox/middleware/sentry/tracing_batch_process_middleware.rb +48 -0
- data/lib/sbmt/outbox/middleware/sentry/tracing_item_process_middleware.rb +65 -0
- data/lib/sbmt/outbox/middleware/sentry/transaction.rb +28 -0
- data/lib/sbmt/outbox/probes/probe.rb +38 -0
- data/lib/sbmt/outbox/redis_client_factory.rb +36 -0
- data/lib/sbmt/outbox/tasks/delete_failed_items.rake +17 -0
- data/lib/sbmt/outbox/tasks/retry_failed_items.rake +20 -0
- data/lib/sbmt/outbox/thread_pool.rb +108 -0
- data/lib/sbmt/outbox/throttler.rb +52 -0
- data/lib/sbmt/outbox/version.rb +7 -0
- data/lib/sbmt/outbox/worker.rb +233 -0
- data/lib/sbmt/outbox.rb +136 -0
- data/lib/sbmt-outbox.rb +3 -0
- 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,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,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
|