familia 1.0.0.pre.rc3 → 1.0.0.pre.rc4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +198 -48
  4. data/VERSION.yml +1 -1
  5. data/lib/familia/base.rb +29 -1
  6. data/lib/familia/features/expiration.rb +90 -0
  7. data/lib/familia/features/quantization.rb +56 -0
  8. data/lib/familia/features/safe_dump.rb +2 -33
  9. data/lib/familia/features.rb +5 -4
  10. data/lib/familia/horreum/class_methods.rb +112 -46
  11. data/lib/familia/horreum/commands.rb +9 -3
  12. data/lib/familia/horreum/relations_management.rb +2 -2
  13. data/lib/familia/horreum/serialization.rb +23 -42
  14. data/lib/familia/horreum/settings.rb +0 -8
  15. data/lib/familia/horreum/utils.rb +0 -1
  16. data/lib/familia/horreum.rb +1 -1
  17. data/lib/familia/logging.rb +26 -4
  18. data/lib/familia/redistype/serialization.rb +60 -38
  19. data/lib/familia/redistype.rb +45 -17
  20. data/lib/familia/settings.rb +11 -1
  21. data/lib/familia/tools.rb +68 -0
  22. data/lib/familia/types/hashkey.rb +5 -5
  23. data/lib/familia/types/list.rb +2 -2
  24. data/lib/familia/types/sorted_set.rb +12 -12
  25. data/lib/familia/types/string.rb +1 -1
  26. data/lib/familia/types/unsorted_set.rb +2 -2
  27. data/lib/familia/utils.rb +106 -51
  28. data/lib/familia/version.rb +2 -2
  29. data/try/10_familia_try.rb +4 -4
  30. data/try/20_redis_type_try.rb +9 -6
  31. data/try/26_redis_bool_try.rb +1 -1
  32. data/try/27_redis_horreum_try.rb +1 -1
  33. data/try/30_familia_object_try.rb +3 -2
  34. data/try/40_customer_try.rb +3 -3
  35. data/try/test_helpers.rb +9 -2
  36. metadata +5 -5
  37. data/lib/familia/features/api_version.rb +0 -19
  38. data/lib/familia/features/atomic_saves.rb +0 -8
  39. data/lib/familia/features/quantizer.rb +0 -35
@@ -4,63 +4,80 @@ class Familia::RedisType
4
4
 
5
5
  module Serialization
6
6
 
7
- # Serializes an individual value for storage in Redis.
8
- #
9
- # This method prepares a value for storage in Redis by converting it to a string representation.
10
- # If a class option is specified, it uses that class's serialization method.
11
- # Otherwise, it relies on the value's own `to_s` method for serialization.
7
+ # Serializes a value for storage in Redis.
12
8
  #
13
9
  # @param val [Object] The value to be serialized.
14
- # @param strict_values [Boolean] Whether to enforce strict value serialization (default: true). Only applies when no class option is specified because the class option is assumed to handle its own serialization.
15
- # @return [String] The serialized representation of the value.
10
+ # @param strict_values [Boolean] Whether to enforce strict value
11
+ # serialization (default: true).
12
+ # @return [String, nil] The serialized representation of the value, or nil
13
+ # if serialization fails.
16
14
  #
17
- # @note When no class option is specified, this method attempts to serialize the value directly.
18
- # If the serialization fails, it falls back to the value's own string representation.
15
+ # @note When a class option is specified, it uses that class's
16
+ # serialization method. Otherwise, it relies on Familia.distinguisher for
17
+ # serialization.
19
18
  #
20
19
  # @example With a class option
21
- # to_redis(User.new(name: "John"), strict_values: false) #=> '{"name":"John"}'
22
- # to_redis(nil, strict_values: false) #=> "" (empty string)
23
- # to_redis(true, strict_values: false) #=> "true"
20
+ # to_redis(User.new(name: "Cloe"), strict_values: false) #=> '{"name":"Cloe"}'
24
21
  #
