canvas_sync 0.21.1 → 0.22.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/canvas_sync/concerns/auto_relations.rb +11 -0
- data/lib/canvas_sync/config.rb +3 -5
- data/lib/canvas_sync/generators/templates/models/rubric.rb +2 -1
- data/lib/canvas_sync/job_batches/batch.rb +432 -402
- data/lib/canvas_sync/job_batches/callback.rb +100 -114
- data/lib/canvas_sync/job_batches/chain_builder.rb +194 -196
- data/lib/canvas_sync/job_batches/{active_job.rb → compat/active_job.rb} +2 -2
- data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/helpers.rb +1 -1
- data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web.rb +3 -3
- data/lib/canvas_sync/job_batches/{sidekiq.rb → compat/sidekiq.rb} +35 -22
- data/lib/canvas_sync/job_batches/compat.rb +20 -0
- data/lib/canvas_sync/job_batches/context_hash.rb +124 -126
- data/lib/canvas_sync/job_batches/jobs/base_job.rb +2 -4
- data/lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb +14 -16
- data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +125 -127
- data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +14 -16
- data/lib/canvas_sync/job_batches/pool.rb +193 -195
- data/lib/canvas_sync/job_batches/redis_model.rb +50 -52
- data/lib/canvas_sync/job_batches/redis_script.rb +129 -131
- data/lib/canvas_sync/job_batches/status.rb +85 -87
- data/lib/canvas_sync/job_uniqueness/compat/active_job.rb +75 -0
- data/lib/canvas_sync/job_uniqueness/compat/sidekiq.rb +135 -0
- data/lib/canvas_sync/job_uniqueness/compat.rb +20 -0
- data/lib/canvas_sync/job_uniqueness/configuration.rb +25 -0
- data/lib/canvas_sync/job_uniqueness/job_uniqueness.rb +47 -0
- data/lib/canvas_sync/job_uniqueness/lock_context.rb +171 -0
- data/lib/canvas_sync/job_uniqueness/locksmith.rb +92 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict/base.rb +32 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict/log.rb +13 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict/null_strategy.rb +9 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict/raise.rb +11 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict/reject.rb +21 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict/reschedule.rb +20 -0
- data/lib/canvas_sync/job_uniqueness/on_conflict.rb +41 -0
- data/lib/canvas_sync/job_uniqueness/strategy/base.rb +104 -0
- data/lib/canvas_sync/job_uniqueness/strategy/until_and_while_executing.rb +35 -0
- data/lib/canvas_sync/job_uniqueness/strategy/until_executed.rb +20 -0
- data/lib/canvas_sync/job_uniqueness/strategy/until_executing.rb +20 -0
- data/lib/canvas_sync/job_uniqueness/strategy/until_expired.rb +16 -0
- data/lib/canvas_sync/job_uniqueness/strategy/while_executing.rb +26 -0
- data/lib/canvas_sync/job_uniqueness/strategy.rb +27 -0
- data/lib/canvas_sync/job_uniqueness/unique_job_common.rb +79 -0
- data/lib/canvas_sync/misc_helper.rb +1 -1
- data/lib/canvas_sync/version.rb +1 -1
- data/lib/canvas_sync.rb +4 -3
- data/spec/dummy/app/models/rubric.rb +2 -1
- data/spec/dummy/config/environments/test.rb +1 -1
- data/spec/job_batching/batch_spec.rb +49 -7
- data/spec/job_batching/{active_job_spec.rb → compat/active_job_spec.rb} +2 -2
- data/spec/job_batching/{sidekiq_spec.rb → compat/sidekiq_spec.rb} +14 -12
- data/spec/job_batching/flow_spec.rb +1 -1
- data/spec/job_batching/integration_helper.rb +1 -1
- data/spec/job_batching/status_spec.rb +2 -2
- data/spec/job_uniqueness/compat/active_job_spec.rb +49 -0
- data/spec/job_uniqueness/compat/sidekiq_spec.rb +68 -0
- data/spec/job_uniqueness/lock_context_spec.rb +95 -0
- data/spec/job_uniqueness/on_conflict/log_spec.rb +11 -0
- data/spec/job_uniqueness/on_conflict/raise_spec.rb +10 -0
- data/spec/job_uniqueness/on_conflict/reschedule_spec.rb +24 -0
- data/spec/job_uniqueness/on_conflict_spec.rb +16 -0
- data/spec/job_uniqueness/spec_helper.rb +14 -0
- data/spec/job_uniqueness/strategy/base_spec.rb +100 -0
- data/spec/job_uniqueness/strategy/until_and_while_executing_spec.rb +48 -0
- data/spec/job_uniqueness/strategy/until_executed_spec.rb +23 -0
- data/spec/job_uniqueness/strategy/until_executing_spec.rb +23 -0
- data/spec/job_uniqueness/strategy/until_expired_spec.rb +23 -0
- data/spec/job_uniqueness/strategy/while_executing_spec.rb +33 -0
- data/spec/job_uniqueness/support/lock_strategy.rb +28 -0
- data/spec/job_uniqueness/support/on_conflict.rb +24 -0
- data/spec/job_uniqueness/support/test_worker.rb +19 -0
- data/spec/job_uniqueness/unique_job_common_spec.rb +45 -0
- data/spec/spec_helper.rb +1 -1
- metadata +278 -204
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/batches_assets/css/styles.less +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/batches_assets/js/batch_tree.js +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/batches_assets/js/util.js +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_batch_tree.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_batches_table.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_common.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_jobs_table.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_pagination.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/batch.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/batches.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/pool.erb +0 -0
- /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/pools.erb +0 -0
@@ -0,0 +1,135 @@
|
|
1
|
+
|
2
|
+
module CanvasSync::JobUniqueness
|
3
|
+
module Compat
|
4
|
+
module Sidekiq
|
5
|
+
module WorkerExtension
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
include UniqueJobCommon
|
8
|
+
|
9
|
+
class_methods do
|
10
|
+
def ensure_uniqueness(**kwargs)
|
11
|
+
super(**kwargs)
|
12
|
+
if !(defined?(@@validated_config) && @@validated_config)
|
13
|
+
Compat::Sidekiq.validate_middleware_placement!()
|
14
|
+
@@validated_config = true
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class SidekiqLockContext < LockContext
|
21
|
+
def job_scheduled_at
|
22
|
+
@job_instance["at"]
|
23
|
+
end
|
24
|
+
|
25
|
+
def reenqueue(schedule_in:)
|
26
|
+
job_class.set(queue: job_queue.to_sym).perform_in(schedule_in, *@job_instance["args"])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class CommonMiddleware
|
31
|
+
def lock_context(msg)
|
32
|
+
opts = worker_uniqueness(msg)
|
33
|
+
return nil unless opts
|
34
|
+
|
35
|
+
SidekiqLockContext.new({
|
36
|
+
job_clazz: msg['class'],
|
37
|
+
jid: msg['jid'],
|
38
|
+
queue: msg['queue'],
|
39
|
+
args: msg['args'],
|
40
|
+
# kwargs: msg['kwargs'],
|
41
|
+
**(msg['uniqueness_cache_data'] || {}),
|
42
|
+
}, job_instance: msg)
|
43
|
+
end
|
44
|
+
|
45
|
+
def worker_uniqueness(msg)
|
46
|
+
return nil if Compat::Sidekiq.is_activejob_job?(msg)
|
47
|
+
|
48
|
+
worker_class = msg['class'].constantize
|
49
|
+
return nil unless worker_class.respond_to?(:unique_job_options)
|
50
|
+
|
51
|
+
worker_class.unique_job_options
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
class ClientMiddleware < CommonMiddleware
|
56
|
+
include ::Sidekiq::ClientMiddleware if defined? ::Sidekiq::ClientMiddleware
|
57
|
+
|
58
|
+
def call(_worker, msg, _queue, _redis_pool = nil, &blk)
|
59
|
+
ctx = lock_context(msg)
|
60
|
+
return blk.call unless ctx
|
61
|
+
msg['uniqueness_cache_data'] = ctx.cache_data
|
62
|
+
ctx.handle_lifecycle!(:enqueue, &blk)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class ServerMiddleware < CommonMiddleware
|
67
|
+
include ::Sidekiq::ServerMiddleware if defined? ::Sidekiq::ServerMiddleware
|
68
|
+
|
69
|
+
def call(_worker, msg, _queue, &blk)
|
70
|
+
ctx = lock_context(msg)
|
71
|
+
return blk.call unless ctx
|
72
|
+
ctx.handle_lifecycle!(:perform, &blk)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.is_activejob_job?(msg)
|
77
|
+
return false unless defined?(::ActiveJob)
|
78
|
+
|
79
|
+
msg['class'] == 'ActiveJob::QueueAdapters::SidekiqAdapter::JobWrapper' && (msg['wrapped'].to_s).constantize < Compat::ActiveJob::UniqueJobExtension
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.validate_middleware_order(chain, order)
|
83
|
+
chain_classes = chain.entries.map(&:klass)
|
84
|
+
filtered = chain_classes.select { |klass| order.include?(klass) }
|
85
|
+
raise "Middleware chain does not contain all required middleware: #{order - filtered}" unless order.all? { |klass| filtered.include?(klass) }
|
86
|
+
raise "Middleware must be in order: #{order.inspect}" if filtered != order
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.sidekiq_middleware(placement, &blk)
|
90
|
+
install_middleware = ->(config) do
|
91
|
+
config.send("#{placement}_middleware") do |chain|
|
92
|
+
blk.call(chain)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
::Sidekiq.configure_client(&install_middleware) if placement == :client
|
97
|
+
::Sidekiq.configure_server(&install_middleware)
|
98
|
+
end
|
99
|
+
|
100
|
+
def self.validate_middleware_placement!
|
101
|
+
sidekiq_middleware(:client) do |chain|
|
102
|
+
# Unique middleware must come _before_ the Batch middleware so that the uniqueness middleware can wrap the job in a batch
|
103
|
+
validate_middleware_order(chain, [
|
104
|
+
CanvasSync::JobUniqueness::Compat::Sidekiq::ClientMiddleware,
|
105
|
+
CanvasSync::JobBatches::Compat::Sidekiq::ClientMiddleware,
|
106
|
+
])
|
107
|
+
end
|
108
|
+
|
109
|
+
sidekiq_middleware(:server) do |chain|
|
110
|
+
# Unique middleware must com _after_ the Batch middleware so that the Batch is loaded before reaching the uniqueness middleware
|
111
|
+
validate_middleware_order(chain, [
|
112
|
+
CanvasSync::JobBatches::Compat::Sidekiq::ServerMiddleware,
|
113
|
+
CanvasSync::JobUniqueness::Compat::Sidekiq::ServerMiddleware,
|
114
|
+
])
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.configure
|
119
|
+
sidekiq_middleware(:client) do |chain|
|
120
|
+
chain.insert_before CanvasSync::JobBatches::Compat::Sidekiq::ClientMiddleware, Compat::Sidekiq::ClientMiddleware
|
121
|
+
end
|
122
|
+
|
123
|
+
sidekiq_middleware(:server) do |chain|
|
124
|
+
chain.insert_after CanvasSync::JobBatches::Compat::Sidekiq::ServerMiddleware, Compat::Sidekiq::ServerMiddleware
|
125
|
+
end
|
126
|
+
|
127
|
+
::Sidekiq::Worker.extend(ActiveSupport::Concern) unless ::Sidekiq::Worker < ActiveSupport::Concern
|
128
|
+
|
129
|
+
::Sidekiq::Worker.send(:include, Compat::Sidekiq::WorkerExtension)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# require_relative 'sidekiq/web'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
module CanvasSync::JobUniqueness
|
3
|
+
module Compat
|
4
|
+
def self.load_compat(name)
|
5
|
+
name = name.to_s
|
6
|
+
begin
|
7
|
+
require name
|
8
|
+
rescue LoadError
|
9
|
+
end
|
10
|
+
|
11
|
+
if name.classify.safe_constantize
|
12
|
+
require_relative "./compat/#{name}"
|
13
|
+
"#{self.name}::#{name.classify}".constantize.configure
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
load_compat(:active_job)
|
18
|
+
load_compat(:sidekiq)
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module CanvasSync::JobUniqueness
|
2
|
+
class Configuration
|
3
|
+
include ActiveSupport::Configurable
|
4
|
+
|
5
|
+
config_accessor(:lock_ttl) { 14.days.to_i }
|
6
|
+
config_accessor(:lock_prefix) { 'uniquejob' }
|
7
|
+
config_accessor(:on_conflict) { :raise }
|
8
|
+
|
9
|
+
config_accessor(:lock_strategies) { {} }
|
10
|
+
config_accessor(:conflict_strategies) { {} }
|
11
|
+
|
12
|
+
config_accessor(:redis_version) { nil }
|
13
|
+
def on_conflict=(action)
|
14
|
+
validate_on_conflict_action!(action)
|
15
|
+
|
16
|
+
config.on_conflict = action
|
17
|
+
end
|
18
|
+
|
19
|
+
def validate_on_conflict_action!(action)
|
20
|
+
return if action.nil? || %i[log raise].include?(action) || action.respond_to?(:call)
|
21
|
+
|
22
|
+
raise ActiveJob::Uniqueness::InvalidOnConflictAction, "Unexpected '#{action}' action on conflict"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
|
2
|
+
require 'canvas_sync/job_uniqueness/lock_context'
|
3
|
+
require 'canvas_sync/job_uniqueness/unique_job_common'
|
4
|
+
|
5
|
+
require 'canvas_sync/job_uniqueness/strategy'
|
6
|
+
require 'canvas_sync/job_uniqueness/on_conflict'
|
7
|
+
require 'canvas_sync/job_uniqueness/compat'
|
8
|
+
|
9
|
+
module CanvasSync::JobUniqueness
|
10
|
+
extend ActiveSupport::Autoload
|
11
|
+
|
12
|
+
autoload :Locksmith
|
13
|
+
autoload :Configuration
|
14
|
+
|
15
|
+
class Conflict < StandardError
|
16
|
+
attr_reader :lock_context
|
17
|
+
|
18
|
+
def initialize(lock_context)
|
19
|
+
super()
|
20
|
+
@lock_context = lock_context
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class CouldNotLockError < StandardError
|
25
|
+
attr_reader :lock_context, :source
|
26
|
+
|
27
|
+
def initialize(lock_context, source:)
|
28
|
+
super()
|
29
|
+
@lock_context = lock_context
|
30
|
+
@source = source
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class << self
|
35
|
+
def configure
|
36
|
+
yield config
|
37
|
+
end
|
38
|
+
|
39
|
+
def config
|
40
|
+
@config ||= Configuration.new
|
41
|
+
end
|
42
|
+
|
43
|
+
def logger
|
44
|
+
CanvasSync.logger
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
|
2
|
+
module CanvasSync::JobUniqueness
|
3
|
+
class LockContext
|
4
|
+
def self.from_serialized(data, **kwargs)
|
5
|
+
context_class = data[:clazz]&.constantize || self
|
6
|
+
context_class.new(data, **kwargs)
|
7
|
+
end
|
8
|
+
|
9
|
+
# { job_clazz, jid, queue, args?, kwargs?, base_key? }
|
10
|
+
def initialize(data, job_instance: nil, config: nil)
|
11
|
+
@base_key = data[:base_key]
|
12
|
+
@context_data = data
|
13
|
+
@job_instance = job_instance
|
14
|
+
@config = config || @context_data[:config]
|
15
|
+
end
|
16
|
+
|
17
|
+
# This is primarily for rehydrating in a Batch Callback, so it is unlikely that args and kwargs are needed.
|
18
|
+
# Honestly, base_key and job_clazz are probably the only needed values
|
19
|
+
def serialize
|
20
|
+
{
|
21
|
+
clazz: self.class.to_s,
|
22
|
+
job_clazz: @context_data[:job_clazz].to_s,
|
23
|
+
jid: @context_data[:jid],
|
24
|
+
queue: @context_data[:queue],
|
25
|
+
**cache_data,
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Properties to cache on the serialized Job object to prevent issues arising from code changes between enqueue and perform
|
30
|
+
def cache_data
|
31
|
+
{
|
32
|
+
base_key: base_key,
|
33
|
+
job_score: job_score,
|
34
|
+
# TODO Should config also be cached on the Job at time of enqueue?
|
35
|
+
# config: config,
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
def debug_data
|
40
|
+
{
|
41
|
+
context_class: self.class.to_s,
|
42
|
+
job_class: job_class.to_s,
|
43
|
+
queue: job_queue,
|
44
|
+
limit: config[:limit],
|
45
|
+
timeout: config[:timeout],
|
46
|
+
ttl: config[:ttl],
|
47
|
+
strategy: config[:strategy],
|
48
|
+
time: Time.now.to_f,
|
49
|
+
at: job_scheduled_at,
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
def handle_lifecycle!(stage, *args, **kwargs, &blk)
|
54
|
+
lock_strategy.send(:"on_#{stage}", *args, **kwargs, &blk)
|
55
|
+
rescue CouldNotLockError => e
|
56
|
+
call_conflict_strategy(stage)
|
57
|
+
end
|
58
|
+
|
59
|
+
def lock_strategy
|
60
|
+
return @lock_strategy if defined?(@lock_strategy)
|
61
|
+
|
62
|
+
strat_name = config[:strategy]
|
63
|
+
@lock_strategy = Strategy.lookup(strat_name).new(self)
|
64
|
+
end
|
65
|
+
|
66
|
+
def config
|
67
|
+
@config ||= job_class.unique_job_options
|
68
|
+
end
|
69
|
+
|
70
|
+
def job_class
|
71
|
+
@job_class ||= begin
|
72
|
+
if (job_clazz = @context_data[:job_clazz]).is_a?(String)
|
73
|
+
job_clazz.constantize
|
74
|
+
else
|
75
|
+
job_clazz
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def job_id
|
81
|
+
@context_data[:jid]
|
82
|
+
end
|
83
|
+
|
84
|
+
def job_queue
|
85
|
+
@context_data[:queue]
|
86
|
+
end
|
87
|
+
|
88
|
+
def job_scheduled_at
|
89
|
+
nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def job_score
|
93
|
+
@context_data[:job_score] || job_scheduled_at.to_s
|
94
|
+
end
|
95
|
+
|
96
|
+
def base_key(any_hash: false)
|
97
|
+
@base_key ||= begin
|
98
|
+
queue = @context_data[:queue] || "default"
|
99
|
+
|
100
|
+
base_key = [
|
101
|
+
CanvasSync::JobUniqueness.config.lock_prefix.presence,
|
102
|
+
].compact
|
103
|
+
|
104
|
+
scope = config[:scope]
|
105
|
+
if scope.is_a?(Proc)
|
106
|
+
base_key << scope.call(queue: queue)
|
107
|
+
elsif scope == :global
|
108
|
+
base_key << job_class.name
|
109
|
+
elsif scope == :per_queue
|
110
|
+
base_key << job_class.name
|
111
|
+
base_key << queue
|
112
|
+
else
|
113
|
+
base_key << scope
|
114
|
+
end
|
115
|
+
|
116
|
+
args = @context_data[:args] || []
|
117
|
+
kwargs = @context_data[:kwargs] || {}
|
118
|
+
if config[:hash].is_a?(Proc)
|
119
|
+
base_key << config[:hash].call(*args, **kwargs)
|
120
|
+
elsif config[:hash].nil?
|
121
|
+
base_key << OpenSSL::Digest::MD5.hexdigest(JSON.dump([ args, kwargs.sort]))
|
122
|
+
else
|
123
|
+
base_key << config[:hash]
|
124
|
+
end
|
125
|
+
|
126
|
+
base_key.join(":")
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def reenqueue
|
131
|
+
raise NotImplementedError, "needs to be implemented in child class"
|
132
|
+
end
|
133
|
+
|
134
|
+
protected
|
135
|
+
|
136
|
+
attr_reader :job_instance
|
137
|
+
|
138
|
+
#
|
139
|
+
# Call whatever strategy that has been configured
|
140
|
+
#
|
141
|
+
# @param [Symbol] origin the origin `:client` or `:server`
|
142
|
+
#
|
143
|
+
# @return [void] the return value is irrelevant
|
144
|
+
#
|
145
|
+
# @yieldparam [void] if a new job id was set and a block is given
|
146
|
+
# @yieldreturn [void] the yield is irrelevant, it only provides a mechanism in
|
147
|
+
# one specific situation to yield back to the middleware.
|
148
|
+
def call_conflict_strategy(origin)
|
149
|
+
new_job_id = nil
|
150
|
+
strategy = conflict_strategy_for(origin)
|
151
|
+
|
152
|
+
strategy.call
|
153
|
+
end
|
154
|
+
|
155
|
+
def conflict_strategy_for(origin)
|
156
|
+
raise ArgumentError, "#origin needs to be either `:enqueue` or `:perform`" unless %i[enqueue perform].include?(origin)
|
157
|
+
|
158
|
+
@conflict_strategies ||= {}
|
159
|
+
@conflict_strategies[origin] ||= begin
|
160
|
+
cfg = config[:on_conflict]
|
161
|
+
cfg = cfg[origin] if cfg.is_a?(Hash)
|
162
|
+
|
163
|
+
cstrat_cls = OnConflict.lookup(cfg || :null_strategy)
|
164
|
+
cstrat = cstrat_cls.new(self) # TODO Pass redis_pool?
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sidekiq_unique_jobs"
|
4
|
+
|
5
|
+
module CanvasSync::JobUniqueness
|
6
|
+
# This class is intended to be the complete translation layer between CanvasSync::JobUniqueness and SidekiqUniqueJobs.
|
7
|
+
# In other words, you could consider it the "locking backend" and thus could potentially swap out SUJ for a more succinct solution.
|
8
|
+
#
|
9
|
+
# SUJ's implementation is somewhat complex, but is somewhat pre-tailored over (eg) https://github.com/leandromoreira/redlock-rb.
|
10
|
+
# Mainly SUJ tracks the JID so that if a process dies, another can pick up the job without having to figure out how to unlock it.
|
11
|
+
# SUJ also handles the integration into Sidekiq Web, which is a nice bonus.
|
12
|
+
class Locksmith < SidekiqUniqueJobs::Locksmith
|
13
|
+
attr_reader :lock_context
|
14
|
+
|
15
|
+
def initialize(key, lock_context, redis_pool = nil)
|
16
|
+
@lock_context = lock_context
|
17
|
+
@job_id = lock_context.job_id
|
18
|
+
@item = lock_context
|
19
|
+
@key = SidekiqUniqueJobs::Key.new(key)
|
20
|
+
|
21
|
+
lcfg = lock_context.config
|
22
|
+
@config = OpenStruct.new({
|
23
|
+
:"type" => lcfg[:strategy],
|
24
|
+
:"pttl" => lcfg[:ttl] * 1000,
|
25
|
+
:"timeout" => lcfg[:timeout],
|
26
|
+
:"wait_for_lock?" => lcfg[:ttl]&.positive?,
|
27
|
+
:"lock_info" => false,
|
28
|
+
:"limit" => lcfg[:limit],
|
29
|
+
})
|
30
|
+
|
31
|
+
@redis_pool = redis_pool
|
32
|
+
end
|
33
|
+
|
34
|
+
def locked_jids
|
35
|
+
SidekiqUniqueJobs::Lock.new(@key).locked_jids
|
36
|
+
end
|
37
|
+
|
38
|
+
def swap_locks(old_jid)
|
39
|
+
olimit = lock_context.config[:limit]
|
40
|
+
new_jid = @job_id
|
41
|
+
return if old_jid == new_jid
|
42
|
+
|
43
|
+
# NB This is quite hacky, but should work
|
44
|
+
#
|
45
|
+
# Ideally the unlock(old) and lock(new) would be atomic, but that increases the amount of coupling with Sidekiq-Unique-Jobs - right now,
|
46
|
+
# we're using fairly stable (though still internal) SUJ APIs; I fear that writing custom Lua will be significantly more brittle
|
47
|
+
#
|
48
|
+
# In the general case, we'd only bump limit by 1, but that leaves a potential race-condition when limit is configured > 1:
|
49
|
+
# (Assuming until_and_while_executing, reschedule, limit = 2):
|
50
|
+
# (Workers are performing 2 Jobs, RUN lock count = 2)
|
51
|
+
# Worker 1 pulls Job A
|
52
|
+
# Worker 2 pulls Job B
|
53
|
+
# W1 and W2 both fail to get the runtime lock
|
54
|
+
# W1 and W2 call swap_locks
|
55
|
+
# W1 calls lock(limit+1), lock is granted, lock count becomes limit+1
|
56
|
+
# W2 calls lock(limit+1), lock is denied because count would be limit+2
|
57
|
+
# W1 calls unlock(old_jid)
|
58
|
+
|
59
|
+
# Force creation of another lock, ignoring the limit
|
60
|
+
@config.limit = olimit + 100
|
61
|
+
result = lock
|
62
|
+
|
63
|
+
# Release the old lock, bringing us back within the limit
|
64
|
+
@job_id = old_jid
|
65
|
+
unlock
|
66
|
+
|
67
|
+
result
|
68
|
+
ensure
|
69
|
+
@config.limit = olimit
|
70
|
+
@job_id = new_jid
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def lock_score
|
76
|
+
lock_context.job_score
|
77
|
+
end
|
78
|
+
|
79
|
+
def lock_info
|
80
|
+
@lock_info = JSON.dump(lock_context.debug_data)
|
81
|
+
end
|
82
|
+
|
83
|
+
# def taken?(conn)
|
84
|
+
# v = conn.hexists(key.locked, job_id)
|
85
|
+
# v.is_a?(Numeric) ? v != 0 : v
|
86
|
+
# end
|
87
|
+
|
88
|
+
def redis_version
|
89
|
+
@redis_version ||= CanvasSync::JobUniqueness.config.redis_version || SidekiqUniqueJobs.fetch_redis_version
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module CanvasSync::JobUniqueness
|
2
|
+
module OnConflict
|
3
|
+
class Base
|
4
|
+
attr_reader :lock_context
|
5
|
+
attr_reader :redis_pool
|
6
|
+
|
7
|
+
class_attribute :_valid_for, instance_writer: false
|
8
|
+
|
9
|
+
def self.valid_for(*origins)
|
10
|
+
if origins.present?
|
11
|
+
orgins = Array(origins).map(&:to_sym)
|
12
|
+
self._valid_for = origins
|
13
|
+
else
|
14
|
+
self._valid_for || [:enqueue, :perform]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(lock_context, redis_pool = nil)
|
19
|
+
@lock_context = lock_context
|
20
|
+
@redis_pool = redis_pool
|
21
|
+
end
|
22
|
+
|
23
|
+
def call
|
24
|
+
raise NotImplementedError, "needs to be implemented in child class"
|
25
|
+
end
|
26
|
+
|
27
|
+
def replace?
|
28
|
+
is_a?(Replace)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module CanvasSync::JobUniqueness
|
2
|
+
module OnConflict
|
3
|
+
class Log < Base
|
4
|
+
valid_for :enqueue, :perform
|
5
|
+
|
6
|
+
def call
|
7
|
+
CanvasSync::JobUniqueness.logger.info(<<~MESSAGE.chomp)
|
8
|
+
Skipping job with id (#{lock_context.job_id}) because key: (#{lock_context.base_key}) is locked
|
9
|
+
MESSAGE
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module CanvasSync::JobUniqueness
|
2
|
+
module OnConflict
|
3
|
+
class Reject < Base
|
4
|
+
valid_for :perform
|
5
|
+
|
6
|
+
def call
|
7
|
+
# TODO Allow this to work on Sidekiq-backed ActiveJob
|
8
|
+
unless lock_context.is_a?(CanvasSync::JobUniqueness::Compat::Sidekiq::SidekiqLockContext)
|
9
|
+
CanvasSync::JobUniqueness.logger.error(":reject conflict strategy is not supported for non-Sidekiq-backed jobs")
|
10
|
+
return
|
11
|
+
end
|
12
|
+
|
13
|
+
kwargs = {}
|
14
|
+
kwargs[:notify_failure] = false if Sidekiq::DeadSet.instance_method(:kill).arity > 1
|
15
|
+
|
16
|
+
sidekiq_message = lock_context.instance_variable_get(:@job_instance)
|
17
|
+
Sidekiq::DeadSet.new.kill(JSON.dump(sidekiq_message), **kwargs)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module CanvasSync::JobUniqueness
|
2
|
+
module OnConflict
|
3
|
+
class Reschedule < OnConflict::Base
|
4
|
+
valid_for :perform
|
5
|
+
|
6
|
+
def call
|
7
|
+
Thread.current[:unique_jobs_previous_jid] = lock_context.job_id
|
8
|
+
rescheduled = lock_context.reenqueue(
|
9
|
+
schedule_in: schedule_in,
|
10
|
+
)
|
11
|
+
ensure
|
12
|
+
Thread.current[:unique_jobs_previous_jid] = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def schedule_in
|
16
|
+
lock_context.config[:reschedule_in] || 5
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module CanvasSync::JobUniqueness
|
2
|
+
module OnConflict
|
3
|
+
extend ActiveSupport::Autoload
|
4
|
+
|
5
|
+
autoload :Base
|
6
|
+
|
7
|
+
autoload :Log
|
8
|
+
autoload :NullStrategy
|
9
|
+
autoload :Raise
|
10
|
+
autoload :Reject
|
11
|
+
# autoload :Replace
|
12
|
+
autoload :Reschedule
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def lookup(strategy)
|
16
|
+
matching_strategy(strategy.to_s.camelize) ||
|
17
|
+
CanvasSync::JobUniqueness.config.conflict_strategies[strategy] ||
|
18
|
+
raise(StrategyNotFound, "on_conflict: #{strategy} is not found. Is it declared in the configuration?")
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate!(on_conflict, lock_strategy)
|
22
|
+
on_conflict = { enqueue: on_conflict, perform: on_conflict } unless on_conflict.is_a?(Hash)
|
23
|
+
|
24
|
+
lock_strategy = Strategy.lookup(lock_strategy) if lock_strategy.is_a?(Symbol)
|
25
|
+
on_conflict.each do |origin, strategy|
|
26
|
+
strategy = OnConflict.lookup(strategy) if strategy.is_a?(Symbol)
|
27
|
+
|
28
|
+
if lock_strategy.locks_on.include?(origin) && !strategy.valid_for.include?(origin)
|
29
|
+
raise ArgumentError, "(#{origin.to_s.titleize}) conflict strategy #{strategy.name.underscore} is not valid for lock strategy #{lock_strategy.name.underscore}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def matching_strategy(const)
|
37
|
+
const_get(const, false) if const_defined?(const, false)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|