sinatra-redis-cache 0.1.3 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 06b3625b3a106890160c40df9820300bfff4f089
4
- data.tar.gz: cbf94d36d64713d4aa0aa5cf2fb8ce02c0d51cd4
3
+ metadata.gz: b87cc8855b185fcd8ed06c830fa184a1d175941e
4
+ data.tar.gz: 6a335f63d0d2fa50caaafd8d3e3a3422f101d428
5
5
  SHA512:
6
- metadata.gz: c0a67b592f9147989575655308251364de3d7131a29772241f78a65a757aba8f5ad8ec1ad0c8da1d61ab0309fce1deb837e7c205f7688451044de7360cde8f1c
7
- data.tar.gz: 5b80698e8592d31960975df62142bbb58c614ab25f48b0f772922b6e89342d04335dc85807edd22869f593a6da68689dde8f7aa4dbb1198766fd57bb5a24c782
6
+ metadata.gz: ca83aa4e02139b50bb1d284ce0f1ee6a2f61bed119bba06a77eeeda0858ef1c352769d902b7c30671f00c679d2d7533b49453e77751cf4e3576284cb5e4a7924
7
+ data.tar.gz: a4326ed7771f25d79f5da08779eca7ecc542b1d40eabc07c329ac5c0135465d836a950ea4e243fd6eabdc5f89891d839bf5f5de2e2ca82635c964bc40c1813c3
data/README.md CHANGED
@@ -40,6 +40,8 @@ Sinatra::RedisCache::Config.config do
40
40
  redis_conn Redis.new
41
41
  namespace 'sinatra_cache'
42
42
  default_expires 3600
43
+ lock_timeout 5
43
44
  environments [:production]
45
+ logger Logger.new(STDERR)
44
46
  end