25
- # @example Without a class option and strict values
26
- # to_redis(123) #=> "123" (which becomes "123" in Redis)
22
+ # @example Without a class option
23
+ # to_redis(123) #=> "123"
27
24
  # to_redis("hello") #=> "hello"
28
- # to_redis(nil) # raises an exception
29
- # to_redis(true) # raises an exception
30
25
  #
31
- # @raise [Familia::HighRiskFactor]
26
+ # @raise [Familia::HighRiskFactor] If serialization fails under strict
27
+ # mode.
32
28
  #
33
29
  def to_redis(val, strict_values = true)
34
- ret = nil
30
+ prepared = nil
35
31
 
36
32
  Familia.trace :TOREDIS, redis, "#{val}<#{val.class}|#{opts[:class]}>", caller(1..1) if Familia.debug?
37
33
 
38
34
  if opts[:class]
39
- ret = Familia.distinguisher(opts[:class], strict_values)
40
- Familia.ld " from opts[class] <#{opts[:class]}>: #{ret||'<nil>'}"
35
+ prepared = Familia.distinguisher(opts[:class], strict_values)
36
+ Familia.ld " from opts[class] <#{opts[:class]}>: #{prepared||'<nil>'}"
41
37
  end
42
38
 
43
- if ret.nil?
39
+ if prepared.nil?
44
40
  # Enforce strict values when no class option is specified
45
- ret = Familia.distinguisher(val, true)
46
- Familia.ld " from value #{val}<#{val.class}>: #{ret}<#{ret.class}>"
41
+ prepared = Familia.distinguisher(val, true)
42
+ Familia.ld " from <#{val.class}> => <#{prepared.class}>"
47
43
  end
48
44
 
49
- Familia.trace :TOREDIS, redis, "#{val}<#{val.class}|#{opts[:class]}> => #{ret}<#{ret.class}>", caller(1..1) if Familia.debug?
45
+ Familia.trace :TOREDIS, redis, "#{val}<#{val.class}|#{opts[:class]}> => #{prepared}<#{prepared.class}>", caller(1..1) if Familia.debug?
50
46
 
51
- Familia.warn "[#{self.class}\#to_redis] nil returned for #{opts[:class]}\##{name}" if ret.nil?
52
- ret
47
+ Familia.warn "[#{self.class}\#to_redis] nil returned for #{opts[:class]}\##{name}" if prepared.nil?
48
+ prepared
53
49
  end
54
50
 
51
+ # Deserializes multiple values from Redis, removing nil values.
52
+ #
53
+ # @param values [Array<String>] The values to deserialize.
54
+ # @return [Array<Object>] Deserialized objects, with nil values removed.
55
+ #
56
+ # @see #multi_from_redis_with_nil
57
+ #
55
58
  def multi_from_redis(*values)
56
- # Avoid using compact! here. Using compact! as the last expression in the method
57
- # can unintentionally return nil if no changes are made, which is not desirable.
58
- # Instead, use compact to ensure the method returns the expected value.
59
+ # Avoid using compact! here. Using compact! as the last expression in the
60
+ # method can unintentionally return nil if no changes are made, which is
61
+ # not desirable. Instead, use compact to ensure the method returns the
62
+ # expected value.
59
63
  multi_from_redis_with_nil(*values).compact
60
64
  end
61
65
 
66
+ # Deserializes multiple values from Redis, preserving nil values.
67
+ #
68
+ # @param values [Array<String>] The values to deserialize.
69
+ # @return [Array<Object, nil>] Deserialized objects, including nil values.
70
+ #
71
+ # @raise [Familia::Problem] If the specified class doesn't respond to the
72
+ # load method.
73
+ #
74
+ # @note This method attempts to deserialize each value using the specified
75
+ # class's load method. If deserialization fails for a value, it's
76
+ # replaced with nil.
77
+ #
62
78
  # NOTE: `multi` in this method name refers to multiple values from
63
79
  # redis and not the Redis server MULTI command.
