multi-background-job 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/specs.yml +35 -0
- data/.gitignore +10 -0
- data/.rspec +1 -0
- data/.tool-versions +1 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +55 -0
- data/LICENSE.txt +21 -0
- data/README.md +137 -0
- data/Rakefile +2 -0
- data/bin/console +13 -0
- data/bin/setup +6 -0
- data/lib/multi-background-job.rb +3 -0
- data/lib/multi_background_job.rb +89 -0
- data/lib/multi_background_job/adapters/adapter.rb +23 -0
- data/lib/multi_background_job/adapters/faktory.rb +111 -0
- data/lib/multi_background_job/adapters/sidekiq.rb +91 -0
- data/lib/multi_background_job/config.rb +152 -0
- data/lib/multi_background_job/errors.rb +25 -0
- data/lib/multi_background_job/lock.rb +137 -0
- data/lib/multi_background_job/lock_digest.rb +39 -0
- data/lib/multi_background_job/middleware/unique_job.rb +69 -0
- data/lib/multi_background_job/middleware/unique_job/faktory.rb +41 -0
- data/lib/multi_background_job/middleware/unique_job/sidekiq.rb +48 -0
- data/lib/multi_background_job/middleware_chain.rb +109 -0
- data/lib/multi_background_job/unique_job.rb +84 -0
- data/lib/multi_background_job/version.rb +5 -0
- data/lib/multi_background_job/worker.rb +114 -0
- data/lib/multi_background_job/workers/faktory.rb +28 -0
- data/lib/multi_background_job/workers/shared_class_methods.rb +26 -0
- data/lib/multi_background_job/workers/sidekiq.rb +28 -0
- data/multi-background-job.gemspec +39 -0
- metadata +122 -0
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MultiBackgroundJob
|
4
|
+
module Adapters
|
5
|
+
# This is a Sidekiq adapter that converts MultiBackgroundJob::Worker object into a sidekiq readable format
|
6
|
+
# and then push the jobs into the service.
|
7
|
+
class Sidekiq < Adapter
|
8
|
+
attr_reader :worker, :queue
|
9
|
+
|
10
|
+
def initialize(worker)
|
11
|
+
@worker = worker
|
12
|
+
@queue = worker.options.fetch(:queue, 'default')
|
13
|
+
|
14
|
+
@payload = worker.payload.merge(
|
15
|
+
'class' => worker.worker_class,
|
16
|
+
'retry' => worker.options.fetch(:retry, true),
|
17
|
+
'queue' => @queue,
|
18
|
+
)
|
19
|
+
@payload['created_at'] ||= Time.now.to_f
|
20
|
+
end
|
21
|
+
|
22
|
+
# Coerces the raw payload into an instance of Worker
|
23
|
+
# @param payload [Hash] The job as json from redis
|
24
|
+
# @options options [Hash] list of options that will be passed along to the Worker instance
|
25
|
+
# @return [MultiBackgroundJob::Worker] and instance of MultiBackgroundJob::Worker
|
26
|
+
def self.coerce_to_worker(payload, **options)
|
27
|
+
raise(Error, 'invalid payload') unless payload.is_a?(Hash)
|
28
|
+
raise(Error, 'invalid payload') unless payload['class'].is_a?(String)
|
29
|
+
|
30
|
+
options[:retry] ||= payload['retry'] if payload.key?('retry')
|
31
|
+
options[:queue] ||= payload['queue'] if payload.key?('queue')
|
32
|
+
|
33
|
+
MultiBackgroundJob[payload['class'], **options].tap do |worker|
|
34
|
+
worker.with_args(*Array(payload['args'])) if payload.key?('args')
|
35
|
+
worker.with_job_jid(payload['jid']) if payload.key?('jid')
|
36
|
+
worker.created_at(payload['created_at']) if payload.key?('created_at')
|
37
|
+
worker.enqueued_at(payload['enqueued_at']) if payload.key?('enqueued_at')
|
38
|
+
worker.at(payload['at']) if payload.key?('at')
|
39
|
+
worker.unique(payload['uniq']) if payload.key?('uniq')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Initializes adapter and push job into the sidekiq service
|
44
|
+
#
|
45
|
+
# @param worker [MultiBackgroundJob::Worker] An instance of MultiBackgroundJob::Worker
|
46
|
+
# @return [Hash] Job payload
|
47
|
+
# @see push method for more details
|
48
|
+
def self.push(worker)
|
49
|
+
new(worker).push
|
50
|
+
end
|
51
|
+
|
52
|
+
# Push sidekiq to the Sidekiq(Redis actually).
|
53
|
+
# * If job has the 'at' key. Then schedule it
|
54
|
+
# * Otherwise enqueue for immediate execution
|
55
|
+
#
|
56
|
+
# @return [Hash] Payload that was sent to redis
|
57
|
+
def push
|
58
|
+
@payload['enqueued_at'] = Time.now.to_f
|
59
|
+
# Optimization to enqueue something now that is scheduled to go out now or in the past
|
60
|
+
if (timestamp = @payload.delete('at')) && (timestamp > Time.now.to_f)
|
61
|
+
MultiBackgroundJob.redis_pool.with do |redis|
|
62
|
+
redis.zadd(scheduled_queue_name, timestamp.to_f.to_s, to_json(@payload))
|
63
|
+
end
|
64
|
+
else
|
65
|
+
MultiBackgroundJob.redis_pool.with do |redis|
|
66
|
+
redis.lpush(immediate_queue_name, to_json(@payload))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
@payload
|
70
|
+
end
|
71
|
+
|
72
|
+
protected
|
73
|
+
|
74
|
+
def namespace
|
75
|
+
MultiBackgroundJob.config.redis_namespace
|
76
|
+
end
|
77
|
+
|
78
|
+
def scheduled_queue_name
|
79
|
+
"#{namespace}:schedule"
|
80
|
+
end
|
81
|
+
|
82
|
+
def immediate_queue_name
|
83
|
+
"#{namespace}:queue:#{queue}"
|
84
|
+
end
|
85
|
+
|
86
|
+
def to_json(value)
|
87
|
+
MultiJson.dump(value, mode: :compat)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frizen_string_literal: true
|
2
|
+
|
3
|
+
require 'redis'
|
4
|
+
require 'connection_pool'
|
5
|
+
|
6
|
+
require_relative './middleware_chain'
|
7
|
+
|
8
|
+
module MultiBackgroundJob
|
9
|
+
class Config
|
10
|
+
class << self
|
11
|
+
private
|
12
|
+
|
13
|
+
def attribute_accessor(field, validator: nil, normalizer: nil, default: nil)
|
14
|
+
normalizer ||= :"normalize_#{field}"
|
15
|
+
validator ||= :"validate_#{field}"
|
16
|
+
|
17
|
+
define_method(field) do
|
18
|
+
unless instance_variable_defined?(:"@#{field}")
|
19
|
+
fallback = config_from_yaml[field.to_s] || default
|
20
|
+
return if fallback.nil?
|
21
|
+
|
22
|
+
send(:"#{field}=", fallback.respond_to?(:call) ? fallback.call : fallback)
|
23
|
+
end
|
24
|
+
instance_variable_get(:"@#{field}")
|
25
|
+
end
|
26
|
+
|
27
|
+
define_method(:"#{field}=") do |value|
|
28
|
+
value = send(normalizer, field, value) if respond_to?(normalizer, true)
|
29
|
+
send(validator, field, value) if respond_to?(validator, true)
|
30
|
+
|
31
|
+
instance_variable_set(:"@#{field}", value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Path to the YAML file with configs
|
37
|
+
attr_accessor :config_path
|
38
|
+
|
39
|
+
# ConnectionPool options for redis
|
40
|
+
attribute_accessor :redis_pool_size, default: 5, normalizer: :normalize_to_int, validator: :validate_greater_than_zero
|
41
|
+
attribute_accessor :redis_pool_timeout, default: 5, normalizer: :normalize_to_int, validator: :validate_greater_than_zero
|
42
|
+
|
43
|
+
# Namespace used to manage internal data like unique job verification data.
|
44
|
+
attribute_accessor :redis_namespace, default: 'multi-bg'
|
45
|
+
|
46
|
+
# List of configurations to be passed along to the Redis.new
|
47
|
+
attribute_accessor :redis_config, default: {}
|
48
|
+
|
49
|
+
# A Hash with all workers definitions. The worker class name must be the main hash key
|
50
|
+
# Example:
|
51
|
+
# "Accounts::ConfirmationEmailWorker":
|
52
|
+
# retry: false
|
53
|
+
# queue: "mailer"
|
54
|
+
# "Elastic::BatchIndex":
|
55
|
+
# retry: 5
|
56
|
+
# queue: "elasticsearch"
|
57
|
+
# adapter: "faktory"
|
58
|
+
attribute_accessor :workers, default: {}
|
59
|
+
|
60
|
+
# Does not validate if it's when set to false
|
61
|
+
attribute_accessor :strict, default: true
|
62
|
+
alias strict? strict
|
63
|
+
|
64
|
+
# Global disable the unique_job_active
|
65
|
+
attribute_accessor :unique_job_active, default: false
|
66
|
+
alias unique_job_active? unique_job_active
|
67
|
+
|
68
|
+
def worker_options(class_name)
|
69
|
+
class_name = class_name.to_s
|
70
|
+
if strict? && !workers.key?(class_name)
|
71
|
+
raise NotDefinedWorker.new(class_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
workers.fetch(class_name, {})
|
75
|
+
end
|
76
|
+
|
77
|
+
def redis_pool
|
78
|
+
{
|
79
|
+
size: redis_pool_size,
|
80
|
+
timeout: redis_pool_timeout,
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def middleware
|
85
|
+
@middleware ||= MiddlewareChain.new
|
86
|
+
yield @middleware if block_given?
|
87
|
+
@middleware
|
88
|
+
end
|
89
|
+
|
90
|
+
def config_path=(value)
|
91
|
+
@config_from_yaml = nil
|
92
|
+
@config_path = value
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def normalize_to_int(_attribute, value)
|
98
|
+
value.to_i
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate_greater_than_zero(attribute, value)
|
102
|
+
return if value > 0
|
103
|
+
|
104
|
+
raise InvalidConfig, format(
|
105
|
+
'The %<value>p for %<attr>s is not valid. It must be greater than zero',
|
106
|
+
value: value,
|
107
|
+
attr: attribute,
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
def normalize_redis_config(_attribute, value)
|
112
|
+
case value
|
113
|
+
when String
|
114
|
+
{ url: value }
|
115
|
+
when Hash
|
116
|
+
value.each_with_object({}) { |(k, v), r| r[k.to_sym] = v }
|
117
|
+
else
|
118
|
+
value
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def validate_redis_config(attribute, value)
|
123
|
+
return if value.is_a?(Hash)
|
124
|
+
|
125
|
+
raise InvalidConfig, format(
|
126
|
+
'The %<value>p for %<attr>i is not valid. It must be a Hash with the redis initialization options. ' +
|
127
|
+
'See https://github.com/redis/redis-rb for all available options',
|
128
|
+
value: value,
|
129
|
+
attr: attribute,
|
130
|
+
)
|
131
|
+
end
|
132
|
+
|
133
|
+
def normalize_workers(_, value)
|
134
|
+
return unless value.is_a?(Hash)
|
135
|
+
|
136
|
+
hash = {}
|
137
|
+
value.each do |class_name, opts|
|
138
|
+
hash[class_name.to_s] = MultiJson.load(MultiJson.dump(opts), symbolize_names: true)
|
139
|
+
end
|
140
|
+
hash
|
141
|
+
end
|
142
|
+
|
143
|
+
def config_from_yaml
|
144
|
+
@config_from_yaml ||= begin
|
145
|
+
config_path ? YAML.load_file(config_path) : {}
|
146
|
+
rescue Errno::ENOENT, Errno::ESRCH
|
147
|
+
{}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MultiBackgroundJob
|
4
|
+
class Error < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
class InvalidConfig < Error
|
8
|
+
end
|
9
|
+
|
10
|
+
class NotDefinedWorker < Error
|
11
|
+
def initialize(worker_class)
|
12
|
+
@worker_class = worker_class
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
format(
|
17
|
+
"The %<worker>p is not defined and the MultiBackgroundJob is configured to work on strict mode.\n" +
|
18
|
+
"it's highly recommended to include this worker class to the list of known workers.\n" +
|
19
|
+
"Example: `MultiBackgroundJob.configure { |config| config.workers = { %<worker>p => {} } }`\n" +
|
20
|
+
'Another option is to set config.strict = false',
|
21
|
+
worker: @worker_class,
|
22
|
+
)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MultiBackgroundJob
|
4
|
+
# Class Lock provides access to redis "sorted set" used to control unique jobs
|
5
|
+
class Lock
|
6
|
+
attr_reader :digest, :lock_id, :ttl
|
7
|
+
|
8
|
+
# @param :digest [String] It's the uniq string used to group similar jobs
|
9
|
+
# @param :lock_id [String] The uniq job id
|
10
|
+
# @param :ttl [Float] The timestamp related lifietime of the lock before being discarded.
|
11
|
+
def initialize(digest:, lock_id:, ttl:)
|
12
|
+
@digest = digest
|
13
|
+
@lock_id = lock_id
|
14
|
+
@ttl = ttl
|
15
|
+
end
|
16
|
+
|
17
|
+
# Initialize a Lock object from hash
|
18
|
+
#
|
19
|
+
# @param value [Hash] Hash with lock properties
|
20
|
+
# @return [MultiBackgroundJob::Lock, nil]
|
21
|
+
def self.coerce(value)
|
22
|
+
return unless value.is_a?(Hash)
|
23
|
+
|
24
|
+
digest = value[:digest] || value['digest']
|
25
|
+
lock_id = value[:lock_id] || value['lock_id']
|
26
|
+
ttl = value[:ttl] || value['ttl']
|
27
|
+
return if [digest, lock_id, ttl].any?(&:nil?)
|
28
|
+
|
29
|
+
new(digest: digest, lock_id: lock_id, ttl: ttl)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Remove expired locks from redis "sorted set"
|
33
|
+
#
|
34
|
+
# @param [String] digest It's the uniq string used to group similar jobs
|
35
|
+
def self.flush_expired_members(digest, redis: nil)
|
36
|
+
return unless digest
|
37
|
+
|
38
|
+
caller = ->(redis) { redis.zremrangebyscore(digest, '-inf', "(#{now}") }
|
39
|
+
|
40
|
+
if redis
|
41
|
+
caller.(redis)
|
42
|
+
else
|
43
|
+
MultiBackgroundJob.redis_pool.with { |conn| caller.(conn) }
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Remove all locks from redis "sorted set"
|
48
|
+
#
|
49
|
+
# @param [String] digest It's the uniq string used to group similar jobs
|
50
|
+
def self.flush(digest, redis: nil)
|
51
|
+
return unless digest
|
52
|
+
|
53
|
+
caller = ->(conn) { conn.del(digest) }
|
54
|
+
|
55
|
+
if redis
|
56
|
+
caller.(redis)
|
57
|
+
else
|
58
|
+
MultiBackgroundJob.redis_pool.with { |conn| caller.(conn) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Number of locks
|
63
|
+
#
|
64
|
+
# @param digest [String] It's the uniq string used to group similar jobs
|
65
|
+
# @option [Number] from The begin of set. Default to 0
|
66
|
+
# @option [Number] to The end of set. Default to the timestamp of 1 week from now
|
67
|
+
# @return Number the amount of entries that within digest
|
68
|
+
def self.count(digest, from: 0, to: nil, redis: nil)
|
69
|
+
to ||= Time.now.to_f + MultiBackgroundJob::UniqueJob::VALID_OPTIONS[:timeout]
|
70
|
+
caller = ->(conn) { conn.zcount(digest, from, to) }
|
71
|
+
|
72
|
+
if redis
|
73
|
+
caller.(redis)
|
74
|
+
else
|
75
|
+
MultiBackgroundJob.redis_pool.with { |conn| caller.(conn) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def to_hash
|
80
|
+
{
|
81
|
+
'ttl' => ttl,
|
82
|
+
'digest' => (digest.to_s if digest),
|
83
|
+
'lock_id' => (lock_id.to_s if lock_id),
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [Float] A float timestamp of current time
|
88
|
+
def self.now
|
89
|
+
Time.now.to_f
|
90
|
+
end
|
91
|
+
|
92
|
+
# Remove lock_id lock from redis
|
93
|
+
# @return [Boolean] Returns true when it's locked or false when there is no lock
|
94
|
+
def unlock
|
95
|
+
MultiBackgroundJob.redis_pool.with do |conn|
|
96
|
+
conn.zrem(digest, lock_id)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Adds lock_id lock to redis
|
101
|
+
# @return [Boolean] Returns true when it's a fresh lock or false when lock already exists
|
102
|
+
def lock
|
103
|
+
MultiBackgroundJob.redis_pool.with do |conn|
|
104
|
+
conn.zadd(digest, ttl, lock_id)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Check if the lock_id lock exist
|
109
|
+
# @return [Boolean] true or false when lock exist or not
|
110
|
+
def locked?
|
111
|
+
locked = false
|
112
|
+
|
113
|
+
MultiBackgroundJob.redis_pool.with do |conn|
|
114
|
+
timestamp = conn.zscore(digest, lock_id)
|
115
|
+
return false unless timestamp
|
116
|
+
|
117
|
+
locked = timestamp >= now
|
118
|
+
self.class.flush_expired_members(digest, redis: conn)
|
119
|
+
end
|
120
|
+
|
121
|
+
locked
|
122
|
+
end
|
123
|
+
|
124
|
+
def eql?(other)
|
125
|
+
return false unless other.is_a?(self.class)
|
126
|
+
|
127
|
+
[digest, lock_id, ttl] == [other.digest, other.lock_id, other.ttl]
|
128
|
+
end
|
129
|
+
alias == eql?
|
130
|
+
|
131
|
+
protected
|
132
|
+
|
133
|
+
def now
|
134
|
+
self.class.now
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MultiBackgroundJob
|
4
|
+
# Class Lock generates the uniq digest acording to the uniq config
|
5
|
+
class LockDigest
|
6
|
+
BASE = 'uniqueness'.freeze
|
7
|
+
SEPARATOR = ':'.freeze
|
8
|
+
|
9
|
+
def initialize(*keys, across:)
|
10
|
+
@keys = keys.map { |k| k.to_s.strip.downcase }
|
11
|
+
@across = across.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
case @across
|
16
|
+
when :systemwide
|
17
|
+
build_name(*@keys.slice(0..-2))
|
18
|
+
when :queue
|
19
|
+
build_name(*@keys)
|
20
|
+
else
|
21
|
+
raise Error, format(
|
22
|
+
'Could not resolve the lock digest using across %<across>p. ' +
|
23
|
+
'Valid options are :systemwide and :queue',
|
24
|
+
across: @across,
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def build_name(*segments)
|
32
|
+
[namespace, BASE, *segments].compact.join(SEPARATOR)
|
33
|
+
end
|
34
|
+
|
35
|
+
def namespace
|
36
|
+
MultiBackgroundJob.config.redis_namespace
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'multi_background_job/lock'
|
4
|
+
require 'multi_background_job/lock_digest'
|
5
|
+
|
6
|
+
module MultiBackgroundJob
|
7
|
+
module Middleware
|
8
|
+
# This middleware uses an external redis queue to control duplications. The locking key
|
9
|
+
# is composed of worker class and its arguments. Before enqueue new jobs it will check if have a "lock" active.
|
10
|
+
# The TTL of lock is 1 week as default. TTL is important to ensure locks won't last forever.
|
11
|
+
class UniqueJob
|
12
|
+
def self.bootstrap(service:)
|
13
|
+
services = Dir[File.expand_path('../unique_job/*.rb', __FILE__)].map { |f| File.basename(f, '.rb').to_sym }
|
14
|
+
unless services.include?(service)
|
15
|
+
msg = "UniqueJob is not supported for the `%<service>p' service. Supported options are: %<services>s."
|
16
|
+
raise MultiBackgroundJob::Error, format(msg, service: service.to_sym, services: services.map { |s| "`:#{s}'" }.join(', '))
|
17
|
+
end
|
18
|
+
if (require("multi_background_job/middleware/unique_job/#{service}"))
|
19
|
+
class_name = service.to_s.split('_').collect!{ |w| w.capitalize }.join
|
20
|
+
MultiBackgroundJob::Middleware::UniqueJob.const_get(class_name).bootstrap
|
21
|
+
end
|
22
|
+
|
23
|
+
MultiBackgroundJob.configure do |config|
|
24
|
+
config.unique_job_active = true
|
25
|
+
config.middleware.add(UniqueJob)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def call(worker, service)
|
30
|
+
if MultiBackgroundJob.config.unique_job_active? &&
|
31
|
+
(uniq_lock = unique_job_lock(worker: worker, service: service))
|
32
|
+
return false if uniq_lock.locked? # Don't push job to server
|
33
|
+
|
34
|
+
# Add unique job information to the job payload
|
35
|
+
worker.unique_job.lock = uniq_lock
|
36
|
+
worker.payload['uniq'] = worker.unique_job.to_hash
|
37
|
+
|
38
|
+
uniq_lock.lock
|
39
|
+
end
|
40
|
+
|
41
|
+
yield
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def unique_job_lock(worker:, service:)
|
47
|
+
return unless worker.unique_job?
|
48
|
+
|
49
|
+
digest = LockDigest.new(
|
50
|
+
*[service || worker.options[:service], worker.options[:queue]].compact,
|
51
|
+
across: worker.unique_job.across,
|
52
|
+
)
|
53
|
+
|
54
|
+
Lock.new(
|
55
|
+
digest: digest.to_s,
|
56
|
+
lock_id: unique_job_lock_id(worker),
|
57
|
+
ttl: worker.unique_job.ttl,
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def unique_job_lock_id(worker)
|
62
|
+
identifier_data = [worker.worker_class, worker.payload.fetch('args'.freeze, [])]
|
63
|
+
Digest::SHA256.hexdigest(
|
64
|
+
MultiJson.dump(identifier_data, mode: :compat),
|
65
|
+
)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|