diff --git a/CHANGELOG.md b/CHANGELOG.md index bc76084..9d26c31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Changelog All notable changes to this project will be documented in this file. +## [0.3.1] - 2018-10-08 +### Added +- patch interface `AnyCache.enable_patch!(:patch_series_name)`: + - `ActiveSupport::Cache::DalliStore` patch: now the `#fetch` method provides + a cache key attribute to the fallback proc (can be enabled via `.enable(:dalli_store)`); + ## [0.3.0] - 2018-10-06 ### Added - support for `ActiveSupport::Cache::DalliStore` client; diff --git a/README.md b/README.md index c6e81ca..ef7062c 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ require 'any_cache' - [Persist](#persist) - [Existence](#existence) - [Clear](#clear) +- [Roadmap](#roadmap) --- @@ -195,6 +196,8 @@ require 'dalli' require 'active_support' require 'any_cache' +AnyCache.enable_patch!(:dalli_store) + AnyCache.configure do |conf| conf.driver = :as_dalli_store conf.as_dalli_store.servers = ... # string or array of strings diff --git a/lib/any_cache.rb b/lib/any_cache.rb index ec82d0c..8e1bf89 100644 --- a/lib/any_cache.rb +++ b/lib/any_cache.rb @@ -14,12 +14,14 @@ class AnyCache require_relative 'any_cache/adapters' require_relative 'any_cache/logging' require_relative 'any_cache/delegation' + require_relative 'any_cache/patches' # @since 0.2.0 include Qonfig::Configurable - # @since 0.3.0 include Delegation + # @since 0.3.1 + extend Patches::InterfaceAccessMixin # @since 0.2.0 # rubocop:disable Metrics/BlockLength diff --git a/lib/any_cache/error.rb b/lib/any_cache/error.rb index ce3edd8..a677773 100644 --- a/lib/any_cache/error.rb +++ b/lib/any_cache/error.rb @@ -8,4 +8,7 @@ class AnyCache # @since 0.1.0 UnsupportedDriverError = Class.new(Error) + + # @since 0.3.1 + NonexistentPatchError = Class.new(ArgumentError) end diff --git a/lib/any_cache/patches.rb b/lib/any_cache/patches.rb new file mode 100644 index 0000000..a8ecf32 --- /dev/null +++ b/lib/any_cache/patches.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# @api private +# @since 0.3.1 +module AnyCache::Patches + # @since 0.3.1 + require_relative 'patches/dalli_store' + + class << self + # @param patch_series [Symbol, String] + # @return [void] + # + # @raise [AnyCache::NonexistentPatchError] + # + # @api private + # @since 0.3.1 + def enable!(patch_series) + case patch_series + when :dalli_store, 'dalli_store' + AnyCache::Patches::DalliStore.enable! + else + raise AnyCache::NonexistentPatchError, "Can't enable nonexistnet patch! (#{patch_series})" + end + end + end + + # @api private + # @since 0.3.1 + module InterfaceAccessMixin + # @param patch_series [Symbol, String] + # @return [void] + # + # @see AnyCache::Patches#enable! + # + # @api private + # @since 0.3.1 + def enable_patch!(patch_series) + AnyCache::Patches.enable!(patch_series) + end + end +end diff --git a/lib/any_cache/patches/dalli_store.rb b/lib/any_cache/patches/dalli_store.rb new file mode 100644 index 0000000..62bac93 --- /dev/null +++ b/lib/any_cache/patches/dalli_store.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +# @api private +# @since 0.3.1 +module AnyCache::Patches::DalliStore + class << self + # @return [void] + # + # @api private + # @since 0.3.1 + def enable! + defined?(::Dalli) && + defined?(::ActiveSupport::Cache::DalliStore) && + Gem::Version.new(::Dalli::VERSION) <= Gem::Version.new('2.7.8') && + ::ActiveSupport::Cache::DalliStore.prepend(ActiveSupportFetchWithKey) + end + end + + # @api private + # @since 0.3.1 + module ActiveSupportFetchWithKey + # NOTE: original fetch implementation with my own little fix (see #43 line of code below) + # rubocop:disable all + def fetch(name, options=nil) + options ||= {} + options[:cache_nils] = true if @options[:cache_nils] + namespaced_name = namespaced_key(name, options) + not_found = options[:cache_nils] ? Dalli::Server::NOT_FOUND : nil + if block_given? + entry = not_found + unless options[:force] + entry = instrument_with_log(:read, namespaced_name, options) do |payload| + read_entry(namespaced_name, options).tap do |result| + if payload + payload[:super_operation] = :fetch + payload[:hit] = not_found != result + end + end + end + end + + if not_found == entry + result = instrument_with_log(:generate, namespaced_name, options) do |payload| + # FIX: https://github.com/petergoldstein/dalli/pull/701 + yield(name) + # FIX: https://github.com/petergoldstein/dalli/pull/701 + end + write(name, result, options) + result + else + instrument_with_log(:fetch_hit, namespaced_name, options) { |payload| } + entry + end + else + read(name, options) + end + end + # rubocop:enable all + end +end + diff --git a/lib/any_cache/version.rb b/lib/any_cache/version.rb index 6bc435a..41e52d2 100644 --- a/lib/any_cache/version.rb +++ b/lib/any_cache/version.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true class AnyCache + # @return [String] + # + # @api public # @since 0.1.0 VERSION = '0.3.0' end diff --git a/spec/features/fetch_multi_spec.rb b/spec/features/fetch_multi_spec.rb index 6122ce0..bd10200 100644 --- a/spec/features/fetch_multi_spec.rb +++ b/spec/features/fetch_multi_spec.rb @@ -44,23 +44,12 @@ "#{key}-#{data_stub}" end - unless test_as_dalli_store_cache? # TODO: remove this condition in future - # NOTE: entry_2 has especial dynamically calculated value - expect(data).to match( - entry_1[:key] => entry_1[:value], - entry_2[:key] => "#{entry_2[:key]}-#{data_stub}", - entry_3[:key] => entry_3[:value] - ) - end - - if test_as_dalli_store_cache? # TODO: remove this block of code in future - # NOTE: entry_2 has especial dynamically calculated value - expect(data).to match( - entry_1[:key] => entry_1[:value], - entry_2[:key] => "-#{data_stub}", - entry_3[:key] => entry_3[:value] - ) - end + # NOTE: entry_2 has especial dynamically calculated value + expect(data).to match( + entry_1[:key] => entry_1[:value], + entry_2[:key] => "#{entry_2[:key]}-#{data_stub}", + entry_3[:key] => entry_3[:value] + ) # NOTE: force rewrite data_stub = SecureRandom.hex(4) @@ -73,23 +62,12 @@ "#{key}-#{data_stub}" end - unless test_as_dalli_store_cache? # TODO: remove this condition in future - # NOTE: entries with new values (and expiration time) - expect(cache_store.fetch_multi(entry_1[:key], entry_2[:key], entry_3[:key])).to match( - entry_1[:key] => "#{entry_1[:key]}-#{data_stub}", - entry_2[:key] => "#{entry_2[:key]}-#{data_stub}", - entry_3[:key] => entry_3[:value] - ) - end - - if test_as_dalli_store_cache? # TODO: remove this block of code in future - # NOTE: entries with new values (and expiration time) - expect(cache_store.fetch_multi(entry_1[:key], entry_2[:key], entry_3[:key])).to match( - entry_1[:key] => "-#{data_stub}", - entry_2[:key] => "-#{data_stub}", - entry_3[:key] => entry_3[:value] - ) - end + # NOTE: entries with new values (and expiration time) + expect(cache_store.fetch_multi(entry_1[:key], entry_2[:key], entry_3[:key])).to match( + entry_1[:key] => "#{entry_1[:key]}-#{data_stub}", + entry_2[:key] => "#{entry_2[:key]}-#{data_stub}", + entry_3[:key] => entry_3[:value] + ) sleep(expires_in + 1) diff --git a/spec/features/patches_interface_spec.rb b/spec/features/patches_interface_spec.rb new file mode 100644 index 0000000..50c5c74 --- /dev/null +++ b/spec/features/patches_interface_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +describe 'Patches interface' do + describe 'patch activation' do + it 'fails when the required patch does not exist' do + random_nonexistent_patch_name = SecureRandom.hex(rand(1..4)).to_sym + + expect do + AnyCache.enable_patch!(random_nonexistent_patch_name) + end.to raise_error(AnyCache::NonexistentPatchError) + end + end +end diff --git a/spec/support/spec_support/cache/active_support_dalli_store.rb b/spec/support/spec_support/cache/active_support_dalli_store.rb index 94c5bf4..8490bef 100644 --- a/spec/support/spec_support/cache/active_support_dalli_store.rb +++ b/spec/support/spec_support/cache/active_support_dalli_store.rb @@ -13,6 +13,7 @@ class CacheStore < AnyCache class << self def build load_dependencies! + enable_patches! build_cache_store end @@ -24,6 +25,10 @@ def load_dependencies! require 'active_support/cache/dalli_store' end + def enable_patches! + AnyCache.enable_patch!(:dalli_store) + end + def build_cache_store CacheStore.build end