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

Support expires_in for List, Set, UniqueList, OrderedSet #143

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,18 @@ limiter.poke # => SET limiter 0 NX + INCRBY limiter 1
false == limiter.exceeded? # => GET "limiter"
```

Lists, unique lists, sets, ordered sets, and hashes support expiration:

```ruby
set = Kredis.set "myset", expires_in: 1.second
set.add "hello", "world" # => SADD myset "hello" "world"
true == set.include?("hello") # => SISMEMBER myset "hello
sleep 2
[] == set.members # => SMEMBERS myset
```

To support lower versions of redis, which does not has `nx` option on `EXPIRE` command, multiple commands are used to achieve the same effect.

### Models

You can use all these structures in models:
Expand All @@ -189,6 +201,7 @@ class Person < ApplicationRecord
kredis_unique_list :skills, limit: 2
kredis_enum :morning, values: %w[ bright blue black ], default: "bright"
kredis_counter :steps, expires_in: 1.hour
kredis_set :favorite_colors, expires_in: 1.day

private
def generate_names_key
Expand Down
1 change: 1 addition & 0 deletions lib/kredis.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
require "kredis/namespace"
require "kredis/type_casting"
require "kredis/default_values"
require "kredis/expiration"
require "kredis/types"
require "kredis/attributes"

Expand Down
20 changes: 10 additions & 10 deletions lib/kredis/attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,20 @@ def kredis_json(name, key: nil, default: nil, config: :shared, after_change: 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, 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
def kredis_list(name, key: nil, default: nil, typed: :string, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, typed: typed, config: config, after_change: after_change, expires_in: expires_in
end

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
def kredis_unique_list(name, limit: nil, key: nil, default: nil, typed: :string, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, limit: limit, typed: typed, config: config, after_change: after_change, expires_in: expires_in
end

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
def kredis_set(name, key: nil, default: nil, typed: :string, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, typed: typed, config: config, after_change: after_change, expires_in: expires_in
end

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
def kredis_ordered_set(name, limit: nil, default: nil, key: nil, typed: :string, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, limit: limit, typed: typed, config: config, after_change: after_change, expires_in: expires_in
end

def kredis_slot(name, key: nil, config: :shared, after_change: nil)
Expand All @@ -76,8 +76,8 @@ def kredis_limiter(name, limit:, key: nil, config: :shared, after_change: nil, e
kredis_connection_with __method__, name, key, limit: limit, config: config, after_change: after_change, expires_in: expires_in
end

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
def kredis_hash(name, key: nil, default: nil, typed: :string, config: :shared, after_change: nil, expires_in: nil)
kredis_connection_with __method__, name, key, default: default, typed: typed, config: config, after_change: after_change, expires_in: expires_in
end

def kredis_boolean(name, key: nil, default: nil, config: :shared, after_change: nil, expires_in: nil)
Expand Down
19 changes: 19 additions & 0 deletions lib/kredis/expiration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Kredis::Expiration
extend ActiveSupport::Concern

included do
proxying :ttl, :expire
attr_accessor :expires_in
end

private
def with_expiration(suppress: false, &block)
result = block.call
if !suppress && expires_in && ttl < 0
expire expires_in.to_i
end
result
end
end
20 changes: 10 additions & 10 deletions lib/kredis/types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,24 +57,24 @@ 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, default: nil, config: :shared, after_change: nil)
type_from(Hash, config, key, after_change: after_change, default: default, typed: typed)
def hash(key, typed: :string, default: nil, config: :shared, after_change: nil, expires_in: nil)
type_from(Hash, config, key, after_change: after_change, default: default, typed: typed, expires_in: expires_in)
end

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)
def list(key, default: nil, typed: :string, config: :shared, after_change: nil, expires_in: nil)
type_from(List, config, key, after_change: after_change, default: default, typed: typed, expires_in: expires_in)
end

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)
def unique_list(key, default: nil, typed: :string, limit: nil, config: :shared, after_change: nil, expires_in: nil)
type_from(UniqueList, config, key, after_change: after_change, default: default, typed: typed, limit: limit, expires_in: expires_in)
end

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)
def set(key, default: nil, typed: :string, config: :shared, after_change: nil, expires_in: nil)
type_from(Set, config, key, after_change: after_change, default: default, typed: typed, expires_in: expires_in)
end

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)
def ordered_set(key, default: nil, typed: :string, limit: nil, config: :shared, after_change: nil, expires_in: nil)
type_from(OrderedSet, config, key, after_change: after_change, default: default, typed: typed, limit: limit, expires_in: expires_in)
end

def slot(key, config: :shared, after_change: nil)
Expand Down
5 changes: 4 additions & 1 deletion lib/kredis/types/hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

class Kredis::Types::Hash < Kredis::Types::Proxying
prepend Kredis::DefaultValues
include Kredis::Expiration

proxying :hget, :hset, :hmget, :hdel, :hgetall, :hkeys, :hvals, :del, :exists?

Expand All @@ -18,7 +19,9 @@ def []=(key, value)
end

def update(**entries)
hset entries.transform_values { |val| type_to_string(val, typed) }.compact if entries.flatten.any?
with_expiration do
hset entries.transform_values { |val| type_to_string(val, typed) }.compact if entries.flatten.any?
end
end

def values_at(*keys)
Expand Down
17 changes: 13 additions & 4 deletions lib/kredis/types/list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

class Kredis::Types::List < Kredis::Types::Proxying
prepend Kredis::DefaultValues
include Kredis::Expiration

proxying :lrange, :lrem, :lpush, :ltrim, :rpush, :exists?, :del

Expand All @@ -16,12 +17,20 @@ def remove(*elements)
types_to_strings(elements, typed).each { |element| lrem 0, element }
end

def prepend(*elements)
lpush types_to_strings(elements, typed) if elements.flatten.any?
def prepend(*elements, suppress_expiration: false)
return if elements.flatten.empty?

with_expiration(suppress: suppress_expiration) do
lpush types_to_strings(elements, typed)
end
end

def append(*elements)
rpush types_to_strings(elements, typed) if elements.flatten.any?
def append(*elements, suppress_expiration: false)
return if elements.flatten.empty?

with_expiration(suppress: suppress_expiration) do
rpush types_to_strings(elements, typed)
end
end
alias << append

Expand Down
9 changes: 6 additions & 3 deletions lib/kredis/types/ordered_set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

class Kredis::Types::OrderedSet < Kredis::Types::Proxying
prepend Kredis::DefaultValues
include Kredis::Expiration

proxying :multi, :zrange, :zrem, :zadd, :zremrangebyrank, :zcard, :exists?, :del, :zscore

Expand Down Expand Up @@ -53,9 +54,11 @@ def insert(elements, prepending: false)
[ score, element ]
end

multi do
zadd(elements_with_scores)
trim(from_beginning: prepending)
with_expiration do
multi do
zadd(elements_with_scores)
trim(from_beginning: prepending)
end
end
end

Expand Down
7 changes: 6 additions & 1 deletion lib/kredis/types/set.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

class Kredis::Types::Set < Kredis::Types::Proxying
prepend Kredis::DefaultValues
include Kredis::Expiration

proxying :smembers, :sadd, :srem, :multi, :del, :sismember, :scard, :spop, :exists?, :srandmember

Expand All @@ -13,7 +14,11 @@ def members
alias to_a members

def add(*members)
sadd types_to_strings(members, typed) if members.flatten.any?
return unless members.flatten.any?

with_expiration do
sadd types_to_strings(members, typed)
end
end
alias << add

Expand Down
21 changes: 13 additions & 8 deletions lib/kredis/types/unique_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,33 @@
# 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?
include Kredis::Expiration

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
with_expiration do
multi do
remove elements
super(elements, suppress_expiration: true)
ltrim 0, (limit - 1) if limit
end
end
end

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

multi do
remove elements
super
ltrim(-limit, -1) if limit
with_expiration do
multi do
remove elements
super(elements, suppress_expiration: true)
ltrim(-limit, -1) if limit
end
end
end
alias << append
Expand Down
45 changes: 45 additions & 0 deletions test/attributes_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ class Person
kredis_list :names_with_custom_key_via_lambda, key: ->(p) { "person:#{p.id}:names_customized" }
kredis_list :names_with_custom_key_via_method, key: :generate_key
kredis_list :names_with_default_via_lambda, default: ->(p) { [ "Random", p.name ] }
kredis_list :names_with_ttl, expires_in: 1.second
kredis_unique_list :skills, limit: 2
kredis_unique_list :skills_with_default_via_lambda, default: ->(p) { [ "Random", "Random", p.name ] }
kredis_unique_list :skills_with_ttl, expires_in: 1.second
kredis_ordered_set :reading_list, limit: 2
kredis_ordered_set :reading_list_with_ttl, expires_in: 1.second
kredis_flag :special
kredis_flag :temporary_special, expires_in: 1.second
kredis_string :address
Expand All @@ -34,6 +37,7 @@ class Person
kredis_slots :meetings, available: 3
kredis_set :vacations
kredis_set :vacations_with_default_via_lambda, default: ->(p) { JSON.parse(p.vacation_destinations).map { |location| location["city"] } }
kredis_set :vacations_with_ttl, expires_in: 1.second
kredis_json :settings
kredis_json :settings_with_default_via_lambda, default: ->(p) { JSON.parse(p.anthropometry).merge(eye_color: p.eye_color) }
kredis_counter :amount
Expand All @@ -42,6 +46,7 @@ class Person
kredis_string :temporary_password, expires_in: 1.second
kredis_hash :high_scores, typed: :integer
kredis_hash :high_scores_with_default_via_lambda, typed: :integer, default: ->(p) { { high_score: JSON.parse(p.scores).max } }
kredis_hash :high_scores_with_ttl, typed: :integer, expires_in: 1.second
kredis_boolean :onboarded
kredis_boolean :adult_with_default_via_lambda, default: ->(p) { Date.today.year - p.birthdate.year >= 18 }
kredis_limiter :update_limit, limit: 3, expires_in: 1.second
Expand Down Expand Up @@ -148,6 +153,14 @@ class AttributesTest < ActiveSupport::TestCase
assert_equal %w[ Random Jason ], Kredis.redis.lrange("people:8:names_with_default_via_lambda", 0, -1)
end

test "list with ttl" do
@person.names_with_ttl.append(%w[ david kasper ])
assert_equal %w[ david kasper ], @person.names_with_ttl.elements

sleep 1.1
assert_equal [], @person.names_with_ttl.elements
end

test "unique list" do
@person.skills.prepend(%w[ trolling photography ])
@person.skills.prepend("racing")
Expand All @@ -160,11 +173,27 @@ class AttributesTest < ActiveSupport::TestCase
assert_equal %w[ Random Jason ], Kredis.redis.lrange("people:8:skills_with_default_via_lambda", 0, -1)
end

test "unique list with ttl" do
@person.skills_with_ttl.prepend(%w[ trolling photography ])
assert_equal %w[ trolling photography ].to_set, @person.skills_with_ttl.elements.to_set

sleep 1.1
assert_equal [], @person.skills_with_ttl.elements
end

test "ordered set" do
@person.reading_list.prepend(%w[ rework shapeup remote ])
assert_equal %w[ remote shapeup ], @person.reading_list.elements
end

test "ordered set with ttl" do
@person.reading_list_with_ttl.prepend(%w[ rework ])
assert_equal %w[ rework ], @person.reading_list_with_ttl.elements

sleep 1.1
assert_equal [], @person.reading_list_with_ttl.elements
end

test "flag" do
assert_not @person.special?

Expand Down Expand Up @@ -324,6 +353,14 @@ class AttributesTest < ActiveSupport::TestCase
assert_equal [ "Paris" ], Kredis.redis.smembers("people:8:vacations_with_default_via_lambda")
end

test "set with ttl" do
@person.vacations_with_ttl.add "paris"
assert_equal [ "paris" ], @person.vacations_with_ttl.members

sleep 1.1
assert_equal [], @person.vacations_with_ttl.members
end

test "json" do
@person.settings.value = { "color" => "red", "count" => 2 }
assert_equal({ "color" => "red", "count" => 2 }, @person.settings.value)
Expand Down Expand Up @@ -368,6 +405,14 @@ class AttributesTest < ActiveSupport::TestCase
assert_equal({ "high_score" => 28 }, @person.high_scores_with_default_via_lambda.to_h)
end

test "hash with ttl" do
@person.high_scores_with_ttl.update(the_lost_viking: 99)
assert_equal({ "the_lost_viking" => 99 }, @person.high_scores_with_ttl.to_h)

sleep 1.1
assert_equal({}, @person.high_scores_with_ttl.to_h)
end

test "boolean" do
@person.onboarded.value = true
assert @person.onboarded.value
Expand Down
10 changes: 10 additions & 0 deletions test/types/hash_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,14 @@ class HashTest < ActiveSupport::TestCase
assert_nil @hash["key"]
assert_equal "value2", @hash["key2"]
end

test "support ttl" do
@hash = Kredis.hash "myhash", expires_in: 1.second
@hash[:key] = :value
@hash.update("key2" => "value2", "key3" => "value3")
assert_equal "value", @hash[:key]

sleep 1.1
assert_equal [], @hash.keys
end
end
Loading