response_bank 1.5.0 → 1.7.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
  SHA256:
3
- metadata.gz: c3db28140576d28fa3faf6af84b571085208f7f40b977e615b102f038d60632a
4
- data.tar.gz: ff991b1105a3c3b7827e5d7872ca55a648b692c6a67a67c144f5c458291adbe5
3
+ metadata.gz: 54fd4c438eab18c36f842002725ec243f6ca9837f03a2d70dc594a8bf4765d8d
4
+ data.tar.gz: cc5512b2fa88dd622da3ef9fae6abda48f9c7122eed828dd988a17343607f0cd
5
5
  SHA512:
6
- metadata.gz: face725382f16dc7ae054efd68de804eb303ff566bbf6758aa88d09922a2d8747e78fc9b9b2a9d109d508a0af9bcea28dc919d7f2b70954661c349f5feaaad2a
7
- data.tar.gz: b9101b86d39ddc5ea9a427326cfd58834bd5d3015509d0411007f286ad34cd00f1113c43f6d20d1d01a2cc317340d905dc9bfb1b24bb2e2df8c6509ca897b00e
6
+ metadata.gz: 9637541060aab3ac047247fdd13e83d57b99e3921689a7dd354ce684c247f9decb04de2c784e6f04bb57796e71279098a2f1a3db670d67ad25bb8406a7eb30f3
7
+ data.tar.gz: d3ba8c122b58152cd1e10e8d1fb504b65c2e8c906d3a862be98050f7b252129d392bbc83d0e220a2c79248f0da16bb3f85d8ae441c0c2b03281ee123a872b4bf
data/README.md CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  This gem supports the following versions of Ruby and Rails:
13
13
 
14
- * Ruby 3.1.0+
14
+ * Ruby 3.3.0+
15
15
  * Rails 6.0.0+
16
16
 
17
17
  ## Usage
@@ -169,6 +169,24 @@ The headers passed to `complete` describe the cached representation. They can di
169
169
 
170
170
  `abort` is idempotent and releases an owned fill lock through `ResponseBank.release_lock`. Its default implementation is a no-op. The existing `write_to_cache` hook remains responsible for cleanup after a write attempt. An integration that releases those fills from `write_to_cache` should also implement `release_lock` for abandoned fills and failures that happen before the write hook. A key-only lock cannot prevent an old fill from releasing a replacement lock after its lease expires; integrations that need that guarantee must use owner tokens in their lock implementation.
171
171
 
172
+ ## Cache Entry Metadata
173
+
174
+ Applications can store a Hash inside a cache entry by setting
175
+ `env['cacheable.metadata']` before the response is stored:
176
+
177
+ ```ruby
178
+ env['cacheable.metadata'] = { 'variant' => 'b' }
179
+ ```
180
+
181
+ On a server cache hit, `env['cacheable.metadata']` holds the Hash stored with the
182
+ served entry, stale or fresh; the key is removed when the entry has none. The Hash
183
+ is stored once and shared by every request served from the entry, so it must not
184
+ contain anything visitor-specific. It is serialized with MessagePack (String keys on
185
+ read) and nested under `ResponseBank::APP_METADATA_KEY`, so it cannot collide with
186
+ ResponseBank's own slots. Keep it to plain MessagePack types: a Hash that does not
187
+ survive a MessagePack round-trip, because a value cannot be serialized or the nesting
188
+ is too deep for the unpacker, is logged and dropped, and the response is cached without it.
189
+
172
190
  ## Brotli Splice Slots
173
191
 
174
192
  Applications that need per-request replacement inside cached Brotli HTML responses can pass an injector builder to `ResponseBank::Middleware`:
@@ -287,6 +305,19 @@ Advanced integrations can still install the per-request injector directly in the
287
305
  env[ResponseBank::BrotliSpliceSlot::INJECTOR_ENV_KEY] = injector
