lock_and_cache 2.0.2 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3a7645f81f4e7ee4980cdb47c841b853a9fdbe6b
4
- data.tar.gz: 3c5288088f4f243f7713255676985214bea7db49
3
+ metadata.gz: 8b4c2d0489b674d6efd75d4a368fda53ffc7f869
4
+ data.tar.gz: ededebda48c306638c3d19c2047f1fff49dc00fc
5
5
  SHA512:
6
- metadata.gz: 82aa1f3555638e500820ffabc1cc6d668928acc692c8e720f20d68962e9b9d0c7182d34cd787def3cc14c69feefea8c60e8d9b36deae2d1f3946122a252c009f
7
- data.tar.gz: 84c281f244ca8b551fd57ba9e48e8193882274e0c3d7abfc12b2a973d3d02bbb109e711cd3446db631f142aa21778be18c7889ff4de0725a53eec824f98a0907
6
+ metadata.gz: 4837ba9ea728c9b330e489aebd0cd0d0f13ddbb7d99259ccfd958edbb9bafb4f58ac1e7f2dd37f3686d03be9d823b6a970df21b313306fff88f75a6b6c0b2a82
7
+ data.tar.gz: 2f07f2ec076d96bc6f6eb7febb2538daf1e3af305ba2947c227a1da6ff586d8cd7cfc56fb86418f6b3b353e518712d86a6dfe00496c759d46f3c864b40a2437e
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ 2.1.0 / 2015-10-26
2
+
3
+ * Enhancements
4
+
5
+ * Better documentation
6
+ * Standalone mode (LockAndCache.lock_and_cache([...]) {})
7
+ * Nulls can be set to expire sooner than non-null return values (`nil_expires`)
8
+
1
9
  2.0.2 / 2015-10-16
2
10
 
3
11
  * Bug fixes (?)
data/README.md CHANGED
@@ -30,7 +30,7 @@ As you can see, most caching libraries only take care of (1) and (4).
30
30
 
31
31
  ## Practice
32
32
 
33
- ### Locking (antirez's Redlock)
33
+ ### Locking
34
34
 
