Skip to content

Default values on initialization #119

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 39 commits into from
Aug 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
133a0e0
Add default proxy value
tleish Jun 10, 2022
4eff947
Add default list values
tleish Jun 10, 2022
903f2a9
Add default to unique_list
tleish Jun 10, 2022
a668f78
Add default to flag
tleish Jun 10, 2022
35dc9bf
Add default to string
tleish Jun 10, 2022
e6bcbbd
Add default to integer
tleish Jun 10, 2022
ba0092c
Add default to decimal
tleish Jun 10, 2022
52b308b
Add default to datetime
tleish Jun 10, 2022
de0e5b1
Add default to float
tleish Jun 10, 2022
77499ed
Add default proc to enum
tleish Jun 10, 2022
46fa83a
rename default_value method to default
tleish Jun 10, 2022
abdf95a
Add default proc to set
tleish Jun 10, 2022
4933f6c
Add additional unit tests
tleish Jun 10, 2022
321d33c
Add default proc to json and counter
tleish Jun 10, 2022
411d04a
Add default proc to hash and boolean
tleish Jun 11, 2022
ed3cbd8
Add before_method_hook
tleish Jun 11, 2022
1030836
Fix for ruby 3
tleish Jun 11, 2022
1bb9288
refactor Kredis::Types::BeforeMethodsHook#before_methods
tleish Jun 20, 2022
10ceac9
code review updates
tleish Jun 20, 2022
1bff6bf
merge main
tleish Jun 29, 2022
7e85720
fix code review feedback
tleish Jul 15, 2022
180f57c
Match indentation
dhh Jul 15, 2022
bf2ba58
create custom callnx method and refactor
tleish Jul 20, 2022
d0146ee
add additional default options to set
tleish Jul 20, 2022
90d9208
move primary #set_default method to Kredis::Types::Proxying
tleish Jul 20, 2022
5c4664b
updated enum multi block to use standar [-1] pattern instead of .last
tleish Jul 20, 2022
ddf555f
refactor init_default_in_multi to not use multi if default not defined
tleish Jul 20, 2022
ef2eacb
refactor multi in Kredis::Types::Proxying
tleish Jul 21, 2022
4b1ab12
Merge remote-tracking branch 'origin/main' into default-values-on-ini…
lewispb Jul 8, 2023
6faf4d0
Set default values on type initialization
lewispb Jul 8, 2023
a140434
Merge branch 'main' into default-values-on-initialize
lewispb Jul 13, 2023
52000bb
Run Rubocop autocorrect over default value tests
lewispb Jul 13, 2023
2237ecc
Document default values and organize readme
lewispb Aug 1, 2023
c32c663
Support concurrent initalization with defaults
lewispb Aug 2, 2023
c808076
Avoid breaking current enum functionality, allowing setting an invali…
lewispb Aug 2, 2023
b29a895
Now we have proper concurrency, no need for scalar default nx: true
lewispb Aug 2, 2023
5c5ee24
Restore test for enum not existing when value is nil
lewispb Aug 2, 2023
cc08f25
Setting an invalid enum default is a bug
lewispb Aug 2, 2023
b1a818a
No need to double proxy watch
lewispb Aug 2, 2023
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
53 changes: 40 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@ list = Kredis.list "mylist"
list << "hello world!" # => RPUSH mylist "hello world!"
[ "hello world!" ] == list.elements # => LRANGE mylist 0, -1

integer_list = Kredis.list "myintegerlist", typed: :integer
integer_list.append([ 1, 2, 3 ]) # => RPUSH myintegerlist "1" "2" "3"
integer_list << 4 # => RPUSH myintegerlist "4"
[ 1, 2, 3, 4 ] == integer_list.elements # => LRANGE myintegerlist 0 -1
integer_list = Kredis.list "myintegerlist", typed: :integer, default: [ 1, 2, 3 ] # => EXISTS? myintegerlist, RPUSH myintegerlist "1" "2" "3"
integer_list.append([ 4, 5, 6 ]) # => RPUSH myintegerlist "4" "5" "6"
integer_list << 7 # => RPUSH myintegerlist "7"
[ 1, 2, 3, 4, 5, 6, 7 ] == integer_list.elements # => LRANGE myintegerlist 0 -1

