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
@@ -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
|