redis-objects 1.3.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/redis/lock.rb CHANGED
@@ -11,7 +11,6 @@ class Redis
11
11
  class Lock < BaseObject
12
12
  class LockTimeout < StandardError; end #:nodoc:
13
13
 
14
- attr_reader :key, :options
15
14
  def initialize(key, *args)
16
15
  super(key, *args)
17
16
  @options[:timeout] ||= 5
@@ -19,13 +18,6 @@ class Redis
19
18
  redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
20
19
  end
21
20
 
22
- # Clear the lock. Should only be needed if there's a server crash
23
- # or some other event that gets locks in a stuck state.
24
- def clear
25
- redis.del(key)
26
- end
27
- alias_method :delete, :clear
28
-
29
21
  def value
30
22
  nil
31
23
  end
@@ -33,28 +25,30 @@ class Redis
33
25
  # Get the lock and execute the code block. Any other code that needs the lock
34
26
  # (on any server) will spin waiting for the lock up to the :timeout
35
27
  # that was specified when the lock was defined.
36
- def lock(&block)
37
- expiration = nil
28
+ def lock
29
+ raise ArgumentError, 'Block not given' unless block_given?
30
+ expiration_ms = generate_expiration
31
+ expiration_s = expiration_ms / 1000.0
32
+ end_time = nil
38
33
  try_until_timeout do
39
- expiration = generate_expiration
40
- # Use the expiration as the value of the lock.
41
- break if redis.setnx(key, expiration)
34
+ end_time = Time.now.to_i + expiration_s
35
+ # Set a NX record and use the Redis expiration mechanism.
36
+ # Empty value because the presence of it is enough to lock
37
+ # `px` only except an Integer in millisecond
38
+ break if redis.set(key, nil, px: expiration_ms, nx: true)
42
39
 
43
- # Lock is being held. Now check to see if it's expired (if we're using
44
- # lock expiration).
45
- # See "Handling Deadlocks" section on http://redis.io/commands/setnx
46
- if !@options[:expiration].nil?
40
+ # Backward compatibility code
41
+ # TODO: remove at the next major release for performance
42
+ unless @options[:expiration].nil?
47
43
  old_expiration = redis.get(key).to_f
48
44
 
49
- if old_expiration < Time.now.to_f
50
- # If it's expired, use GETSET to update it.
51
- expiration = generate_expiration
52
- old_expiration = redis.getset(key, expiration).to_f
53
-
54
- # Since GETSET returns the old value of the lock, if the old expiration
55
- # is still in the past, we know no one else has expired the locked
56
- # and we now have it.
57
- break if old_expiration < Time.now.to_f
45
+ # Check it was not an empty string with `zero?` and
46
+ # the expiration time is passed.
47
+ if !old_expiration.zero? && old_expiration < Time.now.to_f
48
+ expiration_ms = generate_expiration
49
+ expiration_s = expiration_ms / 1000.0
50
+ end_time = Time.now.to_i + expiration_s
51
+ break if redis.set(key, nil, px: expiration_ms)
58
52
  end
59
53
  end
60
54
  end
@@ -66,14 +60,15 @@ class Redis
66
60
  # it's not safe for us to remove it. Check how much time has passed since we
67
61
  # wrote the lock key and only delete it if it hasn't expired (or we're not using
68
62
  # lock expiration)
69
- if @options[:expiration].nil? || expiration > Time.now.to_f
63
+ if @options[:expiration].nil? || end_time > Time.now.to_f
70
64
  redis.del(key)
71
65
  end
72
66
  end
73
67
  end
74
68
 
69
+ # Return expiration in milliseconds
75
70
  def generate_expiration
76
- @options[:expiration].nil? ? 1 : (Time.now + @options[:expiration].to_f + 1).to_f
71
+ ((@options[:expiration].nil? ? 1 : @options[:expiration].to_f) * 1000).to_i
77
72
  end
78
73
 
79
74
  private