80
+ #
64
81
  def multi_from_redis_with_nil(*values)
65
82
  Familia.ld "multi_from_redis: (#{@opts}) #{values}"
66
83
  return [] if values.empty?
@@ -89,6 +106,20 @@ class Familia::RedisType
89
106
  values
90
107
  end
91
108
 
109
+ # Deserializes a single value from Redis.
110
+ #
111
+ # @param val [String, nil] The value to deserialize.
112
+ # @return [Object, nil] The deserialized object, the default value if
113
+ # val is nil, or nil if deserialization fails.
114
+ #
115
+ # @note If no class option is specified, the original value is
116
+ # returned unchanged.
117
+ #
118
+ # NOTE: Currently only the RedisType class uses this method. Horreum
119
+ # fields are a newer addition and don't support the full range of
120
+ # deserialization options that RedisType supports. It uses to_redis
121
+ # for serialization since everything becomes a string in Redis.
122
+ #
92
123
  def from_redis(val)
93
124
  return @opts[:default] if val.nil?
94
125
  return val unless @opts[:class]
@@ -96,15 +127,6 @@ class Familia::RedisType
96
127
  ret = multi_from_redis val
97
128
  ret&.first # return the object or nil
98
129
  end
99
-
100
- def update_expiration(ttl = nil)
101
- ttl ||= opts[:ttl]
102
- return if ttl.to_i.zero? # nil will be zero
103
-
104
- Familia.ld "#{rediskey} to #{ttl}"
105
- expire ttl.to_i
106
- end
107
-
108
130
  end
109
131
 
110
132
  end
@@ -13,31 +13,29 @@ module Familia
13
13
  # @abstract Subclass and implement Redis data type specific methods
14
14
  class RedisType
15
15
  include Familia::Base
16
+ extend Familia::Features
16
17
 
17
18
  @registered_types = {}
18
19
  @valid_options = %i[class parent ttl default db key redis]
19
20
  @db = nil
20
- @ttl = nil
21
+
22
+ feature :expiration
23
+ feature :quantization
21
24
 
22
25
  class << self
23
26
  attr_reader :registered_types, :valid_options
24
27
  attr_accessor :parent
25
- attr_writer :ttl, :db, :uri
28
+ attr_writer :db, :uri
26
29
 
27
30
  # To be called inside every class that inherits RedisType
28
31
  # +methname+ is the term used for the class and instance methods
29
32
  # that are created for the given +klass+ (e.g. set, list, etc)
30
33
  def register(klass, methname)
31
- Familia.ld "[#{self}] Registering #{klass} as #{methname}"
34
+ Familia.ld "[#{self}] Registering #{klass} as #{methname.inspect}"
32
35
 
33
36
  @registered_types[methname] = klass
34
37
  end
35
38
 
36
- def ttl(val = nil)
37
- @ttl = val unless val.nil?
38
- @ttl || parent&.ttl
39
- end
40
-
41
39
  def db(val = nil)
42
40
  @db = val unless val.nil?
43
41
  @db || parent&.db
@@ -49,8 +47,9 @@ module Familia
49
47
  end
50
48
 
51
49
  def inherited(obj)
50
+ Familia.trace :REDISTYPE, nil, "#{obj} is my kinda type", caller(1..1) if Familia.debug?
52
51
  obj.db = db
53
- obj.ttl = ttl
52
+ obj.ttl = ttl # method added via Features::Expiration
54
53
  obj.uri = uri
55
54
  obj.parent = self
56
55
  super(obj)
@@ -101,6 +100,12 @@ module Familia
101
100
  @opts = opts || {}
102
101
  @opts = RedisType.valid_keys_only(@opts)
103
102
 
103
+ # Apply the options to instance method setters of the same name
104
+ @opts.each do |k, v|
105
+ Familia.ld " [setting] #{k} #{v}"
106
+ send(:"#{k}=", v) if respond_to? :"#{k}="
107
+ end
108
+
104
109
  init if respond_to? :init
