acorn_cache 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: cd4a59e38e279614a289b1a8e1e6c235389f615b
4
+ data.tar.gz: 1558ff76e457e53fc6530c180cd5381ec050cbb9
5
+ SHA512:
6
+ metadata.gz: 3014f53f2ec919dcbcae36b7c81c0ce7c686bf71499b681c6fd29d9ba76cc85ee63afce3bbff3fb8d1517964c269b959d090746cd2ce7d78f50b34e9cd317d46
7
+ data.tar.gz: 52bda2e31c8780828b486f6b22aebcf740d26a17a6f019e6daf9877fab93365e4115659e088cfc47429f3898528c935501ba4c24ad0ce9cc08b3edf1f4678067
data/.gitignore ADDED
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.rubocop.yml
3
+ /notes.txt
4
+ /.yardoc
5
+ /Gemfile.lock
6
+ /_yardoc/
7
+ /coverage/
8
+ /doc/
9
+ /pkg/
10
+ /spec/reports/
11
+ /tmp/
12
+ /*.gem
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
4
+ before_install: gem install bundler -v 1.10.4
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Vincent J. DeVendra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # AcornCache
2
+
3
+ AcornCache is a Ruby HTTP proxy caching library that is lightweight, configurable and can be easily integrated with any Rack-based web application. AcornCache allows you to improve page load times and lighten the load on your server by allowing you to implement an in-memory cache shared by every client requesting a resource on your server.
4
+
5
+ Features currently available include the following:
6
+
7
+ * Honors origin server cache control directives according to RFC2616 standards unless directed otherwise.
8
+ * Allows for easily configuring:
9
+ * which resources should be cached,
10
+ * for how long, and
11
+ * whether query params should be ignored
12
+ * Allows for basic browser caching behavior modification by changing out cache control header directives.
13
+ * Uses Redis or Memcached to store cached server responses.
14
+ * Adds a custom header to mark responses returned from the cache (`X-Acorn-Cache: HIT`)
15
+
16
+ ##Getting Started
17
+
18
+ ####Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ ```ruby
23
+ gem 'acorn_cache'
24
+ ```
25
+
26
+ And then execute:
27
+
28
+ $ bundle
29
+
30
+ Or install it yourself:
31
+
32
+ $ gem install acorn_cache
33
+
34
+
35
+ AcornCache must be included in the middleware pipeline of your Rails or Rack application.
36
+
37
+ With Rails, add the following config option to the appropriate environment, probably ```config/environments/production.rb```. Note that we recommend AcornCache be positioned at the top of your middleware stack. Replace `Rack::Sendfile` in the example if necessary.
38
+
39
+ ```ruby
40
+ config.middleware.insert_before(Rack::Sendfile, Rack::AcornCache)
41
+ ```
42
+
43
+ You should now see ```Rack::AcornCache``` listed at the top of your middleware pipeline when you run `rake middleware`.
44
+
45
+ For non-Rails Rack apps, just include the following in your rackup (.ru) file:
46
+ ```ruby
47
+ require 'acorn_cache'
48
+
49
+ use Rack::AcornCache
50
+ ```
51
+
52
+ ####Setting Up Storage
53
+ By default, AcornCache uses Redis to store server responses. You must include
54
+ your Redis host, port, and password (if you have one set) as environment variables.
55
+
56
+ ```
57
+ ACORNCACHE_REDIS_HOST="your_host_name"
58
+ ACORNCACHE_REDIS_PORT="your_port_number"
59
+ ACORNCACHE_REDIS_PASSWORD="your_password"
60
+ ```
61
+ You may also choose to use memcached. If so, set the URL (including host and
62
+ port) and, if you have SASL authentication, username and password.
63
+
64
+ ```
65
+ ACORNCACHE_MEMCACHED_URL="your_url"
66
+ ACORNCACHE_MEMCACHED_USERNAME="your_username"
67
+ ACORNCACHE_MEMCACHED_PASSWORD="your_password"
68
+ ```
69
+ To switch to Memcached, add the following line to your AcornCache config:
70
+ ```ruby
71
+ config.storage = :memcached
72
+ ```
73
+
74
+ ####Configuration
75
+ AcornCache has a range of configuration options. If you're using Rails, set them in an initializer: `config/initializers/acorn_cache.rb`
76
+
77
+ Without configuration, AcornCache won't cache anything. Two basic configuration
78
+ patterns are possible. The most common will be to specify page rules telling
79
+ AcornCache how long to store a resource.
80
+
81
+ The config below specifies two URLs to cache and specifies the time to live, i.e., the time the resource at that location should live in AcornCache and the browser cache. With this config, AcornCache will only cache the resources at these two URLs:
82
+
83
+
84
+
85
+ ```ruby
86
+ if Rails.env.production?
87
+ Rack::AcornCache.configure do |config|
88
+ config.page_rules = {
89
+ "http://example.com/" => { browser_cache_ttl: 30 },
90
+ "http://foo.com/bar" => { acorn_cache_ttl: 100 },
91
+ }
92
+ end
93
+ end
94
+ ```
95
+
96
+
97
+ If you choose to do so, you can have AcornCache act as an RFC compliant
98
+ shared proxy-cache for every resource on your server. For information concerning standard RFC caching rules,
99
+ please refer to the Further Information section below. To operate in this mode, just set:
100
+
101
+ ```ruby
102
+ config.cache_everything = true
103
+ ```
104
+ Keep in mind that you can override standard caching behavior even when in cache everything mode by specifying a page rule.
105
+
106
+ See below for all available options.
107
+
108
+ ## Page Rules
109
+ Configuration options can be set for individual URLs via the
110
+ `page-rules` config option. The value of `page-rules` must be set to a hash. The hash must have a key that is either 1) a URL string, or 2) a pattern that matches the URL of the page(s) for which you are setting the rule, and a value that specifies the caching rule(s) for the page or pages. Here's an example:
111
+
112
+ ```ruby
113
+ Rack::AcornCache.configure do |config|
114
+ config.page_rules = {
115
+ { "http://foo.com" => { acorn_cache_ttl: 3600,
116
+ browser_cache_ttl: 800,
117
+ "http://bar.com/*" => { browser_cache_ttl: 3600,
118
+ ignore_query_params: true },
119
+ /^https+:\/\/.+\.com/ => { respect_default_header: true,
120
+ ignore_query_params: true }
121
+ }
122
+ end
123
+ ```
124
+ ####Deciding Which Resources Are Cached
125
+ Resources best suited for caching are public (not behind authentication) and don't change very often.
126
+ AcornCache provides you with three options for defining the URLs for the resources that you want to cache:
127
+
128
+ 1. You can define a single URL explicitly:
129
+ ```ruby
130
+ "http://www.foobar.com/baz" => { acorn_cache_ttl: 100 }
131
+ ```
132
+
133
+ 2. You can use wildcards to identify multiple pages for a which a given set of rules applies:
134
+ ```ruby
135
+ "http://foo*.com" => { browser_cache_ttl: 86400 }
136
+ ```
137
+
138
+ 3. You can use regex pattern matching simply by using a `Regexp` object as the
139
+ key:
140
+ ```ruby
141
+ /^https+:\/\/.+\.com/ => { acorn_cache_ttl: 100 }
142
+ ```
143
+
144
+
145
+ ####Deciding How Resources Are Cached
146
+ #####Override Existing Cache Control Headers
147
+ Suppose you don't know or want to change the cache control headers provided by your server. AcornCache gives you the ability to control how a resource is
148
+ cached by both AcornCache and the browser cache simply by specifying the
149
+ appropriate page rule settings.
150
+
151
+ AcornCache provides four options, which can be set either as defaults or within
152
+ individual page rules.
153
+
154
+ 1. `acorn_cache_ttl` -
155
+ This option specifies the time a resource should live in AcornCache before
156
+ expiring. It works by overriding the `s-maxage` directive in the cache control
157
+ header with the specified value. Time should be given in seconds. It also removes any directives that would
158
+ prevent caching in a shared proxy cache, like `private` or `no-store`.
159
+
160
+ 2. `browser_cache_ttl` -
161
+ This option specifies the time in seconds a resource should live in private
162
+ browser caches before expiring. It works by overriding the `max-age` directive
163
+ in the cache control header with the specified value. It also removes any
164
+ directives that would prevent caching in a private cache, like `no-store`.
165
+
166
+ 3. `ignore_query_params` -
167
+ If the query params in a request shouldn't affect the response from your server,
168
+ you can set this option to `true` so that all requests for a URL, regardless of
169
+ the specified params, share the same cache entry. This means that if a resource
170
+ living at `http://foo.com` is cached with AcornCache, a request to
171
+ `http://foo.com/?bar=baz` will respond with that cached resource without creating another
172
+ cache entry.
173
+
174
+ 4. `must_revalidate` -
175
+ When set to `true`, the content of the cache will be checked against the origin server using `ETag` or `Last-Modified` headers. With this configuration, AcornCache will not use a cache entry without first revalidating it with the origin server.
176
+
177
+ These four options can be set either as defaults or for individual page rules.
178
+ Default settings apply to any page that AcornCache is allowed to cache unless
179
+ they are overwritten by a page rule. For example, if your
180
+ config looks like this...
181
+
182
+ ```ruby
183
+ RackAcornCache.configure do |config|
184
+ config.default_acorn_cache_ttl = 30
185
+ config.page_rules = {
186
+ "http://foo.com" => { use_defaults: true }
187
+ "http://bar.com" => { acorn_cache_ttl: 100 }
188
+ end
189
+ ```
190
+
191
+ ...then the server response returned by a request to `foo.com` will be cached in AcornCache for 30 seconds, but the server response returned by a request to `bar.com` will be cached for 100 seconds.
192
+
193
+ #####Respect Existing Cache Control Headers
194
+ AcornCache provides you with the ability to respect the cache control headers that were provided from the client or origin server. This can be achieved by setting `respect_existing_headers: true` for a page or given set of pages. This option is useful when you don't want to cache everything but you also want to control caching behavior by ensuring that responses come from your server with the proper cache control headers. If you choose this option, you will likely want to ensure that your response has an `s-maxage` directive, as AcornCache operates as a shared cache.
195
+
196
+ ## Further Information
197
+
198
+ AcornCache's rules and caching guidelines strictly follow RFC 2616 standards. [This flow chart](http://i.imgur.com/o63TJAa.jpg) details the logic and rules that AcornCache is built upon and defines its default behavior.
199
+
200
+ ## Contributing
201
+
202
+ Bug reports and pull requests are welcome on GitHub at https://github.com/acorncache/acorn-cache.
203
+
204
+
205
+ ## License
206
+
207
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "lib"
6
+ t.test_files = FileList['test/*_test.rb']
7
+ t.verbose
8
+ end
9
+
10
+ task :default => [:test]
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'acorn_cache/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "acorn_cache"
8
+ spec.version = AcornCache::VERSION
9
+ spec.authors = ["Vincent J. DeVendra", "Perry Carbone"]
10
+ spec.email = ["VinceDeVendra@gmail.com", "perrycarb@gmail.com"]
11
+
12
+ spec.description = "AcornCache is a Ruby HTTP proxy caching library that is lightweight, configurable and can be easily integrated with any Rack-based web application. AcornCache allows you to improve page load times and lighten the load on your server by allowing you to implement an in-memory cache shared by every client requesting a resource on your server."
13
+ spec.summary = "A HTTP proxy caching library for Rack apps"
14
+ spec.homepage = "https://github.com/acorncache/acorn-cache"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "minitest"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "mocha"
27
+ spec.add_runtime_dependency "rack", "~> 1.6"
28
+ spec.add_runtime_dependency "redis"
29
+ spec.add_runtime_dependency "dalli"
30
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "acorn_cache"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,9 @@
1
+ class Rack::AcornCache
2
+ class AppException < StandardError
3
+ attr_reader :caught_exception
4
+
5
+ def initialize(caught_exception)
6
+ @caught_exception = caught_exception
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,48 @@
1
+ require 'rack'
2
+
3
+ class Rack::AcornCache
4
+ class CacheControlHeader
5
+ attr_accessor :max_age, :s_maxage, :no_cache, :no_store,
6
+ :must_revalidate, :private, :max_fresh, :max_stale
7
+
8
+ def initialize(header_string = "")
9
+ return unless header_string && !header_string.empty?
10
+ set_directive_instance_variables!(header_string)
11
+ end
12
+
13
+ alias_method :max_stale?, :max_stale
14
+ alias_method :no_cache?, :no_cache
15
+ alias_method :private?, :private
16
+ alias_method :no_store?, :no_store
17
+ alias_method :must_revalidate?, :must_revalidate
18
+
19
+ def to_s
20
+ instance_variables.map do |ivar|
21
+ directive = ivar.to_s.sub("@", "").sub("_", "-")
22
+ value = instance_variable_get(ivar)
23
+ next directive if value == true
24
+ next unless value
25
+ "#{directive}=#{value}"
26
+ end.compact.sort.join(", ")
27
+ end
28
+
29
+ private
30
+
31
+ def set_directive_instance_variables!(header_string)
32
+ header_string.gsub(/\s+/, "").split(",").each do |directive, result|
33
+ k, v = directive.split("=")
34
+ instance_variable_set(variable_symbol(k), directive_value(v))
35
+ end
36
+ end
37
+
38
+ def variable_symbol(directive)
39
+ "@#{directive.gsub("-", "_")}".to_sym
40
+ end
41
+
42
+ def directive_value(value)
43
+ return value.to_i if value =~ /^[0-9]+$/
44
+ return true if value.nil?
45
+ value
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,52 @@
1
+ class Rack::AcornCache
2
+ class CacheController
3
+ def initialize(request, app)
4
+ @request = request
5
+ @app = app
6
+ end
7
+
8
+ def response
9
+ if request.no_cache?
10
+ server_response = get_response_from_server
11
+ else
12
+ cached_response = check_for_cached_response
13
+
14
+ if cached_response.must_be_revalidated?
15
+ request.update_conditional_headers!(cached_response)
16
+ server_response = get_response_from_server
17
+ elsif !cached_response.fresh_for_request?(request)
18
+ server_response = get_response_from_server
19
+ end
20
+ end
21
+
22
+ CacheMaintenance
23
+ .new(request.cache_key, server_response, cached_response)
24
+ .update_cache
25
+ .response
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :request, :app
31
+
32
+ def get_response_from_server
33
+ begin
34
+ status, headers, body = @app.call(request.env)
35
+ rescue => e
36
+ raise AppException.new(e)
37
+ end
38
+
39
+ server_response = ServerResponse.new(status, headers, body)
40
+
41
+ if request.page_rule?
42
+ server_response.update_with_page_rules!(request.page_rule)
43
+ else
44
+ server_response
45
+ end
46
+ end
47
+
48
+ def check_for_cached_response
49
+ CacheReader.read(request.cache_key) || NullCachedResponse.new
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,27 @@
1
+ class Rack::AcornCache
2
+ class CacheMaintenance
3
+ attr_reader :response, :cache_key, :server_response, :cached_response
4
+
5
+ def initialize(cache_key, server_response, cached_response)
6
+ @cache_key = cache_key
7
+ @server_response = server_response
8
+ @cached_response = cached_response
9
+ end
10
+
11
+ def update_cache
12
+ if !server_response
13
+ @response = cached_response.add_acorn_cache_header!
14
+ elsif !server_response.cacheable? && !server_response.status_304?
15
+ @response = server_response
16
+ elsif server_response.cacheable?
17
+ @response = server_response.cache!(cache_key)
18
+ elsif cached_response.matches?(server_response)
19
+ @response = cached_response.update_date_and_recache!(cache_key)
20
+ else
21
+ @response = server_response
22
+ end
23
+
24
+ self
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,18 @@
1
+ require 'json'
2
+
3
+ class Rack::AcornCache
4
+ module CacheReader
5
+ def self.read(cache_key)
6
+ response = storage.get(cache_key)
7
+ return false unless response
8
+ response_hash = JSON.parse(response)
9
+ CachedResponse.new(response_hash)
10
+ end
11
+
12
+ private
13
+
14
+ def self.storage
15
+ Rack::AcornCache.configuration.storage
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,13 @@
1
+ class Rack::AcornCache
2
+ module CacheWriter
3
+ def self.write(cache_key, serialized_response)
4
+ storage.set(cache_key, serialized_response)
5
+ end
6
+
7
+ private
8
+
9
+ def self.storage
10
+ Rack::AcornCache.configuration.storage
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,134 @@
1
+ require 'forwardable'
2
+ require 'time'
3
+
4
+ class Rack::AcornCache
5
+ class CachedResponse
6
+ extend Forwardable
7
+ def_delegators :@cache_control_header, :s_maxage, :max_age, :no_cache?, :must_revalidate?
8
+
9
+ attr_reader :body, :status, :headers, :date
10
+ DEFAULT_MAX_AGE = 3600
11
+
12
+ def initialize(args={})
13
+ @body = args["body"]
14
+ @status = args["status"]
15
+ @headers = args["headers"]
16
+ @cache_control_header = CacheControlHeader.new(headers["Cache-Control"])
17
+ end
18
+
19
+ def must_be_revalidated?
20
+ no_cache? || must_revalidate?
21
+ end
22
+
23
+ def update_date!
24
+ headers["Date"] = Time.now.httpdate
25
+ end
26
+
27
+ def serialize
28
+ { headers: headers, status: status, body: body }.to_json
29
+ end
30
+
31
+ def to_a
32
+ [status, headers, [body]]
33
+ end
34
+
35
+ def etag_header
36
+ headers["ETag"]
37
+ end
38
+
39
+ def last_modified_header
40
+ headers["Last-Modified"]
41
+ end
42
+
43
+ def update_date_and_recache!(cache_key)
44
+ cached_response.update_date!
45
+ CacheWriter.write(cache_key, cached_response.serialize)
46
+ self
47
+ end
48
+
49
+ def add_acorn_cache_header!
50
+ unless headers["X-Acorn-Cache"]
51
+ headers["X-Acorn-Cache"] = "HIT"
52
+ end
53
+ self
54
+ end
55
+
56
+ def matches?(server_response)
57
+ if etag_header
58
+ server_response.etag_header == etag_header
59
+ elsif last_modified_header
60
+ server_response.last_modified_header == last_modified_header
61
+ else
62
+ false
63
+ end
64
+ end
65
+
66
+ def time_to_live
67
+ s_maxage || max_age || (expiration_header_time - date)
68
+ end
69
+
70
+ alias_method :stale_time_specified?, :time_to_live
71
+
72
+ def fresh?
73
+ expiration_date > Time.now
74
+ end
75
+
76
+ def date
77
+ @date ||= Time.httpdate(date_header)
78
+ end
79
+
80
+ def expiration_date
81
+ if s_maxage
82
+ date + s_maxage
83
+ elsif max_age
84
+ date + max_age
85
+ elsif expiration_header
86
+ expiration
87
+ else
88
+ date + DEFAULT_MAX_AGE
89
+ end
90
+ end
91
+
92
+ def time_until_expiration
93
+ Time.now - expiration
94
+ end
95
+
96
+ def fresh_for_request?(request)
97
+ FreshnessRules.cached_response_fresh_for_request?(self, request)
98
+ end
99
+
100
+ private
101
+
102
+ def expiration_header_time
103
+ Time.httpdate(expiration_header)
104
+ end
105
+
106
+ def expiration_header
107
+ @expiration_header ||= headers["Expiration"]
108
+ end
109
+
110
+ def expiration
111
+ @expiration ||= Time.httpdate(expiration_header)
112
+ end
113
+
114
+ def date_header
115
+ headers["Date"]
116
+ end
117
+ end
118
+
119
+ class NullCachedResponse
120
+ def must_be_revalidated?
121
+ false
122
+ end
123
+
124
+ def matches?(server_response)
125
+ false
126
+ end
127
+
128
+ def update!; end
129
+
130
+ def fresh_for_request?(request)
131
+ false
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,85 @@
1
+ class Rack::AcornCache
2
+ class << self
3
+ attr_accessor :configuration
4
+ end
5
+
6
+ def self.configure
7
+ self.configuration ||= Configuration.new
8
+ yield(configuration)
9
+ end
10
+
11
+ class Configuration
12
+ attr_writer :storage
13
+ attr_reader :page_rules
14
+ attr_accessor :default_acorn_cache_ttl, :default_browser_cache_ttl,
15
+ :cache_everything, :default_ignore_query_params, :default_must_revalidate
16
+
17
+ def initialize
18
+ @cache_everything = false
19
+ @storage = :redis
20
+ end
21
+
22
+ def page_rules=(user_page_rules)
23
+ @page_rules = user_page_rules.each_with_object({}) do |(k, v), result|
24
+ result[k] = build_page_rule(v)
25
+ end
26
+ end
27
+
28
+ def page_rule_for_url(url)
29
+ if cache_everything
30
+ return default_page_rule unless page_rules
31
+ no_page_rule_found = proc { return default_page_rule }
32
+ else
33
+ return nil unless page_rules
34
+ no_page_rule_found = proc { return nil }
35
+ end
36
+
37
+ page_rules.find(no_page_rule_found) do |k, _|
38
+ page_rule_key_matches_url?(k, url)
39
+ end.last
40
+ end
41
+
42
+ def storage
43
+ if @storage == :redis
44
+ Rack::AcornCache::Storage.redis
45
+ elsif @storage == :memcached
46
+ Rack::AcornCache::Storage.memcached
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def default_page_rule
53
+ { acorn_cache_ttl: default_acorn_cache_ttl,
54
+ browser_cache_ttl: default_browser_cache_ttl,
55
+ ignore_query_params: default_ignore_query_params,
56
+ must_revalidate: default_must_revalidate }
57
+ end
58
+
59
+ def build_page_rule(options)
60
+ options[:ignore_query_params] = default_ignore_query_params
61
+
62
+ return options if options[:respect_existing_headers] || options[:must_revalidate]
63
+ { acorn_cache_ttl: default_acorn_cache_ttl,
64
+ browser_cache_ttl: default_browser_cache_ttl }.merge(options)
65
+ end
66
+
67
+ def page_rule_key_matches_url?(page_rule_key, url)
68
+ return url =~ page_rule_key if page_rule_key.is_a?(Regexp)
69
+ string = page_rule_key.gsub("*", ".*")
70
+ url =~ /^#{string}$/
71
+ end
72
+ end
73
+
74
+ #Example config setup:
75
+ # Rack::AcornCache.configure do |config|
76
+ # config.cache_everything = true
77
+ # config.default_acorn_cache_ttl = 3600
78
+ # config.page_rules = {
79
+ # "http://example.com/*.js" => { browser_cache_ttl: 30,
80
+ # regex: true },
81
+ # "another_url" => { acorn_cache_ttl: 100 },
82
+ # "foo.com" => { respect_existing_headers: true }
83
+ # }
84
+ # end
85
+ end
@@ -0,0 +1,19 @@
1
+ class Rack::AcornCache
2
+ module FreshnessRules
3
+ def self.cached_response_fresh_for_request?(cached_response, request)
4
+ return false unless cached_response
5
+ if cached_response.fresh?
6
+ if request.max_age_more_restrictive?(cached_response)
7
+ return cached_response.date + request.max_age >= Time.now
8
+ elsif request.max_fresh
9
+ return cached_response.expiration_date - request.max_fresh >= Time.now
10
+ end
11
+ true
12
+ else
13
+ return false unless request.max_stale?
14
+ return true if request.max_stale == true
15
+ cached_response.expiration_date + request.max_stale >= Time.now
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
1
+ require 'forwardable'
2
+
3
+ class Rack::AcornCache
4
+ class Request < Rack::Request
5
+ extend Forwardable
6
+ def_delegators :@cache_control_header, :no_cache?, :max_age, :max_fresh,
7
+ :max_stale, :max_stale?
8
+
9
+ def initialize(env)
10
+ super
11
+ @cache_control_header = CacheControlHeader.new(@env["HTTP_CACHE_CONTROL"])
12
+ end
13
+
14
+ def update_conditional_headers!(cached_response)
15
+ if cached_response.etag_header
16
+ self.if_none_match = cached_response.etag_header
17
+ end
18
+
19
+ if cached_response.last_modified_header
20
+ self.if_modified_since = cached_response.last_modified_header
21
+ end
22
+ end
23
+
24
+ def max_age_more_restrictive?(cached_response)
25
+ cached_response.stale_time_specified? &&
26
+ max_age && max_age < cached_response.time_to_live
27
+ end
28
+
29
+ def cacheable?
30
+ get? && (config.cache_everything || page_rule?)
31
+ end
32
+
33
+ def page_rule
34
+ config.page_rule_for_url(url) if config
35
+ end
36
+
37
+ def cache_key
38
+ return base_url + path if page_rule? && page_rule[:ignore_query_params]
39
+ url
40
+ end
41
+
42
+ alias_method :page_rule?, :page_rule
43
+
44
+ private
45
+
46
+ def config
47
+ Rack::AcornCache.configuration
48
+ end
49
+
50
+ def if_none_match=(etag)
51
+ env["HTTP_IF_NONE_MATCH"] = etag
52
+ end
53
+
54
+ def if_modified_since=(last_modified)
55
+ env["HTTP_IF_MODIFIED_SINCE"] = last_modified
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,83 @@
1
+ require 'json'
2
+ require 'time'
3
+
4
+ class Rack::AcornCache
5
+ class ServerResponse < Rack::Response
6
+ CACHEABLE_STATUS_CODES = [200, 203, 300, 301, 302, 404, 410]
7
+ attr_reader :status, :headers, :body, :cache_control_header
8
+
9
+ def initialize(status, headers, body)
10
+ @status = status
11
+ @headers = headers
12
+ @body = body
13
+ @cache_control_header = CacheControlHeader.new(headers["Cache-Control"])
14
+ end
15
+
16
+ def update_date!
17
+ @headers["Date"] = Time.now.httpdate unless @headers["Date"]
18
+ end
19
+
20
+ def cacheable?
21
+ [:private?, :no_store?].none? { |directive| send(directive) } &&
22
+ CACHEABLE_STATUS_CODES.include?(status)
23
+ end
24
+
25
+ def status_304?
26
+ status == 304
27
+ end
28
+
29
+ def serialize
30
+ { status: status, headers: headers, body: body_string }.to_json
31
+ end
32
+
33
+ def body_string
34
+ result = ""
35
+ body.each { |part| result << part }
36
+ result
37
+ end
38
+
39
+ def to_a
40
+ [status, headers, body]
41
+ end
42
+
43
+ def cache!(cache_key)
44
+ update_date!
45
+ CacheWriter.write(cache_key, serialize)
46
+ self
47
+ end
48
+
49
+ def update_with_page_rules!(page_rule)
50
+ if page_rule[:must_revalidate]
51
+ self.no_cache = true
52
+ self.must_revalidate = true
53
+ self.private = nil
54
+ self.max_age = nil
55
+ self.s_maxage = nil
56
+ self.no_store = nil
57
+ end
58
+
59
+ if page_rule[:acorn_cache_ttl] || page_rule[:browser_cache_ttl]
60
+ self.no_cache = nil
61
+ self.no_store = nil
62
+ self.must_revalidate = nil
63
+ end
64
+
65
+ if page_rule[:acorn_cache_ttl]
66
+ self.max_age = nil
67
+ self.s_maxage = page_rule[:acorn_cache_ttl]
68
+ self.private = nil
69
+ end
70
+
71
+ if page_rule[:browser_cache_ttl]
72
+ self.max_age = page_rule[:browser_cache_ttl]
73
+ end
74
+
75
+ headers["Cache-Control"] = cache_control_header.to_s
76
+ self
77
+ end
78
+
79
+ def method_missing(method, *args)
80
+ cache_control_header.send(method, *args)
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,27 @@
1
+ require 'redis'
2
+ require 'dalli'
3
+
4
+ class Rack::AcornCache
5
+ module Storage
6
+ def self.redis
7
+ args = { host: ENV["ACORNCACHE_REDIS_HOST"],
8
+ port: ENV["ACORNCACHE_REDIS_PORT"].to_i }
9
+ if ENV["ACORNCACHE_REDIS_PASSWORD"]
10
+ args.merge!(password: ENV["ACORNCACHE_REDIS_PASSWORD"])
11
+ end
12
+
13
+ @redis ||= Redis.new(args)
14
+ end
15
+
16
+ def self.memcached
17
+ options = {}
18
+
19
+ if ENV["ACORNCACHE_MEMCACHED_USERNAME"]
20
+ options = { username: ENV["ACORNCACHE_MEMCACHED_USERNAME"],
21
+ password: ENV["ACORNCACHE_MEMCACHED_PASSWORD"] }
22
+ end
23
+
24
+ @memcached ||= Dalli::Client.new(ENV["ACORNCACHE_MEMCACHED_URL"], options)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ module AcornCache
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,37 @@
1
+ require 'rack'
2
+
3
+ class Rack::AcornCache
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ dup._call(env)
10
+ end
11
+
12
+ def _call(env)
13
+ request = Request.new(env)
14
+ return @app.call(env) unless request.cacheable?
15
+
16
+ begin
17
+ CacheController.new(request, @app).response.to_a
18
+ rescue AppException => e
19
+ raise e.caught_exception
20
+ rescue => e
21
+ @app.call(env)
22
+ end
23
+ end
24
+ end
25
+
26
+ require 'acorn_cache/request'
27
+ require 'acorn_cache/cache_controller'
28
+ require 'acorn_cache/app_exception'
29
+ require 'acorn_cache/config'
30
+ require 'acorn_cache/cache_control_header'
31
+ require 'acorn_cache/cache_writer'
32
+ require 'acorn_cache/freshness_rules'
33
+ require 'acorn_cache/storage'
34
+ require 'acorn_cache/cache_maintenance'
35
+ require 'acorn_cache/server_response'
36
+ require 'acorn_cache/cached_response'
37
+ require 'acorn_cache/cache_reader'
metadata ADDED
@@ -0,0 +1,186 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: acorn_cache
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Vincent J. DeVendra
8
+ - Perry Carbone
9
+ autorequire:
10
+ bindir: exe
11
+ cert_chain: []
12
+ date: 2016-03-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '1.10'
21
+ type: :development
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '1.10'
28
+ - !ruby/object:Gem::Dependency
29
+ name: minitest
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: rake
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '10.0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '10.0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: pry
58
+ requirement: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ - !ruby/object:Gem::Dependency
71
+ name: mocha
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ - !ruby/object:Gem::Dependency
85
+ name: rack
86
+ requirement: !ruby/object:Gem::Requirement
87
+ requirements:
88
+ - - "~>"
89
+ - !ruby/object:Gem::Version
90
+ version: '1.6'
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '1.6'
98
+ - !ruby/object:Gem::Dependency
99
+ name: redis
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ - !ruby/object:Gem::Dependency
113
+ name: dalli
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ type: :runtime
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ description: AcornCache is a Ruby HTTP proxy caching library that is lightweight,
127
+ configurable and can be easily integrated with any Rack-based web application. AcornCache
128
+ allows you to improve page load times and lighten the load on your server by allowing
129
+ you to implement an in-memory cache shared by every client requesting a resource
130
+ on your server.
131
+ email:
132
+ - VinceDeVendra@gmail.com
133
+ - perrycarb@gmail.com
134
+ executables: []
135
+ extensions: []
136
+ extra_rdoc_files: []
137
+ files:
138
+ - ".gitignore"
139
+ - ".travis.yml"
140
+ - Gemfile
141
+ - LICENSE.txt
142
+ - README.md
143
+ - Rakefile
144
+ - acorn_cache.gemspec
145
+ - bin/console
146
+ - bin/setup
147
+ - lib/acorn_cache.rb
148
+ - lib/acorn_cache/app_exception.rb
149
+ - lib/acorn_cache/cache_control_header.rb
150
+ - lib/acorn_cache/cache_controller.rb
151
+ - lib/acorn_cache/cache_maintenance.rb
152
+ - lib/acorn_cache/cache_reader.rb
153
+ - lib/acorn_cache/cache_writer.rb
154
+ - lib/acorn_cache/cached_response.rb
155
+ - lib/acorn_cache/config.rb
156
+ - lib/acorn_cache/freshness_rules.rb
157
+ - lib/acorn_cache/request.rb
158
+ - lib/acorn_cache/server_response.rb
159
+ - lib/acorn_cache/storage.rb
160
+ - lib/acorn_cache/version.rb
161
+ homepage: https://github.com/acorncache/acorn-cache
162
+ licenses:
163
+ - MIT
164
+ metadata: {}
165
+ post_install_message:
166
+ rdoc_options: []
167
+ require_paths:
168
+ - lib
169
+ required_ruby_version: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ required_rubygems_version: !ruby/object:Gem::Requirement
175
+ requirements:
176
+ - - ">="
177
+ - !ruby/object:Gem::Version
178
+ version: '0'
179
+ requirements: []
180
+ rubyforge_project:
181
+ rubygems_version: 2.4.7
182
+ signing_key:
183
+ specification_version: 4
184
+ summary: A HTTP proxy caching library for Rack apps
185
+ test_files: []
186
+ has_rdoc: