rails-cache-tags 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1fec05417aaeacefe45090371a1987e97811b4a5
4
+ data.tar.gz: 80ab586b1cece89e8463d5306a5f072dcae869bd
5
+ SHA512:
6
+ metadata.gz: 4c4926e08c481634ad5d0307b671fd3103258876c9ab51533364313f79c4ed27ff950396493bb140f7a4905341b0fd7161ddb93a99ae9116ad0c17a2dafbb03b
7
+ data.tar.gz: 2663ab5400dde3728d4cf4187c2190910be91babbde55070fdab636b319fa1429b60ba78dce8e51bb26d32ab3c6f4a5f9b77e35811ddd45d9a5d8ed800d5aa49
@@ -1,42 +1,43 @@
1
- require "active_support/cache"
1
+ require 'active_support'
2
+ require 'active_support/cache'
2
3
 
3
- require "action_controller"
4
+ require 'rails/cache/tags/store'
4
5
 
5
- require "rails/cache/tag"
6
- require "rails/cache/tags/store"
7
-
8
- # Patch ActiveSupport common store
9
6
  module ActiveSupport
10
7
  module Cache
11
- class Store
12
- extend Rails::Cache::Tags::Store
13
- end
14
-
15
8
  class Entry
16
9
  attr_accessor :tags
17
10
  end
11
+
12
+ # patch built-in stores
13
+ [:FileStore, :MemCacheStore, :MemoryStore].each do |const|
14
+ if const_defined?(const)
15
+ begin
16
+ const_get(const).send(:include, Rails::Cache::Tags::Store)
17
+ rescue LoadError, NameError
18
+ # ignore
19
+ end
20
+ end
21
+ end
18
22
  end
19
23
  end
20
24
 
21
- # Patch ActionDispatch
22
- class ActionController::Base < ActionController::Metal
23
- def expire_fragments_by_tags *args
24
- return unless cache_configured?
25
+ # Patch ActionController
26
+ ActiveSupport.on_load(:action_controller) do
27
+ require 'rails/cache/tags/action_controller'
25
28
 
26
- cache_store.delete_tag *args
27
- end
28
- alias expire_fragments_by_tag expire_fragments_by_tags
29
+ ActionController::Base.send(:include, Rails::Cache::Tags::ActionController)
29
30
  end
30
31
 
31
32
  # Patch Dalli store
32
33
  begin
33
- require "dalli"
34
- require "dalli/version"
34
+ require 'dalli'
35
+ require 'dalli/version'
35
36
 
36
37
  if Dalli::VERSION.to_f > 2
37
- require "active_support/cache/dalli_store"
38
+ require 'active_support/cache/dalli_store'
38
39
 
39
- ActiveSupport::Cache::DalliStore.extend(Rails::Cache::Tags::Store)
40
+ ActiveSupport::Cache::DalliStore.send(:include, Rails::Cache::Tags::Store)
40
41
  end
41
42
  rescue LoadError, NameError
42
43
  # ignore
@@ -0,0 +1,14 @@
1
+ module Rails
2
+ module Cache
3
+ module Tags
4
+ module ActionController
5
+ def expire_fragments_by_tags(*args)
6
+ return unless cache_configured?
7
+
8
+ cache_store.delete_tag(*args)
9
+ end
10
+ alias_method :expire_fragments_by_tag, :expire_fragments_by_tags
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,47 @@
1
+ # coding: utf-8
2
+
3
+ require 'rails/cache/tags/tag'
4
+
5
+ module Rails
6
+ module Cache
7
+ module Tags
8
+ class Set
9
+ KEY_PREFIX = '_tags'
10
+
11
+ # @param [ActiveSupport::Cache::Store] cache
12
+ def initialize(cache)
13
+ @cache = cache
14
+ end
15
+
16
+ def current(tag)
17
+ @cache.fetch_without_tags(tag.to_key) { 1 }.to_i
18
+ end
19
+
20
+ def expire(tag)
21
+ version = current(tag) + 1
22
+
23
+ @cache.write_without_tags(tag.to_key, version, :expires_in => nil)
24
+
25
+ version
26
+ end
27
+
28
+ def check(entry)
29
+ return entry unless entry.is_a?(Store::Entry)
30
+ return entry.value if entry.tags.blank?
31
+
32
+ tags = Tag.build(entry.tags.keys)
33
+
34
+ saved_versions = entry.tags.values.map(&:to_i)
35
+ current_versions = read_multi(tags).values.map(&:to_i)
36
+
37
+ saved_versions == current_versions ? entry.value : nil
38
+ end
39
+
40
+ private
41
+ def read_multi(tags)
42
+ @cache.read_multi_without_tags(*Array.wrap(tags).map(&:to_key))
43
+ end
44
+ end # class Set
45
+ end # module Tags
46
+ end # module Cache
47
+ end # module Rails
@@ -1,102 +1,106 @@
1
1
  # coding: utf-8
