benschwarz-merb-cache 1.0.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.
Files changed (37) hide show
  1. data/LICENSE +20 -0
  2. data/README +224 -0
  3. data/Rakefile +17 -0
  4. data/lib/merb-cache.rb +15 -0
  5. data/lib/merb-cache/cache.rb +91 -0
  6. data/lib/merb-cache/cache_request.rb +48 -0
  7. data/lib/merb-cache/core_ext/enumerable.rb +9 -0
  8. data/lib/merb-cache/core_ext/hash.rb +21 -0
  9. data/lib/merb-cache/merb_ext/controller/class_methods.rb +244 -0
  10. data/lib/merb-cache/merb_ext/controller/instance_methods.rb +163 -0
  11. data/lib/merb-cache/stores/fundamental/abstract_store.rb +101 -0
  12. data/lib/merb-cache/stores/fundamental/file_store.rb +113 -0
  13. data/lib/merb-cache/stores/fundamental/memcached_store.rb +110 -0
  14. data/lib/merb-cache/stores/strategy/abstract_strategy_store.rb +119 -0
  15. data/lib/merb-cache/stores/strategy/action_store.rb +61 -0
  16. data/lib/merb-cache/stores/strategy/adhoc_store.rb +69 -0
  17. data/lib/merb-cache/stores/strategy/gzip_store.rb +63 -0
  18. data/lib/merb-cache/stores/strategy/mintcache_store.rb +75 -0
  19. data/lib/merb-cache/stores/strategy/page_store.rb +68 -0
  20. data/lib/merb-cache/stores/strategy/sha1_store.rb +62 -0
  21. data/spec/merb-cache/cache_request_spec.rb +78 -0
  22. data/spec/merb-cache/cache_spec.rb +88 -0
  23. data/spec/merb-cache/core_ext/enumerable_spec.rb +26 -0
  24. data/spec/merb-cache/core_ext/hash_spec.rb +51 -0
  25. data/spec/merb-cache/merb_ext/controller_spec.rb +5 -0
  26. data/spec/merb-cache/stores/fundamental/abstract_store_spec.rb +118 -0
  27. data/spec/merb-cache/stores/fundamental/file_store_spec.rb +205 -0
  28. data/spec/merb-cache/stores/fundamental/memcached_store_spec.rb +258 -0
  29. data/spec/merb-cache/stores/strategy/abstract_strategy_store_spec.rb +78 -0
  30. data/spec/merb-cache/stores/strategy/action_store_spec.rb +208 -0
  31. data/spec/merb-cache/stores/strategy/adhoc_store_spec.rb +227 -0
  32. data/spec/merb-cache/stores/strategy/gzip_store_spec.rb +68 -0
  33. data/spec/merb-cache/stores/strategy/mintcache_store_spec.rb +59 -0
  34. data/spec/merb-cache/stores/strategy/page_store_spec.rb +146 -0
  35. data/spec/merb-cache/stores/strategy/sha1_store_spec.rb +84 -0
  36. data/spec/spec_helper.rb +95 -0
  37. metadata +112 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Ben Burkert, Ben Schwarz, Daniel Neighman
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,224 @@
1
+ merb-cache
2
+ ==========
3
+
4
+ A plugin for the Merb framework that provides caching stores,
5
+ strategies and helpers.
6
+
7
+
8
+
9
+ Tutorial
10
+ ==========
11
+
12
+ Stores usually set up in application init file
13
+ (init.rb) or environment specific init file (so you can
14
+ use different stores for production, staging and development
15
+ environment if you need to).
16
+
17
+ # create a fundamental memcache store named :memcached for localhost
18
+
19
+ Merb::Cache.setup do
20
+ register(:memcached, MemcachedStore, :namespace => "my_app", :servers => ["127.0.0.1:11211"])
21
+ end
22
+
23
+ # a default FileStore
24
+ Merb::Cache.setup do
25
+ register(FileStore)
26
+ end
27
+
28
+ # another FileStore
29
+ Merb::Cache.setup do
30
+ register(:tmp_cache, FileStore, :dir => "/tmp")
31
+ end
32
+
33
+ Now lets see how we can use stores in the application:
34
+
35
+ class Tag
36
+ def find(parameters = {})
37
+ # poor man's identity map
38
+
39
+ if Merb::Cache[:memcached].exists?("tags", parameters)
40
+ Merb::Cache[:memcached].read("tags", parameters)
41
+ else
42
+ results = super(parameters)
43
+ Merb::Cache[:memcached].write("tags", results, parameters)
44
+
45
+ results
46
+ end
47
+ end
48
+
49
+ def popularity_rating
50
+ # lets keep the popularity rating cached for 30 seconds
51
+ # merb-cache will create a key from the model's id & the interval parameter
52
+
53
+ Merb::Cache[:memcached].fetch(self.id, :interval => Time.now.to_i / 30) do
54
+ self.run_long_popularity_rating_query
55
+ end
56
+ end
57
+ end
58
+
59
+
60
+ Or, if you want to use memcache’s built in expire option:
61
+
62
+ # expire a cache entry for "bar" (identified by the key "foo" and
63
+ # parameters {:baz => :bay}) in two hours
64
+ Merb::Cache[:memcached].write("foo", "bar", {:baz => :bay}, :expire_in => 2.hours)
65
+
66
+ # this will fail, because FileStore cannot expire cache entries
67
+ Merb::Cache[:default].write("foo", "bar", {:baz => :bay}, :expire_in => 2.hours)
68
+
69
+ # writing to the FileStore will fail, but the MemcachedStore will succeed
70
+ Merb::Cache[:default, :memcached].write("foo", "bar", {:baz => :bay}, :expire_in => 2.hours)
71
+
72
+ # this will fail
73
+ Merb::Cache[:default, :memcached].write_all("foo", "bar", {:baz => :bay}, :expire_in => 2.hours)
74
+
75
+
76
+ Setting up strategy stores is very similar to fundamental stores:
77
+
78
+ Merb::Cache.setup do
79
+
80
+ # wraps the :memcached store we setup earlier
81
+ register(:zipped, GzipStore[:memcached])
82
+
83
+ # wrap a strategy store
84
+ register(:sha_and_zip, SHA1Store[:zipped])
85
+
86
+ # you can even use unnamed fundamental stores
87
+ register(:zipped_images, GzipStore[FileStore],
88
+ :dir => Merb.root / "public" / "images")
89
+
90
+ # or a combination or strategy & fundamental stores
91
+ register(:secured, SHA1Store[GzipStore[FileStore], FileStore],
92
+ :dir => Merb.root / "private")
93
+ end
94
+
95
+
96
+ You can use these strategy stores exactly like fundamental stores in your app code.
97
+
98
+ Action & Page Caching
99
+ Action & page caching have been implemented in strategy stores. So instead of manually specifying which type of caching you want for each action, you simply ask merb-cache to cache your action, and it will use the fastest cache available.
100
+
101
+ First, let’s setup our page & action stores:
102
+
103
+ config/environments/development.rb
104
+
105
+ Merb::Cache.setup do
106
+
107
+ # the order that stores are setup is important
108
+ # faster stores should be setup first
109
+
110
+ # page cache to the public dir
111
+ register(:page_store, PageStore[FileStore],
112
+ :dir => Merb.root / "public")
113
+
114
+ # action cache to memcache
115
+ register(:action_store, ActionStore[:sha_and_zip])
116
+
117
+ # sets up the ordering of stores when attempting to read/write cache entries
118
+ register(:default, AdhocStore[:page_store, :action_store])
119
+
120
+ end
121
+
122
+ And now in our controller:
123
+ class Tags < Merb::Controller
124
+
125
+ # index & show will be page cached to the public dir. The index
126
+ # action has no parameters, and the show parameter's are part of
127
+ # the URL, making them both page-cache'able
128
+ cache :index, :show
129
+
130
+ def index
131
+ render
132
+ end
133
+
134
+ def show(:slug)
135
+ display Tag.first(:slug => slug)
136
+ end
137
+ end
138
+
139
+ Our controller now page caches but the index & show action. Furthermore,
140
+ the show action is cached separately for each slug parameter automatically.
141
+
142
+
143
+ class Tags < Merb::Controller
144
+
145
+ # the term is a route param, while the page & per_page params are part of the query string.
146
+ # If only the term param is supplied, the request can be page cached, but if the page and/or
147
+ # per_page param is part of the query string, the request will action cache.
148
+ cache :catalog
149
+
150
+ def catalog(term = 'a', page = 1, per_page = 20)
151
+ @tags = Tag.for_term(term).paginate(page, per_page)
152
+
153
+ display @tags
154
+ end
155
+ end
156
+
157
+ Because the specific type of caching is not specified, the same action can either
158
+ be page cached or action cached depending on the context of the request.
159
+
160
+
161
+ Keeping a “Hot” Cache
162
+ =====================
163
+
164
+ Cache expiration is a constant problem for developers. When should content
165
+ be expired? Should we “sweep” stale content? How do we balance serving fresh
166
+ content and maintaining fast response times? These are difficult questions
167
+ for developers, and are usually answered with ugly code added across our
168
+ models, views, and controllers. Instead of designing an elaborate
169
+ caching and expiring system, an alternate approach is to keep a “hot” cache.
170
+
171
+ So what is a “hot” cache? A hot cache is what you get when you ignore
172
+ trying to manually expire content, and instead focus on replacing old
173
+ content with fresh data as soon as it becomes stale. Keeping a hot
174
+ cache means no difficult expiration logic spread out across your
175
+ app, and will all but eliminate cache misses.
176
+
177
+ The problem until now with this approach has been the impact on
178
+ response times. If the request has to wait on any pages that
179
+ it has made stale to render the fresh version, it can slow down
180
+ the response time dramatically. Thankfully, Merb has the run_later
181
+ method which allows the fresh content to render after the
182
+ response has been sent to the browser.
183
+ It’s the best of both worlds. Here’s an example.
184
+
185
+
186
+ class Tags &lt; Merb::Controller
187
+
188
+ cache :index
189
+ eager_cache :create, :index
190
+
191
+ def index
192
+ display Tag.all
193
+ end
194
+
195
+ def create(slug)
196
+ @tag = Tag.new(slug)
197
+
198
+ # redirect them back to the index action
199
+ redirect url(:tags)
200
+ end
201
+ end
202
+
203
+ The controller will eager_cache the index action whenever the create action
204
+ is successfully called. If the client were to post a new tag to the
205
+ create action, they would be redirect back to the index action.
206
+ Right after the response had been sent to the client, the index action
207
+ would be rendered with the newly created tag included and replaced
208
+ in the cache. So when the user requests for the index action gets
209
+ to the server, the freshest version is already in the cache, and
210
+ the cache miss is avoided. This works regardless of the way
211
+ the index action is cached.
212
+
213
+ Hot cache helps fight dog pile effect
214
+ (http://highscalability.com/strategy-break-memcache-dog-pile) but
215
+ should be used with caution. It's great when you want to eagerly cache
216
+ some page that user is not going to see immediately after
217
+ creating/updating something because hot cache in current implementation
218
+ uses worker queue (knows as run_later) and it does not guarantee that
219
+ before redirect hits the action data is gonna be already cached.
220
+
221
+ A good use case of eager caching is front end page of
222
+ some newspaper site when staff updates site content, and
223
+ is not redirected to page that uses new cache values immediately,
224
+ but other users access it frequently.
@@ -0,0 +1,17 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+
4
+ desc "Run all examples (or a specific spec with TASK=xxxx)"
5
+ Spec::Rake::SpecTask.new('spec') do |t|
6
+ t.spec_opts = ["-cfs"]
7
+ t.spec_files = begin
8
+ if ENV["TASK"]
9
+ ENV["TASK"].split(',').map { |task| "spec/**/#{task}_spec.rb" }
10
+ else
11
+ FileList['spec/**/*_spec.rb']
12
+ end
13
+ end
14
+ end
15
+
16
+ desc 'Default: run spec examples'
17
+ task :default => 'spec'
@@ -0,0 +1,15 @@
1
+ # make sure we're running inside Merb
2
+ if defined?(Merb::Plugins)
3
+ require "merb-cache" / "cache"
4
+ require "merb-cache" / "core_ext" / "enumerable"
5
+ require "merb-cache" / "core_ext" / "hash"
6
+ require "merb-cache" / "merb_ext" / "controller" / "class_methods"
7
+ require "merb-cache" / "merb_ext" / "controller" / "instance_methods"
8
+ require "merb-cache" / "cache_request"
9
+
10
+ class Merb::Controller
11
+ extend Merb::Cache::Controller::ClassMethods
12
+ end
13
+
14
+ Merb::Controller.send(:include, Merb::Cache::Controller::InstanceMethods)
15
+ end
@@ -0,0 +1,91 @@
1
+ module Merb
2
+ # A convinient way to get at Merb::Cache
3
+ def self.cache
4
+ Merb::Cache
5
+ end
6
+
7
+ module Cache
8
+
9
+ def self.setup(&blk)
10
+ if Merb::BootLoader.finished?(Merb::BootLoader::BeforeAppLoads)
11
+ instance_eval(&blk) unless blk.nil?
12
+ else
13
+ Merb::BootLoader.before_app_loads do
14
+ instance_eval(&blk) unless blk.nil?
15
+ end
16
+ end
17
+ end
18
+
19
+ # autoload is used so that gem dependencies can be required only when needed by
20
+ # adding the require statement in the store file.
21
+ autoload :AbstractStore, "merb-cache" / "stores" / "fundamental" / "abstract_store"
22
+ autoload :FileStore, "merb-cache" / "stores" / "fundamental" / "file_store"
23
+ autoload :MemcachedStore, "merb-cache" / "stores" / "fundamental" / "memcached_store"
24
+
25
+ autoload :AbstractStrategyStore, "merb-cache" / "stores" / "strategy" / "abstract_strategy_store"
26
+ autoload :ActionStore, "merb-cache" / "stores" / "strategy" / "action_store"
27
+ autoload :AdhocStore, "merb-cache" / "stores" / "strategy" / "adhoc_store"
28
+ autoload :GzipStore, "merb-cache" / "stores" / "strategy" / "gzip_store"
29
+ autoload :PageStore, "merb-cache" / "stores" / "strategy" / "page_store"
30
+ autoload :SHA1Store, "merb-cache" / "stores" / "strategy" / "sha1_store"
31
+ autoload :MintCacheStore, "merb-cache" / "stores" / "strategy" / "mintcache_store"
32
+
33
+ class << self
34
+ attr_accessor :stores
35
+ end
36
+
37
+ self.stores = {}
38
+
39
+ # Cache store lookup
40
+ # name<Symbol> : The name of a registered store
41
+ # Returns<Nil AbstractStore> : A thread-safe copy of the store
42
+ def self.[](*names)
43
+ names = names.first if names.first.is_a? Array
44
+ if names.size == 1
45
+ Thread.current[:'merb-cache'] ||= {}
46
+ (Thread.current[:'merb-cache'][names.first] ||= stores[names.first].clone)
47
+ else
48
+ AdhocStore[*names]
49
+ end
50
+ rescue TypeError
51
+ raise(StoreNotFound, "Could not find the :#{names.first} store")
52
+ end
53
+
54
+ # Clones the cache stores for the current thread
55
+ def self.clone_stores
56
+ @stores.inject({}) {|h, (k, s)| h[k] = s.clone; h}
57
+ end
58
+
59
+ # Registers the cache store name with a type & options
60
+ # name<Symbol> : An optional symbol to give the cache. :default is used if no name is given.
61
+ # klass<Class> : A store type.
62
+ # opts<Hash> : A hash to pass through to the store for configuration.
63
+ def self.register(name, klass = nil, opts = {})
64
+ klass, opts = nil, klass if klass.is_a? Hash
65
+ name, klass = default_store_name, name if klass.nil?
66
+
67
+ raise StoreExists, "#{name} store already setup" if @stores.has_key?(name)
68
+
69
+ @stores[name] = (AdhocStore === klass) ? klass : klass.new(opts)
70
+ end
71
+
72
+ # Checks to see if a given store exists already.
73
+ def self.exists?(name)
74
+ return true if self[name]
75
+ rescue StoreNotFound
76
+ return false
77
+ end
78
+
79
+ # Default store name is :default.
80
+ def self.default_store_name
81
+ :default
82
+ end
83
+
84
+ class NotSupportedError < Exception; end
85
+
86
+ class StoreExists < Exception; end
87
+
88
+ # Raised when requested store cannot be found on the list of registered.
89
+ class StoreNotFound < Exception; end
90
+ end #Cache
91
+ end #Merb
@@ -0,0 +1,48 @@
1
+ module Merb
2
+ module Cache
3
+ class CacheRequest < Merb::Request
4
+
5
+ attr_accessor :params
6
+
7
+ def initialize(uri = "", params = {}, env = {})
8
+ if uri || !env[Merb::Const::REQUEST_URI]
9
+ uri = URI(uri || '/')
10
+ env[Merb::Const::REQUEST_URI] = uri.respond_to?(:request_uri) ? uri.request_uri : uri.to_s
11
+ env[Merb::Const::HTTP_HOST] = uri.host + (uri.port != 80 ? ":#{uri.port}" : '') if uri.host
12
+ env[Merb::Const::SERVER_PORT] = uri.port.to_s if uri.port
13
+ env[Merb::Const::QUERY_STRING] = uri.query.to_s if uri.query
14
+ env[Merb::Const::REQUEST_PATH] = env[Merb::Const::PATH_INFO] = uri.path
15
+ end
16
+
17
+ env[Merb::Const::REQUEST_METHOD] = env[:method] ? env.delete(:method).to_s.upcase : 'GET'
18
+
19
+ super(DEFAULT_ENV.merge(env))
20
+
21
+
22
+ @params = params || {}
23
+ end
24
+
25
+ DEFAULT_ENV = Mash.new({
26
+ 'SERVER_NAME' => 'localhost',
27
+ 'HTTP_ACCEPT_ENCODING' => 'gzip,deflate',
28
+ 'HTTP_USER_AGENT' => 'Ruby/Merb (ver: ' + Merb::VERSION + ') merb-cache',
29
+ 'SCRIPT_NAME' => '/',
30
+ 'SERVER_PROTOCOL' => 'HTTP/1.1',
31
+ 'HTTP_CACHE_CONTROL' => 'max-age=0',
32
+ 'HTTP_ACCEPT_LANGUAGE' => 'en,ja;q=0.9,fr;q=0.9,de;q=0.8,es;q=0.7,it;q=0.7,nl;q=0.6,sv;q=0.5,nb;q=0.5,da;q=0.4,fi;q=0.3,pt;q=0.3,zh-Hans;q=0.2,zh-Hant;q=0.1,ko;q=0.1',
33
+ 'HTTP_HOST' => 'localhost',
34
+ 'REMOTE_ADDR' => '127.0.0.1',
35
+ 'SERVER_SOFTWARE' => 'Mongrel 1.1',
36
+ 'HTTP_KEEP_ALIVE' => '300',
37
+ 'HTTP_REFERER' => 'http://localhost/',
38
+ 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.7',
39
+ 'HTTP_VERSION' => 'HTTP/1.1',
40
+ 'REQUEST_METHOD' => 'GET',
41
+ 'SERVER_PORT' => '80',
42
+ 'GATEWAY_INTERFACE' => 'CGI/1.2',
43
+ 'HTTP_ACCEPT' => 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
44
+ 'HTTP_CONNECTION' => 'keep-alive'
45
+ }) unless defined?(DEFAULT_ENV)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,9 @@
1
+ module Enumerable
2
+ def capture_first
3
+ each do |o|
4
+ return yield(o) || next
5
+ end
6
+
7
+ nil
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ require 'digest'
2
+
3
+ class Hash
4
+
5
+ def to_sha2
6
+ string = ""
7
+ keys.sort_by{|k| k.to_s}.each do |k|
8
+ string << "&#{k}="
9
+ case self[k]
10
+ when Array
11
+ string << self[k].join('|')
12
+ when Hash
13
+ string << self[k].to_sha2
14
+ else
15
+ string << self[k].to_s
16
+ end
17
+ end
18
+ Digest::SHA2.hexdigest(string)
19
+ end
20
+
21
+ end