105
110
  end
106
111
 
@@ -110,10 +115,37 @@ module Familia
110
115
  parent? ? parent.redis : Familia.redis(opts[:db])
111
116
  end
112
117
 
113
- # Produces the full redis key for this object.
118
+ # Produces the full Redis key for this object.
119
+ #
120
+ # @return [String] The full Redis key.
121
+ #
122
+ # This method determines the appropriate Redis key based on the context of the RedisType object:
123
+ #
124
+ # 1. If a hardcoded key is set in the options, it returns that key.
125
+ # 2. For instance-level RedisType objects, it uses the parent instance's rediskey method.
126
+ # 3. For class-level RedisType objects, it uses the parent class's rediskey method.
127
+ # 4. For standalone RedisType objects, it uses the keystring as the full Redis key.
128
+ #
129
+ # For class-level RedisType objects (parent_class? == true):
130
+ # - The suffix is optional and used to differentiate between different types of objects.
131
+ # - If no suffix is provided, the class's default suffix is used (via the self.suffix method).
132
+ # - If a nil suffix is explicitly passed, it won't appear in the resulting Redis key.
133
+ # - Passing nil as the suffix is how class-level RedisType objects are created without
134
+ # the global default 'object' suffix.
135
+ #
136
+ # @example Instance-level RedisType
137
+ # user_instance.some_redistype.rediskey # => "user:123:some_redistype"
138
+ #
139
+ # @example Class-level RedisType
140
+ # User.some_redistype.rediskey # => "user:some_redistype"
141
+ #
142
+ # @example Standalone RedisType
143
+ # RedisType.new("mykey").rediskey # => "mykey"
144
+ #
145
+ # @example Class-level RedisType with explicit nil suffix
146
+ # User.rediskey("123", nil) # => "user:123"
147
+ #
114
148
  def rediskey
115
- Familia.ld "[rediskey] #{keystring} for #{self.class} (#{opts})"
116
-
117
149
  # Return the hardcoded key if it's set. This is useful for
118
150
  # support legacy keys that aren't derived in the same way.
119
151
  return opts[:key] if opts[:key]
@@ -128,7 +160,7 @@ module Familia
128
160
  parent.rediskey(keystring, nil)
129
161
  else
130
162
  # This is a standalone RedisType object where it's keystring
131
- # is the full key.
163
+ # is the full redis key.
132
164
  keystring
133
165
  end
134
166
  end
@@ -153,10 +185,6 @@ module Familia
153
185
  @opts[:parent]
154
186
  end
155
187
 
156
- def ttl
157
- @opts[:ttl] || self.class.ttl
158
- end
159
-
160
188
  def db
161
189
  @opts[:db] || self.class.db
162
190
  end
@@ -5,7 +5,7 @@ module Familia
5
5
  @delim = ':'
6
6
  @prefix = nil
7
7
  @suffix = :object
8
- @ttl = nil
8
+ @ttl = 0 # see update_expiration. Zero is skip. nil is an exception.
9
9
  @db = nil
10
10
 
11
11
  module Settings
@@ -27,6 +27,16 @@ module Familia
27
27
  @suffix
28
28
  end
29
29
 
30
+ def ttl(v = nil)
31
+ @ttl = v unless v.nil?
32
+ @ttl
33
+ end
34
+
35
+ def db(v = nil)
36
+ @db = v unless v.nil?
37
+ @db
38
+ end
39
+
30
40
  # We define this do-nothing method because it reads better
31
41
  # than simply Familia.suffix in some contexts.