2
2
 
3
+ require 'active_support/cache'
4
+ require 'active_support/concern'
5
+ require 'active_support/core_ext/module/aliasing'
6
+
7
+ require 'rails/cache/tags/set'
8
+
3
9
  module Rails
4
10
  module Cache
5
11
  module Tags
6
12
  module Store
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ alias_method_chain :initialize, :tags
17
+ alias_method_chain :exist?, :tags
18
+ alias_method_chain :read, :tags
19
+ alias_method_chain :read_multi, :tags
20
+ alias_method_chain :write, :tags
21
+ alias_method_chain :fetch, :tags
22
+ end
23
+
7
24
  # cache entry (for Dalli mainly)
8
25
  Entry = Struct.new(:value, :tags)
9
26
 
10
- # patched +new+ method
11
- def new(*args, &block) #:nodoc:
12
- unless acts_like?(:cached_tags)
13
- extend ClassMethods
14
- include InstanceMethods
27
+ attr_reader :tag_set
15
28
 
16
- alias_method_chain :read_entry, :tags
17
- alias_method_chain :write_entry, :tags
18
- end
29
+ def initialize_with_tags(*args)
30
+ initialize_without_tags(*args)
19
31
 
20
- super
32
+ @tag_set = Set.new(self)
21
33
  end
22
34
 
23
- module ClassMethods #:nodoc:all:
24
- def acts_like_cached_tags?
25
- end
26
- end
35
+ def read_with_tags(name, options = nil)
36
+ result = read_without_tags(name, options)
37
+ entry = @tag_set.check(result)
27
38
 
28
- module InstanceMethods
29
- # Increment the version of tags, so all entries referring to the tags become invalid
30
- def delete_tag *names
31
- tags = Rails::Cache::Tag.build_tags(names)
39
+ if entry
40
+ entry
41
+ else
42
+ delete(name)
32
43
 
33
- tags.each { |tag| tag.increment(self) } unless tags.empty?
44
+ nil
34
45
  end
35
- alias delete_by_tag delete_tag
36
- alias delete_by_tags delete_tag
37
- alias expire_tag delete_tag
38
-
39
- protected
40
- def read_entry_with_tags(key, options) #:nodoc
41
- entry = read_entry_without_tags(key, options)
46
+ end
42
47
 
43
- # tags are supported only for ActiveSupport::Cache::Entry or Rails::Cache::Tags::Entry
44
- tags = if entry.is_a?(ActiveSupport::Cache::Entry) || entry.is_a?(Entry)
45
- entry.tags
48
+ def write_with_tags(name, value, options = nil)
49
+ if options && options[:tags].present?
50
+ tags = Tag.build(options[:tags])
51
+ tags_hash = tags.each_with_object(Hash.new) do |tag, hash|
52
+ hash[tag.name] = tag_set.current(tag)
46
53
  end
47
54
 
48
- if tags.is_a?(Hash) && tags.present?
49
- current_versions = fetch_tags(entry.tags.keys).values
50
- saved_versions = entry.tags.values
51
-
52
- if current_versions != saved_versions
53
- delete_entry(key, options)
55
+ value = Entry.new(value, tags_hash)
56
+ end
54
57
 
55
- return nil
56
- end
57
- end
58
+ write_without_tags(name, value, options)
59
+ end
58
60
 
59
- entry.is_a?(Entry) ?
60
- entry.value :
61
- entry
62
- end # def read_entry_with_tags
61
+ def exist_with_tags?(name, options = nil)
62
+ exist_without_tags?(name, options) && !read(name).nil?
63
+ end
63
64
 
64
- def write_entry_with_tags(key, entry, options) #:nodoc:
65
- tags = Rails::Cache::Tag.build_tags Array.wrap(options[:tags]).flatten.compact
65
+ def read_multi_with_tags(*names)
66
+ result = read_multi_without_tags(*names)
66
67
 
67
- if entry && tags.present?
68
- options[:raw] = false # force :raw => false
68
+ names.each_with_object(Hash.new) do |name, hash|
69
+ hash[name.to_s] = @tag_set.check(result[name.to_s])
70
+ end
71
+ end
69
72
 
70
- # Dalli treats ActiveSupport::Cache::Entry as deprecated behavior, so we use our own Entry class
71
- entry = Entry.new(entry, nil) unless entry.is_a?(ActiveSupport::Cache::Entry)
73
+ def fetch_with_tags(name, options = nil)
74
+ return read(name, options) unless block_given?
72
75
 
73
- entry.tags = fetch_tags(tags).reduce(HashWithIndifferentAccess.new) do |hash, v|
74
- tag, value = v
76
+ yielded = false
75
77
 
