Skip to content

Commit

Permalink
Merge 34b24a8 into 9d2958c
Browse files Browse the repository at this point in the history
  • Loading branch information
0exp committed Sep 2, 2018
2 parents 9d2958c + 34b24a8 commit 6d9d900
Show file tree
Hide file tree
Showing 22 changed files with 245 additions and 33 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
All notable changes to this project will be documented in this file.

## [Unreleased]
- fetching operation `AnyCache#fetch(key, force:, expires_in:, &block)`
- fetches data from the cache using the given key;
- if a block has been passed and data with the given key does not exist -
that block will be called and the return value will be written to the cache;
- existence operation `AnyCache#exist?(key)` - determine if an entry exists or not;
- support for `ActiveSupport::Cache::MemCacheStore`;
- configuration layer `AnyCache.configure`: an ability to choose and configure a necessary cache client
Expand Down
41 changes: 40 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# AnyCache · [![Gem Version](https://badge.fury.io/rb/any_cache.svg)](https://badge.fury.io/rb/any_cache) [![Build Status](https://travis-ci.org/0exp/any_cache.svg?branch=master)](https://travis-ci.org/0exp/any_cache) [![Coverage Status](https://coveralls.io/repos/github/0exp/any_cache/badge.svg)](https://coveralls.io/github/0exp/any_cache)

AnyCache - a simplest cache wrapper that provides a minimalistic generic interface for all well-known cache storages and includes a minimal set of necessary operations:
`read`, `write`, `delete`, `expire`, `persist`, `exist?`, `clear`, `increment`, `decrement`.
`fetch`, `read`, `write`, `delete`, `expire`, `persist`, `exist?`, `clear`, `increment`, `decrement`.

Supported clients:

Expand Down Expand Up @@ -249,6 +249,7 @@ dalli_cache = DalliCache.build

If you want to use your own cache client implementation, you should provide an object that responds to:

