├── .github └── dependabot.yml ├── .gitignore ├── .rubocop.yml ├── CODE_OF_CONDUCT.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib └── object │ ├── cache.rb │ └── cache │ ├── core_extension.rb │ └── version.rb ├── object-cache.gemspec └── test └── cache_test.rb /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | rubygems-server-gem-fury-io-blendle: 4 | type: rubygems-server 5 | url: https://gem.fury.io/blendle/ 6 | token: "${{secrets.RUBYGEMS_SERVER_GEM_FURY_IO_BLENDLE_TOKEN}}" 7 | rubygems-server-gems-contribsys-com: 8 | type: rubygems-server 9 | url: https://gems.contribsys.com/ 10 | username: "${{secrets.RUBYGEMS_SERVER_GEMS_CONTRIBSYS_COM_USERNAME}}" 11 | password: "${{secrets.RUBYGEMS_SERVER_GEMS_CONTRIBSYS_COM_PASSWORD}}" 12 | 13 | updates: 14 | - package-ecosystem: bundler 15 | directory: "/" 16 | schedule: 17 | interval: monthly 18 | time: "04:00" 19 | open-pull-requests-limit: 15 20 | registries: 21 | - rubygems-server-gem-fury-io-blendle 22 | - rubygems-server-gems-contribsys-com 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.wercker/ 3 | /Gemfile.lock 4 | /pkg/ 5 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.4 3 | Exclude: 4 | - '.wercker/**/*' 5 | 6 | Metrics/MethodLength: 7 | Max: 20 8 | 9 | Metrics/LineLength: 10 | Max: 100 11 | 12 | Style/RescueStandardError: 13 | Exclude: 14 | - 'lib/object/cache.rb' 15 | - 'test/cache_test.rb' 16 | 17 | Security/MarshalLoad: 18 | Exclude: 19 | - 'lib/object/cache.rb' 20 | - 'test/cache_test.rb' 21 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of 4 | fostering an open and welcoming community, we pledge to respect all people who 5 | contribute through reporting issues, posting feature requests, updating 6 | documentation, submitting pull requests or patches, and other activities. 7 | 8 | We are committed to making participation in this project a harassment-free 9 | experience for everyone, regardless of level of experience, gender, gender 10 | identity and expression, sexual orientation, disability, personal appearance, 11 | body size, race, ethnicity, age, religion, or nationality. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | 15 | * The use of sexualized language or imagery 16 | * Personal attacks 17 | * Trolling or insulting/derogatory comments 18 | * Public or private harassment 19 | * Publishing other's private information, such as physical or electronic 20 | addresses, without explicit permission 21 | * Other unethical or unprofessional conduct 22 | 23 | Project maintainers have the right and responsibility to remove, edit, or 24 | reject comments, commits, code, wiki edits, issues, and other contributions 25 | that are not aligned to this Code of Conduct, or to ban temporarily or 26 | permanently any contributor for other behaviors that they deem inappropriate, 27 | threatening, offensive, or harmful. 28 | 29 | By adopting this Code of Conduct, project maintainers commit themselves to 30 | fairly and consistently applying these principles to every aspect of managing 31 | this project. Project maintainers who do not follow or enforce the Code of 32 | Conduct may be permanently removed from the project team. 33 | 34 | This code of conduct applies both within project spaces and in public spaces 35 | when an individual is representing the project or its community. 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 38 | reported by contacting a project maintainer at jean@blendle.com. All 39 | complaints will be reviewed and investigated and will result in a response that 40 | is deemed necessary and appropriate to the circumstances. Maintainers are 41 | obligated to maintain confidentiality with regard to the reporter of an 42 | incident. 43 | 44 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 45 | version 1.3.0, available at 46 | [http://contributor-covenant.org/version/1/3/0/][version] 47 | 48 | [homepage]: http://contributor-covenant.org 49 | [version]: http://contributor-covenant.org/version/1/3/0/ 50 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Blendle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Object::Cache [![wercker status](https://app.wercker.com/status/8f5b16e230784fd4bd75f267d5d62e85/s/master "wercker status")](https://app.wercker.com/project/bykey/8f5b16e230784fd4bd75f267d5d62e85) 2 | 3 | Easy caching of Ruby objects, using [Redis](http://redis.io) as a backend store. 4 | 5 | * [Installation](#installation) 6 | * [Quick Start](#quick-start) 7 | * [Usage](#usage) 8 | * [marshaling data](#marshaling-data) 9 | * [ttl](#ttl) 10 | * [namespaced keys](#namespaced-keys) 11 | * [key prefixes](#key-prefixes) 12 | * [redis replicas](#redis-replicas) 13 | * [core extension](#core-extension) 14 | * [License](#license) 15 | 16 | ## Installation 17 | 18 | Add this line to your application's Gemfile: 19 | 20 | ```ruby 21 | gem 'object-cache' 22 | ``` 23 | 24 | And then execute: 25 | 26 | ```shell 27 | bundle 28 | ``` 29 | 30 | Or install it yourself as: 31 | 32 | ```shell 33 | gem install object-cache 34 | ``` 35 | 36 | ## Quick Start 37 | 38 | ```ruby 39 | # require the proper libraries in your project 40 | require 'redis' 41 | require 'object/cache' 42 | 43 | # set the backend to a new Redis instance 44 | Cache.backend = Redis.new 45 | 46 | # wrap your object in a `Cache.new` block to store the object on first usage, 47 | # and retrieve it again on subsequent usages 48 | Cache.new { 'hello world' } 49 | 50 | # add the core extension for easier access 51 | require 'object/cache/core_extension' 52 | cache { 'hello world' } 53 | ``` 54 | 55 | ## Usage 56 | 57 | Using `Object::Cache`, you can cache objects in Ruby that have a heavy cost 58 | attached to initializing them, and then replay the recorded object on any 59 | subsequent requests. 60 | 61 | For example, database query results can be cached, or HTTP requests to other 62 | services within your infrastructure. 63 | 64 | Caching an object is as easy as wrapping that object in a `Cache.new` block: 65 | 66 | ```ruby 67 | Cache.new { 'hello world' } 68 | ``` 69 | 70 | Here, the object is of type `String`, but it can be any type of object that can 71 | be marshalled using the Ruby [`Marshal`][marshal] library. 72 | 73 | #### marshaling data 74 | 75 | You can only marshal _data_, not _code_, so anything that produces code that is 76 | executed later to return data (like Procs) cannot be cached. You can still wrap 77 | those in a `Cache.new` block, and the block will return the Proc as expected, 78 | but no caching will occur, so there's no point in doing so. 79 | 80 | #### ttl 81 | 82 | By default, a cached object has a `ttl` (time to live) of one week. This means 83 | that every request after the first request uses the value from the cached 84 | object. After one week, the cached value becomes stale, and the first request 85 | after that will again store the (possibly changed) object in the cache store. 86 | 87 | You can globaly set the default ttl to a different value: 88 | 89 | ```ruby 90 | Cache.default_ttl = 120 91 | ``` 92 | 93 | You can easily modify the `ttl` per cached object, using the keyword argument by 94 | that same name: 95 | 96 | ```ruby 97 | Cache.new(ttl: 60) { 'remember me for 60 seconds!' } 98 | ``` 99 | 100 | Or, if you want the cached object to never go stale, disable the TTL entirely: 101 | 102 | ```ruby 103 | Cache.new(ttl: nil) { 'I am forever in your cache!' } 104 | Cache.new(ttl: 0) { 'me too!' } 105 | ``` 106 | 107 | Note that it is best to never leave a value in the backend forever. Since this 108 | library uses file names and line numbers to store the value, a change in your 109 | code might mean a new cache object is created after a deployment, and your old 110 | cache object becomes orphaned, and will polute your storage forever. 111 | 112 | #### namespaced keys 113 | 114 | When storing the key/value object into Redis, the key name is based on the file 115 | name and line number where the cache was initiated. This allows you to cache 116 | objects without specifying any namespacing yourself. 117 | 118 | If however, you are storing an object that changes based on input, you need to 119 | add a unique namespace to the cache, to make sure the correct object is returned 120 | from cache: 121 | 122 | ```ruby 123 | Cache.new(email) { User.find(email: email) } 124 | ``` 125 | 126 | In the above case, we use the customer's email to correctly namespace the 127 | returned object in the cache store. The provided namespace argument is still 128 | merged together with the file name and line number of the cache request, so you 129 | can re-use that same `email` namespace in different locations, without worrying 130 | about any naming collisions. 131 | 132 | #### key prefixes 133 | 134 | By default, the eventual key ending up in Redis is a 6-character long digest, 135 | based on the file name, line number, and optional key passed into the Cache 136 | object: 137 | 138 | ```ruby 139 | Cache.new { 'hello world' } 140 | Cache.backend.keys # => ["22abcc"] 141 | ``` 142 | 143 | This makes working with keys quick and easy, without worying about conflicting 144 | keys. 145 | 146 | However, this does make it more difficult to selectively delete keys from the 147 | backend, if you want to purge the cache of specific keys, before their TTL 148 | expires. 149 | 150 | To support this use-case, you can use the `key_prefix` attribute: 151 | 152 | ```ruby 153 | Cache.new(key_prefix: 'hello') { 'hello world' } 154 | Cache.backend.keys # => ["hello_22abcc"] 155 | ``` 156 | 157 | This allows you to selectively purge keys from Redis: 158 | 159 | ```ruby 160 | keys = Cache.backend.keys('hello_*') 161 | Cache.backend.del(keys) 162 | ``` 163 | 164 | You can also use the special value `:method_name` to dynamically set the key 165 | prefix based on where the cached object was created: 166 | 167 | ```ruby 168 | Cache.new(key_prefix: :method_name) { 'hello world' } 169 | Cache.backend.keys # => ["test_key_prefix_method_name_22abcc"] 170 | ``` 171 | 172 | Or, use `:class_name` to group keys in the same class together: 173 | 174 | ```ruby 175 | Cache.new(key_prefix: :class_name) { 'hello world' } 176 | Cache.backend.keys # => ["CacheTest_22abcc"] 177 | ``` 178 | 179 | You can also define these options globally: 180 | 181 | ```ruby 182 | Cache.default_key_prefix = :method_name 183 | ``` 184 | 185 | #### redis replicas 186 | 187 | Before, we used the following setup to connect `Object::Cache` to a redis 188 | backend: 189 | 190 | ```ruby 191 | Cache.backend = Redis.new 192 | ``` 193 | 194 | The Ruby Redis library has primary/replicas support [built-in using Redis 195 | Sentinel][sentinel]. 196 | 197 | If however, you have your own setup, and want the writes and reads to be 198 | separated between different Redis instances, you can pass in a hash to the 199 | backend config, with a `primary` and `replicas` key: 200 | 201 | ```ruby 202 | Cache.backend = { primary: Redis.new, replicas: [Redis.new, Redis.new] } 203 | ``` 204 | 205 | When writing the initial object to the backend, the `primary` Redis is used. On 206 | subsequent requests, a random replica is used to retrieve the stored value. 207 | 208 | The above example obviously only works if the replicas receive the written data 209 | from the primary instance. 210 | 211 | #### core extension 212 | 213 | Finally, if you want, you can use the `cache` method, for convenient 214 | access to the cache object: 215 | 216 | ```ruby 217 | require 'object/cache/core_extension' 218 | 219 | # these are the same: 220 | cache('hello', ttl: 60) { 'hello world' } 221 | Cache.new('hello', ttl: 60) { 'hello world' } 222 | ``` 223 | 224 | That's it! 225 | 226 | ## License 227 | 228 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 229 | 230 | [marshal]: http://ruby-doc.org/core-2.3.0/Marshal.html 231 | [sentinel]: https://github.com/redis/redis-rb#sentinel-support 232 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rake/testtask' 5 | require 'rubocop/rake_task' 6 | 7 | RuboCop::RakeTask.new do |t| 8 | t.options = %w[--display-cop-names --extra-details --display-style-guide] 9 | end 10 | 11 | Rake::TestTask.new(:test) do |t| 12 | t.libs << 'test' 13 | t.libs << 'lib' 14 | t.test_files = FileList['test/**/*_test.rb'] 15 | end 16 | 17 | task default: %i[test rubocop] 18 | -------------------------------------------------------------------------------- /lib/object/cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'digest/sha1' 4 | require 'object/cache/version' 5 | 6 | # Caching of objects in a Redis store 7 | class Cache 8 | @default_ttl = (7 * 24 * 60 * 60) # 1 week 9 | 10 | class << self 11 | attr_accessor :backend 12 | attr_accessor :default_ttl 13 | attr_accessor :default_key_prefix 14 | 15 | # new 16 | # 17 | # Finds the correct value (based on the provided key) in the cache store, or 18 | # calls the original code, and stores the result in cache. 19 | # 20 | # The TTL of the cached content is provided with the optional `ttl` named 21 | # argument. If left blank, the `DEFAULT_TTL` ttl value will be used. 22 | # 23 | # The caching key will be determined by creating a SHA digest of the 24 | # original code's file location and line number within that file. This makes 25 | # it easier to provide short caching keys like uid's, or ids, and still 26 | # receive a unique caching key under which the data is stored. 27 | # 28 | # The cache key can optionally be left blank. This should **only be done** 29 | # if the provided data by the method will never changes based on some form 30 | # of input. 31 | # 32 | # For example: caching an `Item` should _always_ be done by providing a 33 | # unique item identifier as the caching key, otherwise the cache will return 34 | # the same item every time, even if a different one is stored the second 35 | # time. 36 | # 37 | # good: 38 | # 39 | # Cache.new { 'hello world' } # stored object is always the same 40 | # Cache.new(item.id) { item } # stored item is namespaced using its id 41 | # 42 | # bad: 43 | # 44 | # Cache.new { item } # item is only stored once, and then always 45 | # # retrieved, even if it is a different item 46 | # 47 | def new(key = nil, ttl: default_ttl, key_prefix: default_key_prefix, &block) 48 | return yield unless replica 49 | 50 | begin 51 | key = build_key(key, key_prefix, block) 52 | 53 | if (cached_value = replica.get(key)).nil? 54 | yield.tap do |value| 55 | begin 56 | update_cache(key, value, ttl: ttl) 57 | rescue TypeError 58 | # if `TypeError` is raised, the data could not be Marshal dumped. In that 59 | # case, delete anything left in the cache store, and get the data without 60 | # caching. 61 | # 62 | delete(key) 63 | end 64 | end 65 | else 66 | begin 67 | Marshal.load(cached_value) 68 | rescue 69 | delete(key) 70 | yield 71 | end 72 | end 73 | end 74 | end 75 | 76 | def include?(key) 77 | replica.exists?(key) 78 | rescue 79 | false 80 | end 81 | 82 | def delete(key) 83 | return false unless include?(key) 84 | 85 | primary.del(key) 86 | true 87 | end 88 | 89 | def update_cache(key, value, ttl: default_ttl) 90 | return unless primary && (value = Marshal.dump(value)) 91 | 92 | ttl.to_i.zero? ? primary.set(key, value) : primary.setex(key, ttl.to_i, value) 93 | end 94 | 95 | def primary 96 | backend.is_a?(Hash) ? backend[:primary] : backend 97 | end 98 | 99 | def replicas 100 | replicas = backend.is_a?(Hash) ? backend[:replicas] : backend 101 | replicas.respond_to?(:sample) ? replicas : [replicas] 102 | end 103 | 104 | def replica 105 | replicas.sample 106 | end 107 | 108 | def build_key(key, key_prefix, proc) 109 | hash = Digest::SHA1.hexdigest([key, proc.source_location].flatten.join)[0..11] 110 | prefix = build_key_prefix(key_prefix, proc) 111 | 112 | [prefix, hash].compact.join('_') 113 | end 114 | 115 | def build_key_prefix(key_prefix, proc) 116 | case key_prefix 117 | when :method_name 118 | location = caller_locations.find { |l| proc.source_location.join == "#{l.path}#{l.lineno}" } 119 | location&.base_label 120 | when :class_name 121 | proc.binding.receiver.class.to_s 122 | else 123 | key_prefix 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/object/cache/core_extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'object/cache' 4 | 5 | # :no-doc: 6 | module Kernel 7 | def cache(key = nil, **options, &block) 8 | Cache.new(key, **options, &block) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/object/cache/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Cache 4 | VERSION = '0.1.1' 5 | end 6 | -------------------------------------------------------------------------------- /object-cache.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'object/cache' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'object-cache' 9 | spec.version = Cache::VERSION 10 | spec.authors = %w[Jean Mertz] 11 | spec.email = %w[jean@mertz.fm] 12 | 13 | spec.summary = 'Caching of objects, using a Redis store.' 14 | spec.description = 'Easily cache objects in Ruby, using a Redis store backend' 15 | spec.homepage = 'https://github.com/blendle/object-cache' 16 | spec.license = 'MIT' 17 | spec.files = `git ls-files -z`.split("\x0") 18 | spec.require_paths = %w[lib] 19 | 20 | spec.add_development_dependency 'bundler', '~> 2.1' 21 | spec.add_development_dependency 'm', '~> 1.5' 22 | spec.add_development_dependency 'minitest' 23 | spec.add_development_dependency 'mock_redis', '~> 0.16' 24 | spec.add_development_dependency 'pry' 25 | spec.add_development_dependency 'rake' 26 | spec.add_development_dependency 'rubocop' 27 | end 28 | -------------------------------------------------------------------------------- /test/cache_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('../lib', __dir__) 4 | 5 | require 'mock_redis' 6 | require 'object/cache' 7 | require 'minitest/autorun' 8 | 9 | # :no-doc: 10 | class CacheTest < Minitest::Test # rubocop:disable Metrics/ClassLength 11 | def setup 12 | Cache.backend = MockRedis.new 13 | Cache.default_ttl = 604_800 14 | end 15 | 16 | def redis 17 | Cache.primary 18 | end 19 | 20 | def key_to_value(key) 21 | Marshal.load(redis.get(key)) 22 | end 23 | 24 | def test_cache_returns_object 25 | assert_equal('hello world', Cache.new { 'hello world' }) 26 | end 27 | 28 | def test_cache_stores_object 29 | Cache.new { 'hello world' } 30 | assert redis.keys.one? 31 | end 32 | 33 | def test_cache_stores_correct_value 34 | assert_equal Cache.new { 'hello world' }, key_to_value(redis.keys.first) 35 | end 36 | 37 | def test_does_not_cache_non_marshalable_objects 38 | Cache.new { -> { 'hello world' } } 39 | assert redis.keys.empty? 40 | end 41 | 42 | def test_return_original_value_for_non_marshalable_objects 43 | assert_equal 'hello world', Cache.new { -> { 'hello world' } }.call 44 | end 45 | 46 | def test_returns_cache_on_same_file_line_without_custom_key 47 | Cache.new { 'hello world' } && Cache.new { 'hello universe' } 48 | assert_equal 'hello world', key_to_value(redis.keys.first) 49 | end 50 | 51 | def test_store_multiple_objects_cached_in_same_file_but_different_lines 52 | Cache.new { 'hello world' } 53 | Cache.new { 'hello universe' } 54 | 55 | assert_equal 'hello world', key_to_value(redis.keys.first) 56 | assert_equal 'hello universe', key_to_value(redis.keys.last) 57 | end 58 | 59 | def test_custom_cache_key_on_same_file_and_line 60 | Cache.new('hello') { 'world' } && Cache.new('hi') { 'world' } 61 | assert_equal 2, redis.keys.count 62 | end 63 | 64 | def test_custom_cache_key_on_same_file_but_different_lines 65 | Cache.new('hello') { 'world' } 66 | Cache.new('hi') { 'world' } 67 | assert_equal 2, redis.keys.count 68 | end 69 | 70 | def test_cache_without_ttl 71 | Cache.new(ttl: nil) { 'hello world' } 72 | assert_equal(-1, redis.ttl(redis.keys.first)) 73 | end 74 | 75 | def test_cache_without_ttl_using_zero 76 | Cache.new(ttl: 0) { 'hello world' } 77 | assert_equal(-1, redis.ttl(redis.keys.first)) 78 | end 79 | 80 | def test_cache_default_ttl 81 | Cache.new { 'hello world' } 82 | assert_equal Cache.default_ttl, redis.ttl(redis.keys.first) 83 | end 84 | 85 | def test_cache_custom_default_ttl 86 | Cache.default_ttl = 60 87 | Cache.new { 'hello world' } 88 | assert_equal 60, redis.ttl(redis.keys.first) 89 | end 90 | 91 | def test_cache_with_ttl 92 | Cache.new(ttl: 60) { 'hello world' } 93 | assert_equal 60, redis.ttl(redis.keys.first) 94 | end 95 | 96 | def test_core_extension 97 | load 'object/cache/core_extension.rb' 98 | assert_equal('hello world', cache { 'hello world' }) 99 | assert Kernel.send(:remove_method, :cache) 100 | end 101 | 102 | def test_core_extension_options 103 | load 'object/cache/core_extension.rb' 104 | cache(ttl: 60) { 'hello world' } 105 | assert_equal 60, redis.ttl(redis.keys.first) 106 | assert Kernel.send(:remove_method, :cache) 107 | end 108 | 109 | def test_backend_with_replicas 110 | Cache.backend = { primary: redis, replicas: [redis, redis] } 111 | 112 | Cache.new { 'hello world' } && assert_equal('hello world', Cache.new { 'hello world' }) 113 | end 114 | 115 | def test_backend_with_primary_without_replicas 116 | Cache.backend = { primary: MockRedis.new } 117 | 118 | Cache.new { 'hello world' } && assert_equal('hello world', Cache.new { 'hello world' }) 119 | end 120 | 121 | def test_backend_with_primary_and_single_replica 122 | redis = MockRedis.new 123 | Cache.backend = { primary: redis, replicas: redis } 124 | 125 | Cache.new { 'hello world' } && assert_equal('hello world', Cache.new { 'hello world' }) 126 | end 127 | 128 | def test_backend_with_replicas_not_having_primary_data 129 | primary = MockRedis.new 130 | replica = MockRedis.new 131 | Cache.backend = { primary: primary, replicas: replica } 132 | 133 | Cache.new { 'hello world' } && Cache.new { 'hello world' } 134 | 135 | assert_equal 1, primary.keys.count 136 | assert_equal 0, replica.keys.count 137 | end 138 | 139 | def test_default_key_prefix_custom 140 | Cache.default_key_prefix = 'hello' 141 | 142 | Cache.new { 'hello world' } 143 | assert_match(/^hello/, redis.keys.first) 144 | end 145 | 146 | def test_default_key_prefix_method_name 147 | Cache.default_key_prefix = :method_name 148 | 149 | Cache.new { 'hello world' } 150 | assert_match(/^test_default_key_prefix_method_name/, redis.keys.first) 151 | end 152 | 153 | def test_default_key_prefix_class_name 154 | Cache.default_key_prefix = :class_name 155 | 156 | Cache.new { 'hello world' } 157 | assert_match(/^CacheTest/, redis.keys.first) 158 | end 159 | 160 | def test_key_prefix_custom 161 | Cache.new(key_prefix: 'hello') { 'hello world' } 162 | assert_match(/^hello/, redis.keys.first) 163 | end 164 | 165 | def test_key_prefix_method_name 166 | Cache.new(key_prefix: :method_name) { 'hello world' } 167 | assert_match(/^test_key_prefix_method_name/, redis.keys.first) 168 | end 169 | 170 | def test_key_prefix_class_name 171 | Cache.new(key_prefix: :class_name) { 'hello world' } 172 | assert_match(/^CacheTest/, redis.keys.first) 173 | end 174 | 175 | def test_unset_backend 176 | Cache.backend = nil 177 | val = 0 178 | block = -> { val += 1 } 179 | Cache.new(&block) 180 | Cache.backend = MockRedis.new 181 | 182 | assert_equal 1, val 183 | end 184 | 185 | def test_unset_backend_raising_type_error 186 | Cache.backend = nil 187 | val = 0 188 | begin 189 | Cache.new do 190 | val += 1 191 | raise TypeError 192 | end 193 | rescue 194 | nil 195 | end 196 | 197 | Cache.backend = MockRedis.new 198 | assert_equal 1, val 199 | end 200 | 201 | def test_single_yield_on_failure 202 | val = 0 203 | begin 204 | Cache.new do 205 | val += 1 206 | raise TypeError 207 | end 208 | rescue 209 | nil 210 | end 211 | 212 | assert_equal 1, val 213 | end 214 | 215 | def test_yield_when_marshal_load_fails 216 | testing = -> { Cache.new(key_prefix: 'marshal') { 'hello world' } } 217 | 218 | assert_equal 'hello world', testing.call 219 | redis.set(redis.keys('marshal*').first, 'garbage') 220 | assert_equal 'hello world', testing.call 221 | assert_empty redis.keys('marshal*') 222 | end 223 | end 224 | --------------------------------------------------------------------------------