redis-objects-legacy 1.6.0
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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +16 -0
- data/ATOMICITY.rdoc +154 -0
- data/CHANGELOG.rdoc +362 -0
- data/Gemfile +4 -0
- data/LICENSE +202 -0
- data/README.md +600 -0
- data/Rakefile +14 -0
- data/lib/redis/base_object.rb +62 -0
- data/lib/redis/counter.rb +145 -0
- data/lib/redis/enumerable_object.rb +28 -0
- data/lib/redis/hash_key.rb +163 -0
- data/lib/redis/helpers/core_commands.rb +89 -0
- data/lib/redis/list.rb +160 -0
- data/lib/redis/lock.rb +89 -0
- data/lib/redis/objects/connection_pool_proxy.rb +31 -0
- data/lib/redis/objects/counters.rb +155 -0
- data/lib/redis/objects/hashes.rb +60 -0
- data/lib/redis/objects/lists.rb +58 -0
- data/lib/redis/objects/locks.rb +73 -0
- data/lib/redis/objects/sets.rb +58 -0
- data/lib/redis/objects/sorted_sets.rb +49 -0
- data/lib/redis/objects/values.rb +64 -0
- data/lib/redis/objects/version.rb +5 -0
- data/lib/redis/objects.rb +199 -0
- data/lib/redis/set.rb +182 -0
- data/lib/redis/sorted_set.rb +325 -0
- data/lib/redis/value.rb +65 -0
- data/lib/redis-objects-legacy.rb +1 -0
- data/spec/redis_autoload_objects_spec.rb +46 -0
- data/spec/redis_namespace_compat_spec.rb +24 -0
- data/spec/redis_objects_active_record_spec.rb +162 -0
- data/spec/redis_objects_conn_spec.rb +276 -0
- data/spec/redis_objects_custom_serializer.rb +198 -0
- data/spec/redis_objects_instance_spec.rb +1666 -0
- data/spec/redis_objects_model_spec.rb +1097 -0
- data/spec/spec_helper.rb +92 -0
- metadata +214 -0
data/README.md
ADDED
@@ -0,0 +1,600 @@
|
|
1
|
+
Redis::Objects - Map Redis types directly to Ruby objects
|
2
|
+
=========================================================
|
3
|
+
|
4
|
+
[](https://travis-ci.com/nateware/redis-objects)
|
5
|
+
[](https://codecov.io/gh/nateware/redis-objects)
|
6
|
+
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=MJF7JU5M7F8VL)
|
7
|
+
|
8
|
+
This is **not** an ORM. People that are wrapping ORM’s around Redis are missing the point.
|
9
|
+
|
10
|
+
The killer feature of Redis is that it allows you to perform _atomic_ operations
|
11
|
+
on _individual_ data structures, like counters, lists, and sets. The **atomic** part is HUGE.
|
12
|
+
Using an ORM wrapper that retrieves a "record", updates values, then sends those values back,
|
13
|
+
_removes_ the atomicity, and thus the major advantage of Redis. Just use MySQL, k?
|
14
|
+
|
15
|
+
This gem provides a Rubyish interface to Redis, by mapping [Redis data types](http://redis.io/commands)
|
16
|
+
to Ruby objects, via a thin layer over the `redis` gem. It offers several advantages
|
17
|
+
over the lower-level redis-rb API:
|
18
|
+
|
19
|
+
1. Easy to integrate directly with existing ORMs - ActiveRecord, DataMapper, etc. Add counters to your model!
|
20
|
+
2. Complex data structures are automatically Marshaled (if you set :marshal => true)
|
21
|
+
3. Integers are returned as integers, rather than '17'
|
22
|
+
4. Higher-level types are provided, such as Locks, that wrap multiple calls
|
23
|
+
|
24
|
+
This gem originally arose out of a need for high-concurrency atomic operations;
|
25
|
+
for a fun rant on the topic, see [An Atomic Rant](http://nateware.com/2010/02/18/an-atomic-rant),
|
26
|
+
or scroll down to [Atomic Counters and Locks](#atomicity) in this README.
|
27
|
+
|
28
|
+
There are two ways to use Redis::Objects, either as an include in a model class (to
|
29
|
+
tightly integrate with ORMs or other classes), or standalone by using classes such
|
30
|
+
as `Redis::List` and `Redis::SortedSet`.
|
31
|
+
|
32
|
+
Installation and Setup
|
33
|
+
----------------------
|
34
|
+
Add it to your Gemfile as:
|
35
|
+
|
36
|
+
~~~ruby
|
37
|
+
gem 'redis-objects'
|
38
|
+
~~~
|
39
|
+
|
40
|
+
Redis::Objects needs a handle created by `Redis.new` or a [ConnectionPool](https://github.com/mperham/connection_pool):
|
41
|
+
|
42
|
+
The recommended approach is to use a `ConnectionPool` since this guarantees that most timeouts in the `redis` client
|
43
|
+
do not pollute your existing connection. However, you need to make sure that both `:timeout` and `:size` are set appropriately
|
44
|
+
in a multithreaded environment.
|
45
|
+
~~~ruby
|
46
|
+
require 'connection_pool'
|
47
|
+
Redis::Objects.redis = ConnectionPool.new(size: 5, timeout: 5) { Redis.new(:host => '127.0.0.1', :port => 6379) }
|
48
|
+
~~~
|
49
|
+
|
50
|
+
Redis::Objects can also default to `Redis.current` if `Redis::Objects.redis` is not set.
|
51
|
+
~~~ruby
|
52
|
+
Redis.current = Redis.new(:host => '127.0.0.1', :port => 6379)
|
53
|
+
~~~
|
54
|
+
|
55
|
+
(If you're on Rails, `config/initializers/redis.rb` is a good place for this.)
|
56
|
+
Remember you can use Redis::Objects in any Ruby code. There are **no** dependencies
|
57
|
+
on Rails. Standalone, Sinatra, Resque - no problem.
|
58
|
+
|
59
|
+
Alternatively, you can set the `redis` handle directly:
|
60
|
+
|
61
|
+
~~~ruby
|
62
|
+
Redis::Objects.redis = Redis.new(...)
|
63
|
+
~~~
|
64
|
+
|
65
|
+
Finally, you can even set different handles for different classes:
|
66
|
+
|
67
|
+
~~~ruby
|
68
|
+
class User
|
69
|
+
include Redis::Objects
|
70
|
+
end
|
71
|
+
class Post
|
72
|
+
include Redis::Objects
|
73
|
+
end
|
74
|
+
|
75
|
+
# you can also use a ConnectionPool here as well
|
76
|
+
User.redis = Redis.new(:host => '1.2.3.4')
|
77
|
+
Post.redis = Redis.new(:host => '5.6.7.8')
|
78
|
+
~~~
|
79
|
+
|
80
|
+
As of `0.7.0`, `redis-objects` now autoloads the appropriate `Redis::Whatever`
|
81
|
+
classes on demand. Previous strategies of individually requiring `redis/list`
|
82
|
+
or `redis/set` are no longer required.
|
83
|
+
|
84
|
+
Option 1: Model Class Include
|
85
|
+
=============================
|
86
|
+
Including Redis::Objects in a model class makes it trivial to integrate Redis types
|
87
|
+
with an existing ActiveRecord, DataMapper, Mongoid, or similar class. **Redis::Objects
|
88
|
+
will work with _any_ class that provides an `id` method that returns a unique value.**
|
89
|
+
Redis::Objects automatically creates keys that are unique to each object, in the format:
|
90
|
+
|
91
|
+
model_name:id:field_name
|
92
|
+
|
93
|
+
For illustration purposes, consider this stub class:
|
94
|
+
|
95
|
+
~~~ruby
|
96
|
+
class User
|
97
|
+
include Redis::Objects
|
98
|
+
counter :my_posts
|
99
|
+
def id
|
100
|
+
1
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
user = User.new
|
105
|
+
user.id # 1
|
106
|
+
user.my_posts.increment
|
107
|
+
user.my_posts.increment
|
108
|
+
user.my_posts.increment
|
109
|
+
puts user.my_posts.value # 3
|
110
|
+
user.my_posts.reset
|
111
|
+
puts user.my_posts.value # 0
|
112
|
+
user.my_posts.reset 5
|
113
|
+
puts user.my_posts.value # 5
|
114
|
+
~~~
|
115
|
+
|
116
|
+
Here's an example that integrates several data types with an ActiveRecord model:
|
117
|
+
|
118
|
+
~~~ruby
|
119
|
+
class Team < ActiveRecord::Base
|
120
|
+
include Redis::Objects
|
121
|
+
|
122
|
+
lock :trade_players, :expiration => 15 # sec
|
123
|
+
value :at_bat
|
124
|
+
counter :hits
|
125
|
+
counter :runs
|
126
|
+
counter :outs
|
127
|
+
counter :inning, :start => 1
|
128
|
+
list :on_base
|
129
|
+
list :coaches, :marshal => true
|
130
|
+
set :outfielders
|
131
|
+
hash_key :pitchers_faced # "hash" is taken by Ruby
|
132
|
+
sorted_set :rank, :global => true
|
133
|
+
end
|
134
|
+
~~~
|
135
|
+
|
136
|
+
Familiar Ruby array operations Just Work (TM):
|
137
|
+
|
138
|
+
~~~ruby
|
139
|
+
@team = Team.find_by_name('New York Yankees')
|
140
|
+
@team.on_base << 'player1'
|
141
|
+
@team.on_base << 'player2'
|
142
|
+
@team.on_base << 'player3'
|
143
|
+
@team.on_base # ['player1', 'player2', 'player3']
|
144
|
+
@team.on_base.pop
|
145
|
+
@team.on_base.shift
|
146
|
+
@team.on_base.length # 1
|
147
|
+
@team.on_base.delete('player2')
|
148
|
+
@team.on_base = ['player1', 'player2'] # ['player1', 'player2']
|
149
|
+
~~~
|
150
|
+
|
151
|
+
Sets work too:
|
152
|
+
|
153
|
+
~~~ruby
|
154
|
+
@team.outfielders << 'outfielder1'
|
155
|
+
@team.outfielders << 'outfielder2'
|
156
|
+
@team.outfielders << 'outfielder1' # dup ignored
|
157
|
+
@team.outfielders # ['outfielder1', 'outfielder2']
|
158
|
+
@team.outfielders.each do |player|
|
159
|
+
puts player
|
160
|
+
end
|
161
|
+
player = @team.outfielders.detect{|of| of == 'outfielder2'}
|
162
|
+
@team.outfielders = ['outfielder1', 'outfielder3'] # ['outfielder1', 'outfielder3']
|
163
|
+
~~~
|
164
|
+
|
165
|
+
Hashes work too:
|
166
|
+
|
167
|
+
~~~ruby
|
168
|
+
@team.pitchers_faced['player1'] = 'pitcher2'
|
169
|
+
@team.pitchers_faced['player2'] = 'pitcher1'
|
170
|
+
@team.pitchers_faced = { 'player1' => 'pitcher2', 'player2' => 'pitcher1' }
|
171
|
+
~~~
|
172
|
+
|
173
|
+
And you can do unions and intersections between objects (kinda cool):
|
174
|
+
|
175
|
+
~~~ruby
|
176
|
+
@team1.outfielders | @team2.outfielders # outfielders on both teams
|
177
|
+
@team1.outfielders & @team2.outfielders # in baseball, should be empty :-)
|
178
|
+
~~~
|
179
|
+
|
180
|
+
Counters can be atomically incremented/decremented (but not assigned):
|
181
|
+
|
182
|
+
~~~ruby
|
183
|
+
@team.hits.increment # or incr
|
184
|
+
@team.hits.decrement # or decr
|
185
|
+
@team.hits.incr(3) # add 3
|
186
|
+
@team.runs = 4 # exception
|
187
|
+
~~~
|
188
|
+
|
189
|
+
Defining a different method as the `id` field is easy
|
190
|
+
|
191
|
+
~~~ruby
|
192
|
+
class User
|
193
|
+
include Redis::Objects
|
194
|
+
redis_id_field :uid
|
195
|
+
counter :my_posts
|
196
|
+
end
|
197
|
+
|
198
|
+
user.uid # 195137a1bdea4473
|
199
|
+
user.my_posts.increment # 1
|
200
|
+
~~~
|
201
|
+
|
202
|
+
Finally, for free, you get a `redis` method that points directly to a Redis connection:
|
203
|
+
|
204
|
+
~~~ruby
|
205
|
+
Team.redis.get('somekey')
|
206
|
+
@team = Team.new
|
207
|
+
@team.redis.get('somekey')
|
208
|
+
@team.redis.smembers('someset')
|
209
|
+
~~~
|
210
|
+
|
211
|
+
You can use the `redis` handle to directly call any [Redis API command](http://redis.io/commands).
|
212
|
+
|
213
|
+
Option 2: Standalone Usage
|
214
|
+
===========================
|
215
|
+
There is a Ruby class that maps to each Redis type, with methods for each
|
216
|
+
[Redis API command](http://redis.io/commands).
|
217
|
+
Note that calling `new` does not imply it's actually a "new" value - it just
|
218
|
+
creates a mapping between that Ruby object and the corresponding Redis data
|
219
|
+
structure, which may already exist on the `redis-server`.
|
220
|
+
|
221
|
+
Counters
|
222
|
+
--------
|
223
|
+
The `counter_name` is the key stored in Redis.
|
224
|
+
|
225
|
+
~~~ruby
|
226
|
+
@counter = Redis::Counter.new('counter_name')
|
227
|
+
@counter.increment # or incr
|
228
|
+
@counter.decrement # or decr
|
229
|
+
@counter.increment(3)
|
230
|
+
puts @counter.value
|
231
|
+
~~~
|
232
|
+
|
233
|
+
This gem provides a clean way to do atomic blocks as well:
|
234
|
+
|
235
|
+
~~~ruby
|
236
|
+
@counter.increment do |val|
|
237
|
+
raise "Full" if val > MAX_VAL # rewind counter
|
238
|
+
end
|
239
|
+
~~~
|
240
|
+
|
241
|
+
See the section on [Atomic Counters and Locks](#atomicity) for cool uses of atomic counter blocks.
|
242
|
+
|
243
|
+
Locks
|
244
|
+
-----
|
245
|
+
A convenience class that wraps the pattern of [using setnx to perform locking](http://redis.io/commands/setnx).
|
246
|
+
|
247
|
+
~~~ruby
|
248
|
+
@lock = Redis::Lock.new('serialize_stuff', :expiration => 15, :timeout => 0.1)
|
249
|
+
@lock.lock do
|
250
|
+
# do work
|
251
|
+
end
|
252
|
+
~~~
|
253
|
+
|
254
|
+
This can be especially useful if you're running batch jobs spread across multiple hosts.
|
255
|
+
|
256
|
+
Values
|
257
|
+
------
|
258
|
+
Simple values are easy as well:
|
259
|
+
|
260
|
+
~~~ruby
|
261
|
+
@value = Redis::Value.new('value_name')
|
262
|
+
@value.value = 'a'
|
263
|
+
@value.delete
|
264
|
+
~~~
|
265
|
+
|
266
|
+
Complex data is no problem with :marshal => true:
|
267
|
+
|
268
|
+
~~~ruby
|
269
|
+
@account = Account.create!(params[:account])
|
270
|
+
@newest = Redis::Value.new('newest_account', :marshal => true)
|
271
|
+
@newest.value = @account.attributes
|
272
|
+
puts @newest.value['username']
|
273
|
+
~~~
|
274
|
+
|
275
|
+
Compress data to save memory usage on Redis with :compress => true:
|
276
|
+
|
277
|
+
~~~ruby
|
278
|
+
@account = Account.create!(params[:account])
|
279
|
+
@marshaled_value = Redis::Value.new('marshaled', :marshal => true, :compress => true)
|
280
|
+
@marshaled_value.value = @account.attributes
|
281
|
+
@unmarshaled_value = Redis::Value.new('unmarshaled', :compress => true)
|
282
|
+
@unmarshaled_value = 'Really Long String'
|
283
|
+
puts @marshaled_value.value['username']
|
284
|
+
puts @unmarshaled_value.value
|
285
|
+
~~~
|
286
|
+
|
287
|
+
Lists
|
288
|
+
-----
|
289
|
+
Lists work just like Ruby arrays:
|
290
|
+
|
291
|
+
~~~ruby
|
292
|
+
@list = Redis::List.new('list_name')
|
293
|
+
@list << 'a'
|
294
|
+
@list << 'b'
|
295
|
+
@list.include? 'c' # false
|
296
|
+
@list.values # ['a','b']
|
297
|
+
@list << 'c'
|
298
|
+
@list.delete('c')
|
299
|
+
@list[0]
|
300
|
+
@list[0,1]
|
301
|
+
@list[0..1]
|
302
|
+
@list.shift
|
303
|
+
@list.pop
|
304
|
+
@list.clear
|
305
|
+
# etc
|
306
|
+
~~~
|
307
|
+
|
308
|
+
You can bound the size of the list to only hold N elements like so:
|
309
|
+
|
310
|
+
~~~ruby
|
311
|
+
# Only holds 10 elements, throws out old ones when you reach :maxlength.
|
312
|
+
@list = Redis::List.new('list_name', :maxlength => 10)
|
313
|
+
~~~
|
314
|
+
|
315
|
+
Complex data types are serialized with :marshal => true:
|
316
|
+
|
317
|
+
~~~ruby
|
318
|
+
@list = Redis::List.new('list_name', :marshal => true)
|
319
|
+
@list << {:name => "Nate", :city => "San Diego"}
|
320
|
+
@list << {:name => "Peter", :city => "Oceanside"}
|
321
|
+
@list.each do |el|
|
322
|
+
puts "#{el[:name]} lives in #{el[:city]}"
|
323
|
+
end
|
324
|
+
~~~
|
325
|
+
|
326
|
+
Note: If you run into issues, with Marshal errors, refer to the fix in [Issue #176](https://github.com/nateware/redis-objects/issues/176).
|
327
|
+
|
328
|
+
Hashes
|
329
|
+
------
|
330
|
+
Hashes work like a Ruby [Hash](http://ruby-doc.org/core/classes/Hash.html), with
|
331
|
+
a few Redis-specific additions. (The class name is "HashKey" not just "Hash", due to
|
332
|
+
conflicts with the Ruby core Hash class in other gems.)
|
333
|
+
|
334
|
+
~~~ruby
|
335
|
+
@hash = Redis::HashKey.new('hash_name')
|
336
|
+
@hash['a'] = 1
|
337
|
+
@hash['b'] = 2
|
338
|
+
@hash.each do |k,v|
|
339
|
+
puts "#{k} = #{v}"
|
340
|
+
end
|
341
|
+
@hash['c'] = 3
|
342
|
+
puts @hash.all # {"a"=>"1","b"=>"2","c"=>"3"}
|
343
|
+
@hash.clear
|
344
|
+
~~~
|
345
|
+
|
346
|
+
Redis also adds incrementing and bulk operations:
|
347
|
+
|
348
|
+
~~~ruby
|
349
|
+
@hash.incr('c', 6) # 9
|
350
|
+
@hash.bulk_set('d' => 5, 'e' => 6)
|
351
|
+
@hash.bulk_get('d','e') # "5", "6"
|
352
|
+
~~~
|
353
|
+
|
354
|
+
Remember that numbers become strings in Redis. Unlike with other Redis data types,
|
355
|
+
`redis-objects` can't guess at your data type in this situation, since you may
|
356
|
+
actually mean to store "1.5".
|
357
|
+
|
358
|
+
Sets
|
359
|
+
----
|
360
|
+
Sets work like the Ruby [Set](http://ruby-doc.org/core/classes/Set.html) class.
|
361
|
+
They are unordered, but guarantee uniqueness of members.
|
362
|
+
|
363
|
+
~~~ruby
|
364
|
+
@set = Redis::Set.new('set_name')
|
365
|
+
@set << 'a'
|
366
|
+
@set << 'b'
|
367
|
+
@set << 'a' # dup ignored
|
368
|
+
@set.member? 'c' # false
|
369
|
+
@set.members # ['a','b']
|
370
|
+
@set.members.reverse # ['b','a']
|
371
|
+
@set.each do |member|
|
372
|
+
puts member
|
373
|
+
end
|
374
|
+
@set.clear
|
375
|
+
# etc
|
376
|
+
~~~
|
377
|
+
|
378
|
+
You can perform Redis intersections/unions/diffs easily:
|
379
|
+
|
380
|
+
~~~ruby
|
381
|
+
@set1 = Redis::Set.new('set1')
|
382
|
+
@set2 = Redis::Set.new('set2')
|
383
|
+
@set3 = Redis::Set.new('set3')
|
384
|
+
members = @set1 & @set2 # intersection
|
385
|
+
members = @set1 | @set2 # union
|
386
|
+
members = @set1 + @set2 # union
|
387
|
+
members = @set1 ^ @set2 # difference
|
388
|
+
members = @set1 - @set2 # difference
|
389
|
+
members = @set1.intersection(@set2, @set3) # multiple
|
390
|
+
members = @set1.union(@set2, @set3) # multiple
|
391
|
+
members = @set1.difference(@set2, @set3) # multiple
|
392
|
+
~~~
|
393
|
+
|
394
|
+
Or store them in Redis:
|
395
|
+
|
396
|
+
~~~ruby
|
397
|
+
@set1.interstore('intername', @set2, @set3)
|
398
|
+
members = @set1.redis.get('intername')
|
399
|
+
@set1.unionstore('unionname', @set2, @set3)
|
400
|
+
members = @set1.redis.get('unionname')
|
401
|
+
@set1.diffstore('diffname', @set2, @set3)
|
402
|
+
members = @set1.redis.get('diffname')
|
403
|
+
~~~
|
404
|
+
|
405
|
+
And use complex data types too, with :marshal => true:
|
406
|
+
|
407
|
+
~~~ruby
|
408
|
+
@set1 = Redis::Set.new('set1', :marshal => true)
|
409
|
+
@set2 = Redis::Set.new('set2', :marshal => true)
|
410
|
+
@set1 << {:name => "Nate", :city => "San Diego"}
|
411
|
+
@set1 << {:name => "Peter", :city => "Oceanside"}
|
412
|
+
@set2 << {:name => "Nate", :city => "San Diego"}
|
413
|
+
@set2 << {:name => "Jeff", :city => "Del Mar"}
|
414
|
+
|
415
|
+
@set1 & @set2 # Nate
|
416
|
+
@set1 - @set2 # Peter
|
417
|
+
@set1 | @set2 # all 3 people
|
418
|
+
~~~
|
419
|
+
|
420
|
+
Sorted Sets
|
421
|
+
-----------
|
422
|
+
Due to their unique properties, Sorted Sets work like a hybrid between
|
423
|
+
a Hash and an Array. You assign like a Hash, but retrieve like an Array:
|
424
|
+
|
425
|
+
~~~ruby
|
426
|
+
@sorted_set = Redis::SortedSet.new('number_of_posts')
|
427
|
+
@sorted_set['Nate'] = 15
|
428
|
+
@sorted_set['Peter'] = 75
|
429
|
+
@sorted_set['Jeff'] = 24
|
430
|
+
|
431
|
+
# Array access to get sorted order
|
432
|
+
@sorted_set[0..2] # => ["Nate", "Jeff", "Peter"]
|
433
|
+
@sorted_set[0,2] # => ["Nate", "Jeff"]
|
434
|
+
|
435
|
+
@sorted_set['Peter'] # => 75
|
436
|
+
@sorted_set['Jeff'] # => 24
|
437
|
+
@sorted_set.score('Jeff') # same thing (24)
|
438
|
+
|
439
|
+
@sorted_set.rank('Peter') # => 2
|
440
|
+
@sorted_set.rank('Jeff') # => 1
|
441
|
+
|
442
|
+
@sorted_set.first # => "Nate"
|
443
|
+
@sorted_set.last # => "Peter"
|
444
|
+
@sorted_set.revrange(0,2) # => ["Peter", "Jeff", "Nate"]
|
445
|
+
|
446
|
+
@sorted_set['Newbie'] = 1
|
447
|
+
@sorted_set.members # => ["Newbie", "Nate", "Jeff", "Peter"]
|
448
|
+
@sorted_set.members.reverse # => ["Peter", "Jeff", "Nate", "Newbie"]
|
449
|
+
|
450
|
+
@sorted_set.rangebyscore(10, 100, :limit => 2) # => ["Nate", "Jeff"]
|
451
|
+
@sorted_set.members(:with_scores => true) # => [["Newbie", 1], ["Nate", 16], ["Jeff", 28], ["Peter", 76]]
|
452
|
+
|
453
|
+
# atomic increment
|
454
|
+
@sorted_set.increment('Nate')
|
455
|
+
@sorted_set.incr('Peter') # shorthand
|
456
|
+
@sorted_set.incr('Jeff', 4)
|
457
|
+
~~~
|
458
|
+
|
459
|
+
The other Redis Sorted Set commands are supported as well; see [Sorted Sets API](http://redis.io/commands#sorted_set).
|
460
|
+
|
461
|
+
<a name="atomicity"></a>
|
462
|
+
Atomic Counters and Locks
|
463
|
+
-------------------------
|
464
|
+
You are probably not handling atomicity correctly in your app. For a fun rant
|
465
|
+
on the topic, see [An Atomic Rant](http://nateware.com/an-atomic-rant.html).
|
466
|
+
|
467
|
+
Atomic counters are a good way to handle concurrency:
|
468
|
+
|
469
|
+
~~~ruby
|
470
|
+
@team = Team.find(1)
|
471
|
+
if @team.drafted_players.increment <= @team.max_players
|
472
|
+
# do stuff
|
473
|
+
@team.team_players.create!(:player_id => 221)
|
474
|
+
@team.active_players.increment
|
475
|
+
else
|
476
|
+
# reset counter state
|
477
|
+
@team.drafted_players.decrement
|
478
|
+
end
|
479
|
+
~~~
|
480
|
+
|
481
|
+
An _atomic block_ gives you a cleaner way to do the above. Exceptions or returning nil
|
482
|
+
will rewind the counter back to its previous state:
|
483
|
+
|
484
|
+
~~~ruby
|
485
|
+
@team.drafted_players.increment do |val|
|
486
|
+
raise Team::TeamFullError if val > @team.max_players # rewind
|
487
|
+
@team.team_players.create!(:player_id => 221)
|
488
|
+
@team.active_players.increment
|
489
|
+
end
|
490
|
+
~~~
|
491
|
+
|
492
|
+
Here's a similar approach, using an if block (failure rewinds counter):
|
493
|
+
|
494
|
+
~~~ruby
|
495
|
+
@team.drafted_players.increment do |val|
|
496
|
+
if val <= @team.max_players
|
497
|
+
@team.team_players.create!(:player_id => 221)
|
498
|
+
@team.active_players.increment
|
499
|
+
end
|
500
|
+
end
|
501
|
+
~~~
|
502
|
+
|
503
|
+
Class methods work too, using the familiar ActiveRecord counter syntax:
|
504
|
+
|
505
|
+
~~~ruby
|
506
|
+
Team.increment_counter :drafted_players, team_id
|
507
|
+
Team.decrement_counter :drafted_players, team_id, 2
|
508
|
+
Team.increment_counter :total_online_players # no ID on global counter
|
509
|
+
~~~
|
510
|
+
|
511
|
+
Class-level atomic blocks can also be used. This may save a DB fetch, if you have
|
512
|
+
a record ID and don't need any other attributes from the DB table:
|
513
|
+
|
514
|
+
~~~ruby
|
515
|
+
Team.increment_counter(:drafted_players, team_id) do |val|
|
516
|
+
TeamPitcher.create!(:team_id => team_id, :pitcher_id => 181)
|
517
|
+
Team.increment_counter(:active_players, team_id)
|
518
|
+
end
|
519
|
+
~~~
|
520
|
+
|
521
|
+
### Locks ###
|
522
|
+
|
523
|
+
Locks work similarly. On completion or exception the lock is released:
|
524
|
+
|
525
|
+
~~~ruby
|
526
|
+
class Team < ActiveRecord::Base
|
527
|
+
lock :reorder # declare a lock
|
528
|
+
end
|
529
|
+
|
530
|
+
@team.reorder_lock.lock do
|
531
|
+
@team.reorder_all_players
|
532
|
+
end
|
533
|
+
~~~
|
534
|
+
|
535
|
+
Class-level lock (same concept)
|
536
|
+
|
537
|
+
~~~ruby
|
538
|
+
Team.obtain_lock(:reorder, team_id) do
|
539
|
+
Team.reorder_all_players(team_id)
|
540
|
+
end
|
541
|
+
~~~
|
542
|
+
|
543
|
+
Lock expiration. Sometimes you want to make sure your locks are cleaned up should
|
544
|
+
the unthinkable happen (server failure). You can set lock expirations to handle
|
545
|
+
this. Expired locks are released by the next process to attempt lock. Just
|
546
|
+
make sure you expiration value is sufficiently large compared to your expected
|
547
|
+
lock time.
|
548
|
+
|
549
|
+
~~~ruby
|
550
|
+
class Team < ActiveRecord::Base
|
551
|
+
lock :reorder, :expiration => 15.minutes
|
552
|
+
end
|
553
|
+
~~~
|
554
|
+
|
555
|
+
Keep in mind that true locks serialize your entire application at that point. As
|
556
|
+
such, atomic counters are strongly preferred.
|
557
|
+
|
558
|
+
### Expiration ###
|
559
|
+
|
560
|
+
Use :expiration and :expireat options to set default expiration.
|
561
|
+
|
562
|
+
~~~ruby
|
563
|
+
value :value_with_expiration, :expiration => 1.hour
|
564
|
+
value :value_with_expireat, :expireat => lambda { Time.now + 1.hour }
|
565
|
+
~~~
|
566
|
+
|
567
|
+
:warning: In the above example, `expiration` is evaluated at class load time.
|
568
|
+
In this example, it will be one hour after loading the class, not after one hour
|
569
|
+
after setting a value. If you want to expire one hour after setting the value,
|
570
|
+
please use `:expireat` with `lambda`.
|
571
|
+
|
572
|
+
Custom serialization
|
573
|
+
--------------------
|
574
|
+
You can customize how values are serialized by setting `serializer: CustomSerializer`.
|
575
|
+
The default is `Marshal` from the standard lib, but it can be anything that responds to `dump` and
|
576
|
+
`load`. `JSON` and `YAML` are popular options.
|
577
|
+
|
578
|
+
If you need to pass extra arguments to `dump` or `load`, you can set
|
579
|
+
`marshal_dump_args: { foo: 'bar' }` and `marshal_load_args: { foo: 'bar' }` respectively.
|
580
|
+
|
581
|
+
~~~ruby
|
582
|
+
class CustomSerializer
|
583
|
+
def self.dump(value)
|
584
|
+
# custom code for serializing
|
585
|
+
end
|
586
|
+
|
587
|
+
def self.load(value)
|
588
|
+
# custom code for deserializing
|
589
|
+
end
|
590
|
+
end
|
591
|
+
|
592
|
+
@account = Account.create!(params[:account])
|
593
|
+
@newest = Redis::Value.new('custom_serializer', marshal: true, serializer: CustomSerializer)
|
594
|
+
@newest.value = @account.attributes
|
595
|
+
~~~
|
596
|
+
|
597
|
+
Author
|
598
|
+
=======
|
599
|
+
Copyright (c) 2009-2019 [Nate Wiger](http://nateware.com). All Rights Reserved.
|
600
|
+
Released under the [Artistic License](http://www.opensource.org/licenses/artistic-license-2.0.php).
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
desc "run all the specs"
|
4
|
+
task :test do
|
5
|
+
sh "bacon spec/*_spec.rb"
|
6
|
+
end
|
7
|
+
task :default => :test
|
8
|
+
task :spec => :test
|
9
|
+
|
10
|
+
desc "show changelog"
|
11
|
+
task :changelog do
|
12
|
+
latest = `git tag |tail -1`.chomp
|
13
|
+
sh "git log --pretty=format:'* %s %b [%an]' #{latest}..HEAD"
|
14
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'redis/helpers/core_commands'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
# Defines base functionality for all redis-objects.
|
5
|
+
class BaseObject
|
6
|
+
include Redis::Helpers::CoreCommands
|
7
|
+
|
8
|
+
attr_reader :key, :options
|
9
|
+
|
10
|
+
def initialize(key, *args)
|
11
|
+
@key = key.is_a?(Array) ? key.flatten.join(':') : key
|
12
|
+
@options = args.last.is_a?(Hash) ? args.pop : {}
|
13
|
+
@myredis = Objects::ConnectionPoolProxy.proxy_if_needed(args.first)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Dynamically query the handle to enable resetting midstream
|
17
|
+
def redis
|
18
|
+
@myredis || ::Redis::Objects.redis
|
19
|
+
end
|
20
|
+
|
21
|
+
alias :inspect :to_s # Ruby 1.9.2
|
22
|
+
|
23
|
+
def set_expiration
|
24
|
+
if !@options[:expiration].nil?
|
25
|
+
redis.expire(@key, @options[:expiration]) if redis.ttl(@key) < 0
|
26
|
+
elsif !@options[:expireat].nil?
|
27
|
+
expireat = @options[:expireat]
|
28
|
+
at = expireat.respond_to?(:call) ? expireat.call.to_i : expireat.to_i
|
29
|
+
redis.expireat(@key, at) if redis.ttl(@key) < 0
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def allow_expiration(&block)
|
34
|
+
result = block.call
|
35
|
+
set_expiration
|
36
|
+
result
|
37
|
+
end
|
38
|
+
|
39
|
+
def to_json(*args)
|
40
|
+
to_hash.to_json(*args)
|
41
|
+
rescue NoMethodError => e
|
42
|
+
raise e.class, "The current runtime does not provide a `to_json` implementation. Require 'json' or another JSON library and try again."
|
43
|
+
end
|
44
|
+
|
45
|
+
def as_json(*)
|
46
|
+
to_hash
|
47
|
+
end
|
48
|
+
|
49
|
+
def to_hash
|
50
|
+
{ "key" => @key, "options" => @options, "value" => value }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Math ops - delegate to value method
|
54
|
+
%w(== < > <= >=).each do |m|
|
55
|
+
class_eval <<-EndOverload
|
56
|
+
def #{m}(what)
|
57
|
+
value #{m} what
|
58
|
+
end
|
59
|
+
EndOverload
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|