unique_list = Kredis.unique_list "myuniquelist"
unique_list.append(%w[ 2 3 4 ]) # => LREM myuniquelist 0, "2" + LREM myuniquelist 0, "3" + LREM myuniquelist 0, "4" + RPUSH myuniquelist "2", "3", "4"
Expand Down Expand Up @@ -163,15 +163,7 @@ sleep 0.6.seconds
false == flag.marked? #=> EXISTS myflag
```

And using structures on a different than the default `shared` redis instance, relying on `config/redis/secondary.yml`:

```ruby
one_string = Kredis.string "mystring"
two_string = Kredis.string "mystring", config: :secondary

one_string.value = "just on shared"
two_string.value != one_string.value
```
### Models

You can use all these structures in models:

Expand All @@ -197,6 +189,29 @@ person.morning.value = "blue" # => SET people:5:morning
true == person.morning.blue? # => GET people:5:morning
```

### Default values

You can set a default value for all types. For example:

```ruby
list = Kredis.list "favorite_colors", default: [ "red", "green", "blue" ]

# or, in a model
class Person < ApplicationRecord
kredis_string :name, default: "Unknown"
kredis_list :favorite_colors, default: [ "red", "green", "blue" ]
end
```

There's a performance overhead to consider though. When you first read or write an attribute in a model, Kredis will
check if the underlying Redis key exists, while watching for concurrent changes, and if it does not,
write the specified default value.

This means that using default values in a typical Rails app additional Redis calls (WATCH, EXISTS, UNWATCH) will be
executed for each Kredis attribute with a default value read or written during a request.
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this occur every request, or only when a value does not exist?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the context of a Rails web request, as the Kredis object is initialized, the key is checked with WATCH, EXISTS, UNWATCH once, but then subsequent operations within the request do not incur any Redis command overhead. If no default value is specified, no additional Redis command overhead is incurred at all.

Copy link
Contributor

Choose a reason for hiding this comment

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

To clarify:

Scenario 1: Default NOT defined

(no additional overhead)

Scenario 2: Default defined, value NOT exists

WATCH, EXISTS, UNWATCH

Scenario 3: Default defined, value exists

(no additional overhead?)

Copy link
Contributor Author

@lewispb lewispb Aug 7, 2023

Choose a reason for hiding this comment

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

Actually, it's:

Scenario 1: Default NOT defined

(no additional overhead)

Scenario 2: Default defined, value NOT exists

WATCH, EXISTS, write default (depending on type), UNWATCH

Scenario 3: Default defined, value exists

WATCH, EXISTS, UNWATCH

Copy link
Contributor

Choose a reason for hiding this comment

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

It seems a more efficient approach would be to only add extra overhead of default is defined AND value doesn't exist. In other words:

  1. Get Value
  2. Value doesn't exist
  3. Execute Default Logic

This avoids extra overhead if the value has been set already.

Copy link
Contributor Author

@lewispb lewispb Aug 8, 2023

Choose a reason for hiding this comment

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

I agree @tleish. That's exactly what we do here, but with the added WATCH / UNWATCH.

To illustrate the problem that the WATCH / UNWATCH solves check this test case:

test "concurrent initialization with default" do
5.times.map do
Thread.new do
Kredis.counter("mycounter", default: 5).increment
end
end.each(&:join)
assert_equal 10, Kredis.counter("mycounter").value
end

Without the WATCH / UNWATCH, the value existing check is prone to a race condition.


### Callbacks

You can also define `after_change` callbacks that trigger on mutations:

```ruby
Expand All @@ -209,6 +224,18 @@ class Person < ApplicationRecord
end
```

### Multiple Redis servers

And using structures on a different than the default `shared` redis instance, relying on `config/redis/secondary.yml`:

```ruby
one_string = Kredis.string "mystring"
two_string = Kredis.string "mystring", config: :secondary

one_string.value = "just on shared"
two_string.value != one_string.value
```

## Installation

1. Run `./bin/bundle add kredis`
Expand Down
1 change: 1 addition & 0 deletions lib/kredis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
require "kredis/log_subscriber"
require "kredis/namespace"
require "kredis/type_casting"
require "kredis/default_values"
require "kredis/types"
require "kredis/attributes"

