sbmt-outbox 6.2.0 → 6.3.1
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/README.md +129 -78
- data/app/controllers/sbmt/outbox/api/base_controller.rb +24 -0
- data/app/controllers/sbmt/outbox/api/inbox_classes_controller.rb +41 -0
- data/app/controllers/sbmt/outbox/api/outbox_classes_controller.rb +41 -0
- data/app/controllers/sbmt/outbox/root_controller.rb +12 -0
- data/app/jobs/sbmt/outbox/base_delete_stale_items_job.rb +1 -1
- data/app/models/sbmt/outbox/api/application_record.rb +80 -0
- data/app/models/sbmt/outbox/api/box_class.rb +12 -0
- data/app/models/sbmt/outbox/api/inbox_class.rb +10 -0
- data/app/models/sbmt/outbox/api/outbox_class.rb +10 -0
- data/app/models/sbmt/outbox/base_item.rb +5 -1
- data/app/models/sbmt/outbox/base_item_config.rb +24 -4
- data/app/models/sbmt/outbox/inbox_item_config.rb +5 -1
- data/app/models/sbmt/outbox/outbox_item_config.rb +5 -1
- data/app/views/sbmt/outbox/root/index.erb +140 -0
- data/config/routes.rb +10 -0
- data/lib/generators/outbox/install/templates/outbox.rb +1 -28
- data/lib/sbmt/outbox/engine.rb +8 -0
- data/lib/sbmt/outbox/v2/poll_throttler/paused_box.rb +28 -0
- data/lib/sbmt/outbox/v2/poll_throttler.rb +10 -4
- data/lib/sbmt/outbox/version.rb +1 -1
- data/lib/sbmt/outbox.rb +19 -0
- metadata +13 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 952db6c0164054fe8a19297680900749dacf124e2987d2a49a02304f4a02f0db
|
4
|
+
data.tar.gz: b765f1c66e936431070f61e946ac9a50b7b2b2966af01f124923c8cb5f70cdab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c6e1a536dc8ece25ceb807db39e1f4f52fa70f79d6eadfd834707da8d7f4593575cc0563e638133f973fa4205c8e9fe14f1e28ffbbe44bf1f5f35ec1748fb9a
|
7
|
+
data.tar.gz: f5b8196a367b1c38ed65fdaa95cecdd7dd245524c17e317088f73387ea114262d81ed1062b4f1486cf084f55e0a31b64b08999a0d42b30117e41c841bbb15012
|
data/README.md
CHANGED
@@ -3,12 +3,12 @@
|
|
3
3
|
|
4
4
|
# Sbmt-Outbox
|
5
5
|
|
6
|
+
<img src="https://raw.githubusercontent.com/SberMarket-Tech/sbmt-outbox/master/.github/outbox-logo.png" alt="sbmt-outbox logo" height="220" align="right" />
|
7
|
+
|
6
8
|
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
9
|
|
8
10
|
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
11
|
|
10
|
-
Read more about the Outbox pattern at https://microservices.io/patterns/data/transactional-outbox.html
|
11
|
-
|
12
12
|
## Installation
|
13
13
|
|
14
14
|
Add this line to your application's Gemfile:
|
@@ -97,6 +97,91 @@ Example of a Grafana dashboard that you can import [from a file](./examples/graf
|
|
97
97
|
|
98
98
|
## Manual configuration
|
99
99
|
|
100
|
+
### `Outboxfile`
|
101
|
+
|
102
|
+
First of all you shoudl create an `Outboxfile` at the root of your application with the following code:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
# frozen_string_literal: true
|
106
|
+
|
107
|
+
require_relative "config/environment"
|
108
|
+
|
109
|
+
# Comment out this line if you don't want to use a metrics exporter
|
110
|
+
Yabeda::Prometheus::Exporter.start_metrics_server!
|
111
|
+
```
|
112
|
+
|
113
|
+
### `config/initializers/outbox.rb`
|
114
|
+
|
115
|
+
The `config/initializers/outbox.rb` file contains the overall general configuration.
|
116
|
+
|
117
|
+
```ruby
|
118
|
+
# config/initializers/outbox.rb
|
119
|
+
|
120
|
+
Rails.application.config.outbox.tap do |config|
|
121
|
+
config.redis = {url: ENV.fetch("REDIS_URL")} # Redis is used as a coordinator service
|
122
|
+
config.paths << Rails.root.join("config/outbox.yml").to_s # optional; configuration file paths, deep merged at the application start, useful with Rails engines
|
123
|
+
|
124
|
+
# optional (worker v2: default)
|
125
|
+
c.poller = ActiveSupport::OrderedOptions.new.tap do |pc|
|
126
|
+
# max parallel threads (per box-item, globally)
|
127
|
+
pc.concurrency = 6
|
128
|
+
# max threads count (per worker process)
|
129
|
+
pc.threads_count = 1
|
130
|
+
# maximum processing time of the batch, after which the batch will be considered hung and processing will be aborted
|
131
|
+
pc.general_timeout = 60
|
132
|
+
# poll buffer consists of regular items (errors_count = 0, i.e. without any processing errors) and retryable items (errors_count > 0)
|
133
|
+
# max poll buffer size = regular_items_batch_size + retryable_items_batch_size
|
134
|
+
pc.regular_items_batch_size = 200
|
135
|
+
pc.retryable_items_batch_size = 100
|
136
|
+
|
137
|
+
# poll tactic: default is optimal for most cases: rate limit + redis job-queue size threshold
|
138
|
+
# poll tactic: aggressive is for high-intencity data: without rate limits + redis job-queue size threshold
|
139
|
+
# poll tactic: low-priority is for low-intencity data: rate limits + redis job-queue size threshold + + redis job-queue lag threshold
|
140
|
+
pc.tactic = "default"
|
141
|
+
# number of batches that one thread will process per rate interval
|
142
|
+
pc.rate_limit = 60
|
143
|
+
# rate interval in seconds
|
144
|
+
pc.rate_interval = 60
|
145
|
+
# mix / max redis job queue thresholds per box-item for default / aggressive / low-priority poll tactics
|
146
|
+
pc.min_queue_size = 10
|
147
|
+
pc.max_queue_size = 100
|
148
|
+
# min redis job queue time lag threshold per box-item for low-priority poll tactic (in seconds)
|
149
|
+
pc.min_queue_timelag = 5
|
150
|
+
# throttling delay for default / aggressive / low-priority poll tactics (in seconds)
|
151
|
+
pc.queue_delay = 0.1
|
152
|
+
end
|
153
|
+
|
154
|
+
# optional (worker v2: default)
|
155
|
+
c.processor = ActiveSupport::OrderedOptions.new.tap do |pc|
|
156
|
+
# max threads count (per worker process)
|
157
|
+
pc.threads_count = 4
|
158
|
+
# maximum processing time of the batch, after which the batch will be considered hung and processing will be aborted
|
159
|
+
pc.general_timeout = 120
|
160
|
+
# BRPOP delay (in seconds) for polling redis job queue per box-item
|
161
|
+
pc.brpop_delay = 2
|
162
|
+
end
|
163
|
+
|
164
|
+
# optional (worker v1: DEPRECATED)
|
165
|
+
config.process_items.tap do |x|
|
166
|
+
# maximum processing time of the batch, after which the batch will be considered hung and processing will be aborted
|
167
|
+
x.general_timeout = 180
|
168
|
+
# maximum batch processing time, after which the processing of the batch will be aborted in the current thread,
|
169
|
+
# and the next thread that picks up the batch will start processing from the same place
|
170
|
+
x.cutoff_timeout = 60
|
171
|
+
# batch size
|
172
|
+
x.batch_size = 200
|
173
|
+
end
|
174
|
+
|
175
|
+
# optional (worker v1: DEPRECATED)
|
176
|
+
config.worker.tap do |worker|
|
177
|
+
# number of batches that one thread will process per rate interval
|
178
|
+
worker.rate_limit = 10
|
179
|
+
# rate interval in seconds
|
180
|
+
worker.rate_interval = 60
|
181
|
+
end
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
100
185
|
### Outbox pattern
|
101
186
|
|
102
187
|
You should create a database table in order for the process to view your outgoing messages.
|
@@ -199,76 +284,6 @@ outbox_items:
|
|
199
284
|
topic: "orders_completed_topic"
|
200
285
|
```
|
201
286
|
|
202
|
-
#### outbox.rb
|
203
|
-
|
204
|
-
The `outbox.rb` file contains the overall general configuration.
|
205
|
-
|
206
|
-
```ruby
|
207
|
-
# config/initializers/outbox.rb
|
208
|
-
|
209
|
-
Rails.application.config.outbox.tap do |config|
|
210
|
-
config.redis = {url: ENV.fetch("REDIS_URL")} # Redis is used as a coordinator service
|
211
|
-
config.paths << Rails.root.join("config/outbox.yml").to_s # optional; configuration file paths, deep merged at the application start, useful with Rails engines
|
212
|
-
|
213
|
-
# optional
|
214
|
-
config.process_items.tap do |x|
|
215
|
-
# maximum processing time of the batch, after which the batch will be considered hung and processing will be aborted
|
216
|
-
x[:general_timeout] = 180
|
217
|
-
# maximum batch processing time, after which the processing of the batch will be aborted in the current thread,
|
218
|
-
# and the next thread that picks up the batch will start processing from the same place
|
219
|
-
x[:cutoff_timeout] = 60
|
220
|
-
# batch size
|
221
|
-
x[:batch_size] = 200
|
222
|
-
end
|
223
|
-
|
224
|
-
# optional (worker v1: DEPRECATED)
|
225
|
-
config.worker.tap do |worker|
|
226
|
-
# number of batches that one thread will process per rate interval
|
227
|
-
worker[:rate_limit] = 10
|
228
|
-
# rate interval in seconds
|
229
|
-
worker[:rate_interval] = 60
|
230
|
-
end
|
231
|
-
|
232
|
-
# optional (worker v2: default)
|
233
|
-
c.poller = ActiveSupport::OrderedOptions.new.tap do |pc|
|
234
|
-
# max parallel threads (per box-item, globally)
|
235
|
-
pc.concurrency = 6
|
236
|
-
# max threads count (per worker process)
|
237
|
-
pc.threads_count = 1
|
238
|
-
# maximum processing time of the batch, after which the batch will be considered hung and processing will be aborted
|
239
|
-
pc.general_timeout = 60
|
240
|
-
# poll buffer consists of regular items (errors_count = 0, i.e. without any processing errors) and retryable items (errors_count > 0)
|
241
|
-
# max poll buffer size = regular_items_batch_size + retryable_items_batch_size
|
242
|
-
pc.regular_items_batch_size = 200
|
243
|
-
pc.retryable_items_batch_size = 100
|
244
|
-
|
245
|
-
# poll tactic: default is optimal for most cases: rate limit + redis job-queue size threshold
|
246
|
-
# poll tactic: aggressive is for high-intencity data: without rate limits + redis job-queue size threshold
|
247
|
-
# poll tactic: low-priority is for low-intencity data: rate limits + redis job-queue size threshold + + redis job-queue lag threshold
|
248
|
-
pc.tactic = "default"
|
249
|
-
# number of batches that one thread will process per rate interval
|
250
|
-
pc.rate_limit = 60
|
251
|
-
# rate interval in seconds
|
252
|
-
pc.rate_interval = 60
|
253
|
-
# mix / max redis job queue thresholds per box-item for default / aggressive / low-priority poll tactics
|
254
|
-
pc.min_queue_size = 10
|
255
|
-
pc.max_queue_size = 100
|
256
|
-
# min redis job queue time lag threshold per box-item for low-priority poll tactic (in seconds)
|
257
|
-
pc.min_queue_timelag = 5
|
258
|
-
# throttling delay for default / aggressive / low-priority poll tactics (in seconds)
|
259
|
-
pc.queue_delay = 0.1
|
260
|
-
end
|
261
|
-
c.processor = ActiveSupport::OrderedOptions.new.tap do |pc|
|
262
|
-
# max threads count (per worker process)
|
263
|
-
pc.threads_count = 4
|
264
|
-
# maximum processing time of the batch, after which the batch will be considered hung and processing will be aborted
|
265
|
-
pc.general_timeout = 120
|
266
|
-
# BRPOP delay (in seconds) for polling redis job queue per box-item
|
267
|
-
pc.brpop_delay = 2
|
268
|
-
end
|
269
|
-
end
|
270
|
-
```
|
271
|
-
|
272
287
|
### Inbox pattern
|
273
288
|
|
274
289
|
The database migration will be the same as described in the Outbox pattern.
|
@@ -376,7 +391,7 @@ outbox_items:
|
|
376
391
|
|
377
392
|
The worker process consists of a poller and a processor, each of which has its own thread pool.
|
378
393
|
The poller is responsible for fetching messages ready for processing from the database table.
|
379
|
-
The processor, in turn, is used for their consistent processing (while preserving the order of messages and the partitioning key).
|
394
|
+
The processor, in turn, is used for their consistent processing (while preserving the order of messages and the partitioning key).
|
380
395
|
Each bunch of buckets (i.e. buckets partition) is consistently fetched by poller one at a time. Each bucket is processed one at a time by a processor.
|
381
396
|
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`.
|
382
397
|
The number of bucket partitions, which poller uses is 6 by default. The number of poller threads is 2 by default and is not intended for customization.
|
@@ -469,12 +484,41 @@ end
|
|
469
484
|
|
470
485
|
The gem is optionally integrated with OpenTelemetry. If your main application has `opentelemetry-*` gems, the tracing will be configured automatically.
|
471
486
|
|
472
|
-
##
|
487
|
+
## Web UI
|
473
488
|
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
489
|
+
Outbox comes with a [Ract web application](https://github.com/SberMarket-Tech/sbmt-outbox-ui) that can list existing outbox and inbox models.
|
490
|
+
|
491
|
+
```ruby
|
492
|
+
Rails.application.routes.draw do
|
493
|
+
mount Sbmt::Outbox::Engine => "/outbox-ui"
|
494
|
+
end
|
495
|
+
```
|
496
|
+
|
497
|
+
**The path `/outbox-ui` cannot be changed for now**
|
498
|
+
|
499
|
+
Under the hood it uses a React application provided as [npm package](https://www.npmjs.com/package/sbmt-outbox-ui).
|
500
|
+
|
501
|
+
By default, the npm packages is served from `https://cdn.jsdelivr.net/npm/sbmt-outbox-ui@x.y.z/dist/assets/index.js`. It could be changed by the following config option:
|
502
|
+
```ruby
|
503
|
+
# config/initializers/outbox.rb
|
504
|
+
Rails.application.config.outbox.tap do |config|
|
505
|
+
config.cdn_url = "https://some-cdn-url"
|
506
|
+
end
|
507
|
+
```
|
508
|
+
|
509
|
+
### UI development
|
510
|
+
|
511
|
+
If you want to implement some features for Outbox UI, you can serve javascript locally like the following:
|
512
|
+
1. Start React application by `npm run dev`
|
513
|
+
2. Configure Outbox to serve UI scripts locally:
|
514
|
+
```ruby
|
515
|
+
# config/initializers/outbox.rb
|
516
|
+
Rails.application.config.outbox.tap do |config|
|
517
|
+
config.ui.serve_local = true
|
518
|
+
end
|
519
|
+
```
|
520
|
+
|
521
|
+
We would like to see more features added to the web UI. If you have any suggestions, please feel free to submit a pull request 🤗.
|
478
522
|
|
479
523
|
## CLI Arguments (v2: default)
|
480
524
|
|
@@ -487,6 +531,13 @@ The gem is optionally integrated with OpenTelemetry. If your main application ha
|
|
487
531
|
| `--poll-tactic or -t` | Poll tactic. Default "default". |
|
488
532
|
| `--worker-version or -w` | Worker version. Default 2. |
|
489
533
|
|
534
|
+
## CLI Arguments (v1: DEPRECATED)
|
535
|
+
|
536
|
+
| Key | Description |
|
537
|
+
|-----------------------|---------------------------------------------------------------------------|
|
538
|
+
| `--boxes or -b` | Outbox/Inbox processors to start` |
|
539
|
+
| `--concurrency or -c` | Number of threads. Default 10. |
|
540
|
+
|
490
541
|
## Development & Test
|
491
542
|
|
492
543
|
### Installation
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
module Api
|
6
|
+
class BaseController < Sbmt::Outbox.action_controller_api_base_class
|
7
|
+
private
|
8
|
+
|
9
|
+
def render_ok
|
10
|
+
render json: "OK"
|
11
|
+
end
|
12
|
+
|
13
|
+
def render_one(record)
|
14
|
+
render json: record
|
15
|
+
end
|
16
|
+
|
17
|
+
def render_list(records)
|
18
|
+
response.headers["X-Total-Count"] = records.size
|
19
|
+
render json: records
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
module Api
|
6
|
+
class InboxClassesController < BaseController
|
7
|
+
def index
|
8
|
+
render_list(Sbmt::Outbox.inbox_item_classes.map do |item|
|
9
|
+
Sbmt::Outbox::Api::InboxClass.find_or_initialize(item.box_id)
|
10
|
+
end)
|
11
|
+
end
|
12
|
+
|
13
|
+
def show
|
14
|
+
render_one Sbmt::Outbox::Api::InboxClass.find_or_initialize(params.require(:id))
|
15
|
+
end
|
16
|
+
|
17
|
+
def update
|
18
|
+
record = Sbmt::Outbox::Api::InboxClass.find_or_initialize(params.require(:id))
|
19
|
+
record.assign_attributes(
|
20
|
+
params.require(:inbox_class).permit(:polling_enabled)
|
21
|
+
)
|
22
|
+
record.save
|
23
|
+
|
24
|
+
render_one record
|
25
|
+
end
|
26
|
+
|
27
|
+
def destroy
|
28
|
+
record = Sbmt::Outbox::Api::InboxClass.find(params.require(:id))
|
29
|
+
unless record
|
30
|
+
render_ok
|
31
|
+
return
|
32
|
+
end
|
33
|
+
|
34
|
+
record.destroy
|
35
|
+
|
36
|
+
render_one record
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
module Api
|
6
|
+
class OutboxClassesController < BaseController
|
7
|
+
def index
|
8
|
+
render_list(Sbmt::Outbox.outbox_item_classes.map do |item|
|
9
|
+
Api::OutboxClass.find_or_initialize(item.box_id)
|
10
|
+
end)
|
11
|
+
end
|
12
|
+
|
13
|
+
def show
|
14
|
+
render_one Api::OutboxClass.find_or_initialize(params.require(:id))
|
15
|
+
end
|
16
|
+
|
17
|
+
def update
|
18
|
+
record = Api::OutboxClass.find_or_initialize(params.require(:id))
|
19
|
+
record.assign_attributes(
|
20
|
+
params.require(:outbox_class).permit(:polling_enabled)
|
21
|
+
)
|
22
|
+
record.save
|
23
|
+
|
24
|
+
render_one record
|
25
|
+
end
|
26
|
+
|
27
|
+
def destroy
|
28
|
+
record = Api::OutboxClass.find(params.require(:id))
|
29
|
+
unless record
|
30
|
+
render_ok
|
31
|
+
return
|
32
|
+
end
|
33
|
+
|
34
|
+
record.destroy
|
35
|
+
|
36
|
+
render_one record
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
class RootController < Sbmt::Outbox.action_controller_base_class
|
6
|
+
def index
|
7
|
+
@local_endpoint = Outbox.config.ui.local_endpoint
|
8
|
+
@cdn_url = Outbox.config.ui.cdn_url
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
module Api
|
6
|
+
class ApplicationRecord
|
7
|
+
include ActiveModel::Model
|
8
|
+
include ActiveModel::Attributes
|
9
|
+
|
10
|
+
delegate :redis, to: "Sbmt::Outbox"
|
11
|
+
|
12
|
+
class << self
|
13
|
+
delegate :redis, to: "Sbmt::Outbox"
|
14
|
+
|
15
|
+
def find(id)
|
16
|
+
attributes = redis.call "HGETALL", redis_key(id)
|
17
|
+
return nil if attributes.empty?
|
18
|
+
|
19
|
+
new(attributes)
|
20
|
+
end
|
21
|
+
|
22
|
+
def find_or_initialize(id, params = {})
|
23
|
+
record = find(id)
|
24
|
+
record || new(params.merge(id: id))
|
25
|
+
end
|
26
|
+
|
27
|
+
def delete(id)
|
28
|
+
redis.call "DEL", redis_key(id)
|
29
|
+
end
|
30
|
+
|
31
|
+
def attributes(*attrs)
|
32
|
+
attrs.each do |name|
|
33
|
+
attribute name
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def attribute(name, type = ActiveModel::Type::Value.new, **options)
|
38
|
+
super
|
39
|
+
# Add predicate methods for boolean types
|
40
|
+
alias_method :"#{name}?", name if type == :boolean || type.is_a?(ActiveModel::Type::Boolean)
|
41
|
+
end
|
42
|
+
|
43
|
+
def redis_key(id)
|
44
|
+
"#{name}:#{id}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def initialize(params)
|
49
|
+
super
|
50
|
+
assign_attributes(params)
|
51
|
+
end
|
52
|
+
|
53
|
+
def save
|
54
|
+
redis.call "HMSET", redis_key, attributes.to_a.flatten.map(&:to_s)
|
55
|
+
end
|
56
|
+
|
57
|
+
def destroy
|
58
|
+
self.class.delete(id)
|
59
|
+
end
|
60
|
+
|
61
|
+
def as_json(*)
|
62
|
+
attributes
|
63
|
+
end
|
64
|
+
|
65
|
+
def eql?(other)
|
66
|
+
return false unless other.is_a?(self.class)
|
67
|
+
id == other.id
|
68
|
+
end
|
69
|
+
|
70
|
+
alias_method :==, :eql?
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def redis_key
|
75
|
+
self.class.redis_key(id)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
module Api
|
6
|
+
class BoxClass < Api::ApplicationRecord
|
7
|
+
attribute :id, :string
|
8
|
+
attribute :polling_enabled, :boolean, default: -> { !Outbox.yaml_config.fetch(:polling_auto_disabled, false) }
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -16,8 +16,12 @@ module Sbmt
|
|
16
16
|
@box_name ||= name.underscore
|
17
17
|
end
|
18
18
|
|
19
|
+
def box_id
|
20
|
+
@box_id ||= name.underscore.tr("/", "-").dasherize
|
21
|
+
end
|
22
|
+
|
19
23
|
def config
|
20
|
-
@config ||= lookup_config.new(box_name)
|
24
|
+
@config ||= lookup_config.new(box_id: box_id, box_name: box_name)
|
21
25
|
end
|
22
26
|
|
23
27
|
def calc_bucket_partitions(count)
|
@@ -6,7 +6,10 @@ module Sbmt
|
|
6
6
|
DEFAULT_BUCKET_SIZE = 16
|
7
7
|
DEFAULT_PARTITION_STRATEGY = :number
|
8
8
|
|
9
|
-
|
9
|
+
delegate :yaml_config, :memory_store, to: "Sbmt::Outbox"
|
10
|
+
|
11
|
+
def initialize(box_id:, box_name:)
|
12
|
+
self.box_id = box_id
|
10
13
|
self.box_name = box_name
|
11
14
|
|
12
15
|
validate!
|
@@ -15,11 +18,11 @@ module Sbmt
|
|
15
18
|
def owner
|
16
19
|
return @owner if defined? @owner
|
17
20
|
|
18
|
-
@owner = options[:owner].presence ||
|
21
|
+
@owner = options[:owner].presence || yaml_config[:owner].presence
|
19
22
|
end
|
20
23
|
|
21
24
|
def bucket_size
|
22
|
-
@bucket_size ||= (options[:bucket_size] ||
|
25
|
+
@bucket_size ||= (options[:bucket_size] || yaml_config.fetch(:bucket_size, DEFAULT_BUCKET_SIZE)).to_i
|
23
26
|
end
|
24
27
|
|
25
28
|
def partition_size
|
@@ -107,7 +110,7 @@ module Sbmt
|
|
107
110
|
|
108
111
|
private
|
109
112
|
|
110
|
-
attr_accessor :box_name
|
113
|
+
attr_accessor :box_id, :box_name
|
111
114
|
|
112
115
|
def options
|
113
116
|
@options ||= lookup_config || {}
|
@@ -120,6 +123,23 @@ module Sbmt
|
|
120
123
|
def validate!
|
121
124
|
raise ConfigError, "Bucket size should be greater or equal to partition size" if partition_size > bucket_size
|
122
125
|
end
|
126
|
+
|
127
|
+
def polling_auto_disabled?
|
128
|
+
return @polling_auto_disabled if defined?(@polling_auto_disabled)
|
129
|
+
@polling_auto_disabled = yaml_config.fetch(:polling_auto_disabled, false)
|
130
|
+
end
|
131
|
+
|
132
|
+
def polling_enabled_for?(api_model)
|
133
|
+
record = memory_store.fetch("sbmt/outbox/outbox_item_config/#{box_id}", expires_in: 10) do
|
134
|
+
api_model.find(box_id)
|
135
|
+
end
|
136
|
+
|
137
|
+
if record.nil?
|
138
|
+
!polling_auto_disabled?
|
139
|
+
else
|
140
|
+
record.polling_enabled?
|
141
|
+
end
|
142
|
+
end
|
123
143
|
end
|
124
144
|
end
|
125
145
|
end
|
@@ -3,10 +3,14 @@
|
|
3
3
|
module Sbmt
|
4
4
|
module Outbox
|
5
5
|
class InboxItemConfig < BaseItemConfig
|
6
|
+
def polling_enabled?
|
7
|
+
polling_enabled_for?(Sbmt::Outbox::Api::InboxClass)
|
8
|
+
end
|
9
|
+
|
6
10
|
private
|
7
11
|
|
8
12
|
def lookup_config
|
9
|
-
|
13
|
+
yaml_config.dig(:inbox_items, box_name)
|
10
14
|
end
|
11
15
|
end
|
12
16
|
end
|
@@ -3,10 +3,14 @@
|
|
3
3
|
module Sbmt
|
4
4
|
module Outbox
|
5
5
|
class OutboxItemConfig < BaseItemConfig
|
6
|
+
def polling_enabled?
|
7
|
+
polling_enabled_for?(Sbmt::Outbox::Api::OutboxClass)
|
8
|
+
end
|
9
|
+
|
6
10
|
private
|
7
11
|
|
8
12
|
def lookup_config
|
9
|
-
|
13
|
+
yaml_config.dig(:outbox_items, box_name)
|
10
14
|
end
|
11
15
|
end
|
12
16
|
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<% if Sbmt::Outbox.config.ui.serve_local %>
|
5
|
+
<script type="module">
|
6
|
+
import RefreshRuntime from "<%= @local_endpoint %>/@react-refresh"
|
7
|
+
RefreshRuntime.injectIntoGlobalHook(window)
|
8
|
+
window.$RefreshReg$ = () => {}
|
9
|
+
window.$RefreshSig$ = () => (type) => type
|
10
|
+
window.__vite_plugin_react_preamble_installed__ = true
|
11
|
+
</script>
|
12
|
+
<script type="module" src="<%= @local_endpoint %>/@vite/client"></script>
|
13
|
+
<% end %>
|
14
|
+
<meta charset="utf-8" />
|
15
|
+
<meta
|
16
|
+
name="viewport"
|
17
|
+
content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"
|
18
|
+
/>
|
19
|
+
<meta name="theme-color" content="#000000" />
|
20
|
+
<link rel="manifest" href="./manifest.json" />
|
21
|
+
<link rel="shortcut icon" href="./favicon.ico" />
|
22
|
+
<title>outbox-ui</title>
|
23
|
+
<style>
|
24
|
+
body {
|
25
|
+
margin: 0;
|
26
|
+
padding: 0;
|
27
|
+
font-family: sans-serif;
|
28
|
+
}
|
29
|
+
|
30
|
+
.loader-container {
|
31
|
+
display: flex;
|
32
|
+
align-items: center;
|
33
|
+
justify-content: center;
|
34
|
+
flex-direction: column;
|
35
|
+
position: absolute;
|
36
|
+
top: 0;
|
37
|
+
bottom: 0;
|
38
|
+
left: 0;
|
39
|
+
right: 0;
|
40
|
+
background-color: #fafafa;
|
41
|
+
}
|
42
|
+
|
43
|
+
/* CSS Spinner from https://projects.lukehaas.me/css-loaders/ */
|
44
|
+
|
45
|
+
.loader,
|
46
|
+
.loader:before,
|
47
|
+
.loader:after {
|
48
|
+
border-radius: 50%;
|
49
|
+
}
|
50
|
+
|
51
|
+
.loader {
|
52
|
+
color: #283593;
|
53
|
+
font-size: 11px;
|
54
|
+
text-indent: -99999em;
|
55
|
+
margin: 55px auto;
|
56
|
+
position: relative;
|
57
|
+
width: 10em;
|
58
|
+
height: 10em;
|
59
|
+
box-shadow: inset 0 0 0 1em;
|
60
|
+
-webkit-transform: translateZ(0);
|
61
|
+
-ms-transform: translateZ(0);
|
62
|
+
transform: translateZ(0);
|
63
|
+
}
|
64
|
+
|
65
|
+
.loader:before,
|
66
|
+
.loader:after {
|
67
|
+
position: absolute;
|
68
|
+
content: '';
|
69
|
+
}
|
70
|
+
|
71
|
+
.loader:before {
|
72
|
+
width: 5.2em;
|
73
|
+
height: 10.2em;
|
74
|
+
background: #fafafa;
|
75
|
+
border-radius: 10.2em 0 0 10.2em;
|
76
|
+
top: -0.1em;
|
77
|
+
left: -0.1em;
|
78
|
+
-webkit-transform-origin: 5.2em 5.1em;
|
79
|
+
transform-origin: 5.2em 5.1em;
|
80
|
+
-webkit-animation: load2 2s infinite ease 1.5s;
|
81
|
+
animation: load2 2s infinite ease 1.5s;
|
82
|
+
}
|
83
|
+
|
84
|
+
.loader:after {
|
85
|
+
width: 5.2em;
|
86
|
+
height: 10.2em;
|
87
|
+
background: #fafafa;
|
88
|
+
border-radius: 0 10.2em 10.2em 0;
|
89
|
+
top: -0.1em;
|
90
|
+
left: 5.1em;
|
91
|
+
-webkit-transform-origin: 0px 5.1em;
|
92
|
+
transform-origin: 0px 5.1em;
|
93
|
+
-webkit-animation: load2 2s infinite ease;
|
94
|
+
animation: load2 2s infinite ease;
|
95
|
+
}
|
96
|
+
|
97
|
+
@-webkit-keyframes load2 {
|
98
|
+
0% {
|
99
|
+
-webkit-transform: rotate(0deg);
|
100
|
+
transform: rotate(0deg);
|
101
|
+
}
|
102
|
+
100% {
|
103
|
+
-webkit-transform: rotate(360deg);
|
104
|
+
transform: rotate(360deg);
|
105
|
+
}
|
106
|
+
}
|
107
|
+
|
108
|
+
@keyframes load2 {
|
109
|
+
0% {
|
110
|
+
-webkit-transform: rotate(0deg);
|
111
|
+
transform: rotate(0deg);
|
112
|
+
}
|
113
|
+
100% {
|
114
|
+
-webkit-transform: rotate(360deg);
|
115
|
+
transform: rotate(360deg);
|
116
|
+
}
|
117
|
+
}
|
118
|
+
</style>
|
119
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" />
|
120
|
+
<link
|
121
|
+
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap"
|
122
|
+
rel="stylesheet"
|
123
|
+
/>
|
124
|
+
</head>
|
125
|
+
|
126
|
+
<body>
|
127
|
+
<noscript> You need to enable JavaScript to run this app. </noscript>
|
128
|
+
<div id="root">
|
129
|
+
<div class="loader-container">
|
130
|
+
<div class="loader">Loading...</div>
|
131
|
+
</div>
|
132
|
+
</div>
|
133
|
+
<% if Sbmt::Outbox.config.ui.serve_local %>
|
134
|
+
<script type="module" crossorigin src="<%= @local_endpoint %>/src/index.tsx"></script>
|
135
|
+
<% else %>
|
136
|
+
<script type="module" crossorigin src="<%= @cdn_url %>"></script>
|
137
|
+
<% end %>
|
138
|
+
</body>
|
139
|
+
|
140
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Sbmt::Outbox::Engine.routes.draw do
|
4
|
+
root to: "root#index"
|
5
|
+
|
6
|
+
namespace :api, defaults: {format: :json} do
|
7
|
+
resources :outbox_classes, only: [:index, :show, :update, :destroy]
|
8
|
+
resources :inbox_classes, only: [:index, :show, :update, :destroy]
|
9
|
+
end
|
10
|
+
end
|
@@ -1,32 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
Rails.application.config.outbox.tap do |config|
|
4
|
-
|
5
|
-
# config.error_tracker = "ErrorTracker"
|
6
|
-
|
7
|
-
# customize redis
|
8
|
-
# config.redis = {url: ENV.fetch("REDIS_URL", "redis://127.0.0.1:6379")}
|
9
|
-
|
10
|
-
# setup custom batch process middlewares
|
11
|
-
# config.batch_process_middlewares << "MyBatchProcessMiddleware"
|
12
|
-
|
13
|
-
# setup custom item process middlewares
|
14
|
-
# config.item_process_middlewares << "MyItemProcessMiddleware"
|
15
|
-
|
16
|
-
# config.process_items.tap do |x|
|
17
|
-
# # maximum processing time of the batch, after which the batch will be considered hung and processing will be aborted
|
18
|
-
# x[:general_timeout] = 180
|
19
|
-
# # maximum patch processing time, after which the processing of the patch will be aborted in the current thread,
|
20
|
-
# # and the next thread that picks up the batch will start processing from the same place
|
21
|
-
# x[:cutoff_timeout] = 60
|
22
|
-
# # batch size
|
23
|
-
# x[:batch_size] = 200
|
24
|
-
# end
|
25
|
-
|
26
|
-
# config.worker.tap do |worker|
|
27
|
-
# # number of batches that one thread will process per rate interval
|
28
|
-
# worker[:rate_limit] = 10
|
29
|
-
# # rate interval in seconds
|
30
|
-
# worker[:rate_interval] = 60
|
31
|
-
# end
|
4
|
+
config.redis = {url: ENV.fetch("REDIS_URL", "redis://127.0.0.1:6379")}
|
32
5
|
end
|
data/lib/sbmt/outbox/engine.rb
CHANGED
@@ -10,12 +10,20 @@ module Sbmt
|
|
10
10
|
config.outbox = ActiveSupport::OrderedOptions.new.tap do |c|
|
11
11
|
c.active_record_base_class = "ApplicationRecord"
|
12
12
|
c.active_job_base_class = "ApplicationJob"
|
13
|
+
c.action_controller_api_base_class = "ActionController::API"
|
14
|
+
# We cannot use ApplicationController because often it could be inherited from ActionController::API
|
15
|
+
c.action_controller_base_class = "ActionController::Base"
|
13
16
|
c.error_tracker = "Sbmt::Outbox::ErrorTracker"
|
14
17
|
c.outbox_item_classes = []
|
15
18
|
c.inbox_item_classes = []
|
16
19
|
c.paths = []
|
17
20
|
c.disposable_transports = false
|
18
21
|
c.redis = {url: ENV.fetch("REDIS_URL", "redis://127.0.0.1:6379")}
|
22
|
+
c.ui = ActiveSupport::OrderedOptions.new.tap do |c|
|
23
|
+
c.serve_local = false
|
24
|
+
c.local_endpoint = "http://localhost:5173"
|
25
|
+
c.cdn_url = "https://cdn.jsdelivr.net/npm/sbmt-outbox-ui@0.0.8/dist/assets/index.js"
|
26
|
+
end
|
19
27
|
c.process_items = ActiveSupport::OrderedOptions.new.tap do |c|
|
20
28
|
c.general_timeout = 120
|
21
29
|
c.cutoff_timeout = 60
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sbmt/outbox/v2/poll_throttler/base"
|
4
|
+
require "sbmt/outbox/v2/thread_pool"
|
5
|
+
|
6
|
+
module Sbmt
|
7
|
+
module Outbox
|
8
|
+
module V2
|
9
|
+
module PollThrottler
|
10
|
+
class PausedBox < Base
|
11
|
+
def initialize(delay: 0.1)
|
12
|
+
super()
|
13
|
+
|
14
|
+
@delay = delay
|
15
|
+
end
|
16
|
+
|
17
|
+
def wait(worker_num, poll_task, task_result)
|
18
|
+
return Success(Sbmt::Outbox::V2::Throttler::NOOP_STATUS) if poll_task.item_class.config.polling_enabled?
|
19
|
+
|
20
|
+
sleep(@delay)
|
21
|
+
|
22
|
+
Success(Sbmt::Outbox::V2::Throttler::SKIP_STATUS)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -7,6 +7,7 @@ require "sbmt/outbox/v2/poll_throttler/fixed_delay"
|
|
7
7
|
require "sbmt/outbox/v2/poll_throttler/noop"
|
8
8
|
require "sbmt/outbox/v2/poll_throttler/redis_queue_size"
|
9
9
|
require "sbmt/outbox/v2/poll_throttler/redis_queue_time_lag"
|
10
|
+
require "sbmt/outbox/v2/poll_throttler/paused_box"
|
10
11
|
|
11
12
|
module Sbmt
|
12
13
|
module Outbox
|
@@ -17,10 +18,14 @@ module Sbmt
|
|
17
18
|
def self.build(tactic, redis, poller_config)
|
18
19
|
raise "WARN: invalid poller poll tactic provided: #{tactic}, available options: #{POLL_TACTICS}" unless POLL_TACTICS.include?(tactic)
|
19
20
|
|
21
|
+
# no-op, for testing purposes
|
22
|
+
return Noop.new if tactic == "noop"
|
23
|
+
|
20
24
|
if tactic == "default"
|
21
25
|
# composite of RateLimited & RedisQueueSize (upper bound only)
|
22
26
|
# optimal polling performance for most cases
|
23
27
|
Composite.new(throttlers: [
|
28
|
+
PausedBox.new,
|
24
29
|
RedisQueueSize.new(redis: redis, max_size: poller_config.max_queue_size, delay: poller_config.queue_delay),
|
25
30
|
RateLimited.new(limit: poller_config.rate_limit, interval: poller_config.rate_interval)
|
26
31
|
])
|
@@ -30,6 +35,7 @@ module Sbmt
|
|
30
35
|
# and also by min redis queue oldest item lag
|
31
36
|
# optimal polling performance for low-intensity data flow
|
32
37
|
Composite.new(throttlers: [
|
38
|
+
PausedBox.new,
|
33
39
|
RedisQueueSize.new(redis: redis, min_size: poller_config.min_queue_size, max_size: poller_config.max_queue_size, delay: poller_config.queue_delay),
|
34
40
|
RedisQueueTimeLag.new(redis: redis, min_lag: poller_config.min_queue_timelag, delay: poller_config.queue_delay),
|
35
41
|
RateLimited.new(limit: poller_config.rate_limit, interval: poller_config.rate_interval)
|
@@ -37,10 +43,10 @@ module Sbmt
|
|
37
43
|
elsif tactic == "aggressive"
|
38
44
|
# throttles only by max job queue size, max polling performance
|
39
45
|
# optimal polling performance for high-intensity data flow
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
46
|
+
Composite.new(throttlers: [
|
47
|
+
PausedBox.new,
|
48
|
+
RedisQueueSize.new(redis: redis, max_size: poller_config.max_queue_size, delay: poller_config.queue_delay)
|
49
|
+
])
|
44
50
|
end
|
45
51
|
end
|
46
52
|
end
|
data/lib/sbmt/outbox/version.rb
CHANGED
data/lib/sbmt/outbox.rb
CHANGED
@@ -12,6 +12,7 @@ require "cutoff"
|
|
12
12
|
require "http_health_check"
|
13
13
|
require "redis-client"
|
14
14
|
require "connection_pool"
|
15
|
+
require "ostruct"
|
15
16
|
|
16
17
|
begin
|
17
18
|
require "sentry-rails"
|
@@ -75,6 +76,14 @@ module Sbmt
|
|
75
76
|
@active_job_base_class ||= config.active_job_base_class.safe_constantize || ::ActiveJob::Base
|
76
77
|
end
|
77
78
|
|
79
|
+
def action_controller_api_base_class
|
80
|
+
@action_controller_api_base_class ||= config.action_controller_api_base_class.safe_constantize || ::ActionController::API
|
81
|
+
end
|
82
|
+
|
83
|
+
def action_controller_base_class
|
84
|
+
@action_controller_base_class ||= config.action_controller_base_class.safe_constantize || ::ActionController::Base
|
85
|
+
end
|
86
|
+
|
78
87
|
def error_tracker
|
79
88
|
@error_tracker ||= config.error_tracker.constantize
|
80
89
|
end
|
@@ -83,6 +92,16 @@ module Sbmt
|
|
83
92
|
@database_switcher ||= config.database_switcher.constantize
|
84
93
|
end
|
85
94
|
|
95
|
+
def redis
|
96
|
+
@redis ||= ConnectionPool::Wrapper.new(size: ENV.fetch("RAILS_MAX_THREADS", 5).to_i) do
|
97
|
+
RedisClientFactory.build(config.redis)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def memory_store
|
102
|
+
@memory_store ||= ActiveSupport::Cache::MemoryStore.new
|
103
|
+
end
|
104
|
+
|
86
105
|
def outbox_item_classes
|
87
106
|
@outbox_item_classes ||= if config.outbox_item_classes.empty?
|
88
107
|
(yaml_config[:outbox_items] || {}).keys.map { |name| name.camelize.constantize }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sbmt-outbox
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 6.
|
4
|
+
version: 6.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sbermarket Ruby-Platform Team
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-05-02 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: connection_pool
|
@@ -513,6 +513,10 @@ extra_rdoc_files: []
|
|
513
513
|
files:
|
514
514
|
- README.md
|
515
515
|
- Rakefile
|
516
|
+
- app/controllers/sbmt/outbox/api/base_controller.rb
|
517
|
+
- app/controllers/sbmt/outbox/api/inbox_classes_controller.rb
|
518
|
+
- app/controllers/sbmt/outbox/api/outbox_classes_controller.rb
|
519
|
+
- app/controllers/sbmt/outbox/root_controller.rb
|
516
520
|
- app/interactors/sbmt/outbox/base_create_item.rb
|
517
521
|
- app/interactors/sbmt/outbox/create_inbox_item.rb
|
518
522
|
- app/interactors/sbmt/outbox/create_outbox_item.rb
|
@@ -528,14 +532,20 @@ files:
|
|
528
532
|
- app/jobs/sbmt/outbox/base_delete_stale_items_job.rb
|
529
533
|
- app/jobs/sbmt/outbox/delete_stale_inbox_items_job.rb
|
530
534
|
- app/jobs/sbmt/outbox/delete_stale_outbox_items_job.rb
|
535
|
+
- app/models/sbmt/outbox/api/application_record.rb
|
536
|
+
- app/models/sbmt/outbox/api/box_class.rb
|
537
|
+
- app/models/sbmt/outbox/api/inbox_class.rb
|
538
|
+
- app/models/sbmt/outbox/api/outbox_class.rb
|
531
539
|
- app/models/sbmt/outbox/base_item.rb
|
532
540
|
- app/models/sbmt/outbox/base_item_config.rb
|
533
541
|
- app/models/sbmt/outbox/inbox_item.rb
|
534
542
|
- app/models/sbmt/outbox/inbox_item_config.rb
|
535
543
|
- app/models/sbmt/outbox/outbox_item.rb
|
536
544
|
- app/models/sbmt/outbox/outbox_item_config.rb
|
545
|
+
- app/views/sbmt/outbox/root/index.erb
|
537
546
|
- config/initializers/schked.rb
|
538
547
|
- config/initializers/yabeda.rb
|
548
|
+
- config/routes.rb
|
539
549
|
- config/schedule.rb
|
540
550
|
- exe/outbox
|
541
551
|
- lib/generators/helpers.rb
|
@@ -589,6 +599,7 @@ files:
|
|
589
599
|
- lib/sbmt/outbox/v2/poll_throttler/composite.rb
|
590
600
|
- lib/sbmt/outbox/v2/poll_throttler/fixed_delay.rb
|
591
601
|
- lib/sbmt/outbox/v2/poll_throttler/noop.rb
|
602
|
+
- lib/sbmt/outbox/v2/poll_throttler/paused_box.rb
|
592
603
|
- lib/sbmt/outbox/v2/poll_throttler/rate_limited.rb
|
593
604
|
- lib/sbmt/outbox/v2/poll_throttler/redis_queue_size.rb
|
594
605
|
- lib/sbmt/outbox/v2/poll_throttler/redis_queue_time_lag.rb
|