garner 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -24,7 +24,7 @@ To cache a value, invoke `cache` from within your API. Without any parameters it
24
24
  ``` ruby
25
25
  get "/" do
26
26
  cache do
27
- { :counter => 42 }
27
+ { counter: 42 }
28
28
  end
29
29
  end
30
30
  ```
@@ -34,7 +34,7 @@ To enable support for the date-based `If-Modified-Since` and the ETag-based `If-
34
34
  ``` ruby
35
35
  get "/" do
36
36
  cache_or_304 do
37
- { :counter => 42 }
37
+ { counter: 42 }
38
38
  end
39
39
  end
40
40
  ```
@@ -43,7 +43,7 @@ The cached value can also be bound to other models. For example, if a user has a
43
43
 
44
44
  ``` ruby
45
45
  get "/me/address" do
46
- cache_or_304({ :bind => [ User, current_user.id ] }) do
46
+ cache_or_304({ bind: [ User, current_user.id ] }) do
47
47
  current_user.address
48
48
  end
49
49
  end
@@ -144,6 +144,31 @@ Available Key Strategies
144
144
  * `Garner::Strategies::Keys::RequestGet` inserts the value of HTTP request's GET parameters into the cache key when `:request` is present in the context.
145
145
  * `Garner::Strategies::Keys::RequestPath` inserts the value of the HTTP request's path into the cache key when `:request` is present in the context.
146
146
 
147
+ Fetching Objects Directly from Cache
148
+ ------------------------------------
149
+
150
+ Garner supports fetching objects or collections of objects directly from cache by supplying a binding or an array of bindings.
151
+
152
+ ``` ruby
153
+ object_id = ...
154
+ Garner::Cache::ObjectIdentity.cache({ bind: [ Model, { id: object_id }] }) do
155
+ Model.find(object_id)
156
+ end
157
+ ```
158
+
159
+ Various cache stores, including Memcached, support bulk read operations. The [Dalli gem](https://github.com/mperham/dalli) exposes this via the `read_multi` method. When invoked with a collection of bindings, Garner will call `read_multi` if available. This may significantly reduce the number of network roundtrips to the cache servers.
160
+
161
+ ``` ruby
162
+ object_ids = [ ... ]
163
+ bindings = object_ids.map do |object_id|
164
+ { bind: [ Model, { id: object_id }]}
165
+ end
166
+ Garner::Cache::ObjectIdentity.cache_multi(bindings) do |binding|
167
+ # the object binding is passed into the block for every cache miss
168
+ Model.find(binding[:bind][1][:id])
169
+ end
170
+ ```
171
+
147
172
  Configuration
148
173
  -------------
149
174
 
@@ -31,7 +31,7 @@ module Garner
31
31
  # `bind: [{ klass: Artwork }, { klass: User, object: { id: current_user.id } }]`
32
32
  #
33
33
  module ObjectIdentity
34
-
34
+
35
35
  IDENTITY_FIELDS = [ :id ]
36
36
 
37
37
  KEY_STRATEGIES = [
@@ -40,39 +40,45 @@ module Garner
40
40
  Garner::Strategies::Keys::RequestPath,
41
41
  Garner::Strategies::Keys::RequestGet
42
42
  ]
43
-
43
+
44
44
  CACHE_STRATEGIES = [
45
45
  Garner::Strategies::Cache::Expiration
46
46
  ]
47
47
 
48
48
  ETAG_STRATEGY = Garner::Strategies::ETags::Grape
49
-
49
+
50
50
  class << self
51
51
 
52
52
  # cache the result of an executable block
53
- def cache(binding = nil, context = {})
54
- # apply cache strategies
55
- cache_options = cache_options(context)
56
- CACHE_STRATEGIES.each do |strategy|
57
- cache_options = strategy.apply(cache_options)
58
- end
59
- key = key(binding, key_context(context))
60
- result = Garner.config.cache.fetch(key, cache_options) do
61
- object = yield
62
- reset_cache_metadata(key, object)
63
- object
53
+ def cache(binding = nil, context = {}, &block)
54
+ cache_options = apply_cache_options(context)
55
+ ctx = key_context(context)
56
+ fetch(key(binding, ctx), nil, cache_options, &block)
57
+ end
58
+
59
+ # cache a collection of results
60
+ def cache_multi(bindings, context = {}, &block)
61
+ cache_options = apply_cache_options(context)
62
+ ctx = key_context(context)
63
+ keys = bindings.map do |binding|
64
+ key(binding, ctx)
65
+ end
66
+ # attempt to do a read_multi if the cache supports it
67
+ local_cache = keys.size > 1 && Garner.config.cache.respond_to?(:read_multi) ? Garner.config.cache.read_multi(keys) : {}
68
+ # fetch all missing values
69
+ bindings.each_with_index.map do |binding, index|
70
+ key = keys[index]
71
+ local_cache[key] || fetch(key, binding, cache_options, &block)
64
72
  end
65
- Garner.config.cache.delete(key) unless result
66
- result
67
73
  end
68
-
74
+
69
75
  # invalidate an object that has been cached
70
76
  def invalidate(* args)
71
77
  options = index(*args)
72
78
  reset_key_prefix_for(options[:klass], options[:object])
73
79
  reset_key_prefix_for(options[:klass]) if options[:object]
74
80
  end
75
-
81
+
76
82
  # metadata for cached objects:
77
83
  # :etag - Unique hash of object content
78
84
  # :last_modified - Timestamp of last modification event
@@ -80,14 +86,28 @@ module Garner
80
86
  key = key(binding, key_context(context))
81
87
  Garner.config.cache.read(meta(key))
82
88
  end
83
-
89
+
84
90
  private
85
91
 
92
+ def fetch(key, binding, cache_options = {}, &block)
93
+ result = Garner.config.cache.fetch(key, cache_options) do
94
+ object = binding ? yield(binding) : yield
95
+ reset_cache_metadata key, object
96
+ object
97
+ end
98
+ Garner.config.cache.delete(key) unless result
99
+ result
100
+ end
101
+
86
102
  # applied cache options
87
- def cache_options(context)
88
- context[:cache_options] || {}
103
+ def apply_cache_options(context)
104
+ cache_options = context[:cache_options] || {}
105
+ CACHE_STRATEGIES.each do |strategy|
106
+ cache_options = strategy.apply(cache_options)
107
+ end
108
+ cache_options
89
109
  end
90
-
110
+
91
111
  # applied key context
92
112
  def key_context(context)
93
113
  new_context = {}
@@ -111,11 +131,11 @@ module Garner
111
131
  meta_key = meta(key)
112
132
  Garner.config.cache.write(meta_key, metadata)
113
133
  end
114
-
134
+
115
135
  def new_key_prefix_for(klass, object = nil)
116
136
  Digest::MD5.hexdigest("#{klass}/#{object || "*"}:#{new_key_postfix}")
117
137
  end
118
-
138
+
119
139
  # Generate a key in the Klass/id format.
120
140
  # @example Widget/id=1,Gadget/slug=forty-two,Fudget/*
121
141
  def key(binding = nil, context = {})
@@ -131,7 +151,7 @@ module Garner
131
151
  KEY_STRATEGIES.map { |strategy| context[strategy.field] }.compact.join("\n")
132
152
  )
