Class: CanvasSync::JobUniqueness::Locksmith

Inherits:
SidekiqUniqueJobs::Locksmith
  • Object
show all
Defined in:
lib/canvas_sync/job_uniqueness/locksmith.rb

Overview

This class is intended to be the complete translation layer between CanvasSync::JobUniqueness and SidekiqUniqueJobs. In other words, you could consider it the “locking backend” and thus could potentially swap out SUJ for a more succinct solution.

SUJ’s implementation is somewhat complex, but is somewhat pre-tailored over (eg) github.com/leandromoreira/redlock-rb. 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. SUJ also handles the integration into Sidekiq Web, which is a nice bonus.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(key, lock_context, redis_pool = nil) ⇒ Locksmith

Returns a new instance of Locksmith.



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/canvas_sync/job_uniqueness/locksmith.rb', line 15

def initialize(key, lock_context, redis_pool = nil)
  @lock_context = lock_context
  @job_id = lock_context.lock_id # Yes, .lock_id is intentional
  @item = lock_context
  @key = SidekiqUniqueJobs::Key.new(key)

  lcfg = lock_context.config
  @config = OpenStruct.new({
    :"type" => lcfg[:strategy],
    :"pttl" => lcfg[:ttl] * 1000,
    :"timeout" => lcfg[:timeout],
    :"wait_for_lock?" => lcfg[:ttl]&.positive?,
    :"lock_info" => false,
    :"limit" => lcfg[:limit],
  })

  @redis_pool = redis_pool
end

Instance Attribute Details

#lock_contextObject (readonly)

Returns the value of attribute lock_context.



13
14
15
# File 'lib/canvas_sync/job_uniqueness/locksmith.rb', line 13

def lock_context
  @lock_context
end

Instance Method Details

#locked_jidsObject



34
35
36
# File 'lib/canvas_sync/job_uniqueness/locksmith.rb', line 34

def locked_jids
  SidekiqUniqueJobs::Lock.new(@key).locked_jids
end

#swap_locks(old_jid) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/canvas_sync/job_uniqueness/locksmith.rb', line 38

def swap_locks(old_jid)
  olimit = lock_context.config[:limit]
  new_jid = @job_id
  return if old_jid == new_jid

  # NB This is quite hacky, but should work
  #
  # Ideally the unlock(old) and lock(new) would be atomic, but that increases the amount of coupling with Sidekiq-Unique-Jobs - right now,
  #   we're using fairly stable (though still internal) SUJ APIs; I fear that writing custom Lua will be significantly more brittle
  #
  # In the general case, we'd only bump limit by 1, but that leaves a potential race-condition when limit is configured > 1:
  # (Assuming until_and_while_executing, reschedule, limit = 2):
  #   (Workers are performing 2 Jobs, RUN lock count = 2)
  #   Worker 1 pulls Job A
  #   Worker 2 pulls Job B
  #   W1 and W2 both fail to get the runtime lock
  #   W1 and W2 call swap_locks
  #   W1 calls lock(limit+1), lock is granted, lock count becomes limit+1
  #   W2 calls lock(limit+1), lock is denied because count would be limit+2
  #   W1 calls unlock(old_jid)

  # Force creation of another lock, ignoring the limit
  @config.limit = olimit + 100
  result = lock

  # Release the old lock, bringing us back within the limit
  @job_id = old_jid
  unlock

  result
ensure
  @config.limit = olimit
  @job_id = new_jid
end