sinatra-redis-cache 0.1.3 → 0.2.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: 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