Skip to content
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

Improved version of UniqueList: OrderedSet #76

Merged
merged 19 commits into from
Jun 18, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,12 @@ integer_list << 4 # => RPUSH myintegerlist "4"
[ 1, 2, 3, 4 ] == 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"
unique_list.prepend(%w[ 1 2 3 4 ]) # => LREM myuniquelist 0, "1" + LREM myuniquelist 0, "2" + LREM myuniquelist 0, "3" + LREM myuniquelist 0, "4" + LPUSH myuniquelist "1", "2", "3", "4"
unique_list.append(%w[ 2 3 4 ]) # => ZADD myuniquelist 1645805148.509043932 2 1645805148.509057045 3 1645805148.509061813 4
unique_list.prepend(%w[ 1 2 3 4 ]) # => ZADD myuniquelist -1645805148.510507107 1 -1645805148.510514021 2 -1645805148.510519028 3 -1645805148.510522127 4
unique_list.append([])
unique_list << "5" # => LREM myuniquelist 0, "5" + RPUSH myuniquelist "5"
unique_list.remove(3) # => LREM myuniquelist 0, "3"
[ "4", "2", "1", "5" ] == unique_list.elements # => LRANGE myuniquelist 0, -1
unique_list << "5" # => ZADD myuniquelist 1645805148.510908842 5
unique_list.remove(3) # => ZREM myuniquelist 3
[ "4", "2", "1", "5" ] == unique_list.elements # => ZRANGE myuniquelist 0 -1

