Class: Redstruct::Lock

Inherits:
Factory::Object show all
Includes:
Utils::Coercion, Utils::Scriptable
Defined in:
lib/redstruct/lock.rb

Overview

Implementation of a simple binary lock (locked/not locked), with option to block and wait for the lock. Uses two redis structures: a string for the lease, and a list for blocking operations.

Constant Summary collapse

DEFAULT_EXPIRY =

The default expiry on the underlying redis keys, in seconds; can be between 0 and 1 as a float for milliseconds

1
DEFAULT_TIMEOUT =

The default timeout when blocking, in seconds

nil

Instance Attribute Summary collapse

Attributes inherited from Factory::Object

#factory

Instance Method Summary collapse

Methods included from Utils::Coercion

coerce_array, coerce_bool

Methods included from Utils::Scriptable

included

Methods inherited from Factory::Object

#connection

Methods included from Utils::Inspectable

#inspect

Constructor Details

#initialize(resource, expiry: DEFAULT_EXPIRY, timeout: DEFAULT_TIMEOUT, **options) ⇒ Lock

Returns a new instance of Lock.

Parameters:

  • resource (String)

    the name of the resource to be locked (or ID)

  • expiry (Integer) (defaults to: DEFAULT_EXPIRY)

    in seconds; to prevent infinite locking, you should pass a minimum expiry; you can pass 0 if you want to control it yourself

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT)

    in seconds; if > 0, will block when trying to obtain the lock; if 0, blocks indefinitely; if nil, does not block



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/redstruct/lock.rb', line 37

def initialize(resource, expiry: DEFAULT_EXPIRY, timeout: DEFAULT_TIMEOUT, **options)
  super(**options)

  @resource = resource
  @token = nil
  @expiry = expiry
  @acquired = Redstruct::Utils::AtomicCounter.new

  @timeout = case timeout
  when nil then nil
  when Float::INFINITY then 0
  else
    timeout.to_i
  end

  factory = @factory.factory(@resource)
  @lease = factory.string('lease')
  @tokens = factory.list('tokens')
end

Instance Attribute Details

#expiryFloat, Integer (readonly)

Returns the expiry of the underlying redis structure in seconds.

Returns:

  • (Float, Integer)

    the expiry of the underlying redis structure in seconds



29
30
31
# File 'lib/redstruct/lock.rb', line 29

def expiry
  @expiry
end

#resourceString (readonly)

Returns the resource name (or ID of the lock).

Returns:

  • (String)

    the resource name (or ID of the lock)



23
24
25
# File 'lib/redstruct/lock.rb', line 23

def resource
  @resource
end

#timeoutInteger (readonly)

Returns if greater than 0, will block until timeout is reached or the lock is acquired.

Returns:

  • (Integer)

    if greater than 0, will block until timeout is reached or the lock is acquired



32
33
34
# File 'lib/redstruct/lock.rb', line 32

def timeout
  @timeout
end

#tokenString (readonly)

Returns the current token.

Returns:

  • (String)

    the current token



26
27
28
# File 'lib/redstruct/lock.rb', line 26

def token
  @token
end

Instance Method Details

#acquireBoolean

Attempts to acquire the lock. First attempts to grab the lease (a redis string). If the current token is already the lease token, the lock is considered acquired. If there is no current lease, then sets it to the current token. If there is a current lease that is not the current token, then:

1) If this not a blocking lock (see Lock#blocking?), return false
2) If this is a blocking lock, block and wait for the next token to be pushed on the tokens list
3) If a token was pushed, set it as our token and refresh the expiry

Returns:

  • (Boolean)

    True if acquired, false otherwise



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
# File 'lib/redstruct/lock.rb', line 93

def acquire
  acquired = false

  token = non_blocking_acquire
  token = blocking_acquire if token.nil? && blocking?

  unless token.nil?
    @lease.expire(@expiry)
    @token = token
    @acquired.increment

    acquired = true
  end

  return acquired
end

#blocking?Boolean

Whether or not the lock will block when attempting to acquire it

Returns:

  • (Boolean)


81
82
83
# File 'lib/redstruct/lock.rb', line 81

def blocking?
  return !@timeout.nil?
end

#deleteBoolean

Deletes all traces of this lock

Returns:

  • (Boolean)

    true if deleted, false otherwise



59
60
61
# File 'lib/redstruct/lock.rb', line 59

def delete
  return coerce_bool(delete_script(keys: [@lease.key, @tokens.key]))
end

#locked { ... } ⇒ Object

Executes the given block if the lock can be acquired

Yields:

  • Block to be executed if the lock is acquired



65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/redstruct/lock.rb', line 65

def locked
  Thread.handle_interrupt(Exception => :never) do
    begin
      if acquire
        Thread.handle_interrupt(Exception => :immediate) do
          yield
        end
      end
    ensure
      release
    end
  end
end

#releaseBoolean

Releases the lock only if the current token is the value of the lease. If the lock is a blocking lock (see Lock#blocking?), push the next token on the tokens list.

Returns:

  • (Boolean)

    True if released, false otherwise



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/redstruct/lock.rb', line 113

def release
  return false if @token.nil?

  released = true

  if @acquired.decrement.zero?
    keys = [@lease.key, @tokens.key]
    argv = [@token, generate_token, (@expiry.to_f * 1000).floor]

    released = coerce_bool(release_script(keys: keys, argv: argv))
    @token = nil
  end

  return released
end