data/lib/redis/objects.rb CHANGED
@@ -46,8 +46,6 @@ class Redis
46
46
  #
47
47
  #
48
48
  module Objects
49
- dir = File.expand_path(__FILE__.sub(/\.rb$/,''))
50
-
51
49
  autoload :Counters, 'redis/objects/counters'
52
50
  autoload :Lists, 'redis/objects/lists'
53
51
  autoload :Locks, 'redis/objects/locks'
@@ -173,8 +171,15 @@ class Redis
173
171
  def redis() self.class.redis end
174
172
  def redis_objects() self.class.redis_objects end
175
173
 
176
- def delete!
177
- redis.del(redis_objects.keys.map { |k| send(k) }.reject(&:nil?).map { |obj| obj.key })
174
+ def redis_delete_objects
175
+ redis.del(redis_instance_keys)
176
+ end
177
+
178
+ def redis_instance_keys
179
+ redis_objects
180
+ .reject { |_, value| value[:global] }
181
+ .keys
182
+ .collect { |name| redis_field_key(name) }
178
183
  end
179
184
 
180
185
  def redis_options(name) #:nodoc:
@@ -9,6 +9,7 @@ class Redis
9
9
  def method_missing(name, *args, &block)
10
10
  @pool.with { |x| x.send(name, *args, &block) }
11
11
  end
12
+ ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
12
13
 
13
14
  def respond_to_missing?(name, include_all = false)
14
15
  @pool.with { |x| x.respond_to?(name, include_all) }
@@ -26,6 +26,15 @@ class Redis
26
26
  )
27
27
  )
28
28
  end
29
+
30
+ define_method(:"#{name}=") do |values|
31
+ hash_key = public_send(name)
32
+
33
+ redis.pipelined do
34
+ hash_key.clear
35
+ hash_key.bulk_set(values)
36
+ end
37
+ end
29
38
  end
30
39
 
31
40
  if options[:global]
@@ -26,6 +26,15 @@ class Redis
26
26
  )
27
27
  )
28
28
  end
29
+
30
+ define_method(:"#{name}=") do |values|
31
+ list = public_send(name)
32
+
33
+ redis.pipelined do
34
+ list.clear
35
+ list.push(*values)
36
+ end
37
+ end
29
38
  end
30
39
 
31
40
  if options[:global]
@@ -26,6 +26,15 @@ class Redis
26
26
  )
27
27
  )
28
28
  end
29
+
30
+ define_method(:"#{name}=") do |values|
31
+ set = public_send(name)
32
+
33
+ redis.pipelined do
34
+ set.clear
35
+ set.merge(*values)
36
+ end
37
+ end
29
38
  end
30
39
 
31
40
  if options[:global]
@@ -1,5 +1,5 @@
1
1
  class Redis
2
2
  module Objects
3
- VERSION = "1.3.1"
3
+ VERSION = "1.5.1"
4
4
  end
5
5
  end
data/lib/redis/set.rb CHANGED
@@ -1,17 +1,10 @@
1
- require File.dirname(__FILE__) + '/base_object'
1
+ require File.dirname(__FILE__) + '/enumerable_object'
2
2
 
3
3
  class Redis
4
4
  #
5
5
  # Class representing a set.
6
6
  #
7
- class Set < BaseObject
8
- require 'enumerator'
9
- include Enumerable
10
- require 'redis/helpers/core_commands'
11
- include Redis::Helpers::CoreCommands
12
-
13
- attr_reader :key, :options
14
-
7
+ class Set < EnumerableObject
15
8
  # Works like add. Can chain together: list << 'a' << 'b'
16
9
  def <<(value)
17
10
  add(value)
@@ -27,8 +20,8 @@ class Redis
27
20
  end
28
21
 
29
22
  # Remove and return a random member. Redis: SPOP
30
- def pop
31
- unmarshal redis.spop(key)
23
+ def pop(count = nil)
24
+ unmarshal redis.spop(key, count)
32
25
  end
33
26
 
34
27
  # return a random member. Redis: SRANDMEMBER
