lock_and_cache 4.0.4 → 6.0.1
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.
- checksums.yaml +5 -5
- data/.travis.yml +2 -1
- data/CHANGELOG +37 -0
- data/README.md +13 -10
- data/benchmarks/allowed_in_keys.rb +87 -0
- data/lib/lock_and_cache.rb +36 -15
- data/lib/lock_and_cache/action.rb +45 -18
- data/lib/lock_and_cache/key.rb +26 -6
- data/lib/lock_and_cache/version.rb +1 -1
- data/lock_and_cache.gemspec +0 -2
- data/spec/lock_and_cache/key_spec.rb +15 -5
- data/spec/lock_and_cache_spec.rb +30 -1
- data/spec/spec_helper.rb +2 -1
- metadata +4 -19
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 | 
            -
             | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 2 | 
            +
            SHA256:
         | 
| 3 | 
            +
              metadata.gz: c89613cc8fdd7ed23861149ccfc49e664af988d6b917757f09c18b18b5415f0b
         | 
| 4 | 
            +
              data.tar.gz: 286efd9473fedfbd8e7de5175d9a4ec374bde72757d103bd9c964faa6af1bc70
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 15f13a8204942588e436ddd5a0eec963148d179a10bf3268401316b8479f2857f50e5e83a47d324765c57f614c7e11b2656b7643c139ebbce6e6b67f1c2969ba
         | 
| 7 | 
            +
              data.tar.gz: 28d901547a8b96a9065514151149176eef91c5ed9fb76274b7a7e655bba4f8f584b6246e20534342022ae1bddbfe4918cbe73cedcb7936fa886962b117c698bd
         | 
    
        data/.travis.yml
    CHANGED
    
    
    
        data/CHANGELOG
    CHANGED
    
    | @@ -1,3 +1,40 @@ | |
| 1 | 
            +
            6.0.1
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            * Enhancements
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              * Use Redis#exists? to avoid deprec warning
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            6.0.0
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            * Breaking changes
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              * Set lock_storage and cache_storage separately
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            5.0.0
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            * Enhancements / breaking changes
         | 
| 16 | 
            +
             | 
| 17 | 
            +
              * Propagate errors to all waiters up to 1 second after the error
         | 
| 18 | 
            +
              * Stop using redlock, just use plain single-node Redis locking
         | 
| 19 | 
            +
             | 
| 20 | 
            +
            4.0.6
         | 
| 21 | 
            +
             | 
| 22 | 
            +
            * ?
         | 
| 23 | 
            +
             | 
| 24 | 
            +
              * Don't test on ruby 2.1
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            * Enhancements
         | 
| 27 | 
            +
             | 
| 28 | 
            +
              * LockAndCache.cached?(*key_parts) to check if a value is cached
         | 
| 29 | 
            +
             | 
| 30 | 
            +
            4.0.5 / 2017-04-01
         | 
| 31 | 
            +
             | 
| 32 | 
            +
            * Enhancements
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              * allow dates and times in keys
         | 
| 35 | 
            +
              * Test on ruby 2.3.0 and 2.4.1
         | 
| 36 | 
            +
              * 2x faster key generation
         | 
| 37 | 
            +
             | 
| 1 38 | 
             
            4.0.4 / 2016-04-11
         | 
| 2 39 |  | 
| 3 40 | 
             
            * Bug fixes
         | 
    
        data/README.md
    CHANGED
    
    | @@ -2,7 +2,6 @@ | |
