familia 0.10.2 → 1.0.0.pre.rc2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.pre-commit-config.yaml +1 -1
  4. data/.rubocop.yml +75 -0
  5. data/.rubocop_todo.yml +63 -0
  6. data/Gemfile +6 -1
  7. data/Gemfile.lock +47 -15
  8. data/README.md +65 -13
  9. data/VERSION.yml +4 -3
  10. data/familia.gemspec +18 -13
  11. data/lib/familia/base.rb +33 -0
  12. data/lib/familia/connection.rb +87 -0
  13. data/lib/familia/core_ext.rb +119 -124
  14. data/lib/familia/errors.rb +33 -0
  15. data/lib/familia/features/api_version.rb +19 -0
  16. data/lib/familia/features/atomic_saves.rb +8 -0
  17. data/lib/familia/features/quantizer.rb +35 -0
  18. data/lib/familia/features/safe_dump.rb +194 -0
  19. data/lib/familia/features.rb +51 -0
  20. data/lib/familia/horreum/class_methods.rb +292 -0
  21. data/lib/familia/horreum/commands.rb +106 -0
  22. data/lib/familia/horreum/relations_management.rb +141 -0
  23. data/lib/familia/horreum/serialization.rb +193 -0
  24. data/lib/familia/horreum/settings.rb +63 -0
  25. data/lib/familia/horreum/utils.rb +44 -0
  26. data/lib/familia/horreum.rb +248 -0
  27. data/lib/familia/logging.rb +232 -0
  28. data/lib/familia/redistype/commands.rb +56 -0
  29. data/lib/familia/redistype/serialization.rb +110 -0
  30. data/lib/familia/redistype.rb +185 -0
  31. data/lib/familia/refinements.rb +88 -0
  32. data/lib/familia/settings.rb +38 -0
  33. data/lib/familia/types/hashkey.rb +107 -0
  34. data/lib/familia/types/list.rb +155 -0
  35. data/lib/familia/types/sorted_set.rb +234 -0
  36. data/lib/familia/types/string.rb +115 -0
  37. data/lib/familia/types/unsorted_set.rb +123 -0
  38. data/lib/familia/utils.rb +125 -0
  39. data/lib/familia/version.rb +25 -0
  40. data/lib/familia.rb +57 -161
  41. data/lib/redis_middleware.rb +109 -0
  42. data/try/00_familia_try.rb +5 -4
  43. data/try/10_familia_try.rb +21 -17
  44. data/try/20_redis_type_try.rb +67 -0
  45. data/try/{21_redis_object_zset_try.rb → 21_redis_type_zset_try.rb} +2 -2
  46. data/try/{22_redis_object_set_try.rb → 22_redis_type_set_try.rb} +2 -2
  47. data/try/{23_redis_object_list_try.rb → 23_redis_type_list_try.rb} +2 -2
  48. data/try/{24_redis_object_string_try.rb → 24_redis_type_string_try.rb} +6 -6
  49. data/try/{25_redis_object_hash_try.rb → 25_redis_type_hash_try.rb} +3 -3
  50. data/try/26_redis_bool_try.rb +10 -6
  51. data/try/27_redis_horreum_try.rb +93 -0
  52. data/try/30_familia_object_try.rb +21 -20
  53. data/try/35_feature_safedump_try.rb +83 -0
  54. data/try/40_customer_try.rb +140 -0
  55. data/try/41_customer_safedump_try.rb +86 -0
  56. data/try/test_helpers.rb +194 -0
  57. metadata +51 -47
  58. data/lib/familia/helpers.rb +0 -70
  59. data/lib/familia/object.rb +0 -533
  60. data/lib/familia/redisobject.rb +0 -1017
  61. data/lib/familia/test_helpers.rb +0 -40
  62. data/lib/familia/tools.rb +0 -67
  63. data/try/20_redis_object_try.rb +0 -44
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ class HashKey < RedisType
5
+ def size
6
+ redis.hlen rediskey
7
+ end
8
+ alias length size
9
+
10
+ def empty?
11
+ size.zero?
12
+ end
13
+
14
+ # +return+ [Integer] Returns 1 if the field is new and added, 0 if the
15
+ # field already existed and the value was updated.
16
+ def []=(field, val)
17
+ ret = redis.hset rediskey, field, to_redis(val)
18
+ update_expiration
19
+ ret
20
+ rescue TypeError => e
21
+ Familia.le "[hset]= #{e.message}"
22
+ Familia.ld "[hset]= #{rediskey} #{field}=#{val}" if Familia.debug
23
+ echo :hset, caller(1..1).first if Familia.debug # logs via echo to redis and back
24
+ klass = val.class
25
+ msg = "Cannot store #{field} => #{val.inspect} (#{klass}) in #{rediskey}"
26
+ raise e.class, msg
27
+ end
28
+ alias put []=
29
+ alias store []=
30
+
31
+ def [](field)
32
+ from_redis redis.hget(rediskey, field)
33
+ end
34
+ alias get []
35
+
36
+ def fetch(field, default = nil)
37
+ ret = self[field]
38
+ if ret.nil?
39
+ raise IndexError, "No such index for: #{field}" if default.nil?
40
+
41
+ default
42
+ else
43
+ ret
44
+ end
45
+ end
46
+
47
+ def keys
48
+ redis.hkeys rediskey
49
+ end
50
+
51
+ def values
52
+ el = redis.hvals(rediskey)
53
+ multi_from_redis(*el)
54
+ end
55
+
56
+ def hgetall
57
+ # TODO: Use from_redis. Also name `all` is confusing with
58
+ # Onetime::Customer.all which returns all customers.
59
+ redis.hgetall rediskey
60
+ end
61
+ alias all hgetall
62
+
63
+ def has_key?(field)
64
+ redis.hexists rediskey, field
65
+ end
66
+ alias include? has_key?
67
+ alias member? has_key?
68
+
69
+ def delete(field)
70
+ redis.hdel rediskey, field
71
+ end
72
+ alias remove delete
73
+ alias rem delete
74
+ alias del delete
75
+
76
+ def increment(field, by = 1)
77
+ redis.hincrby(rediskey, field, by).to_i
78
+ end
79
+ alias incr increment
80
+ alias incrby increment
81
+
82
+ def decrement(field, by = 1)
83
+ increment field, -by
84
+ end
85
+ alias decr decrement
86
+ alias decrby decrement
87
+
88
+ def update(hsh = {})
89
+ raise ArgumentError, 'Argument to bulk_set must be a hash' unless hsh.is_a?(Hash)
90
+
91
+ data = hsh.inject([]) { |ret, pair| ret << [pair[0], to_redis(pair[1])] }.flatten
92
+
93
+ ret = redis.hmset(rediskey, *data)
94
+ update_expiration
95
+ ret
96
+ end
97
+ alias merge! update
98
+
99
+ def values_at *fields
100
+ el = redis.hmget(rediskey, *fields.flatten.compact)
101
+ multi_from_redis(*el)
102
+ end
103
+
104
+ Familia::RedisType.register self, :hash # legacy, deprecated
105
+ Familia::RedisType.register self, :hashkey
106
+ end
107
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ class List < RedisType
5
+ def size
6
+ redis.llen rediskey
7
+ end
8
+ alias length size
9
+
10
+ def empty?
11
+ size.zero?
12
+ end
13
+
14
+ def push *values
15
+ echo :push, caller(1..1).first if Familia.debug
16
+ values.flatten.compact.each { |v| redis.rpush rediskey, to_redis(v) }
17
+ redis.ltrim rediskey, -@opts[:maxlength], -1 if @opts[:maxlength]
18
+ update_expiration
19
+ self
20
+ end
21
+
22
+ def <<(val)
23
+ push val
24
+ end
25
+ alias add <<
26
+
27
+ def unshift *values
28
+ values.flatten.compact.each { |v| redis.lpush rediskey, to_redis(v) }
29
+ # TODO: test maxlength
30
+ redis.ltrim rediskey, 0, @opts[:maxlength] - 1 if @opts[:maxlength]
31
+ update_expiration
32
+ self
33
+ end
34
+
35
+ def pop
36
+ from_redis redis.rpop(rediskey)
37
+ end
38
+
39
+ def shift
40
+ from_redis redis.lpop(rediskey)
41
+ end
42
+
43
+ def [](idx, count = nil)
44
+ if idx.is_a? Range
45
+ range idx.first, idx.last
46
+ elsif count
47
+ case count <=> 0
48
+ when 1 then range(idx, idx + count - 1)
49
+ when 0 then []
50
+ when -1 then nil
51
+ end
52
+ else
53
+ at idx
54
+ end
55
+ end
56
+ alias slice []
57
+
58
+ def delete(v, count = 0)
59
+ redis.lrem rediskey, count, to_redis(v)
60
+ end
61
+ alias remove delete
62
+ alias rem delete
63
+ alias del delete
64
+
65
+ def range(sidx = 0, eidx = -1)
66
+ el = rangeraw sidx, eidx
67
+ multi_from_redis(*el)
68
+ end
69
+
70
+ def rangeraw(sidx = 0, eidx = -1)
71
+ redis.lrange(rediskey, sidx, eidx)
72
+ end
73
+
74
+ def members(count = -1)
75
+ echo :members, caller(1..1).first if Familia.debug
76
+ count -= 1 if count.positive?
77
+ range 0, count
78
+ end
79
+ alias all members
80
+ alias to_a members
81
+
82
+ def membersraw(count = -1)
83
+ count -= 1 if count.positive?
84
+ rangeraw 0, count
85
+ end
86
+
87
+ def each(&blk)
88
+ range.each(&blk)
89
+ end
90
+
91
+ def each_with_index(&blk)
92
+ range.each_with_index(&blk)
93
+ end
94
+
95
+ def eachraw(&blk)
96
+ rangeraw.each(&blk)
97
+ end
98
+
99
+ def eachraw_with_index(&blk)
100
+ rangeraw.each_with_index(&blk)
101
+ end
102
+
103
+ def collect(&blk)
104
+ range.collect(&blk)
105
+ end
106
+
107
+ def select(&blk)
108
+ range.select(&blk)
109
+ end
110
+
111
+ def collectraw(&blk)
112
+ rangeraw.collect(&blk)
113
+ end
114
+
115
+ def selectraw(&blk)
116
+ rangeraw.select(&blk)
117
+ end
118
+
119
+ def at(idx)
120
+ from_redis redis.lindex(rediskey, idx)
121
+ end
122
+
123
+ def first
124
+ at 0
125
+ end
126
+
127
+ def last
128
+ at(-1)
129
+ end
130
+
131
+ # TODO: def replace
132
+ ## Make the value stored at KEY identical to the given list
133
+ # define_method :"#{name}_sync" do |*latest|
134
+ # latest = latest.flatten.compact
135
+ # # Do nothing if we're given an empty Array.
136
+ # # Otherwise this would clear all current values
137
+ # if latest.empty?
138
+ # false
139
+ # else
140
+ # # Convert to a list of index values if we got the actual objects
141
+ # latest = latest.collect { |obj| obj.index } if klass === latest.first
142
+ # current = send("#{name_plural}raw")
143
+ # added = latest-current
144
+ # removed = current-latest
145
+ # #Familia.info "#{self.index}: adding: #{added}"
146
+ # added.each { |v| self.send("add_#{name_singular}", v) }
147
+ # #Familia.info "#{self.index}: removing: #{removed}"
148
+ # removed.each { |v| self.send("remove_#{name_singular}", v) }
149
+ # true
150
+ # end
151
+ # end
152
+
153
+ Familia::RedisType.register self, :list
154
+ end
155
+ end
@@ -0,0 +1,234 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ class SortedSet < RedisType
5
+ def size
6
+ redis.zcard rediskey
7
+ end
8
+ alias length size
9
+
10
+ def empty?
11
+ size.zero?
12
+ end
13
+
14
+ # Adds a new element to the sorted set with the current timestamp as the
15
+ # score.
16
+ #
17
+ # This method provides a convenient way to add elements to the sorted set
18
+ # without explicitly specifying a score. It uses the current Unix timestamp
19
+ # as the score, which effectively sorts elements by their insertion time.
20
+ #
21
+ # @param val [Object] The value to be added to the sorted set.
22
+ # @return [Integer] Returns 1 if the element is new and added, 0 if the
23
+ # element already existed and the score was updated.
24
+ #
25
+ # @example
26
+ # sorted_set << "new_element"
27
+ #
28
+ # @note This is a non-standard operation for sorted sets as it doesn't allow
29
+ # specifying a custom score. Use `add` or `[]=` for more control.
30
+ #
31
+ def <<(val)
32
+ add(Time.now.to_i, val)
33
+ end
34
+
35
+ # NOTE: The argument order is the reverse of #add. We do this to
36
+ # more naturally align with how the [] and []= methods are used.
37
+ #
38
+ # e.g.
39
+ # obj.metrics[VALUE] = SCORE
40
+ # obj.metrics[VALUE] # => SCORE
41
+ #
42
+ def []=(val, score)
43
+ add score, val
44
+ end
45
+
46
+ def add(score, val)
47
+ ret = redis.zadd rediskey, score, to_redis(val)
48
+ update_expiration
49
+ ret
50
+ end
51
+
52
+ def score(val)
53
+ ret = redis.zscore rediskey, to_redis(val, false)
54
+ ret&.to_f
55
+ end
56
+ alias [] score
57
+
58
+ def member?(val)
59
+ Familia.trace :MEMBER, redis, "#{val}<#{val.class}>", caller(1..1) if Familia.debug?
60
+ !rank(val).nil?
61
+ end
62
+ alias include? member?
63
+
64
+ # rank of member +v+ when ordered lowest to highest (starts at 0)
65
+ def rank(v)
66
+ ret = redis.zrank rediskey, to_redis(v, false)
67
+ ret&.to_i
68
+ end
69
+
70
+ # rank of member +v+ when ordered highest to lowest (starts at 0)
71
+ def revrank(v)
72
+ ret = redis.zrevrank rediskey, to_redis(v, false)
73
+ ret&.to_i
74
+ end
75
+
76
+ def members(count = -1, opts = {})
77
+ count -= 1 if count.positive?
78
+ el = membersraw count, opts
79
+ multi_from_redis(*el)
80
+ end
81
+ alias to_a members
82
+ alias all members
83
+
84
+ def membersraw(count = -1, opts = {})
85
+ count -= 1 if count.positive?
86
+ rangeraw 0, count, opts
87
+ end
88
+
89
+ def revmembers(count = -1, opts = {})
90
+ count -= 1 if count.positive?
91
+ el = revmembersraw count, opts
92
+ multi_from_redis(*el)
93
+ end
94
+
95
+ def revmembersraw(count = -1, opts = {})
96
+ count -= 1 if count.positive?
97
+ revrangeraw 0, count, opts
98
+ end
99
+
100
+ def each(&blk)
101
+ members.each(&blk)
102
+ end
103
+
104
+ def each_with_index(&blk)
105
+ members.each_with_index(&blk)
106
+ end
107
+
108
+ def collect(&blk)
109
+ members.collect(&blk)
110
+ end
111
+
112
+ def select(&blk)
113
+ members.select(&blk)
114
+ end
115
+
116
+ def eachraw(&blk)
117
+ membersraw.each(&blk)
118
+ end
119
+
120
+ def eachraw_with_index(&blk)
121
+ membersraw.each_with_index(&blk)
122
+ end
123
+
124
+ def collectraw(&blk)
125
+ membersraw.collect(&blk)
126
+ end
127
+
128
+ def selectraw(&blk)
129
+ membersraw.select(&blk)
130
+ end
131
+
132
+ def range(sidx, eidx, opts = {})
133
+ echo :range, caller(1..1).first if Familia.debug
134
+ el = rangeraw(sidx, eidx, opts)
135
+ multi_from_redis(*el)
136
+ end
137
+
138
+ def rangeraw(sidx, eidx, opts = {})
139
+ # NOTE: :withscores (no underscore) is the correct naming for the
140
+ # redis-4.x gem. We pass :withscores through explicitly b/c
141
+ # redis.zrange et al only accept that one optional argument.
142
+ # Passing `opts`` through leads to an ArgumentError:
143
+ #
144
+ # sorted_sets.rb:374:in `zrevrange': wrong number of arguments (given 4, expected 3) (ArgumentError)
145
+ #
146
+ redis.zrange(rediskey, sidx, eidx, **opts)
147
+ end
148
+
149
+ def revrange(sidx, eidx, opts = {})
150
+ echo :revrange, caller(1..1).first if Familia.debug
151
+ el = revrangeraw(sidx, eidx, opts)
152
+ multi_from_redis(*el)
153
+ end
154
+
155
+ def revrangeraw(sidx, eidx, opts = {})
156
+ redis.zrevrange(rediskey, sidx, eidx, **opts)
157
+ end
158
+
159
+ # e.g. obj.metrics.rangebyscore (now-12.hours), now, :limit => [0, 10]
160
+ def rangebyscore(sscore, escore, opts = {})
161
+ echo :rangebyscore, caller(1..1).first if Familia.debug
162
+ el = rangebyscoreraw(sscore, escore, opts)
163
+ multi_from_redis(*el)
164
+ end
165
+
166
+ def rangebyscoreraw(sscore, escore, opts = {})
167
+ echo :rangebyscoreraw, caller(1..1).first if Familia.debug
168
+ redis.zrangebyscore(rediskey, sscore, escore, **opts)
169
+ end
170
+
171
+ # e.g. obj.metrics.revrangebyscore (now-12.hours), now, :limit => [0, 10]
172
+ def revrangebyscore(sscore, escore, opts = {})
173
+ echo :revrangebyscore, caller(1..1).first if Familia.debug
174
+ el = revrangebyscoreraw(sscore, escore, opts)
175
+ multi_from_redis(*el)
176
+ end
177
+
178
+ def revrangebyscoreraw(sscore, escore, opts = {})
179
+ echo :revrangebyscoreraw, caller(1..1).first if Familia.debug
180
+ opts[:with_scores] = true if opts[:withscores]
181
+ redis.zrevrangebyscore(rediskey, sscore, escore, opts)
182
+ end
183
+
184
+ def remrangebyrank(srank, erank)
185
+ redis.zremrangebyrank rediskey, srank, erank
186
+ end
187
+
188
+ def remrangebyscore(sscore, escore)
189
+ redis.zremrangebyscore rediskey, sscore, escore
190
+ end
191
+
192
+ def increment(val, by = 1)
193
+ redis.zincrby(rediskey, by, val).to_i
194
+ end
195
+ alias incr increment
196
+ alias incrby increment
197
+
198
+ def decrement(val, by = 1)
199
+ increment val, -by
200
+ end
201
+ alias decr decrement
202
+ alias decrby decrement
203
+
204
+ def delete(val)
205
+ Familia.trace :DELETE, redis, "#{val}<#{val.class}>", caller(1..1) if Familia.debug?
206
+ # We use `strict_values: false` here to allow for the deletion of values
207
+ # that are in the sorted set. If it's a horreum object, the value is
208
+ # the identifier and not a serialized version of the object. So either
209
+ # the value exists in the sorted set or it doesn't -- we don't need to
210
+ # raise an error if it's not found.
211
+ redis.zrem rediskey, to_redis(val, false)
212
+ end
213
+ alias remove delete
214
+ alias rem delete
215
+ alias del delete
216
+
217
+ def at(idx)
218
+ range(idx, idx).first
219
+ end
220
+
221
+ # Return the first element in the list. Redis: ZRANGE(0)
222
+ def first
223
+ at(0)
224
+ end
225
+
226
+ # Return the last element in the list. Redis: ZRANGE(-1)
227
+ def last
228
+ at(-1)
229
+ end
230
+
231
+ Familia::RedisType.register self, :sorted_set
232
+ Familia::RedisType.register self, :zset
233
+ end
234
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Familia
4
+ class String < RedisType
5
+ def init; end
6
+
7
+ def size
8
+ to_s.size
9
+ end
10
+ alias length size
11
+
12
+ def empty?
13
+ size.zero?
14
+ end
15
+
16
+ def value
17
+ echo :value, caller[0..0] if Familia.debug
18
+ redis.setnx rediskey, @opts[:default] if @opts[:default]
19
+ from_redis redis.get(rediskey)
20
+ end
21
+ alias content value
22
+ alias get value
23
+
24
+ def to_s
25
+ value.to_s # value can return nil which to_s should not
26
+ end
27
+
28
+ def to_i
29
+ value.to_i
30
+ end
31
+
32
+ def value=(val)
33
+ ret = redis.set(rediskey, to_redis(val))
34
+ update_expiration
35
+ ret
36
+ end
37
+ alias replace value=
38
+ alias set value=
39
+
40
+ def setnx(val)
41
+ ret = redis.setnx(rediskey, to_redis(val))
42
+ update_expiration
43
+ ret
44
+ end
45
+
46
+ def increment
47
+ ret = redis.incr(rediskey)
48
+ update_expiration
49
+ ret
50
+ end
51
+ alias incr increment
52
+
53
+ def incrementby(val)
54
+ ret = redis.incrby(rediskey, val.to_i)
55
+ update_expiration
56
+ ret
57
+ end
58
+ alias incrby incrementby
59
+
60
+ def decrement
61
+ ret = redis.decr rediskey
62
+ update_expiration
63
+ ret
64
+ end
65
+ alias decr decrement
66
+
67
+ def decrementby(val)
68
+ ret = redis.decrby rediskey, val.to_i
69
+ update_expiration
70
+ ret
71
+ end
72
+ alias decrby decrementby
73
+
74
+ def append(val)
75
+ ret = redis.append rediskey, val
76
+ update_expiration
77
+ ret
78
+ end
79
+ alias << append
80
+
81
+ def getbit(offset)
82
+ redis.getbit rediskey, offset
83
+ end
84
+
85
+ def setbit(offset, val)
86
+ ret = redis.setbit rediskey, offset, val
87
+ update_expiration
88
+ ret
89
+ end
90
+
91
+ def getrange(spoint, epoint)
92
+ redis.getrange rediskey, spoint, epoint
93
+ end
94
+
95
+ def setrange(offset, val)
96
+ ret = redis.setrange rediskey, offset, val
97
+ update_expiration
98
+ ret
99
+ end
100
+
101
+ def getset(val)
102
+ ret = redis.getset rediskey, val
103
+ update_expiration
104
+ ret
105
+ end
106
+
107
+ def nil?
108
+ value.nil?
109
+ end
110
+
111
+ Familia::RedisType.register self, :string
112
+ Familia::RedisType.register self, :counter
113
+ Familia::RedisType.register self, :lock
114
+ end
115
+ end