def lock(timeout: nil, &block)
      ensure_exists_and_release_stale_locks!

      success = @redis.with do |conn|
        if timeout
          !conn.blpop(available_key, timeout.to_i).nil?
        else
          !conn.lpop(available_key).nil?
        end
      end

      return false unless success

      token = SecureRandom.hex(16)
      @tokens.push(token)
      @redis.with do |conn|
        conn.zadd(grabbed_key, epoch_f(conn), token)
      end

      return_or_yield(token, &block)
    end
def lock(timeout: nil, &block)
      ensure_exists_and_release_stale_locks!

      success = @redis.with do |conn|
        if timeout
          !conn.blpop(available_key, timeout.to_i).nil?
        else
          !conn.lpop(available_key).nil?
        end
      end

      return false unless success

      token = SecureRandom.hex(16)
      @tokens.push(token)
      @redis.with do |conn|
        conn.zadd(grabbed_key, epoch_f(conn), token)
      end

      return_or_yield(token, &block)
    end
Acquire a resource from the semaphore, if available. Returns false if no
 resources are available.

 `timeout` is how long to wait, blocking, until a resource is available.
 The default is nil, meaning don't block. A timeout of zero means block forever.
 (This is a bit weird, but corresponds to how blpop uses timeout values.)

 If passed a block, if a resource is available, runs the block and then
 unlocks.

 If called without a block, if a resource is available, returns a token.
 Caller is then responsible for unlocking the token.

 This isn't atomic--if the process dies, we could remove something from the
 available queue without adding it to the grabbed set--but that's ok, the
 semaphore will recover just as if this was a stale client that left its
 token in the grabbed set forever.