133
153
  end
134
-
154
+
135
155
  # Generate an index key from args
136
156
  def index(* args)
137
157
  case args[0]
@@ -150,7 +170,7 @@ module Garner
150
170
  raise ArgumentError, "invalid args, must be (klass, identifier) or hash (#{args})"
151
171
  end
152
172
  end
153
-
173
+
154
174
  def find_or_create_key_prefix_for(klass, object = nil)
155
175
  Garner.config.cache.fetch(index_string_for(klass, object), {}) do
156
176
  new_key_prefix_for(klass, object)
@@ -160,7 +180,7 @@ module Garner
160
180
  def new_key_prefix_for(klass, object = nil)
161
181
  Digest::MD5.hexdigest("#{klass}/#{object || "*"}:#{new_key_postfix}")
162
182
  end
163
-
183
+
164
184
  def new_key_postfix
165
185
  SecureRandom.respond_to?(:uuid) ? SecureRandom.uuid : (0...16).map{ ('a'..'z').to_a[rand(26)] }.join
166
186
  end
@@ -175,7 +195,7 @@ module Garner
175
195
  nil
176
196
  end
177
197
  end
178
-
198
+
179
199
  # Generate a metadata key.
180
200
  def meta(key)
181
201
  "#{key}:meta"
@@ -195,7 +215,7 @@ module Garner
195
215
  raise ArgumentError, "invalid argument type #{ary[0].class} in :bind (#{ary[0]})"
196
216
  end
197
217
  end
198
-
218
+
199
219
  def index_string_for(klass, object = nil)
200
220
  prefix = "INDEX"
201
221
  IDENTITY_FIELDS.each do |field|
@@ -205,7 +225,7 @@ module Garner
205
225
  end
206
226
  "#{prefix}:#{klass}/*"
207
227
  end
208
-
228
+
209
229
  end
210
230
  end
211
231
  end
@@ -1,3 +1,3 @@
1
1
  module Garner
2
- VERSION = '0.2.1'
2
+ VERSION = '0.3.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: garner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-01-22 00:00:00.000000000 Z
13
+ date: 2013-01-24 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rack
@@ -172,6 +172,22 @@ dependencies:
172
172
  - - ~>
173
173
  - !ruby/object:Gem::Version
174
174
  version: '3.0'
175
+ - !ruby/object:Gem::Dependency
176
+ name: dalli
177
+ requirement: !ruby/object:Gem::Requirement
178
+ none: false
179
+ requirements:
180
+ - - ~>
181
+ - !ruby/object:Gem::Version
182
+ version: '2.6'
183
+ type: :development
184
+ prerelease: false
185
+ version_requirements: !ruby/object:Gem::Requirement
186
+ none: false
187
+ requirements:
188
+ - - ~>
189
+ - !ruby/object:Gem::Version
190
+ version: '2.6'
175
191
  - !ruby/object:Gem::Dependency
176
192
  name: yard
177
193
  requirement: !ruby/object:Gem::Requirement
@@ -263,7 +279,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
263
279
  version: '0'
264
280
  segments:
265
281
  - 0
266
- hash: 889347935123245080
282
+ hash: 4499796191639143111
267
283
  required_rubygems_version: !ruby/object:Gem::Requirement
268
284
  none: false
269
285
  requirements: