tagged-cache 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,3 @@
1
+ === 1.0.0 2010-10-06
2
+
3
+ * Added ActiveSupport::Cache adapter.
data/README.rdoc ADDED
@@ -0,0 +1,92 @@
1
+ = tagged-cache
2
+
3
+ * http://github.com/antage/tagged-cache
4
+
5
+ == DESCRIPTION:
6
+
7
+ ActiveSupport cache adapter with tagged dependencies.
8
+
9
+ == SYNOPSIS:
10
+
11
+ === RUBY EXAMPLE:
12
+
13
+ require 'active_support'
14
+
15
+ cache = ActiveSupport::Cache.lookup_store(:tagged_store,
16
+ :tag_store => [:memory_store, :namespace => "tags"],
17
+ :entity_store => [:memory_store, :namespace => "entities"]
18
+ )
19
+
20
+ # tags management
21
+ cache.read_tag("tag1") # returns Fixnum
22
+ cache.read_tags("tag1", "tag2") # returns Hash { "tag1" => tag1_version, "tag2" => tag2_version }
23
+ cache.touch_tag("tag1") # increment tag1's version
24
+
25
+ # reading and writing cache
26
+ cache.write("cache key", value, :depends => ["tag1", "tag2"])
27
+ cache.fetch("cache key", :depends => ["tag1", "tag2]) { value }
28
+
29
+ # or
30
+
31
+ cache.tagged_fetch("cache key") do |entry|
32
+ entry.depends "tag1"
33
+ entry << "tag2"
34
+ entry.concat ["tag3", "tag4"]
35
+ entry.concat "tag5", "tag6"
36
+ value
37
+ end
38
+
39
+ # any object with method 'cache_tag' can be used as tag name
40
+ class A
41
+ attr_accessor :id
42
+
43
+ def cache_tag
44
+ "class/A/#{id}"
45
+ end
46
+ end
47
+
48
+ cache.tagged_fetch("some cache path") do |entry|
49
+ obj1 = A.new
50
+ obj1.id = 1
51
+
52
+ obj2 = A.new
53
+ obj2.id = 2
54
+
55
+ entry.depends obj1
56
+ entry.depends obj2
57
+
58
+ [obj1, obj2]
59
+ end
60
+
61
+ == REQUIREMENTS:
62
+
63
+ * activesupport >= 3.0.0
64
+
65
+ == INSTALL:
66
+
67
+ * sudo gem install tagged-cache
68
+
69
+ == LICENSE:
70
+
71
+ (The MIT License)
72
+
73
+ Copyright (c) 2010 Anton Ageev
74
+
75
+ Permission is hereby granted, free of charge, to any person obtaining
76
+ a copy of this software and associated documentation files (the
77
+ 'Software'), to deal in the Software without restriction, including
78
+ without limitation the rights to use, copy, modify, merge, publish,
79
+ distribute, sublicense, and/or sell copies of the Software, and to
80
+ permit persons to whom the Software is furnished to do so, subject to
81
+ the following conditions:
82
+
83
+ The above copyright notice and this permission notice shall be
84
+ included in all copies or substantial portions of the Software.
85
+
86
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
87
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
88
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
89
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
90
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
91
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
92
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,181 @@
1
+ require "active_support/core_ext/integer/time"
2
+
3
+ module ActiveSupport
4
+ module Cache
5
+ module TaggedEntry
6
+ attr_accessor :depends
7
+ end
8
+
9
+ class TaggedStore < Store
10
+ class Dependencies
11
+ def initialize(initial)
12
+ @tags = initial || []
13
+ end
14
+
15
+ def tags
16
+ @tags
17
+ end
18
+
19
+ def depends(tag)
20
+ @tags << tag
21
+ end
22
+
23
+ def <<(tag)
24
+ @tags << tag
25
+ end
26
+
27
+ def concat(*args)
28
+ if args.size == 1 && args.first.is_a?(Array)
29
+ @tags.concat(args.first)
30
+ else
31
+ @tags.concat(args)
32
+ end
33
+ end
34
+ end
35
+
36
+ def initialize(options = nil)
37
+ unless options && options[:tag_store] && options[:entity_store]
38
+ raise ":tag_store and :entity_store options are required"
39
+ else
40
+ @tag_store = Cache.lookup_store(*options.delete(:tag_store))
41
+ @entity_store = Cache.lookup_store(*options.delete(:entity_store))
42
+
43
+ @options = {}
44
+ end
45
+ end
46
+
47
+ def read_tag(key)
48
+ instrument(:read_tag, key) do
49
+ key = expanded_tag(key)
50
+ tag_value = @tag_store.read(key, :raw => true)
51
+ if tag_value.nil? || tag_value.to_i.zero?
52
+ new_value = Time.now.to_i
53
+ @tag_store.write(key, new_value, :raw => true)
54
+ new_value
55
+ else
56
+ tag_value.to_i
57
+ end
58
+ end
59
+ end
60
+
61
+ def read_tags(*keys)
62
+ instrument(:read_tags, keys) do
63
+ options = keys.extract_options! || {}
64
+ tag_names = keys.map { |k| expanded_tag(k) }
65
+ tags = @tag_store.read_multi(*(keys << options.merge(:raw => true)))
66
+ (tag_names - tags.keys).each do |unknown_tag|
67
+ tags[unknown_tag] = read_tag(unknown_tag)
68
+ end
69
+ tags
70
+ end
71
+ end
72
+
73
+ def touch_tag(key)
74
+ instrument(:touch_tag, key) do
75
+ key = expanded_tag(key)
76
+ @tag_store.increment(key) || @tag_store.write(key, Time.now.to_i, :raw => true)
77
+ end
78
+ end
79
+
80
+ def tagged_fetch(name, options = nil)
81
+ if block_given?
82
+ options = merged_options(options)
83
+ key = namespaced_key(name, options)
84
+ unless options[:force]
85
+ entry = instrument(:read, name, options) do |payload|
86
+ payload[:super_operation] = :fetch if payload
87
+ read_entry(key, options)
88
+ end
89
+ end
90
+ if entry && entry.expired?
91
+ race_ttl = options[:race_condition_ttl].to_f
92
+ if race_ttl and Time.now.to_f - entry.expires_at <= race_ttl
93
+ entry.expires_at = Time.now + race_ttl
94
+ write_entry(key, entry, :expires_in => race_ttl * 2)
95
+ else
96
+ delete_entry(key, options)
97
+ end
98
+ entry = nil
99
+ end
100
+
101
+ if entry
102
+ instrument(:fetch_hit, name, options) { |payload| }
103
+ entry.value
104
+ else
105
+ dependencies = Dependencies.new(options[:depends])
106
+ result = instrument(:generate, name, options) do |payload|
107
+ yield(dependencies)
108
+ end
109
+ write(name, result, options.merge(:depends => dependencies.tags))
110
+ result
111
+ end
112
+ else
113
+ read(name, options)
114
+ end
115
+ end
116
+
117
+ def delete_matched(matcher, options = nil)
118
+ @entity_store.delete_matched(matcher, options)
119
+ end
120
+
121
+ def increment(name, amount = 1, options = nil)
122
+ @entity_store.increment(name, amount, options)
123
+ end
124
+
125
+ def decrement(name, amount = 1, options = nil)
126
+ @entity_store.decrement(name, amount, options)
127
+ end
128
+
129
+ def cleanup(options = nil)
130
+ @entity_store.cleanup(options)
131
+ end
132
+
133
+ def clear(options = nil)
134
+ @entity_store.clear(options)
135
+ end
136
+
137
+ protected
138
+
139
+ def read_entry(key, options)
140
+ entry = @entity_store.send(:read_entry, key, options)
141
+ if entry.respond_to?(:depends) && entry.depends && !entry.depends.empty?
142
+ tags = read_tags(*entry.depends.keys)
143
+ valid = entry.depends.all? { |k, v| tags[k] == v }
144
+ entry.expires_at = (entry.created_at - 1.year) unless valid
145
+ end
146
+ entry
147
+ end
148
+
149
+ def write_entry(key, entry, options)
150
+ depends = (options[:depends] || []).uniq
151
+ unless depends.empty?
152
+ entry.extend(TaggedEntry)
153
+ entry.depends = read_tags(*depends)
154
+ end
155
+ @entity_store.send(:write_entry, key, entry, options.delete(:depends))
156
+ end
157
+
158
+ def delete_entry(key, options)
159
+ @entity_store.send(:delete_entry, key, options)
160
+ end
161
+
162
+ private
163
+
164
+ def namespaced_key(key, options)
165
+ key = expanded_key(key)
166
+ namespace = @entity_store.options[:namespace] if @entity_store.options
167
+ prefix = namespace.is_a?(Proc) ? namespace.call : namespace
168
+ key = "#{prefix}:#{key}" if prefix
169
+ key
170
+ end
171
+
172
+ def expanded_tag(tag)
173
+ if tag.respond_to?(:cache_tag)
174
+ tag = tag.cache_tag.to_s
175
+ else
176
+ tag.to_s
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,7 @@
1
+ require 'rspec'
2
+
3
+ Rspec.configure do |c|
4
+ c.mock_with :rspec
5
+ end
6
+
7
+ $LOAD_PATH << File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
@@ -0,0 +1,185 @@
1
+ require "active_support"
2
+
3
+ describe "TaggedStore" do
4
+ before(:each) do
5
+ @options = { :tag_store => [:memory_store, { :namespace => "tags" }], :entity_store => [:memory_store, { :namespace => "entities" }] }
6
+ end
7
+
8
+ context "without :tag_store option" do
9
+ subject { lambda { ActiveSupport::Cache.lookup_store :tagged_store, @options.delete(:tag_store) } }
10
+ it { should raise_exception }
11
+ end
12
+
13
+ context "without :entity_store option" do
14
+ subject { lambda { ActiveSupport::Cache.lookup_store :tagged_store, @options.delete(:entity_store) } }
15
+ it { should raise_exception }
16
+ end
17
+
18
+ context "with :tag_store and :entity_store options" do
19
+ subject { lambda { ActiveSupport::Cache.lookup_store :tagged_store, @options } }
20
+ it { should_not raise_exception }
21
+ end
22
+
23
+ context "instance" do
24
+ before(:each) do
25
+ @store ||= ActiveSupport::Cache.lookup_store :tagged_store, @options
26
+ @store.clear
27
+ end
28
+
29
+ context "#read_tag('abc')" do
30
+ subject { @store.read_tag("abc") }
31
+
32
+ it { should_not be_nil }
33
+ it "should be less or equal Time.now.to_i" do
34
+ should <= Time.now.to_i
35
+ end
36
+ end
37
+
38
+ context "#touch_tag('abc')" do
39
+ it "should increment tag value" do
40
+ old_value = @store.read_tag("abc")
41
+ @store.touch_tag("abc")
42
+ @store.read_tag("abc").should > old_value
43
+ end
44
+ end
45
+
46
+ context "#read_tags('tag1', 'tag2')" do
47
+ before(:each) do
48
+ @tag1_value = @store.read_tag("tag1")
49
+ @tag2_value = @store.read_tag("tag2")
50
+
51
+ @tags = @store.read_tags("tag1", "tag2")
52
+ end
53
+
54
+ specify { @tags.should have_key("tag1") }
55
+ specify { @tags.should have_key("tag2") }
56
+
57
+ it "should return tag1's value" do
58
+ @tags["tag1"].should == @tag1_value
59
+ end
60
+
61
+ it "should return tag2's value" do
62
+ @tags["tag2"].should == @tag2_value
63
+ end
64
+
65
+ ["tag1", "tag2"].each do |tag_name|
66
+ context "#{tag_name}" do
67
+ subject { @tags[tag_name] }
68
+ it { should be_instance_of(Fixnum) }
69
+ end
70
+ end
71
+ end
72
+
73
+ context "#read_tags('tagA', 'tagB')" do
74
+ context "when 'tagA' and 'tagB' don't exist in cache" do
75
+ before(:each) do
76
+ @tags = @store.read_tags("tagA", "tagB")
77
+ end
78
+
79
+ ["tagA", "tagB"].each do |tag_name|
80
+ context tag_name do
81
+ subject { @tags[tag_name] }
82
+ it { should_not be_nil }
83
+ it { should be_instance_of(Fixnum) }
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ context "#clear" do
90
+ before(:each) do
91
+ @tag_abc_value = @store.read_tag("abc")
92
+ @store.write("abc_entity", "test")
93
+ sleep 0.1
94
+ @store.clear
95
+ end
96
+
97
+ it "should clear store of entities" do
98
+ @store.read("abc_entity").should be_nil
99
+ end
100
+
101
+ it "should not touch any tags" do
102
+ @store.read_tag("abc").should == @tag_abc_value
103
+ end
104
+ end
105
+
106
+ context "entity" do
107
+ before(:each) do
108
+ @store.write("abc", "test", :depends => ["tag1", "tag2"])
109
+ end
110
+
111
+ it "should be read from cache when tags are untouched" do
112
+ @store.read("abc").should == "test"
113
+ end
114
+
115
+ ["tag1", "tag2"].each do |tag_name|
116
+ it "should not be read from cache when '#{tag_name}' is touched" do
117
+ @store.touch_tag(tag_name)
118
+ @store.read("abc").should be_nil
119
+ end
120
+ end
121
+ end
122
+
123
+ context "#fetch('abc', ...)" do
124
+ before(:each) do
125
+ @depends = ["tag1", "tag2"]
126
+ @store.write("abc", true, :depends => @depends)
127
+ end
128
+
129
+ it "should fetch value from cache when tags are untouched" do
130
+ @store.fetch("abc", :depends => @depends) { false }.should be_true
131
+ end
132
+
133
+ ["tag1", "tag2"].each do |tag_name|
134
+ it "should fetch value from proc when '#{tag_name}' is touched" do
135
+ @store.touch_tag(tag_name)
136
+ @store.fetch("abc", :depends => @depends) { false }.should be_false
137
+ end
138
+ end
139
+ end
140
+
141
+ context "#tagged_fetch('abc', ...)" do
142
+ before(:each) do
143
+ @depends = ["tag1", "tag2"]
144
+ @store.tagged_fetch("abc") do |entry|
145
+ "value".tap do
146
+ entry.depends "tag1"
147
+ entry << "tag2"
148
+ entry.concat "tag1", "tag2"
149
+ entry.concat ["tag1", "tag2"]
150
+ end
151
+ end
152
+ end
153
+
154
+ it "should fetch value from cache when tags are untouched" do
155
+ @store.read("abc").should == "value"
156
+ end
157
+
158
+ ["tag1", "tag2"].each do |tag_name|
159
+ it "should fetch value from proc when '#{tag_name}' is touched" do
160
+ @store.touch_tag(tag_name)
161
+ @store.read("abc").should be_nil
162
+ end
163
+ end
164
+ end
165
+
166
+ context "#touch_tag(object)" do
167
+ before(:each) do
168
+ @tag_value = @store.read_tag("tag1")
169
+ end
170
+
171
+ it "should calculate tag name and increment it" do
172
+ obj = Object.new
173
+ module ObjectCacheTag
174
+ def cache_tag
175
+ "tag1"
176
+ end
177
+ end
178
+ obj.extend(ObjectCacheTag)
179
+ @store.touch_tag(obj)
180
+ @store.read_tag("tag1").should > @tag_value
181
+ end
182
+ end
183
+
184
+ end
185
+ end
metadata ADDED
@@ -0,0 +1,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tagged-cache
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Anton Ageev
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-10-06 00:00:00 +04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activesupport
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 7
30
+ segments:
31
+ - 3
32
+ - 0
33
+ - 0
34
+ version: 3.0.0
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description:
38
+ email: antage@gmail.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files: []
44
+
45
+ files:
46
+ - README.rdoc
47
+ - History.txt
48
+ - Rakefile
49
+ - lib/active_support/cache/tagged_store.rb
50
+ - spec/tagged_store_spec.rb
51
+ - spec/spec_helper.rb
52
+ has_rdoc: true
53
+ homepage: http://github.com/antage/tagged-cache
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options: []
58
+
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ hash: 3
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ hash: 23
76
+ segments:
77
+ - 1
78
+ - 3
79
+ - 6
80
+ version: 1.3.6
81
+ requirements: []
82
+
83
+ rubyforge_project:
84
+ rubygems_version: 1.3.7
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: ActiveSupport cache adapter with tagged dependencies
88
+ test_files:
89
+ - spec/tagged_store_spec.rb
90
+ - spec/spec_helper.rb