Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-20.04
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

github actions needed this update to run

runs-on: ubuntu-latest

strategy:
fail-fast: false
Expand Down
59 changes: 59 additions & 0 deletions lib/dalli/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ module Dalli
##
# rubocop:disable Metrics/ClassLength
class Client
LOCK_TTL = 5 # seconds, default lock TTL
FILL_LOCK_INTERVAL = 0.01 # seconds, default fill lock interval
##
# Dalli::Client is the main class which developers will use to interact with
# the memcached server. Usage:
Expand Down Expand Up @@ -148,6 +150,47 @@ def fetch(key, ttl = nil, req_options = nil)
new_val
end

# Fetch the value associated with the key, along with a lock.
# If a value is found, then it is returned.
#
# If a value is not found and no block is given, then nil is returned.
#
# If a value is not found (or if the found value is nil and :cache_nils is false)
# and a block is given, the block will be invoked and its return value
# written to the cache and returned.
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def fetch_with_lock(key, ttl = nil, req_options = nil)
req_options = {} if req_options.nil?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need an early return here since there is a default lock TTL and interval?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this just sets to an empty hash opposed to nil as it is annoying to keep checking for if the value is in the hash or if the options don't exist at all.

clean_req_options = cache_nils ? req_options.merge(CACHE_NILS) : req_options
lock_ttl, fill_lock_interval, lock_wait_end_time = get_lock_options(req_options)

req_options = clean_req_options.dup
req_options[:meta_flags] ||= []
req_options[:meta_flags] << "N#{lock_ttl}"

loop do
val, meta_flags = get(key, req_options)

if val && val != ''
return val
elsif meta_flags[:w]
new_val = yield
set(key, new_val, ttl_or_default(ttl), clean_req_options)
return new_val
elsif meta_flags[:z]
break if Time.now.to_f >= lock_wait_end_time
end

sleep(fill_lock_interval)
end
yield # fails to read value in wait time, yield back the value
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable Metrics/CyclomaticComplexity
# rubocop:enable Metrics/PerceivedComplexity

##
# compare and swap values using optimistic locking.
# Fetch the existing value for key.
Expand Down Expand Up @@ -390,6 +433,22 @@ def with

private

def get_lock_options(req_options)
lock_ttl = req_options.delete(:lock_ttl) || LOCK_TTL
fill_lock_interval = req_options.delete(:fill_lock_interval) || FILL_LOCK_INTERVAL

raise ArgumentError, 'lock_ttl must be a positive integer' if !lock_ttl.is_a?(Integer) && lock_ttl < 1

if fill_lock_interval.is_a?(Numeric) && fill_lock_interval <= 0
raise ArgumentError,
'fill_lock_interval must be a positive number'
end

lock_wait_end_time = Time.now.to_f + lock_ttl

[lock_ttl, fill_lock_interval, lock_wait_end_time]
end

def check_positive!(amt)
raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
end
Expand Down
2 changes: 1 addition & 1 deletion lib/dalli/protocol/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,10 @@ def quiet?
alias multi? quiet?

# NOTE: Additional public methods should be overridden in Dalli::Threadsafe
ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rubocop update required moving this outside of private


private

ALLOWED_QUIET_OPS = %i[add replace set delete incr decr append prepend flush noop].freeze
def verify_allowed_quiet!(opkey)
return if ALLOWED_QUIET_OPS.include?(opkey)

Expand Down
8 changes: 5 additions & 3 deletions lib/dalli/protocol/meta.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ def get(key, options = nil)
if !meta_options && !base64 && !quiet? && @value_marshaller.raw_by_default
response_processor.meta_get_with_value(cache_nils: cache_nils?(options), skip_flags: true)
elsif meta_options
response_processor.meta_get_with_value_and_meta_flags(cache_nils: cache_nils?(options))
response_processor.meta_get_with_value_and_meta_flags(cache_nils: cache_nils?(options),
meta_flags: meta_options)
else
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
end
Expand All @@ -122,8 +123,9 @@ def gat(key, ttl, options = nil)
req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, base64: base64,
meta_flags: meta_flag_options(options))
write(req)
if meta_flag_options(options)
response_processor.meta_get_with_value_and_meta_flags(cache_nils: cache_nils?(options))
if (meta_options = meta_flag_options(options))
response_processor.meta_get_with_value_and_meta_flags(cache_nils: cache_nils?(options),
meta_flags: meta_options)
else
response_processor.meta_get_with_value(cache_nils: cache_nils?(options))
end
Expand Down
43 changes: 34 additions & 9 deletions lib/dalli/protocol/meta/response_processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ def meta_get_with_value_and_cas
[@value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens)), cas]
end

def meta_get_with_value_and_meta_flags(cache_nils: false)
def meta_get_with_value_and_meta_flags(cache_nils: false, meta_flags: [])
tokens = error_on_unexpected!([VA, EN, HD])
return [(cache_nils ? ::Dalli::NOT_FOUND : nil), {}] if tokens.first == EN

meta_flags = meta_flags_from_tokens(tokens)
meta_flags = meta_flags_from_tokens(tokens, meta_flags)
return [(cache_nils ? ::Dalli::NOT_FOUND : nil), meta_flags] unless tokens.first == VA

value, bitflag = @value_marshaller.retrieve(read_data(tokens[1].to_i), bitflags_from_tokens(tokens))
Expand Down Expand Up @@ -192,14 +192,39 @@ def error_on_unexpected!(expected_codes)
raise Dalli::DalliError, "Response error: #{tokens.first}"
end

def meta_flags_from_tokens(tokens)
{
c: cas_from_tokens(tokens),
h: hit_from_tokens(tokens),
l: last_accessed_from_tokens(tokens),
t: ttl_remaining_from_tokens(tokens)
}
# rubocop:disable Metrics/MethodLength
def meta_flags_from_tokens(tokens, meta_flags)
flag_values = {}

meta_flags.each do |flag|
if flag == :c
flag_values[:c] = cas_from_tokens(tokens)
next
end

if flag == :h
flag_values[:h] = hit_from_tokens(tokens)
next
end

if flag == :l
flag_values[:l] = last_accessed_from_tokens(tokens)
next
end

if flag == :t
flag_values[:t] = ttl_remaining_from_tokens(tokens)
next
end

if flag.match?(/^N\d+$/)
flag_values[:w] = tokens.any?('W')
flag_values[:z] = tokens.any?('Z')
end
end
flag_values
end
# rubocop:enable Metrics/MethodLength

def bitflags_from_tokens(tokens)
value_from_tokens(tokens, 'f')&.to_i
Expand Down
Loading