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.
- data/LICENSE +20 -0
- data/README +224 -0
- data/Rakefile +17 -0
- data/lib/merb-cache.rb +15 -0
- data/lib/merb-cache/cache.rb +91 -0
- data/lib/merb-cache/cache_request.rb +48 -0
- data/lib/merb-cache/core_ext/enumerable.rb +9 -0
- data/lib/merb-cache/core_ext/hash.rb +21 -0
- data/lib/merb-cache/merb_ext/controller/class_methods.rb +244 -0
- data/lib/merb-cache/merb_ext/controller/instance_methods.rb +163 -0
- data/lib/merb-cache/stores/fundamental/abstract_store.rb +101 -0
- data/lib/merb-cache/stores/fundamental/file_store.rb +113 -0
- data/lib/merb-cache/stores/fundamental/memcached_store.rb +110 -0
- data/lib/merb-cache/stores/strategy/abstract_strategy_store.rb +119 -0
- data/lib/merb-cache/stores/strategy/action_store.rb +61 -0
- data/lib/merb-cache/stores/strategy/adhoc_store.rb +69 -0
- data/lib/merb-cache/stores/strategy/gzip_store.rb +63 -0
- data/lib/merb-cache/stores/strategy/mintcache_store.rb +75 -0
- data/lib/merb-cache/stores/strategy/page_store.rb +68 -0
- data/lib/merb-cache/stores/strategy/sha1_store.rb +62 -0
- data/spec/merb-cache/cache_request_spec.rb +78 -0
- data/spec/merb-cache/cache_spec.rb +88 -0
- data/spec/merb-cache/core_ext/enumerable_spec.rb +26 -0
- data/spec/merb-cache/core_ext/hash_spec.rb +51 -0
- data/spec/merb-cache/merb_ext/controller_spec.rb +5 -0
- data/spec/merb-cache/stores/fundamental/abstract_store_spec.rb +118 -0
- data/spec/merb-cache/stores/fundamental/file_store_spec.rb +205 -0
- data/spec/merb-cache/stores/fundamental/memcached_store_spec.rb +258 -0
- data/spec/merb-cache/stores/strategy/abstract_strategy_store_spec.rb +78 -0
- data/spec/merb-cache/stores/strategy/action_store_spec.rb +208 -0
- data/spec/merb-cache/stores/strategy/adhoc_store_spec.rb +227 -0
- data/spec/merb-cache/stores/strategy/gzip_store_spec.rb +68 -0
- data/spec/merb-cache/stores/strategy/mintcache_store_spec.rb +59 -0
- data/spec/merb-cache/stores/strategy/page_store_spec.rb +146 -0
- data/spec/merb-cache/stores/strategy/sha1_store_spec.rb +84 -0
- data/spec/spec_helper.rb +95 -0
- metadata +112 -0
@@ -0,0 +1,113 @@
|
|
1
|
+
module Merb::Cache
|
2
|
+
# Cache store that uses files. Usually this is good for fragment
|
3
|
+
# and page caching but not object caching.
|
4
|
+
#
|
5
|
+
# By default cached files are stored in tmp/cache under Merb.root directory.
|
6
|
+
# To use other location pass :dir option to constructor.
|
7
|
+
#
|
8
|
+
# File caching does not support expiration time.
|
9
|
+
class FileStore < AbstractStore
|
10
|
+
attr_accessor :dir
|
11
|
+
|
12
|
+
# Creates directory for cached files unless it exists.
|
13
|
+
def initialize(config = {})
|
14
|
+
@dir = config[:dir] || Merb.root_path(:tmp / :cache)
|
15
|
+
|
16
|
+
create_path(@dir)
|
17
|
+
end
|
18
|
+
|
19
|
+
# File caching does not support expiration time.
|
20
|
+
def writable?(key, parameters = {}, conditions = {})
|
21
|
+
case key
|
22
|
+
when String, Numeric, Symbol
|
23
|
+
!conditions.has_key?(:expire_in)
|
24
|
+
else
|
25
|
+
false
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Reads cached template from disk if it exists.
|
30
|
+
def read(key, parameters = {})
|
31
|
+
if exists?(key, parameters)
|
32
|
+
read_file(pathify(key, parameters))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Writes cached template to disk, creating cache directory
|
37
|
+
# if it does not exist.
|
38
|
+
def write(key, data = nil, parameters = {}, conditions = {})
|
39
|
+
if writable?(key, parameters, conditions)
|
40
|
+
if File.file?(path = pathify(key, parameters))
|
41
|
+
write_file(path, data)
|
42
|
+
else
|
43
|
+
create_path(path) && write_file(path, data)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Fetches cached data by key if it exists. If it does not,
|
49
|
+
# uses passed block to get new cached value and writes it
|
50
|
+
# using given key.
|
51
|
+
def fetch(key, parameters = {}, conditions = {}, &blk)
|
52
|
+
read(key, parameters) || (writable?(key, parameters, conditions) && write(key, value = blk.call, parameters, conditions) && value)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Checks if cached template with given key exists.
|
56
|
+
def exists?(key, parameters = {})
|
57
|
+
File.file?(pathify(key, parameters))
|
58
|
+
end
|
59
|
+
|
60
|
+
# Deletes cached template by key using FileUtils#rm.
|
61
|
+
def delete(key, parameters = {})
|
62
|
+
if File.file?(path = pathify(key, parameters))
|
63
|
+
FileUtils.rm(path)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def delete_all
|
68
|
+
raise NotSupportedError
|
69
|
+
end
|
70
|
+
|
71
|
+
def pathify(key, parameters = {})
|
72
|
+
if key.to_s =~ /^\//
|
73
|
+
path = "#{@dir}#{key}"
|
74
|
+
else
|
75
|
+
path = "#{@dir}/#{key}"
|
76
|
+
end
|
77
|
+
|
78
|
+
path << "--#{parameters.to_sha2}" unless parameters.empty?
|
79
|
+
path
|
80
|
+
end
|
81
|
+
|
82
|
+
protected
|
83
|
+
|
84
|
+
def create_path(path)
|
85
|
+
FileUtils.mkdir_p(File.dirname(path))
|
86
|
+
end
|
87
|
+
|
88
|
+
# Reads file content. Access to the file
|
89
|
+
# uses file locking.
|
90
|
+
def read_file(path)
|
91
|
+
data = nil
|
92
|
+
File.open(path, "r") do |file|
|
93
|
+
file.flock(File::LOCK_EX)
|
94
|
+
data = file.read
|
95
|
+
file.flock(File::LOCK_UN)
|
96
|
+
end
|
97
|
+
|
98
|
+
data
|
99
|
+
end
|
100
|
+
|
101
|
+
# Writes file content. Access to the file
|
102
|
+
# uses file locking.
|
103
|
+
def write_file(path, data)
|
104
|
+
File.open(path, "w+") do |file|
|
105
|
+
file.flock(File::LOCK_EX)
|
106
|
+
file.write(data)
|
107
|
+
file.flock(File::LOCK_UN)
|
108
|
+
end
|
109
|
+
|
110
|
+
true
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'memcached'
|
2
|
+
|
3
|
+
module Merb::Cache
|
4
|
+
# Memcached store uses one or several Memcached
|
5
|
+
# servers for caching. It's flexible and can be used
|
6
|
+
# for fragment caching, action caching, page caching
|
7
|
+
# or object caching.
|
8
|
+
class MemcachedStore < AbstractStore
|
9
|
+
attr_accessor :namespace, :servers, :memcached
|
10
|
+
|
11
|
+
def initialize(config = {})
|
12
|
+
@namespace = config[:namespace]
|
13
|
+
@servers = config[:servers] || ["127.0.0.1:11211"]
|
14
|
+
|
15
|
+
connect(config)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Memcached store consideres all keys and parameters
|
19
|
+
# writable.
|
20
|
+
def writable?(key, parameters = {}, conditions = {})
|
21
|
+
true
|
22
|
+
end
|
23
|
+
|
24
|
+
# Reads key from the cache.
|
25
|
+
def read(key, parameters = {})
|
26
|
+
begin
|
27
|
+
@memcached.get(normalize(key, parameters))
|
28
|
+
rescue Memcached::NotFound, Memcached::Stored
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Writes data to the cache using key, parameters and conditions.
|
34
|
+
def write(key, data = nil, parameters = {}, conditions = {})
|
35
|
+
if writable?(key, parameters, conditions)
|
36
|
+
begin
|
37
|
+
@memcached.set(normalize(key, parameters), data, expire_time(conditions))
|
38
|
+
true
|
39
|
+
rescue
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Fetches cached data by key if it exists. If it does not,
|
46
|
+
# uses passed block to get new cached value and writes it
|
47
|
+
# using given key.
|
48
|
+
def fetch(key, parameters = {}, conditions = {}, &blk)
|
49
|
+
read(key, parameters) || (writable?(key, parameters, conditions) && write(key, value = blk.call, parameters, conditions) && value)
|
50
|
+
end
|
51
|
+
|
52
|
+
# returns true/false based on if data identified by the key & parameters
|
53
|
+
# is persisted in the store.
|
54
|
+
#
|
55
|
+
# With Memcached 1.2 protocol the only way to
|
56
|
+
# find if key exists in the cache is to read it.
|
57
|
+
# It is very fast and shouldn't be a concern.
|
58
|
+
def exists?(key, parameters = {})
|
59
|
+
begin
|
60
|
+
true if @memcached.get(normalize(key, parameters))
|
61
|
+
rescue Memcached::NotFound
|
62
|
+
false
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Deletes entry from cached by key.
|
67
|
+
def delete(key, parameters = {})
|
68
|
+
begin
|
69
|
+
@memcached.delete(normalize(key, parameters))
|
70
|
+
rescue Memcached::NotFound
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Flushes the cache.
|
76
|
+
def delete_all
|
77
|
+
@memcached.flush
|
78
|
+
end
|
79
|
+
|
80
|
+
def clone
|
81
|
+
twin = super
|
82
|
+
twin.memcached = @memcached.clone
|
83
|
+
twin
|
84
|
+
end
|
85
|
+
|
86
|
+
# Establishes connection to Memcached.
|
87
|
+
#
|
88
|
+
# Use :buffer_requests option to use bufferring,
|
89
|
+
# :no_block to use non-blocking async I/O.
|
90
|
+
def connect(config = {})
|
91
|
+
@memcached = ::Memcached.new(@servers, config.only(:buffer_requests, :no_block).merge(:namespace => @namespace))
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns cache key calculated from base key
|
95
|
+
# and SHA2 hex from parameters.
|
96
|
+
def normalize(key, parameters = {})
|
97
|
+
parameters.empty? ? "#{key}" : "#{key}--#{parameters.to_sha2}"
|
98
|
+
end
|
99
|
+
|
100
|
+
# Returns expiration timestamp if :expire_in key is
|
101
|
+
# given.
|
102
|
+
def expire_time(conditions = {})
|
103
|
+
if t = conditions[:expire_in]
|
104
|
+
Time.now + t
|
105
|
+
else
|
106
|
+
0
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# -*- coding: undecided -*-
|
2
|
+
module Merb::Cache
|
3
|
+
# A strategy store wraps one or
|
4
|
+
# more fundamental stores, acting as a middle man between caching
|
5
|
+
# requests.
|
6
|
+
#
|
7
|
+
# For example, if you need to save memory on your Memcache server,
|
8
|
+
# you could wrap your MemcachedStore with a GzipStore. This would
|
9
|
+
# automatically compress the cached data when put into the cache, and
|
10
|
+
# decompress it on the way out. You can even wrap strategy caches
|
11
|
+
# with other strategy caches. If your key was comprised of sensitive
|
12
|
+
# information, like a SSN, you might want to encrypt the key before
|
13
|
+
# storage. Wrapping your GzipStore in a SHA1Store would take
|
14
|
+
# care of that for you.
|
15
|
+
#
|
16
|
+
# The AbstractStore class defines 9 methods as the API:
|
17
|
+
#
|
18
|
+
# writable?(key, parameters = {}, conditions = {})
|
19
|
+
# exists?(key, parameters = {})
|
20
|
+
# read(key, parameters = {})
|
21
|
+
# write(key, data = nil, parameters = {}, conditions = {})
|
22
|
+
# write_all(key, data = nil, parameters = {}, conditions = {})
|
23
|
+
# fetch(key, parameters = {}, conditions = {}, &blk)
|
24
|
+
# delete(key, parameters = {})
|
25
|
+
# delete_all
|
26
|
+
# delete_all!
|
27
|
+
#
|
28
|
+
# AbstractStrategyStore implements all of these with the exception
|
29
|
+
# of delete_all. If a strategy store can guarantee that calling
|
30
|
+
# delete_all on it’s wrapped store(s) will only delete entries
|
31
|
+
# populated by the strategy store, it may define the safe
|
32
|
+
# version of delete_all. However, this is usually not the
|
33
|
+
# case, hence delete_all is not part of the
|
34
|
+
# public API for AbstractStrategyStore.
|
35
|
+
class AbstractStrategyStore < AbstractStore
|
36
|
+
# START: interface for creating strategy stores. This should/might change.
|
37
|
+
def self.[](*stores)
|
38
|
+
Class.new(self) do
|
39
|
+
cattr_accessor :contextualized_stores
|
40
|
+
|
41
|
+
self.contextualized_stores = stores
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_accessor :stores
|
46
|
+
|
47
|
+
def initialize(config = {})
|
48
|
+
@stores = contextualized_stores.map do |cs|
|
49
|
+
case cs
|
50
|
+
when Symbol
|
51
|
+
Merb::Cache[cs]
|
52
|
+
when Class
|
53
|
+
cs.new(config)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# END: interface for creating strategy stores.
|
59
|
+
|
60
|
+
attr_accessor :stores
|
61
|
+
|
62
|
+
# determines if the store is able to persist data identified by the key & parameters
|
63
|
+
# with the given conditions.
|
64
|
+
def writable?(key, parameters = {}, conditions = {})
|
65
|
+
raise NotImplementedError
|
66
|
+
end
|
67
|
+
|
68
|
+
# gets the data from the store identified by the key & parameters.
|
69
|
+
# return nil if the entry does not exist.
|
70
|
+
def read(key, parameters = {})
|
71
|
+
raise NotImplementedError
|
72
|
+
end
|
73
|
+
|
74
|
+
# persists the data so that it can be retrieved by the key & parameters.
|
75
|
+
# returns nil if it is unable to persist the data.
|
76
|
+
# returns true if successful.
|
77
|
+
def write(key, data = nil, parameters = {}, conditions = {})
|
78
|
+
raise NotImplementedError
|
79
|
+
end
|
80
|
+
|
81
|
+
# persists the data to all context stores.
|
82
|
+
# returns nil if none of the stores were able to persist the data.
|
83
|
+
# returns true if at least one write was successful.
|
84
|
+
def write_all(key, data = nil, parameters = {}, conditions = {})
|
85
|
+
raise NotImplementedError
|
86
|
+
end
|
87
|
+
|
88
|
+
# tries to read the data from the store. If that fails, it calls
|
89
|
+
# the block parameter and persists the result.
|
90
|
+
def fetch(key, parameters = {}, conditions = {}, &blk)
|
91
|
+
raise NotImplementedError
|
92
|
+
end
|
93
|
+
|
94
|
+
# returns true/false/nil based on if data identified by the key & parameters
|
95
|
+
# is persisted in the store.
|
96
|
+
def exists?(key, parameters = {})
|
97
|
+
raise NotImplementedError
|
98
|
+
end
|
99
|
+
|
100
|
+
# deletes the entry for the key & parameter from the store.
|
101
|
+
def delete(key, parameters = {})
|
102
|
+
raise NotImplementedError
|
103
|
+
end
|
104
|
+
|
105
|
+
# deletes all entries for the key & parameters for the store.
|
106
|
+
# considered dangerous because strategy stores which call delete_all!
|
107
|
+
# on their context stores could delete other store's entrees.
|
108
|
+
def delete_all!
|
109
|
+
raise NotImplementedError
|
110
|
+
end
|
111
|
+
|
112
|
+
def clone
|
113
|
+
twin = super
|
114
|
+
twin.stores = self.stores.map {|s| s.clone}
|
115
|
+
twin
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Merb::Cache
|
2
|
+
# Store well suited for action caching.
|
3
|
+
class ActionStore < AbstractStrategyStore
|
4
|
+
# If you're not sending a controller dispatch, then we
|
5
|
+
# can't really write a cache
|
6
|
+
def writable?(dispatch, parameters = {}, conditions = {})
|
7
|
+
return @stores.any?{|s| s.writable?(normalize(dispatch), parameters, conditions)} if dispatch.is_a? Merb::Controller
|
8
|
+
return false
|
9
|
+
end
|
10
|
+
|
11
|
+
def read(dispatch, parameters = {})
|
12
|
+
if writable?(dispatch, parameters)
|
13
|
+
@stores.capture_first {|s| s.read(normalize(dispatch), parameters)}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def write(dispatch, data = nil, parameters = {}, conditions = {})
|
18
|
+
if writable?(dispatch, parameters)
|
19
|
+
return @stores.capture_first {|s| s.write(normalize(dispatch), (data || dispatch.body), parameters, conditions)}
|
20
|
+
else
|
21
|
+
return false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def write_all(dispatch, data = nil, parameters = {}, conditions = {})
|
26
|
+
if writable?(dispatch, parameters, conditions)
|
27
|
+
return @stores.map {|s| s.write_all(normalize(dispatch), data || dispatch.body, parameters, conditions)}.all?
|
28
|
+
else
|
29
|
+
return false
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch(dispatch, parameters = {}, conditions = {}, &blk)
|
34
|
+
if writable?(dispatch, parameters, conditions)
|
35
|
+
return read(dispatch, parameters) || @stores.capture_first {|s| s.fetch(normalize(dispatch), data || dispatch.body, parameters, conditions, &blk)}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def exists?(dispatch, parameters = {})
|
40
|
+
if writable?(dispatch, parameters)
|
41
|
+
return @stores.capture_first {|s| s.exists?(normalize(dispatch), parameters)}
|
42
|
+
end
|
43
|
+
|
44
|
+
return false
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete(dispatch, parameters = {})
|
48
|
+
if writable?(dispatch, parameters)
|
49
|
+
@stores.map {|s| s.delete(normalize(dispatch), parameters)}.any?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def delete_all!
|
54
|
+
@stores.map {|s| s.delete_all!}.all?
|
55
|
+
end
|
56
|
+
|
57
|
+
def normalize(dispatch)
|
58
|
+
"#{dispatch.class.name}##{dispatch.action_name}" unless dispatch.class.name.blank? || dispatch.action_name.blank?
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module Merb::Cache
|
2
|
+
# General purpose store, use for your own
|
3
|
+
# contexts. Since it wraps access to multiple
|
4
|
+
# fundamental stores, it's easy to use
|
5
|
+
# this strategy store with distributed cache
|
6
|
+
# stores like Memcached.
|
7
|
+
class AdhocStore < AbstractStrategyStore
|
8
|
+
class << self
|
9
|
+
alias_method :[], :new
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_accessor :stores
|
13
|
+
|
14
|
+
def initialize(*names)
|
15
|
+
@stores = names.map {|n| Merb::Cache[n]}
|
16
|
+
end
|
17
|
+
|
18
|
+
def writable?(key, parameters = {}, conditions = {})
|
19
|
+
@stores.capture_first {|s| s.writable?(key, parameters, conditions)}
|
20
|
+
end
|
21
|
+
|
22
|
+
# gets the data from the store identified by the key & parameters.
|
23
|
+
# return nil if the entry does not exist.
|
24
|
+
def read(key, parameters = {})
|
25
|
+
@stores.capture_first {|s| s.read(key, parameters)}
|
26
|
+
end
|
27
|
+
|
28
|
+
# persists the data so that it can be retrieved by the key & parameters.
|
29
|
+
# returns nil if it is unable to persist the data.
|
30
|
+
# returns true if successful.
|
31
|
+
def write(key, data = nil, parameters = {}, conditions = {})
|
32
|
+
@stores.capture_first {|s| s.write(key, data, parameters, conditions)}
|
33
|
+
end
|
34
|
+
|
35
|
+
# persists the data to all context stores.
|
36
|
+
# returns nil if none of the stores were able to persist the data.
|
37
|
+
# returns true if at least one write was successful.
|
38
|
+
def write_all(key, data = nil, parameters = {}, conditions = {})
|
39
|
+
@stores.map {|s| s.write_all(key, data, parameters, conditions)}.all?
|
40
|
+
end
|
41
|
+
|
42
|
+
# tries to read the data from the store. If that fails, it calls
|
43
|
+
# the block parameter and persists the result. If it cannot be fetched,
|
44
|
+
# the block call is returned.
|
45
|
+
def fetch(key, parameters = {}, conditions = {}, &blk)
|
46
|
+
read(key, parameters) ||
|
47
|
+
@stores.capture_first {|s| s.fetch(key, parameters, conditions, &blk)} ||
|
48
|
+
blk.call
|
49
|
+
end
|
50
|
+
|
51
|
+
# returns true/false/nil based on if data identified by the key & parameters
|
52
|
+
# is persisted in the store.
|
53
|
+
def exists?(key, parameters = {})
|
54
|
+
@stores.any? {|s| s.exists?(key, parameters)}
|
55
|
+
end
|
56
|
+
|
57
|
+
# deletes the entry for the key & parameter from the store.
|
58
|
+
def delete(key, parameters = {})
|
59
|
+
@stores.map {|s| s.delete(key, parameters)}.any?
|
60
|
+
end
|
61
|
+
|
62
|
+
# deletes all entries for the key & parameters for the store.
|
63
|
+
# considered dangerous because strategy stores which call delete_all!
|
64
|
+
# on their context stores could delete other store's entrees.
|
65
|
+
def delete_all!
|
66
|
+
@stores.map {|s| s.delete_all!}.all?
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|