tagged-cache 1.0.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.
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