stacks 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,16 @@
1
+ class Stacks::Items::Proc
2
+
3
+ def initialize(identifier, proc)
4
+ @identifier = identifier
5
+ @proc = proc
6
+ end
7
+
8
+ def key
9
+ @key ||= @identifier
10
+ end
11
+
12
+ def value
13
+ @proc.call
14
+ end
15
+
16
+ end
@@ -0,0 +1,11 @@
1
+ class Stacks::Items::Timestamp
2
+
3
+ def key
4
+ "cache_timestamp"
5
+ end
6
+
7
+ def value
8
+ Time.now
9
+ end
10
+
11
+ 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