45
47
  ```
data/Rakefile CHANGED
@@ -7,7 +7,12 @@ namespace :sinatra_cache do
7
7
 
8
8
  desc 'Print configured namespace'
9
9
  task :namespace do
10
+ puts Sinatra::RedisCache::Config.namespace
11
+ end
12
+
13
+ desc 'Show all cache keys'
14
+ task :list_keys do
10
15
  include Sinatra::RedisCache
11
- Sinatra::RedisCache::Config.config.namespace
16
+ puts cache_list_keys.map{|k| "#{k} [age: #{cache_key_age(k).to_i}, ttl: #{cache_key_ttl(k)}]"}
12
17
  end
13
18
  end
@@ -28,17 +28,36 @@ module Sinatra
28
28
  end
29
29
  end
30
30
 
31
- class RedisCache
32
- def do(key, expires, params={}, block)
33
- key = key_with_namespace(key)
31
+ class Cache
32
+ def do(key, expires=config.default_expires, block)
33
+ debug_log "do #{key_without_namespace(key)}"
34
34
  if Sinatra::RedisCache::Config.environments.include? Sinatra::Base.settings.environment
35
- object = get(key)
36
- if object
37
- object
38
- else
39
- object = block.call
40
- store(key, object, expires)
41
- object
35
+ try = 0
36
+ begin
37
+ redis.watch(key = key_with_namespace(key)) do |watched|
38
+ object = get(key)
39
+ unless object.empty?
40
+ if object['locked']
41
+ raise SinatraRedisCacheKeyLocked
42
+ end
43
+ object['object']
44
+ else
45
+ lock_key(key, watched, config.lock_timeout)
46
+ new_object = block.call
47
+ store(key, new_object, expires)
48
+ new_object
49
+ end
50
+ end
51
+ rescue SinatraRedisCacheKeyLocked
52
+ sleeptime = (((try += 1)*50 + rand(100).to_f)/1000)
53
+ debug_log "key is locked, waiting #{sleeptime}s for retry ##{try}."
54
+ sleep sleeptime
55
+ retry
56
+ rescue SinatraRedisCacheKeyAlreadyLocked
57
+ sleeptime = (((try += 1)*50 + rand(100).to_f)/1000)
58
+ debug_log "failed to obtain lock, waiting #{sleeptime}s for retry ##{try}."
59
+ sleep sleeptime
60
+ retry
42
61
  end
43
62
  else
44
63
  # Just run the block without cache if we're not in an allowed environment
@@ -46,25 +65,51 @@ module Sinatra
46
65
  end
47
66
  end
48
67
 
49
- def get(key, params={})
50
- key = key_with_namespace(key)
51
- string = redis.get(key)
52
- unless string.nil?
53
- deserialize(string)
68
+ def get(key)
69
+ debug_log "get #{key_without_namespace(key)}"
70
+ unless (hash = redis.hgetall(key_with_namespace(key))).nil?
71
+ hash.each{|k,v| hash[k]=deserialize(v)}
54
72
  else
55
73
  false
56
74
  end
57
75
  end
58
76
 
59
- def store(key, object, expires, params={})
60
- key = key_with_namespace(key)
61
- expires = expires || config.default_expires
62
- redis.set(key, serialize(object))
63
- redis.expire(key, expires)
77
+ def store(key, object, expires=config.default_expires)
78
+ debug_log "store #{key_without_namespace(key)}"
79
+ properties = { created_at: Time.now.utc.to_i }
80
+ redis.watch(key = key_with_namespace(key)) do |watched|
81
+ watched.multi do |multi|
82
+ multi.hset(key, 'object', serialize(object))
83
+ multi.hset(key, 'properties', serialize(properties))
84
+ multi.hdel(key, 'locked')
85
+ multi.expire(key, expires)
86
+ end
87
+ end
88
+ end
89
+
90
+ def properties(key)
91
+ unless (string = redis.hget(key_with_namespace(key), 'properties')).nil?
92
+ deserialize(string)
93
+ end
94
+ end
95
+
96
+ def ttl(key)
97
+ redis.ttl(key_with_namespace(key))
98
+ end
99
+
100
+ def all_keys(params={with_namespace: true})
101
+ redis.keys("#{namespace}:*").map{|k| params[:with_namespace] ? k : key_without_namespace(k) }
102
+ end
103
+
104
+ def del(keys)
105
+ debug_log "del #{keys.map{|k| key_without_namespace(k)}}"
106
+ redis.del(keys)
64
107
  end
65
108
 
66
109
  def flush
67
- redis.del(all_keys)
110
+ unless (keys = all_keys).empty?
111
+ del(keys)
112
+ end
68
113
  end
69
114
 
70
115
  private
@@ -73,6 +118,12 @@ module Sinatra
73
118
  Sinatra::RedisCache::Config
74
119
  end
75
120
 
121
+ def debug_log(message)
122
+ if config.logger
123
+ config.logger.debug("sinatra-redis-cache/#{Process.pid}/#{Thread.current.__id__}") { message }
124
+ end
125
+ end
126
+
76
127
  def redis
77
128
  config.redis_conn
78
129
  end
@@ -97,10 +148,6 @@ module Sinatra
97
148
  end
98
149
  end
99
150
 
100
- def all_keys
101
- redis.keys(namespace + '*')
102
- end
103
-
104
151
  def serialize(object)
105
152
  Marshal.dump(object)
106
153
  end
@@ -108,25 +155,61 @@ module Sinatra
108
155
  def deserialize(string)
109
156
  Marshal.load(string)
110
157
  end
158
+
159
+ def lock_key(key, redis, timeout=config.lock_timeout)
160
+ debug_log "locking #{key_without_namespace(key)} for #{timeout}s"
161
+ unless redis.multi do |multi|
162
+ multi.hsetnx(key, 'locked', serialize(true))
163
+ multi.expire(key, timeout)
164
+ end.eql? [true,true]
165
+ raise SinatraRedisCacheKeyAlreadyLocked
166
+ end
167
+
168
+ end
111
169
  end
112
170
 
113
- def cache_do(key, expires=nil, params={}, &block)
114
- cache = RedisCache.new
115
- cache.do(key, expires, params, block)
171
+ def cache_do(key, expires=nil, &block)
172
+ cache = Cache.new
173
+ cache.do(key, expires, block)
116
174
  end
117
175
 
118
- def cache_get(key, params={})
119
- cache = RedisCache.new
120
- cache.get(key, params)
176
+ def cache_get(key)
177
+ cache = Cache.new
178
+ cache.get(key)
121
179
  end
122
180
 
123
- def cache_store(key, value, expires, params={})
124
- cache = RedisCache.new
125
- cache.store(key, value, expires, params)
181
+ def cache_key_properties(key)
182
+ cache = Cache.new
183
+ cache.properties(key)
184
+ end
185
+
186
+ def cache_key_age(key)
187
+ cache = Cache.new
188
+ Time.now.utc.to_i - cache.properties(key)[:created_at]
189
+ end
190
+
191
+ def cache_key_ttl(key)
192
+ cache = Cache.new
193
+ cache.ttl(key)
194
+ end
195
+
196
+ def cache_store(key, value, expires=nil)
197
+ cache = Cache.new
198
+ cache.store(key, value, expires)
199
+ end
200
+
201
+ def cache_list_keys
202
+ cache = Cache.new
203
+ cache.all_keys(with_namespace: false)
204
+ end
205
+
206
+ def cache_del(keys)
207
+ cache = Cache.new
208
+ cache.del(keys)
126
209
  end
127
210
 
128
211
  def cache_flush
129
- cache = RedisCache.new
212
+ cache = Cache.new
130
213
  cache.flush
131
214
  end
132
215
 
@@ -135,12 +218,20 @@ module Sinatra
135
218
  helpers RedisCache
136
219
  end
137
220
 
221
+ class SinatraRedisCacheKeyLocked < Exception
222
+ end
223
+
224
+ class SinatraRedisCacheKeyAlreadyLocked < Exception
225
+ end
226
+
138
227
  Sinatra::RedisCache::Config.config do
139
228
  # Set up configurable values
140
229
  parameter :redis_conn
141
230
  parameter :namespace
142
231
  parameter :default_expires
232
+ parameter :lock_timeout
143
233
  parameter :environments
234
+ parameter :logger
144
235
  end
145
236
 
146
237
  Sinatra::RedisCache::Config.config do
@@ -148,7 +239,9 @@ Sinatra::RedisCache::Config.config do
148
239
  redis_conn Redis.new
149
240
  namespace 'sinatra_cache'
150
241
  default_expires 3600
242
+ lock_timeout 5
151
243
  environments [:production]
244
+ logger Logger.new(STDERR)
152
245
  end
153
246
 
154
247
  unless defined?(Rake).nil?
@@ -0,0 +1,251 @@
1
+ require 'sinatra/base'
2
+ require 'redis'
3
+ require 'socket'
4
+
5
+ module Sinatra
6
+
7
+ module RedisCache
8
+
9
+ module Config
10
+ extend self
11
+
12
+ def parameter(*names)
13
+ names.each do |name|
14
+ attr_accessor name
15
+
16
+ define_method name do |*values|
17
+ value = values.first
18
+ if value
19
+ self.send("#{name}=", value)
20
+ else
21
+ instance_variable_get("@#{name}")
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ def config(&block)
28
+ instance_eval &block
29
+ end
30
+ end
31
+
32
+ class Cache
33
+ def do(key, expires, lock, block)
34
+ log "def do #{key}"
35
+ if Sinatra::RedisCache::Config.environments.include? Sinatra::Base.settings.environment
36
+ begin
37
+ redis.watch(key = key_with_namespace(key)) do
38
+ log " in watch"
39
+ object = redis.hgetall(key)
40
+ unless object.empty?
41
+ log " object is not empty"
42
+ if lock && object['locked']
43
+ log " object is locked"
44
+ raise SinatraRedisCacheKeyLocked
45
+ end
46
+ log " returning object"
47
+ deserialize(object['object'])
48
+ else
49
+ log " apparently object is empty"
50
+ if lock
51
+ log " locking"
52
+ lock_key(key, lock.class == Integer ? lock : 2)
53
+ end
54
+ new_object = block.call
55
+ store(key, new_object, expires)
56
+ new_object
57
+ end
58
+ end
59
+ rescue SinatraRedisCacheKeyLocked
60
+ log " raise SinatraRedisCacheKeyLocked"
61
+ sleep ((50 + rand(100).to_f)/1000)
62
+ retry
63
+ rescue SinatraRedisCacheKeyAlreadyLocked
64
+ log " raise SinatraRedisCacheKeyAlreadyLocked"
65
+ sleep ((50 + rand(100).to_f)/1000)
66
+ retry
67
+ end
68
+ else
69
+ # Just run the block without cache if we're not in an allowed environment
70
+ block.call
71
+ end
72
+ end
73
+
74
+ def get(key)
75
+ log "def get #{key}"
76
+ unless (string = redis.hget(key_with_namespace(key),'object')).nil?
77
+ deserialize(string)
78
+ else
79
+ false
80
+ end
81
+ end
82
+
83
+ def store(key, object, expires=config.default_expires)
84
+ log "def store #{key}"
85
+ redis.watch(key = key_with_namespace(key)) do
86
+ redis.multi do |multi|
87
+ multi.hset(key, 'object', serialize(object))
88
+ multi.expire(key, expires)
89
+ multi.hdel(key, 'locked')
90
+ end
91
+ end
92
+ end
93
+
94
+ def properties(key)
95
+ log "def properties #{key}"
96
+ unless (string = redis.hget(key_with_namespace(key), 'properties')).nil?
97
+ deserialize(string)
98
+ end
99
+ end
100
+
101
+ def ttl(key)
102
+ redis.ttl(key_with_namespace(key))
103
+ end
104
+
105
+ def all_keys(params={with_namespace: true})
106
+ redis.keys("#{namespace}:*").map{|k| params[:with_namespace] ? k : key_without_namespace(k) }
107
+ end
108
+
109
+ def del(keys)
110
+ redis.del(keys)
111
+ end
112
+
113
+ def flush
114
+ unless (keys = all_keys).empty?
115
+ del(keys)
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def config
122
+ Sinatra::RedisCache::Config
123
+ end
124
+
125
+ def log(message)
126
+ if config.logger
127
+ config.logger.debug('sinatra-redis-cache') { message }
128
+ end
129
+ end
130
+
131
+ def redis
132
+ config.redis_conn
133
+ end
134
+
135
+ def namespace
136
+ config.namespace
137
+ end
138
+
139
+ def key_with_namespace(key)
140
+ if key.start_with? namespace
141
+ key
142
+ else
143
+ "#{namespace}:#{key}"
144
+ end
145
+ end
146
+
147
+ def key_without_namespace(key)
148
+ if key.start_with? namespace
149
+ key.gsub(/^#{namespace}:/,'')
150
+ else
151
+ key
152
+ end
153
+ end
154
+
155
+ def serialize(object)
156
+ Marshal.dump(object)
157
+ end
158
+
159
+ def deserialize(string)
160
+ Marshal.load(string)
161
+ end
162
+
163
+ def lock_key(key, timeout=2)
164
+ log " def lock"
165
+ unless redis.multi do |multi|
166
+ multi.hsetnx(key, 'locked', true)
167
+ multi.hsetnx(key, 'created_by', "#{Socket.gethostname}/#{Process.pid.to_s}/#{Thread.current.to_s}")
168
+ multi.expire(key, timeout)
169
+ end[0]
170
+ raise SinatraRedisCacheKeyAlreadyLocked
171
+ end
172
+ end
173
+ end
174
+
175
+ def cache_do(key, expires=nil, lock=false, &block)
176
+ cache = Cache.new
177
+ cache.do(key, expires, lock, block)
178
+ end
179
+
180
+ def cache_get(key)
181
+ cache = Cache.new
182
+ cache.get(key)
183
+ end
184
+
185
+ def cache_key_properties(key)
186
+ cache = Cache.new
187
+ cache.properties(key)
188
+ end
189
+
190
+ def cache_key_age(key)
191
+ cache = Cache.new
192
+ Time.now.utc.to_i - cache.properties(key)[:created_at]
193
+ end
194
+
195
+ def cache_key_ttl(key)
196
+ cache = Cache.new
197
+ cache.ttl(key)
198
+ end
199
+
200
+ def cache_store(key, value, expires=nil)
201
+ cache = Cache.new
202
+ cache.store(key, value, expires)
203
+ end
204
+
205
+ def cache_list_keys
206
+ cache = Cache.new
207
+ cache.all_keys(with_namespace: false)
208
+ end
209
+
210
+ def cache_del(keys)
211
+ cache = Cache.new
212
+ cache.del(keys)
213
+ end
214
+
215
+ def cache_flush
216
+ cache = Cache.new
217
+ cache.flush
218
+ end
219
+
220
+ end
221
+
222
+ helpers RedisCache
223
+ end
224
+
225
+ class SinatraRedisCacheKeyLocked < Exception
226
+ end
227
+
228
+ class SinatraRedisCacheKeyAlreadyLocked < Exception
229
+ end
230
+
231
+ Sinatra::RedisCache::Config.config do
232
+ # Set up configurable values
233
+ parameter :redis_conn
234
+ parameter :namespace
235
+ parameter :default_expires
236
+ parameter :environments
237
+ parameter :logger
238
+ end
239
+
240
+ Sinatra::RedisCache::Config.config do
241
+ # Default values
242
+ redis_conn Redis.new
243
+ namespace 'sinatra_cache'
244
+ default_expires 3600
245
+ environments [:production]
246
+ logger Logger.new(STDOUT)
247
+ end
248
+
249
+ unless defined?(Rake).nil?
250
+ Rake.load_rakefile("#{File.dirname(__FILE__)}/../../Rakefile")
251
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sinatra-redis-cache
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Warren Guy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-10 00:00:00.000000000 Z
11
+ date: 2014-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sinatra
@@ -62,6 +62,7 @@ files:
62
62
  - README.md
63
63
  - Rakefile
64
64
  - lib/sinatra/redis-cache.rb
65
+ - lib/sinatra/redis-cache.rb.bak
65
66
  homepage: https://github.com/warrenguy/sinatra-redis-cache
66
67
  licenses:
67
68
  - MIT