Skip to content

Commit

Permalink
[operation/fetch] AnyCache#fetch
Browse files Browse the repository at this point in the history
  • Loading branch information
0exp committed Sep 2, 2018
1 parent 9d2958c commit ee44760
Show file tree
Hide file tree
Showing 20 changed files with 194 additions and 25 deletions.
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
14 changes: 14 additions & 0 deletions lib/any_cache/adapters/active_support_mem_cache_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,5 +143,19 @@ def persist(key, **options)
def exist?(key, **options)
driver.exist?(key)
end

# @param key [String]
# @option expires_in [Integer]
# @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
16 changes: 16 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,22 @@ def exist?(key, **options)
lock.with_read_lock { super }
end

# @param key [String]
# @option expires_in [Integer]
# @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
18 changes: 16 additions & 2 deletions lib/any_cache/adapters/active_support_redis_cache_store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ 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)
is_initial = exist?(key)

if is_initial
write(key, amount, expires_in: expires_in) && amount
Expand All @@ -90,7 +90,7 @@ 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)
is_initial = exist?(key)

if is_initial
write(key, -amount, expires_in: expires_in) && -amount
Expand Down Expand Up @@ -133,5 +133,19 @@ def persist(key, **options)
def exist?(key, **options)
driver.exist?(key)
end

# @param key [String]
# @option expires_in [Integer]
# @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
18 changes: 16 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,21 @@ def clear(**options)
def exist?(key, **options)
exists(key)
end

# @param key [String]
# @option expires_in [Integer]
# @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
4 changes: 4 additions & 0 deletions 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 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
76 changes: 76 additions & 0 deletions spec/features/fetch_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# frozen_string_literal: true

describe 'Command: #fetch' do
include_context 'cache store'

let(:entry) { { key: SecureRandom.hex, value: SecureRandom.hex(4) } }
let(:expires_in) { 5 } # NOTE: in seconds

specify 'fetching without expiration attribue creates a permanent entry' do
expect(cache_store.fetch(entry[:key])).to eq(nil)

result = cache_store.fetch(entry[:key]) { entry[:value] }

expect(result).to eq(entry[:value])
expect(cache_store.fetch(entry[:key])).to eq(entry[:value])

sleep(expires_in + 1)

expect(cache_store.fetch(entry[:key])).to eq(entry[:value])
end

specify 'fetching with expiration attribute creates a temporal entry' do
expect(cache_store.fetch(entry[:key])).to eq(nil)

result = cache_store.fetch(entry[:key], expires_in: expires_in) { entry[:value] }

expect(result).to eq(entry[:value])
expect(cache_store.fetch(entry[:key])).to eq(entry[:value])

sleep(expires_in + 1)

expect(cache_store.fetch(entry[:key])).to eq(nil)
end

specify 'fetching with :force option creates new entry' do
expect(cache_store.fetch(entry[:key])).to eq(nil)

# NOTE: initial permanent entry
entry_value = SecureRandom.hex(4)
result = cache_store.fetch(entry[:key], force: true) { entry_value }

expect(result).to eq(entry_value)
expect(cache_store.fetch(entry[:key])).to eq(entry_value)

# NOTE: new permanent value
entry_value = SecureRandom.hex(4)
result = cache_store.fetch(entry[:key], force: true) { entry_value }

expect(result).to eq(entry_value)
expect(cache_store.fetch(entry[:key])).to eq(entry_value)

# NOTE: new temporal entry
entry_value = SecureRandom.hex(4)
result = cache_store.fetch(entry[:key], expires_in: expires_in, force: true) do
entry_value
end

expect(result).to eq(entry_value)
expect(cache_store.fetch(entry[:key])).to eq(entry_value)
sleep(expires_in + 1) # NOTE: expire new temporal entry
expect(cache_store.fetch(entry[:key])).to eq(nil)

# NOTE: prepare new temporal entry
entry_value = SecureRandom.hex(4)
cache_store.write(entry[:key], entry[:value], expires_in: expires_in)

# NOTE: rewrite new temporal entry with permanent entry
result = cache_store.fetch(entry[:key], force: true) { entry_value }
expect(result).to eq(entry_value)
expect(cache_store.fetch(entry[:key])).to eq(entry_value)

sleep(expires_in + 1) # NOTE: try to expire rewritten entry

expect(cache_store.fetch(entry[:key])).to eq(entry_value)
end
end
3 changes: 1 addition & 2 deletions spec/features/increment_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# frozen_string_literal: true

describe 'Operation: #increment' 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
3 changes: 1 addition & 2 deletions spec/features/persist_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# frozen_string_literal: true

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

let(:cache_store) { build_cache_store }
let(:first_entry) { { key: SecureRandom.hex, value: SecureRandom.hex(4) } }
let(:second_entry) { { key: SecureRandom.hex, value: SecureRandom.hex(4) } }

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

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

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

context 'when the required entry exists' do
let(:expiration_time) { 8 } # NOTE: in seconds
Expand Down
3 changes: 1 addition & 2 deletions spec/features/write_spec.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# frozen_string_literal: true

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

let(:cache_store) { build_cache_store }
let(:expiration_time) { 8 } # NOTE: in seconds
let(:first_pair) { { key: SecureRandom.hex, value: SecureRandom.hex(4) } }
let(:second_pair) { { key: SecureRandom.hex, value: SecureRandom.hex(4) } }
Expand Down
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
require 'pry'

require_relative 'support/spec_support'
require_relative 'support/shared_contexts'

RSpec.configure do |config|
config.filter_run_when_matching :focus
Expand Down
3 changes: 3 additions & 0 deletions spec/support/shared_contexts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# frozen_string_literal: true

require_relative 'shared_contexts/cache_store'
7 changes: 7 additions & 0 deletions spec/support/shared_contexts/cache_store.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

shared_context 'cache store' do
after { cache_store.clear }

let(:cache_store) { build_cache_store }
end

0 comments on commit ee44760

Please sign in to comment.