@@ -74,12 +67,6 @@ class Redis
74
67
  res
75
68
  end
76
69
 
77
- # Iterate through each member of the set. Redis::Objects mixes in Enumerable,
78
- # so you can also use familiar methods like +collect+, +detect+, and so forth.
79
- def each(&block)
80
- members.each(&block)
81
- end
82
-
83
70
  # Return the intersection with another set. Can pass it either another set
84
71
  # object or set name. Also available as & which is a bit cleaner:
85
72
  #
@@ -165,7 +152,7 @@ class Redis
165
152
  redis.smove(key, destination.is_a?(Redis::Set) ? destination.key : destination.to_s, value)
166
153
  end
167
154
 
168
- # The number of members in the set. Aliased as size. Redis: SCARD
155
+ # The number of members in the set. Aliased as size or count. Redis: SCARD
169
156
  def length
170
157
  redis.scard(key)
171
158
  end
@@ -185,10 +172,6 @@ class Redis
185
172
  members.join(', ')
186
173
  end
187
174
 
188
- def as_json(*)
189
- to_hash
190
- end
191
-
192
175
  private
193
176
 
194
177
  def keys_from_objects(sets)
@@ -1,17 +1,10 @@
1
- require File.dirname(__FILE__) + '/base_object'
1
+ require File.dirname(__FILE__) + '/enumerable_object'
2
2
 
3
3
  class Redis
4
4
  #
5
5
  # Class representing a sorted set.
6
6
  #
7
- class SortedSet < BaseObject
8
- # require 'enumerator'
9
- # include Enumerable
10
- require 'redis/helpers/core_commands'
11
- include Redis::Helpers::CoreCommands
12
-
13
- attr_reader :key, :options
14
-
7
+ class SortedSet < EnumerableObject
15
8
  # How to add values using a sorted set. The key is the member, eg,
16
9
  # "Peter", and the value is the score, eg, 163. So:
17
10
  # num_posts['Peter'] = 163
@@ -124,7 +117,7 @@ class Redis
124
117
  options[:offset] || options[:limit] || options[:count]
125
118
  args[:with_scores] = true if options[:withscores] || options[:with_scores]
126
119
 
127
- redis.zrangebyscore(key, min, max, args).map{|v| unmarshal(v) }
120
+ redis.zrangebyscore(key, min, max, **args).map{|v| unmarshal(v) }
128
121
  end
129
122
 
130
123
  # Returns all the elements in the sorted set at key with a score between max and min
@@ -140,7 +133,7 @@ class Redis
140
133
  options[:offset] || options[:limit] || options[:count]
141
134
  args[:with_scores] = true if options[:withscores] || options[:with_scores]
142
135
 
143
- redis.zrevrangebyscore(key, max, min, args).map{|v| unmarshal(v) }
136
+ redis.zrevrangebyscore(key, max, min, **args).map{|v| unmarshal(v) }
144
137
  end
145
138
 
146
139
  # Remove all elements in the sorted set at key with rank between start and end. Start and end are
@@ -208,7 +201,17 @@ class Redis
208
201
  #
209
202
  # Redis: SINTER
210
203
  def intersection(*sets)
211
- redis.zinter(key, *keys_from_objects(sets)).map{|v| unmarshal(v) }
204
+ result = nil
205
+ temp_key = :"#{key}:intersection:#{Time.current.to_i + rand}"
206
+
207
+ redis.multi do
208
+ interstore(temp_key, *sets)
209
+ redis.expire(temp_key, 1)
210
+
211
+ result = redis.zrange(temp_key, 0, -1)
212
+ end
213
+
214
+ result.value
212
215
  end
213
216
  alias_method :intersect, :intersection
214
217
  alias_method :inter, :intersection
@@ -219,7 +222,7 @@ class Redis
219
222
  def interstore(name, *sets)
220
223
  allow_expiration do
221
224
  opts = sets.last.is_a?(Hash) ? sets.pop : {}