- `#fetch(*key, [**options])` ([doc](#fetch))
- `#read(key, [**options])` ([doc](#read))
- `#write(key, value, [**options])` ([doc](#write))
- `#delete(key, [**options])` ([doc](#delete))
Expand Down Expand Up @@ -281,6 +282,7 @@ AnyCache.build(MyCacheClient.new)

`AnyCache` provides a following operation set:

- [fetch](#fetch)
- [read](#read)
- [write](#write)
- [delete](#delete)
Expand All @@ -293,6 +295,43 @@ AnyCache.build(MyCacheClient.new)

---

### Fetch

- `AnyCache#fetch(key, [force:], [expires_in:], [&block])`
- works in `ActiveSupport::Cache::Store#fetch`-manner;
- fetches data from the cache, using the given key;
- if there is data in the cache with the given key, then that data is returned;
- if there is no such data in the cache (a cache miss), then nil will be returned:
- if a block has been passed, that block will be passed the key and executed in the event of a cache miss;
- the return value of the block will be written to the cache under the given cache key, and that return value will be returned;

```ruby
# --- entry exists ---
any_cache.fetch("data") # => "some_data"
any_cache.fetch("data") { "new_data" } # => "some_data"

# --- entry does not exist ---
any_cache.fetch("data") # => nil
any_cache.fetch("data") { "new_data" } # => "new_data"
any_cache.fetch("data") # => "new_data"

# --- new entry with expiration time ---
any_cache.fetch("data") # => nil
any_cache.fetch("data", expires_in: 8) { "new_data" } # => "new_data"
any_cache.fetch("data") # => "new_data"
# ...sleep 8 seconds...
any_cache.fetch("data") # => nil

# --- force update/rewrite ---
any_cache.fetch("data") # => "some_data"
any_cache.fetch("data", expires_in: 8, force: true) { "new_data" } # => "new_data"
any_cache.fetch("data") # => "new_data"
# ...sleep 8 seconds...
any_cache.fetch("data") # => nil
```

---

### Read

- `AnyCache#read(key)` - get entry value from cache storage
Expand Down
3 changes: 2 additions & 1 deletion lib/any_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ def build(driver = Drivers.build(config))
:expire,
:persist,
:clear,
:exist?
:exist?,
:fetch

# @return [AnyCache::Adapters::Basic]
#
Expand Down
21 changes: 17 additions & 4 deletions lib/any_cache/adapters/active_support_mem_cache_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,8 @@ def write(key, value, **options)
# @since 0.2.0
def increment(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options)
expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL)
is_initial = !read(key) # TODO: rewrite with #exist?(key)

if is_initial
unless exist?(key)
write(key, amount, expires_in: expires_in) && amount
else
driver.increment(key, amount).tap do
Expand All @@ -96,9 +95,8 @@ def increment(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options)
# @since 0.2.0
def decrement(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options)
expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL)
is_initial = !read(key) # TODO: rewrite with #exist?(key)

if is_initial
unless exist?(key)
# NOTE: Dalli::Client can't decrement:
# - non-raw values;
# - values lower than zero;
Expand Down Expand Up @@ -143,5 +141,20 @@ def persist(key, **options)
def exist?(key, **options)
driver.exist?(key)
end

# @param key [String]
# @option expires_in [Integer]
# @option force [Boolean]
# @return [Object]
#
# @api private
# @since 0.2.0
def fetch(key, **options, &block)
force_rewrite = options.fetch(:force, false)
force_rewrite = force_rewrite.call if force_rewrite.respond_to?(:call)
expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL)

driver.fetch(key, force: force_rewrite, expires_in: expires_in, &block)
end
end
end
17 changes: 17 additions & 0 deletions lib/any_cache/adapters/active_support_naive_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,23 @@ def exist?(key, **options)
lock.with_read_lock { super }
end

# @param key [String]
# @option expires_in [Integer]
# @option force [Boolean]
# @return [Object]
#
# @api private
# @since 0.2.0
def fetch(key, **options, &block)
lock.with_write_lock do
force_rewrite = options.fetch(:force, false)
force_rewrite = force_rewrite.call if force_rewrite.respond_to?(:call)
expires_in = options.fetch(:expires_in, self.class::Operation::NO_EXPIRATION_TTL)

super(key, force: force_rewrite, expires_in: expires_in, &block)
end
end

private

# @return [Concurrent::ReentrantReadWriteLock]
Expand Down
21 changes: 17 additions & 4 deletions lib/any_cache/adapters/active_support_redis_cache_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,8 @@ def write(key, value, **options)
# @since 0.1.0
def increment(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options)
expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL)
is_initial = !read(key) # TODO: rewrite with #exist?(key)

if is_initial
unless exist?(key)
write(key, amount, expires_in: expires_in) && amount
else
driver.increment(key, amount).tap do
Expand All @@ -90,9 +89,8 @@ def increment(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options)
# @since 0.1.0
def decrement(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options)
expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL)
is_initial = !read(key) # TODO: rewrite with #exist?(key)

if is_initial
unless exist?(key)
write(key, -amount, expires_in: expires_in) && -amount
else
driver.decrement(key, amount).tap do
Expand Down Expand Up @@ -133,5 +131,20 @@ def persist(key, **options)
def exist?(key, **options)
driver.exist?(key)
end

# @param key [String]
# @option expires_in [Integer]
# @option force [Boolean]
# @return [Object]
#
# @api private
# @since 0.2.0
def fetch(key, **options, &block)
force_rewrite = options.fetch(:force, false)
force_rewrite = force_rewrite.call if force_rewrite.respond_to?(:call)
expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL)

driver.fetch(key, force: force_rewrite, expires_in: expires_in, &block)
end
end
end
10 changes: 10 additions & 0 deletions lib/any_cache/adapters/basic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,15 @@ def clear(**options)
def exist?(key, **options)
raise NotImplementedError
end

# @param key [String]
# @param options [Hash]
# @return [Object]
#
# @api private
# @since 0.2.0
def fetch(key, **options, &block)
raise NotImplementedError
end
end
end
19 changes: 18 additions & 1 deletion lib/any_cache/adapters/dalli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,24 @@ def clear(**options)
# @api private
# @since 0.2.0
def exist?(key, **options)
!get(key).nil? # NOTE: can conflict with :cache_nils Dalli::Client option
!get(key).nil? # NOTE: can conflict with :cache_nils Dalli::Client's config
end

# @param key [String]
# @option expires_in [Integer]
# @option force [Boolean]
# @return [Object]
#
# @api private
# @since 0.2.0
def fetch(key, **options)
force_rewrite = options.fetch(:force, false)
force_rewrite = force_rewrite.call 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

yield.tap { |value| write(key, value, **options) } if block_given?
end
end
end
6 changes: 4 additions & 2 deletions lib/any_cache/adapters/delegator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def supported_driver?(driver)
driver.respond_to?(:expire) &&
driver.respond_to?(:persist) &&
driver.respond_to?(:clear) &&
driver.respond_to?(:exist?)
driver.respond_to?(:exist?) &&
driver.respond_to?(:fetch)
end
end

Expand All @@ -33,6 +34,7 @@ def supported_driver?(driver)
:expire,
:persist,
:clear,
:exist?
:exist?,
:fetch
end
end
19 changes: 17 additions & 2 deletions lib/any_cache/adapters/redis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ def increment(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options)
expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL)
new_amount = nil

# TODO: think about Redis#multi
pipelined do
new_amount = incrby(key, amount)
expire(key, expires_in: expires_in) if expires_in
Expand All @@ -109,7 +108,6 @@ def decrement(key, amount = DEFAULT_INCR_DECR_AMOUNT, **options)
expires_in = options.fetch(:expires_in, NO_EXPIRATION_TTL)
new_amount = nil

# TODO: think about Redis#multi
pipelined do
new_amount = decrby(key, amount)
expire(key, expires_in: expires_in) if expires_in
Expand Down Expand Up @@ -158,5 +156,22 @@ def clear(**options)
def exist?(key, **options)
exists(key)
end

# @param key [String]
# @option expires_in [Integer]
# @option force [Boolean]
# @return [Object]
#
# @api private
# @since 0.2.0
def fetch(key, **options)
force_rewrite = options.fetch(:force, false)
force_rewrite = force_rewrite.call if force_rewrite.respond_to?(:call)

# NOTE: think about #pipelined
read(key).tap { |value| return value if value } unless force_rewrite

yield.tap { |value| write(key, value, **options) } if block_given?
end
end
end
4 changes: 1 addition & 3 deletions spec/features/clear_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# frozen_string_literal: true

describe 'Operation: #clear' do
let(:cache_store) { build_cache_store }

after { cache_store.clear }
include_context 'cache store'

it 'clears storage' do
# NOTE: write random values
Expand Down
6 changes: 5 additions & 1 deletion spec/features/custom_cache_clients_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ def expire(key, **); end
def persist(key, **); end
def clear(key, **); end
def exist?(key, **); end
def fetch(key, **); end
# rubocop:enable Layout/EmptyLineBetweenDefs
end.new
end
Expand All @@ -30,6 +31,7 @@ def expire; end
def persist; end
def clear; end
def exist?; end
def fetch; end
end.new
# rubocop:enable Layout/EmptyLineBetweenDefs
end
Expand All @@ -48,6 +50,7 @@ def exist?; end
expire
persist
clear
fetch
exist?
].each do |operation|
specify "AnyCache instance delegates :#{operation} operation to the custom client" do
Expand Down Expand Up @@ -90,6 +93,7 @@ def exist?; end
expire
persist
clear
fetch
exist?
]
end
Expand All @@ -98,7 +102,7 @@ def exist?; end
while required_methods.shift
incomplete_cache_client = Class.new.tap do |klass|
required_methods.each do |required_method|
klass.define_method(required_method) {}
klass.send(:define_method, required_method, &(proc {}))
end
end.new

Expand Down
3 changes: 1 addition & 2 deletions spec/features/decrement_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# frozen_string_literal: true

describe 'Operation: #decrement' do
after { cache_store.clear }
include_context 'cache store'

let(:cache_store) { build_cache_store }
let(:expiration_time) { 8 } # NOTE: in seconds
let(:entry) { { key: SecureRandom.hex, value: 1 } }

Expand Down
4 changes: 1 addition & 3 deletions spec/features/delete_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# frozen_string_literal: true

describe 'Operation: #delete' do
after { cache_store.clear }

let(:cache_store) { build_cache_store }
include_context 'cache store'

it 'removes entry from cache' do
first_pair = { key: SecureRandom.hex, value: SecureRandom.hex(4) }
Expand Down

0 comments on commit 6d9d900

Please sign in to comment.