stacks 0.1.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.
@@ -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