222
- redis.zinterstore(key_from_object(name), keys_from_objects([self] + sets), opts)
225
+ redis.zinterstore(key_from_object(name), keys_from_objects([self] + sets), **opts)
223
226
  end
224
227
  end
225
228
 
@@ -235,7 +238,17 @@ class Redis
235
238
  #
236
239
  # Redis: SUNION
237
240
  def union(*sets)
238
- redis.zunion(key, *keys_from_objects(sets)).map{|v| unmarshal(v) }
241
+ result = nil
242
+ temp_key = :"#{key}:union:#{Time.current.to_i + rand}"
243
+
244
+ redis.multi do
245
+ unionstore(temp_key, *sets)
246
+ redis.expire(temp_key, 1)
247
+
248
+ result = redis.zrange(temp_key, 0, -1)
249
+ end
250
+
251
+ result.value
239
252
  end
240
253
  alias_method :|, :union
241
254
  alias_method :+, :union
@@ -245,35 +258,10 @@ class Redis
245
258
  def unionstore(name, *sets)
246
259
  allow_expiration do
247
260
  opts = sets.last.is_a?(Hash) ? sets.pop : {}
248
- redis.zunionstore(key_from_object(name), keys_from_objects([self] + sets), opts)
261
+ redis.zunionstore(key_from_object(name), keys_from_objects([self] + sets), **opts)
249
262
  end
250
263
  end
251
264
 
252
- # Return the difference vs another set. Can pass it either another set
253
- # object or set name. Also available as ^ or - which is a bit cleaner:
254
- #
255
- # members_difference = set1 ^ set2
256
- # members_difference = set1 - set2
257
- #
258
- # If you want to specify multiple sets, you must use +difference+:
259
- #
260
- # members_difference = set1.difference(set2, set3, set4)
261
- # members_difference = set1.diff(set2, set3, set4)
262
- #
263
- # Redis: SDIFF
264
- def difference(*sets)
265
- redis.zdiff(key, *keys_from_objects(sets)).map{|v| unmarshal(v) }
266
- end
267
- alias_method :diff, :difference
268
- alias_method :^, :difference
269
- alias_method :-, :difference
270
-
271
- # Calculate the diff and store it in Redis as +name+. Returns the number
272
- # of elements in the stored union. Redis: SDIFFSTORE
273
- def diffstore(name, *sets)
274
- redis.zdiffstore(name, key, *keys_from_objects(sets))
275
- end
276
-
277
265
  # Returns true if the set has no members. Redis: SCARD == 0
278
266
  def empty?
279
267
  length == 0
@@ -303,11 +291,12 @@ class Redis
303
291
  at(-1)
304
292
  end
305
293
 
306
- # The number of members in the set. Aliased as size. Redis: ZCARD
294
+ # The number of members in the set. Aliased as size or count. Redis: ZCARD
307
295
  def length
308
296
  redis.zcard(key)
309
297
  end
310
298
  alias_method :size, :length
299
+ alias_method :count, :length
311
300
 
312
301
  # The number of members within a range of scores. Redis: ZCOUNT
313
302
  def range_size(min, max)
data/lib/redis/value.rb CHANGED
@@ -1,15 +1,11 @@
1
1
  require File.dirname(__FILE__) + '/base_object'
2
+ require 'zlib'
2
3
 
3
4
  class Redis
4
5
  #
5
6
  # Class representing a simple value. You can use standard Ruby operations on it.
6
7
  #
7
8
  class Value < BaseObject
8
- require 'redis/helpers/core_commands'
9
- include Redis::Helpers::CoreCommands
10
-
11
- attr_reader :key, :options
12
-
13
9
  def value=(val)
14
10
  allow_expiration do
15
11
  if val.nil?
@@ -31,6 +27,30 @@ class Redis
31
27
  end
32
28
  alias_method :get, :value
33
29
 