288
306
  ```
289
307
 
308
+ ## Observing server cache hits
309
+
310
+ After successfully serving an entry from the server cache, ResponseBank exposes its generation time and stale-while-revalidate status in the Rack environment:
311
+
312
+ ```ruby
313
+ timestamp = env['cacheable.timestamp']
314
+ stale = env['cacheable.stale']
315
+ ```
316
+
317
+ `cacheable.timestamp` is only set after successfully serving a server-cache entry.
318
+ `cacheable.stale` is initialized to `false` when a server-cache entry is evaluated
319
+ and set to `true` only when ResponseBank serves it through stale-while-revalidate.
320
+
290
321
  ## Exception Handling
291
322
 
292
323
  ResponseBank handles all exceptions gracefully during cache operations. If an exception occurs while reading from cache, deserializing cached data, or writing to cache, the middleware will:
@@ -3,4 +3,11 @@
3
3
  module ResponseBank
4
4
  CACHEABLE_HEADERS = ["Location", "Content-Type", "ETag", "Content-Encoding", "Last-Modified", "Cache-Control", "Expires", "Link", "Surrogate-Keys", "Cache-Tags", "Speculation-Rules"].freeze
5
5
  CACHEABLE_STATUSES = [200, 404, 301].freeze
6
+
7
+ # Application metadata stored inside the cache entry: set `env[METADATA_ENV_KEY]`
8
+ # (a Hash) before the response is stored; on a server cache hit it holds the Hash
9
+ # stored with the served entry.
10
+ METADATA_ENV_KEY = 'cacheable.metadata'
11
+ # Keeps application metadata apart from ResponseBank's own slots.
12
+ APP_METADATA_KEY = 'app'
6
13
  end
@@ -157,9 +157,34 @@ module ResponseBank
157
157
  end
158
158
  cached_headers = representation_headers.slice(*ResponseBank::CACHEABLE_HEADERS)
159
159
  data = [status, cached_headers, stored.compressed_body, timestamp, env['cacheable.compression_level']]
160
- data << stored.metadata if stored.metadata
160
+ metadata = entry_metadata(env, stored.metadata)
161
+ data << metadata if metadata
161
162
  data
162
163
  end
164
+
165
+ # Application metadata never fails the write, nor the reads that follow: anything
166
+ # that is not a Hash, or that does not round-trip through MessagePack, is logged
167
+ # and left out of the entry.
168
+ def entry_metadata(env, metadata)
169
+ app_metadata = env[ResponseBank::METADATA_ENV_KEY]
170
+ return metadata if app_metadata.nil?
171
+ unless app_metadata.is_a?(Hash)
172
+ ResponseBank.log("Ignoring #{ResponseBank::METADATA_ENV_KEY}: expected a Hash, got #{app_metadata.class}")
173
+ return metadata
174
+ end
175
+
176
+ merged = (metadata || {}).merge(ResponseBank::APP_METADATA_KEY => app_metadata)
177
+ begin
178
+ # Loading catches what dumping accepts but the reader rejects, such as nesting
179
+ # past the unpacker's stack; the array puts the Hash at its depth in the entry.
180
+ MessagePack.load(MessagePack.dump([merged]))
181
+ rescue StandardError => error
182
+ ResponseBank.log("Ignoring #{ResponseBank::METADATA_ENV_KEY}: #{error.class} - #{error.message}")
183
+ return metadata
184
+ end
185
+
186
+ merged
187
+ end
163
188
  end
164
189
  end
165
190
  end
@@ -133,6 +133,7 @@ module ResponseBank
133
133
  @env['cacheable.compression_level'] = compression_level
134
134
 
135
135
  @env['cacheable.locked'] ||= false
136
+ @env['cacheable.stale'] = false
136
137
 
137
138
  # to preserve the unversioned/versioned logging messages from past releases we split the match_entity_tag test
138
139
  if match_entity_tag == "*"
@@ -149,6 +150,7 @@ module ResponseBank
149
150
  return
150
151
  elsif stale_while_revalidate?(timestamp, cache_age_tolerance)
151
152
  # cache is being regenerated, can we avoid piling on and use a stale version in the interim?
153
+ @env['cacheable.stale'] = true
152
154
  ResponseBank.log("Cache hit: server (recent)")
153
155
  else
154
156
  ResponseBank.log("Found an unversioned cache entry, but it was too old (#{timestamp})")
@@ -176,6 +178,15 @@ module ResponseBank
176
178
  ResponseBank.log("Cache hit, but missing content-encoding in the cache value headers, maybe because of 301 or 404 response or empty body string")
177
179
  end
178
180
 
181
+ # After decompression and splicing, so a refill never inherits the rejected
182
+ # entry's metadata; cleared when the served entry has none.
183
+ if metadata&.key?(ResponseBank::APP_METADATA_KEY)
184
+ @env[ResponseBank::METADATA_ENV_KEY] = metadata[ResponseBank::APP_METADATA_KEY]
185
+ else
186
+ @env.delete(ResponseBank::METADATA_ENV_KEY)
187
+ end
188
+
189
+ @env['cacheable.timestamp'] = timestamp
179
190
  [status, @headers, [body]]
180
191
 
181
192
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module ResponseBank
3
- VERSION = "1.5.0"
3
+ VERSION = "1.7.0"
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: response_bank
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.0
4
+ version: 1.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Lütke
@@ -169,14 +169,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
169
169
  requirements:
170
170
  - - ">="
171
171
  - !ruby/object:Gem::Version
172
- version: 3.1.0
172
+ version: 3.3.0
173
173
  required_rubygems_version: !ruby/object:Gem::Requirement
174
174
  requirements:
175
175
  - - ">="
176
176
  - !ruby/object:Gem::Version
177
177
  version: '0'
178
178
  requirements: []
179
- rubygems_version: 4.0.19
179
+ rubygems_version: 4.0.21
180
180
  specification_version: 4
181
181
  summary: Simple response caching for Ruby applications
182
182
  test_files: []