Redis lock implementation











up vote
1
down vote

favorite












Considering I'm not implementing distributed lock mechanism, is this code correct and clear?



class RedisLock
class NotAcquired < StandardError; end

def initialize(redis)
@redis = redis
end

def lock(key, expiration_ms)
val = SecureRandom.random_number(100000000000000000).to_s

if @redis.set(key, val, nx: true, px: expiration_ms)
yield
unlock(key, val)
true
else
false
end
end

def lock!(*args, &block)
unless lock(*args, &block)
raise NotAcquired.new("Could not acquire lock with #{args}")
end
end

def unlock(key, val)
check_and_delete = <<-LUA
if redis.call('get', KEYS[1]) == KEYS[2] then
redis.call('del', KEYS[1])
end
LUA

@redis.eval(check_and_delete, [key, val])
end
end


RedisLock.new(Redis.current).lock!('key', 10000) { do_something }









share|improve this question
















bumped to the homepage by Community 1 hour ago


This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.



















    up vote
    1
    down vote

    favorite












    Considering I'm not implementing distributed lock mechanism, is this code correct and clear?



    class RedisLock
    class NotAcquired < StandardError; end

    def initialize(redis)
    @redis = redis
    end

    def lock(key, expiration_ms)
    val = SecureRandom.random_number(100000000000000000).to_s

    if @redis.set(key, val, nx: true, px: expiration_ms)
    yield
    unlock(key, val)
    true
    else
    false
    end
    end

    def lock!(*args, &block)
    unless lock(*args, &block)
    raise NotAcquired.new("Could not acquire lock with #{args}")
    end
    end

    def unlock(key, val)
    check_and_delete = <<-LUA
    if redis.call('get', KEYS[1]) == KEYS[2] then
    redis.call('del', KEYS[1])
    end
    LUA

    @redis.eval(check_and_delete, [key, val])
    end
    end


    RedisLock.new(Redis.current).lock!('key', 10000) { do_something }









    share|improve this question
















    bumped to the homepage by Community 1 hour ago


    This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.

















      up vote
      1
      down vote

      favorite









      up vote
      1
      down vote

      favorite











      Considering I'm not implementing distributed lock mechanism, is this code correct and clear?



      class RedisLock
      class NotAcquired < StandardError; end

      def initialize(redis)
      @redis = redis
      end

      def lock(key, expiration_ms)
      val = SecureRandom.random_number(100000000000000000).to_s

      if @redis.set(key, val, nx: true, px: expiration_ms)
      yield
      unlock(key, val)
      true
      else
      false
      end
      end

      def lock!(*args, &block)
      unless lock(*args, &block)
      raise NotAcquired.new("Could not acquire lock with #{args}")
      end
      end

      def unlock(key, val)
      check_and_delete = <<-LUA
      if redis.call('get', KEYS[1]) == KEYS[2] then
      redis.call('del', KEYS[1])
      end
      LUA

      @redis.eval(check_and_delete, [key, val])
      end
      end


      RedisLock.new(Redis.current).lock!('key', 10000) { do_something }









      share|improve this question















      Considering I'm not implementing distributed lock mechanism, is this code correct and clear?



      class RedisLock
      class NotAcquired < StandardError; end

      def initialize(redis)
      @redis = redis
      end

      def lock(key, expiration_ms)
      val = SecureRandom.random_number(100000000000000000).to_s

      if @redis.set(key, val, nx: true, px: expiration_ms)
      yield
      unlock(key, val)
      true
      else
      false
      end
      end

      def lock!(*args, &block)
      unless lock(*args, &block)
      raise NotAcquired.new("Could not acquire lock with #{args}")
      end
      end

      def unlock(key, val)
      check_and_delete = <<-LUA
      if redis.call('get', KEYS[1]) == KEYS[2] then
      redis.call('del', KEYS[1])
      end
      LUA

      @redis.eval(check_and_delete, [key, val])
      end
      end


      RedisLock.new(Redis.current).lock!('key', 10000) { do_something }






      ruby locking redis






      share|improve this question















      share|improve this question













      share|improve this question




      share|improve this question








      edited Mar 22 '17 at 12:42









      200_success

      128k15149412




      128k15149412










      asked Feb 9 '17 at 18:26









      cutalion

      1645




      1645





      bumped to the homepage by Community 1 hour ago


      This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.







      bumped to the homepage by Community 1 hour ago


      This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.
























          1 Answer
          1






          active

          oldest

          votes

















          up vote
          0
          down vote













          Here's a stab at clarifying the intent without changing any of the functionality. I didn't try to run it or anything, so don't hold me to syntax.



          # handles locking and unlocking redis for a particular operation
          class RedisLock
          # thrown when a lock cannot be acquired
          class NotAcquired < StandardError; end

          # since the redis key and ms are used in pretty much every method,
          # might as well go ahead and pass them in the constructor.
          # set defaults here if there are defaults that make sense.
          # make a yardoc comment and describe what these all are.
          def initialize(redis: Redis.current, nx: true, redis_key:, expiration_ms:)
          @redis = redis
          @redis_key = redis_key
          @expiration_ms = expiration_ms
          @nx = nx
          end

          # call it with_lock instead of lock
          # to make it more apparent it accepts a block
          def with_lock!
          raise(NotAcquired, "Could not acquire lock with #{args}") unless lock
          yield
          unlock(redis_key, random_lock_number)
          true # do you really need to return true here?
          end

          def with_lock
          with_lock!
          rescue NotAcquired
          false
          end

          private

          # set instance variables in the initializer, but never call them directly.
          attr_reader :redis, :redis_key, :expiration_ms, :nx

          def lock
          redis.set(redis_key, random_lock_number, nx: nx, px: expiration_ms)
          end

          # number is a little decieving here when you call `.to_s` at the end.
          # can you use SecureRandom.uuid instead?
          def random_lock_number
          @random_lock_number ||= SecureRandom.random_number(100_000_000_000_000_000).to_s
          end

          # no need for this to be exposed publicly.
          # calling eval directly is usually a bad idea.
          def unlock
          redis.eval(redis_check_and_delete, [redis_key, random_lock_number])
          end

          def redis_check_and_delete
          <<-LUA
          if redis.call('get', KEYS[1]) == KEYS[2] then
          redis.call('del', KEYS[1])
          end
          LUA
          end
          end


          And to call it:



          RedisLock.new(redis_key: 'key', expiration_ms: 10000).with_lock! { do_something }





          share|improve this answer























          • Hi @Chris! Thanks you for the answer! Though I cannot agree with the suggested improvements. The general concern about it is that the new object cannot be re-used with another key. Moreover it even cannot be re-used with the same key. I also have minor concerns (like nx in parameters makes no sense), but this is not so important after the first point.
            – cutalion
            Feb 20 '17 at 12:56










          • object instantiation is free. You have no need to to re-use the single object. the Redis connection is already passed in. you've even named the class RedisLock and not something like RedisLockFactory. The major takeaway here, if you care to take away anything is that your methods are doing too much internally. If you're passing arguments into every single method, that's a code smell. also usually the ! version of a method is called by the non-bang version instead of the other way around. good luck.
            – Chris Drappier
            Feb 20 '17 at 16:42











          Your Answer





          StackExchange.ifUsing("editor", function () {
          return StackExchange.using("mathjaxEditing", function () {
          StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
          StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
          });
          });
          }, "mathjax-editing");

          StackExchange.ifUsing("editor", function () {
          StackExchange.using("externalEditor", function () {
          StackExchange.using("snippets", function () {
          StackExchange.snippets.init();
          });
          });
          }, "code-snippets");

          StackExchange.ready(function() {
          var channelOptions = {
          tags: "".split(" "),
          id: "196"
          };
          initTagRenderer("".split(" "), "".split(" "), channelOptions);

          StackExchange.using("externalEditor", function() {
          // Have to fire editor after snippets, if snippets enabled
          if (StackExchange.settings.snippets.snippetsEnabled) {
          StackExchange.using("snippets", function() {
          createEditor();
          });
          }
          else {
          createEditor();
          }
          });

          function createEditor() {
          StackExchange.prepareEditor({
          heartbeatType: 'answer',
          convertImagesToLinks: false,
          noModals: true,
          showLowRepImageUploadWarning: true,
          reputationToPostImages: null,
          bindNavPrevention: true,
          postfix: "",
          imageUploader: {
          brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
          contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
          allowUrls: true
          },
          onDemand: true,
          discardSelector: ".discard-answer"
          ,immediatelyShowMarkdownHelp:true
          });


          }
          });














          draft saved

          draft discarded


















          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f154936%2fredis-lock-implementation%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown

























          1 Answer
          1






          active

          oldest

          votes








          1 Answer
          1






          active

          oldest

          votes









          active

          oldest

          votes






          active

          oldest

          votes








          up vote
          0
          down vote













          Here's a stab at clarifying the intent without changing any of the functionality. I didn't try to run it or anything, so don't hold me to syntax.



          # handles locking and unlocking redis for a particular operation
          class RedisLock
          # thrown when a lock cannot be acquired
          class NotAcquired < StandardError; end

          # since the redis key and ms are used in pretty much every method,
          # might as well go ahead and pass them in the constructor.
          # set defaults here if there are defaults that make sense.
          # make a yardoc comment and describe what these all are.
          def initialize(redis: Redis.current, nx: true, redis_key:, expiration_ms:)
          @redis = redis
          @redis_key = redis_key
          @expiration_ms = expiration_ms
          @nx = nx
          end

          # call it with_lock instead of lock
          # to make it more apparent it accepts a block
          def with_lock!
          raise(NotAcquired, "Could not acquire lock with #{args}") unless lock
          yield
          unlock(redis_key, random_lock_number)
          true # do you really need to return true here?
          end

          def with_lock
          with_lock!
          rescue NotAcquired
          false
          end

          private

          # set instance variables in the initializer, but never call them directly.
          attr_reader :redis, :redis_key, :expiration_ms, :nx

          def lock
          redis.set(redis_key, random_lock_number, nx: nx, px: expiration_ms)
          end

          # number is a little decieving here when you call `.to_s` at the end.
          # can you use SecureRandom.uuid instead?
          def random_lock_number
          @random_lock_number ||= SecureRandom.random_number(100_000_000_000_000_000).to_s
          end

          # no need for this to be exposed publicly.
          # calling eval directly is usually a bad idea.
          def unlock
          redis.eval(redis_check_and_delete, [redis_key, random_lock_number])
          end

          def redis_check_and_delete
          <<-LUA
          if redis.call('get', KEYS[1]) == KEYS[2] then
          redis.call('del', KEYS[1])
          end
          LUA
          end
          end


          And to call it:



          RedisLock.new(redis_key: 'key', expiration_ms: 10000).with_lock! { do_something }





          share|improve this answer























          • Hi @Chris! Thanks you for the answer! Though I cannot agree with the suggested improvements. The general concern about it is that the new object cannot be re-used with another key. Moreover it even cannot be re-used with the same key. I also have minor concerns (like nx in parameters makes no sense), but this is not so important after the first point.
            – cutalion
            Feb 20 '17 at 12:56










          • object instantiation is free. You have no need to to re-use the single object. the Redis connection is already passed in. you've even named the class RedisLock and not something like RedisLockFactory. The major takeaway here, if you care to take away anything is that your methods are doing too much internally. If you're passing arguments into every single method, that's a code smell. also usually the ! version of a method is called by the non-bang version instead of the other way around. good luck.
            – Chris Drappier
            Feb 20 '17 at 16:42















          up vote
          0
          down vote













          Here's a stab at clarifying the intent without changing any of the functionality. I didn't try to run it or anything, so don't hold me to syntax.



          # handles locking and unlocking redis for a particular operation
          class RedisLock
          # thrown when a lock cannot be acquired
          class NotAcquired < StandardError; end

          # since the redis key and ms are used in pretty much every method,
          # might as well go ahead and pass them in the constructor.
          # set defaults here if there are defaults that make sense.
          # make a yardoc comment and describe what these all are.
          def initialize(redis: Redis.current, nx: true, redis_key:, expiration_ms:)
          @redis = redis
          @redis_key = redis_key
          @expiration_ms = expiration_ms
          @nx = nx
          end

          # call it with_lock instead of lock
          # to make it more apparent it accepts a block
          def with_lock!
          raise(NotAcquired, "Could not acquire lock with #{args}") unless lock
          yield
          unlock(redis_key, random_lock_number)
          true # do you really need to return true here?
          end

          def with_lock
          with_lock!
          rescue NotAcquired
          false
          end

          private

          # set instance variables in the initializer, but never call them directly.
          attr_reader :redis, :redis_key, :expiration_ms, :nx

          def lock
          redis.set(redis_key, random_lock_number, nx: nx, px: expiration_ms)
          end

          # number is a little decieving here when you call `.to_s` at the end.
          # can you use SecureRandom.uuid instead?
          def random_lock_number
          @random_lock_number ||= SecureRandom.random_number(100_000_000_000_000_000).to_s
          end

          # no need for this to be exposed publicly.
          # calling eval directly is usually a bad idea.
          def unlock
          redis.eval(redis_check_and_delete, [redis_key, random_lock_number])
          end

          def redis_check_and_delete
          <<-LUA
          if redis.call('get', KEYS[1]) == KEYS[2] then
          redis.call('del', KEYS[1])
          end
          LUA
          end
          end


          And to call it:



          RedisLock.new(redis_key: 'key', expiration_ms: 10000).with_lock! { do_something }





          share|improve this answer























          • Hi @Chris! Thanks you for the answer! Though I cannot agree with the suggested improvements. The general concern about it is that the new object cannot be re-used with another key. Moreover it even cannot be re-used with the same key. I also have minor concerns (like nx in parameters makes no sense), but this is not so important after the first point.
            – cutalion
            Feb 20 '17 at 12:56










          • object instantiation is free. You have no need to to re-use the single object. the Redis connection is already passed in. you've even named the class RedisLock and not something like RedisLockFactory. The major takeaway here, if you care to take away anything is that your methods are doing too much internally. If you're passing arguments into every single method, that's a code smell. also usually the ! version of a method is called by the non-bang version instead of the other way around. good luck.
            – Chris Drappier
            Feb 20 '17 at 16:42













          up vote
          0
          down vote










          up vote
          0
          down vote









          Here's a stab at clarifying the intent without changing any of the functionality. I didn't try to run it or anything, so don't hold me to syntax.



          # handles locking and unlocking redis for a particular operation
          class RedisLock
          # thrown when a lock cannot be acquired
          class NotAcquired < StandardError; end

          # since the redis key and ms are used in pretty much every method,
          # might as well go ahead and pass them in the constructor.
          # set defaults here if there are defaults that make sense.
          # make a yardoc comment and describe what these all are.
          def initialize(redis: Redis.current, nx: true, redis_key:, expiration_ms:)
          @redis = redis
          @redis_key = redis_key
          @expiration_ms = expiration_ms
          @nx = nx
          end

          # call it with_lock instead of lock
          # to make it more apparent it accepts a block
          def with_lock!
          raise(NotAcquired, "Could not acquire lock with #{args}") unless lock
          yield
          unlock(redis_key, random_lock_number)
          true # do you really need to return true here?
          end

          def with_lock
          with_lock!
          rescue NotAcquired
          false
          end

          private

          # set instance variables in the initializer, but never call them directly.
          attr_reader :redis, :redis_key, :expiration_ms, :nx

          def lock
          redis.set(redis_key, random_lock_number, nx: nx, px: expiration_ms)
          end

          # number is a little decieving here when you call `.to_s` at the end.
          # can you use SecureRandom.uuid instead?
          def random_lock_number
          @random_lock_number ||= SecureRandom.random_number(100_000_000_000_000_000).to_s
          end

          # no need for this to be exposed publicly.
          # calling eval directly is usually a bad idea.
          def unlock
          redis.eval(redis_check_and_delete, [redis_key, random_lock_number])
          end

          def redis_check_and_delete
          <<-LUA
          if redis.call('get', KEYS[1]) == KEYS[2] then
          redis.call('del', KEYS[1])
          end
          LUA
          end
          end


          And to call it:



          RedisLock.new(redis_key: 'key', expiration_ms: 10000).with_lock! { do_something }





          share|improve this answer














          Here's a stab at clarifying the intent without changing any of the functionality. I didn't try to run it or anything, so don't hold me to syntax.



          # handles locking and unlocking redis for a particular operation
          class RedisLock
          # thrown when a lock cannot be acquired
          class NotAcquired < StandardError; end

          # since the redis key and ms are used in pretty much every method,
          # might as well go ahead and pass them in the constructor.
          # set defaults here if there are defaults that make sense.
          # make a yardoc comment and describe what these all are.
          def initialize(redis: Redis.current, nx: true, redis_key:, expiration_ms:)
          @redis = redis
          @redis_key = redis_key
          @expiration_ms = expiration_ms
          @nx = nx
          end

          # call it with_lock instead of lock
          # to make it more apparent it accepts a block
          def with_lock!
          raise(NotAcquired, "Could not acquire lock with #{args}") unless lock
          yield
          unlock(redis_key, random_lock_number)
          true # do you really need to return true here?
          end

          def with_lock
          with_lock!
          rescue NotAcquired
          false
          end

          private

          # set instance variables in the initializer, but never call them directly.
          attr_reader :redis, :redis_key, :expiration_ms, :nx

          def lock
          redis.set(redis_key, random_lock_number, nx: nx, px: expiration_ms)
          end

          # number is a little decieving here when you call `.to_s` at the end.
          # can you use SecureRandom.uuid instead?
          def random_lock_number
          @random_lock_number ||= SecureRandom.random_number(100_000_000_000_000_000).to_s
          end

          # no need for this to be exposed publicly.
          # calling eval directly is usually a bad idea.
          def unlock
          redis.eval(redis_check_and_delete, [redis_key, random_lock_number])
          end

          def redis_check_and_delete
          <<-LUA
          if redis.call('get', KEYS[1]) == KEYS[2] then
          redis.call('del', KEYS[1])
          end
          LUA
          end
          end


          And to call it:



          RedisLock.new(redis_key: 'key', expiration_ms: 10000).with_lock! { do_something }






          share|improve this answer














          share|improve this answer



          share|improve this answer








          edited Feb 20 '17 at 7:55

























          answered Feb 20 '17 at 7:50









          Chris Drappier

          255129




          255129












          • Hi @Chris! Thanks you for the answer! Though I cannot agree with the suggested improvements. The general concern about it is that the new object cannot be re-used with another key. Moreover it even cannot be re-used with the same key. I also have minor concerns (like nx in parameters makes no sense), but this is not so important after the first point.
            – cutalion
            Feb 20 '17 at 12:56










          • object instantiation is free. You have no need to to re-use the single object. the Redis connection is already passed in. you've even named the class RedisLock and not something like RedisLockFactory. The major takeaway here, if you care to take away anything is that your methods are doing too much internally. If you're passing arguments into every single method, that's a code smell. also usually the ! version of a method is called by the non-bang version instead of the other way around. good luck.
            – Chris Drappier
            Feb 20 '17 at 16:42


















          • Hi @Chris! Thanks you for the answer! Though I cannot agree with the suggested improvements. The general concern about it is that the new object cannot be re-used with another key. Moreover it even cannot be re-used with the same key. I also have minor concerns (like nx in parameters makes no sense), but this is not so important after the first point.
            – cutalion
            Feb 20 '17 at 12:56










          • object instantiation is free. You have no need to to re-use the single object. the Redis connection is already passed in. you've even named the class RedisLock and not something like RedisLockFactory. The major takeaway here, if you care to take away anything is that your methods are doing too much internally. If you're passing arguments into every single method, that's a code smell. also usually the ! version of a method is called by the non-bang version instead of the other way around. good luck.
            – Chris Drappier
            Feb 20 '17 at 16:42
















          Hi @Chris! Thanks you for the answer! Though I cannot agree with the suggested improvements. The general concern about it is that the new object cannot be re-used with another key. Moreover it even cannot be re-used with the same key. I also have minor concerns (like nx in parameters makes no sense), but this is not so important after the first point.
          – cutalion
          Feb 20 '17 at 12:56




          Hi @Chris! Thanks you for the answer! Though I cannot agree with the suggested improvements. The general concern about it is that the new object cannot be re-used with another key. Moreover it even cannot be re-used with the same key. I also have minor concerns (like nx in parameters makes no sense), but this is not so important after the first point.
          – cutalion
          Feb 20 '17 at 12:56












          object instantiation is free. You have no need to to re-use the single object. the Redis connection is already passed in. you've even named the class RedisLock and not something like RedisLockFactory. The major takeaway here, if you care to take away anything is that your methods are doing too much internally. If you're passing arguments into every single method, that's a code smell. also usually the ! version of a method is called by the non-bang version instead of the other way around. good luck.
          – Chris Drappier
          Feb 20 '17 at 16:42




          object instantiation is free. You have no need to to re-use the single object. the Redis connection is already passed in. you've even named the class RedisLock and not something like RedisLockFactory. The major takeaway here, if you care to take away anything is that your methods are doing too much internally. If you're passing arguments into every single method, that's a code smell. also usually the ! version of a method is called by the non-bang version instead of the other way around. good luck.
          – Chris Drappier
          Feb 20 '17 at 16:42


















          draft saved

          draft discarded




















































          Thanks for contributing an answer to Code Review Stack Exchange!


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          Use MathJax to format equations. MathJax reference.


          To learn more, see our tips on writing great answers.





          Some of your past answers have not been well-received, and you're in danger of being blocked from answering.


          Please pay close attention to the following guidance:


          • Please be sure to answer the question. Provide details and share your research!

          But avoid



          • Asking for help, clarification, or responding to other answers.

          • Making statements based on opinion; back them up with references or personal experience.


          To learn more, see our tips on writing great answers.




          draft saved


          draft discarded














          StackExchange.ready(
          function () {
          StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f154936%2fredis-lock-implementation%23new-answer', 'question_page');
          }
          );

          Post as a guest















          Required, but never shown





















































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown

































          Required, but never shown














          Required, but never shown












          Required, but never shown







          Required, but never shown







          Popular posts from this blog

          Ottavio Pratesi

          Tricia Helfer

          15 giugno