redis-em-mutex 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/HISTORY.rdoc +9 -0
- data/README.rdoc +92 -2
- data/lib/redis/em-mutex/ns.rb +47 -0
- data/lib/redis/em-mutex/version.rb +1 -1
- data/lib/redis/em-mutex.rb +215 -132
- data/spec/redis-em-mutex-condition.rb +162 -0
- data/spec/redis-em-mutex-namespaces.rb +16 -16
- data/spec/redis-em-mutex-owners.rb +156 -0
- data/spec/redis-em-mutex-semaphores.rb +146 -76
- metadata +19 -14
    
        data/lib/redis/em-mutex.rb
    CHANGED
    
    | @@ -22,6 +22,7 @@ class Redis | |
| 22 22 | 
             
                #
         | 
| 23 23 | 
             
                class Mutex
         | 
| 24 24 |  | 
| 25 | 
            +
                  autoload :NS, 'redis/em-mutex/ns'
         | 
| 25 26 | 
             
                  autoload :Macro, 'redis/em-mutex/macro'
         | 
| 26 27 |  | 
| 27 28 | 
             
                  module Errors
         | 
| @@ -46,62 +47,34 @@ class Redis | |
| 46 47 | 
             
                  @@ns = nil
         | 
| 47 48 | 
             
                  @@uuid = nil
         | 
| 48 49 |  | 
| 49 | 
            -
                   | 
| 50 | 
            -
                  attr_reader :names, :ns
         | 
| 50 | 
            +
                  attr_reader :names, :ns, :block_timeout
         | 
| 51 51 | 
             
                  alias_method :namespace, :ns
         | 
| 52 52 |  | 
| 53 | 
            -
                   | 
| 54 | 
            -
                    attr_reader :ns
         | 
| 55 | 
            -
                    alias_method :namespace, :ns
         | 
| 56 | 
            -
                    # Creates a new namespace (Mutex factory).
         | 
| 57 | 
            -
                    #
         | 
| 58 | 
            -
                    # - ns = namespace
         | 
| 59 | 
            -
                    # - opts = options hash:
         | 
| 60 | 
            -
                    # - :block - default block timeout
         | 
| 61 | 
            -
                    # - :expire - default expire timeout
         | 
| 62 | 
            -
                    def initialize(ns, opts = {})
         | 
| 63 | 
            -
                      @ns = ns
         | 
| 64 | 
            -
                      @opts = (opts || {}).merge(:ns => ns)
         | 
| 65 | 
            -
                    end
         | 
| 66 | 
            -
             | 
| 67 | 
            -
                    # Creates a namespaced cross machine/process/fiber semaphore.
         | 
| 68 | 
            -
                    #
         | 
| 69 | 
            -
                    # for arguments see: Redis::EM::Mutex.new
         | 
| 70 | 
            -
                    def new(*args)
         | 
| 71 | 
            -
                      if args.last.kind_of?(Hash)
         | 
| 72 | 
            -
                        args[-1] = @opts.merge(args.last)
         | 
| 73 | 
            -
                      else
         | 
| 74 | 
            -
                        args.push @opts
         | 
| 75 | 
            -
                      end
         | 
| 76 | 
            -
                      Redis::EM::Mutex.new(*args)
         | 
| 77 | 
            -
                    end
         | 
| 53 | 
            +
                  def expire_timeout; @expire_timeout || @@default_expire; end
         | 
| 78 54 |  | 
| 79 | 
            -
             | 
| 80 | 
            -
                    # 
         | 
| 81 | 
            -
                     | 
| 82 | 
            -
             | 
| 83 | 
            -
                      mutex = new(*args)
         | 
| 84 | 
            -
                      mutex if mutex.lock
         | 
| 85 | 
            -
                    end
         | 
| 55 | 
            +
                  def expire_timeout=(value)
         | 
| 56 | 
            +
                    raise ArgumentError, "#{self.class.name}\#expire_timeout value must be greater than 0" unless (value = value.to_f) > 0
         | 
| 57 | 
            +
                    @expire_timeout = value
         | 
| 58 | 
            +
                  end
         | 
| 86 59 |  | 
| 87 | 
            -
             | 
| 88 | 
            -
                     | 
| 89 | 
            -
                    # See: Redis::EM::Mutex.synchronize
         | 
| 90 | 
            -
                    def synchronize(*args, &block)
         | 
| 91 | 
            -
                      new(*args).synchronize(&block)
         | 
| 92 | 
            -
                    end
         | 
| 60 | 
            +
                  def block_timeout=(value)
         | 
| 61 | 
            +
                    @block_timeout = value.nil? ? nil : value.to_f
         | 
| 93 62 | 
             
                  end
         | 
| 94 63 |  | 
| 95 64 | 
             
                  # Creates a new cross machine/process/fiber semaphore
         | 
