redis-objects 1.3.1 → 1.5.1

Sign up to get free protection for your applications and to get access to all the features.
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