diff --git a/CHANGELOG.md b/CHANGELOG.md index e9e060f..e12249c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] - `AnyCache#cleanup` - remove expired entries manually (make sence only for `:as_file_store` and `:as_memory_store` at this moment); +- automatic object marshaling (used in `fetch`, `fetch_multi`, `write`, `write_multi`, `read`, `read_multi`): + - used by default (`raw: false`); + - can be disabled via `raw: true` option; + - `raw: true` is required for incrementable/decrementable entries; ## [0.3.1] - 2018-10-08 ### Added diff --git a/README.md b/README.md index 930d279..90c732f 100644 --- a/README.md +++ b/README.md @@ -353,10 +353,10 @@ params: INSPECTED_ARGUMENTS and options: INSPECTED_OPTIONS ```ruby any_cache.write("data", 123, expires_in: 60) -# I, [2018-09-07T10:04:56.649960 #15761] INFO -- [AnyCache/Activity]: performed operation with params: ["data", 123] and options: {:expires_in=>60}. +# I, [2018-09-07T10:04:56.649960 #15761] INFO -- [AnyCache/Activity]: performed operation with attributes: ["data", 123] and options: {:expires_in=>60}. any_cache.clear -# I, [2018-09-07T10:05:26.999847 #15761] INFO -- [AnyCache/Activity]: performed operation with params: [] and options: {}. +# I, [2018-09-07T10:05:26.999847 #15761] INFO -- [AnyCache/Activity]: performed operation with attributes: [] and options: {}. ``` ## Operations @@ -382,6 +382,7 @@ any_cache.clear - fetches data from the cache using the given key; - if a `fallback` block has been passed and data with the given key does not exist - that block will be called with the given key and the return value will be written to the cache; + - use `raw: true` if you want to fetch incrementable/decrementable entry; ```ruby # --- entry exists --- @@ -416,6 +417,7 @@ cache_store.fetch("data") # => nil - get a set of entries in hash form from the cache storage using given keys; - works in `#fetch` manner but with a series of entries; - nonexistent entries will be fetched with `nil` values; + - use `raw: true` if you want to fetch incrementable/decrementable entries; ```ruby # --- fetch entries --- @@ -451,6 +453,7 @@ cache_store.fetch_multi("data", "second_data", "last_data", force: true) { |key| ### Read - `AnyCache#read(key)` - get an entry value from the cache storage + - pass `raw: true` if you want to read incrementable/decrementable entries; ```ruby # --- entry exists --- @@ -458,6 +461,9 @@ cache_store.read("data") # => "some_data" # --- entry doesnt exist --- cache_store.read("data") # => nil + +# --- read incrementable/decrementable entry --- +cache_store.read("data", raw: true) # => "2" (for example) ``` --- @@ -467,6 +473,7 @@ cache_store.read("data") # => nil - `AnyCache#read_multi(*keys)` - get entries from the cache storage in hash form; - nonexistent entries will be fetched with `nil` values; + - pass `raw: true` if you want to read incrementable/decrementable entries; ```ruby cache_store.read_multi("data", "another_data", "last_data", "super_data") @@ -477,13 +484,22 @@ cache_store.read_multi("data", "another_data", "last_data", "super_data") "last_data" => "some_data", # exisitng enry "super_data" => nil # existing entry } + +# --- read incrementable/decrementable entries --- +cache_store.read_multi("data", "another_data", raw: true) +# => returns +{ + "data" => "1", + "another_data" => "2", +} ``` --- ### Write -- `AnyCache#write(key, value, [expires_in:])` - write a new entry to the cache storage +- `AnyCache#write(key, value, [expires_in:])` - write a new entry to the cache storage; + - pass `raw: true` if you want to store incrementable/decrementable entries; ```ruby # --- permanent entry --- @@ -491,23 +507,30 @@ cache_store.write("data", 123) # --- temporal entry (expires in 60 seconds) --- cache_store.write("data", 123, expires_in: 60) + +# --- incrementable/decrementable entry --- +cache_store.write("data", 123, raw: true) ``` --- ### Write Multi -- `AnyCache#write_multi(**entries)` - write a set of permanent entries to the cache storage +- `AnyCache#write_multi(**entries)` - write a set of permanent entries to the cache storage; + - pass `raw: true` if you want to store incrementable/decrementable entries; ```ruby cache_store.write_multi("data" => "test", "another_data" => 123) + +# --- incrementable/decrementable entries --- +cache_store.write_multi("data" => 1, "another_data" => 2, raw: true) ``` --- ### Delete -- `AnyCache#delete(key)` - remove entry from the cache storage +- `AnyCache#delete(key)` - remove entry from the cache storage; ```ruby cache_store.delete("data") @@ -519,7 +542,7 @@ cache_store.delete("data") - `AnyCache#delete_matched(pattern)` - removes all entries with keys matching the pattern; - - currently unsupported: `:dalli`, `:as_mem_cache_store`, `:as_dalli_Store`; + - currently unsupported: `:dalli`, `:as_mem_cache_store`, `:as_dalli_store`; ```ruby # --- using a regepx --- @@ -534,11 +557,12 @@ cache_store.delete_matched("data") ### Increment - `AnyCache#increment(key, amount = 1, [expires_in:])` - increment entry's value by the given amount - and set the new expiration time if needed + and set the new expiration time if needed; + - can increment only nonexistent entries OR entries that were written with `raw: true` option; ```ruby # --- increment existing entry --- -cache_store.write("data", 1) +cache_store.write("data", 1, raw: true) # you must provide :raw => true for incrementable entries # --- increment by default value (1) --- cache_store.increment("data") # => 2 @@ -551,6 +575,9 @@ cache_store.incrmeent("data", expires_in: 31) # => 15 # --- increment nonexistent entry (create new entry) --- cache_store.increment("another_data", 5, expires_in: 5) # => 5 + +# --- read incrementable entry --- +cache_store.read("data", raw: true) # you must provide :raw => true for incrementable entries ``` --- @@ -558,11 +585,12 @@ cache_store.increment("another_data", 5, expires_in: 5) # => 5 ### Decrement - `AnyCache#decrement(key, amount = 1, [expires_in:])` - decrement entry's value by the given amount - and set the new expiration time if needed + and set the new expiration time if needed; + - can decrement only nonexistent entries OR entries that were written with `raw: true` option; ```ruby # --- decrement existing entry --- -cache_store.write("data", 15) +cache_store.write("data", 15, raw: true) # you must provide :raw => true for decrementable entries # --- decrement by default value (1) --- cache_store.decrement("data") # => 14 @@ -575,13 +603,16 @@ cache_store.decrememnt("data", expirs_in: 5) # => 3 # --- decrement nonexistent entry (create new entry) --- cache_store.decrememnt("another_data", 2, expires_in: 10) # => -2 (or 0 for Dalli::Client) + +# --- read decrementable entry --- +cache_store.read("data", raw: true) # you must provide :raw => true for decrementable entries ``` --- ### Expire -- `AnyCache#expire(key, [expires_in:])` - expire entry immediately or set the new expiration time +- `AnyCache#expire(key, [expires_in:])` - expire entry immediately or set the new expiration time; ```ruby # --- expire immediately --- @@ -595,7 +626,7 @@ cache_store.expire("data", expires_in: 36) ### Persist -- `AnyCache#persist(key)` - change entry's expiration time to permanent +- `AnyCache#persist(key)` - change entry's expiration time to permanent; ```ruby # --- create temporal entry (30 seconds) --- @@ -609,7 +640,7 @@ cache_store.persist("data") ### Existence -- `AnyCache#exist?(key)` - determine if an entry exists +- `AnyCache#exist?(key)` - determine if an entry exists; ```ruby # --- entry exists --- @@ -623,7 +654,7 @@ cache_store.exist?("another-data") # => false ### Clear -- `AnyCache#clear()` - clear cache database +- `AnyCache#clear()` - clear cache database; ```ruby # --- prepare cache data --- @@ -644,7 +675,8 @@ cache_store.read("another_data") # => nil ### Cleanup -- `AnyCache#cleanup()` - remove expired entries from cache database (make sense only for `:as_file_store` and `:as_memory_store` cache clients) +- `AnyCache#cleanup()` - remove expired entries from cache database + (make sense only for `:as_file_store` and `:as_memory_store` cache clients); ```ruby # --- prepare cache data --- diff --git a/any_cache.gemspec b/any_cache.gemspec index 7726b73..4904dc6 100644 --- a/any_cache.gemspec +++ b/any_cache.gemspec @@ -32,7 +32,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'coveralls', '~> 0.8' spec.add_development_dependency 'simplecov', '~> 0.16' - spec.add_development_dependency 'armitage-rubocop', '~> 0.12' + spec.add_development_dependency 'armitage-rubocop', '~> 0.13' spec.add_development_dependency 'rspec', '~> 3.8' spec.add_development_dependency 'bundler' diff --git a/bin/rspec b/bin/rspec index 70ab5f0..6b67e39 100755 --- a/bin/rspec +++ b/bin/rspec @@ -56,7 +56,7 @@ module AnyCacheSpecRunner ) { run_as_memory_store_cache_specs! } opts.on( - '--test-as-mem-cache-store', + '--test-as-memcache-store', 'Run specs with ActiveSupport::Cache::MemCacheStore cache storage' ) { run_as_mem_cache_store_cache_specs! } diff --git a/lib/any_cache.rb b/lib/any_cache.rb index ea329c5..0ad7826 100644 --- a/lib/any_cache.rb +++ b/lib/any_cache.rb @@ -10,6 +10,7 @@ class AnyCache require_relative 'any_cache/version' require_relative 'any_cache/error' + require_relative 'any_cache/dumper' require_relative 'any_cache/drivers' require_relative 'any_cache/adapters' require_relative 'any_cache/logging' diff --git a/lib/any_cache/adapters/active_support_dalli_store.rb b/lib/any_cache/adapters/active_support_dalli_store.rb index 65f68cb..2804f7d 100644 --- a/lib/any_cache/adapters/active_support_dalli_store.rb +++ b/lib/any_cache/adapters/active_support_dalli_store.rb @@ -49,25 +49,25 @@ def supported_driver?(driver) def_delegators :driver, :delete, :clear, :cleanup # @param key [String] - # @param options [Hash] + # @option raw [Boolean] # @return [void] # # @api private # @since 0.3.0 def read(key, **options) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) driver.read(key, raw: raw) end # @param keys [Array] - # @param options [Hash] + # @option raw [Boolean] # @return [Hash] # # @api private # @since 0.3.0 def read_multi(*keys, **options) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) driver.read_multi(*keys, raw: raw).tap do |entries| entries.merge!(Hash[(keys - entries.keys).zip(READ_MULTI_EMPTY_KEYS_SET)]) @@ -77,25 +77,26 @@ def read_multi(*keys, **options) # @param key [String] # @param value [Object] # @option expires_in [NilClass, Integer] Time in seconds + # @option raw [Boolean] # @return [void] # # @api private # @since 0.3.0 def write(key, value, **options) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) driver.write(key, value, expires_in: expires_in, raw: raw) end # @param entries [Hash] - # @param options [Hash] + # @option raw [Boolean] # @return [void] # # @api private # @since 0.3.0 def write_multi(entries, **options) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) # NOTE: ActiveSupport::Cache::DalliStore does not support #write_multi :\ entries.each_pair do |key, value| @@ -106,6 +107,7 @@ def write_multi(entries, **options) # @param key [String] # @option expires_in [Integer] # @option force [Boolean, Proc] + # @option raw [Boolean] # @return [Object] # # @api private @@ -114,8 +116,9 @@ def fetch(key, **options, &fallback) force_rewrite = options.fetch(:force, false) force_rewrite = force_rewrite.call(key) if force_rewrite.respond_to?(:call) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) + raw = options.fetch(:raw, false) - driver.fetch(key, force: force_rewrite, expires_in: expires_in, &fallback) + driver.fetch(key, force: force_rewrite, expires_in: expires_in, raw: raw, &fallback) end # @param keys [Array] @@ -142,7 +145,7 @@ def increment(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) unless exist?(key) - write(key, amount, expires_in: expires_in) && amount + write(key, amount, expires_in: expires_in, raw: true) && amount else driver.increment(key, amount).tap do expire(key, expires_in: expires_in) if expires_in @@ -165,7 +168,8 @@ def decrement(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options) # - non-raw values; # - values lower than zero; # - empty entries; - write(key, INITIAL_DECREMNETED_VALUE, expires_in: expires_in) && INITIAL_DECREMNETED_VALUE + write(key, INITIAL_DECREMNETED_VALUE, expires_in: expires_in, raw: true) + INITIAL_DECREMNETED_VALUE else driver.decrement(key, amount).tap do expire(key, expires_in: expires_in) if expires_in @@ -190,9 +194,9 @@ def delete_matched(pattern, **options) # @api private # @since 0.3.0 def expire(key, expires_in: DEAD_TTL) - read(key).tap do |value| + read(key, raw: true).tap do |value| is_alive = expires_in ? expires_in.positive? : false - is_alive ? write(key, value, expires_in: expires_in) : delete(key) + is_alive ? write(key, value, expires_in: expires_in, raw: true) : delete(key) end end diff --git a/lib/any_cache/adapters/active_support_mem_cache_store.rb b/lib/any_cache/adapters/active_support_mem_cache_store.rb index 70c00a0..54bb824 100644 --- a/lib/any_cache/adapters/active_support_mem_cache_store.rb +++ b/lib/any_cache/adapters/active_support_mem_cache_store.rb @@ -51,25 +51,25 @@ def supported_driver?(driver) def_delegators :driver, :delete, :clear # @param key [String] - # @param options [Hash] + # @option raw [Boolean] # @return [Object] # # @api private # @since 0.2.0 def read(key, **options) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) driver.read(key, raw: raw) end # @param keys [Array] - # @param options [Hash] + # @option raw [Boolean] # @return [Hash] # # @api private # @since 0.3.0 def read_multi(*keys, **options) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) driver.read_multi(*keys, raw: raw).tap do |entries| entries.merge!(Hash[(keys - entries.keys).zip(READ_MULTI_EMPTY_KEYS_SET)]) @@ -79,25 +79,26 @@ def read_multi(*keys, **options) # @param key [String] # @param value [Object] # @option expires_in [NilClass, Integer] Time in seconds + # @option raw [Boolean] # @return [void] # # @api private # @sicne 0.2.0 def write(key, value, **options) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) driver.write(key, value, expires_in: expires_in, raw: raw) end # @param entries [Hash] - # @param options [Hash] + # @option raw [Boolean] # @return [void] # # @api private # @since 0.3.0 def write_multi(entries, **options) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) driver.write_multi(entries, expires_in: NO_EXPIRATION_TTL, raw: raw) end @@ -105,6 +106,7 @@ def write_multi(entries, **options) # @param key [String] # @option expires_in [Integer] # @option force [Boolean, Proc] + # @option raw [Boolean] # @return [Object] # # @api private @@ -113,8 +115,9 @@ def fetch(key, **options, &fallback) force_rewrite = options.fetch(:force, false) force_rewrite = force_rewrite.call(key) if force_rewrite.respond_to?(:call) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) + raw = options.fetch(:raw, false) - driver.fetch(key, force: force_rewrite, expires_in: expires_in, &fallback) + driver.fetch(key, force: force_rewrite, expires_in: expires_in, raw: raw, &fallback) end # @param keys [Array] @@ -151,7 +154,7 @@ def increment(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) unless exist?(key) - write(key, amount, expires_in: expires_in) && amount + write(key, amount, expires_in: expires_in, raw: true) && amount else driver.increment(key, amount).tap do expire(key, expires_in: expires_in) if expires_in @@ -174,7 +177,8 @@ def decrement(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options) # - non-raw values; # - values lower than zero; # - empty entries; - write(key, INITIAL_DECREMNETED_VALUE, expires_in: expires_in) && INITIAL_DECREMNETED_VALUE + write(key, INITIAL_DECREMNETED_VALUE, expires_in: expires_in, raw: true) + INITIAL_DECREMNETED_VALUE else driver.decrement(key, amount).tap do expire(key, expires_in: expires_in) if expires_in @@ -189,9 +193,9 @@ def decrement(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options) # @api private # @since 0.2.0 def expire(key, expires_in: DEAD_TTL) - read(key).tap do |value| + read(key, raw: true).tap do |value| is_alive = expires_in ? expires_in.positive? : false - is_alive ? write(key, value, expires_in: expires_in) : delete(key) + is_alive ? write(key, value, expires_in: expires_in, raw: true) : delete(key) end end diff --git a/lib/any_cache/adapters/active_support_redis_cache_store.rb b/lib/any_cache/adapters/active_support_redis_cache_store.rb index f1129ab..c6c0ac7 100644 --- a/lib/any_cache/adapters/active_support_redis_cache_store.rb +++ b/lib/any_cache/adapters/active_support_redis_cache_store.rb @@ -45,25 +45,25 @@ def supported_driver?(driver) def_delegators :driver, :delete, :delete_matched, :clear # @param key [String] - # @param options [Hash] + # @option raw [Boolean] # @return [Object] # # @api private # @since 0.1.0 def read(key, **options) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) driver.read(key, raw: raw) end # @param keys [Array] - # @param options [Hash] + # @option raw [Boolean] # @return [Hash] # # @api private # @since 0.3.0 def read_multi(*keys, **options) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) driver.read_multi(*keys, raw: raw).tap do |res| res.merge!(Hash[(keys - res.keys).zip(READ_MULTI_EMPTY_KEYS_SET)]) @@ -73,25 +73,26 @@ def read_multi(*keys, **options) # @param key [String] # @param value [Object] # @option expires_in [NilClass, Integer] Time in seconds + # @option raw [Boolean] # @return [void] # # @api private # @since 0.1.0 def write(key, value, **options) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) driver.write(key, value, expires_in: expires_in, raw: raw) end # @param entries [Hash] - # @param options [Hash] + # @option raw [Boolean] # @return [void] # # @api private # @sicne 0.3.0 def write_multi(entries, **options) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) driver.write_multi(entries, expires_in: NO_EXPIRATION_TTL, raw: raw) end @@ -100,6 +101,7 @@ def write_multi(entries, **options) # @param fallback [Proc] # @option expires_in [Integer] # @option force [Boolean, Proc] + # @option raw [Boolean] # @return [Object] # # @api private @@ -108,7 +110,7 @@ def fetch(key, **options, &fallback) force_rewrite = options.fetch(:force, false) force_rewrite = force_rewrite.call(key) if force_rewrite.respond_to?(:call) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) driver.fetch(key, force: force_rewrite, expires_in: expires_in, raw: raw, &fallback) end @@ -141,7 +143,7 @@ def increment(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) unless exist?(key) - write(key, amount, expires_in: expires_in) && amount + write(key, amount, expires_in: expires_in, raw: true) && amount else driver.increment(key, amount).tap do expire(key, expires_in: expires_in) if expires_in @@ -160,7 +162,7 @@ def decrement(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) unless exist?(key) - write(key, -amount, expires_in: expires_in) && -amount + write(key, -amount, expires_in: expires_in, raw: true) && -amount else driver.decrement(key, amount).tap do expire(key, expires_in: expires_in) if expires_in @@ -175,9 +177,12 @@ def decrement(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options) # @api private # @since 0.1.0 def expire(key, expires_in: DEAD_TTL) - read(key).tap do |value| + # NOTE: + # raw is true cuz we want the raw cached value. + # this raw value would be cached again if needed. + read(key, raw: true).tap do |value| is_alive = expires_in ? expires_in.positive? : false - is_alive ? write(key, value, expires_in: expires_in) : delete(key) + is_alive ? write(key, value, expires_in: expires_in, raw: true) : delete(key) end end diff --git a/lib/any_cache/adapters/basic.rb b/lib/any_cache/adapters/basic.rb index d93f0cd..7d366e8 100644 --- a/lib/any_cache/adapters/basic.rb +++ b/lib/any_cache/adapters/basic.rb @@ -6,6 +6,8 @@ module AnyCache::Adapters class Basic # @since 0.1.0 extend Forwardable + # @since 0.4.0 + include AnyCache::Dumper::InterfaceAccessMixin class << self # @param driver [Object] diff --git a/lib/any_cache/adapters/dalli.rb b/lib/any_cache/adapters/dalli.rb index c462213..387bf4f 100644 --- a/lib/any_cache/adapters/dalli.rb +++ b/lib/any_cache/adapters/dalli.rb @@ -57,29 +57,46 @@ def supported_driver?(driver) :flush # @param key [String] - # @param options [Hash] + # @option raw [Boolean] # @return [Object] # # @api private # @since 0.1.0 def read(key, **options) - get(key) + raw = options.fetch(:raw, false) + value = get(key) + + raw ? value : detransform_value(value) end # @param keys [Array] - # @param options [Hash] + # @option raw [Boolean] # @return [Hash] # # @api private # @since 0.3.0 def read_multi(*keys, **options) - get_multi(*keys).tap do |res| - res.merge!(Hash[(keys - res.keys).zip(READ_MULTI_EMPTY_KEYS_SET)]) + raw = options.fetch(:raw, false) + + entries = get_multi(*keys).tap do |res| + # NOTE: + # dalli does not return nonexistent entries + # but we want to be consistent with another cache storages + # that returns nonexistent antries as { key => nil } pair + res.merge!(Hash[(keys.map(&:to_s) - res.keys).zip(READ_MULTI_EMPTY_KEYS_SET)]) + + # NOTE: + # dalli stringifies requred keys but we want to be consistent with symbol keys + # cuz another cache storages are already consistent + keys.each { |key| res.key?(key) ? next : (res[key] = res.delete(key.to_s)) } end + + raw ? entries : detransform_pairset(entries) end # @param key [String] # @param value [Object] + # @option raw [Boolean] # @option expires_in [Integer] # @return [void] # @@ -87,7 +104,8 @@ def read_multi(*keys, **options) # @since 0.1.0 def write(key, value, **options) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) + value = transform_value(value) unless raw set(key, value, expires_in, raw: raw) end @@ -99,9 +117,7 @@ def write(key, value, **options) # @api private # @since 0.3.0 def write_multi(entries, **options) - raw = options.fetch(:raw, true) - - entries.each_pair { |key, value| write(key, value, raw: raw) } + entries.each_pair { |key, value| write(key, value, **options) } end # @param key [String] @@ -117,7 +133,7 @@ def fetch(key, **options, &fallback) force_rewrite = force_rewrite.call(key) if force_rewrite.respond_to?(:call) # NOTE: can conflict with :cache_nils Dalli::Client's config - read(key).tap { |value| return value if value } unless force_rewrite + read(key, **options).tap { |value| return value if value } unless force_rewrite yield(key).tap { |value| write(key, value, **options) } if block_given? end diff --git a/lib/any_cache/adapters/redis.rb b/lib/any_cache/adapters/redis.rb index 956ac6f..1cefccd 100644 --- a/lib/any_cache/adapters/redis.rb +++ b/lib/any_cache/adapters/redis.rb @@ -3,7 +3,7 @@ module AnyCache::Adapters # @api private # @since 0.1.0 - class Redis < Basic + class Redis < Basic # rubocop:disable Metrics/ClassLength class << self # @param driver [Object] # @return [Boolean] @@ -61,45 +61,57 @@ def supported_driver?(driver) :scan # @param key [String] - # @param options [Hash] + # @option raw [Boolean] # @return [Object] # # @api private # @since 0.1.0 def read(key, **options) - get(key) + value = get(key) + raw = options.fetch(:raw, false) + + raw ? value : detransform_value(value) end # @param keys [Array] - # @param options [Hash] + # @option raw [Boolean] # @return [Hash] # # @api private # @since 0.3.0 def read_multi(*keys, **options) - mapped_mget(*keys) + raw = options.fetch(:raw, false) + entries = mapped_mget(*keys) + + raw ? entries : detransform_pairset(entries) end # @param key [String] # @param value [Object] + # @option raw [Boolean] # @option expires_in [NilClass, Integer] Time in seconds # @return [void] # # @api private # @since 0.1.0 def write(key, value, **options) + raw = options.fetch(:raw, false) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) + value = transform_value(value) unless raw expires_in ? setex(key, expires_in, value) : set(key, value) end # @param entries [Hash] - # @param options [Hash] + # @option raw [Boolean] # @return [void] # # @api private # @since 0.3.0 def write_multi(entries, **options) + raw = options.fetch(:raw, false) + entries = transform_pairset(entries) unless raw + mapped_mset(entries) end @@ -115,7 +127,7 @@ def fetch(key, **options, &fallback) force_rewrite = force_rewrite.call(key) if force_rewrite.respond_to?(:call) # NOTE: think about #pipelined - read(key).tap { |value| return value if value } unless force_rewrite + read(key, **options).tap { |value| return value if value } unless force_rewrite yield(key).tap { |value| write(key, value, **options) } if block_given? end diff --git a/lib/any_cache/adapters/redis_store.rb b/lib/any_cache/adapters/redis_store.rb index 1039ece..c792162 100644 --- a/lib/any_cache/adapters/redis_store.rb +++ b/lib/any_cache/adapters/redis_store.rb @@ -18,15 +18,16 @@ def supported_driver?(driver) def_delegators :driver, :mset # @param key [String] - # @param options [Hash] + # @option raw [Boolean] # @return [Object] # # @api private # @since 0.1.0 def read(key, **options) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) + value = get(key, raw: true) - get(key, raw: raw) + raw ? value : detransform_value(value) end # @param keys [Array] @@ -43,24 +44,29 @@ def read_multi(*keys, **options) # @param key [String] # @param value [Object] # @option expires_in [NilClass, Integer] Time in seconds + # @option raw [Boolean] # @return [void] # # @api private # @since 0.1.0 def write(key, value, **options) expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL) - raw = options.fetch(:raw, true) + raw = options.fetch(:raw, false) + value = transform_value(value) unless raw - expires_in ? setex(key, expires_in, value, raw: raw) : set(key, value, raw: raw) + expires_in ? setex(key, expires_in, value, raw: true) : set(key, value, raw: true) end # @param entries [Hash] - # @param options [Hash] + # @option raw [Boolean] # @return [void] # # @api private # @since 0.3.0 def write_multi(entries, **options) + raw = options.fetch(:raw, false) + entries = transform_pairset(entries) unless raw + mset(*entries.to_a.flatten!, raw: true) end end diff --git a/lib/any_cache/delegation.rb b/lib/any_cache/delegation.rb index c4ab4c1..1448612 100644 --- a/lib/any_cache/delegation.rb +++ b/lib/any_cache/delegation.rb @@ -40,7 +40,7 @@ def def_loggable_delegator(receiver, delegat) AnyCache::Logging::Activity.log( self, logger, activity: delegat, message: "performed <#{delegat}> operation with " \ - "params: #{args.inspect} and options: #{opts.inspect}." + "attributes: #{args.inspect} and options: #{opts.inspect}." ) if logger end end diff --git a/lib/any_cache/dumper.rb b/lib/any_cache/dumper.rb new file mode 100644 index 0000000..37e4a74 --- /dev/null +++ b/lib/any_cache/dumper.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +# @api private +# @since 0.4.0 +module AnyCache::Dumper + require_relative 'dumper/interface_access_mixin' + + class << self + # @param hash [Hash] + # @return [Hash] + # + # @api private + # @since 0.4.0 + def transform_hash(hash) + {}.tap do |entries| + hash.each_pair do |key, value| + entries[key] = dump(value) + end + end + end + + # @param hash [Hash] + # @return [Hash] + # + # @api private + # @since 0.4.0 + def detransform_hash(hash) + {}.tap do |entries| + hash.each_pair do |key, value| + entries[key] = load(value) + end + end + end + + # @param value [Object] + # @return [String] + # + # @api private + # @since 0.4.0 + def dump(value) + return value if value.nil? + Zlib::Deflate.deflate(Marshal.dump(value)) + end + + # @param value [String] + # @return [Object] + # + # @api private + # @since 0.4.0 + def load(value) + return value if value.nil? + Marshal.load(Zlib::Inflate.inflate(value)) + end + end +end diff --git a/lib/any_cache/dumper/interface_access_mixin.rb b/lib/any_cache/dumper/interface_access_mixin.rb new file mode 100644 index 0000000..8c62364 --- /dev/null +++ b/lib/any_cache/dumper/interface_access_mixin.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# @api private +# @since 0.4.0 +module AnyCache::Dumper::InterfaceAccessMixin + # @param value [Object] + # @return [Object] + # + # @api private + # @since 0.4.0 + def transform_value(value) + AnyCache::Dumper.dump(value) + end + + # @param value [Object] + # @return [Object] + # + # @api private + # @since 0.4.0 + def detransform_value(value) + AnyCache::Dumper.load(value) + end + + # @param pairset [Hash] + # @return [Hash] + # + # @api private + # @since 0.4.0 + def transform_pairset(pairset) + AnyCache::Dumper.transform_hash(pairset) + end + + # @param pairset [Hash] + # @return [Hash] + # + # @api private + # @since 0.4.0 + def detransform_pairset(pairset) + AnyCache::Dumper.detransform_hash(pairset) + end +end diff --git a/spec/features/cleanup_spec.rb b/spec/features/cleanup_spec.rb index eae336b..2d084cb 100644 --- a/spec/features/cleanup_spec.rb +++ b/spec/features/cleanup_spec.rb @@ -15,7 +15,7 @@ key_1: 'key_1_value', key_2: 'key_2_value', key_3: 'key_3_value', - key_4: 'key_4_value', + key_4: 'key_4_value' ) sleep(3) # 4 seconds left @@ -24,7 +24,7 @@ key_1: nil, # expired key_2: 'key_2_value', key_3: 'key_3_value', - key_4: 'key_4_value', + key_4: 'key_4_value' ) sleep(4) # 8 seconds left @@ -33,7 +33,7 @@ key_1: nil, # expired key_2: nil, # expired key_3: 'key_3_value', - key_4: 'key_4_value', + key_4: 'key_4_value' ) sleep(4) # 12 seconds left @@ -42,7 +42,7 @@ key_1: nil, # expired key_2: nil, # expired key_3: nil, # expired - key_4: 'key_4_value', + key_4: 'key_4_value' ) sleep(4) # 16 seconds left diff --git a/spec/features/decrement_spec.rb b/spec/features/decrement_spec.rb index d6be1c5..69c5062 100644 --- a/spec/features/decrement_spec.rb +++ b/spec/features/decrement_spec.rb @@ -31,7 +31,14 @@ end context 'with previously defined temporal entry' do - before { cache_store.write(entry[:key], entry[:value], expires_in: expiration_time) } + before do + cache_store.write( + entry[:key], + entry[:value], + expires_in: expiration_time, + raw: true + ) + end it_behaves_like 'decrementation' @@ -43,10 +50,10 @@ cache_store.decrement(entry[:key], 2, expires_in: expiration_time) sleep(4) # NOTE: remaining time: 4 seconds again, current value: -2 - expect(cache_store.read(entry[:key]).to_i).to eq(-2) | eq(0) + expect(cache_store.read(entry[:key], raw: true).to_i).to eq(-2) | eq(0) sleep(5) # NOTE: remaining time: -1 seconds - expect(cache_store.read(entry[:key])).to eq(nil) + expect(cache_store.read(entry[:key], raw: true)).to eq(nil) end end @@ -59,13 +66,13 @@ # NOTE: new amount: -2, old entry is dead, new entry is permanent expect(new_amount).to eq(-2) | eq(0) sleep(expiration_time + 1) # NOTE: remaining time: -1 esconds, current value: -2 - expect(cache_store.read(entry[:key]).to_i).to eq(-2) | eq(0) + expect(cache_store.read(entry[:key], raw: true).to_i).to eq(-2) | eq(0) end end end context 'with previously defined permanent entry' do - before { cache_store.write(entry[:key], entry[:value]) } + before { cache_store.write(entry[:key], entry[:value], raw: true) } it_behaves_like 'decrementation' @@ -77,10 +84,10 @@ cache_store.decrement(entry[:key], 2, expires_in: expiration_time) sleep(4) # NOTE: remaining time: 4 seconds again, current value: -2 - expect(cache_store.read(entry[:key]).to_i).to eq(-2) | eq(0) + expect(cache_store.read(entry[:key], raw: true).to_i).to eq(-2) | eq(0) sleep(5) # NOTE: remaining time: -1 seconds - expect(cache_store.read(entry[:key])).to eq(nil) + expect(cache_store.read(entry[:key], raw: true)).to eq(nil) end end diff --git a/spec/features/increment_spec.rb b/spec/features/increment_spec.rb index cda6955..7a75c3b 100644 --- a/spec/features/increment_spec.rb +++ b/spec/features/increment_spec.rb @@ -4,7 +4,7 @@ include_context 'cache store' let(:expiration_time) { 8 } # NOTE: in seconds - let(:entry) { { key: SecureRandom.hex, value: 1 } } + let(:entry) { { key: SecureRandom.hex, value: 1 } } shared_examples 'incrementation' do specify 'by default: decrements by 1' do @@ -31,7 +31,14 @@ end context 'with previously defined temporal entry' do - before { cache_store.write(entry[:key], entry[:value], expires_in: expiration_time) } + before do + cache_store.write( + entry[:key], + entry[:value], + expires_in: expiration_time, + raw: true + ) + end it_behaves_like 'incrementation' @@ -43,10 +50,10 @@ cache_store.increment(entry[:key], 2, expires_in: expiration_time) sleep(4) # NOTE: remaining time: 4 seconds again, current value: 4 - expect(cache_store.read(entry[:key]).to_i).to eq(4) + expect(cache_store.read(entry[:key], raw: true).to_i).to eq(4) sleep(5) # NOTE: remaining time: -1 seconds - expect(cache_store.read(entry[:key])).to eq(nil) + expect(cache_store.read(entry[:key], raw: true)).to eq(nil) end end @@ -60,13 +67,13 @@ expect(new_amount).to eq(2) sleep(expiration_time + 1) # NOTE: remaining time: -1 esconds, current value: 2 - expect(cache_store.read(entry[:key]).to_i).to eq(2) + expect(cache_store.read(entry[:key], raw: true).to_i).to eq(2) end end end context 'with previously defined permanent entry' do - before { cache_store.write(entry[:key], entry[:value]) } + before { cache_store.write(entry[:key], entry[:value], raw: true) } it_behaves_like 'incrementation' @@ -78,10 +85,10 @@ cache_store.increment(entry[:key], 2, expires_in: expiration_time) sleep(4) # NOTE: remaining time: 4 seconds again, current value: 4 - expect(cache_store.read(entry[:key]).to_i).to eq(4) + expect(cache_store.read(entry[:key], raw: true).to_i).to eq(4) sleep(5) # NOTE: remaining time: -1 seconds - expect(cache_store.read(entry[:key])).to eq(nil) + expect(cache_store.read(entry[:key], raw: true)).to eq(nil) end end diff --git a/spec/features/logging_spec.rb b/spec/features/logging_spec.rb index c813584..8a142fa 100644 --- a/spec/features/logging_spec.rb +++ b/spec/features/logging_spec.rb @@ -63,7 +63,7 @@ specify '#increment' do log_message = "[AnyCache<#{cacher_name}>/Activity]" - cache_store.write(entry[:key], rand(2..10)) + cache_store.write(entry[:key], rand(2..10), raw: true) expect(output.string).not_to include(log_message) cache_store.increment(entry[:key], rand(1..2), expires_in: expires_in) @@ -72,7 +72,7 @@ specify '#decrmenet' do log_message = "[AnyCache<#{cacher_name}>/Activity]" - cache_store.write(entry[:key], rand(2..10)) + cache_store.write(entry[:key], rand(2..10), raw: true) expect(output.string).not_to include(log_message) cache_store.decrement(entry[:key], rand(1..2), expires_in: expires_in) @@ -122,5 +122,13 @@ end expect(output.string).to include(log_message) end + + specify '#cleanup' do + log_message = "[AnyCache<#{cacher_name}>/Activity]" + + expect(output.string).not_to include(log_message) + cache_store.cleanup(custom_option: SecureRandom.hex(4)) + expect(output.string).to include(log_message) + end end end diff --git a/spec/features/raw_non_raw_read_write_spec.rb b/spec/features/raw_non_raw_read_write_spec.rb new file mode 100644 index 0000000..55da8f4 --- /dev/null +++ b/spec/features/raw_non_raw_read_write_spec.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +describe 'raw read/write / non-raw read/write' do + include_context 'cache store' + + before { stub_const('SimpleRubyObject', Struct.new(:a, :b, :c)) } + + specify 'non-raw is used by default' do + ruby_object = SimpleRubyObject.new('a', 123, true) + cache_store.write(:ruby_object, ruby_object) + cached_ruby_object = cache_store.read(:ruby_object) + + expect(cached_ruby_object).to be_a(SimpleRubyObject) + expect(cached_ruby_object).to have_attributes(a: 'a', b: 123, c: true) + end + + context 'write non-raw (write real ruby object) (:raw => false)' do + context 'single read/write' do + specify '#read/#write works correctly' do + # works with initial structures + ruby_object = { a: 1, b: 2, c: 3 } + cache_store.write(:non_raw, ruby_object, raw: false) + cached_ruby_object = cache_store.read(:non_raw, raw: false) + + expect(cached_ruby_object).to be_a(Hash) + expect(cached_ruby_object).to match(ruby_object) + + # works with custom classes + another_ruby_object = SimpleRubyObject.new(1, 2, 3) + cache_store.write(:non_raw, another_ruby_object, raw: false) + cached_another_ruby_object = cache_store.read(:non_raw, raw: false) + + expect(cached_another_ruby_object).to be_a(SimpleRubyObject) + expect(cached_another_ruby_object).to have_attributes(a: 1, b: 2, c: 3) + end + + specify '#fetch works correctly' do + returned_ruby_object = cache_store.fetch(:non_raw, raw: false) do |key| + SimpleRubyObject.new(key, 1, false) + end + + cached_ruby_object = cache_store.fetch(:non_raw, raw: false) + + expect([cached_ruby_object, returned_ruby_object]).to all( + be_a(SimpleRubyObject).and(have_attributes(a: :non_raw, b: 1, c: false)) + ) + end + end + + context 'multi read/write' do + specify '#read_multi/#write_multi works correctly' do + # worsk with any adequate type + ruby_entries = { + hash_ruby_obj: { a: 1, b: 2, c: 3 }, + bool_ruby_obj: true, + real_ruby_obj: SimpleRubyObject.new(1, 2, 3) + } + + cache_store.write_multi(ruby_entries, raw: false) + cached_ruby_entries = cache_store.read_multi(*ruby_entries.keys, raw: false) + + expect(cached_ruby_entries).to match(ruby_entries) + end + + specify '#fetch_multi works correctly' do + returned_ruby_entries = cache_store.fetch_multi(:a, :b, :c, raw: false) do |key| + SimpleRubyObject.new(key, 1, true) + end + + cached_ruby_entries = cache_store.fetch_multi(:a, :b, :c, raw: false) + + [cached_ruby_entries, returned_ruby_entries].each do |cached_entries| + expect(cached_entries[:a]).to be_a(SimpleRubyObject) + expect(cached_entries[:b]).to be_a(SimpleRubyObject) + expect(cached_entries[:c]).to be_a(SimpleRubyObject) + + expect(cached_entries[:a]).to have_attributes(a: :a, b: 1, c: true) + expect(cached_entries[:b]).to have_attributes(a: :b, b: 1, c: true) + expect(cached_entries[:c]).to have_attributes(a: :c, b: 1, c: true) + end + end + end + end + + # NOTE: + # ActiveSupport::Cache::FileStore and ActiveSupport::Cache::ReadStore does not support + # raw/non-raw optionality cuz under the hood these classes invokes #write_entry method + # that uses Marshal.dump before the write-to-disk and write-to-memory operations respectively. + context( + 'write raw (:raw => true) (uses internal cache-related string-like object write operation)', + exclude: %i[as_file_store as_memory_store] + ) do + + before { stub_const('SimpleRubyObject', Struct.new(:a, :b, :c)) } + + context 'single read/write' do + specify '#read/#write works correctly' do + ruby_object = { a: 1, b: 2, c: 3 } + cache_store.write(:raw, ruby_object, raw: true) + cached_ruby_object = cache_store.read(:raw, raw: true) + + expect(cached_ruby_object).to be_a(String) + + another_ruby_object = SimpleRubyObject.new(1, 2, 3) + cache_store.write(:raw, another_ruby_object, raw: true) + cached_another_ruby_object = cache_store.read(:raw, raw: true) + + expect(cached_another_ruby_object).to be_a(String) + end + + specify '#fetch works correctly' do + # rubocop:disable Lint/UselessAssignment + returned_ruby_object = cache_store.fetch(:raw, raw: true) do |key| + SimpleRubyObject.new(key, 1, false) + end + # rubocop:enable Lint/UselessAssignment + + cached_ruby_object = cache_store.fetch(:raw, raw: true) + + # TODO: think about consistent fetching + # - expect(returned_ruby_object).to be_a(String) + # - nonexistent value (first #fetch invokation) + # will return the &fallback's proc result (real ruby object) + + expect(cached_ruby_object).to be_a(String) + end + end + + context 'multi read/write' do + specify '#read_multi/#write_multi works correctly' do + # works with any adequate type + ruby_entries = { + hash_ruby_obj: { a: 1, b: 2, c: 3 }, + bool_ruby_obj: true, + real_ruby_obj: SimpleRubyObject.new(1, 2, 3) + } + + cache_store.write_multi(ruby_entries, raw: true) + cached_ruby_entries = cache_store.read_multi(*ruby_entries.keys, raw: true) + + expect(cached_ruby_entries.keys).to contain_exactly(*ruby_entries.keys) + expect(cached_ruby_entries.values).to all(be_a(String)) + end + + specify '#fetch_multi works correctly' do + # rubocop:disable Lint/UselessAssignment + returned_ruby_entries = cache_store.fetch_multi(:a, :b, raw: true) do |key| + SimpleRubyObject.new(key, true, false) + end + # rubocop:enable Lint/UselessAssignment + + cached_ruby_entries = cache_store.fetch_multi(:a, :b, raw: true) + + # TODO: think about consistent fetching + # - expect(returned_ruby_entries.values).to all(be_a(String)) and so on + # - nonexistent value (first #fetch invokation) + # will return the &fallback's proc result (real ruby object) + + expect(cached_ruby_entries.keys).to contain_exactly(:a, :b) + expect(cached_ruby_entries.values).to all(be_a(String)) + end + end + end +end