async-http-cache 0.1.0 → 0.1.1
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 +4 -0
- data/async-http-cache.gemspec +1 -1
- data/lib/async/http/cache/body.rb +3 -1
- data/lib/async/http/cache/general.rb +20 -17
- data/lib/async/http/cache/response.rb +8 -9
- data/lib/async/http/cache/store/memory.rb +60 -2
- data/lib/async/http/cache/store/vary.rb +30 -7
- data/lib/async/http/cache/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 923d97088cda9896dcec9c48c21b6142fc9002acfc15ae70a5cb6fac4eacf086
|
4
|
+
data.tar.gz: 9819dd06fa5f44a42c7b9f866477fa163df6e857ce0c5618306f5962f2bbfcc1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1ffbd84fe0928014e500f0290a25720581e50ef688e30ad8161fd3b62ea7a6acc21fe2b40a12cf47ef0f3ec2e007e2613f63cb4fdf1518bf324ae88c63b58ad
|
7
|
+
data.tar.gz: e4ece204ed54c4d23c1e831d887ae2b7cb169e702b80e890f4bac5438f393fb31c15a873f494ac1ffcc1d3a88663b4eb12e0abb2c95c9fee4e4f030771158b8c
|
data/README.md
CHANGED
@@ -30,6 +30,10 @@ ensure
|
|
30
30
|
end
|
31
31
|
```
|
32
32
|
|
33
|
+
## Vary
|
34
|
+
|
35
|
+
The `vary` header creates a headache for proxy implementations, because it creates a combinatorial explosion of cache keys, even if the content is the same. Try to avoid it unless absolutely necessary.
|
36
|
+
|
33
37
|
## Contributing
|
34
38
|
|
35
39
|
1. Fork it
|
data/async-http-cache.gemspec
CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
24
|
spec.add_dependency "async-http"
|
25
|
-
spec.add_dependency "protocol-http", "~> 0.14.
|
25
|
+
spec.add_dependency "protocol-http", "~> 0.14.4"
|
26
26
|
|
27
27
|
spec.add_development_dependency "async-rspec", "~> 1.10"
|
28
28
|
|
@@ -33,6 +33,7 @@ module Async
|
|
33
33
|
CACHE_CONTROL = 'cache-control'
|
34
34
|
CONTENT_TYPE = 'content-type'
|
35
35
|
AUTHORIZATION = 'authorization'
|
36
|
+
COOKIE = 'cookie'
|
36
37
|
|
37
38
|
def initialize(app, store: Store.default)
|
38
39
|
super(app)
|
@@ -40,12 +41,20 @@ module Async
|
|
40
41
|
@count = 0
|
41
42
|
|
42
43
|
@store = store
|
43
|
-
@maximum_length = 128 * 1024
|
44
44
|
end
|
45
45
|
|
46
46
|
attr :count
|
47
|
+
attr :store
|
48
|
+
|
49
|
+
def close
|
50
|
+
@store.close
|
51
|
+
ensure
|
52
|
+
super
|
53
|
+
end
|
47
54
|
|
48
55
|
def key(request)
|
56
|
+
@store.normalize(request)
|
57
|
+
|
49
58
|
[request.authority, request.method, request.path]
|
50
59
|
end
|
51
60
|
|
@@ -60,17 +69,21 @@ module Async
|
|
60
69
|
return false
|
61
70
|
end
|
62
71
|
|
72
|
+
# We only support caching GET and HEAD requests:
|
73
|
+
unless request.method == 'GET' || request.method == 'HEAD'
|
74
|
+
return false
|
75
|
+
end
|
76
|
+
|
63
77
|
if request.headers[AUTHORIZATION]
|
64
78
|
return false
|
65
79
|
end
|
66
80
|
|
67
|
-
|
68
|
-
|
69
|
-
return true
|
81
|
+
if request.headers[COOKIE]
|
82
|
+
return false
|
70
83
|
end
|
71
84
|
|
72
|
-
# Otherwise, we can
|
73
|
-
return
|
85
|
+
# Otherwise, we can cache it:
|
86
|
+
return true
|
74
87
|
end
|
75
88
|
|
76
89
|
def wrap(key, request, response)
|
@@ -78,17 +91,8 @@ module Async
|
|
78
91
|
return response
|
79
92
|
end
|
80
93
|
|
81
|
-
if body = response.body
|
82
|
-
if length = body.length
|
83
|
-
# Don't cache responses bigger than 128Kb:
|
84
|
-
return response if length > @maximum_length
|
85
|
-
else
|
86
|
-
# Don't cache responses without length:
|
87
|
-
return response
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
94
|
return Body.wrap(response) do |message, body|
|
95
|
+
Async.logger.debug(self) {"Updating cache for #{key}..."}
|
92
96
|
@store.insert(key, request, Response.new(message, body))
|
93
97
|
end
|
94
98
|
end
|
@@ -110,7 +114,6 @@ module Async
|
|
110
114
|
|
111
115
|
unless cache_control&.no_store?
|
112
116
|
if cacheable?(request)
|
113
|
-
Async.logger.debug(self) {"Updating cache for #{key}..."}
|
114
117
|
return wrap(key, request, super)
|
115
118
|
end
|
116
119
|
end
|
@@ -47,19 +47,18 @@ module Async
|
|
47
47
|
attr :generated_at
|
48
48
|
|
49
49
|
def cachable?
|
50
|
-
if set_cookie = @headers[SET_COOKIE]
|
51
|
-
return false
|
52
|
-
end
|
53
|
-
|
54
50
|
if cache_control = @headers[CACHE_CONTROL]
|
55
|
-
if cache_control.private?
|
51
|
+
if cache_control.private? || !cache_control.public?
|
56
52
|
return false
|
57
53
|
end
|
58
|
-
|
59
|
-
if cache_control.public?
|
60
|
-
return true
|
61
|
-
end
|
62
54
|
end
|
55
|
+
|
56
|
+
if set_cookie = @headers[SET_COOKIE]
|
57
|
+
Async.logger.warn(self) {"Cannot cache response with set-cookie header!"}
|
58
|
+
return false
|
59
|
+
end
|
60
|
+
|
61
|
+
return true
|
63
62
|
end
|
64
63
|
|
65
64
|
def age
|
@@ -25,24 +25,82 @@ module Async
|
|
25
25
|
module Cache
|
26
26
|
module Store
|
27
27
|
class Memory
|
28
|
-
def initialize
|
28
|
+
def initialize(limit: 1024)
|
29
29
|
@index = {}
|
30
|
+
@limit = limit
|
31
|
+
|
32
|
+
@hit = 0
|
33
|
+
@miss = 0
|
34
|
+
@pruned = 0
|
35
|
+
|
36
|
+
@gardener = Async do |task|
|
37
|
+
while true
|
38
|
+
task.sleep(60)
|
39
|
+
|
40
|
+
pruned = self.prune
|
41
|
+
@pruned += pruned
|
42
|
+
|
43
|
+
Async.logger.debug(self) do |buffer|
|
44
|
+
if pruned > 0
|
45
|
+
buffer.puts "Pruned #{pruned} entries."
|
46
|
+
end
|
47
|
+
|
48
|
+
buffer.puts "Hits: #{@hit} Misses: #{@miss} Pruned: #{@pruned} Ratio: #{(100.0*@hit/@miss).round(2)}%"
|
49
|
+
|
50
|
+
body_usage = @index.sum{|key, value| value.body.length}
|
51
|
+
buffer.puts "Index size: #{@index.size} Memory usage: #{(body_usage / 1024.0**2).round(2)}MiB"
|
52
|
+
|
53
|
+
# @index.each do |key, value|
|
54
|
+
# buffer.puts "#{key.join('-')}: #{value.body.length}B"
|
55
|
+
# end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
30
59
|
end
|
31
60
|
|
61
|
+
def close
|
62
|
+
@gardener.stop
|
63
|
+
end
|
64
|
+
|
65
|
+
attr :index
|
66
|
+
|
32
67
|
def lookup(key, request)
|
33
68
|
if response = @index[key]
|
34
69
|
if response.expired?
|
35
70
|
@index.delete(key)
|
36
71
|
|
72
|
+
@pruned += 1
|
73
|
+
|
37
74
|
return nil
|
38
75
|
end
|
39
76
|
|
77
|
+
@hit += 1
|
78
|
+
|
40
79
|
return response.dup
|
80
|
+
else
|
81
|
+
@miss += 1
|
82
|
+
|
83
|
+
return nil
|
41
84
|
end
|
42
85
|
end
|
43
86
|
|
44
87
|
def insert(key, request, response)
|
45
|
-
@index
|
88
|
+
if @index.size < @limit
|
89
|
+
if response.body.length < 1024*64
|
90
|
+
@index[key] = response
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Integer] the number of pruned entries.
|
96
|
+
def prune
|
97
|
+
initial_count = @index.size
|
98
|
+
|
99
|
+
@index.delete_if do |key, value|
|
100
|
+
value.expired?
|
101
|
+
end
|
102
|
+
|
103
|
+
return initial_count - @index.size
|
46
104
|
end
|
47
105
|
end
|
48
106
|
end
|
@@ -25,28 +25,51 @@ module Async
|
|
25
25
|
module Cache
|
26
26
|
module Store
|
27
27
|
VARY = 'vary'
|
28
|
+
ACCEPT_ENCODING = 'accept-encoding'
|
28
29
|
|
29
30
|
class Vary
|
30
|
-
def initialize(
|
31
|
-
@
|
31
|
+
def initialize(delegate, vary = {})
|
32
|
+
@delegate = delegate
|
32
33
|
@vary = vary
|
33
34
|
end
|
34
35
|
|
36
|
+
def close
|
37
|
+
@delegate.close
|
38
|
+
end
|
39
|
+
|
40
|
+
attr :delegate
|
41
|
+
|
42
|
+
def normalize(request)
|
43
|
+
if accept_encoding = request.headers[ACCEPT_ENCODING]
|
44
|
+
if accept_encoding.include?("gzip")
|
45
|
+
request.headers.set(ACCEPT_ENCODING, "gzip")
|
46
|
+
else
|
47
|
+
request.headers.delete(ACCEPT_ENCODING)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def key_for(headers, vary)
|
53
|
+
vary.map{|key| headers[key]}
|
54
|
+
end
|
55
|
+
|
35
56
|
def lookup(key, request)
|
36
57
|
if vary = @vary[key]
|
37
58
|
# We should provide user-supported normalization here:
|
38
|
-
key = key + request.headers
|
59
|
+
key = key + key_for(request.headers, vary)
|
39
60
|
end
|
40
61
|
|
41
|
-
return @
|
62
|
+
return @delegate.lookup(key, request)
|
42
63
|
end
|
43
64
|
|
44
65
|
def insert(key, request, response)
|
45
|
-
if vary = response.headers[VARY]
|
46
|
-
key =
|
66
|
+
if vary = response.headers[VARY]&.sort
|
67
|
+
@vary[key] = vary
|
68
|
+
|
69
|
+
key = key + key_for(request.headers, vary)
|
47
70
|
end
|
48
71
|
|
49
|
-
@
|
72
|
+
@delegate.insert(key, request, response)
|
50
73
|
end
|
51
74
|
end
|
52
75
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: async-http-cache
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-03-
|
11
|
+
date: 2020-03-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: async-http
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: 0.14.
|
33
|
+
version: 0.14.4
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: 0.14.
|
40
|
+
version: 0.14.4
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: async-rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -149,7 +149,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
149
149
|
- !ruby/object:Gem::Version
|
150
150
|
version: '0'
|
151
151
|
requirements: []
|
152
|
-
rubygems_version: 3.
|
152
|
+
rubygems_version: 3.1.2
|
153
153
|
signing_key:
|
154
154
|
specification_version: 4
|
155
155
|
summary: Standard-compliant cache for async-http.
|