kubra-sidekiq-throttled 1.5.3
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/LICENSE.txt +23 -0
- data/README.adoc +416 -0
- data/lib/sidekiq/throttled/config.rb +66 -0
- data/lib/sidekiq/throttled/cooldown.rb +55 -0
- data/lib/sidekiq/throttled/errors.rb +8 -0
- data/lib/sidekiq/throttled/expirable_set.rb +70 -0
- data/lib/sidekiq/throttled/job.rb +143 -0
- data/lib/sidekiq/throttled/message.rb +32 -0
- data/lib/sidekiq/throttled/middlewares/server.rb +28 -0
- data/lib/sidekiq/throttled/patches/basic_fetch.rb +34 -0
- data/lib/sidekiq/throttled/patches/super_fetch.rb +39 -0
- data/lib/sidekiq/throttled/patches/throttled_retriever.rb +26 -0
- data/lib/sidekiq/throttled/registry.rb +120 -0
- data/lib/sidekiq/throttled/strategy/base.rb +25 -0
- data/lib/sidekiq/throttled/strategy/concurrency.lua +61 -0
- data/lib/sidekiq/throttled/strategy/concurrency.rb +127 -0
- data/lib/sidekiq/throttled/strategy/threshold.lua +14 -0
- data/lib/sidekiq/throttled/strategy/threshold.rb +104 -0
- data/lib/sidekiq/throttled/strategy.rb +213 -0
- data/lib/sidekiq/throttled/strategy_collection.rb +73 -0
- data/lib/sidekiq/throttled/version.rb +8 -0
- data/lib/sidekiq/throttled/web/stats.rb +75 -0
- data/lib/sidekiq/throttled/web/throttled.html.erb +35 -0
- data/lib/sidekiq/throttled/web.rb +43 -0
- data/lib/sidekiq/throttled/worker.rb +13 -0
- data/lib/sidekiq/throttled.rb +116 -0
- metadata +119 -0
@@ -0,0 +1,143 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# internal
|
4
|
+
require_relative "./registry"
|
5
|
+
|
6
|
+
module Sidekiq
|
7
|
+
module Throttled
|
8
|
+
# Adds helpers to your worker classes
|
9
|
+
#
|
10
|
+
# @example Usage
|
11
|
+
#
|
12
|
+
# class MyJob
|
13
|
+
# include Sidekiq::Job
|
14
|
+
# include Sidekiq::Throttled::Job
|
15
|
+
#
|
16
|
+
# sidkiq_options :queue => :my_queue
|
17
|
+
# sidekiq_throttle :threshold => { :limit => 123, :period => 1.hour },
|
18
|
+
# :requeue => { :to => :other_queue, :with => :schedule }
|
19
|
+
#
|
20
|
+
# def perform
|
21
|
+
# # ...
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# @see ClassMethods
|
26
|
+
module Job
|
27
|
+
# Extends worker class with {ClassMethods}.
|
28
|
+
#
|
29
|
+
# @note Using `included` hook with extending worker with {ClassMethods}
|
30
|
+
# in order to make API inline with `include Sidekiq::Job`.
|
31
|
+
#
|
32
|
+
# @private
|
33
|
+
def self.included(base)
|
34
|
+
base.sidekiq_class_attribute :sidekiq_throttled_requeue_options
|
35
|
+
base.extend(ClassMethods)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Helper methods added to the singleton class of destination
|
39
|
+
module ClassMethods
|
40
|
+
# Registers some strategy for the worker.
|
41
|
+
#
|
42
|
+
# @example Allow max 123 MyJob jobs per hour
|
43
|
+
#
|
44
|
+
# class MyJob
|
45
|
+
# include Sidekiq::Job
|
46
|
+
# include Sidekiq::Throttled::Job
|
47
|
+
#
|
48
|
+
# sidekiq_throttle({
|
49
|
+
# :threshold => { :limit => 123, :period => 1.hour }
|
50
|
+
# })
|
51
|
+
# end
|
52
|
+
#
|
53
|
+
# @example Allow max 10 concurrently running MyJob jobs
|
54
|
+
#
|
55
|
+
# class MyJob
|
56
|
+
# include Sidekiq::Job
|
57
|
+
# include Sidekiq::Throttled::Job
|
58
|
+
#
|
59
|
+
# sidekiq_throttle({
|
60
|
+
# :concurrency => { :limit => 10 }
|
61
|
+
# })
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# @example Allow max 10 concurrent MyJob jobs and max 123 per hour
|
65
|
+
#
|
66
|
+
# class MyJob
|
67
|
+
# include Sidekiq::Job
|
68
|
+
# include Sidekiq::Throttled::Job
|
69
|
+
#
|
70
|
+
# sidekiq_throttle({
|
71
|
+
# :threshold => { :limit => 123, :period => 1.hour },
|
72
|
+
# :concurrency => { :limit => 10 }
|
73
|
+
# })
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# @example Allow max 123 MyJob jobs per hour; when jobs are throttled, schedule them for later in :other_queue
|
77
|
+
#
|
78
|
+
# class MyJob
|
79
|
+
# include Sidekiq::Job
|
80
|
+
# include Sidekiq::Throttled::Job
|
81
|
+
#
|
82
|
+
# sidekiq_throttle({
|
83
|
+
# :threshold => { :limit => 123, :period => 1.hour },
|
84
|
+
# :requeue => { :to => :other_queue, :with => :schedule }
|
85
|
+
# })
|
86
|
+
# end
|
87
|
+
#
|
88
|
+
# @param [Hash] requeue What to do with jobs that are throttled
|
89
|
+
# @see Registry.add
|
90
|
+
# @return [void]
|
91
|
+
def sidekiq_throttle(**kwargs)
|
92
|
+
Registry.add(self, **kwargs)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Adds current worker to preconfigured throttling strategy. Allows
|
96
|
+
# sharing same pool for multiple workers.
|
97
|
+
#
|
98
|
+
# First of all we need to create shared throttling strategy:
|
99
|
+
#
|
100
|
+
# # Create google_api throttling strategy
|
101
|
+
# Sidekiq::Throttled::Registry.add(:google_api, {
|
102
|
+
# :threshold => { :limit => 123, :period => 1.hour },
|
103
|
+
# :concurrency => { :limit => 10 }
|
104
|
+
# })
|
105
|
+
#
|
106
|
+
# Now we can assign it to our workers:
|
107
|
+
#
|
108
|
+
# class FetchProfileJob
|
109
|
+
# include Sidekiq::Job
|
110
|
+
# include Sidekiq::Throttled::Job
|
111
|
+
#
|
112
|
+
# sidekiq_throttle_as :google_api
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# class FetchCommentsJob
|
116
|
+
# include Sidekiq::Job
|
117
|
+
# include Sidekiq::Throttled::Job
|
118
|
+
#
|
119
|
+
# sidekiq_throttle_as :google_api
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
# With the above configuration we ensure that there are maximum 10
|
123
|
+
# concurrently running jobs of FetchProfileJob or FetchCommentsJob
|
124
|
+
# allowed. And only 123 jobs of those are executed per hour.
|
125
|
+
#
|
126
|
+
# In other words, it will allow:
|
127
|
+
#
|
128
|
+
# - only `X` concurrent `FetchProfileJob`s
|
129
|
+
# - max `XX` `FetchProfileJob` per hour
|
130
|
+
# - only `Y` concurrent `FetchCommentsJob`s
|
131
|
+
# - max `YY` `FetchCommentsJob` per hour
|
132
|
+
#
|
133
|
+
# Where `(X + Y) == 10` and `(XX + YY) == 123`
|
134
|
+
#
|
135
|
+
# @see Registry.add_alias
|
136
|
+
# @return [void]
|
137
|
+
def sidekiq_throttle_as(name)
|
138
|
+
Registry.add_alias(self, name)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
module Throttled
|
5
|
+
class Message
|
6
|
+
def initialize(item)
|
7
|
+
@item = item.is_a?(Hash) ? item : parse(item)
|
8
|
+
end
|
9
|
+
|
10
|
+
def job_class
|
11
|
+
@item.fetch("wrapped") { @item["class"] }
|
12
|
+
end
|
13
|
+
|
14
|
+
def job_args
|
15
|
+
@item.key?("wrapped") ? @item.dig("args", 0, "arguments") : @item["args"]
|
16
|
+
end
|
17
|
+
|
18
|
+
def job_id
|
19
|
+
@item["jid"]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def parse(item)
|
25
|
+
item = Sidekiq.load_json(item)
|
26
|
+
item.is_a?(Hash) ? item : {}
|
27
|
+
rescue JSON::ParserError
|
28
|
+
{}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# internal
|
4
|
+
require_relative "../message"
|
5
|
+
require_relative "../registry"
|
6
|
+
|
7
|
+
module Sidekiq
|
8
|
+
module Throttled
|
9
|
+
module Middlewares
|
10
|
+
# Server middleware required for Sidekiq::Throttled functioning.
|
11
|
+
class Server
|
12
|
+
include Sidekiq::ServerMiddleware
|
13
|
+
|
14
|
+
def call(_worker, msg, _queue)
|
15
|
+
yield
|
16
|
+
ensure
|
17
|
+
message = Message.new(msg)
|
18
|
+
|
19
|
+
if message.job_class && message.job_id
|
20
|
+
Registry.get(message.job_class) do |strategy|
|
21
|
+
strategy.finalize!(message.job_id, *message.job_args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sidekiq"
|
4
|
+
require "sidekiq/fetch"
|
5
|
+
|
6
|
+
require_relative "./throttled_retriever"
|
7
|
+
|
8
|
+
module Sidekiq
|
9
|
+
module Throttled
|
10
|
+
module Patches
|
11
|
+
module BasicFetch
|
12
|
+
def self.prepended(base)
|
13
|
+
base.prepend(ThrottledRetriever)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Returns list of queues to try to fetch jobs from.
|
19
|
+
#
|
20
|
+
# @note It may return an empty array.
|
21
|
+
# @param [Array<String>] queues
|
22
|
+
# @return [Array<String>]
|
23
|
+
def queues_cmd
|
24
|
+
throttled_queues = Throttled.cooldown&.queues
|
25
|
+
return super if throttled_queues.nil? || throttled_queues.empty?
|
26
|
+
|
27
|
+
super - throttled_queues
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Sidekiq::BasicFetch.prepend(Sidekiq::Throttled::Patches::BasicFetch)
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sidekiq"
|
4
|
+
|
5
|
+
require_relative "./throttled_retriever"
|
6
|
+
|
7
|
+
module Sidekiq
|
8
|
+
module Throttled
|
9
|
+
module Patches
|
10
|
+
module SuperFetch
|
11
|
+
def self.prepended(base)
|
12
|
+
base.prepend(ThrottledRetriever)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# Returns list of non-paused queues to try to fetch jobs from.
|
18
|
+
#
|
19
|
+
# @note It may return an empty array.
|
20
|
+
# @return [Array<Array(String, String)>]
|
21
|
+
def active_queues
|
22
|
+
# Create a hash of throttled queues for fast lookup
|
23
|
+
throttled_queues = Throttled.cooldown&.queues&.to_h { |queue| [queue, true] }
|
24
|
+
return super if throttled_queues.nil? || throttled_queues.empty?
|
25
|
+
|
26
|
+
# Reject throttled queues from the list of active queues
|
27
|
+
super.reject { |queue, _private_queue| throttled_queues[queue] }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
begin
|
35
|
+
require "sidekiq/pro/super_fetch"
|
36
|
+
Sidekiq::Pro::SuperFetch.prepend(Sidekiq::Throttled::Patches::SuperFetch)
|
37
|
+
rescue LoadError
|
38
|
+
# Sidekiq Pro is not available
|
39
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
module Throttled
|
5
|
+
module Patches
|
6
|
+
module ThrottledRetriever
|
7
|
+
# Retrieves job from redis.
|
8
|
+
#
|
9
|
+
# @return [Sidekiq::BasicFetch::UnitOfWork, nil]
|
10
|
+
def retrieve_work
|
11
|
+
work = super
|
12
|
+
|
13
|
+
if work && Throttled.throttled?(work.job)
|
14
|
+
Throttled.cooldown&.notify_throttled(work.queue)
|
15
|
+
Throttled.requeue_throttled(work)
|
16
|
+
return nil
|
17
|
+
end
|
18
|
+
|
19
|
+
Throttled.cooldown&.notify_admitted(work.queue) if work
|
20
|
+
|
21
|
+
work
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# internal
|
4
|
+
require_relative "./strategy"
|
5
|
+
|
6
|
+
module Sidekiq
|
7
|
+
module Throttled
|
8
|
+
# Registred strategies.
|
9
|
+
#
|
10
|
+
# @private
|
11
|
+
module Registry
|
12
|
+
@strategies = {}
|
13
|
+
@aliases = {}
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# Adds strategy to the registry.
|
17
|
+
#
|
18
|
+
# @param (see Strategy#initialize)
|
19
|
+
# @return [Strategy]
|
20
|
+
def add(name, **kwargs)
|
21
|
+
name = name.to_s
|
22
|
+
|
23
|
+
@strategies[name] = Strategy.new(name, **kwargs)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Adds alias for existing strategy.
|
27
|
+
#
|
28
|
+
# @param (#to_s) new_name
|
29
|
+
# @param (#to_s) old_name
|
30
|
+
# @raise [RuntimeError] if no strategy found with `old_name`
|
31
|
+
# @return [Strategy]
|
32
|
+
def add_alias(new_name, old_name)
|
33
|
+
new_name = new_name.to_s
|
34
|
+
old_name = old_name.to_s
|
35
|
+
|
36
|
+
raise "Strategy not found: #{old_name}" unless @strategies[old_name]
|
37
|
+
|
38
|
+
@aliases[new_name] = @strategies[old_name]
|
39
|
+
end
|
40
|
+
|
41
|
+
# @overload get(name)
|
42
|
+
# @param [#to_s] name
|
43
|
+
# @return [Strategy, nil] registred strategy
|
44
|
+
#
|
45
|
+
# @overload get(name, &block)
|
46
|
+
# Yields control to the block if requested strategy was found.
|
47
|
+
# @param [#to_s] name
|
48
|
+
# @yieldparam [Strategy] strategy
|
49
|
+
# @yield [strategy] Gives found strategy to the block
|
50
|
+
# @return result of a block
|
51
|
+
def get(name)
|
52
|
+
strategy = find(name.to_s) || find_by_class(name)
|
53
|
+
|
54
|
+
return yield strategy if strategy && block_given?
|
55
|
+
|
56
|
+
strategy
|
57
|
+
end
|
58
|
+
|
59
|
+
# @overload each()
|
60
|
+
# @return [Enumerator]
|
61
|
+
#
|
62
|
+
# @overload each(&block)
|
63
|
+
# @yieldparam [String] name
|
64
|
+
# @yieldparam [Strategy] strategy
|
65
|
+
# @yield [strategy] Gives strategy to the block
|
66
|
+
# @return [Registry]
|
67
|
+
def each
|
68
|
+
return to_enum(__method__) unless block_given?
|
69
|
+
|
70
|
+
@strategies.each { |*args| yield(*args) }
|
71
|
+
self
|
72
|
+
end
|
73
|
+
|
74
|
+
# @overload each_with_static_keys()
|
75
|
+
# @return [Enumerator]
|
76
|
+
#
|
77
|
+
# @overload each_with_static_keys(&block)
|
78
|
+
# @yieldparam [String] name
|
79
|
+
# @yieldparam [Strategy] strategy
|
80
|
+
# @yield [strategy] Gives strategy to the block
|
81
|
+
# @return [Registry]
|
82
|
+
def each_with_static_keys
|
83
|
+
return to_enum(__method__) unless block_given?
|
84
|
+
|
85
|
+
@strategies.each do |name, strategy|
|
86
|
+
yield(name, strategy) unless strategy.dynamic?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Find strategy by it's name.
|
93
|
+
#
|
94
|
+
# @param name [String]
|
95
|
+
# @return [Strategy, nil]
|
96
|
+
def find(name)
|
97
|
+
@strategies[name] || @aliases[name]
|
98
|
+
end
|
99
|
+
|
100
|
+
# Find strategy by class or it's parents.
|
101
|
+
#
|
102
|
+
# @param name [Class, #to_s]
|
103
|
+
# @return [Strategy, nil]
|
104
|
+
def find_by_class(name)
|
105
|
+
const = name.is_a?(Class) ? name : Object.const_get(name)
|
106
|
+
return unless const.is_a?(Class)
|
107
|
+
|
108
|
+
const.ancestors.each do |m|
|
109
|
+
strategy = find(m.name)
|
110
|
+
return strategy if strategy
|
111
|
+
end
|
112
|
+
|
113
|
+
nil
|
114
|
+
rescue NameError
|
115
|
+
nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sidekiq
|
4
|
+
module Throttled
|
5
|
+
class Strategy
|
6
|
+
module Base
|
7
|
+
def limit(job_args = nil)
|
8
|
+
@limit.respond_to?(:call) ? @limit.call(*job_args) : @limit
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def key(job_args)
|
14
|
+
key = @base_key.dup
|
15
|
+
return key unless @key_suffix
|
16
|
+
|
17
|
+
key << ":#{@key_suffix.call(*job_args)}"
|
18
|
+
rescue StandardError => e
|
19
|
+
Sidekiq.logger.error "Failed to get key suffix: #{e}"
|
20
|
+
raise e
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
local in_progress_jobs_key = KEYS[1]
|
2
|
+
local backlog_info_key = KEYS[2]
|
3
|
+
local jid = ARGV[1]
|
4
|
+
local lmt = tonumber(ARGV[2])
|
5
|
+
local lost_job_threshold = tonumber(ARGV[3])
|
6
|
+
local now = tonumber(ARGV[4])
|
7
|
+
|
8
|
+
-- supporting functions
|
9
|
+
local function over_limit()
|
10
|
+
return lmt <= redis.call("ZCARD", in_progress_jobs_key)
|
11
|
+
end
|
12
|
+
|
13
|
+
local function job_already_in_progress()
|
14
|
+
return redis.call("ZSCORE", in_progress_jobs_key, jid)
|
15
|
+
end
|
16
|
+
|
17
|
+
-- Estimates current backlog size. This function tends to underestimate
|
18
|
+
-- the actual backlog. This is intentional. Overestimates are bad as it
|
19
|
+
-- can cause unnecessary delays in job processing. Underestimates are much
|
20
|
+
-- safer as they only increase workload of sidekiq processors.
|
21
|
+
local function est_current_backlog_size()
|
22
|
+
local old_size = tonumber(redis.call("HGET", backlog_info_key, "size") or 0)
|
23
|
+
local old_timestamp = tonumber(redis.call("HGET", backlog_info_key, "timestamp") or now)
|
24
|
+
|
25
|
+
local jobs_lost_since_old_timestamp = (now - old_timestamp) / lost_job_threshold * lmt
|
26
|
+
|
27
|
+
return math.max(old_size - jobs_lost_since_old_timestamp, 0)
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
local function change_backlog_size(delta)
|
32
|
+
local curr_backlog_size = est_current_backlog_size()
|
33
|
+
|
34
|
+
redis.call("HSET", backlog_info_key, "size", curr_backlog_size + delta)
|
35
|
+
redis.call("HSET", backlog_info_key, "timestamp", now)
|
36
|
+
redis.call("EXPIRE", backlog_info_key, math.ceil((lost_job_threshold * curr_backlog_size) + 1 / lmt))
|
37
|
+
end
|
38
|
+
|
39
|
+
local function register_job_in_progress()
|
40
|
+
redis.call("ZADD", in_progress_jobs_key, now + lost_job_threshold , jid)
|
41
|
+
redis.call("EXPIRE", in_progress_jobs_key, lost_job_threshold)
|
42
|
+
end
|
43
|
+
|
44
|
+
local function clear_stale_in_progress_jobs()
|
45
|
+
local cleared_count = redis.call("ZREMRANGEBYSCORE", in_progress_jobs_key, "-inf", "(" .. now)
|
46
|
+
change_backlog_size(-cleared_count)
|
47
|
+
end
|
48
|
+
|
49
|
+
-- END supporting functions
|
50
|
+
|
51
|
+
clear_stale_in_progress_jobs()
|
52
|
+
|
53
|
+
if over_limit() and not job_already_in_progress() then
|
54
|
+
change_backlog_size(1)
|
55
|
+
return 1
|
56
|
+
end
|
57
|
+
|
58
|
+
register_job_in_progress()
|
59
|
+
change_backlog_size(-1)
|
60
|
+
|
61
|
+
return 0
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "redis_prescription"
|
4
|
+
|
5
|
+
require_relative "./base"
|
6
|
+
|
7
|
+
module Sidekiq
|
8
|
+
module Throttled
|
9
|
+
class Strategy
|
10
|
+
# Concurrency throttling strategy
|
11
|
+
class Concurrency
|
12
|
+
include Base
|
13
|
+
|
14
|
+
# LUA script used to limit fetch concurrency.
|
15
|
+
# Logic behind the scene can be described in following pseudo code:
|
16
|
+
#
|
17
|
+
# if @limit <= LLEN(@key)
|
18
|
+
# return 1
|
19
|
+
# else
|
20
|
+
# PUSH(@key, @jid)
|
21
|
+
# return 0
|
22
|
+
# end
|
23
|
+
SCRIPT = RedisPrescription.new(File.read("#{__dir__}/concurrency.lua"))
|
24
|
+
private_constant :SCRIPT
|
25
|
+
|
26
|
+
# @param [#to_s] strategy_key
|
27
|
+
# @param [#to_i, #call] limit Amount of allowed concurrent jobs
|
28
|
+
# per processors running for given key.
|
29
|
+
# @param [#to_i] avg_job_duration Average number of seconds needed
|
30
|
+
# to complete a job of this type. Default: 300 or 1/3 of lost_job_threshold
|
31
|
+
# @param [#to_i] lost_job_threshold Seconds to wait before considering
|
32
|
+
# a job lost or dead. Default: 900 or 3 * avg_job_duration
|
33
|
+
# @param [Proc] key_suffix Dynamic key suffix generator.
|
34
|
+
# @deprecated @param [#to_i] ttl Obsolete alias for `lost_job_threshold`.
|
35
|
+
# Default: 900 or 3 * avg_job_duration
|
36
|
+
def initialize(strategy_key, limit:, avg_job_duration: nil, ttl: nil, lost_job_threshold: ttl, key_suffix: nil) # rubocop:disable Metrics/ParameterLists
|
37
|
+
@base_key = "#{strategy_key}:concurrency.v2"
|
38
|
+
@limit = limit
|
39
|
+
@avg_job_duration, @lost_job_threshold = interp_duration_args(avg_job_duration, lost_job_threshold)
|
40
|
+
@key_suffix = key_suffix
|
41
|
+
|
42
|
+
raise(ArgumentError, "lost_job_threshold must be greater than avg_job_duration") if
|
43
|
+
@lost_job_threshold <= @avg_job_duration
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Boolean] Whenever strategy has dynamic config
|
47
|
+
def dynamic?
|
48
|
+
@key_suffix || @limit.respond_to?(:call)
|
49
|
+
end
|
50
|
+
|
51
|
+
# @return [Boolean] whenever job is throttled or not
|
52
|
+
def throttled?(jid, *job_args)
|
53
|
+
job_limit = limit(job_args)
|
54
|
+
return false unless job_limit
|
55
|
+
return true if job_limit <= 0
|
56
|
+
|
57
|
+
keys = [key(job_args), backlog_info_key(job_args)]
|
58
|
+
argv = [jid.to_s, job_limit, @lost_job_threshold, Time.now.to_f]
|
59
|
+
|
60
|
+
Sidekiq.redis { |redis| (1 == SCRIPT.call(redis, keys: keys, argv: argv)) }
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Float] How long, in seconds, before we'll next be able to take on jobs
|
64
|
+
def retry_in(_jid, *job_args)
|
65
|
+
job_limit = limit(job_args)
|
66
|
+
return 0.0 if !job_limit || count(*job_args) < job_limit
|
67
|
+
|
68
|
+
estimated_backlog_size(job_args) * @avg_job_duration / limit(job_args)
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Integer] Current count of jobs
|
72
|
+
def count(*job_args)
|
73
|
+
Sidekiq.redis { |conn| conn.zcard(key(job_args)) }.to_i
|
74
|
+
end
|
75
|
+
|
76
|
+
# Resets count of jobs
|
77
|
+
# @return [void]
|
78
|
+
def reset!(*job_args)
|
79
|
+
Sidekiq.redis { |conn| conn.del(key(job_args)) }
|
80
|
+
end
|
81
|
+
|
82
|
+
# Remove jid from the pool of jobs in progress
|
83
|
+
# @return [void]
|
84
|
+
def finalize!(jid, *job_args)
|
85
|
+
Sidekiq.redis do |conn|
|
86
|
+
conn.zrem(key(job_args), jid.to_s)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def backlog_info_key(job_args)
|
93
|
+
"#{key(job_args)}.backlog_info"
|
94
|
+
end
|
95
|
+
|
96
|
+
def estimated_backlog_size(job_args)
|
97
|
+
old_size_str, old_timestamp_str =
|
98
|
+
Sidekiq.redis { |conn| conn.hmget(backlog_info_key(job_args), "size", "timestamp") }
|
99
|
+
old_size = (old_size_str || 0).to_f
|
100
|
+
old_timestamp = (old_timestamp_str || Time.now).to_f
|
101
|
+
|
102
|
+
nonneg(old_size - jobs_lost_since(old_timestamp, job_args))
|
103
|
+
end
|
104
|
+
|
105
|
+
def jobs_lost_since(timestamp, job_args)
|
106
|
+
(Time.now.to_f - timestamp) / @lost_job_threshold * limit(job_args)
|
107
|
+
end
|
108
|
+
|
109
|
+
def nonneg(number)
|
110
|
+
[number, 0].max
|
111
|
+
end
|
112
|
+
|
113
|
+
def interp_duration_args(avg_job_duration, lost_job_threshold)
|
114
|
+
if avg_job_duration && lost_job_threshold
|
115
|
+
[avg_job_duration.to_i, lost_job_threshold.to_i]
|
116
|
+
elsif avg_job_duration && lost_job_threshold.nil?
|
117
|
+
[avg_job_duration.to_i, avg_job_duration.to_i * 3]
|
118
|
+
elsif avg_job_duration.nil? && lost_job_threshold
|
119
|
+
[lost_job_threshold.to_i / 3, lost_job_threshold.to_i]
|
120
|
+
else
|
121
|
+
[300, 900]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
local key = KEYS[1]
|
2
|
+
local lmt = tonumber(ARGV[1])
|
3
|
+
local ttl = tonumber(ARGV[2])
|
4
|
+
local now = tonumber(ARGV[3])
|
5
|
+
|
6
|
+
if lmt <= redis.call("LLEN", key) and now - redis.call("LINDEX", key, -1) < ttl then
|
7
|
+
return 1
|
8
|
+
end
|
9
|
+
|
10
|
+
redis.call("LPUSH", key, now)
|
11
|
+
redis.call("LTRIM", key, 0, lmt - 1)
|
12
|
+
redis.call("EXPIRE", key, ttl)
|
13
|
+
|
14
|
+
return 0
|