sbmt-outbox 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
module Middleware
|
6
|
+
module OpenTelemetry
|
7
|
+
class TracingCreateItemMiddleware
|
8
|
+
def call(item_class, item_attributes)
|
9
|
+
return yield unless defined?(::OpenTelemetry)
|
10
|
+
|
11
|
+
span_attributes = {
|
12
|
+
"messaging.system" => "outbox",
|
13
|
+
"messaging.outbox.box_type" => item_class.box_type.to_s,
|
14
|
+
"messaging.outbox.box_name" => item_class.box_name,
|
15
|
+
"messaging.outbox.owner" => item_class.owner,
|
16
|
+
"messaging.destination" => item_class.name,
|
17
|
+
"messaging.destination_kind" => "database"
|
18
|
+
}
|
19
|
+
|
20
|
+
options = item_attributes[:options] ||= {}
|
21
|
+
headers = options[:headers] || options["headers"]
|
22
|
+
headers ||= options[:headers] ||= {}
|
23
|
+
tracer.in_span(span_name(item_class), attributes: span_attributes.compact, kind: :producer) do
|
24
|
+
::OpenTelemetry.propagation.inject(headers)
|
25
|
+
yield
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def tracer
|
32
|
+
::Sbmt::Outbox::Instrumentation::OpenTelemetryLoader.instance.tracer
|
33
|
+
end
|
34
|
+
|
35
|
+
def span_name(item_class)
|
36
|
+
"#{item_class.box_type}/#{item_class.box_name} create item"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
module Middleware
|
6
|
+
module OpenTelemetry
|
7
|
+
class TracingItemProcessMiddleware
|
8
|
+
def call(item)
|
9
|
+
return yield unless defined?(::OpenTelemetry)
|
10
|
+
|
11
|
+
item_class = item.class
|
12
|
+
item_options = item.options || {}
|
13
|
+
item_options_headers = item_options[:headers] || item_options["headers"]
|
14
|
+
|
15
|
+
return yield unless item_class && item_options_headers
|
16
|
+
|
17
|
+
span_attributes = {
|
18
|
+
"messaging.system" => "outbox",
|
19
|
+
"messaging.outbox.item_id" => item.id.to_s,
|
20
|
+
"messaging.outbox.box_type" => item_class.box_type.to_s,
|
21
|
+
"messaging.outbox.box_name" => item_class.box_name,
|
22
|
+
"messaging.outbox.owner" => item_class.owner,
|
23
|
+
"messaging.destination" => item_class.name,
|
24
|
+
"messaging.destination_kind" => "database",
|
25
|
+
"messaging.operation" => "process"
|
26
|
+
}
|
27
|
+
|
28
|
+
extracted_context = ::OpenTelemetry.propagation.extract(item_options_headers)
|
29
|
+
::OpenTelemetry::Context.with_current(extracted_context) do
|
30
|
+
tracer.in_span(span_name(item_class), attributes: span_attributes.compact, kind: :consumer) do
|
31
|
+
yield
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def tracer
|
39
|
+
::Sbmt::Outbox::Instrumentation::OpenTelemetryLoader.instance.tracer
|
40
|
+
end
|
41
|
+
|
42
|
+
def span_name(item_class)
|
43
|
+
"#{item_class.box_type}/#{item_class.box_name} process item"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
module Middleware
|
6
|
+
class Runner
|
7
|
+
attr_reader :stack
|
8
|
+
|
9
|
+
def initialize(stack)
|
10
|
+
@stack = stack
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(*args)
|
14
|
+
return yield if stack.empty?
|
15
|
+
|
16
|
+
chain = stack.map { |i| i.new }
|
17
|
+
traverse_chain = proc do
|
18
|
+
if chain.empty?
|
19
|
+
yield
|
20
|
+
else
|
21
|
+
chain.shift.call(*args, &traverse_chain)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
traverse_chain.call
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sbmt/outbox/middleware/sentry/transaction"
|
4
|
+
|
5
|
+
module Sbmt
|
6
|
+
module Outbox
|
7
|
+
module Middleware
|
8
|
+
module Sentry
|
9
|
+
class TracingBatchProcessMiddleware
|
10
|
+
include Transaction
|
11
|
+
|
12
|
+
def call(job)
|
13
|
+
return yield unless ::Sentry.initialized?
|
14
|
+
|
15
|
+
scope = ::Sentry.get_current_scope
|
16
|
+
|
17
|
+
# transaction will be nil if sentry tracing is not enabled
|
18
|
+
transaction = start_transaction(scope, job)
|
19
|
+
job.log_tags[:trace_id] = scope&.tags&.[](:trace_id)
|
20
|
+
|
21
|
+
begin
|
22
|
+
yield
|
23
|
+
rescue
|
24
|
+
finish_sentry_transaction(scope, transaction, 500)
|
25
|
+
raise
|
26
|
+
end
|
27
|
+
|
28
|
+
finish_sentry_transaction(scope, transaction, 200)
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def start_transaction(scope, job)
|
34
|
+
start_sentry_transaction(scope, op(job), transaction_name(job), job.log_tags)
|
35
|
+
end
|
36
|
+
|
37
|
+
def op(job)
|
38
|
+
"sbmt.#{job.log_tags[:box_type]&.downcase}.batch_process"
|
39
|
+
end
|
40
|
+
|
41
|
+
def transaction_name(job)
|
42
|
+
"Sbmt.#{job.log_tags[:box_type]&.capitalize}.#{job.log_tags[:box_name]&.capitalize}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sbmt/outbox/middleware/sentry/transaction"
|
4
|
+
|
5
|
+
module Sbmt
|
6
|
+
module Outbox
|
7
|
+
module Middleware
|
8
|
+
module Sentry
|
9
|
+
class TracingItemProcessMiddleware
|
10
|
+
include Transaction
|
11
|
+
|
12
|
+
attr_reader :new_transaction
|
13
|
+
|
14
|
+
def call(item)
|
15
|
+
return yield unless ::Sentry.initialized?
|
16
|
+
|
17
|
+
scope = ::Sentry.get_current_scope
|
18
|
+
|
19
|
+
# transaction will be nil if sentry tracing is not enabled
|
20
|
+
transaction = scope&.get_transaction || start_transaction(scope, item.class)
|
21
|
+
span = transaction&.start_child(op: op(item.class), description: "Starting item processing")
|
22
|
+
span&.set_data(:item_id, item.id)
|
23
|
+
|
24
|
+
begin
|
25
|
+
yield
|
26
|
+
rescue
|
27
|
+
finish_span(span, 500)
|
28
|
+
finish_sentry_transaction(scope, transaction, 500) if new_transaction
|
29
|
+
raise
|
30
|
+
end
|
31
|
+
|
32
|
+
finish_span(span, 200)
|
33
|
+
finish_sentry_transaction(scope, transaction, 200) if new_transaction
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def finish_span(span, status)
|
39
|
+
return unless span
|
40
|
+
|
41
|
+
span.set_data(:status, status)
|
42
|
+
span.finish
|
43
|
+
end
|
44
|
+
|
45
|
+
def start_transaction(scope, item_class)
|
46
|
+
@new_transaction = true
|
47
|
+
start_sentry_transaction(scope, op(item_class), transaction_name(item_class), tags(item_class))
|
48
|
+
end
|
49
|
+
|
50
|
+
def transaction_name(item_class)
|
51
|
+
"Sbmt.#{item_class.box_type.capitalize}.#{item_class.box_name.capitalize}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def tags(item_class)
|
55
|
+
{box_type: item_class.box_type, box_name: item_class.box_name}
|
56
|
+
end
|
57
|
+
|
58
|
+
def op(item_class)
|
59
|
+
"sbmt.#{item_class.box_type.downcase}.item_process"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
module Middleware
|
6
|
+
module Sentry
|
7
|
+
module Transaction
|
8
|
+
def start_sentry_transaction(scope, op, name, tags = {})
|
9
|
+
trace_id = SecureRandom.base58
|
10
|
+
scope&.set_tags(tags.merge(trace_id: trace_id))
|
11
|
+
transaction = ::Sentry.start_transaction(op: op, name: name)
|
12
|
+
scope&.set_span(transaction) if transaction
|
13
|
+
|
14
|
+
transaction
|
15
|
+
end
|
16
|
+
|
17
|
+
def finish_sentry_transaction(scope, transaction, status)
|
18
|
+
return unless transaction
|
19
|
+
|
20
|
+
transaction.set_http_status(status)
|
21
|
+
transaction.finish
|
22
|
+
scope.clear
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
module Probes
|
6
|
+
class Probe
|
7
|
+
DEFAULT_PROBE_PORT = 5555
|
8
|
+
class << self
|
9
|
+
def run_probes
|
10
|
+
::HttpHealthCheck.run_server_async(
|
11
|
+
port: probe_port,
|
12
|
+
rack_app: HttpHealthCheck::RackApp.configure do |c|
|
13
|
+
c.logger Rails.logger
|
14
|
+
c.probe "/readiness/outbox" do |_env|
|
15
|
+
code = Sbmt::Outbox.current_worker.ready? ? 200 : 500
|
16
|
+
[code, {}, ["Outbox version: #{Sbmt::Outbox::VERSION}"]]
|
17
|
+
end
|
18
|
+
|
19
|
+
c.probe "/liveness/outbox" do |_env|
|
20
|
+
code = Sbmt::Outbox.current_worker.alive? ? 200 : 500
|
21
|
+
[code, {}, ["Outbox version: #{Sbmt::Outbox::VERSION}"]]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def probe_port
|
30
|
+
return DEFAULT_PROBE_PORT if Outbox.yaml_config["probes"].nil?
|
31
|
+
|
32
|
+
Sbmt::Outbox.yaml_config.fetch(:probes).fetch(:port)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
module RedisClientFactory
|
6
|
+
def self.build(options)
|
7
|
+
options = options.deep_symbolize_keys
|
8
|
+
|
9
|
+
unless options.key?(:reconnect_attempts)
|
10
|
+
options[:reconnect_attempts] = 3
|
11
|
+
end
|
12
|
+
|
13
|
+
if options.key?(:sentinels)
|
14
|
+
if (url = options.delete(:url))
|
15
|
+
uri = URI.parse(url)
|
16
|
+
if !options.key?(:name) && uri.host
|
17
|
+
options[:name] = uri.host
|
18
|
+
end
|
19
|
+
|
20
|
+
if !options.key?(:password) && uri.password && !uri.password.empty?
|
21
|
+
options[:password] = uri.password
|
22
|
+
end
|
23
|
+
|
24
|
+
if !options.key?(:username) && uri.user && !uri.user.empty?
|
25
|
+
options[:username] = uri.user
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
RedisClient.sentinel(**options).new_client
|
30
|
+
else
|
31
|
+
RedisClient.config(**options).new_client
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :outbox do
|
4
|
+
desc "Delete messages that we were unable to deliver"
|
5
|
+
# rake 'outbox:delete_failed_items[some/box_item]'
|
6
|
+
# rake 'outbox:delete_failed_items[some/box_item,1,2,3,4,5]'
|
7
|
+
task :delete_failed_items, %i[] => :environment do |_, args|
|
8
|
+
item_class_name, *ids = args.extras
|
9
|
+
item_class_name = item_class_name.classify
|
10
|
+
raise "Invalid item name" unless Sbmt::Outbox.item_classes.map(&:to_s).include?(item_class_name)
|
11
|
+
item_class = item_class_name.constantize
|
12
|
+
|
13
|
+
scope = item_class.failed
|
14
|
+
scope = scope.where(id: ids) unless ids.empty?
|
15
|
+
scope.in_batches.delete_all
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
namespace :outbox do
|
4
|
+
desc "Retry messages that we were unable to deliver"
|
5
|
+
# rake 'outbox:retry_failed_items[some/box_item]'
|
6
|
+
# rake 'outbox:retry_failed_items[some/box_item,1,2,3,4,5]'
|
7
|
+
task :retry_failed_items, %i[] => :environment do |_, args|
|
8
|
+
item_class_name, *ids = args.extras
|
9
|
+
item_class_name = item_class_name.classify
|
10
|
+
raise "Invalid item name" unless Sbmt::Outbox.item_classes.map(&:to_s).include?(item_class_name)
|
11
|
+
item_class = item_class_name.constantize
|
12
|
+
|
13
|
+
scope = item_class.failed
|
14
|
+
scope = scope.where(id: ids) unless ids.empty?
|
15
|
+
scope.in_batches.update_all(
|
16
|
+
status: Sbmt::Outbox::BaseItem.statuses[:pending],
|
17
|
+
errors_count: 0
|
18
|
+
)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sbmt/outbox/throttler"
|
4
|
+
|
5
|
+
module Sbmt
|
6
|
+
module Outbox
|
7
|
+
class ThreadPool
|
8
|
+
BREAK = Object.new.freeze
|
9
|
+
SKIPPED = Object.new.freeze
|
10
|
+
PROCESSED = Object.new.freeze
|
11
|
+
|
12
|
+
def initialize(&block)
|
13
|
+
self.task_source = block
|
14
|
+
self.task_mutex = Mutex.new
|
15
|
+
self.stopped = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def next_task
|
19
|
+
task_mutex.synchronize do
|
20
|
+
return if stopped
|
21
|
+
item = task_source.call
|
22
|
+
|
23
|
+
if item == BREAK
|
24
|
+
self.stopped = true
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
item
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def start(concurrency:)
|
33
|
+
self.stopped = false
|
34
|
+
result = run_threads(count: concurrency) do |item|
|
35
|
+
yield worker_number, item
|
36
|
+
end
|
37
|
+
|
38
|
+
raise result if result.is_a?(Exception)
|
39
|
+
nil
|
40
|
+
ensure
|
41
|
+
self.stopped = true
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop
|
45
|
+
self.stopped = true
|
46
|
+
end
|
47
|
+
|
48
|
+
def worker_number
|
49
|
+
Thread.current["thread_pool_worker_number:#{object_id}"]
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
attr_accessor :task_source, :task_mutex, :stopped
|
55
|
+
|
56
|
+
def run_threads(count:)
|
57
|
+
exception = nil
|
58
|
+
|
59
|
+
in_threads(count: count) do |worker_num|
|
60
|
+
self.worker_number = worker_num
|
61
|
+
# We don't want to start all threads at the same time
|
62
|
+
random_sleep = rand * (worker_num + 1)
|
63
|
+
|
64
|
+
throttler = Throttler.new(
|
65
|
+
limit: Outbox.config.worker.rate_limit,
|
66
|
+
interval: Outbox.config.worker.rate_interval + random_sleep
|
67
|
+
)
|
68
|
+
|
69
|
+
sleep(random_sleep)
|
70
|
+
|
71
|
+
last_result = nil
|
72
|
+
until exception
|
73
|
+
throttler.wait if last_result == PROCESSED
|
74
|
+
item = next_task
|
75
|
+
break unless item
|
76
|
+
|
77
|
+
begin
|
78
|
+
last_result = yield item
|
79
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
80
|
+
exception = e
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
exception
|
86
|
+
end
|
87
|
+
|
88
|
+
def in_threads(count:)
|
89
|
+
threads = []
|
90
|
+
|
91
|
+
Thread.handle_interrupt(Exception => :never) do
|
92
|
+
Thread.handle_interrupt(Exception => :immediate) do
|
93
|
+
count.times do |i|
|
94
|
+
threads << Thread.new { yield(i) }
|
95
|
+
end
|
96
|
+
threads.map(&:value)
|
97
|
+
end
|
98
|
+
ensure
|
99
|
+
threads.each(&:kill)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def worker_number=(num)
|
104
|
+
Thread.current["thread_pool_worker_number:#{object_id}"] = num
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sbmt
|
4
|
+
module Outbox
|
5
|
+
# Based on https://github.com/Shopify/limiter/blob/master/lib/limiter/rate_queue.rb
|
6
|
+
# We cannot use that gem because we have to support Ruby 2.5,
|
7
|
+
# but Shopify's limiter requires minimum Ruby 2.6
|
8
|
+
class Throttler
|
9
|
+
def initialize(limit: nil, interval: nil)
|
10
|
+
@limit = limit
|
11
|
+
@interval = limit
|
12
|
+
@map = (0...@limit).map { |i| base_time + (gap * i) }
|
13
|
+
@index = 0
|
14
|
+
@mutex = Mutex.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def wait
|
18
|
+
time = nil
|
19
|
+
|
20
|
+
@mutex.synchronize do
|
21
|
+
time = @map[@index]
|
22
|
+
|
23
|
+
sleep_until(time + @interval)
|
24
|
+
|
25
|
+
@map[@index] = now
|
26
|
+
@index = (@index + 1) % @limit
|
27
|
+
end
|
28
|
+
|
29
|
+
time
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def sleep_until(time)
|
35
|
+
period = time - now
|
36
|
+
sleep(period) if period > 0
|
37
|
+
end
|
38
|
+
|
39
|
+
def base_time
|
40
|
+
now - @interval
|
41
|
+
end
|
42
|
+
|
43
|
+
def gap
|
44
|
+
@interval.to_f / @limit.to_f
|
45
|
+
end
|
46
|
+
|
47
|
+
def now
|
48
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|