| 2 2 |  | 
| 3 3 | 
             
            [](https://travis-ci.org/seamusabshere/lock_and_cache)
         | 
| 4 4 | 
             
            [](https://codeclimate.com/github/seamusabshere/lock_and_cache)
         | 
| 5 | 
            -
            [](https://gemnasium.com/seamusabshere/lock_and_cache)
         | 
| 6 5 | 
             
            [](http://badge.fury.io/rb/lock_and_cache)
         | 
| 7 6 | 
             
            [](https://hakiri.io/github/seamusabshere/lock_and_cache/master)
         | 
| 8 7 | 
             
            [](http://inch-ci.org/github/seamusabshere/lock_and_cache)
         | 
| @@ -14,7 +13,8 @@ Most caching libraries don't do locking, meaning that >1 process can be calculat | |
| 14 13 | 
             
            ## Quickstart
         | 
| 15 14 |  | 
| 16 15 | 
             
            ```ruby
         | 
| 17 | 
            -
            LockAndCache. | 
| 16 | 
            +
            LockAndCache.lock_storage = Redis.new db: 3
         | 
| 17 | 
            +
            LockAndCache.cache_storage = Redis.new db: 4
         | 
| 18 18 |  | 
| 19 19 | 
             
            LockAndCache.lock_and_cache(:stock_price, {company: 'MSFT', date: '2015-05-05'}, expires: 10, nil_expires: 1) do
         | 
| 20 20 | 
             
              # get yer stock quote
         | 
| @@ -27,9 +27,9 @@ end | |
| 27 27 |  | 
| 28 28 | 
             
            ## Sponsor
         | 
| 29 29 |  | 
| 30 | 
            -
            <p><a href=" | 
| 30 | 
            +
            <p><a href="https://www.faraday.io"><img src="https://s3.amazonaws.com/faraday-assets/files/img/logo.svg" alt="Faraday logo"/></a></p>
         | 
| 31 31 |  | 
| 32 | 
            -
            We use [`lock_and_cache`](https://github.com/seamusabshere/lock_and_cache) for [ | 
| 32 | 
            +
            We use [`lock_and_cache`](https://github.com/seamusabshere/lock_and_cache) for [B2C customer intelligence at Faraday](https://www.faraday.io).
         | 
| 33 33 |  | 
| 34 34 | 
             
            ## TOC
         | 
| 35 35 |  | 
| @@ -70,21 +70,24 @@ We use [`lock_and_cache`](https://github.com/seamusabshere/lock_and_cache) for [ | |
| 70 70 |  | 
| 71 71 | 
             
            As you can see, most caching libraries only take care of (1) and (4) (well, and (5) of course).
         | 
| 72 72 |  | 
| 73 | 
            +
            If an error is raised during calculation, that error is propagated to all waiters for 1 second.
         | 
| 74 | 
            +
             | 
| 73 75 | 
             
            ## Practice
         | 
| 74 76 |  | 
| 75 77 | 
             
            ### Setup
         | 
| 76 78 |  | 
| 77 79 | 
             
            ```ruby
         | 
| 78 | 
            -
            LockAndCache. | 
| 80 | 
            +
            LockAndCache.lock_storage = Redis.new db: 3
         | 
| 81 | 
            +
            LockAndCache.cache_storage = Redis.new db: 4
         | 
| 79 82 | 
             
            ```
         | 
| 80 83 |  | 
| 81 84 | 
             
            It will use this redis for both locking and storing cached values.
         | 
| 82 85 |  | 
| 83 86 | 
             
            ### Locking
         | 
| 84 87 |  | 
| 85 | 
            -
             | 
| 88 | 
            +
            Just uses Redis naive locking with NX.
         | 
| 86 89 |  | 
| 87 | 
            -
             | 
| 90 | 
            +
            A 32-second heartbeat is used that will clear the lock if a process is killed.
         | 
| 88 91 |  | 
| 89 92 | 
             
            ### Caching
         | 
| 90 93 |  | 
| @@ -189,7 +192,7 @@ Most caching libraries don't do locking, meaning that >1 process can be calculat | |
| 189 192 |  | 
| 190 193 | 
             
            ### Heartbeat
         | 
| 191 194 |  | 
| 192 | 
            -
            If the process holding the lock dies, we automatically remove the lock so somebody else can do it (using heartbeats | 
| 195 | 
            +
            If the process holding the lock dies, we automatically remove the lock so somebody else can do it (using heartbeats).
         | 
| 193 196 |  | 
| 194 197 | 
             
            ### Context mode
         | 
| 195 198 |  | 
| @@ -203,14 +206,14 @@ You can expire nil values with a different timeout (`nil_expires`) than other va | |
| 203 206 |  | 
| 204 207 | 
             
            ## Tunables
         | 
| 205 208 |  | 
| 206 | 
            -
            * `LockAndCache. | 
| 209 | 
            +
            * `LockAndCache.lock_storage=[redis]`
         | 
| 210 | 
            +
            * `LockAndCache.cache_storage=[redis]`
         | 
| 207 211 | 
             
            * `ENV['LOCK_AND_CACHE_DEBUG']='true'` if you want some debugging output on `$stderr`
         | 
| 208 212 |  | 
| 209 213 | 
             
            ## Few dependencies
         | 
| 210 214 |  | 
| 211 215 | 
             
            * [activesupport](https://rubygems.org/gems/activesupport) (come on, it's the bomb)
         | 
| 212 216 | 
             
            * [redis](https://github.com/redis/redis-rb)
         | 
| 213 | 
            -
            * [redlock](https://github.com/leandromoreira/redlock-rb)
         | 
| 214 217 |  | 
| 215 218 | 
             
            ## Known issues
         | 
| 216 219 |  | 
| @@ -0,0 +1,87 @@ | |
| 1 | 
            +
            require 'set'
         | 
| 2 | 
            +
            require 'date'
         | 
| 3 | 
            +
            require 'benchmark/ips'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            ALLOWED_IN_KEYS = [
         | 
| 6 | 
            +
              ::String,
         | 
| 7 | 
            +
              ::Symbol,
         | 
| 8 | 
            +
              ::Numeric,
         | 
| 9 | 
            +
              ::TrueClass,
         | 
| 10 | 
            +
              ::FalseClass,
         | 
| 11 | 
            +
              ::NilClass,
         | 
| 12 | 
            +
              ::Integer,
         | 
| 13 | 
            +
              ::Float,
         | 
| 14 | 
            +
              ::Date,
         | 
| 15 | 
            +
              ::DateTime,
         | 
| 16 | 
            +
              ::Time,
         | 
| 17 | 
            +
            ].to_set
         | 
| 18 | 
            +
            parts = RUBY_VERSION.split('.').map(&:to_i)
         | 
| 19 | 
            +
            unless parts[0] >= 2 and parts[1] >= 4
         | 
| 20 | 
            +
              ALLOWED_IN_KEYS << ::Fixnum
         | 
| 21 | 
            +
              ALLOWED_IN_KEYS << ::Bignum  
         | 
| 22 | 
            +
            end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            EXAMPLES = [
         | 
| 25 | 
            +
              'hi',
         | 
| 26 | 
            +
              :there,
         | 
| 27 | 
            +
              123,
         | 
| 28 | 
            +
              123.54,
         | 
| 29 | 
            +
              1e99,
         | 
| 30 | 
            +
              123456789 ** 2,
         | 
| 31 | 
            +
              1e999,
         | 
| 32 | 
            +
              true,
         | 
| 33 | 
            +
              false,
         | 
| 34 | 
            +
              nil,
         | 
| 35 | 
            +
              Date.new(2015,1,1),
         | 
| 36 | 
            +
              Time.now,
         | 
| 37 | 
            +
              DateTime.now,
         | 
| 38 | 
            +
              Mutex,
         | 
| 39 | 
            +
              Mutex.new,
         | 
| 40 | 
            +
              Benchmark,
         | 
| 41 | 
            +
              { hi: :world },
         | 
| 42 | 
            +
              [[]],
         | 
| 43 | 
            +
              Fixnum,
         | 
| 44 | 
            +
              Struct,
         | 
| 45 | 
            +
              Struct.new(:a),
         | 
| 46 | 
            +
              Struct.new(:a).new(123)
         | 
| 47 | 
            +
            ]
         | 
| 48 | 
            +
            EXAMPLES.each do |example|
         | 
| 49 | 
            +
              puts "#{example} -> #{example.class}"
         | 
| 50 | 
            +
            end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
            puts
         | 
| 53 | 
            +
             | 
| 54 | 
            +
            [
         | 
| 55 | 
            +
              Date.new(2015,1,1),
         | 
| 56 | 
            +
              Time.now,
         | 
| 57 | 
            +
              DateTime.now,
         | 
| 58 | 
            +
            ].each do |x|
         | 
| 59 | 
            +
                puts x.to_s
         | 
| 60 | 
            +
            end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
            puts
         | 
| 63 | 
            +
             | 
| 64 | 
            +
            EXAMPLES.each do |example|
         | 
| 65 | 
            +
              a = ALLOWED_IN_KEYS.any? { |thing| example.is_a?(thing) }
         | 
| 66 | 
            +
              b = ALLOWED_IN_KEYS.include? example.class
         | 
| 67 | 
            +
              unless a == b
         | 
| 68 | 
            +
                raise "#{example.inspect}: #{a.inspect} vs #{b.inspect}"
         | 
| 69 | 
            +
              end
         | 
| 70 | 
            +
            end
         | 
| 71 | 
            +
             | 
| 72 | 
            +
            Benchmark.ips do |x|
         | 
| 73 | 
            +
              x.report("any") do
         | 
| 74 | 
            +
                example = EXAMPLES.sample
         | 
| 75 | 
            +
                y = ALLOWED_IN_KEYS.any? { |thing| example.is_a?(thing) }
         | 
| 76 | 
            +
                a = 1
         | 
| 77 | 
            +
                y
         | 
| 78 | 
            +
              end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
              x.report("include") do
         | 
| 81 | 
            +
                example = EXAMPLES.sample
         | 
| 82 | 
            +
                y = ALLOWED_IN_KEYS.include? example.class
         | 
| 83 | 
            +
                a = 1
         | 
| 84 | 
            +
                y
         | 
| 85 | 
            +
              end
         | 
| 86 | 
            +
             | 
| 87 | 
            +
            end
         | 
    
        data/lib/lock_and_cache.rb
    CHANGED
    
    | @@ -3,7 +3,6 @@ require 'timeout' | |
| 3 3 | 
             
            require 'digest/sha1'
         | 
| 4 4 | 
             
            require 'base64'
         | 
| 5 5 | 
             
            require 'redis'
         | 
| 6 | 
            -
            require 'redlock'
         | 
| 7 6 | 
             
            require 'active_support'
         | 
| 8 7 | 
             
            require 'active_support/core_ext'
         | 
| 9 8 |  | 
| @@ -21,16 +20,26 @@ module LockAndCache | |
| 21 20 |  | 
| 22 21 | 
             
              class TimeoutWaitingForLock < StandardError; end
         | 
| 23 22 |  | 
| 24 | 
            -
              # @param redis_connection [Redis] A redis connection to be used for lock  | 
| 25 | 
            -
              def LockAndCache. | 
| 23 | 
            +
              # @param redis_connection [Redis] A redis connection to be used for lock storage
         | 
| 24 | 
            +
              def LockAndCache.lock_storage=(redis_connection)
         | 
| 26 25 | 
             
                raise "only redis for now" unless redis_connection.class.to_s == 'Redis'
         | 
| 27 | 
            -
                @ | 
| 28 | 
            -
                @lock_manager = Redlock::Client.new [redis_connection], retry_count: 1
         | 
| 26 | 
            +
                @lock_storage = redis_connection
         | 
| 29 27 | 
             
              end
         | 
| 30 28 |  | 
| 31 29 | 
             
              # @return [Redis] The redis connection used for lock and cached value storage
         | 
| 32 | 
            -
              def LockAndCache. | 
| 33 | 
            -
                @ | 
| 30 | 
            +
              def LockAndCache.lock_storage
         | 
| 31 | 
            +
                @lock_storage
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              # @param redis_connection [Redis] A redis connection to be used for cached value storage
         | 
| 35 | 
            +
              def LockAndCache.cache_storage=(redis_connection)
         | 
| 36 | 
            +
                raise "only redis for now" unless redis_connection.class.to_s == 'Redis'
         | 
| 37 | 
            +
                @cache_storage = redis_connection
         | 
| 38 | 
            +
              end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
              # @return [Redis] The redis connection used for cached value storage
         | 
| 41 | 
            +
              def LockAndCache.cache_storage
         | 
| 42 | 
            +
                @cache_storage
         | 
| 34 43 | 
             
              end
         | 
| 35 44 |  | 
| 36 45 | 
             
              # @param logger [Logger] A logger.
         | 
| @@ -43,13 +52,22 @@ module LockAndCache | |
| 43 52 | 
             
                @logger
         | 
| 44 53 | 
             
              end
         | 
| 45 54 |  | 
| 46 | 
            -
              # Flush LockAndCache's storage.
         | 
| 55 | 
            +
              # Flush LockAndCache's cached value storage.
         | 
| 47 56 | 
             
              #
         | 
| 48 57 | 
             
              # @note If you are sharing a redis database, it will clear it...
         | 
| 49 58 | 
             
              #
         | 
| 50 59 | 
             
              # @note If you want to clear a single key, try `LockAndCache.clear(key)` (standalone mode) or `#lock_and_cache_clear(method_id, *key_parts)` in context mode.
         | 
| 51 | 
            -
              def LockAndCache. | 
| 52 | 
            -
                 | 
| 60 | 
            +
              def LockAndCache.flush_cache
         | 
| 61 | 
            +
                cache_storage.flushdb
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
             | 
| 64 | 
            +
              # Flush LockAndCache's lock storage.
         | 
| 65 | 
            +
              #
         | 
| 66 | 
            +
              # @note If you are sharing a redis database, it will clear it...
         | 
| 67 | 
            +
              #
         | 
| 68 | 
            +
              # @note If you want to clear a single key, try `LockAndCache.clear(key)` (standalone mode) or `#lock_and_cache_clear(method_id, *key_parts)` in context mode.
         | 
| 69 | 
            +
              def LockAndCache.flush_locks
         | 
| 70 | 
            +
                lock_storage.flushdb
         | 
| 53 71 | 
             
              end
         | 
| 54 72 |  | 
| 55 73 | 
             
              # Lock and cache based on a key.
         | 
| @@ -83,6 +101,14 @@ module LockAndCache | |
| 83 101 | 
             
                key.locked?
         | 
| 84 102 | 
             
              end
         | 
| 85 103 |  | 
| 104 | 
            +
              # Check if a key is cached already
         | 
| 105 | 
            +
              #
         | 
| 106 | 
            +
              # @note Standalone mode. See also "context mode," where you mix LockAndCache into a class and call it from within its methods.
         | 
| 107 | 
            +
              def LockAndCache.cached?(*key_parts)
         | 
| 108 | 
            +
                key = LockAndCache::Key.new key_parts
         | 
| 109 | 
            +
                key.cached?
         | 
| 110 | 
            +
              end
         | 
| 111 | 
            +
             | 
| 86 112 | 
             
              # @param seconds [Numeric] Maximum wait time to get a lock
         | 
| 87 113 | 
             
              #
         | 
| 88 114 | 
             
              # @note Can be overridden by putting `max_lock_wait:` in your call to `#lock_and_cache`
         | 
| @@ -109,11 +135,6 @@ module LockAndCache | |
| 109 135 | 
             
                @heartbeat_expires || DEFAULT_HEARTBEAT_EXPIRES
         | 
| 110 136 | 
             
              end
         | 
| 111 137 |  | 
| 112 | 
            -
              # @private
         | 
| 113 | 
            -
              def LockAndCache.lock_manager
         | 
| 114 | 
            -
                @lock_manager
         | 
| 115 | 
            -
              end
         | 
| 116 | 
            -
             | 
| 117 138 | 
             
              # Check if a method is locked on an object.
         | 
| 118 139 | 
             
              #
         | 
| 119 140 | 
             
              # @note Subject mode - this is expected to be called on an object whose class has LockAndCache mixed in. See also standalone mode.
         | 
| @@ -1,6 +1,8 @@ | |
| 1 1 | 
             
            module LockAndCache
         | 
| 2 2 | 
             
              # @private
         | 
| 3 3 | 
             
              class Action
         | 
| 4 | 
            +
                ERROR_MAGIC_KEY = :lock_and_cache_error
         | 
| 5 | 
            +
             | 
| 4 6 | 
             
                attr_reader :key
         | 
| 5 7 | 
             
                attr_reader :options
         | 
| 6 8 | 
             
                attr_reader :blk
         | 
| @@ -30,8 +32,21 @@ module LockAndCache | |
| 30 32 | 
             
                  @lock_digest ||= key.lock_digest
         | 
| 31 33 | 
             
                end
         | 
| 32 34 |  | 
| 33 | 
            -
                def  | 
| 34 | 
            -
                  @ | 
| 35 | 
            +
                def lock_storage
         | 
| 36 | 
            +
                  @lock_storage ||= LockAndCache.lock_storage or raise("must set LockAndCache.lock_storage=[Redis]")
         | 
| 37 | 
            +
                end
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                def cache_storage
         | 
| 40 | 
            +
                  @cache_storage ||= LockAndCache.cache_storage or raise("must set LockAndCache.cache_storage=[Redis]")
         | 
| 41 | 
            +
                end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                def load_existing(existing)
         | 
| 44 | 
            +
                  v = ::Marshal.load(existing)
         | 
| 45 | 
            +
                  if v.is_a?(::Hash) and (founderr = v[ERROR_MAGIC_KEY])
         | 
| 46 | 
            +
                    raise "Another LockAndCache process raised #{founderr}"
         | 
| 47 | 
            +
                  else
         | 
| 48 | 
            +
                    v
         | 
| 49 | 
            +
                  end
         | 
| 35 50 | 
             
                end
         | 
| 36 51 |  | 
| 37 52 | 
             
                def perform
         | 
| @@ -40,24 +55,25 @@ module LockAndCache | |
| 40 55 | 
             
                  raise "heartbeat_expires must be >= 2 seconds" unless heartbeat_expires >= 2
         | 
| 41 56 | 
             
                  heartbeat_frequency = (heartbeat_expires / 2).ceil
         | 
| 42 57 | 
             
                  LockAndCache.logger.debug { "[lock_and_cache] A1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
         | 
| 43 | 
            -
                  if  | 
| 44 | 
            -
                    return  | 
| 58 | 
            +
                  if cache_storage.exists?(digest) and (existing = cache_storage.get(digest)).is_a?(String)
         | 
| 59 | 
            +
                    return load_existing(existing)
         | 
| 45 60 | 
             
                  end
         | 
| 46 61 | 
             
                  LockAndCache.logger.debug { "[lock_and_cache] B1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
         | 
| 47 62 | 
             
                  retval = nil
         | 
| 48 | 
            -
                   | 
| 49 | 
            -
                   | 
| 63 | 
            +
                  lock_secret = SecureRandom.hex 16
         | 
| 64 | 
            +
                  acquired = false
         | 
| 50 65 | 
             
                  begin
         | 
| 51 66 | 
             
                    Timeout.timeout(max_lock_wait, TimeoutWaitingForLock) do
         | 
| 52 | 
            -
                      until  | 
| 67 | 
            +
                      until lock_storage.set(lock_digest, lock_secret, nx: true, ex: heartbeat_expires)
         | 
| 53 68 | 
             
                        LockAndCache.logger.debug { "[lock_and_cache] C1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
         | 
| 54 69 | 
             
                        sleep rand
         | 
| 55 70 | 
             
                      end
         | 
| 71 | 
            +
                      acquired = true
         | 
| 56 72 | 
             
                    end
         | 
| 57 73 | 
             
                    LockAndCache.logger.debug { "[lock_and_cache] D1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
         | 
| 58 | 
            -
                    if  | 
| 74 | 
            +
                    if cache_storage.exists?(digest) and (existing = cache_storage.get(digest)).is_a?(String)
         | 
| 59 75 | 
             
                      LockAndCache.logger.debug { "[lock_and_cache] E1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
         | 
| 60 | 
            -
                      retval =  | 
| 76 | 
            +
                      retval = load_existing existing
         | 
| 61 77 | 
             
                    end
         | 
| 62 78 | 
             
                    unless retval
         | 
| 63 79 | 
             
                      LockAndCache.logger.debug { "[lock_and_cache] F1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
         | 
| @@ -70,39 +86,50 @@ module LockAndCache | |
| 70 86 | 
             
                            sleep heartbeat_frequency
         | 
| 71 87 | 
             
                            break if done
         | 
| 72 88 | 
             
                            LockAndCache.logger.debug { "[lock_and_cache] heartbeat2 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
         | 
| 73 | 
            -
                             | 
| 89 | 
            +
                            # FIXME use lua to check the value
         | 
| 90 | 
            +
                            raise "unexpectedly lost lock for #{key.debug}" unless lock_storage.get(lock_digest) == lock_secret
         | 
| 91 | 
            +
                            lock_storage.set lock_digest, lock_secret, xx: true, ex: heartbeat_expires
         | 
| 74 92 | 
             
                          end
         | 
| 75 93 | 
             
                        end
         | 
| 76 | 
            -
                         | 
| 77 | 
            -
             | 
| 94 | 
            +
                        begin
         | 
| 95 | 
            +
                          retval = blk.call
         | 
| 96 | 
            +
                          retval.nil? ? set_nil : set_non_nil(retval)
         | 
| 97 | 
            +
                        rescue
         | 
| 98 | 
            +
                          set_error $!
         | 
| 99 | 
            +
                          raise
         | 
| 100 | 
            +
                        end
         | 
| 78 101 | 
             
                      ensure
         | 
| 79 102 | 
             
                        done = true
         | 
| 80 103 | 
             
                        lock_extender.join if lock_extender.status.nil?
         | 
| 81 104 | 
             
                      end
         | 
| 82 105 | 
             
                    end
         | 
| 83 106 | 
             
                  ensure
         | 
| 84 | 
            -
                     | 
| 107 | 
            +
                    lock_storage.del lock_digest if acquired
         | 
| 85 108 | 
             
                  end
         | 
| 86 109 | 
             
                  retval
         | 
| 87 110 | 
             
                end
         | 
| 88 111 |  | 
| 112 | 
            +
                def set_error(exception)
         | 
| 113 | 
            +
                  cache_storage.set digest, ::Marshal.dump(ERROR_MAGIC_KEY => exception.message), ex: 1
         | 
| 114 | 
            +
                end
         | 
| 115 | 
            +
             | 
| 89 116 | 
             
                NIL = Marshal.dump nil
         | 
| 90 117 | 
             
                def set_nil
         | 
| 91 118 | 
             
                  if nil_expires
         | 
| 92 | 
            -
                     | 
| 119 | 
            +
                    cache_storage.set digest, NIL, ex: nil_expires
         | 
| 93 120 | 
             
                  elsif expires
         | 
| 94 | 
            -
                     | 
| 121 | 
            +
                    cache_storage.set digest, NIL, ex: expires
         | 
| 95 122 | 
             
                  else
         | 
| 96 | 
            -
                     | 
| 123 | 
            +
                    cache_storage.set digest, NIL
         | 
| 97 124 | 
             
                  end
         | 
| 98 125 | 
             
                end
         | 
| 99 126 |  | 
| 100 127 | 
             
                def set_non_nil(retval)
         | 
| 101 128 | 
             
                  raise "expected not null #{retval.inspect}" if retval.nil?
         | 
| 102 129 | 
             
                  if expires
         | 
| 103 | 
            -
                     | 
| 130 | 
            +
                    cache_storage.set digest, ::Marshal.dump(retval), ex: expires
         | 
| 104 131 | 
             
                  else
         | 
| 105 | 
            -
                     | 
| 132 | 
            +
                    cache_storage.set digest, ::Marshal.dump(retval)
         | 
| 106 133 | 
             
                  end
         | 
| 107 134 | 
             
                end
         | 
| 108 135 | 
             
              end
         | 
    
        data/lib/lock_and_cache/key.rb
    CHANGED
    
    | @@ -1,3 +1,5 @@ | |
| 1 | 
            +
            require 'date'
         | 
| 2 | 
            +
             | 
| 1 3 | 
             
            module LockAndCache
         | 
| 2 4 | 
             
              # @private
         | 
| 3 5 | 
             
              class Key
         | 
| @@ -22,8 +24,11 @@ module LockAndCache | |
| 22 24 | 
             
                  #
         | 
| 23 25 | 
             
                  # Recursively extract id from obj. Calls #lock_and_cache_key if available, otherwise #id
         | 
| 24 26 | 
             
                  def extract_obj_id(obj)
         | 
| 25 | 
            -
                     | 
| 27 | 
            +
                    klass = obj.class
         | 
| 28 | 
            +
                    if ALLOWED_IN_KEYS.include?(klass)
         | 
| 26 29 | 
             
                      obj
         | 
| 30 | 
            +
                    elsif DATE.include?(klass)
         | 
| 31 | 
            +
                      obj.to_s
         | 
| 27 32 | 
             
                    elsif obj.respond_to?(:lock_and_cache_key)
         | 
| 28 33 | 
             
                      extract_obj_id obj.lock_and_cache_key
         | 
| 29 34 | 
             
                    elsif obj.respond_to?(:id)
         | 
| @@ -43,7 +48,19 @@ module LockAndCache | |
| 43 48 | 
             
                  ::TrueClass,
         | 
| 44 49 | 
             
                  ::FalseClass,
         | 
| 45 50 | 
             
                  ::NilClass,
         | 
| 46 | 
            -
             | 
| 51 | 
            +
                  ::Integer,
         | 
| 52 | 
            +
                  ::Float,
         | 
| 53 | 
            +
                ].to_set
         | 
| 54 | 
            +
                parts = ::RUBY_VERSION.split('.').map(&:to_i)
         | 
| 55 | 
            +
                unless parts[0] >= 2 and parts[1] >= 4
         | 
| 56 | 
            +
                  ALLOWED_IN_KEYS << ::Fixnum
         | 
| 57 | 
            +
                  ALLOWED_IN_KEYS << ::Bignum  
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
                DATE = [
         | 
| 60 | 
            +
                  ::Date,
         | 
| 61 | 
            +
                  ::DateTime,
         | 
| 62 | 
            +
                  ::Time,
         | 
| 63 | 
            +
                ].to_set
         | 
| 47 64 | 
             
                METHOD_NAME_IN_CALLER = /in `([^']+)'/
         | 
| 48 65 |  | 
| 49 66 | 
             
                attr_reader :context
         | 
| @@ -81,14 +98,17 @@ module LockAndCache | |
| 81 98 | 
             
                end
         | 
| 82 99 |  | 
| 83 100 | 
             
                def locked?
         | 
| 84 | 
            -
                  LockAndCache. | 
| 101 | 
            +
                  LockAndCache.lock_storage.exists? lock_digest
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                def cached?
         | 
| 105 | 
            +
                  LockAndCache.cache_storage.exists? digest
         | 
| 85 106 | 
             
                end
         | 
| 86 107 |  | 
| 87 108 | 
             
                def clear
         | 
| 88 109 | 
             
                  LockAndCache.logger.debug { "[lock_and_cache] clear #{debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" }
         | 
| 89 | 
            -
                   | 
| 90 | 
            -
                   | 
| 91 | 
            -
                  storage.del lock_digest
         | 
| 110 | 
            +
                  LockAndCache.cache_storage.del digest
         | 
| 111 | 
            +
                  LockAndCache.lock_storage.del lock_digest
         | 
| 92 112 | 
             
                end
         | 
| 93 113 |  | 
| 94 114 | 
             
                alias debug key
         | 
    
        data/lock_and_cache.gemspec
    CHANGED
    
    | @@ -20,8 +20,6 @@ Gem::Specification.new do |spec| | |
| 20 20 |  | 
| 21 21 | 
             
              spec.add_runtime_dependency 'activesupport'
         | 
| 22 22 | 
             
              spec.add_runtime_dependency 'redis'
         | 
| 23 | 
            -
              # temporary until https://github.com/leandromoreira/redlock-rb/pull/20 is merged
         | 
| 24 | 
            -
              spec.add_runtime_dependency 'redlock', '>=0.1.3'
         | 
| 25 23 |  | 
| 26 24 | 
             
              spec.add_development_dependency 'pry'
         | 
| 27 25 | 
             
              spec.add_development_dependency 'bundler', '~> 1.6'
         | 
| @@ -21,13 +21,23 @@ describe LockAndCache::Key do | |
| 21 21 | 
             
                  expect(described_class.new(a: 1).send(:parts)).to eq(described_class.new([[:a, 1]]).send(:parts))
         | 
| 22 22 | 
             
                end
         | 
| 23 23 |  | 
| 24 | 
            +
                now = Time.now
         | 
| 25 | 
            +
                today = Date.today
         | 
| 24 26 | 
             
                {
         | 
| 25 27 | 
             
                  [1]                                                => [1],
         | 
| 26 | 
            -
                  [ | 
| 27 | 
            -
                  [[ | 
| 28 | 
            -
                  [[ | 
| 29 | 
            -
                  [[ | 
| 30 | 
            -
                   | 
| 28 | 
            +
                  ['you']                                            => ['you'],
         | 
| 29 | 
            +
                  [['you']]                                          => [['you']],
         | 
| 30 | 
            +
                  [['you'], "person"]                                => [['you'], "person"],
         | 
| 31 | 
            +
                  [['you'], {:silly=>:person}]                       => [['you'], [[:silly, :person]] ],
         | 
| 32 | 
            +
                  [now]                                              => [now.to_s],
         | 
| 33 | 
            +
                  [[now]]                                            => [[now.to_s]],
         | 
| 34 | 
            +
                  [today]                                            => [today.to_s],
         | 
| 35 | 
            +
                  [[today]]                                          => [[today.to_s]],
         | 
| 36 | 
            +
                  { hi: 'you' }                                      => [[:hi, 'you']],
         | 
| 37 | 
            +
                  { hi: 123 }                                        => [[:hi, 123]],
         | 
| 38 | 
            +
                  { hi: 123.0 }                                      => [[:hi, 123.0]],
         | 
| 39 | 
            +
                  { hi: now }                                        => [[:hi, now.to_s]],
         | 
| 40 | 
            +
                  { hi: today }                                      => [[:hi, today.to_s]],
         | 
| 31 41 | 
             
                  [KeyTestId.new]                                    => ['id'],
         | 
| 32 42 | 
             
                  [[KeyTestId.new]]                                  => [['id']],
         | 
| 33 43 | 
             
                  { a: KeyTestId.new }                               => [[:a, "id"]],
         | 
    
        data/spec/lock_and_cache_spec.rb
    CHANGED
    
    | @@ -140,7 +140,8 @@ end | |
| 140 140 |  | 
| 141 141 | 
             
            describe LockAndCache do
         | 
| 142 142 | 
             
              before do
         | 
| 143 | 
            -
                LockAndCache. | 
| 143 | 
            +
                LockAndCache.flush_locks
         | 
| 144 | 
            +
                LockAndCache.flush_cache
         | 
| 144 145 | 
             
              end
         | 
| 145 146 |  | 
| 146 147 | 
             
              it 'has a version number' do
         | 
| @@ -337,6 +338,34 @@ describe LockAndCache do | |
| 337 338 | 
             
                  expect(count).to eq(1)
         | 
| 338 339 | 
             
                end
         | 
| 339 340 |  | 
| 341 | 
            +
                it 'really caches' do
         | 
| 342 | 
            +
                  expect(LockAndCache.lock_and_cache('hello') { :red }).to eq(:red)
         | 
| 343 | 
            +
                  expect(LockAndCache.lock_and_cache('hello') { raise(Exception.new("stop")) }).to eq(:red)
         | 
| 344 | 
            +
                end
         | 
| 345 | 
            +
             | 
| 346 | 
            +
                it 'caches errors (briefly)' do
         | 
| 347 | 
            +
                  count = 0
         | 
| 348 | 
            +
                  expect {
         | 
| 349 | 
            +
                    LockAndCache.lock_and_cache('hello') { count += 1; raise("stop") }
         | 
| 350 | 
            +
                  }.to raise_error(/stop/)
         | 
| 351 | 
            +
                  expect(count).to eq(1)
         | 
| 352 | 
            +
                  expect {
         | 
| 353 | 
            +
                    LockAndCache.lock_and_cache('hello') { count += 1; raise("no no not me") }
         | 
| 354 | 
            +
                  }.to raise_error(/LockAndCache.*stop/)
         | 
| 355 | 
            +
                  expect(count).to eq(1)
         | 
| 356 | 
            +
                  sleep 1
         | 
| 357 | 
            +
                  expect {
         | 
| 358 | 
            +
                    LockAndCache.lock_and_cache('hello') { count += 1; raise("retrying") }
         | 
| 359 | 
            +
                  }.to raise_error(/retrying/)
         | 
| 360 | 
            +
                  expect(count).to eq(2)
         | 
| 361 | 
            +
                end
         | 
| 362 | 
            +
             | 
| 363 | 
            +
                it "can be queried for cached?" do
         | 
| 364 | 
            +
                  expect(LockAndCache.cached?('hello')).to be_falsy
         | 
| 365 | 
            +
                  LockAndCache.lock_and_cache('hello') { nil }
         | 
| 366 | 
            +
                  expect(LockAndCache.cached?('hello')).to be_truthy
         | 
| 367 | 
            +
                end
         | 
| 368 | 
            +
             | 
| 340 369 | 
             
                it 'allows expiry' do
         | 
| 341 370 | 
             
                  count = 0
         | 
| 342 371 | 
             
                  expect(LockAndCache.lock_and_cache('hello', expires: 1) { count += 1 }).to eq(1)
         | 
    
        data/spec/spec_helper.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | @@ -1,14 +1,14 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: lock_and_cache
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version:  | 
| 4 | 
            +
              version: 6.0.1
         | 
| 5 5 | 
             
            platform: ruby
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Seamus Abshere
         | 
| 8 8 | 
             
            autorequire: 
         | 
| 9 9 | 
             
            bindir: bin
         | 
| 10 10 | 
             
            cert_chain: []
         | 
| 11 | 
            -
            date:  | 
| 11 | 
            +
            date: 2020-10-06 00:00:00.000000000 Z
         | 
| 12 12 | 
             
            dependencies:
         | 
| 13 13 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 14 14 | 
             
              name: activesupport
         | 
| @@ -38,20 +38,6 @@ dependencies: | |
| 38 38 | 
             
                - - ">="
         | 
| 39 39 | 
             
                  - !ruby/object:Gem::Version
         | 
| 40 40 | 
             
                    version: '0'
         | 
| 41 | 
            -
            - !ruby/object:Gem::Dependency
         | 
| 42 | 
            -
              name: redlock
         | 
| 43 | 
            -
              requirement: !ruby/object:Gem::Requirement
         | 
| 44 | 
            -
                requirements:
         | 
| 45 | 
            -
                - - ">="
         | 
| 46 | 
            -
                  - !ruby/object:Gem::Version
         | 
| 47 | 
            -
                    version: 0.1.3
         | 
| 48 | 
            -
              type: :runtime
         | 
| 49 | 
            -
              prerelease: false
         | 
| 50 | 
            -
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 51 | 
            -
                requirements:
         | 
| 52 | 
            -
                - - ">="
         | 
| 53 | 
            -
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            -
                    version: 0.1.3
         | 
| 55 41 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 56 42 | 
             
              name: pry
         | 
| 57 43 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -167,6 +153,7 @@ files: | |
| 167 153 | 
             
            - LICENSE.txt
         | 
| 168 154 | 
             
            - README.md
         | 
| 169 155 | 
             
            - Rakefile
         | 
| 156 | 
            +
            - benchmarks/allowed_in_keys.rb
         | 
| 170 157 | 
             
            - lib/lock_and_cache.rb
         | 
| 171 158 | 
             
            - lib/lock_and_cache/action.rb
         | 
| 172 159 | 
             
            - lib/lock_and_cache/key.rb
         | 
| @@ -194,8 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement | |
| 194 181 | 
             
                - !ruby/object:Gem::Version
         | 
| 195 182 | 
             
                  version: '0'
         | 
| 196 183 | 
             
            requirements: []
         | 
| 197 | 
            -
             | 
| 198 | 
            -
            rubygems_version: 2.2.2
         | 
| 184 | 
            +
            rubygems_version: 3.0.3
         | 
| 199 185 | 
             
            signing_key: 
         | 
| 200 186 | 
             
            specification_version: 4
         | 
| 201 187 | 
             
            summary: Lock and cache methods.
         | 
| @@ -203,4 +189,3 @@ test_files: | |
| 203 189 | 
             
            - spec/lock_and_cache/key_spec.rb
         | 
| 204 190 | 
             
            - spec/lock_and_cache_spec.rb
         | 
| 205 191 | 
             
            - spec/spec_helper.rb
         | 
| 206 | 
            -
            has_rdoc: 
         |