async-http-cache 0.1.0 → 0.1.1

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
  SHA256:
3
- metadata.gz: 21fb7edbe7e010844dd2e65be5e6e89a254cf5255fd69fc8ab5361a8887fe736
4
- data.tar.gz: ba72529946ade707aa5eb8b841f1dc1dbe4fcb89386b00d626e4cb2f6ddad4fe
3
+ metadata.gz: 923d97088cda9896dcec9c48c21b6142fc9002acfc15ae70a5cb6fac4eacf086
4
+ data.tar.gz: 9819dd06fa5f44a42c7b9f866477fa163df6e857ce0c5618306f5962f2bbfcc1
5
5
  SHA512:
6
- metadata.gz: 3b595aa2984299737811729f0834e9fdef80f06aff21f3ce408f7ee2ccf02d1e93791559782b8568cb5aeb04674b65d8d0d10c54055ea297ce7d5765b4409152
7
- data.tar.gz: 80617864a94487a8ab42bec43f005ff434353b774810613815d0894da5c762491ecb91ccbdac609470d2da30ff39f94844273a9051d4c63183ff52c4ac36519e
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
@@ -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.2"
25
+ spec.add_dependency "protocol-http", "~> 0.14.4"
26
26
 
27
27
  spec.add_development_dependency "async-rspec", "~> 1.10"
28
28
 
@@ -37,7 +37,9 @@ module Async
37
37
 
38
38
  # Wrap the message with the callback:
39
39
  ::Protocol::HTTP::Body::Streamable.wrap(message) do |error|
40
- unless error
40
+ if error
41
+ Async.logger.error(self) {error}
42
+ else
41
43
  yield message, rewindable.buffered
42
44
  end
43
45
  end
@@ -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
- # We only support caching GET and HEAD requests:
68
- if request.method == 'GET' || request.method == 'HEAD'
69
- return true
81
+ if request.headers[COOKIE]
82
+ return false
70
83
  end
71
84
 
72
- # Otherwise, we can't cache it:
73
- return false
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[key] = response
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(store, vary = {})
31
- @store = store
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.extract(vary)
59
+ key = key + key_for(request.headers, vary)
39
60
  end
40
61
 
41
- return @store.lookup(key, request)
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 = key + request.headers.extract(vary)
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
- @store.insert(key, request, response)
72
+ @delegate.insert(key, request, response)
50
73
  end
51
74
  end
52
75
  end
@@ -23,7 +23,7 @@
23
23
  module Async
24
24
  module HTTP
25
25
  module Cache
26
- VERSION = "0.1.0"
26
+ VERSION = "0.1.1"
27
27
  end
28
28
  end
29
29
  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.0
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-08 00:00:00.000000000 Z
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.2
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.2
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.0.6
152
+ rubygems_version: 3.1.2
153
153
  signing_key:
154
154
  specification_version: 4
155
155
  summary: Standard-compliant cache for async-http.