| 96 65 | 
             
                  #
         | 
| 97 | 
            -
                  #   Redis::EM::Mutex.new(*names,  | 
| 66 | 
            +
                  #   Redis::EM::Mutex.new(*names, options = {})
         | 
| 98 67 | 
             
                  #
         | 
| 99 68 | 
             
                  # - *names = lock identifiers - if none they are auto generated
         | 
| 100 | 
            -
                  # -  | 
| 69 | 
            +
                  # - options = hash:
         | 
| 101 70 | 
             
                  # - :name - same as *names (in case *names arguments were omitted)
         | 
| 102 71 | 
             
                  # - :block - default block timeout
         | 
| 103 72 | 
             
                  # - :expire - default expire timeout (see: Mutex#lock and Mutex#try_lock)
         | 
| 104 73 | 
             
                  # - :ns - local namespace (otherwise global namespace is used)
         | 
| 74 | 
            +
                  # - :owner - owner definition instead of Fiber#__id__
         | 
| 75 | 
            +
                  #
         | 
| 76 | 
            +
                  # Raises MutexError if used before Mutex.setup.
         | 
| 77 | 
            +
                  # Raises ArgumentError on invalid options.
         | 
| 105 78 | 
             
                  def initialize(*args)
         | 
| 106 79 | 
             
                    raise MutexError, "call #{self.class}::setup first" unless @@redis_pool
         | 
| 107 80 |  | 
| @@ -109,13 +82,23 @@ class Redis | |
| 109 82 |  | 
| 110 83 | 
             
                    @names = args
         | 
| 111 84 | 
             
                    @names = Array(opts[:name] || "#{@@name_index.succ!}.lock") if @names.empty?
         | 
| 112 | 
            -
                     | 
| 85 | 
            +
                    @slept = {}
         | 
| 86 | 
            +
                    raise ArgumentError, "semaphore names must not be empty" if @names.empty?
         | 
| 113 87 | 
             
                    @multi = !@names.one?
         | 
| 114 88 | 
             
                    @ns = opts[:ns] || @@ns
         | 
| 115 89 | 
             
                    @ns_names = @ns ? @names.map {|n| "#@ns:#{n}".freeze }.freeze : @names.map {|n| n.to_s.dup.freeze }.freeze
         | 
| 116 | 
            -
                     | 
| 117 | 
            -
                     | 
| 118 | 
            -
                    @locked_id = nil
         | 
| 90 | 
            +
                    self.expire_timeout = opts[:expire] if opts.key?(:expire)
         | 
| 91 | 
            +
                    self.block_timeout = opts[:block] if opts.key?(:block)
         | 
| 92 | 
            +
                    @locked_owner_id = @locked_id = nil
         | 
| 93 | 
            +
                    if (owner = opts[:owner])
         | 
| 94 | 
            +
                      self.define_singleton_method(:owner_ident) do |lock_id = nil|
         | 
| 95 | 
            +
                        if lock_id
         | 
| 96 | 
            +
                          "#@@uuid$#$$@#{owner} #{lock_id}"
         | 
| 97 | 
            +
                        else
         | 
| 98 | 
            +
                          "#@@uuid$#$$@#{owner}"
         | 
| 99 | 
            +
                        end
         | 
| 100 | 
            +
                      end
         | 
| 101 | 
            +
                    end
         | 
| 119 102 | 
             
                  end
         | 
| 120 103 |  | 
| 121 104 | 
             
                  # Returns `true` if this semaphore (at least one of locked `names`) is currently being held by some owner.
         | 
| @@ -129,49 +112,92 @@ class Redis | |
| 129 112 | 
             
                    end
         | 
| 130 113 | 
             
                  end
         | 
| 131 114 |  | 
| 132 | 
            -
                  # Returns `true` if this semaphore (all the locked `names`) is currently being held by calling  | 
| 133 | 
            -
                  #  | 
| 115 | 
            +
                  # Returns `true` if this semaphore (all the locked `names`) is currently being held by calling owner.
         | 
| 116 | 
            +
                  # This is the method you should use to check if lock is still held and valid.
         | 
| 134 117 | 
             
                  def owned?
         | 
| 135 | 
            -
                    !!if @locked_id
         | 
| 136 | 
            -
                      lock_full_ident = owner_ident(@locked_id)
         | 
| 118 | 
            +
                    !!if @locked_id && owner_ident(@locked_id) == (lock_full_ident = @locked_owner_id)
         | 
| 137 119 | 
             
                      @@redis_pool.mget(*@ns_names).all? {|v| v == lock_full_ident}
         | 
| 138 120 | 
             
                    end
         | 
| 139 121 | 
             
                  end
         | 
| 140 122 |  | 
| 123 | 
            +
                  # Returns `true` when the semaphore is being held and have already expired.
         | 
