sbmt-outbox 6.2.0 → 6.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a8ef5d8d2289f5e84b0218df4d0ae6ebf4c3887891f4a694c53f5b3fb2a617c
4
- data.tar.gz: 5df6474dd32187affba9667f86e3638dfb51349519626e247e0928d9b44bdb4b
3
+ metadata.gz: d936531e4d8411170774717a4a325846ee05ad292027285a8a6dd1a337c7bb9f
4
+ data.tar.gz: 46a7e007d5f3be785b7616e0eb683354cbdb6654663f744914e3309441aa1a52
5
5
  SHA512:
6
- metadata.gz: 4635896765ebef22224ea23123cbcd6d47facd6c88793f5ecd419cab54fa7afc281527cd43a1fe7466a098c53008d6d692903a85e1d23cd53080713f770ae010
7
- data.tar.gz: 0b632a652cf286f792fe57dfa373409804782ccc5a8bfd6f7859edde9a6067de8ebdd9f5b653e84e61840e1c252607c2fd943746bd7ef409a415707a8c5a0e98
6
+ metadata.gz: 5ff4f4b9b1d3e5e9260b227b4781a66233908c36023cae98d4ab8344baa612b9f0195a02d930054fa3ecf51f256518c7d05a36083887eccc67cb39cb25592c67
7
+ data.tar.gz: 52d88f0391854c934059f21daff079f242e736536efd4ac0bfad2edb7b595f4027c0b3bdf44dd469a6058e26d5a7e4f9c1d811793af0f8a2ebfaf370d0da1f25
data/README.md CHANGED
@@ -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
- ## CLI Arguments (v1: DEPRECATED)
487
+ ## Web UI
473
488
 
474
- | Key | Description |
475
- |-----------------------|---------------------------------------------------------------------------|
476
- | `--boxes or -b` | Outbox/Inbox processors to start` |
477
- | `--concurrency or -c` | Number of threads. Default 10. |
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_name)
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_name)
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
@@ -31,7 +31,7 @@ module Sbmt
31
31
  self.item_class = item_class_name.constantize
32
32
 
33
33
  client = if Gem::Version.new(Redlock::VERSION) >= Gem::Version.new("2.0.0")
34
- RedisClientFactory.build(config.redis)
34
+ Sbmt::Outbox.redis
35
35
  else
36
36
  Redis.new(config.redis)
37
37
  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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Outbox
5
+ module Api
6
+ class InboxClass < Api::BoxClass
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sbmt
4
+ module Outbox
5
+ module Api
6
+ class OutboxClass < Api::BoxClass
7
+ end
8
+ end
9
+ end
10
+ end
@@ -6,6 +6,8 @@ module Sbmt
6
6
  DEFAULT_BUCKET_SIZE = 16
7
7
  DEFAULT_PARTITION_STRATEGY = :number
8
8
 
9
+ delegate :yaml_config, :memory_store, to: "Sbmt::Outbox"
10
+
9
11
  def initialize(box_name)
10
12
  self.box_name = box_name
11
13
 
@@ -15,11 +17,11 @@ module Sbmt
15
17
  def owner
16
18
  return @owner if defined? @owner
17
19
 
18
- @owner = options[:owner].presence || Outbox.yaml_config[:owner].presence
20
+ @owner = options[:owner].presence || yaml_config[:owner].presence
19
21
  end
20
22
 
21
23
  def bucket_size
22
- @bucket_size ||= (options[:bucket_size] || Outbox.yaml_config.fetch(:bucket_size, DEFAULT_BUCKET_SIZE)).to_i
24
+ @bucket_size ||= (options[:bucket_size] || yaml_config.fetch(:bucket_size, DEFAULT_BUCKET_SIZE)).to_i
23
25
  end
24
26
 
25
27
  def partition_size
@@ -120,6 +122,23 @@ module Sbmt
120
122
  def validate!
121
123
  raise ConfigError, "Bucket size should be greater or equal to partition size" if partition_size > bucket_size
122
124
  end
125
+
126
+ def polling_auto_disabled?
127
+ return @polling_auto_disabled if defined?(@polling_auto_disabled)
128
+ @polling_auto_disabled = yaml_config.fetch(:polling_auto_disabled, false)
129
+ end
130
+
131
+ def polling_enabled_for?(api_model)
132
+ record = memory_store.fetch("sbmt/outbox/outbox_item_config/#{box_name}", expires_in: 10) do
133
+ api_model.find(box_name)
134
+ end
135
+
136
+ if record.nil?
137
+ !polling_auto_disabled?
138
+ else
139
+ record.polling_enabled?
140
+ end
141
+ end
123
142
  end
124
143
  end
125
144
  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
- Outbox.yaml_config.dig(:inbox_items, box_name)
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
- Outbox.yaml_config.dig(:outbox_items, box_name) || Outbox.yaml_config.dig(:items, box_name)
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
- # setup custom ErrorTracker
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
@@ -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.7/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
- RedisQueueSize.new(redis: redis, max_size: poller_config.max_queue_size, delay: poller_config.queue_delay)
41
- elsif tactic == "noop"
42
- # no-op, for testing purposes
43
- Noop.new
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Sbmt
4
4
  module Outbox
5
- VERSION = "6.2.0"
5
+ VERSION = "6.3.0"
6
6
  end
7
7
  end
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.2.0
4
+ version: 6.3.0
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-04-18 00:00:00.000000000 Z
11
+ date: 2024-04-23 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