response_bank 1.4.0 → 1.5.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 +4 -4
- data/README.md +20 -0
- data/lib/response_bank/brotli_splice_slot.rb +31 -15
- data/lib/response_bank/cache_writer.rb +55 -7
- data/lib/response_bank/deferred_store.rb +40 -17
- data/lib/response_bank/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c3db28140576d28fa3faf6af84b571085208f7f40b977e615b102f038d60632a
|
|
4
|
+
data.tar.gz: ff991b1105a3c3b7827e5d7872ca55a648b692c6a67a67c144f5c458291adbe5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: face725382f16dc7ae054efd68de804eb303ff566bbf6758aa88d09922a2d8747e78fc9b9b2a9d109d508a0af9bcea28dc919d7f2b70954661c349f5feaaad2a
|
|
7
|
+
data.tar.gz: b9101b86d39ddc5ea9a427326cfd58834bd5d3015509d0411007f286ad34cd00f1113c43f6d20d1d01a2cc317340d905dc9bfb1b24bb2e2df8c6509ca897b00e
|
data/README.md
CHANGED
|
@@ -145,6 +145,26 @@ The middleware arms the handle after the application returns the Rack tuple. It
|
|
|
145
145
|
|
|
146
146
|
Call `complete` only after the intended response was generated and written successfully. Never call it from a rescue or ensure path. Do not complete failed, timed-out, disconnected, or truncated responses. The body must be the shared cache representation and must not contain client-specific data added for the live response.
|
|
147
147
|
|
|
148
|
+
A caller that has already produced a Brotli representation can complete the fill without a second compression pass:
|
|
149
|
+
|
|
150
|
+
```ruby
|
|
151
|
+
deferred_store.complete_spliced(
|
|
152
|
+
headers: cache_headers,
|
|
153
|
+
body: compressed_body,
|
|
154
|
+
compression_level: 5,
|
|
155
|
+
slot: {
|
|
156
|
+
name: 'shopify_y',
|
|
157
|
+
compressed_offset: slot_offset,
|
|
158
|
+
replacement_length: replacement_length,
|
|
159
|
+
html_placeholder_offset: html_placeholder_offset,
|
|
160
|
+
html_placeholder_length: html_placeholder_length,
|
|
161
|
+
context_suffix: "\r\n",
|
|
162
|
+
},
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The body must be a complete, non-empty Brotli stream in the server cache encoding. It must already be safe for shared caching: if the live response contained client-specific slot bytes, replace them with the neutral placeholder before calling `complete_spliced`. ResponseBank builds versioned cache metadata from the slot descriptor. Omit `slot` when the Brotli stream has no replaceable content.
|
|
167
|
+
|
|
148
168
|
The headers passed to `complete` describe the cached representation. They can differ from headers already sent to the client. ResponseBank rejects final cache headers that contain `private` or `no-store`, and it does not mutate the supplied hash. It captures the cache timestamp when `defer_store` is called, before deferred rendering and compression.
|
|
149
169
|
|
|
150
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.
|
|
@@ -40,24 +40,17 @@ module ResponseBank
|
|
|
40
40
|
begin
|
|
41
41
|
result = BrotliSplice.encode(prepared_body, html_offset, html_length, quality: compression_level)
|
|
42
42
|
|
|
43
|
-
metadata_slot = {
|
|
44
|
-
'name' => slot_name,
|
|
45
|
-
'compressed_offset' => result[:secret_offset],
|
|
46
|
-
'replacement_length' => result[:secret_length],
|
|
47
|
-
'html_placeholder_offset' => html_offset,
|
|
48
|
-
'html_placeholder_length' => html_length,
|
|
49
|
-
}
|
|
50
|
-
metadata_slot['context_suffix'] = result[:context_suffix] if result[:context_suffix]
|
|
51
|
-
|
|
52
43
|
EncodedBody.new(
|
|
53
44
|
body: prepared_body,
|
|
54
45
|
compressed_body: result[:data],
|
|
55
|
-
metadata:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
46
|
+
metadata: metadata_for(
|
|
47
|
+
name: slot_name,
|
|
48
|
+
compressed_offset: result[:secret_offset],
|
|
49
|
+
replacement_length: result[:secret_length],
|
|
50
|
+
html_placeholder_offset: html_offset,
|
|
51
|
+
html_placeholder_length: html_length,
|
|
52
|
+
context_suffix: result[:context_suffix],
|
|
53
|
+
),
|
|
61
54
|
)
|
|
62
55
|
rescue BrotliSplice::Error, ArgumentError => error
|
|
63
56
|
ResponseBank.log("BrotliSplice encode skipped: #{error.class}")
|
|
@@ -65,6 +58,29 @@ module ResponseBank
|
|
|
65
58
|
end
|
|
66
59
|
end
|
|
67
60
|
|
|
61
|
+
def metadata_for(
|
|
62
|
+
name:,
|
|
63
|
+
compressed_offset:,
|
|
64
|
+
replacement_length:,
|
|
65
|
+
html_placeholder_offset:,
|
|
66
|
+
html_placeholder_length:,
|
|
67
|
+
context_suffix:
|
|
68
|
+
)
|
|
69
|
+
{
|
|
70
|
+
METADATA_KEY => {
|
|
71
|
+
'version' => METADATA_VERSION,
|
|
72
|
+
'slots' => [{
|
|
73
|
+
'name' => name.to_s,
|
|
74
|
+
'compressed_offset' => compressed_offset,
|
|
75
|
+
'replacement_length' => replacement_length,
|
|
76
|
+
'html_placeholder_offset' => html_placeholder_offset,
|
|
77
|
+
'html_placeholder_length' => html_placeholder_length,
|
|
78
|
+
'context_suffix' => context_suffix,
|
|
79
|
+
}],
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
68
84
|
def replace_compressed_secret(env, body, metadata)
|
|
69
85
|
injector = env[INJECTOR_ENV_KEY]
|
|
70
86
|
slots = metadata_slots(metadata)
|
|
@@ -30,20 +30,61 @@ module ResponseBank
|
|
|
30
30
|
content_encoding: env.fetch('response_bank.server_cache_encoding'),
|
|
31
31
|
before_write: nil
|
|
32
32
|
)
|
|
33
|
-
|
|
34
|
-
unversioned_key = env.fetch('cacheable.unversioned-key')
|
|
35
|
-
representation_headers = headers.slice(*ResponseBank::CACHEABLE_HEADERS)
|
|
36
|
-
representation_headers['ETag'] = %{"#{cache_key}"}
|
|
33
|
+
representation_headers = representation_headers(env, headers)
|
|
37
34
|
stored = prepare_body(env, representation_headers, body, content_encoding)
|
|
35
|
+
persist(
|
|
36
|
+
env,
|
|
37
|
+
status: status,
|
|
38
|
+
representation_headers: representation_headers,
|
|
39
|
+
stored: stored,
|
|
40
|
+
timestamp: timestamp,
|
|
41
|
+
content_encoding: content_encoding,
|
|
42
|
+
before_write: before_write,
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def store_spliced(
|
|
47
|
+
env,
|
|
48
|
+
status:,
|
|
49
|
+
headers:,
|
|
50
|
+
body:,
|
|
51
|
+
compression_level:,
|
|
52
|
+
slot: nil,
|
|
53
|
+
timestamp:,
|
|
54
|
+
before_write: nil
|
|
55
|
+
)
|
|
56
|
+
validate_spliced_body!(env, body)
|
|
57
|
+
metadata = slot && BrotliSpliceSlot.metadata_for(**slot)
|
|
58
|
+
env['cacheable.compression_level'] = compression_level
|
|
59
|
+
persist(
|
|
60
|
+
env,
|
|
61
|
+
status: status,
|
|
62
|
+
representation_headers: representation_headers(env, headers),
|
|
63
|
+
stored: Stored.new(body: nil, compressed_body: body, metadata: metadata),
|
|
64
|
+
timestamp: timestamp,
|
|
65
|
+
content_encoding: 'br',
|
|
66
|
+
before_write: before_write,
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def representation_headers(env, headers)
|
|
73
|
+
headers.slice(*ResponseBank::CACHEABLE_HEADERS).tap do |cached_headers|
|
|
74
|
+
cached_headers['ETag'] = %{"#{env.fetch('cacheable.key')}"}
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def persist(env, status:, representation_headers:, stored:, timestamp:, content_encoding:, before_write:)
|
|
38
79
|
generated_at = timestamp.respond_to?(:call) ? timestamp.call : timestamp
|
|
39
80
|
data = cache_data(status, representation_headers, stored, env, generated_at, content_encoding)
|
|
40
81
|
|
|
41
82
|
before_write&.call
|
|
42
|
-
ResponseBank.write_to_cache(
|
|
83
|
+
ResponseBank.write_to_cache(env.fetch('cacheable.key')) do
|
|
43
84
|
payload = MessagePack.dump(data)
|
|
44
85
|
ResponseBank.write_to_backing_cache_store(
|
|
45
86
|
env,
|
|
46
|
-
|
|
87
|
+
env.fetch('cacheable.unversioned-key'),
|
|
47
88
|
payload,
|
|
48
89
|
expires_in: env['cacheable.versioned-cache-expiry'],
|
|
49
90
|
)
|
|
@@ -52,7 +93,14 @@ module ResponseBank
|
|
|
52
93
|
stored
|
|
53
94
|
end
|
|
54
95
|
|
|
55
|
-
|
|
96
|
+
def validate_spliced_body!(env, body)
|
|
97
|
+
unless body.is_a?(String) && !body.empty?
|
|
98
|
+
raise ArgumentError, 'spliced body must be a non-empty String'
|
|
99
|
+
end
|
|
100
|
+
return if env.fetch('response_bank.server_cache_encoding') == 'br'
|
|
101
|
+
|
|
102
|
+
raise ArgumentError, 'spliced bodies require br server cache encoding'
|
|
103
|
+
end
|
|
56
104
|
|
|
57
105
|
def prepare_body(env, headers, body, content_encoding)
|
|
58
106
|
body = flatten(body)
|
|
@@ -60,32 +60,33 @@ module ResponseBank
|
|
|
60
60
|
# `body` and `headers` must describe the complete shared cache representation,
|
|
61
61
|
# not a partial response or bytes personalized for the live client.
|
|
62
62
|
def complete(body:, headers: nil)
|
|
63
|
-
status, cached_headers,
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
63
|
+
complete_with(headers) do |status, cached_headers, before_write|
|
|
64
|
+
CacheWriter.store(
|
|
65
|
+
@env,
|
|
66
|
+
status: status,
|
|
67
|
+
headers: cached_headers,
|
|
68
|
+
body: body,
|
|
69
|
+
timestamp: @timestamp,
|
|
70
|
+
before_write: before_write,
|
|
71
|
+
)
|
|
68
72
|
end
|
|
69
|
-
|
|
73
|
+
end
|
|
70
74
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
# `body` must be a complete Brotli stream that is already safe for shared
|
|
76
|
+
# caching. If present, `slot` describes its neutral replacement slot.
|
|
77
|
+
def complete_spliced(body:, compression_level:, slot: nil, headers: nil)
|
|
78
|
+
complete_with(headers) do |status, cached_headers, before_write|
|
|
79
|
+
CacheWriter.store_spliced(
|
|
75
80
|
@env,
|
|
76
81
|
status: status,
|
|
77
82
|
headers: cached_headers,
|
|
78
83
|
body: body,
|
|
84
|
+
compression_level: compression_level,
|
|
85
|
+
slot: slot,
|
|
79
86
|
timestamp: @timestamp,
|
|
80
|
-
before_write:
|
|
87
|
+
before_write: before_write,
|
|
81
88
|
)
|
|
82
|
-
completed = true
|
|
83
|
-
ensure
|
|
84
|
-
@mutex.synchronize { @state = :consumed }
|
|
85
|
-
@env['cacheable.locked'] = false if @owns_lock
|
|
86
|
-
release_owned_lock if @owns_lock && !completed && !write_started
|
|
87
89
|
end
|
|
88
|
-
true
|
|
89
90
|
end
|
|
90
91
|
|
|
91
92
|
def abort
|
|
@@ -104,6 +105,28 @@ module ResponseBank
|
|
|
104
105
|
|
|
105
106
|
private
|
|
106
107
|
|
|
108
|
+
def complete_with(headers)
|
|
109
|
+
status, cached_headers, release_lock = prepare_completion(headers)
|
|
110
|
+
|
|
111
|
+
if release_lock
|
|
112
|
+
release_owned_lock
|
|
113
|
+
return false
|
|
114
|
+
end
|
|
115
|
+
return false unless status
|
|
116
|
+
|
|
117
|
+
write_started = false
|
|
118
|
+
completed = false
|
|
119
|
+
begin
|
|
120
|
+
yield(status, cached_headers, -> { write_started = true })
|
|
121
|
+
completed = true
|
|
122
|
+
ensure
|
|
123
|
+
@mutex.synchronize { @state = :consumed }
|
|
124
|
+
@env['cacheable.locked'] = false if @owns_lock
|
|
125
|
+
release_owned_lock if @owns_lock && !completed && !write_started
|
|
126
|
+
end
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
|
|
107
130
|
def arm(status:, headers:)
|
|
108
131
|
@mutex.synchronize do
|
|
109
132
|
if @state != :aborted
|
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.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tobias Lütke
|
|
@@ -58,14 +58,14 @@ dependencies:
|
|
|
58
58
|
requirements:
|
|
59
59
|
- - '='
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 0.
|
|
61
|
+
version: 0.2.0
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - '='
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 0.
|
|
68
|
+
version: 0.2.0
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: minitest
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -176,7 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
176
176
|
- !ruby/object:Gem::Version
|
|
177
177
|
version: '0'
|
|
178
178
|
requirements: []
|
|
179
|
-
rubygems_version: 4.0.
|
|
179
|
+
rubygems_version: 4.0.19
|
|
180
180
|
specification_version: 4
|
|
181
181
|
summary: Simple response caching for Ruby applications
|
|
182
182
|
test_files: []
|