prefixed_cache_store 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0ae20416193b23bdb080fdbb7ccebbb9eb5912f4
4
+ data.tar.gz: 9c88a5ff0f2a52f573f2ed7b1166651547f6b6c3
5
+ SHA512:
6
+ metadata.gz: c60c6c9af8e90303dfab957406a6d54a29fe3d5b653f81f5cf841d9897624627dcae7150a2526f965916df82d9c612053a2e0137a02ab75044565a1fdae07e9c
7
+ data.tar.gz: 58e34568ae7ee5720af4347280dd73ab95417352cb453eea74469d86db535f68daebafd8f089ee5801886674eefc484e2670ee45c31a74483ab15625ed829d59
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ rvm:
2
+ - 1.9.2
3
+ - 1.9.3
4
+ - 2.0.0
5
+ - 2.1.1
6
+ - 2.1.2
7
+ cache: bundler
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ # Add dependencies to develop your gem here.
7
+ # Include everything needed to run rake, tests, features, etc.
8
+ group :development do
9
+ gem "activesupport", "~> 3"
10
+ gem 'rspec', '~> 3.1'
11
+ gem "bundler", "~> 1.0"
12
+ gem "jeweler", "~> 2.0.1"
13
+ gem "simplecov", ">= 0"
14
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2014 Julik Tarkhanov
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ ## prefixed_cache_store
2
+
3
+ A cache wrapper for ActiveSupport that allows you to expire parts of your cache imperatively.
4
+
5
+ Sometimes you need to selectively nuke parts of your cache. All cache stores for Rails support the `Store#clear()`
6
+ method, but it usually nukes the entire backing store - be it memcached or Redis. This is mostly not what you expect.
7
+
8
+ Originally, this wrapper has been developed to selectively expire cached items for translations when the translations
9
+ have been updated on the backend.
10
+
11
+ The way it works is that the wrapper accepts any `ActiveSupport::Cache::Store` and will prefix all keys going into or out of
12
+ that `Store`. The prefix will be memorized under a separate cache key. This cache key is going to be set periodically to prevent it from expiring too early.
13
+
14
+ When you increase this prefix value, all of the keys going through the store will change - so new
15
+ versions of the values are going to be generated and saved to the store. Consider a simple example:
16
+
17
+ store = PrefixedCacheStore.new(Rails.cache, "pfx") # Sets "pfx-version" to 0
18
+ store.write("foo", "Hello!") # Writes "pfx-0/foo"
19
+ store.read("foo") # Reads "pfx-0/foo"
20
+ store.clear # Replaces "pfx-version" with 1, does not flush your cache cluster
21
+ store.read("foo") # Reads "pfx-1/foo", which is not found now
22
+ store.write("foo", "Different hello!") # Writes "pfx-1/foo"
23
+
24
+ The value of the prefixed `version` key will be cached in memory of the Ruby process for 10 seconds between refetches - so most
25
+ of page renders will incur no more than one `read()` call for that specific key. Any `Store` can be wrapped in this object,
26
+ to your liking.
27
+
28
+ The gem has been extensively tested in production with Rails 3.x but should work fine with Rails 4 as well.
29
+
30
+ ## Contributing to prefixed_cache_store
31
+
32
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
33
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
34
+ * Fork the project.
35
+ * Start a feature/bugfix branch.
36
+ * Commit and push until you are happy with your contribution.
37
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
38
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
39
+
40
+ == Copyright
41
+
42
+ Copyright (c) 2014 Julik Tarkhanov. See LICENSE.txt for
43
+ further details.
44
+
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ # encoding: utf-8
2
+ require 'rubygems'
3
+ require 'bundler'
4
+ begin
5
+ Bundler.setup(:default, :development)
6
+ rescue Bundler::BundlerError => e
7
+ $stderr.puts e.message
8
+ $stderr.puts "Run `bundle install` to install missing gems"
9
+ exit e.status_code
10
+ end
11
+ require 'rake'
12
+
13
+ require_relative 'lib/prefixed_cache_store'
14
+
15
+ require 'jeweler'
16
+ Jeweler::Tasks.new do |gem|
17
+ # gem is a Gem::Specification... see http://guides.rubygems.org/specification-reference/ for more options
18
+ gem.version = PrefixedCacheStore::VERSION
19
+ gem.name = "prefixed_cache_store"
20
+ gem.homepage = "http://github.com/julik/prefixed_cache_store"
21
+ gem.license = "MIT"
22
+ gem.summary = %Q{Namespaced versioned cache wrapper for ActiveSupport}
23
+ gem.description = %Q{Allows you to expire parts of your cache separately}
24
+ gem.email = "me@julik.nl"
25
+ gem.authors = ["Julik Tarkhanov"]
26
+ # dependencies defined in Gemfile
27
+ end
28
+ Jeweler::RubygemsDotOrgTasks.new
29
+
30
+ require 'rspec/core/rake_task'
31
+ RSpec::Core::RakeTask.new(:spec) do |t|
32
+ t.rspec_opts = ["-c", "-f progress", "-r ./spec/spec_helper.rb"]
33
+ t.pattern = 'spec/**/*_spec.rb'
34
+ end
35
+ task :default => :spec
36
+
37
+ desc "Code coverage detail"
38
+ task :simplecov do
39
+ ENV['COVERAGE'] = "true"
40
+ Rake::Task['test'].execute
41
+ end
42
+
43
+ require 'rdoc/task'
44
+ Rake::RDocTask.new do |rdoc|
45
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
+
47
+ rdoc.rdoc_dir = 'rdoc'
48
+ rdoc.title = "prefixed_cache_store #{version}"
49
+ rdoc.rdoc_files.include('README*')
50
+ rdoc.rdoc_files.include('lib/**/*.rb')
51
+ end
@@ -0,0 +1,140 @@
1
+ # encoding: ascii
2
+ require 'forwardable'
3
+
4
+ # A caching store that will expire only the keys saved through it, by using a common prefix
5
+ # for the namespace and the version number that can be ratched up to "unlink" all the related keys.
6
+ # It assumes that the keys are being evicted automatically if they do not get used often.
7
+ class PrefixedCacheStore
8
+ VERSION = '0.0.1'
9
+
10
+ RETAIN_PREFIX_FOR_SECONDS = 10
11
+
12
+ extend Forwardable
13
+
14
+ attr_reader :prefix, :store
15
+
16
+ def_delegators :@store, :silence?,
17
+ :silence, :silence!,
18
+ :mute, :cleanup,
19
+ :logger, :logger=,
20
+ :instrument=, :instrument, :namespace, :namespace=
21
+
22
+ def initialize(store, prefix = 'pfx')
23
+ @store = store
24
+ @prefix = prefix
25
+ end
26
+
27
+ def fetch(name, options=nil)
28
+ if block_given?
29
+ @store.fetch(prefix_key(name), options) { yield }
30
+ else
31
+ @store.fetch(prefix_key(name), options)
32
+ end
33
+ end
34
+
35
+ def read(name, options=nil)
36
+ @store.read(prefix_key(name), options)
37
+ end
38
+
39
+ def write(name, value, options=nil)
40
+ @store.write(prefix_key(name), value, options)
41
+ end
42
+
43
+ def exist?(name, options=nil)
44
+ @store.exist?(prefix_key(name), options)
45
+ end
46
+
47
+ def delete(name, options=nil)
48
+ @store.delete(prefix_key(name), options)
49
+ end
50
+
51
+ # Reads multiple keys from the cache using a single call to the
52
+ # servers for all keys. Keys must be Strings.
53
+ def read_multi(*names)
54
+ names.extract_options!
55
+ prefixed_names = names.map{|e| prefix_key(e) }
56
+ result = @store.read_multi(*prefixed_names)
57
+ # Unprefix the keys received
58
+ result.inject({}) do |memo, (prefixed_key, value)|
59
+ memo.merge(unprefix_key(prefixed_key) => value)
60
+ end
61
+ end
62
+
63
+ # Increment a cached value.
64
+ def increment(name, amount = 1, options=nil)
65
+ @store.increment(prefix_key(name), amount, options)
66
+ end
67
+
68
+ # Decrement a cached value.
69
+ def decrement(name, amount = 1, options=nil)
70
+ @store.decrement(prefix_key(name), amount, options)
71
+ end
72
+
73
+ # Bump the version prefix making all keys obsolete.
74
+ def clear(options=nil)
75
+ bump_version! # First bump the version
76
+ @last_prefix = nil # Then make sure the cached version number will not be used
77
+ get_and_set_current_version
78
+ end
79
+
80
+ private
81
+
82
+ def bump_version!
83
+ key = [@prefix, "version"].join('-')
84
+ @store.write(key, @store.read(key).to_i + 1) # Redis does not support increment() with ActiveSupport values
85
+ end
86
+
87
+ # If the version prefix was last seen not too long ago, reuse it without asking the backend
88
+ # for it over and over. If it wasn't just fetch it as normally
89
+ def get_and_set_current_version
90
+ if @last_prefix && @last_seen && (Time.now.utc - @last_seen) < RETAIN_PREFIX_FOR_SECONDS
91
+ @last_prefix
92
+ else
93
+ @last_seen = Time.now.utc
94
+ @last_prefix = read_and_set_current_version_from_backend
95
+ @last_prefix
96
+ end
97
+ end
98
+
99
+ def read_and_set_current_version_from_backend
100
+ key = [@prefix, "version"].join('-')
101
+ @store.fetch(key) { 0 }
102
+ end
103
+
104
+ def prefix_key(key)
105
+ current_version = get_and_set_current_version
106
+ [@prefix, current_version, expanded_key(key)].join('-')
107
+ end
108
+
109
+ def unprefix_key(key)
110
+ @prefix_removal_re ||= /^#{prefix}\-\d+\-/ # Memoize to not parse the regex all the time
111
+ key.gsub(@prefix_removal_re, '')
112
+ end
113
+
114
+ # Expand key to be a consistent string value. Invoke +cache_key+ if
115
+ # object responds to +cache_key+. Otherwise, to_param method will be
116
+ # called. If the key is a Hash, then keys will be sorted alphabetically.
117
+ # Picked from the Dalli store adapter (cant use that one because it is a private method)
118
+ def expanded_key(key) # :nodoc:
119
+ return key.cache_key.to_s if key.respond_to?(:cache_key)
120
+
121
+ case key
122
+ when Array
123
+ if key.size > 1
124
+ key = key.collect{|element| expanded_key(element)}
125
+ else
126
+ key = key.first
127
+ end
128
+ when Hash
129
+ key = key.sort_by { |k,_| k.to_s }.collect{|k,v| "#{k}=#{v}"}
130
+ end
131
+
132
+ key = key.to_param
133
+ if key.respond_to? :force_encoding
134
+ key = key.dup
135
+ key.force_encoding('binary')
136
+ end
137
+ key
138
+ end
139
+
140
+ end
@@ -0,0 +1,133 @@
1
+ require_relative 'spec_helper'
2
+ require 'active_support'
3
+
4
+ describe PrefixedCacheStore do
5
+ describe 'instantiation' do
6
+ it 'creates a store wrapping another without passing the prefix' do
7
+ some_store = double('SomeStore')
8
+ store = described_class.new(some_store)
9
+
10
+ expect(store.store).to eq(some_store)
11
+ expect(store.prefix).to eq('pfx')
12
+ end
13
+ it 'creates a store wrapping another with given the prefix' do
14
+ some_store = double('SomeStore')
15
+ store = described_class.new(some_store, 'translations')
16
+ expect(store.store).to eq(some_store)
17
+ expect(store.prefix).to eq('translations')
18
+ end
19
+ end
20
+
21
+ describe 'delegated methods' do
22
+ it 'get forwarded to the backing store' do
23
+ method_names = [:silence?, :silence, :silence!,
24
+ :mute, :cleanup, :logger, :logger=, :instrument=, :instrument,
25
+ :namespace, :namespace=
26
+ ]
27
+
28
+ store_double = double('SomeStore')
29
+ subject = described_class.new(store_double)
30
+ method_names.each do | method_name |
31
+ expect(subject).to respond_to(method_name)
32
+ expect(store_double).to receive(method_name).once
33
+ subject.public_send(method_name)
34
+ end
35
+ end
36
+ end
37
+
38
+ describe 'fetch' do
39
+ it 'fetches the key and saves it to the cache and it then exists?' do
40
+ some_store = ActiveSupport::Cache::MemoryStore.new
41
+ subject = described_class.new(some_store, 'pre')
42
+
43
+ data = subject.fetch("record1") { 123 }
44
+ expect(data).to eq(123)
45
+ expect(subject.exist?('record1')).to eq(true)
46
+
47
+ expect(some_store.read("pre-version")).to eq(0), "Should have initialized the version to 0"
48
+ expect(some_store.read("pre-0-record1")).to eq(123), "Should have saved the prefixed value"
49
+ expect(some_store.read("record1")).to be_nil, "Should not have saved the unprefixed key"
50
+ end
51
+ end
52
+
53
+ describe 'write' do
54
+ it 'writes the key with the prefix and it then exists?' do
55
+ some_store = ActiveSupport::Cache::MemoryStore.new
56
+ subject = described_class.new(some_store, 'pre')
57
+
58
+ subject.write("record2", 456)
59
+ expect(subject.exist?('record2')).to eq(true)
60
+
61
+ expect(some_store.read("pre-version")).to eq(0)
62
+ expect(some_store.read("pre-0-record2")).to eq(456)
63
+ end
64
+ end
65
+
66
+ describe 'read' do
67
+ it 'reads the prefixed key instead' do
68
+ some_store = ActiveSupport::Cache::MemoryStore.new
69
+ subject = described_class.new(some_store, 'pre')
70
+
71
+ some_store.write('pre-version', 0)
72
+ some_store.write('pre-0-record3', "Ola peoples!")
73
+
74
+ expect(subject.read("record3")).to eq('Ola peoples!')
75
+ end
76
+ end
77
+
78
+ describe 'read_multi' do
79
+ it 'performs multi-reads' do
80
+ some_store = ActiveSupport::Cache::MemoryStore.new
81
+ subject = described_class.new(some_store, 'pre')
82
+
83
+ subject.write('record1', 'John')
84
+ subject.write('record2', 'Jake')
85
+ subject.write('record3', 'Mary')
86
+
87
+ expect(subject.read_multi('record1', 'record2', 'record3')).to eq({"record1"=>"John", "record2"=>"Jake", "record3"=>"Mary"})
88
+
89
+ expect(some_store.read("pre-version")).to eq(0)
90
+ expect(some_store.read("pre-0-record1")).to eq('John')
91
+ expect(some_store.read("pre-0-record2")).to eq('Jake')
92
+ expect(some_store.read("pre-0-record3")).to eq('Mary')
93
+ end
94
+ end
95
+
96
+ describe 'clear' do
97
+ it 'bumps the version' do
98
+ some_store = ActiveSupport::Cache::MemoryStore.new
99
+ subject = described_class.new(some_store, 'pre')
100
+
101
+ subject.write('record1', 'John')
102
+ expect(subject.read('record1')).to eq('John')
103
+
104
+ expect(some_store.read("pre-version")).to eq(0)
105
+
106
+ subject.clear
107
+
108
+ expect(subject.read('record1')).to be_nil
109
+ subject.write('record1', 'Jake')
110
+ expect(subject.read('record1')).to eq('Jake')
111
+
112
+ expect(some_store.read("pre-version")).to eq(1)
113
+ expect(some_store.read("pre-0-record1")).to eq('John')
114
+ expect(some_store.read("pre-1-record1")).to eq('Jake')
115
+ end
116
+ end
117
+
118
+ describe 'current_version'do
119
+ it 'caches the current value for 10 seconds' do
120
+ some_store = ActiveSupport::Cache::MemoryStore.new
121
+ expect(some_store).to receive(:fetch).with("pre-version").once { 4 }
122
+
123
+ subject = described_class.new(some_store, 'pre')
124
+ 5.times { subject.read('record1') }
125
+
126
+ some_seconds_after = Time.now + 11.seconds
127
+ allow(Time).to receive(:now) { some_seconds_after }
128
+ expect(some_store).to receive(:fetch).with("pre-version").once { 4 }
129
+ subject.read('record1')
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,31 @@
1
+ require 'simplecov'
2
+
3
+ module SimpleCov::Configuration
4
+ def clean_filters
5
+ @filters = []
6
+ end
7
+ end
8
+
9
+ SimpleCov.configure do
10
+ clean_filters
11
+ load_profile 'test_frameworks'
12
+ end
13
+
14
+ ENV["COVERAGE"] && SimpleCov.start do
15
+ add_filter "/.rvm/"
16
+ end
17
+ require 'rubygems'
18
+ require 'bundler'
19
+ begin
20
+ Bundler.setup(:default, :development)
21
+ rescue Bundler::BundlerError => e
22
+ $stderr.puts e.message
23
+ $stderr.puts "Run `bundle install` to install missing gems"
24
+ exit e.status_code
25
+ end
26
+ require 'rspec'
27
+
28
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
29
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
30
+ require 'prefixed_cache_store'
31
+
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prefixed_cache_store
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Julik Tarkhanov
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.1'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.1'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: jeweler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 2.0.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 2.0.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Allows you to expire parts of your cache separately
84
+ email: me@julik.nl
85
+ executables: []
86
+ extensions: []
87
+ extra_rdoc_files:
88
+ - LICENSE.txt
89
+ - README.md
90
+ files:
91
+ - ".document"
92
+ - ".travis.yml"
93
+ - Gemfile
94
+ - LICENSE.txt
95
+ - README.md
96
+ - Rakefile
97
+ - lib/prefixed_cache_store.rb
98
+ - spec/prefixed_cache_store_spec.rb
99
+ - spec/spec_helper.rb
100
+ homepage: http://github.com/julik/prefixed_cache_store
101
+ licenses:
102
+ - MIT
103
+ metadata: {}
104
+ post_install_message:
105
+ rdoc_options: []
106
+ require_paths:
107
+ - lib
108
+ required_ruby_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ required_rubygems_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ requirements: []
119
+ rubyforge_project:
120
+ rubygems_version: 2.2.2
121
+ signing_key:
122
+ specification_version: 4
123
+ summary: Namespaced versioned cache wrapper for ActiveSupport
124
+ test_files: []