76
- hash[tag.name] = value || tag.increment(self)
78
+ result = fetch_without_tags(name, options) do
79
+ yielded = true
80
+ yield
81
+ end
77
82
 
78
- hash
79
- end
83
+ if yielded
84
+ result
85
+ else # only :read occured
86
+ # read occured, and result is fresh
87
+ if (entry = @tag_set.check(result))
88
+ entry
89
+ else # result is stale
90
+ delete(name)
91
+ fetch(name, options) { yield }
80
92
  end
93
+ end
94
+ end
81
95
 
82
- write_entry_without_tags(key, entry, options)
83
- end # def write_entry_without_tags
84
-
85
- private
86
- # fetch tags versions from store
87
- # fetch ['user/1', 'post/2', 'country/2'] => [Tag('user/1') => 3, Tag('post/2') => 4, Tag('country/2') => nil]
88
- def fetch_tags(names) #:nodoc:
89
- tags = Rails::Cache::Tag.build_tags names
90
- stored = read_multi(*tags.map(&:to_key))
91
-
92
- # build hash
93
- tags.reduce(Hash.new) do |hash, t|
94
- hash[t] = stored[t.to_key]
95
-
96
- hash
97
- end
98
- end # def fetch_tags
99
- end # module InstanceMethods
96
+ # Increment the version of tags, so all entries referring to the tags become invalid
97
+ def delete_tag(*names)
98
+ tags = Tag.build(names)
99
+ tags.each { |tag| tag_set.expire(tag) }
100
+ end
101
+ alias delete_by_tag delete_tag
102
+ alias delete_by_tags delete_tag
103
+ alias expire_tag delete_tag
100
104
  end # module Store
101
105
  end # module Tags
102
106
  end # module Cache
@@ -0,0 +1,36 @@
1
+ module Rails
2
+ module Cache
3
+ module Tags
4
+ class Tag
5
+ KEY_PREFIX = '_tags'
6
+
7
+ # {:post => ['1', '2', '3']} => [Tag(post/1), Tag(post/2), Tag(post/3)]
8
+ # {:post => 1, :user => 2} => [Tag(post/1), Tag(user/2)]
9
+ # ['post/1', 'post/2', 'post/3'] => [Tag(post/1), Tag(post/2), Tag(post/3)]
10
+ def self.build(names)
11
+ case names
12
+ when NilClass then nil
13
+ when Hash then names.map do |key, value|
14
+ Array.wrap(value).map { |v| new([key, v]) }
15
+ end.flatten
16
+ when Enumerable then names.map { |v| build(v) }.flatten
17
+ when self then names
18
+ else [new(names)]
19
+ end
20
+ end
21
+
22
+ attr_reader :name
23
+
24
+ # Tag constructor
25
+ def initialize(name)
26
+ @name = ActiveSupport::Cache.expand_cache_key name
27
+ end
28
+
29
+ # real cache key
30
+ def to_key
31
+ [KEY_PREFIX, @name].join('/')
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -1,7 +1,7 @@
1
1
  module Rails
2
2
  module Cache
3
3
  module Tags