| 124 | 
            +
                  # Returns `false` when the semaphore is still locked and valid
         | 
| 125 | 
            +
                  # or `nil` if the semaphore wasn't locked by current owner.
         | 
| 126 | 
            +
                  #
         | 
| 127 | 
            +
                  # The check is performed only on the Mutex object instance and should only be used as a hint.
         | 
| 128 | 
            +
                  # For reliable lock status information use #refresh or #owned? instead.
         | 
| 129 | 
            +
                  def expired?
         | 
| 130 | 
            +
                    Time.now.to_f > @locked_id.to_f if @locked_id && owner_ident(@locked_id) == @locked_owner_id
         | 
| 131 | 
            +
                  end
         | 
| 132 | 
            +
             | 
| 133 | 
            +
                  # Returns the number of seconds left until the semaphore expires.
         | 
| 134 | 
            +
                  # The number of seconds less than 0 means that the semaphore expired and could be grabbed
         | 
| 135 | 
            +
                  # by some other owner.
         | 
| 136 | 
            +
                  # Returns `nil` if the semaphore wasn't locked by current owner.
         | 
| 137 | 
            +
                  #
         | 
| 138 | 
            +
                  # The check is performed only on the Mutex object instance and should only be used as a hint.
         | 
| 139 | 
            +
                  # For reliable lock status information use #refresh or #owned? instead.
         | 
| 140 | 
            +
                  def expires_in
         | 
| 141 | 
            +
                    @locked_id.to_f - Time.now.to_f if @locked_id && owner_ident(@locked_id) == @locked_owner_id
         | 
| 142 | 
            +
                  end
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                  # Returns local time at which the semaphore will expire or have expired.
         | 
| 145 | 
            +
                  # Returns `nil` if the semaphore wasn't locked by current owner.
         | 
| 146 | 
            +
                  #
         | 
| 147 | 
            +
                  # The check is performed only on the Mutex object instance and should only be used as a hint.
         | 
| 148 | 
            +
                  # For reliable lock status information use #refresh or #owned? instead.
         | 
| 149 | 
            +
                  def expires_at
         | 
| 150 | 
            +
                    Time.at(@locked_id.to_f) if @locked_id && owner_ident(@locked_id) == @locked_owner_id
         | 
| 151 | 
            +
                  end
         | 
| 152 | 
            +
             | 
| 153 | 
            +
                  # Returns timestamp at which the semaphore will expire or have expired.
         | 
| 154 | 
            +
                  # Returns `nil` if the semaphore wasn't locked by current owner.
         | 
| 155 | 
            +
                  #
         | 
| 156 | 
            +
                  # The check is performed only on the Mutex object instance and should only be used as a hint.
         | 
| 157 | 
            +
                  # For reliable lock status information use #refresh or #owned? instead.
         | 
| 158 | 
            +
                  def expiration_timestamp
         | 
| 159 | 
            +
                    @locked_id.to_f if @locked_id && owner_ident(@locked_id) == @locked_owner_id
         | 
| 160 | 
            +
                  end
         | 
| 161 | 
            +
             | 
| 141 162 | 
             
                  # Attempts to obtain the lock and returns immediately.
         | 
| 142 163 | 
             
                  # Returns `true` if the lock was granted.
         | 
| 143 | 
            -
                  # Use Mutex#expire_timeout= to set  | 
| 164 | 
            +
                  # Use Mutex#expire_timeout= to set lock expiration time in secods.
         | 
| 144 165 | 
             
                  # Otherwise global Mutex.default_expire is used.
         | 
| 145 166 | 
             
                  #
         | 
| 146 | 
            -
                  # This method  | 
| 167 | 
            +
                  # This method doesn't capture expired semaphores
         | 
| 168 | 
            +
                  # and therefore it should not be used under normal circumstances.
         | 
| 147 169 | 
             
                  # Use Mutex#lock with block_timeout = 0 to obtain expired lock without blocking.
         | 
| 148 170 | 
             
                  def try_lock
         | 
