stacks 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +85 -0
- data/LICENSE.txt +20 -0
- data/README.md +8 -0
- data/Rakefile +34 -0
- data/VERSION +1 -0
- data/lib/stacks.rb +48 -0
- data/lib/stacks/backends/backend.rb +38 -0
- data/lib/stacks/backends/key_value_backend.rb +29 -0
- data/lib/stacks/backends/namespaced_backend.rb +47 -0
- data/lib/stacks/cache.rb +13 -0
- data/lib/stacks/column_dependent_cache.rb +98 -0
- data/lib/stacks/items/column_dependent_block.rb +29 -0
- data/lib/stacks/items/method_call.rb +27 -0
- data/lib/stacks/items/proc.rb +16 -0
- data/lib/stacks/items/timestamp.rb +11 -0
- data/lib/stacks/method_cache.rb +18 -0
- data/lib/stacks/model_extensions.rb +57 -0
- data/lib/stacks/report_cache.rb +78 -0
- data/spec/backends/backend_spec.rb +98 -0
- data/spec/backends/key_value_backend_spec.rb +83 -0
- data/spec/backends/namespaced_backend_spec.rb +108 -0
- data/spec/column_dependent_cache_spec.rb +148 -0
- data/spec/items/column_dependent_block_spec.rb +42 -0
- data/spec/items/method_call_spec.rb +43 -0
- data/spec/items/proc_spec.rb +17 -0
- data/spec/method_cache_spec.rb +45 -0
- data/spec/model_extensions_spec.rb +106 -0
- data/spec/report_cache_spec.rb +141 -0
- data/spec/spec_helper.rb +55 -0
- data/stacks.gemspec +96 -0
- metadata +196 -0
@@ -0,0 +1,29 @@
|
|
1
|
+
class Stacks::Items::ColumnDependentBlock
|
2
|
+
|
3
|
+
attr_accessor :model
|
4
|
+
|
5
|
+
def initialize(model, columns, identifier, proc)
|
6
|
+
@model = model
|
7
|
+
@columns = columns.sort!
|
8
|
+
@columns = @columns.map { |c| c.to_s }
|
9
|
+
@identifier = identifier
|
10
|
+
@proc = proc
|
11
|
+
end
|
12
|
+
|
13
|
+
def key
|
14
|
+
@key ||= [@identifier].concat(@columns).join(Stacks::key_separator)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.key_to_columns(key)
|
18
|
+
all_keys = key.split(Stacks::key_separator)
|
19
|
+
|
20
|
+
# The identifier takes the first slot
|
21
|
+
all_keys.shift
|
22
|
+
all_keys
|
23
|
+
end
|
24
|
+
|
25
|
+
def value
|
26
|
+
@proc.call
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Stacks::Items::MethodCall
|
2
|
+
|
3
|
+
def initialize(object, method, args)
|
4
|
+
@object = object
|
5
|
+
@method = method
|
6
|
+
@args = args
|
7
|
+
end
|
8
|
+
|
9
|
+
def key_str
|
10
|
+
return @key_str if @key_str
|
11
|
+
|
12
|
+
object_str = Marshal.dump(@object)
|
13
|
+
method_str = @method.to_s
|
14
|
+
arg_str = Marshal.dump(@args)
|
15
|
+
|
16
|
+
@key_str ||= [object_str, method_str, arg_str].join(Stacks::key_separator)
|
17
|
+
end
|
18
|
+
|
19
|
+
def key
|
20
|
+
@key = Digest::SHA2.hexdigest(key_str)
|
21
|
+
end
|
22
|
+
|
23
|
+
def value
|
24
|
+
@object.send(@method, *@args)
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Stacks::MethodCache
|
2
|
+
|
3
|
+
extend Stacks::Cache
|
4
|
+
|
5
|
+
def self.backend
|
6
|
+
Stacks::Backends::KeyValueBackend.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.get_item(object, method, args, ttl)
|
10
|
+
Stacks::Items::MethodCall.new(object, method, args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.cached(object, method, args, ttl)
|
14
|
+
item = get_item(object, method, args, ttl)
|
15
|
+
get_value(item, backend, ttl)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class Stacks::ModelExtensions
|
2
|
+
|
3
|
+
def self.watched_models
|
4
|
+
@watched_models ||= Set.new
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.bust_cache_for_column(model, column)
|
8
|
+
bust_cache_for_columns(model, [column])
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.bust_cache_for_columns(model, columns)
|
12
|
+
Stacks.model_listening_caches.each do |cache|
|
13
|
+
cache.bust_cache(model, columns)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
module Extension
|
18
|
+
|
19
|
+
def self.included(base)
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
base.class_eval { before_save(:stacks_check_columns) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def stacks_check_columns
|
25
|
+
return unless Stacks::ModelExtensions.watched_models.include?(self.class)
|
26
|
+
|
27
|
+
changed.each do |column|
|
28
|
+
column = column.to_sym
|
29
|
+
|
30
|
+
if self.class.stacks_watched_columns.include?(column)
|
31
|
+
Stacks::ModelExtensions.bust_cache_for_column(self.class, column)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module ClassMethods
|
37
|
+
|
38
|
+
def stacks_watched_columns
|
39
|
+
@nbc_watched_columns ||= Set.new
|
40
|
+
end
|
41
|
+
|
42
|
+
def stacks_watch_column(column)
|
43
|
+
stacks_watched_columns << column
|
44
|
+
Stacks::ModelExtensions.watched_models << self
|
45
|
+
end
|
46
|
+
|
47
|
+
def bust_stacks
|
48
|
+
Stacks::ModelExtensions.bust_cache_for_columns(self, stacks_watched_columns)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
ActiveRecord::Base.class_eval { include Stacks::ModelExtensions::Extension }
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class Stacks::ReportCache
|
2
|
+
|
3
|
+
extend Stacks::Cache
|
4
|
+
|
5
|
+
attr_accessor(:report_name, :ttl, :values, :cache_condition)
|
6
|
+
|
7
|
+
def self.reports
|
8
|
+
@reports ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.get_report(report_name)
|
12
|
+
raise "Invalid report name" unless @reports.keys.include?(report_name)
|
13
|
+
reports[report_name]
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(report_name, ttl)
|
17
|
+
@report_name = report_name
|
18
|
+
@values = {}
|
19
|
+
@ttl = ttl
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_cache_condition(&block)
|
23
|
+
@cache_condition = block
|
24
|
+
end
|
25
|
+
|
26
|
+
def value(value_name, &block)
|
27
|
+
@values[value_name] = Stacks::Items::Proc.new(value_name, block)
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_item(item)
|
31
|
+
if cache_condition
|
32
|
+
Stacks.deactivate = true unless cache_condition.call
|
33
|
+
end
|
34
|
+
|
35
|
+
value = self.class.get_value(item, backend, @ttl)
|
36
|
+
Stacks.deactivate = false
|
37
|
+
value
|
38
|
+
end
|
39
|
+
|
40
|
+
def get_value(value_name)
|
41
|
+
get_item(@values[value_name])
|
42
|
+
end
|
43
|
+
|
44
|
+
def timestamp
|
45
|
+
get_item(Stacks::Items::Timestamp.new)
|
46
|
+
end
|
47
|
+
|
48
|
+
def fill_cache
|
49
|
+
if cache_condition
|
50
|
+
return unless cache_condition.call
|
51
|
+
end
|
52
|
+
|
53
|
+
@values.each { |value_name, item| backend.fill(item, @ttl) }
|
54
|
+
backend.fill(Stacks::Items::Timestamp.new, @ttl)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.report(report_name, ttl, &block)
|
58
|
+
report = new(report_name, ttl)
|
59
|
+
report.instance_eval(&block)
|
60
|
+
reports[report_name] = report
|
61
|
+
report
|
62
|
+
end
|
63
|
+
|
64
|
+
def register_instance_variables(instance)
|
65
|
+
values.each do |value_name, item|
|
66
|
+
instance.instance_variable_set("@#{value_name}".to_sym, get_value(value_name))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def backend
|
71
|
+
return @backend if @backend
|
72
|
+
|
73
|
+
backend = Stacks::Backends::NamespacedBackend.new
|
74
|
+
backend.namespace = @report_name
|
75
|
+
@backend ||= backend
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class TestBackend
|
4
|
+
|
5
|
+
include Stacks::Backends::Backend
|
6
|
+
|
7
|
+
def backend_key
|
8
|
+
"test_backend_key"
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
describe Stacks::Backends::Backend do
|
14
|
+
|
15
|
+
before(:each) do
|
16
|
+
@backend = TestBackend.new
|
17
|
+
@item = double
|
18
|
+
end
|
19
|
+
|
20
|
+
describe ".prefix_keys" do
|
21
|
+
|
22
|
+
it "includes the redis and backend prefix" do
|
23
|
+
@backend.prefix_keys.should == ["stacks", "test_backend_key"]
|
24
|
+
end
|
25
|
+
|
26
|
+
context "set extra prefix" do
|
27
|
+
|
28
|
+
before(:each) do
|
29
|
+
Stacks.extra_prefix = lambda { "test_extra_prefix" }
|
30
|
+
end
|
31
|
+
|
32
|
+
after(:each) do
|
33
|
+
Stacks.extra_prefix = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
it "now includes 3 keys, including the extra prefix" do
|
37
|
+
@backend.prefix_keys.should == ["stacks", "test_backend_key", "test_extra_prefix"]
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "#prefix_key" do
|
45
|
+
|
46
|
+
it "joins the prefix keys with the separator" do
|
47
|
+
@backend.prefix_key.should == "stacks:test_backend_key"
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#suffix_key" do
|
53
|
+
|
54
|
+
it "returns the items key" do
|
55
|
+
@item.should_receive(:key).and_return("test_key")
|
56
|
+
@backend.suffix_key(@item).should == "test_key"
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "#fill" do
|
62
|
+
|
63
|
+
it "should set and expire an item" do
|
64
|
+
ttl = 100
|
65
|
+
@backend.should_receive(:set).with(@item)
|
66
|
+
@backend.should_receive(:expire).with(@item, ttl)
|
67
|
+
|
68
|
+
@backend.fill(@item, ttl)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#get_or_set" do
|
74
|
+
|
75
|
+
it "returns the item's value if it's a cache hit" do
|
76
|
+
@backend.stub(:get).and_return("test_value")
|
77
|
+
@backend.should_receive(:fill).exactly(0).times
|
78
|
+
|
79
|
+
@backend.get_or_set(@item, 100).should == "test_value"
|
80
|
+
end
|
81
|
+
|
82
|
+
it "fills the cache with item's value if it's not a cache hit" do
|
83
|
+
@backend.stub(:get).and_raise(Stacks::NoValueException)
|
84
|
+
@backend.should_receive(:fill).exactly(1).times.with(@item, 100).and_return("test_value")
|
85
|
+
|
86
|
+
@backend.get_or_set(@item, 100).should == "test_value"
|
87
|
+
end
|
88
|
+
|
89
|
+
it "does not call fill if an unmarshaled value is nil" do
|
90
|
+
@backend.stub(:get).and_return(nil)
|
91
|
+
@backend.should_receive(:fill).exactly(0).times
|
92
|
+
|
93
|
+
@backend.get_or_set(@item, 100).should == nil
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Stacks::Backends::KeyValueBackend do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@backend = Stacks::Backends::KeyValueBackend.new
|
7
|
+
@key = "stacks:test_redis_key"
|
8
|
+
@item = double
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#backend_key" do
|
12
|
+
|
13
|
+
it "is set" do
|
14
|
+
@backend.backend_key.should == "kv"
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#hit?" do
|
20
|
+
|
21
|
+
before(:each) do
|
22
|
+
@backend.should_receive(:key).with(@item).and_return(@key)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "returns true if the redis key is set" do
|
26
|
+
Stacks.redis.set(@key, Marshal.dump("test_value"))
|
27
|
+
@backend.get(@item).should == "test_value"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "raises an exception if the redis key is not set" do
|
31
|
+
expect { @backend.get(@item).should be_nil }.to raise_error(Stacks::NoValueException)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#get" do
|
37
|
+
|
38
|
+
it "unmarshals what's stored at the key in redis" do
|
39
|
+
Stacks.redis.set(@key, Marshal.dump("test_value"))
|
40
|
+
@backend.should_receive(:key).with(@item).and_return(@key)
|
41
|
+
@backend.get(@item).should == "test_value"
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#set" do
|
47
|
+
|
48
|
+
it "stores the item's value in redis" do
|
49
|
+
@item.stub(:value) { "test_value" }
|
50
|
+
@backend.should_receive(:key).with(@item).exactly(2).times.and_return(@key)
|
51
|
+
@backend.set(@item)
|
52
|
+
|
53
|
+
@backend.get(@item).should == "test_value"
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#del" do
|
59
|
+
|
60
|
+
it "deletes an item from redis" do
|
61
|
+
@item.stub(:value) { "test_value" }
|
62
|
+
@backend.should_receive(:key).with(@item).exactly(4).times.and_return(@key)
|
63
|
+
@backend.set(@item)
|
64
|
+
|
65
|
+
@backend.get(@item).should == "test_value"
|
66
|
+
@backend.del(@item)
|
67
|
+
|
68
|
+
expect { @backend.get(@item) }.to raise_error(Stacks::NoValueException)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#expire" do
|
74
|
+
|
75
|
+
it "sets a redis expire for an item" do
|
76
|
+
@backend.should_receive(:key).with(@item).and_return(@key)
|
77
|
+
Stacks.redis.should_receive(:expire).with(@key, 100)
|
78
|
+
@backend.expire(@item, 100)
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Stacks::Backends::NamespacedBackend do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@backend = Stacks::Backends::NamespacedBackend.new
|
7
|
+
@backend.namespace = "test_namespace"
|
8
|
+
@item = double
|
9
|
+
end
|
10
|
+
|
11
|
+
describe "#prefix_keys" do
|
12
|
+
|
13
|
+
it "includes the namespace in the prefix keys" do
|
14
|
+
@backend.prefix_keys.include?("test_namespace")
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#prefix_key" do
|
20
|
+
|
21
|
+
it "includes the name space in the full key" do
|
22
|
+
@backend.prefix_key.include?("test_namespace")
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
context "with prefix and suffix keys" do
|
28
|
+
|
29
|
+
before(:each) do
|
30
|
+
@item.stub(:key) { "test_key" }
|
31
|
+
@item.stub(:value) { "test_value" }
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#get" do
|
35
|
+
|
36
|
+
it "returns the unmarshaled value of what's stored for the item" do
|
37
|
+
Stacks.redis.hset(@backend.prefix_key,
|
38
|
+
@backend.suffix_key(@item),
|
39
|
+
Marshal.dump(@item.value))
|
40
|
+
@backend.get(@item).should == "test_value"
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "#set" do
|
46
|
+
|
47
|
+
it "stores the marshaled version of an item's value in the cache" do
|
48
|
+
@backend.set(@item)
|
49
|
+
@backend.get(@item).should == "test_value"
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "#expire" do
|
55
|
+
|
56
|
+
it "sets a redis expiration for the prefix key" do
|
57
|
+
Stacks.redis.should_receive(:expire).with(@backend.prefix_key, 666)
|
58
|
+
@backend.expire(@item, 666)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "#clear_cache" do
|
64
|
+
|
65
|
+
it "deletes all keys under the namespace" do
|
66
|
+
backend1 = Stacks::Backends::NamespacedBackend.new
|
67
|
+
backend1.namespace = "test_namespace1"
|
68
|
+
backend2 = Stacks::Backends::NamespacedBackend.new
|
69
|
+
backend2.namespace = "test_namespace2"
|
70
|
+
|
71
|
+
backend1.set(@item)
|
72
|
+
backend2.set(@item)
|
73
|
+
|
74
|
+
backend1.get(@item).should == @item.value
|
75
|
+
backend2.get(@item).should == @item.value
|
76
|
+
|
77
|
+
backend1.clear_cache
|
78
|
+
|
79
|
+
expect { backend1.get(@item) }.to raise_error(Stacks::NoValueException)
|
80
|
+
backend2.get(@item).should == @item.value
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "#keys" do
|
86
|
+
|
87
|
+
it "returns the various suffix keys used in the namespace" do
|
88
|
+
@backend.set(@item)
|
89
|
+
@backend.keys.should == [@item.key]
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
describe "#del" do
|
95
|
+
|
96
|
+
it "removes the item from the cache" do
|
97
|
+
expect { @backend.get(@item) }.to raise_error(Stacks::NoValueException)
|
98
|
+
@backend.set(@item)
|
99
|
+
@backend.get(@item).should == "test_value"
|
100
|
+
@backend.del(@item)
|
101
|
+
expect { @backend.get(@item) }.to raise_error(Stacks::NoValueException)
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|