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