multi-background-job 0.1.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/.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
|