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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/lib/canvas_sync/concerns/auto_relations.rb +11 -0
  3. data/lib/canvas_sync/config.rb +3 -5
  4. data/lib/canvas_sync/generators/templates/models/rubric.rb +2 -1
  5. data/lib/canvas_sync/job_batches/batch.rb +432 -402
  6. data/lib/canvas_sync/job_batches/callback.rb +100 -114
  7. data/lib/canvas_sync/job_batches/chain_builder.rb +194 -196
  8. data/lib/canvas_sync/job_batches/{active_job.rb → compat/active_job.rb} +2 -2
  9. data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/helpers.rb +1 -1
  10. data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web.rb +3 -3
  11. data/lib/canvas_sync/job_batches/{sidekiq.rb → compat/sidekiq.rb} +35 -22
  12. data/lib/canvas_sync/job_batches/compat.rb +20 -0
  13. data/lib/canvas_sync/job_batches/context_hash.rb +124 -126
  14. data/lib/canvas_sync/job_batches/jobs/base_job.rb +2 -4
  15. data/lib/canvas_sync/job_batches/jobs/concurrent_batch_job.rb +14 -16
  16. data/lib/canvas_sync/job_batches/jobs/managed_batch_job.rb +125 -127
  17. data/lib/canvas_sync/job_batches/jobs/serial_batch_job.rb +14 -16
  18. data/lib/canvas_sync/job_batches/pool.rb +193 -195
  19. data/lib/canvas_sync/job_batches/redis_model.rb +50 -52
  20. data/lib/canvas_sync/job_batches/redis_script.rb +129 -131
  21. data/lib/canvas_sync/job_batches/status.rb +85 -87
  22. data/lib/canvas_sync/job_uniqueness/compat/active_job.rb +75 -0
  23. data/lib/canvas_sync/job_uniqueness/compat/sidekiq.rb +135 -0
  24. data/lib/canvas_sync/job_uniqueness/compat.rb +20 -0
  25. data/lib/canvas_sync/job_uniqueness/configuration.rb +25 -0
  26. data/lib/canvas_sync/job_uniqueness/job_uniqueness.rb +47 -0
  27. data/lib/canvas_sync/job_uniqueness/lock_context.rb +171 -0
  28. data/lib/canvas_sync/job_uniqueness/locksmith.rb +92 -0
  29. data/lib/canvas_sync/job_uniqueness/on_conflict/base.rb +32 -0
  30. data/lib/canvas_sync/job_uniqueness/on_conflict/log.rb +13 -0
  31. data/lib/canvas_sync/job_uniqueness/on_conflict/null_strategy.rb +9 -0
  32. data/lib/canvas_sync/job_uniqueness/on_conflict/raise.rb +11 -0
  33. data/lib/canvas_sync/job_uniqueness/on_conflict/reject.rb +21 -0
  34. data/lib/canvas_sync/job_uniqueness/on_conflict/reschedule.rb +20 -0
  35. data/lib/canvas_sync/job_uniqueness/on_conflict.rb +41 -0
  36. data/lib/canvas_sync/job_uniqueness/strategy/base.rb +104 -0
  37. data/lib/canvas_sync/job_uniqueness/strategy/until_and_while_executing.rb +35 -0
  38. data/lib/canvas_sync/job_uniqueness/strategy/until_executed.rb +20 -0
  39. data/lib/canvas_sync/job_uniqueness/strategy/until_executing.rb +20 -0
  40. data/lib/canvas_sync/job_uniqueness/strategy/until_expired.rb +16 -0
  41. data/lib/canvas_sync/job_uniqueness/strategy/while_executing.rb +26 -0
  42. data/lib/canvas_sync/job_uniqueness/strategy.rb +27 -0
  43. data/lib/canvas_sync/job_uniqueness/unique_job_common.rb +79 -0
  44. data/lib/canvas_sync/misc_helper.rb +1 -1
  45. data/lib/canvas_sync/version.rb +1 -1
  46. data/lib/canvas_sync.rb +4 -3
  47. data/spec/dummy/app/models/rubric.rb +2 -1
  48. data/spec/dummy/config/environments/test.rb +1 -1
  49. data/spec/job_batching/batch_spec.rb +49 -7
  50. data/spec/job_batching/{active_job_spec.rb → compat/active_job_spec.rb} +2 -2
  51. data/spec/job_batching/{sidekiq_spec.rb → compat/sidekiq_spec.rb} +14 -12
  52. data/spec/job_batching/flow_spec.rb +1 -1
  53. data/spec/job_batching/integration_helper.rb +1 -1
  54. data/spec/job_batching/status_spec.rb +2 -2
  55. data/spec/job_uniqueness/compat/active_job_spec.rb +49 -0
  56. data/spec/job_uniqueness/compat/sidekiq_spec.rb +68 -0
  57. data/spec/job_uniqueness/lock_context_spec.rb +95 -0
  58. data/spec/job_uniqueness/on_conflict/log_spec.rb +11 -0
  59. data/spec/job_uniqueness/on_conflict/raise_spec.rb +10 -0
  60. data/spec/job_uniqueness/on_conflict/reschedule_spec.rb +24 -0
  61. data/spec/job_uniqueness/on_conflict_spec.rb +16 -0
  62. data/spec/job_uniqueness/spec_helper.rb +14 -0
  63. data/spec/job_uniqueness/strategy/base_spec.rb +100 -0
  64. data/spec/job_uniqueness/strategy/until_and_while_executing_spec.rb +48 -0
  65. data/spec/job_uniqueness/strategy/until_executed_spec.rb +23 -0
  66. data/spec/job_uniqueness/strategy/until_executing_spec.rb +23 -0
  67. data/spec/job_uniqueness/strategy/until_expired_spec.rb +23 -0
  68. data/spec/job_uniqueness/strategy/while_executing_spec.rb +33 -0
  69. data/spec/job_uniqueness/support/lock_strategy.rb +28 -0
  70. data/spec/job_uniqueness/support/on_conflict.rb +24 -0
  71. data/spec/job_uniqueness/support/test_worker.rb +19 -0
  72. data/spec/job_uniqueness/unique_job_common_spec.rb +45 -0
  73. data/spec/spec_helper.rb +1 -1
  74. metadata +278 -204
  75. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/batches_assets/css/styles.less +0 -0
  76. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/batches_assets/js/batch_tree.js +0 -0
  77. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/batches_assets/js/util.js +0 -0
  78. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_batch_tree.erb +0 -0
  79. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_batches_table.erb +0 -0
  80. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_common.erb +0 -0
  81. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_jobs_table.erb +0 -0
  82. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/_pagination.erb +0 -0
  83. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/batch.erb +0 -0
  84. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/batches.erb +0 -0
  85. /data/lib/canvas_sync/job_batches/{sidekiq → compat/sidekiq}/web/views/pool.erb +0 -0
  86. /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,9 @@
1
+ module CanvasSync::JobUniqueness
2
+ module OnConflict
3
+ class NullStrategy < Base
4
+ valid_for :enqueue, :perform
5
+
6
+ def call; end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ module CanvasSync::JobUniqueness
2
+ module OnConflict
3
+ class Raise < Base
4
+ valid_for :enqueue, :perform
5
+
6
+ def call
7
+ raise Conflict, lock_context
8
+ end
9
+ end
10
+ end
11
+ 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