Class: Redstruct::Hls::Lock

Inherits:
Types::Base show all
Includes:
Utils::Coercion, Utils::Scriptable
Defined in:
lib/redstruct/hls/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 milliseconds

1000
DEFAULT_TIMEOUT =

The default timeout when blocking, in seconds; a nil value means it is non-blocking

nil

Instance Attribute Summary collapse

Attributes inherited from Types::Base

#key

Serialization collapse

Instance Method Summary collapse

Methods included from Utils::Scriptable

included

Methods included from Utils::Coercion

coerce_array, coerce_bool

Methods inherited from Types::Base

#with

Methods included from Utils::Inspectable

#inspect, #to_s

Constructor Details

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

Returns a new instance of Lock.

Parameters:

  • expiry (Integer) (defaults to: DEFAULT_EXPIRY)

    in milliseconds; to prevent infinite locking, each mutex is released after a certain expiry time

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT)

    in seconds; if > 0, will block for this amount of time when trying to obtain the lock



26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/redstruct/hls/lock.rb', line 26

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

  @token = nil
  @expiry = expiry
  @timeout = timeout.to_i

  create do |factory|
    @lease = factory.string('lease')
    @tokens = factory.list('tokens')
  end
end

Instance Attribute Details

#expiryFixnum (readonly)

expiry of the underlying redis structures in milliseconds

Returns:

  • (Fixnum)

    the current value of expiry



13
14
15
# File 'lib/redstruct/hls/lock.rb', line 13

def expiry
  @expiry
end

#timeoutFixnum? (readonly)

the timeout to wait when attempting to acquire the lock, in seconds

Returns:

  • (Fixnum, nil)

    the current value of timeout



13
14
15
# File 'lib/redstruct/hls/lock.rb', line 13

def timeout
  @timeout
end

#token::String? (readonly)

the current token or nil

Returns:

  • (::String, nil)

    the current value of token



13
14
15
# File 'lib/redstruct/hls/lock.rb', line 13

def token
  @token
end

Class Method Details

.from_h(hash, factory) ⇒ Lock

Builds a lock from a hash.

Parameters:

  • hash (Hash)

    hash generated by calling Lock#to_h. Ensure beforehand that keys are symbols.

Returns:

See Also:

  • #to_h
  • Factory#create_from_h


157
158
159
160
# File 'lib/redstruct/hls/lock.rb', line 157

def from_h(hash, factory)
  hash[:factory] = factory
  return new(**hash)
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



61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/redstruct/hls/lock.rb', line 61

def acquire
  acquired = false
  token = non_blocking_acquire(@token)
  token = blocking_acquire if token.nil? && blocking?

  unless token.nil?
    @token = token
    acquired = true
  end

  return acquired
end

#acquire_script::String

The acquire script attempts to set the lease (keys) to the given token (argv), only if it wasn’t already set. It then compares to check if the value of the lease is that of the token, and if so refreshes the expiry (argv) time of the lease.

Parameters:

  • keys (Array<(::String)>)

    The lease key specifying who owns the mutex at the moment

  • argv (Array<(::String, Fixnum)>)

    The current token; the expiry time in milliseconds

Returns:

  • (::String)

    Returns the token if acquired, nil otherwise.



109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/redstruct/hls/lock.rb', line 109

defscript :acquire_script, <<~LUA
  local token = ARGV[1]
  local expiry = tonumber(ARGV[2])

  redis.call('set', KEYS[1], token, 'NX')
  if redis.call('get', KEYS[1]) == token then
    redis.call('pexpire', KEYS[1], expiry)
    return token
  end

  return false
LUA

#blocking?Boolean

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

Returns:

  • (Boolean)


49
50
51
# File 'lib/redstruct/hls/lock.rb', line 49

def blocking?
  return @timeout.positive?
end

#inspectable_attributesObject

Helper method for easy inspection



170
171
172
# File 'lib/redstruct/hls/lock.rb', line 170

def inspectable_attributes
  super.merge(expiry: @expiry, blocking: blocking?)
end

#locked { ... } ⇒ Object

Executes the given block if the lock can be acquired

Yields:

  • Block to be executed if the lock is acquired



41
42
43
44
45
# File 'lib/redstruct/hls/lock.rb', line 41

def locked
  yield if acquire
ensure
  release
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



77
78
79
80
81
82
# File 'lib/redstruct/hls/lock.rb', line 77

def release
  return false if @token.nil?

  next_token = SecureRandom.uuid
  return coerce_bool(release_script(keys: [@lease.key, @tokens.key], argv: [@token, next_token, @expiry]))
end

#release_scriptFixnum

The release script compares the given token (argv) with the lease value (keys); if they are the same, then a new token (argv) is set as the lease, and pushed on the tokens (keys) list for the next acquire request.

Parameters:

  • keys (Array<(::String, ::String)>)

    The lease key; the tokens list key

  • argv (Array<(::String, ::String, Fixnum)>)

    The current token; the next token to push; the expiry time of both keys

Returns:

  • (Fixnum)

    1 if released, 0 otherwise



128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/redstruct/hls/lock.rb', line 128

defscript :release_script, <<~LUA
  local currentToken = ARGV[1]
  local nextToken = ARGV[2]
  local expiry = tonumber(ARGV[3])

  if redis.call('get', KEYS[1]) == currentToken then
    redis.call('set', KEYS[1], nextToken, 'PX', expiry)
    redis.call('lpush', KEYS[2], nextToken)
    redis.call('pexpire', KEYS[2], expiry)
    return true
  end

  return false
LUA

#to_hHash<Symbol, Object>

Returns a hash representation of the object

Returns:

  • (Hash<Symbol, Object>)

    hash representation of the lock

See Also:



147
148
149
# File 'lib/redstruct/hls/lock.rb', line 147

def to_h
  return super.merge(token: @token, expiry: @expiry, timeout: @timeout)
end