canvas_sync 0.21.1 → 0.22.0.beta1
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 +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
|