35
35
  Based on [antirez's Redlock algorithm](http://redis.io/topics/distlock).
36
36
 
@@ -42,36 +42,104 @@ LockAndCache.storage = Redis.new
42
42
 
43
43
  It will use this redis for both locking and storing cached values.
44
44
 
45
- ### Caching (block inside of a method)
45
+ ### Caching
46
46
 
47
47
  (be sure to set up storage as above)
48
48
 
49
- You put a block inside of a method:
49
+ #### Standalone mode
50
50
 
51
51
  ```ruby
52
- class Blog
53
- def click(arg1, arg2)
54
- lock_and_cache(arg1, arg2, expires: 5) do
52
+ LockAndCache.lock_and_cache('stock_price') do
53
+ # get yer stock quote
54
+ end
55
+ ```
56
+
57
+ But that's probably not very useful without parameters
58
+
59
+ ```ruby
60
+ LockAndCache.lock_and_cache('stock_price', company: 'MSFT', date: '2015-05-05') do
61
+ # get yer stock quote
62
+ end
63
+ ```
64
+
65
+ And you probably want an expiry
66
+
67
+ ```ruby
68
+ LockAndCache.lock_and_cache('stock_price', {company: 'MSFT', date: '2015-05-05'}, expires: 10) do
69
+ # get yer stock quote
70
+ end
71
+ ```
72
+
73
+ Note how we separated options (`{expires: 10}`) from a hash that is part of the cache key (`{company: 'MSFT', date: '2015-05-05'}`).
74
+
75
+ You can clear a cache:
76
+
77
+ ```ruby
78
+ LockAndCache.lock_and_cache('stock_price', company: 'MSFT', date: '2015-05-05')
79
+ ```
80
+
81
+ One other crazy thing: let's say you want to check more often if the external stock price service returned nil
82
+
83
+ ```ruby
84
+ LockAndCache.lock_and_cache('stock_price', {company: 'MSFT', date: '2015-05-05'}, expires: 10, nil_expires: 1) do
85
+ # get yer stock quote
86
+ end
87
+ ```
88
+
89
+ #### Context mode
90
+
91
+ "Context mode" simply adds the class name, method name, and context key (the results of `#id` or `#lock_and_cache_key`) of the caller to the cache key.
92
+
93
+ (This gem evolved from https://github.com/seamusabshere/cache_method, where you always cached a method call...)
94
+
95
+ ```ruby
96
+ class Stock
97
+ include LockAndCache
98
+
99
+ def initialize(ticker_symbol)
100
+ [...]
101
+ end
102
+
103
+ def price(date)
104
+ lock_and_cache(date, expires: 10) do # <------ see how the cache key depends on the method args?
55
105
  # do the work
56
106
  end
57
107
  end
108
+
109
+ def lock_and_cache_key # <---------- if you don't define this, it will try to call #id
110
+ ticker_symbol
111
+ end
58
112
  end
59
113
  ```
60
114
 
61
- The key will be `{ Blog, :click, $id, $arg1, $arg2 }`. In other words, it auto-detects the class, method, object id ... and you add other args if you want.
115
+ The key will be `{ StockQuote, :get, $id, $date, }`. In other words, it auto-detects the class, method, context key ... and you add other args if you want.
62
116
 
63
- You can change the object id easily:
117
+ Here's how to clear a cache in context mode:
64
118
 
65
119
  ```ruby
66
- class Blog
67
- # [...]
68
- # if you don't define this, it will try to call #id
69
- def lock_and_cache_key
70
- [author, title]
71
- end
72
- end
120
+ blog.lock_and_cache_clear(:get, date)
73
121
  ```
74
122
 
123
+ ## Special features
124
+
125
+ ### Locking of course!
126
+
127
+ Most caching libraries don't do locking, meaning that >1 process can be calculating a cached value at the same time. Since you presumably cache things because they cost CPU, database reads, or money, doesn't it make sense to lock while caching?
128
+
129
+ ### Heartbeat
130
+
131
+ If the process holding the lock dies, we automatically remove the lock so somebody else can do it (using heartbeats and redlock extends).
132
+
133
+ ### Context mode
134
+
135
+ This pulls information about the context of a lock_and_cache block from the surrounding class, method, and object... so that you don't have to!
136
+
137
+ Standalone mode is cool too, tho.
138
+
139
+ ### nil_expires
140
+
141
+ You can expire nil values with a different timeout (`nil_expires`) than other values (`expires`).
142
+
75
143
  ## Tunables
76
144
 
77
145
  * `LockAndCache.storage=[redis]`
@@ -83,6 +151,13 @@ end
83
151
  * [redis](https://github.com/redis/redis-rb)
84
152
  * [redlock](https://github.com/leandromoreira/redlock-rb)
85
153
 
154
+ ## Wishlist
155
+
156
+ * Convert most tests to use standalone mode, which is easier to understand
157
+ * Check options
158
+ * Lengthen heartbeat so it's not so sensitive
159
+ * Clarify which options are seconds or milliseconds
160
+
86
161
  ## Contributing
87
162
 
88
163
  1. Fork it ( https://github.com/[my-github-username]/lock_and_cache/fork )
@@ -1,4 +1,3 @@
1
- require 'lock_and_cache/version'
2
1
  require 'timeout'
3
2
  require 'digest/sha1'
4
3
  require 'base64'
@@ -7,6 +6,10 @@ require 'redlock'
7
6
  require 'active_support'
8
7
  require 'active_support/core_ext'
9
8
 
9
+ require_relative 'lock_and_cache/version'
10
+ require_relative 'lock_and_cache/action'
11
+ require_relative 'lock_and_cache/key'
12
+
10
13
  # Lock and cache methods using redis!
11
14
  #
12
15
  # I bet you're caching, but are you locking?
@@ -33,13 +36,37 @@ module LockAndCache
33
36
  @storage
34
37
  end
35
38
 
36
- # Flush LockAndCache's storage
39
+ # Flush LockAndCache's storage.
37
40
  #
38
41
  # @note If you are sharing a redis database, it will clear it...
42
+ #
43
+ # @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.
39
44
  def LockAndCache.flush
40
45
  storage.flushdb
41
46
  end
42
47
 
48
+ # Lock and cache based on a key.
49
+ #
50
+ # @param key_parts [*] Parts that should be used to construct a key.
51
+ #
52
+ # @note Standalone mode. See also "context mode," where you mix LockAndCache into a class and call it from within its methods.
53
+ #
54
+ # @note A single hash arg is treated as a cached key. `LockAndCache.lock_and_cache(foo: :bar, expires: 100)` will be treated as a cache key of `foo: :bar, expires: 100` (which is probably wrong!!!). `LockAndCache.lock_and_cache({foo: :bar}, expires: 100)` will be treated as a cache key of `foo: :bar` and options `expires: 100`. This is the opposite of context mode and is true because we don't have any context to set the cache key from otherwise.
55
+ def LockAndCache.lock_and_cache(*key_parts_and_options, &blk)
56
+ options = (key_parts_and_options.last.is_a?(Hash) && key_parts_and_options.length > 1) ? key_parts_and_options.pop : {}
57
+ key = LockAndCache::Key.new key_parts_and_options
58
+ action = LockAndCache::Action.new key, options, blk
59
+ action.perform
60
+ end
61
+
62
+ # Clear a single key
63
+ #
64
+ # @note Standalone mode. See also "context mode," where you mix LockAndCache into a class and call it from within its methods.
65
+ def LockAndCache.clear(*key_parts)
66
+ key = LockAndCache::Key.new key_parts
67
+ key.clear
68
+ end
69
+
43
70
  # @param seconds [Numeric] Maximum wait time to get a lock
44
71
  #
45
72
  # @note Can be overridden by putting `max_lock_wait:` in your call to `#lock_and_cache`
@@ -57,133 +84,37 @@ module LockAndCache
57
84
  @lock_manager
58
85
  end
59
86
 
60
- # @private
61
- class Key
62
- attr_reader :obj
63
- attr_reader :method_id
64
-
65
- def initialize(obj, method_id, parts)
66
- @obj = obj
67
- @method_id = method_id.to_sym
68
- @_parts = parts
69
- end
70
-
71
- # A (non-cryptographic) digest of the key parts for use as the cache key
72
- def digest
73
- @digest ||= ::Digest::SHA1.hexdigest ::Marshal.dump(key)
74
- end
75
-
76
- # A (non-cryptographic) digest of the key parts for use as the lock key
77
- def lock_digest
78
- @lock_digest ||= 'lock/' + digest
79
- end
80
-
81
- # A human-readable representation of the key parts
82
- def key
83
- @key ||= [obj_class_name, method_id, parts]
84
- end
85
-
86
- alias debug key
87
-
88
- # An array of the parts we use for the key
89
- def parts
90
- @parts ||= @_parts.map do |v|
91
- case v
92
- when ::String, ::Symbol, ::Hash, ::Array
93
- v
94
- else
95
- v.respond_to?(:lock_and_cache_key) ? v.lock_and_cache_key : v.id
96
- end
97
- end
98
- end
99
-
100
- # An object (or its class's) name
101
- def obj_class_name
102
- @obj_class_name ||= (obj.class == ::Class) ? obj.name : obj.class.name
103
- end
104
-
105
- end
106
-
87
+ # Check if a method is locked on an object.
88
+ #
89
+ # @note Subject mode - this is expected to be called on an object that has LockAndCache mixed in. See also standalone mode.
107
90
  def lock_and_cache_locked?(method_id, *key_parts)
108
- debug = (ENV['LOCK_AND_CACHE_DEBUG'] == 'true')
109
- key = LockAndCache::Key.new self, method_id, key_parts
110
- LockAndCache.storage.exists key.lock_digest
91
+ key = LockAndCache::Key.new key_parts, context: self, method_id: method_id
92
+ key.locked?
111
93
  end
112
94
 
113
95
  # Clear a lock and cache given exactly the method and exactly the same arguments
96
+ #
97
+ # @note Subject mode - this is expected to be called on an object that has LockAndCache mixed in. See also standalone mode.
114
98
  def lock_and_cache_clear(method_id, *key_parts)
115
- debug = (ENV['LOCK_AND_CACHE_DEBUG'] == 'true')
116
- key = LockAndCache::Key.new self, method_id, key_parts
117
- Thread.exclusive { $stderr.puts "[lock_and_cache] clear #{key.debug} #{Base64.encode64(key.digest).strip} #{Digest::SHA1.hexdigest key.digest}" } if debug
118
- LockAndCache.storage.del key.digest
119
- LockAndCache.storage.del key.lock_digest
99
+ key = LockAndCache::Key.new key_parts, context: self, method_id: method_id
100
+ key.clear
120
101
  end
121
102
 
122
103
  # Lock and cache a method given key parts.
123
104
  #
124
- # @param key_parts [*] Parts that you want to include in the lock and cache key
105
+ # This is the defining characteristic of context mode: the cache key will automatically include the class name of the object calling it (the context!) and the name of the method it is called from.
106
+ #
107
+ # @param key_parts_and_options [*] Parts that you want to include in the lock and cache key. If the last element is a Hash, it will be treated as options.
125
108
  #
126
109
  # @return The cached value (possibly newly calculated).
127
- def lock_and_cache(*key_parts)
128
- raise "need a block" unless block_given?
129
- debug = (ENV['LOCK_AND_CACHE_DEBUG'] == 'true')
130
- caller[0] =~ /in `([^']+)'/
131
- method_id = $1 or raise "couldn't get method_id from #{caller[0]}"
132
- options = key_parts.last.is_a?(Hash) ? key_parts.pop.stringify_keys : {}
133
- expires = options['expires']
134
- max_lock_wait = options.fetch 'max_lock_wait', LockAndCache.max_lock_wait
135
- key = LockAndCache::Key.new self, method_id, key_parts
136
- digest = key.digest
137
- storage = LockAndCache.storage or raise("must set LockAndCache.storage=[Redis]")
138
- Thread.exclusive { $stderr.puts "[lock_and_cache] A1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
139
- if storage.exists(digest) and (existing = storage.get(digest)).is_a?(String)
140
- return ::Marshal.load(existing)
141
- end
142
- Thread.exclusive { $stderr.puts "[lock_and_cache] B1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
143
- retval = nil
144
- lock_manager = LockAndCache.lock_manager
145
- lock_digest = key.lock_digest
146
- lock_info = nil
147
- begin
148
- Timeout.timeout(max_lock_wait, TimeoutWaitingForLock) do
149
- until lock_info = lock_manager.lock(lock_digest, LockAndCache::LOCK_HEARTBEAT_EXPIRES*1000)
150
- Thread.exclusive { $stderr.puts "[lock_and_cache] C1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
151
- sleep rand
152
- end
153
- end
154
- Thread.exclusive { $stderr.puts "[lock_and_cache] D1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
155
- if storage.exists(digest) and (existing = storage.get(digest)).is_a?(String)
156
- Thread.exclusive { $stderr.puts "[lock_and_cache] E1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
157
- retval = ::Marshal.load existing
158
- end
159
- unless retval
160
- Thread.exclusive { $stderr.puts "[lock_and_cache] F1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
161
- done = false
162
- begin
163
- lock_extender = Thread.new do
164
- loop do
165
- Thread.exclusive { $stderr.puts "[lock_and_cache] heartbeat1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
166
- break if done
167
- sleep LockAndCache::LOCK_HEARTBEAT_PERIOD
168
- break if done
169
- Thread.exclusive { $stderr.puts "[lock_and_cache] heartbeat2 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
170
- lock_manager.lock lock_digest, LockAndCache::LOCK_HEARTBEAT_EXPIRES*1000, extend: lock_info
171
- end
172
- end
173
- retval = yield
174
- if expires
175
- storage.setex digest, expires, ::Marshal.dump(retval)
176
- else
177
- storage.set digest, ::Marshal.dump(retval)
178
- end
179
- ensure
180
- done = true
181
- lock_extender.join if lock_extender.status.nil?
182
- end
183
- end
184
- ensure
185
- lock_manager.unlock lock_info if lock_info
186
- end
187
- retval
110
+ #
111
+ # @note Subject mode - this is expected to be called on an object that has LockAndCache mixed in. See also standalone mode.
112
+ #
113
+ # @note A single hash arg is treated as an options hash. `lock_and_cache(expires: 100)` will be treated as options `expires: 100`. This is the opposite of standalone mode and true because we want to support people constructing cache keys from the context (context) PLUS an arbitrary hash of stuff.
114
+ def lock_and_cache(*key_parts_and_options, &blk)
115
+ options = key_parts_and_options.last.is_a?(Hash) ? key_parts_and_options.pop : {}
116
+ key = LockAndCache::Key.new key_parts_and_options, context: self, caller: caller
117
+ action = LockAndCache::Action.new key, options, blk
118
+ action.perform
188
119
  end
189
120
  end
@@ -0,0 +1,105 @@
1
+ module LockAndCache
2
+ # @private
3
+ class Action
4
+ attr_reader :key
5
+ attr_reader :options
6
+ attr_reader :blk
7
+
8
+ def initialize(key, options, blk)
9
+ raise "need a block" unless blk
10
+ @key = key
11
+ @options = options.stringify_keys
12
+ @blk = blk
13
+ end
14
+
15
+ def expires
16
+ options['expires']
17
+ end
18
+
19
+ def nil_expires
20
+ options['nil_expires']
21
+ end
22
+
23
+ def digest
24
+ @digest ||= key.digest
25
+ end
26
+
27
+ def lock_digest
28
+ @lock_digest ||= key.lock_digest
29
+ end
30
+
31
+ def storage
32
+ @storage ||= LockAndCache.storage or raise("must set LockAndCache.storage=[Redis]")
33
+ end
34
+
35
+ def perform
36
+ debug = (ENV['LOCK_AND_CACHE_DEBUG'] == 'true')
37
+ max_lock_wait = options.fetch 'max_lock_wait', LockAndCache.max_lock_wait
38
+ Thread.exclusive { $stderr.puts "[lock_and_cache] A1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
39
+ if storage.exists(digest) and (existing = storage.get(digest)).is_a?(String)
40
+ return ::Marshal.load(existing)
41
+ end
42
+ Thread.exclusive { $stderr.puts "[lock_and_cache] B1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
43
+ retval = nil
44
+ lock_manager = LockAndCache.lock_manager
45
+ lock_info = nil
46
+ begin
47
+ Timeout.timeout(max_lock_wait, TimeoutWaitingForLock) do
48
+ until lock_info = lock_manager.lock(lock_digest, LockAndCache::LOCK_HEARTBEAT_EXPIRES*1000)
49
+ Thread.exclusive { $stderr.puts "[lock_and_cache] C1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
50
+ sleep rand
51
+ end
52
+ end
53
+ Thread.exclusive { $stderr.puts "[lock_and_cache] D1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
54
+ if storage.exists(digest) and (existing = storage.get(digest)).is_a?(String)
55
+ Thread.exclusive { $stderr.puts "[lock_and_cache] E1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
56
+ retval = ::Marshal.load existing
57
+ end
58
+ unless retval
59
+ Thread.exclusive { $stderr.puts "[lock_and_cache] F1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
60
+ done = false
61
+ begin
62
+ lock_extender = Thread.new do
63
+ loop do
64
+ Thread.exclusive { $stderr.puts "[lock_and_cache] heartbeat1 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
65
+ break if done
66
+ sleep LockAndCache::LOCK_HEARTBEAT_PERIOD
67
+ break if done
68
+ Thread.exclusive { $stderr.puts "[lock_and_cache] heartbeat2 #{key.debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if debug
69
+ lock_manager.lock lock_digest, LockAndCache::LOCK_HEARTBEAT_EXPIRES*1000, extend: lock_info
70
+ end
71
+ end
72
+ retval = blk.call
73
+ retval.nil? ? set_nil : set_non_nil(retval)
74
+ ensure
75
+ done = true
76
+ lock_extender.join if lock_extender.status.nil?
77
+ end
78
+ end
79
+ ensure
80
+ lock_manager.unlock lock_info if lock_info
81
+ end
82
+ retval
83
+ end
84
+
85
+ NIL = Marshal.dump nil
86
+ def set_nil
87
+ if nil_expires
88
+ storage.setex digest, nil_expires, NIL
89
+ elsif expires
90
+ storage.setex digest, expires, NIL
91
+ else
92
+ storage.set digest, NIL
93
+ end
94
+ end
95
+
96
+ def set_non_nil(retval)
97
+ raise "expected not null #{retval.inspect}" if retval.nil?
98
+ if expires
99
+ storage.setex digest, expires, ::Marshal.dump(retval)
100
+ else
101
+ storage.set digest, ::Marshal.dump(retval)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,81 @@
1
+ module LockAndCache
2
+ # @private
3
+ class Key
4
+ class << self
5
+ # @private
6
+ #
7
+ # Extract the method id from a method's caller array.
8
+ def extract_method_id_from_caller(kaller)
9
+ kaller[0] =~ METHOD_NAME_IN_CALLER
10
+ raise "couldn't get method_id from #{kaller[0]}" unless $1
11
+ $1.to_sym
12
+ end
13
+
14
+ # @private
15
+ #
16
+ # Get a context object's class name, which is its own name if it's an object.
17
+ def extract_class_name(context)
18
+ (self.class == ::Class) ? self.name : self.class.name
19
+ end
20
+ end
21
+
22
+ METHOD_NAME_IN_CALLER = /in `([^']+)'/
23
+
24
+ attr_reader :method_id
25
+ attr_reader :class_name
26
+
27
+ def initialize(parts, options = {})
28
+ @_parts = parts
29
+ @method_id = if options.has_key?(:method_id)
30
+ options[:method_id]
31
+ elsif options.has_key?(:caller)
32
+ Key.extract_method_id_from_caller options[:caller]
33
+ end
34
+ @class_name = Key.extract_class_name options[:context] if options.has_key?(:context)
35
+ end
36
+
37
+ # A (non-cryptographic) digest of the key parts for use as the cache key
38
+ def digest
39
+ @digest ||= ::Digest::SHA1.hexdigest ::Marshal.dump(key)
40
+ end
41
+
42
+ # A (non-cryptographic) digest of the key parts for use as the lock key
43
+ def lock_digest
44
+ @lock_digest ||= 'lock/' + digest
45
+ end
46
+
47
+ # A human-readable representation of the key parts
48
+ def key
49
+ @key ||= if method_id
50
+ [class_name, method_id, parts]
51
+ else
52
+ parts
53
+ end
54
+ end
55
+
56
+ def locked?
57
+ LockAndCache.storage.exists lock_digest
58
+ end
59
+
60
+ def clear
61
+ Thread.exclusive { $stderr.puts "[lock_and_cache] clear #{debug} #{Base64.encode64(digest).strip} #{Digest::SHA1.hexdigest digest}" } if ENV['LOCK_AND_CACHE_DEBUG'] == 'true'
62
+ storage = LockAndCache.storage
63
+ storage.del digest
64
+ storage.del lock_digest
65
+ end
66
+
67
+ alias debug key
68
+
69
+ # An array of the parts we use for the key
70
+ def parts
71
+ @parts ||= @_parts.map do |v|
72
+ case v
73
+ when ::String, ::Symbol, ::Hash, ::Array
74
+ v
75
+ else
76
+ v.respond_to?(:lock_and_cache_key) ? v.lock_and_cache_key : v.id
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,3 +1,3 @@
1
1
  module LockAndCache
2
- VERSION = '2.0.2'
2
+ VERSION = '2.1.0'
3
3
  end
@@ -7,6 +7,8 @@ class Foo
7
7
  @id = id
8
8
  @count = 0
9
9
  @count_exp = 0
10
+ @click_single_hash_arg_as_options = 0
11
+ @click_last_hash_as_options = 0
10
12
  end
11
13
 
12
14
  def click
@@ -22,11 +24,25 @@ class Foo
22
24
  end
23
25
 
24
26
  def click_exp
25
- lock_and_cache(expires: 1, foo: :bar) do
27
+ lock_and_cache(expires: 1) do
26
28
  @count_exp += 1
27
29
  end
28
30
  end
29
31
 
32
+ # foo will be treated as option, so this is cacheable
33
+ def click_single_hash_arg_as_options
34
+ lock_and_cache(foo: rand, expires: 1) do
35
+ @click_single_hash_arg_as_options += 1
36
+ end
37
+ end
38
+
39
+ # foo will be treated as part of cache key, so this is uncacheable
40
+ def click_last_hash_as_options
41
+ lock_and_cache({foo: rand}, expires: 1) do
42
+ @click_last_hash_as_options += 1
43
+ end
44
+ end
45
+
30
46
  def lock_and_cache_key
31
47
  @id
32
48
  end
@@ -114,7 +130,7 @@ describe LockAndCache do
114
130
  it "can be expired" do
115
131
  expect(foo.click_exp).to eq(1)
116
132
  expect(foo.click_exp).to eq(1)
117
- sleep 2
133
+ sleep 1.5
118
134
  expect(foo.click_exp).to eq(2)
119
135
  end
120
136
 
@@ -122,6 +138,20 @@ describe LockAndCache do
122
138
  expect(foo.click_null).to eq(nil)
123
139
  expect(foo.click_null).to eq(nil)
124
140
  end
141
+
142
+ it "treats single hash arg as options" do
143
+ expect(foo.click_single_hash_arg_as_options).to eq(1)
144
+ expect(foo.click_single_hash_arg_as_options).to eq(1)
145
+ sleep 1.1
146
+ expect(foo.click_single_hash_arg_as_options).to eq(2)
147
+ end
148
+
149
+ it "treats last hash as options" do
150
+ expect(foo.click_last_hash_as_options).to eq(1)
151
+ expect(foo.click_last_hash_as_options).to eq(2) # it's uncacheable to prove we're not using as part of options
152
+ expect(foo.click_last_hash_as_options).to eq(3)
153
+ end
154
+
125
155
  end
126
156
 
127
157
  describe "locking" do
@@ -237,17 +267,100 @@ describe LockAndCache do
237
267
 
238
268
  end
239
269
 
240
- describe 'keying' do
241
- it "doesn't conflate symbol and string args" do
242
- symbol = LockAndCache::Key.new(Foo.new(:me), :click, a: 1)
243
- string = LockAndCache::Key.new(Foo.new(:me), :click, 'a' => 1)
244
- expect(symbol.digest).not_to eq(string.digest)
270
+ describe 'standalone' do
271
+ it 'works like you expect' do
272
+ count = 0
273
+ expect(LockAndCache.lock_and_cache('hello') { count += 1 }).to eq(1)
274
+ expect(count).to eq(1)
275
+ expect(LockAndCache.lock_and_cache('hello') { count += 1 }).to eq(1)
276
+ expect(count).to eq(1)
277
+ end
278
+
279
+ it 'allows expiry' do
280
+ count = 0
281
+ expect(LockAndCache.lock_and_cache('hello', expires: 1) { count += 1 }).to eq(1)
282
+ expect(count).to eq(1)
283
+ expect(LockAndCache.lock_and_cache('hello') { count += 1 }).to eq(1)
284
+ expect(count).to eq(1)
285
+ sleep 1.1
286
+ expect(LockAndCache.lock_and_cache('hello') { count += 1 }).to eq(2)
287
+ expect(count).to eq(2)
288
+ end
289
+
290
+ it 'allows clearing' do
291
+ count = 0
292
+ expect(LockAndCache.lock_and_cache('hello') { count += 1 }).to eq(1)
293
+ expect(count).to eq(1)
294
+ LockAndCache.clear('hello')
295
+ expect(LockAndCache.lock_and_cache('hello') { count += 1 }).to eq(2)
296
+ expect(count).to eq(2)
297
+ end
298
+
299
+ it 'allows multi-part keys' do
300
+ count = 0
301
+ expect(LockAndCache.lock_and_cache(['hello', 1, { target: 'world' }]) { count += 1 }).to eq(1)
302
+ expect(count).to eq(1)
303
+ expect(LockAndCache.lock_and_cache(['hello', 1, { target: 'world' }]) { count += 1 }).to eq(1)
304
+ expect(count).to eq(1)
305
+ end
306
+
307
+ it 'treats a single hash arg as a cache key (not as options)' do
308
+ count = 0
309
+ LockAndCache.lock_and_cache(hello: 'world', expires: 100) { count += 1 }
310
+ expect(count).to eq(1)
311
+ LockAndCache.lock_and_cache(hello: 'world', expires: 100) { count += 1 }
312
+ expect(count).to eq(1)
313
+ LockAndCache.lock_and_cache(hello: 'world', expires: 200) { count += 1 } # expires is being treated as part of cache key
314
+ expect(count).to eq(2)
315
+ end
316
+
317
+ it "correctly identifies options hash" do
318
+ count = 0
319
+ LockAndCache.lock_and_cache({ hello: 'world' }, expires: 1, ignored: rand) { count += 1 }
320
+ expect(count).to eq(1)
321
+ LockAndCache.lock_and_cache({ hello: 'world' }, expires: 1, ignored: rand) { count += 1 } # expires is not being treated as part of cache key
322
+ expect(count).to eq(1)
323
+ sleep 1.1
324
+ LockAndCache.lock_and_cache({ hello: 'world' }) { count += 1 }
325
+ expect(count).to eq(2)
326
+ end
327
+ end
328
+
329
+ describe "shorter expiry for null results" do
330
+ it "optionally caches null for less time" do
331
+ count = 0
332
+ LockAndCache.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; nil }
333
+ expect(count).to eq(1)
334
+ LockAndCache.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; nil }
335
+ expect(count).to eq(1)
336
+ sleep 1.1 # this is enough to expire
337
+ LockAndCache.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; nil }
338
+ expect(count).to eq(2)
245
339
  end
246
340
 
247
- it "cares about order" do
248
- symbol = LockAndCache::Key.new(Foo.new(:me), :click, {a: 1, b: 2})
249
- string = LockAndCache::Key.new(Foo.new(:me), :click, {b: 2, a: 1})
250
- expect(symbol.digest).not_to eq(string.digest)
341
+ it "normally caches null for the same amount of time" do
342
+ count = 0
343
+ expect(LockAndCache.lock_and_cache('hello', expires: 1) { count += 1; nil }).to be_nil
344
+ expect(count).to eq(1)
345
+ expect(LockAndCache.lock_and_cache('hello', expires: 1) { count += 1; nil }).to be_nil
346
+ expect(count).to eq(1)
347
+ sleep 1.1
348
+ expect(LockAndCache.lock_and_cache('hello', expires: 1) { count += 1; nil }).to be_nil
349
+ expect(count).to eq(2)
350
+ end
351
+
352
+ it "caches non-null for normal time" do
353
+ count = 0
354
+ LockAndCache.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; true }
355
+ expect(count).to eq(1)
356
+ LockAndCache.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; true }
357
+ expect(count).to eq(1)
358
+ sleep 1.1
359
+ LockAndCache.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; true }
360
+ expect(count).to eq(1)
361
+ sleep 1
362
+ LockAndCache.lock_and_cache('hello', nil_expires: 1, expires: 2) { count += 1; true }
363
+ expect(count).to eq(2)
251
364
  end
252
365
  end
253
366
 
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: 2.0.2
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Seamus Abshere
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-10-16 00:00:00.000000000 Z
11
+ date: 2015-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -168,6 +168,8 @@ files:
168
168
  - README.md
169
169
  - Rakefile
170
170
  - lib/lock_and_cache.rb
171
+ - lib/lock_and_cache/action.rb
172
+ - lib/lock_and_cache/key.rb
171
173
  - lib/lock_and_cache/version.rb
172
174
  - lock_and_cache.gemspec
173
175
  - spec/lock_and_cache_spec.rb