set = Kredis.set "myset", typed: :datetime
set.add(DateTime.tomorrow, DateTime.yesterday) # => SADD myset "2021-02-03 00:00:00 +0100" "2021-02-01 00:00:00 +0100"
Expand Down
7 changes: 6 additions & 1 deletion lib/kredis/types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ def unique_list(key, typed: :string, limit: nil, config: :shared, after_change:
type_from(UniqueList, config, key, after_change: after_change, typed: typed, limit: limit)
end

def unique_list_legacy(key, typed: :string, limit: nil, config: :shared, after_change: nil)
type_from(UniqueListLegacy, config, key, after_change: after_change, 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)
end
Expand All @@ -81,7 +85,7 @@ def slots(key, available:, config: :shared, after_change: nil)

private
def type_from(type_klass, config, key, after_change: nil, **options)
type_klass.new(configured_for(config), namespaced_key(key), **options).then do |type|
type_klass.new(configured_for(config), namespaced_key(key), config: config, after_change: after_change, **options).then do |type|
after_change ? CallbacksProxy.new(type, after_change) : type
end
end
Expand All @@ -98,5 +102,6 @@ def type_from(type_klass, config, key, after_change: nil, **options)
require "kredis/types/hash"
require "kredis/types/list"
require "kredis/types/unique_list"
require "kredis/types/unique_list_legacy"
require "kredis/types/set"
require "kredis/types/slots"
2 changes: 2 additions & 0 deletions lib/kredis/types/proxy/failsafe.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ def initialize(*)

def failsafe
yield
rescue Redis::CommandError => e
raise if e.message.include? "WRONGTYPE" || fail_safe_suppressed?
rescue Redis::BaseError
raise if fail_safe_suppressed?
end
Expand Down
2 changes: 1 addition & 1 deletion lib/kredis/types/proxying.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
require "active_support/core_ext/module/delegation"

class Kredis::Types::Proxying
attr_accessor :proxy, :redis, :key
attr_accessor :proxy, :redis, :key, :config, :after_change

def self.proxying(*commands)
delegate *commands, to: :proxy
Expand Down
87 changes: 71 additions & 16 deletions lib/kredis/types/unique_list.rb
Original file line number Diff line number Diff line change
@@ -1,29 +1,84 @@
# You'd normally call this a set, but Redis already has another data type for that
class Kredis::Types::UniqueList < Kredis::Types::List
proxying :multi, :ltrim, :exists?
class Kredis::Types::UniqueList < Kredis::Types::Proxying
proxying :multi, :zrange, :zrem, :zadd, :zremrangebyrank, :exists?, :del

attr_accessor :typed, :limit

def prepend(elements)
elements = Array(elements).uniq
return if elements.empty?
def elements
auto_fallback(:elements) do
strings_to_types(zrange(0, -1) || [], typed)
end
end
alias to_a elements

multi do
remove elements
super
ltrim 0, (limit - 1) if limit
def remove(*elements)
auto_migrate do
zrem(types_to_strings(elements, typed))
end
end

def append(elements)
elements = Array(elements).uniq
return if elements.empty?
def prepend(elements)
auto_migrate do
insert(elements, prepending: true)
end
end

multi do
remove elements
super
ltrim -limit, -1 if limit
def append(elements)
auto_migrate do
insert(elements)
end
end
alias << append

private
def insert(elements, prepending: false)
elements = Array(elements)
return if elements.empty?

elements_with_scores = types_to_strings(elements, typed).map do |element|
[ current_nanoseconds(negative: prepending), element ]
end

multi do
zadd(elements_with_scores)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Consider LT / GT options for ordering safety, avoiding problems with clock skew.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not possible due to Redis server compatibility target being version 4.

trim(from_beginning: prepending)
end
end

def current_nanoseconds(negative:)
"%10.9f" % (negative ? -Time.now.to_f : Time.now.to_f)

This comment was marked as resolved.

end

def trim(from_beginning:)
return unless limit&.positive?
Copy link
Member

Choose a reason for hiding this comment

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

Are negative/zero limits supported? If not, would fail earlier, e.g. in a #limit= method, and only check for nil limit here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea!


if from_beginning
zremrangebyrank(limit, -1)
else
zremrangebyrank(0, -(limit + 1))
end
end

def auto_fallback(method)
yield
rescue Redis::CommandError
legacy_list.send(method)
end

def auto_migrate
yield
rescue Redis::CommandError
migrate_list_to_sorted_set
retry
end

def migrate_list_to_sorted_set
legacy_elements = legacy_list.elements
legacy_list.del
append(legacy_elements)
end

This comment was marked as resolved.

Copy link
Member

Choose a reason for hiding this comment

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

Does this need to be within an atomic MULTI…EXEC?


def legacy_list
Kredis.unique_list_legacy(key, typed: typed, limit: limit, config: config, after_change: after_change)
end
end
29 changes: 29 additions & 0 deletions lib/kredis/types/unique_list_legacy.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# You'd normally call this a set, but Redis already has another data type for that
class Kredis::Types::UniqueListLegacy < Kredis::Types::List
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed the existing UniqueList to UniqueListLegacy.

proxying :multi, :ltrim, :exists?

attr_accessor :typed, :limit

def prepend(elements)
elements = Array(elements).uniq
return if elements.empty?

multi do
remove elements
super
ltrim 0, (limit - 1) if limit
end
end

def append(elements)
elements = Array(elements).uniq
return if elements.empty?

multi do
remove elements
super
ltrim -limit, -1 if limit
end
end
alias << append
end
28 changes: 28 additions & 0 deletions test/types/unique_list_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,32 @@ class UniqueListTest < ActiveSupport::TestCase
@list.prepend(%w[ 1 1 1 ])
assert_equal %w[ 1 ], @list.elements
end

class LegacyTest < ActiveSupport::TestCase
setup do
@legacy_list = Kredis.unique_list_legacy "myuniquelist", limit: 5
@legacy_list.append(%w[ 1 2 3 ])

@list = Kredis.unique_list "myuniquelist", limit: 5
end

test "reading from a legacy UniqueList backed by a Redis list still works" do
assert_equal %w[ 1 2 3 ], @list.elements
end

test "writing to a UniqueList previously backed by a Redis list automatically migrates" do
@list.append(%w[ 4 ])
assert_equal %w[ 1 2 3 4 ], @list.elements
end

test "prepending to a UniqueList previously backed by a Redis list automatically migrates" do
@list.prepend(%w[ 9 ])
assert_equal %w[ 9 1 2 3 ], @list.elements
end

test "removing an element from a UniqueList previously backed by a Redis list automatically migrates" do
@list.remove(%w[ 3 ])
assert_equal %w[ 1 2 ], @list.elements
end
end
end