4
- VERSION = "1.2.0"
4
+ VERSION = "1.3.0"
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,171 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'fileutils'
5
+ require 'active_support/core_ext'
6
+
7
+ describe Rails::Cache::Tags do
8
+ shared_examples 'cache with tags support for' do |object|
9
+ before { cache.clear }
10
+
11
+ def assert_read(key, object)
12
+ expect(cache.exist?(key)).to eq !!object
13
+ expect(cache.read(key)).to eq object
14
+ end
15
+
16
+ def assert_blank(key)
17
+ assert_read key, nil
18
+ end
19
+
20
+ it 'reads and writes a key with tags' do
21
+ cache.write 'foo', object, :tags => 'baz'
22
+
23
+ assert_read 'foo', object
24
+ end
25
+
26
+ it 'deletes a key if tag is deleted' do
27
+ cache.write('foo', object, :tags => 'baz')
28
+ cache.delete_tag 'baz'
29
+
30
+ assert_blank 'foo'
31
+ end
32
+
33
+ it 'reads a key if another tag was deleted' do
34
+ cache.write('foo', object, :tags => 'baz')
35
+ cache.delete_tag 'fu'
36
+
37
+ assert_read 'foo', object
38
+ end
39
+
40
+ it 'reads and writes if multiple tags given' do
41
+ cache.write('foo', object, :tags => [:baz, :kung])
42
+
43
+ assert_read 'foo', object
44
+ end
45
+
46
+ it 'deletes a key if one of tags is deleted' do
47
+ cache.write('foo', object, :tags => [:baz, :kung])
48
+ cache.delete_tag :kung
49
+
50
+ assert_blank 'foo'
51
+ end
52
+
53
+ #it 'does not read a key if it is expired' do
54
+ # ttl = 0.01
55
+ # # dalli does not support float TTLs
56
+ # ttl *= 100 if cache.class.name == 'ActiveSupport::Cache::DalliStore'
57
+ #
58
+ # cache.write 'foo', object, :tags => [:baz, :kung], :expires_in => ttl
59
+ #
60
+ # sleep ttl * 2
61
+ #
62
+ # assert_blank 'foo'
63
+ #end
64
+
65
+ it 'reads and writes a key if hash of tags given' do
66
+ cache.write('foo', object, :tags => {:baz => 1})
67
+ assert_read 'foo', object
68
+
69
+ cache.delete_tag :baz => 2
70
+ assert_read 'foo', object
71
+
72
+ cache.delete_tag :baz => 1
73
+ assert_blank 'foo'
74
+ end
75
+
76
+ it 'reads and writes a key if array of object given as tags' do
77
+ tag1 = 1.day.ago
78
+ tag2 = 2.days.ago
79
+
80
+ cache.write 'foo', object, :tags => [tag1, tag2]
81
+ assert_read 'foo', object
82
+
83
+ cache.delete_tag tag1
84
+ assert_blank 'foo'
85
+ end
86
+
87
+ it 'reads multiple keys with tags check' do
88
+ cache.write 'foo', object, :tags => :bar
89
+ cache.write 'bar', object, :tags => :baz
90
+
91
+ assert_read 'foo', object
92
+ assert_read 'bar', object
93
+
94
+ cache.delete_tag :bar
95
+
96
+ assert_blank 'foo'
97
+ assert_read 'bar', object
98
+
99
+ expect(cache.read_multi('foo', 'bar')).to eq('foo' => nil, 'bar' => object)
100
+ end
101
+
102
+ it 'fetches key with tag check' do
103
+ cache.write 'foo', object, :tags => :bar
104
+
105
+ expect(cache.fetch('foo') { 'baz' }).to eq object
106
+ expect(cache.fetch('foo')).to eq object
107
+
108
+ cache.delete_tag :bar
109
+
110
+ expect(cache.fetch('foo')).to be_nil
111
+ expect(cache.fetch('foo', :tags => :bar) { object }).to eq object
112
+ assert_read 'foo', object
113
+
114
+ cache.delete_tag :bar
115
+
116
+ assert_blank 'foo'
117
+ end
118
+ end
119
+
120
+ class ComplexObject < Struct.new(:value)
121
+ end
122
+
123
+ SCALAR_OBJECT = 'bar'
124
+ COMPLEX_OBJECT = ComplexObject.new('bar')
125
+
126
+ shared_examples 'cache with tags support' do |*tags|
127
+ context '', tags do
128
+ include_examples 'cache with tags support for', SCALAR_OBJECT
129
+ include_examples 'cache with tags support for', COMPLEX_OBJECT
130
+
131
+ # test everything with locale cache
132
+ include_examples 'cache with tags support for', SCALAR_OBJECT do
133
+ include ActiveSupport::Cache::Strategy::LocalCache
134
+
135
+ around(:each) do |example|
136
+ if cache.respond_to?(:with_local_cache)
137
+ cache.with_local_cache { example.run }
138
+ end
139
+ end
140
+ end
141
+
142
+ include_examples 'cache with tags support for', COMPLEX_OBJECT do
143
+ include ActiveSupport::Cache::Strategy::LocalCache
144
+
145
+ around(:each) do |example|
146
+ cache.with_local_cache { example.run }
147
+ end
148
+ end
149
+ end
150
+ end
151
+
152
+ it_should_behave_like 'cache with tags support', :memory_store do
153
+ let(:cache) { ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => 100) }
154
+ end
155
+
156
+ it_should_behave_like 'cache with tags support', :file_store do
157
+ let(:cache_dir) { File.join(Dir.pwd, 'tmp_cache') }
158
+ before { FileUtils.mkdir_p(cache_dir) }
159
+ after { FileUtils.rm_rf(cache_dir) }
160
+
161
+ let(:cache) { ActiveSupport::Cache.lookup_store(:file_store, cache_dir, :expires_in => 60) }
162
+ end
163
+
164
+ it_should_behave_like 'cache with tags support', :memcache, :mem_cache_store do
165
+ let(:cache) { ActiveSupport::Cache.lookup_store(:mem_cache_store, :expires_in => 60) }
166
+ end
167
+
168
+ it_should_behave_like 'cache with tags support', :memcache, :dalli_store do
169
+ let(:cache) { ActiveSupport::Cache.lookup_store(:dalli_store, :expires_in => 60) }
170
+ end
171
+ end