32
42
  def default_suffix
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ module Tools
5
+ extend self
6
+ def move_keys(filter, source_uri, target_uri, &each_key)
7
+ raise "Source and target are the same (#{target_uri})" if target_uri == source_uri
8
+
9
+ Familia.connect target_uri
10
+ source_keys = Familia.redis(source_uri).keys(filter)
11
+ puts "Moving #{source_keys.size} keys from #{source_uri} to #{target_uri} (filter: #{filter})"
12
+ source_keys.each_with_index do |key, idx|
13
+ type = Familia.redis(source_uri).type key
14
+ ttl = Familia.redis(source_uri).ttl key
15
+ if source_uri.host == target_uri.host && source_uri.port == target_uri.port
16
+ Familia.redis(source_uri).move key, target_uri.db
17
+ else
18
+ case type
19
+ when 'string'
20
+ Familia.redis(source_uri).get key
21
+ when 'list'
22
+ Familia.redis(source_uri).lrange key, 0, -1
23
+ when 'set'
24
+ Familia.redis(source_uri).smembers key
25
+ else
26
+ raise Familia::Problem, "unknown key type: #{type}"
27
+ end
28
+ raise 'Not implemented'
29
+ end
30
+ yield(idx, type, key, ttl) unless each_key.nil?
31
+ end
32
+ end
33
+
34
+ # Use the return value from each_key as the new key name
35
+ def rename(filter, source_uri, target_uri = nil, &each_key)
36
+ target_uri ||= source_uri
37
+ move_keys filter, source_uri, target_uri if source_uri != target_uri
38
+ source_keys = Familia.redis(source_uri).keys(filter)
39
+ puts "Renaming #{source_keys.size} keys from #{source_uri} (filter: #{filter})"
40
+ source_keys.each_with_index do |key, idx|
41
+ Familia.trace :RENAME1, Familia.redis(source_uri), "#{key}", ''
42
+ type = Familia.redis(source_uri).type key
43
+ ttl = Familia.redis(source_uri).ttl key
44
+ newkey = yield(idx, type, key, ttl) unless each_key.nil?
45
+ Familia.trace :RENAME2, Familia.redis(source_uri), "#{key} -> #{newkey}", caller(1..1).first
46
+ Familia.redis(source_uri).renamenx key, newkey
47
+ end
48
+ end
49
+
50
+ def get_any(keyname, uri = nil)
51
+ type = Familia.redis(uri).type keyname
52
+ case type
53
+ when 'string'
54
+ Familia.redis(uri).get keyname
55
+ when 'list'
56
+ Familia.redis(uri).lrange(keyname, 0, -1) || []
57
+ when 'set'
58
+ Familia.redis(uri).smembers(keyname) || []
59
+ when 'zset'
60
+ Familia.redis(uri).zrange(keyname, 0, -1) || []
61
+ when 'hash'
62
+ Familia.redis(uri).hgetall(keyname) || {}
63
+ else
64
+ nil
65
+ end
66
+ end
67
+ end
68
+ end
@@ -49,12 +49,12 @@ module Familia
49
49
  end
50
50
 
51
51
  def values
52
- el = redis.hvals(rediskey)
53
- multi_from_redis(*el)
52
+ elements = redis.hvals(rediskey)
53
+ multi_from_redis(*elements)
54
54
  end
55
55
 
56
56
  def hgetall
57
- # TODO: Use from_redis. Also name `all` is confusing with
57
+ # TODO: Use from_redis. Also alias `all` is confusing with
58
58
  # Onetime::Customer.all which returns all customers.
59
59
  redis.hgetall rediskey
60
60
  end
@@ -98,8 +98,8 @@ module Familia
98
98
  alias merge! update
99
99
 
100
100
  def values_at *fields
101
- el = redis.hmget(rediskey, *fields.flatten.compact)
102
- multi_from_redis(*el)
101
+ elements = redis.hmget(rediskey, *fields.flatten.compact)
102
+ multi_from_redis(*elements)
103
103
  end
104
104
 
105
105
  Familia::RedisType.register self, :hash # legacy, deprecated
@@ -65,8 +65,8 @@ module Familia
65
65
  alias del delete
66
66
 
67
67
  def range(sidx = 0, eidx = -1)
68
- el = rangeraw sidx, eidx
69
- multi_from_redis(*el)
68
+ elements = rangeraw sidx, eidx
69
+ multi_from_redis(*elements)
70
70
  end