Expand Down
65 changes: 37 additions & 28 deletions lib/kredis/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,56 +8,56 @@ def kredis_proxy(name, key: nil, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change
end

def kredis_string(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
def kredis_string(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in
end

def kredis_integer(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
def kredis_integer(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in
end

def kredis_decimal(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
def kredis_decimal(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in
end

def kredis_datetime(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
def kredis_datetime(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in
end

def kredis_flag(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
def kredis_flag(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in

define_method("#{name}?") do
send(name).marked?
end
end

def kredis_float(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
def kredis_float(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in
end

def kredis_enum(name, key: nil, values:, default:, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, values: values, default: default, config: config, after_change: after_change
end

def kredis_json(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
def kredis_json(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in
end

def kredis_list(name, key: nil, typed: :string, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, typed: typed, config: config, after_change: after_change
def kredis_list(name, key: nil, default: nil, typed: :string, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, default: default, typed: typed, config: config, after_change: after_change
end

def kredis_unique_list(name, limit: nil, key: nil, typed: :string, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, limit: limit, typed: typed, config: config, after_change: after_change
def kredis_unique_list(name, limit: nil, key: nil, default: nil, typed: :string, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, default: default, limit: limit, typed: typed, config: config, after_change: after_change
end

def kredis_ordered_set(name, limit: nil, key: nil, typed: :string, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, limit: limit, typed: typed, config: config, after_change: after_change
def kredis_set(name, key: nil, default: nil, typed: :string, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, default: default, typed: typed, config: config, after_change: after_change
end

def kredis_set(name, key: nil, typed: :string, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, typed: typed, config: config, after_change: after_change
def kredis_ordered_set(name, limit: nil, default: nil, key: nil, typed: :string, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, default: default, limit: limit, typed: typed, config: config, after_change: after_change
end

def kredis_slot(name, key: nil, config: :shared, after_change: nil)
Expand All @@ -68,16 +68,16 @@ def kredis_slots(name, available:, key: nil, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, available: available, config: config, after_change: after_change
end

def kredis_counter(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
def kredis_counter(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in
end

def kredis_hash(name, key: nil, typed: :string, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, typed: typed, config: config, after_change: after_change
def kredis_hash(name, key: nil, default: nil, typed: :string, config: :shared, after_change: nil)
kredis_connection_with __method__, name, key, default: default, typed: typed, config: config, after_change: after_change
end

def kredis_boolean(name, key: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, config: config, after_change: after_change, expires_in: expires_in
def kredis_boolean(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, config: config, after_change: after_change, expires_in: expires_in
end

private
Expand All @@ -90,6 +90,7 @@ def kredis_connection_with(method, name, key, **options)
if instance_variable_defined?(ivar_symbol)
instance_variable_get(ivar_symbol)
else
options[:default] = kredis_default_evaluated(options[:default]) if options[:default]
new_type = Kredis.send(type, kredis_key_evaluated(key) || kredis_key_for_attribute(name), **options)
instance_variable_set ivar_symbol,
after_change ? enrich_after_change_with_record_access(new_type, after_change) : new_type
Expand Down Expand Up @@ -121,4 +122,12 @@ def enrich_after_change_with_record_access(type, original_after_change)
when Symbol then Kredis::Types::CallbacksProxy.new(type, ->(_) { send(original_after_change) })
end
end

def kredis_default_evaluated(default)
case default
when Proc then Proc.new { default.call(self) }
when Symbol then send(default)
else default
end
end
end
36 changes: 36 additions & 0 deletions lib/kredis/default_values.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# frozen_string_literal: true

module Kredis::DefaultValues
extend ActiveSupport::Concern

prepended do
attr_writer :default

proxying :watch, :unwatch, :exists?

def default
case @default
when Proc then @default.call
when Symbol then send(@default)
else @default
end
end

private
def set_default
raise NotImplementedError, "Kredis type #{self.class} needs to define #set_default"
end
end

def initialize(...)
super

if default
watch do
set_default unless exists?

unwatch
end
end
end
end
28 changes: 14 additions & 14 deletions lib/kredis/types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,40 +41,40 @@ def json(key, default: nil, config: :shared, after_change: nil, expires_in: nil)
end


def counter(key, expires_in: nil, config: :shared, after_change: nil)
type_from(Counter, config, key, after_change: after_change, expires_in: expires_in)
def counter(key, expires_in: nil, default: nil, config: :shared, after_change: nil)
type_from(Counter, config, key, after_change: after_change, default: default, expires_in: expires_in)
end

def cycle(key, values:, expires_in: nil, config: :shared, after_change: nil)
type_from(Cycle, config, key, after_change: after_change, values: values, expires_in: expires_in)
end

def flag(key, config: :shared, after_change: nil, expires_in: nil)
type_from(Flag, config, key, after_change: after_change, expires_in: expires_in)
def flag(key, default: nil, config: :shared, after_change: nil, expires_in: nil)
type_from(Flag, config, key, after_change: after_change, default: default, expires_in: expires_in)
end

def enum(key, values:, default:, config: :shared, after_change: nil)
type_from(Enum, config, key, after_change: after_change, values: values, default: default)
end

def hash(key, typed: :string, config: :shared, after_change: nil)
type_from(Hash, config, key, after_change: after_change, typed: typed)
def hash(key, typed: :string, default: nil, config: :shared, after_change: nil)
type_from(Hash, config, key, after_change: after_change, default: default, typed: typed)
end

def list(key, typed: :string, config: :shared, after_change: nil)
type_from(List, config, key, after_change: after_change, typed: typed)
def list(key, default: nil, typed: :string, config: :shared, after_change: nil)
type_from(List, config, key, after_change: after_change, default: default, typed: typed)
end

def unique_list(key, typed: :string, limit: nil, config: :shared, after_change: nil)
type_from(UniqueList, config, key, after_change: after_change, typed: typed, limit: limit)
def unique_list(key, default: nil, typed: :string, limit: nil, config: :shared, after_change: nil)
type_from(UniqueList, config, key, after_change: after_change, default: default, typed: typed, limit: limit)
end

def set(key, typed: :string, config: :shared, after_change: nil)
type_from(Set, config, key, after_change: after_change, typed: typed)
def set(key, default: nil, typed: :string, config: :shared, after_change: nil)
type_from(Set, config, key, after_change: after_change, default: default, typed: typed)
end

def ordered_set(key, typed: :string, limit: nil, config: :shared, after_change: nil)
type_from(OrderedSet, config, key, after_change: after_change, typed: typed, limit: limit)
def ordered_set(key, default: nil, typed: :string, limit: nil, config: :shared, after_change: nil)
type_from(OrderedSet, config, key, after_change: after_change, default: default, typed: typed, limit: limit)
end

def slot(key, config: :shared, after_change: nil)
Expand Down
7 changes: 7 additions & 0 deletions lib/kredis/types/counter.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

class Kredis::Types::Counter < Kredis::Types::Proxying
prepend Kredis::DefaultValues

proxying :multi, :set, :incrby, :decrby, :get, :del, :exists?

attr_accessor :expires_in
Expand All @@ -26,4 +28,9 @@ def value
def reset
del
end

private
def set_default
increment by: default
end
end
23 changes: 19 additions & 4 deletions lib/kredis/types/enum.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@
require "active_support/core_ext/object/inclusion"

class Kredis::Types::Enum < Kredis::Types::Proxying
proxying :set, :get, :del, :exists?
prepend Kredis::DefaultValues

attr_accessor :values, :default
InvalidDefault = Class.new(StandardError)

proxying :set, :get, :del, :exists?, :multi

attr_accessor :values

def initialize(...)
super
Expand All @@ -19,11 +23,14 @@ def value=(value)
end

def value
get || default
get
end

def reset
del
multi do
del
set_default
end
end

private
Expand All @@ -33,4 +40,12 @@ def define_predicates_for_values
define_singleton_method("#{defined_value}!") { self.value = defined_value }
end
end

def set_default
if default.in?(values) || default.nil?
set default
else
raise InvalidDefault, "Default value #{default.inspect} for #{key} is not a valid option (Valid values: #{values.join(", ")})"
end
end
end
7 changes: 7 additions & 0 deletions lib/kredis/types/flag.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# frozen_string_literal: true

class Kredis::Types::Flag < Kredis::Types::Proxying
prepend Kredis::DefaultValues

proxying :set, :exists?, :del

attr_accessor :expires_in
Expand All @@ -16,4 +18,9 @@ def marked?
def remove
del
end

private
def set_default
mark if default
end
end
Loading