rails-cache-tags 1.2.0 → 1.3.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,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