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
@@ -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