cachetier 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/Gemfile +10 -0
- data/LICENSE +22 -0
- data/README.md +60 -0
- data/Rakefile +7 -0
- data/cachetier.gemspec +20 -0
- data/lib/cachetier.rb +6 -0
- data/lib/cachetier/cache.rb +67 -0
- data/lib/cachetier/cachetier.rb +67 -0
- data/lib/cachetier/memory_tier.rb +41 -0
- data/lib/cachetier/nil_value.rb +12 -0
- data/lib/cachetier/redis_tier.rb +43 -0
- data/lib/cachetier/tier.rb +109 -0
- data/lib/cachetier/version.rb +3 -0
- data/spec/lib/cachetier_spec.rb +139 -0
- data/spec/lib/memory_tier_spec.rb +47 -0
- data/spec/lib/redis_tier_spec.rb +33 -0
- data/spec/lib/tier_spec.rb +29 -0
- data/spec/spec_helper.rb +1 -0
- metadata +107 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Yuval Larom
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# Cachetier
|
2
|
+
|
3
|
+
Data caching is useful when you need to invest in order to fetch the same data over and over, especially when each fetch is expensive, for example DB queries, web service calls, or the result of a non-trivial calculations.
|
4
|
+
|
5
|
+
The fastest way to cache data is local memory, but you can also use shared memory such as Redis or memcached which makes the data available to other application servers.
|
6
|
+
In some cases, even a DB can be a useful cache for very long operations, or such that you can only access a limited number of times (e.g. web service API with a daily limit).
|
7
|
+
|
8
|
+
Cachetier offers a way to define several layers of cache and automatically fetch and store the data over all layers.
|
9
|
+
|
10
|
+
## Usage
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
def MyClass
|
14
|
+
def self.get_value(key)
|
15
|
+
...
|
16
|
+
end
|
17
|
+
|
18
|
+
include Cachetier
|
19
|
+
cachetier :get_value, { mem: { ttl: 1.minute }, redis: { ttl: 10.minutes } }
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
In the example above, accessing ```MyClass.get_value``` will:
|
24
|
+
* Search a local hash for the an entry for ```key```
|
25
|
+
* If not found, lookup entry ```key``` in Redis
|
26
|
+
* If not found, invoke the original ```get_value``` method, and will store the result in Redis and in the local hash.
|
27
|
+
|
28
|
+
Cachetier can also receive a block as a parameter instead of using an existing method. Cachetier will define a class method for the given name.
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
class MyOtherClass
|
32
|
+
cachetier :get_other_value, { mem: { ttl: 10.sec } }, do |key|
|
33
|
+
...
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
MyOtherClass.get_other_value(42)
|
38
|
+
```
|
39
|
+
|
40
|
+
## Configuration
|
41
|
+
|
42
|
+
Cachetier configuration includes the layers at which data will be cached, and specific options per layer, such as TTL (time to live before expiration).
|
43
|
+
|
44
|
+
It currently defined are memory (hash) and Redis layers, but more layers (such as memcached and MongoDB) can be easily added.
|
45
|
+
|
46
|
+
You can set global default settings and override them for each method.
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
|
50
|
+
Cachetier.configuration = { mem: { ttl: 10.seconds } }
|
51
|
+
|
52
|
+
```
|
53
|
+
|
54
|
+
## Contributing
|
55
|
+
|
56
|
+
1. Fork it
|
57
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
58
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
59
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
60
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/cachetier.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/cachetier/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Yuval Larom"]
|
6
|
+
gem.email = ["yuval@ftbpro.com"]
|
7
|
+
gem.description = %q{Multi-tiered cache}
|
8
|
+
gem.summary = %q{Cache your data on multiple tiers: Redis, Memcached, Mongo, locally, etc}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = "cachetier"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Cachetier::VERSION
|
17
|
+
|
18
|
+
gem.add_development_dependency 'rake'
|
19
|
+
gem.add_development_dependency 'rspec'
|
20
|
+
end
|
data/lib/cachetier.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'cachetier/tier'
|
2
|
+
require 'cachetier/nil_value'
|
3
|
+
|
4
|
+
module Cachetier
|
5
|
+
|
6
|
+
class Cache
|
7
|
+
|
8
|
+
attr_reader :tiers, :getter_block
|
9
|
+
|
10
|
+
def initialize(tiers, &getter_block)
|
11
|
+
raise "Tiers cannot be nil" if !tiers
|
12
|
+
raise "Tiers cannot be empty" if tiers.empty?
|
13
|
+
|
14
|
+
@tiers = tiers.map do |name, options|
|
15
|
+
tier_class = Tier.get_tier_class(name)
|
16
|
+
tier = tier_class.new(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
@getter_block = getter_block
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](key)
|
23
|
+
prev_tiers = []
|
24
|
+
|
25
|
+
# some tiers override the key. save original clone of it
|
26
|
+
orig_key = begin
|
27
|
+
key.clone
|
28
|
+
rescue
|
29
|
+
key
|
30
|
+
end
|
31
|
+
|
32
|
+
tiers.each do |tier|
|
33
|
+
value = tier[key]
|
34
|
+
key = orig_key
|
35
|
+
|
36
|
+
if value
|
37
|
+
update_tiers(key, value, prev_tiers)
|
38
|
+
return nil if value == NilValue.value
|
39
|
+
return value
|
40
|
+
end
|
41
|
+
prev_tiers << tier
|
42
|
+
end
|
43
|
+
|
44
|
+
# block might change key
|
45
|
+
self[orig_key] = getter_block.call(key) if getter_block
|
46
|
+
end
|
47
|
+
|
48
|
+
def []=(key, value)
|
49
|
+
|
50
|
+
value = NilValue if value.nil?
|
51
|
+
tiers.each do |tier|
|
52
|
+
tier[key] = value if tier.writable?
|
53
|
+
end
|
54
|
+
return value
|
55
|
+
end
|
56
|
+
|
57
|
+
protected
|
58
|
+
|
59
|
+
def update_tiers(key, value, tiers)
|
60
|
+
tiers.each do |tier|
|
61
|
+
tier[key] = value if tier.writable?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'cachetier/cache'
|
2
|
+
|
3
|
+
module Cachetier
|
4
|
+
|
5
|
+
def self.config
|
6
|
+
@@config ||= {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.config=(val)
|
10
|
+
@@config = val
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.included(base)
|
14
|
+
base.send(:extend, ClassMethods)
|
15
|
+
end
|
16
|
+
|
17
|
+
module ClassMethods
|
18
|
+
|
19
|
+
def create_class_method(name, &block)
|
20
|
+
self.class.instance_eval do
|
21
|
+
define_method(name, &block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def alias_class_method(new_name, original_name)
|
26
|
+
class_eval %Q{
|
27
|
+
class << self
|
28
|
+
alias_method :#{new_name}, :#{original_name}
|
29
|
+
end
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def cachetier(method_name, options = nil, &block)
|
34
|
+
|
35
|
+
# given a block, create a new class method called method_name
|
36
|
+
# if not given a block, a method already exists. create a method called X_with_cachetier
|
37
|
+
cached_method_name = block ? method_name : "#{method_name}_with_cachetier"
|
38
|
+
|
39
|
+
# create a class method that uses cachetier
|
40
|
+
create_class_method cached_method_name do |key|
|
41
|
+
@@cachetiers[method_name][key]
|
42
|
+
end
|
43
|
+
|
44
|
+
# no block given - need to rename the existing method
|
45
|
+
if !block
|
46
|
+
|
47
|
+
# the original method will be called via X_without_cachetier
|
48
|
+
uncached_method_name = "#{method_name}_without_cachetier"
|
49
|
+
alias_class_method uncached_method_name, method_name
|
50
|
+
|
51
|
+
# calling the original method name will call X_with_cachetier
|
52
|
+
alias_class_method method_name, cached_method_name
|
53
|
+
|
54
|
+
# create a block for cachetier that calls the uncached version
|
55
|
+
block = proc { |key| self.send(uncached_method_name, key) }
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
options = Cachetier::config.merge(options || {})
|
60
|
+
cache = Cachetier::Cache.new(options, &block)
|
61
|
+
(@@cachetiers ||= {})[method_name] = cache
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'cachetier/tier'
|
2
|
+
|
3
|
+
module Cachetier
|
4
|
+
class MemoryTier < Tier
|
5
|
+
|
6
|
+
register_tier_class :mem, MemoryTier
|
7
|
+
|
8
|
+
def initialize(options = nil)
|
9
|
+
super
|
10
|
+
@cache = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def get_val_and_expiration_time(key)
|
14
|
+
val, expiration_time = @cache[key]
|
15
|
+
end
|
16
|
+
|
17
|
+
def reset(key)
|
18
|
+
@cache.delete(key)
|
19
|
+
end
|
20
|
+
|
21
|
+
def size
|
22
|
+
@cache.size
|
23
|
+
end
|
24
|
+
|
25
|
+
def keys
|
26
|
+
return @cache.keys
|
27
|
+
end
|
28
|
+
|
29
|
+
def expired?(key)
|
30
|
+
val, expiration_time = get_val_and_expiration_time(key)
|
31
|
+
return expiration_time < Time.now
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
def set(key, value, ttl)
|
37
|
+
@cache[key] = [value, Time.now + ttl.to_f]
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'cachetier/tier'
|
2
|
+
|
3
|
+
module Cachetier
|
4
|
+
class RedisTier < Tier
|
5
|
+
|
6
|
+
register_tier_class :redis, RedisTier
|
7
|
+
|
8
|
+
def initialize(options)
|
9
|
+
super
|
10
|
+
@redis = options[:redis]
|
11
|
+
raise "Option :redis is required" if !@redis
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_val_and_expiration_time(key)
|
15
|
+
val = @redis.get(key)
|
16
|
+
expiration_time = Time.now + @redis.ttl(key) if val
|
17
|
+
return [val, expiration_time]
|
18
|
+
end
|
19
|
+
|
20
|
+
def reset(key)
|
21
|
+
@redis.del(key)
|
22
|
+
end
|
23
|
+
|
24
|
+
protected
|
25
|
+
|
26
|
+
def size
|
27
|
+
0
|
28
|
+
end
|
29
|
+
|
30
|
+
def sweepable?
|
31
|
+
false
|
32
|
+
end
|
33
|
+
|
34
|
+
def set(key, value, ttl)
|
35
|
+
@redis.multi do
|
36
|
+
@redis.set(key, value)
|
37
|
+
@redis.expire(key, ttl)
|
38
|
+
end
|
39
|
+
value
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Cachetier
|
2
|
+
class Tier
|
3
|
+
|
4
|
+
def self.register_tier_class(name, klass)
|
5
|
+
@@tier_classes ||= {}
|
6
|
+
@@tier_classes[name] = klass
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.get_tier_class(name)
|
10
|
+
return @@tier_classes[name]
|
11
|
+
end
|
12
|
+
|
13
|
+
DEFAULTS = {
|
14
|
+
ttl: 0,
|
15
|
+
high_watermark: nil,
|
16
|
+
low_watermark: nil
|
17
|
+
}
|
18
|
+
|
19
|
+
def initialize(options = nil)
|
20
|
+
@options = DEFAULTS.merge(options || {})
|
21
|
+
raise "TTL must be a positive number" if ttl && ttl < 0
|
22
|
+
raise "High watermark must be a positive number" if high_watermark && high_watermark <= 0
|
23
|
+
raise "Low watermark must be a positive number" if low_watermark && low_watermark <= 0
|
24
|
+
raise "High watermark must be larger than lower watermark" if high_watermark && low_watermark && high_watermark <= low_watermark
|
25
|
+
end
|
26
|
+
|
27
|
+
def ttl
|
28
|
+
@options[:ttl]
|
29
|
+
end
|
30
|
+
|
31
|
+
def high_watermark
|
32
|
+
@options[:high_watermark]
|
33
|
+
end
|
34
|
+
|
35
|
+
def low_watermark
|
36
|
+
@options[:low_watermark]
|
37
|
+
end
|
38
|
+
|
39
|
+
def [](key)
|
40
|
+
val, expiration_time = get_val_and_expiration_time(key)
|
41
|
+
|
42
|
+
if expiration_time && Time.now > expiration_time
|
43
|
+
val = nil
|
44
|
+
reset key
|
45
|
+
end
|
46
|
+
|
47
|
+
return val
|
48
|
+
end
|
49
|
+
|
50
|
+
def reset(key)
|
51
|
+
raise NotImplementedError
|
52
|
+
end
|
53
|
+
|
54
|
+
def []=(key, value)
|
55
|
+
raise "Read-only tier" if !writable?
|
56
|
+
sweep_if_needed if sweepable?
|
57
|
+
set(key, value, ttl)
|
58
|
+
end
|
59
|
+
|
60
|
+
def writable?
|
61
|
+
return @options[:writable] if @options.has_key?(:writable)
|
62
|
+
true
|
63
|
+
end
|
64
|
+
|
65
|
+
def sweepable?
|
66
|
+
return @options[:sweepable] if @options.has_key?(:sweepable)
|
67
|
+
true
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
def sweep_if_needed
|
73
|
+
if high_watermark && low_watermark
|
74
|
+
sweep if size >= high_watermark
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def sweep
|
79
|
+
do_sweep(force: false)
|
80
|
+
if size >= high_watermark
|
81
|
+
do_sweep(force: true)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def do_sweep(options = {force: false})
|
86
|
+
force = options[:force]
|
87
|
+
raise "Read-only tier" if !writable?
|
88
|
+
raise "Un-sweeable tier" if !sweepable?
|
89
|
+
curr_size = size
|
90
|
+
keys.each do |key|
|
91
|
+
if force || expired?(key)
|
92
|
+
reset(key)
|
93
|
+
curr_size -= 1
|
94
|
+
break if curr_size <= low_watermark
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def get_val_and_expiration_time(key)
|
100
|
+
raise NotImplementedError
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
def keys
|
105
|
+
raise NotImplementedError
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cachetier'
|
3
|
+
|
4
|
+
class DummyTier < Cachetier::Tier
|
5
|
+
def set(key, value, ttl)
|
6
|
+
end
|
7
|
+
|
8
|
+
def reset(key)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
class AlwaysExpiredDummyTier < DummyTier
|
14
|
+
register_tier_class :always_expired, AlwaysExpiredDummyTier
|
15
|
+
|
16
|
+
def get_val_and_expiration_time(key)
|
17
|
+
return :dummy_cached_value_that_should_never_be_returned, Time.now - 1 # always ewxpired
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class AlwaysFreshDummyTier < DummyTier
|
22
|
+
register_tier_class :always_fresh, AlwaysFreshDummyTier
|
23
|
+
|
24
|
+
def get_val_and_expiration_time(key)
|
25
|
+
return :dummy_cached_value, Time.now + 10 # never expires
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class SingleValueDummyTier < DummyTier
|
30
|
+
register_tier_class :single_value, SingleValueDummyTier
|
31
|
+
|
32
|
+
def initialize(options)
|
33
|
+
super
|
34
|
+
@value = options[:value]
|
35
|
+
@expires_at = Time.now + options[:ttl]
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_val_and_expiration_time(key)
|
39
|
+
return @value, @expires_at # never expires
|
40
|
+
end
|
41
|
+
|
42
|
+
def set(key, value, ttl)
|
43
|
+
@value = [:single_value_tier, value]
|
44
|
+
@expires_at = Time.now + ttl
|
45
|
+
end
|
46
|
+
|
47
|
+
def reset(key)
|
48
|
+
end
|
49
|
+
|
50
|
+
def has_key?(key)
|
51
|
+
true
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
describe Cachetier::Cache do
|
57
|
+
|
58
|
+
it "should return nil if nothing there" do
|
59
|
+
cache = Cachetier::Cache.new(mem: { ttl: 0.2 })
|
60
|
+
cache[:a].should == nil
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should use memory tier to save data for 2 sec then expire it" do
|
64
|
+
cache = Cachetier::Cache.new(mem: { ttl: 0.2 })
|
65
|
+
cache[:a] = 1
|
66
|
+
cache[:a].should == 1
|
67
|
+
sleep 0.1
|
68
|
+
cache[:a].should == 1
|
69
|
+
sleep 0.2
|
70
|
+
cache[:a].should == nil
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should use getter_block to set value" do
|
74
|
+
cache = Cachetier::Cache.new(mem: { ttl: 0.2 }) { :the_value }
|
75
|
+
cache[:a].should == :the_value
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should create a cachetier for class and always return the cached value" do
|
79
|
+
|
80
|
+
class DummyClass1
|
81
|
+
include Cachetier
|
82
|
+
|
83
|
+
cachetier :get_cached_val, { always_fresh: nil } do |key|
|
84
|
+
:dummy_uncached_value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
DummyClass1.get_cached_val(:dummy_key).should == :dummy_cached_value
|
89
|
+
end
|
90
|
+
|
91
|
+
it "should create a cachetier for class and never return the cached value" do
|
92
|
+
|
93
|
+
class DummyClass2
|
94
|
+
include Cachetier
|
95
|
+
|
96
|
+
cachetier :get_cached_val, { always_expired: nil } do |key|
|
97
|
+
:dummy_uncached_value
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
DummyClass2.get_cached_val(:dummy_key).should == :dummy_uncached_value
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should fallback from one tier to the next" do
|
105
|
+
|
106
|
+
class DummyClass3
|
107
|
+
include Cachetier
|
108
|
+
|
109
|
+
cachetier :get_cached_val, { always_expired: nil, always_fresh: nil } do |key|
|
110
|
+
:dummy_uncached_value
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# the first tier is always expired, the second is always fresh.
|
115
|
+
# the request should try the first tier, fail, and get from the fresh tier
|
116
|
+
|
117
|
+
DummyClass3.get_cached_val(:dummy_key).should == :dummy_cached_value
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should update expired tiers" do
|
121
|
+
|
122
|
+
class DummyClass4
|
123
|
+
include Cachetier
|
124
|
+
|
125
|
+
cachetier :get_cached_val, { single_value: { value: :initial_value, ttl: 0.1 } } do |key|
|
126
|
+
:set_value
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
DummyClass4.get_cached_val(:whatever).should == :initial_value
|
132
|
+
sleep 0.2
|
133
|
+
DummyClass4.get_cached_val(:whatever).should == :set_value # returned by getter proc
|
134
|
+
DummyClass4.get_cached_val(:whatever).should == [:single_value_tier, :set_value] # returned by tier
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
end
|
139
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cachetier'
|
3
|
+
|
4
|
+
describe Cachetier::MemoryTier do
|
5
|
+
|
6
|
+
it "should save data for 200ms sec then expire it" do
|
7
|
+
cache = Cachetier::MemoryTier.new(ttl: 0.2)
|
8
|
+
cache[:a] = 1
|
9
|
+
cache[:a].should == 1
|
10
|
+
sleep 0.1
|
11
|
+
cache[:a].should == 1
|
12
|
+
sleep 0.2
|
13
|
+
cache[:a].should be_nil
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should sweep oldest keys when reaching high_watermark until only low_watermark items remain" do
|
17
|
+
cache = Cachetier::MemoryTier.new(ttl: 0.2, high_watermark: 20, low_watermark: 10)
|
18
|
+
|
19
|
+
20.times do |i|
|
20
|
+
cache[i] = i
|
21
|
+
cache.size.should == i + 1
|
22
|
+
end
|
23
|
+
|
24
|
+
# now that we have 20 items in the cache,
|
25
|
+
# adding a new item should triggr a sweep old keys until size is 10,
|
26
|
+
# and then the new item is added, bringing size to 11
|
27
|
+
|
28
|
+
cache[99] = 99
|
29
|
+
cache.size.should == 11
|
30
|
+
|
31
|
+
# make sure oldest keys are gone
|
32
|
+
|
33
|
+
10.times do |i|
|
34
|
+
cache[i].should == nil
|
35
|
+
end
|
36
|
+
|
37
|
+
# make sure newest keys are there
|
38
|
+
|
39
|
+
(11 .. 19).each do |i|
|
40
|
+
cache[i].should == i
|
41
|
+
end
|
42
|
+
|
43
|
+
cache[99].should == 99
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cachetier'
|
3
|
+
require 'cachetier/redis_tier'
|
4
|
+
|
5
|
+
describe Cachetier::RedisTier do
|
6
|
+
|
7
|
+
it "should require redis param" do
|
8
|
+
expect { Cachetier::RedisTier.new({}) }.to raise_error
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should try fetching value from redis, and check redis ttl" do
|
12
|
+
redis = double("redis")
|
13
|
+
redis.stub(get: :redis_cached_value)
|
14
|
+
redis.stub(ttl: 1)
|
15
|
+
|
16
|
+
cache = Cachetier::RedisTier.new({redis: redis})
|
17
|
+
|
18
|
+
redis.should_receive(:ttl).with(:key)
|
19
|
+
cache[:key].should == :redis_cached_value
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should try fetching value from redis, and check redis ttl" do
|
23
|
+
redis = double("redis")
|
24
|
+
redis.stub(multi: 1)
|
25
|
+
|
26
|
+
cache = Cachetier::RedisTier.new({redis: redis})
|
27
|
+
|
28
|
+
redis.should_receive(:multi)
|
29
|
+
cache[:key] = 1
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'cachetier'
|
3
|
+
|
4
|
+
describe Cachetier::Tier do
|
5
|
+
|
6
|
+
it "should validate tier ctor args" do
|
7
|
+
expect { Cachetier::Tier.new(ttl: -1) }.to raise_error
|
8
|
+
Cachetier::Tier.new(ttl: 10).should_not be_nil
|
9
|
+
|
10
|
+
expect { Cachetier::Tier.new(high_watermark: -1) }.to raise_error
|
11
|
+
expect { Cachetier::Tier.new(low_watermark: -1) }.to raise_error
|
12
|
+
expect { Cachetier::Tier.new(high_watermark: 1, low_watermark: 3) }.to raise_error
|
13
|
+
Cachetier::Tier.new(high_watermark: 3, low_watermark: 1).should_not be_nil
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should be able to modify tier writability and sweepability" do
|
17
|
+
|
18
|
+
Cachetier::Tier.new.writable?.should be_true
|
19
|
+
Cachetier::Tier.new.sweepable?.should be_true
|
20
|
+
|
21
|
+
Cachetier::Tier.new(writable: true, sweepable: false).writable?.should be_true
|
22
|
+
Cachetier::Tier.new(writable: true, sweepable: false).sweepable?.should be_false
|
23
|
+
|
24
|
+
Cachetier::Tier.new(writable: false, sweepable: true).writable?.should be_false
|
25
|
+
Cachetier::Tier.new(writable: false, sweepable: true).sweepable?.should be_true
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'cachetier'
|
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cachetier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Yuval Larom
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-04 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Multi-tiered cache
|
47
|
+
email:
|
48
|
+
- yuval@ftbpro.com
|
49
|
+
executables: []
|
50
|
+
extensions: []
|
51
|
+
extra_rdoc_files: []
|
52
|
+
files:
|
53
|
+
- .gitignore
|
54
|
+
- Gemfile
|
55
|
+
- LICENSE
|
56
|
+
- README.md
|
57
|
+
- Rakefile
|
58
|
+
- cachetier.gemspec
|
59
|
+
- lib/cachetier.rb
|
60
|
+
- lib/cachetier/cache.rb
|
61
|
+
- lib/cachetier/cachetier.rb
|
62
|
+
- lib/cachetier/memory_tier.rb
|
63
|
+
- lib/cachetier/nil_value.rb
|
64
|
+
- lib/cachetier/redis_tier.rb
|
65
|
+
- lib/cachetier/tier.rb
|
66
|
+
- lib/cachetier/version.rb
|
67
|
+
- spec/lib/cachetier_spec.rb
|
68
|
+
- spec/lib/memory_tier_spec.rb
|
69
|
+
- spec/lib/redis_tier_spec.rb
|
70
|
+
- spec/lib/tier_spec.rb
|
71
|
+
- spec/spec_helper.rb
|
72
|
+
homepage: ''
|
73
|
+
licenses: []
|
74
|
+
post_install_message:
|
75
|
+
rdoc_options: []
|
76
|
+
require_paths:
|
77
|
+
- lib
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
hash: -4048666811473957132
|
87
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
hash: -4048666811473957132
|
96
|
+
requirements: []
|
97
|
+
rubyforge_project:
|
98
|
+
rubygems_version: 1.8.24
|
99
|
+
signing_key:
|
100
|
+
specification_version: 3
|
101
|
+
summary: ! 'Cache your data on multiple tiers: Redis, Memcached, Mongo, locally, etc'
|
102
|
+
test_files:
|
103
|
+
- spec/lib/cachetier_spec.rb
|
104
|
+
- spec/lib/memory_tier_spec.rb
|
105
|
+
- spec/lib/redis_tier_spec.rb
|
106
|
+
- spec/lib/tier_spec.rb
|
107
|
+
- spec/spec_helper.rb
|