| 149 | 
            -
                    lock_id = (Time.now +  | 
| 171 | 
            +
                    lock_id = (Time.now + expire_timeout).to_f.to_s
         | 
| 172 | 
            +
                    lock_full_ident = owner_ident(lock_id)
         | 
| 150 173 | 
             
                    !!if @multi
         | 
| 151 | 
            -
                      lock_full_ident = owner_ident(lock_id)
         | 
| 152 174 | 
             
                      if @@redis_pool.msetnx(*@ns_names.map {|k| [k, lock_full_ident]}.flatten)
         | 
| 153 175 | 
             
                        @locked_id = lock_id
         | 
| 176 | 
            +
                        @locked_owner_id = lock_full_ident
         | 
| 154 177 | 
             
                      end
         | 
| 155 | 
            -
                    elsif @@redis_pool.setnx(@ns_names.first,  | 
| 178 | 
            +
                    elsif @@redis_pool.setnx(@ns_names.first, lock_full_ident)
         | 
| 156 179 | 
             
                      @locked_id = lock_id
         | 
| 180 | 
            +
                      @locked_owner_id = lock_full_ident
         | 
| 157 181 | 
             
                    end
         | 
| 158 182 | 
             
                  end
         | 
| 159 183 |  | 
| 160 184 | 
             
                  # Refreshes lock expiration timeout.
         | 
| 161 | 
            -
                  # Returns true if refresh was successfull | 
| 185 | 
            +
                  # Returns `true` if refresh was successfull.
         | 
| 186 | 
            +
                  # Returns `false` if the semaphore wasn't locked or when it was locked but it has expired
         | 
| 187 | 
            +
                  # and now it's got a new owner.
         | 
| 162 188 | 
             
                  def refresh(expire_timeout=nil)
         | 
| 163 189 | 
             
                    ret = false
         | 
| 164 | 
            -
                    if @locked_id
         | 
| 165 | 
            -
                      new_lock_id = (Time.now + (expire_timeout.to_f.nonzero? ||  | 
| 190 | 
            +
                    if @locked_id && owner_ident(@locked_id) == (lock_full_ident = @locked_owner_id)
         | 
| 191 | 
            +
                      new_lock_id = (Time.now + (expire_timeout.to_f.nonzero? || self.expire_timeout)).to_f.to_s
         | 
| 166 192 | 
             
                      new_lock_full_ident = owner_ident(new_lock_id)
         | 
| 167 | 
            -
                      lock_full_ident = owner_ident(@locked_id)
         | 
| 168 193 | 
             
                      @@redis_pool.execute(false) do |r|
         | 
| 169 194 | 
             
                        r.watch(*@ns_names) do
         | 
| 170 195 | 
             
                          if r.mget(*@ns_names).all? {|v| v == lock_full_ident}
         | 
| 171 | 
            -
                             | 
| 172 | 
            -
                               | 
| 196 | 
            +
                            if r.multi {|m| m.mset(*@ns_names.map {|k| [k, new_lock_full_ident]}.flatten)}
         | 
| 197 | 
            +
                              @locked_id = new_lock_id
         | 
| 198 | 
            +
                              @locked_owner_id = new_lock_full_ident
         | 
| 199 | 
            +
                              ret = true
         | 
| 173 200 | 
             
                            end
         | 
| 174 | 
            -
                            @locked_id = new_lock_id if ret
         | 
| 175 201 | 
             
                          else
         | 
| 176 202 | 
             
                            r.unwatch
         | 
| 177 203 | 
             
                          end
         | 
| @@ -181,16 +207,16 @@ class Redis | |
| 181 207 | 
             
                    ret
         | 
| 182 208 | 
             
                  end
         | 
| 183 209 |  | 
| 184 | 
            -
                  # Releases the lock  | 
| 185 | 
            -
                  #  | 
| 186 | 
            -
                  #  | 
| 187 | 
            -
                  def unlock
         | 
| 188 | 
            -
                     | 
| 189 | 
            -
             | 
| 210 | 
            +
                  # Releases the lock. Returns self on success.
         | 
| 211 | 
            +
                  # Returns `false` if the semaphore wasn't locked or when it was locked but it has expired
         | 
| 212 | 
            +
                  # and now it's got a new owner.
         | 
| 213 | 
            +
                  def unlock!
         | 
| 214 | 
            +
                    ret = false
         | 
| 215 | 
            +
                    if @locked_id && owner_ident(@locked_id) == (lock_full_ident = @locked_owner_id)
         | 
| 190 216 | 
             
                      @@redis_pool.execute(false) do |r|
         | 
| 191 217 | 
             
                        r.watch(*@ns_names) do
         | 
| 192 218 | 
             
                          if r.mget(*@ns_names).all? {|v| v == lock_full_ident}
         | 
| 193 | 
            -
                            r.multi do |multi|
         | 
| 219 | 
            +
                            ret = !!r.multi do |multi|
         | 
| 194 220 | 
             
                              multi.del(*@ns_names)
         | 
| 195 221 | 
             
                              multi.publish SIGNAL_QUEUE_CHANNEL, Marshal.dump(@ns_names)
         | 
| 196 222 | 
             
                            end
         | 
| @@ -199,7 +225,16 @@ class Redis | |
| 199 225 | 
             
                          end
         | 
| 200 226 | 
             
                        end
         | 
| 201 227 | 
             
                      end
         | 
| 228 | 
            +
                      @locked_owner_id = @locked_id = nil
         | 
| 202 229 | 
             
                    end
         | 
| 230 | 
            +
                    ret && self
         | 
| 231 | 
            +
                  end
         | 
| 232 | 
            +
             | 
| 233 | 
            +
                  # Releases the lock unconditionally.
         | 
| 234 | 
            +
                  # If the semaphore wasn’t locked by the current owner it is silently ignored.
         | 
| 235 | 
            +
                  # Returns self.
         | 
| 236 | 
            +
                  def unlock
         | 
| 237 | 
            +
                    unlock!
         | 
| 203 238 | 
             
                    self
         | 
| 204 239 | 
             
                  end
         | 
| 205 240 |  | 
| @@ -215,7 +250,7 @@ class Redis | |
| 215 250 | 
             
                  # Use Mutex#expire_timeout= to set lock expiration timeout.
         | 
| 216 251 | 
             
                  # Otherwise global Mutex.default_expire is used.
         | 
| 217 252 | 
             
                  def lock(block_timeout = nil)
         | 
| 218 | 
            -
                    block_timeout||=  | 
| 253 | 
            +
                    block_timeout||= self.block_timeout
         | 
| 219 254 | 
             
                    names = @ns_names
         | 
| 220 255 | 
             
                    timer = fiber = nil
         | 
| 221 256 | 
             
                    try_again = false
         | 
| @@ -223,62 +258,97 @@ class Redis | |
| 223 258 | 
             
                      try_again = true
         | 
| 224 259 | 
             
                      ::EM.next_tick { fiber.resume if fiber } if fiber
         | 
| 225 260 | 
             
                    end
         | 
| 226 | 
            -
                     | 
| 227 | 
            -
             | 
| 228 | 
            -
             | 
| 229 | 
            -
                       | 
| 230 | 
            -
             | 
| 231 | 
            -
             | 
| 232 | 
            -
             | 
| 233 | 
            -
                         | 
| 234 | 
            -
                           | 
| 235 | 
            -
                             | 
| 236 | 
            -
                               | 
| 237 | 
            -
             | 
| 238 | 
            -
             | 
| 239 | 
            -
             | 
| 240 | 
            -
             | 
| 241 | 
            -
                                 | 
| 261 | 
            +
                    begin
         | 
| 262 | 
            +
                      queues = names.map {|n| @@signal_queue[n] << handler }
         | 
| 263 | 
            +
                      ident_match = owner_ident
         | 
| 264 | 
            +
                      until try_lock
         | 
| 265 | 
            +
                        Mutex.start_watcher unless watching?
         | 
| 266 | 
            +
                        start_time = Time.now.to_f
         | 
| 267 | 
            +
                        expire_time = nil
         | 
| 268 | 
            +
                        @@redis_pool.execute(false) do |r|
         | 
| 269 | 
            +
                          r.watch(*names) do
         | 
| 270 | 
            +
                            expired_names = names.zip(r.mget(*names)).map do |name, lock_value|
         | 
| 271 | 
            +
                              if lock_value
         | 
| 272 | 
            +
                                owner, exp_id = lock_value.split ' '
         | 
| 273 | 
            +
                                exp_time = exp_id.to_f
         | 
| 274 | 
            +
                                expire_time = exp_time if expire_time.nil? || exp_time < expire_time
         | 
| 275 | 
            +
                                raise MutexError, "deadlock; recursive locking #{owner}" if owner == ident_match
         | 
| 276 | 
            +
                                if exp_time < start_time
         | 
| 277 | 
            +
                                  name
         | 
| 278 | 
            +
                                end
         | 
| 242 279 | 
             
                              end
         | 
| 243 280 | 
             
                            end
         | 
| 244 | 
            -
             | 
| 245 | 
            -
             | 
| 246 | 
            -
             | 
| 247 | 
            -
             | 
| 248 | 
            -
             | 
| 249 | 
            -
                               | 
| 281 | 
            +
                            if expire_time && expire_time < start_time
         | 
| 282 | 
            +
                              r.multi do |multi|
         | 
| 283 | 
            +
                                expired_names = expired_names.compact
         | 
| 284 | 
            +
                                multi.del(*expired_names)
         | 
| 285 | 
            +
                                multi.publish SIGNAL_QUEUE_CHANNEL, Marshal.dump(expired_names)
         | 
| 286 | 
            +
                              end
         | 
| 287 | 
            +
                            else
         | 
| 288 | 
            +
                              r.unwatch
         | 
| 250 289 | 
             
                            end
         | 
| 251 | 
            -
                          else
         | 
| 252 | 
            -
                            r.unwatch
         | 
| 253 290 | 
             
                          end
         | 
| 254 291 | 
             
                        end
         | 
| 255 | 
            -
             | 
| 256 | 
            -
             | 
| 257 | 
            -
                      timeout = block_timeout if block_timeout && block_timeout < timeout
         | 
| 292 | 
            +
                        timeout = (expire_time = expire_time.to_f) - start_time
         | 
| 293 | 
            +
                        timeout = block_timeout if block_timeout && block_timeout < timeout
         | 
| 258 294 |  | 
| 259 | 
            -
             | 
| 260 | 
            -
             | 
| 261 | 
            -
             | 
| 262 | 
            -
             | 
| 295 | 
            +
                        if !try_again && timeout > 0
         | 
| 296 | 
            +
                          timer = ::EM::Timer.new(timeout) do
         | 
| 297 | 
            +
                            timer = nil
         | 
| 298 | 
            +
                            ::EM.next_tick { fiber.resume if fiber } if fiber
         | 
| 299 | 
            +
                          end
         | 
| 300 | 
            +
                          fiber = Fiber.current
         | 
| 301 | 
            +
                          Fiber.yield
         | 
| 302 | 
            +
                          fiber = nil
         | 
| 303 | 
            +
                        end
         | 
| 304 | 
            +
                        finish_time = Time.now.to_f
         | 
| 305 | 
            +
                        if try_again || finish_time > expire_time
         | 
| 306 | 
            +
                          block_timeout-= finish_time - start_time if block_timeout
         | 
| 307 | 
            +
                          try_again = false
         | 
| 308 | 
            +
                        else
         | 
| 309 | 
            +
                          return false
         | 
| 263 310 | 
             
                        end
         | 
| 264 | 
            -
                        fiber = Fiber.current
         | 
| 265 | 
            -
                        Fiber.yield 
         | 
| 266 | 
            -
                        fiber = nil
         | 
| 267 311 | 
             
                      end
         | 
| 268 | 
            -
                       | 
| 269 | 
            -
             | 
| 270 | 
            -
             | 
| 271 | 
            -
             | 
| 272 | 
            -
                       | 
| 273 | 
            -
             | 
| 312 | 
            +
                      true
         | 
| 313 | 
            +
                    ensure
         | 
| 314 | 
            +
                      timer.cancel if timer
         | 
| 315 | 
            +
                      timer = nil
         | 
| 316 | 
            +
                      queues.each {|q| q.delete handler }
         | 
| 317 | 
            +
                      names.each {|n| @@signal_queue.delete(n) if @@signal_queue[n].empty? }
         | 
| 318 | 
            +
                    end
         | 
| 319 | 
            +
                  end
         | 
| 320 | 
            +
             | 
| 321 | 
            +
                  # Wakes up currently sleeping fiber on a mutex.
         | 
| 322 | 
            +
                  def wakeup(fiber)
         | 
| 323 | 
            +
                    fiber.resume if @slept.delete(fiber)
         | 
| 324 | 
            +
                  end
         | 
| 325 | 
            +
             | 
| 326 | 
            +
                  # for compatibility with EventMachine::Synchrony::Thread::ConditionVariable
         | 
| 327 | 
            +
                  alias_method :_wakeup, :wakeup
         | 
| 328 | 
            +
             | 
| 329 | 
            +
                  # Releases the lock and sleeps `timeout` seconds if it is given and non-nil or forever.
         | 
| 330 | 
            +
                  # Raises MutexError if mutex wasn’t locked by the current owner.
         | 
| 331 | 
            +
                  # Raises MutexTimeout if #block_timeout= was set and timeout
         | 
| 332 | 
            +
                  # occured while locking after sleep.
         | 
| 333 | 
            +
                  # If code block is provided it is executed after waking up, just before grabbing a lock.
         | 
| 334 | 
            +
                  def sleep(timeout = nil)
         | 
| 335 | 
            +
                    raise MutexError, "can't sleep #{self.class} wasn't locked" unless unlock!
         | 
| 336 | 
            +
                    start = Time.now
         | 
| 337 | 
            +
                    current = Fiber.current
         | 
| 338 | 
            +
                    @slept[current] = true
         | 
| 339 | 
            +
                    if timeout
         | 
| 340 | 
            +
                      timer = ::EM.add_timer(timeout) do
         | 
| 341 | 
            +
                        wakeup(current)
         | 
| 274 342 | 
             
                      end
         | 
| 343 | 
            +
                      Fiber.yield
         | 
| 344 | 
            +
                      ::EM.cancel_timer timer
         | 
| 345 | 
            +
                    else
         | 
| 346 | 
            +
                      Fiber.yield
         | 
| 275 347 | 
             
                    end
         | 
| 276 | 
            -
                     | 
| 277 | 
            -
             | 
| 278 | 
            -
                     | 
| 279 | 
            -
                     | 
| 280 | 
            -
                    queues.each {|q| q.delete handler }
         | 
| 281 | 
            -
                    names.each {|n| @@signal_queue.delete(n) if @@signal_queue[n].empty? }
         | 
| 348 | 
            +
                    @slept.delete current
         | 
| 349 | 
            +
                    yield if block_given?
         | 
| 350 | 
            +
                    raise MutexTimeout unless lock
         | 
| 351 | 
            +
                    Time.now - start
         | 
| 282 352 | 
             
                  end
         | 
| 283 353 |  | 
| 284 354 | 
             
                  # Execute block of code protected with semaphore.
         | 
| @@ -288,7 +358,7 @@ class Redis | |
| 288 358 | 
             
                  # If `block_timeout` or Mutex#block_timeout is set and
         | 
| 289 359 | 
             
                  # lock isn't obtained within `block_timeout` seconds this method raises
         | 
| 290 360 | 
             
                  # MutexTimeout.
         | 
| 291 | 
            -
                  def synchronize(block_timeout | 
| 361 | 
            +
                  def synchronize(block_timeout=nil)
         | 
| 292 362 | 
             
                    if lock(block_timeout)
         | 
| 293 363 | 
             
                      begin
         | 
| 294 364 | 
             
                        yield self
         | 
| @@ -300,18 +370,27 @@ class Redis | |
| 300 370 | 
             
                    end
         | 
| 301 371 | 
             
                  end
         | 
| 302 372 |  | 
| 373 | 
            +
                  # Returns true if watcher is connected
         | 
| 374 | 
            +
                  def watching?; @@watching == $$; end
         | 
| 375 | 
            +
             | 
| 376 | 
            +
                  # Returns true if watcher is connected
         | 
| 377 | 
            +
                  def self.watching?; @@watching == $$; end
         | 
| 378 | 
            +
             | 
| 303 379 | 
             
                  class << self
         | 
| 304 380 | 
             
                    def ns; @@ns; end
         | 
| 305 381 | 
             
                    def ns=(namespace); @@ns = namespace; end
         | 
| 306 382 | 
             
                    alias_method :namespace, :ns
         | 
| 307 383 | 
             
                    alias_method :'namespace=', :'ns='
         | 
| 308 | 
            -
             | 
| 384 | 
            +
             | 
| 309 385 | 
             
                    # Default value of expiration timeout in seconds.
         | 
| 310 386 | 
             
                    def default_expire; @@default_expire; end
         | 
| 311 | 
            -
             | 
| 387 | 
            +
             | 
| 312 388 | 
             
                    # Assigns default value of expiration timeout in seconds.
         | 
| 313 389 | 
             
                    # Must be > 0.
         | 
| 314 | 
            -
                    def default_expire=(value) | 
| 390 | 
            +
                    def default_expire=(value)
         | 
| 391 | 
            +
                      raise ArgumentError, "#{name}.default_expire value must be greater than 0" unless (value = value.to_f) > 0
         | 
| 392 | 
            +
                      @@default_expire = value
         | 
| 393 | 
            +
                    end
         | 
| 315 394 |  | 
| 316 395 | 
             
                    # Setup redis database and other defaults
         | 
| 317 396 | 
             
                    # MUST BE called once before any semaphore is created.
         | 
| @@ -414,8 +493,8 @@ class Redis | |
| 414 493 | 
             
                    # Mutex.setup for "lightweight" startup procedure.
         | 
| 415 494 | 
             
                    def start_watcher
         | 
| 416 495 | 
             
                      raise MutexError, "call #{self.class}::setup first" unless @@redis_watcher
         | 
| 417 | 
            -
                      return if  | 
| 418 | 
            -
                      if @@watching
         | 
| 496 | 
            +
                      return if watching?
         | 
| 497 | 
            +
                      if @@watching # Process id changed, we've been forked alive!
         | 
| 419 498 | 
             
                        @@redis_watcher = Redis.new @redis_options
         | 
| 420 499 | 
             
                        @@signal_queue.clear
         | 
| 421 500 | 
             
                      end
         | 
| @@ -456,16 +535,17 @@ class Redis | |
| 456 535 | 
             
                          else
         | 
| 457 536 | 
             
                            sleep retries > 1 ? 1 : 0.1
         | 
| 458 537 | 
             
                          end
         | 
| 459 | 
            -
                        end while  | 
| 538 | 
            +
                        end while watching?
         | 
| 460 539 | 
             
                      end.resume
         | 
| 461 540 | 
             
                      until @@watcher_subscribed
         | 
| 462 | 
            -
                        raise MutexError, "Can not establish watcher channel connection!" unless  | 
| 541 | 
            +
                        raise MutexError, "Can not establish watcher channel connection!" unless watching?
         | 
| 463 542 | 
             
                        fiber = Fiber.current
         | 
| 464 543 | 
             
                        ::EM.next_tick { fiber.resume }
         | 
| 465 544 | 
             
                        Fiber.yield
         | 
| 466 545 | 
             
                      end
         | 
| 467 546 | 
             
                    end
         | 
| 468 547 |  | 
| 548 | 
            +
                    # EM sleep helper
         | 
| 469 549 | 
             
                    def sleep(seconds)
         | 
| 470 550 | 
             
                      fiber = Fiber.current
         | 
| 471 551 | 
             
                      ::EM::Timer.new(secs) { fiber.resume }
         | 
| @@ -480,7 +560,7 @@ class Redis | |
| 480 560 | 
             
                    # Pass `true` to forcefully stop it. This might instead cause
         | 
| 481 561 | 
             
                    # MutexError to be raised in waiting fibers.
         | 
| 482 562 | 
             
                    def stop_watcher(force = false)
         | 
| 483 | 
            -
                      return unless  | 
| 563 | 
            +
                      return unless watching?
         | 
| 484 564 | 
             
                      raise MutexError, "call #{self.class}::setup first" unless @@redis_watcher
         | 
| 485 565 | 
             
                      unless @@signal_queue.empty? || force
         | 
| 486 566 | 
             
                        raise MutexError, "can't stop: active signal queue handlers"
         | 
| @@ -504,14 +584,16 @@ class Redis | |
| 504 584 | 
             
                    end
         | 
| 505 585 |  | 
| 506 586 | 
             
                    # Attempts to grab the lock and waits if it isn’t available.
         | 
| 507 | 
            -
                    # Raises MutexError if mutex was locked by the current owner | 
| 587 | 
            +
                    # Raises MutexError if mutex was locked by the current owner
         | 
| 588 | 
            +
                    # or if used before Mutex.setup.
         | 
| 589 | 
            +
                    # Raises ArgumentError on invalid options.
         | 
| 508 590 | 
             
                    # Returns instance of Redis::EM::Mutex if lock was successfully obtained.
         | 
| 509 591 | 
             
                    # Returns `nil` if lock wasn't available within `:block` seconds.
         | 
| 510 592 | 
             
                    #
         | 
| 511 | 
            -
                    #   Redis::EM::Mutex.lock(*names,  | 
| 593 | 
            +
                    #   Redis::EM::Mutex.lock(*names, options = {})
         | 
| 512 594 | 
             
                    #
         | 
| 513 595 | 
             
                    # - *names = lock identifiers - if none they are auto generated
         | 
| 514 | 
            -
                    # -  | 
| 596 | 
            +
                    # - options = hash:
         | 
| 515 597 | 
             
                    # - :name - same as name (in case *names arguments were omitted)
         | 
| 516 598 | 
             
                    # - :block - block timeout
         | 
| 517 599 | 
             
                    # - :expire - expire timeout (see: Mutex#lock and Mutex#try_lock)
         | 
| @@ -520,13 +602,14 @@ class Redis | |
| 520 602 | 
             
                      mutex = new(*args)
         | 
| 521 603 | 
             
                      mutex if mutex.lock
         | 
| 522 604 | 
             
                    end
         | 
| 605 | 
            +
             | 
| 523 606 | 
             
                    # Execute block of code protected with named semaphore.
         | 
| 524 607 | 
             
                    # Returns result of code block.
         | 
| 525 608 | 
             
                    #
         | 
| 526 | 
            -
                    #   Redis::EM::Mutex.synchronize(*names,  | 
| 609 | 
            +
                    #   Redis::EM::Mutex.synchronize(*names, options = {}, &block)
         | 
| 527 610 | 
             
                    # 
         | 
| 528 611 | 
             
                    # - *names = lock identifiers - if none they are auto generated
         | 
| 529 | 
            -
                    # -  | 
| 612 | 
            +
                    # - options = hash:
         | 
| 530 613 | 
             
                    # - :name - same as name (in case *names arguments were omitted)
         | 
| 531 614 | 
             
                    # - :block - block timeout
         | 
| 532 615 | 
             
                    # - :expire - expire timeout (see: Mutex#lock and Mutex#try_lock)
         | 
| @@ -534,13 +617,13 @@ class Redis | |
| 534 617 | 
             
                    # 
         | 
| 535 618 | 
             
                    # If `:block` is set and lock isn't obtained within `:block` seconds this method raises
         | 
| 536 619 | 
             
                    # MutexTimeout.
         | 
| 620 | 
            +
                    # Raises MutexError if used before Mutex.setup.
         | 
| 621 | 
            +
                    # Raises ArgumentError on invalid options.
         | 
| 537 622 | 
             
                    def synchronize(*args, &block)
         | 
| 538 623 | 
             
                      new(*args).synchronize(&block)
         | 
| 539 624 | 
             
                    end
         | 
| 540 625 | 
             
                  end
         | 
| 541 626 |  | 
| 542 | 
            -
                  private
         | 
| 543 | 
            -
             | 
| 544 627 | 
             
                  def owner_ident(lock_id = nil)
         | 
| 545 628 | 
             
                    if lock_id
         | 
| 546 629 | 
             
                      "#@@uuid$#$$@#{Fiber.current.__id__} #{lock_id}"
         |