30
+ def marshal(value, *args)
31
+ if !value.nil? && options[:compress]
32
+ compress(super)
33
+ else
34
+ super
35
+ end
36
+ end
37
+
38
+ def unmarshal(value, *args)
39
+ if !value.nil? && options[:compress]
40
+ super(decompress(value), *args)
41
+ else
42
+ super
43
+ end
44
+ end
45
+
46
+ def decompress(value)
47
+ Zlib::Inflate.inflate(value)
48
+ end
49
+
50
+ def compress(value)
51
+ Zlib::Deflate.deflate(value)
52
+ end
53
+
34
54
  def inspect
35
55
  "#<Redis::Value #{value.inspect}>"
36
56
  end
@@ -0,0 +1,198 @@
1
+
2
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
3
+
4
+ require 'redis/objects'
5
+
6
+ class CustomSerializer
7
+ class << self
8
+ attr_accessor :dump_called, :load_called, :dump_args, :load_args
9
+ end
10
+
11
+ def self.dump(value, *args, **kargs)
12
+ @dump_called = true
13
+ @dump_args = [args, kargs]
14
+ Marshal.dump(value)
15
+ end
16
+
17
+ def self.load(value, *args, **kargs)
18
+ @load_called = true
19
+ @load_args = [args, kargs]
20
+ Marshal.load(value)
21
+ end
22
+
23
+ def self.reset!
24
+ @dump_called = nil
25
+ @load_called = nil
26
+ end
27
+ end
28
+
29
+ describe 'with custom serialization' do
30
+ before do
31
+ CustomSerializer.reset!
32
+ end
33
+
34
+ describe Redis::Value do
35
+ before do
36
+ @value = Redis::Value.new(
37
+ 'spec/value_custom_serializer',
38
+ marshal: true,
39
+ serializer: CustomSerializer
40
+ )
41
+ @value.clear
42
+ end
43
+
44
+ it 'uses custom serializer' do
45
+ @value.value = { json: 'data' }
46
+ CustomSerializer.dump_called.should == true
47
+ CustomSerializer.load_called.should == nil
48
+ @value.value.should == { json: 'data' }
49
+ CustomSerializer.dump_called.should == true
50
+ CustomSerializer.load_called.should == true
51
+ end
52
+
53
+ it 'passes extra arguments to dump' do
54
+ @value.options[:marshal_dump_args] = ['some', { extra: 'arguments' }]
55
+ @value.value = 1
56
+ CustomSerializer.dump_args.should == [['some'], { extra: 'arguments' }]
57
+ end
58
+
59
+ it 'passes extra arguments to load' do
60
+ @value.options[:marshal_load_args] = ['some', { extra: 'arguments' }]
61
+ @value.value = 1
62
+ @value.value.should == 1
63
+ CustomSerializer.load_args.should == [['some'], { extra: 'arguments' }]
64
+ end
65
+ end
66
+
67
+ describe Redis::List do
68
+ before do
69
+ @list = Redis::List.new(
70
+ 'spec/list_custom_serializer',
71
+ marshal: true,
72
+ serializer: CustomSerializer
73
+ )
74
+ @list.clear
75
+ end
76
+
77
+ it 'uses custom serializer' do
78
+ @list << { json: 'data' }
79
+ CustomSerializer.dump_called.should == true
80
+ CustomSerializer.load_called.should == nil
81
+ @list.should == [{ json: 'data' }]
82
+ CustomSerializer.dump_called.should == true
83
+ CustomSerializer.load_called.should == true
84
+ end
85
+
86
+ it 'passes extra arguments to dump' do
87
+ @list.options[:marshal_dump_args] = ['some', { extra: 'arguments' }]
88
+ @list << 1
89
+ CustomSerializer.dump_args.should == [['some'], { extra: 'arguments' }]
90
+ end
91
+
92
+ it 'passes extra arguments to load' do
93
+ @list.options[:marshal_load_args] = ['some', { extra: 'arguments' }]
94
+ @list << 1
95
+ @list.values.should == [1]
96
+ CustomSerializer.load_args.should == [['some'], { extra: 'arguments' }]
97
+ end
98
+ end
99
+
100
+ describe Redis::HashKey do
101
+ before do
102
+ @hash = Redis::HashKey.new(
103
+ 'spec/hash_custom_serializer',
104
+ marshal: true,
105
+ serializer: CustomSerializer
106
+ )
107
+ @hash.clear
108
+ end
109
+
110
+ it 'uses custom serializer' do
111
+ @hash['a'] = 1
112
+ CustomSerializer.dump_called.should == true
113
+ CustomSerializer.load_called.should == nil
114
+ @hash.value.should == { 'a' => 1 }
115
+ CustomSerializer.dump_called.should == true
116
+ CustomSerializer.load_called.should == true
117
+ end
118
+
119
+ it 'passes extra arguments to dump' do
120
+ @hash.options[:marshal_dump_args] = ['some', { extra: 'arguments' }]
121
+ @hash['a'] = 1
122
+ CustomSerializer.dump_args.should == [['some'], { extra: 'arguments' }]
123
+ end
124
+
125
+ it 'passes extra arguments to load' do
126
+ @hash.options[:marshal_load_args] = ['some', { extra: 'arguments' }]
127
+ @hash['a'] = 1
128
+ @hash.value.should == { 'a' => 1 }
129
+ CustomSerializer.load_args.should == [['some'], { extra: 'arguments' }]
130
+ end
131
+ end
132
+
133
+ describe Redis::Set do
134
+ before do
135
+ @set = Redis::Set.new(
136
+ 'spec/set_custom_serializer',
137
+ marshal: true,
138
+ serializer: CustomSerializer
139
+ )
140
+ @set.clear
141
+ end
142
+
143
+ it 'uses custom serializer' do
144
+ @set << 'a'
145
+ CustomSerializer.dump_called.should == true
146
+ CustomSerializer.load_called.should == nil
147
+ @set.members.should == ['a']
148
+ CustomSerializer.dump_called.should == true
149
+ CustomSerializer.load_called.should == true
150
+ end
151
+
152
+ it 'passes extra arguments to dump' do
153
+ @set.options[:marshal_dump_args] = ['some', { extra: 'arguments' }]
154
+ @set << 'a'
155
+ CustomSerializer.dump_args.should == [['some'], { extra: 'arguments' }]
156
+ end
157
+
158
+ it 'passes extra arguments to load' do
159
+ @set.options[:marshal_load_args] = ['some', { extra: 'arguments' }]
160
+ @set << 'a'
161
+ @set.members.should == ['a']
162
+ CustomSerializer.load_args.should == [['some'], { extra: 'arguments' }]
163
+ end
164
+ end
165
+
166
+ describe Redis::SortedSet do
167
+ before do
168
+ @set = Redis::SortedSet.new(
169
+ 'spec/zset_custom_serializer',
170
+ marshal: true,
171
+ serializer: CustomSerializer
172
+ )
173
+ @set.clear
174
+ end
175
+
176
+ it 'uses custom serializer' do
177
+ @set['a'] = 1
178
+ CustomSerializer.dump_called.should == true
179
+ CustomSerializer.load_called.should == nil
180
+ @set.members.should == ['a']
181
+ CustomSerializer.dump_called.should == true
182
+ CustomSerializer.load_called.should == true
183
+ end
184
+
185
+ it 'passes extra arguments to dump' do
186
+ @set.options[:marshal_dump_args] = ['some', { extra: 'arguments' }]
187
+ @set['a'] = 1
188
+ CustomSerializer.dump_args.should == [['some'], { extra: 'arguments' }]
189
+ end
190
+
191
+ it 'passes extra arguments to load' do
192
+ @set.options[:marshal_load_args] = ['some', { extra: 'arguments' }]
193
+ @set['a'] = 1
194
+ @set.members.should == ['a']
195
+ CustomSerializer.load_args.should == [['some'], { extra: 'arguments' }]
196
+ end
197
+ end
198
+ end