71
71
 
72
72
  def rangeraw(sidx = 0, eidx = -1)
@@ -75,8 +75,8 @@ module Familia
75
75
 
76
76
  def members(count = -1, opts = {})
77
77
  count -= 1 if count.positive?
78
- el = membersraw count, opts
79
- multi_from_redis(*el)
78
+ elements = membersraw count, opts
79
+ multi_from_redis(*elements)
80
80
  end
81
81
  alias to_a members
82
82
  alias all members
@@ -88,8 +88,8 @@ module Familia
88
88
 
89
89
  def revmembers(count = -1, opts = {})
90
90
  count -= 1 if count.positive?
91
- el = revmembersraw count, opts
92
- multi_from_redis(*el)
91
+ elements = revmembersraw count, opts
92
+ multi_from_redis(*elements)
93
93
  end
94
94
 
95
95
  def revmembersraw(count = -1, opts = {})
@@ -131,8 +131,8 @@ module Familia
131
131
 
132
132
  def range(sidx, eidx, opts = {})
133
133
  echo :range, caller(1..1).first if Familia.debug
134
- el = rangeraw(sidx, eidx, opts)
135
- multi_from_redis(*el)
134
+ elements = rangeraw(sidx, eidx, opts)
135
+ multi_from_redis(*elements)
136
136
  end
137
137
 
138
138
  def rangeraw(sidx, eidx, opts = {})
@@ -148,8 +148,8 @@ module Familia
148
148
 
149
149
  def revrange(sidx, eidx, opts = {})
150
150
  echo :revrange, caller(1..1).first if Familia.debug
151
- el = revrangeraw(sidx, eidx, opts)
152
- multi_from_redis(*el)
151
+ elements = revrangeraw(sidx, eidx, opts)
152
+ multi_from_redis(*elements)
153
153
  end
154
154
 
155
155
  def revrangeraw(sidx, eidx, opts = {})
@@ -159,8 +159,8 @@ module Familia
159
159
  # e.g. obj.metrics.rangebyscore (now-12.hours), now, :limit => [0, 10]
160
160
  def rangebyscore(sscore, escore, opts = {})
161
161
  echo :rangebyscore, caller(1..1).first if Familia.debug
162
- el = rangebyscoreraw(sscore, escore, opts)
163
- multi_from_redis(*el)
162
+ elements = rangebyscoreraw(sscore, escore, opts)
163
+ multi_from_redis(*elements)
164
164
  end
165
165
 
166
166
  def rangebyscoreraw(sscore, escore, opts = {})
@@ -171,8 +171,8 @@ module Familia
171
171
  # e.g. obj.metrics.revrangebyscore (now-12.hours), now, :limit => [0, 10]
172
172
  def revrangebyscore(sscore, escore, opts = {})
173
173
  echo :revrangebyscore, caller(1..1).first if Familia.debug
174
- el = revrangebyscoreraw(sscore, escore, opts)
175
- multi_from_redis(*el)
174
+ elements = revrangebyscoreraw(sscore, escore, opts)
175
+ multi_from_redis(*elements)
176
176
  end
177
177
 
178
178
  def revrangebyscoreraw(sscore, escore, opts = {})
@@ -14,7 +14,7 @@ module Familia
14
14
  end
15
15
 
16
16
  def value
17
- echo :value, caller[0..0] if Familia.debug
17
+ echo :value, caller(0..0) if Familia.debug
18
18
  redis.setnx rediskey, @opts[:default] if @opts[:default]
19
19
  from_redis redis.get(rediskey)
20
20
  end
@@ -23,8 +23,8 @@ module Familia
23
23
 
24
24
  def members
25
25
  echo :members, caller(1..1).first if Familia.debug
26
- el = membersraw
27
- multi_from_redis(*el)
26
+ elements = membersraw
27
+ multi_from_redis(*elements)
28
28
  end
29
29
  alias all members
30
30
  alias to_a members