benschwarz-merb-cache 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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