redis-objects-legacy 1.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,145 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base_object'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
#
|
5
|
+
# Class representing a Redis counter. This functions like a proxy class, in
|
6
|
+
# that you can say @object.counter_name to get the value and then
|
7
|
+
# @object.counter_name.increment to operate on it. You can use this
|
8
|
+
# directly, or you can use the counter :foo class method in your
|
9
|
+
# class to define a counter.
|
10
|
+
#
|
11
|
+
class Counter < BaseObject
|
12
|
+
def initialize(key, *args)
|
13
|
+
super(key, *args)
|
14
|
+
@options[:start] ||= @options[:default] || 0
|
15
|
+
raise ArgumentError, "Marshalling redis counters does not make sense" if @options[:marshal]
|
16
|
+
redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
|
17
|
+
end
|
18
|
+
|
19
|
+
# Reset the counter to its starting value. Not atomic, so use with care.
|
20
|
+
# Normally only useful if you're discarding all sub-records associated
|
21
|
+
# with a parent and starting over (for example, restarting a game and
|
22
|
+
# disconnecting all players).
|
23
|
+
def reset(to=options[:start])
|
24
|
+
allow_expiration do
|
25
|
+
redis.set key, to.to_i
|
26
|
+
true # hack for redis-rb regression
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Reset the counter to its starting value, and return previous value.
|
31
|
+
# Use this to "reap" the counter and save it somewhere else. This is
|
32
|
+
# atomic in that no increments or decrements are lost if you process
|
33
|
+
# the returned value.
|
34
|
+
def getset(to=options[:start])
|
35
|
+
redis.getset(key, to.to_i).to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns the current value of the counter. Normally just calling the
|
39
|
+
# counter will lazily fetch the value, and only update it if increment
|
40
|
+
# or decrement is called. This forces a network call to redis-server
|
41
|
+
# to get the current value.
|
42
|
+
def value
|
43
|
+
redis.get(key).to_i
|
44
|
+
end
|
45
|
+
alias_method :get, :value
|
46
|
+
|
47
|
+
def value=(val)
|
48
|
+
allow_expiration do
|
49
|
+
if val.nil?
|
50
|
+
delete
|
51
|
+
else
|
52
|
+
redis.set key, val
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
alias_method :set, :value=
|
57
|
+
|
58
|
+
# Like .value but casts to float since Redis addresses these differently.
|
59
|
+
def to_f
|
60
|
+
redis.get(key).to_f
|
61
|
+
end
|
62
|
+
|
63
|
+
# Increment the counter atomically and return the new value. If passed
|
64
|
+
# a block, that block will be evaluated with the new value of the counter
|
65
|
+
# as an argument. If the block returns nil or throws an exception, the
|
66
|
+
# counter will automatically be decremented to its previous value. This
|
67
|
+
# method is aliased as incr() for brevity.
|
68
|
+
def increment(by=1, &block)
|
69
|
+
allow_expiration do
|
70
|
+
val = redis.incrby(key, by).to_i
|
71
|
+
block_given? ? rewindable_block(:decrement, by, val, &block) : val
|
72
|
+
end
|
73
|
+
end
|
74
|
+
alias_method :incr, :increment
|
75
|
+
alias_method :incrby, :increment
|
76
|
+
|
77
|
+
# Decrement the counter atomically and return the new value. If passed
|
78
|
+
# a block, that block will be evaluated with the new value of the counter
|
79
|
+
# as an argument. If the block returns nil or throws an exception, the
|
80
|
+
# counter will automatically be incremented to its previous value. This
|
81
|
+
# method is aliased as decr() for brevity.
|
82
|
+
def decrement(by=1, &block)
|
83
|
+
allow_expiration do
|
84
|
+
val = redis.decrby(key, by).to_i
|
85
|
+
block_given? ? rewindable_block(:increment, by, val, &block) : val
|
86
|
+
end
|
87
|
+
end
|
88
|
+
alias_method :decr, :decrement
|
89
|
+
alias_method :decrby, :decrement
|
90
|
+
|
91
|
+
# Increment a floating point counter atomically.
|
92
|
+
# Redis uses separate API's to interact with integers vs floats.
|
93
|
+
def incrbyfloat(by=1.0, &block)
|
94
|
+
allow_expiration do
|
95
|
+
val = redis.incrbyfloat(key, by).to_f
|
96
|
+
block_given? ? rewindable_block(:decrbyfloat, by, val, &block) : val
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Decrement a floating point counter atomically.
|
101
|
+
# Redis uses separate API's to interact with integers vs floats.
|
102
|
+
def decrbyfloat(by=1.0, &block)
|
103
|
+
allow_expiration do
|
104
|
+
val = redis.incrbyfloat(key, -by).to_f
|
105
|
+
block_given? ? rewindable_block(:incrbyfloat, by, val, &block) : val
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
##
|
110
|
+
# Proxy methods to help make @object.counter == 10 work
|
111
|
+
def to_s; value.to_s; end
|
112
|
+
alias_method :to_i, :value
|
113
|
+
|
114
|
+
def nil?
|
115
|
+
!redis.exists?(key)
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Math ops
|
120
|
+
# This needs to handle +/- either actual integers or other Redis::Counters
|
121
|
+
%w(+ - == < > <= >=).each do |m|
|
122
|
+
class_eval <<-EndOverload
|
123
|
+
def #{m}(what)
|
124
|
+
value.to_i #{m} what.to_i
|
125
|
+
end
|
126
|
+
EndOverload
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
# Implements atomic increment/decrement blocks
|
132
|
+
def rewindable_block(rewind, by, value, &block)
|
133
|
+
raise ArgumentError, "Missing block to rewindable_block somehow" unless block_given?
|
134
|
+
ret = nil
|
135
|
+
begin
|
136
|
+
ret = yield value
|
137
|
+
rescue
|
138
|
+
send(rewind, by)
|
139
|
+
raise
|
140
|
+
end
|
141
|
+
send(rewind, by) if ret.nil?
|
142
|
+
ret
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base_object'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
#
|
5
|
+
# Class representing a Redis enumerable type (list, set, sorted set, or hash).
|
6
|
+
#
|
7
|
+
class EnumerableObject < BaseObject
|
8
|
+
include Enumerable
|
9
|
+
|
10
|
+
# Iterate through each member. Redis::Objects mixes in Enumerable,
|
11
|
+
# so you can also use familiar methods like +collect+, +detect+, and so forth.
|
12
|
+
def each(&block)
|
13
|
+
value.each(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def sort(options={})
|
17
|
+
return super() if block_given?
|
18
|
+
options[:order] = "asc alpha" if options.keys.count == 0 # compat with Ruby
|
19
|
+
val = redis.sort(key, **options)
|
20
|
+
val.is_a?(Array) ? val.map{|v| unmarshal(v)} : val
|
21
|
+
end
|
22
|
+
|
23
|
+
# ActiveSupport's core extension `Enumerable#as_json` implementation is incompatible with ours.
|
24
|
+
def as_json(*)
|
25
|
+
to_hash
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/enumerable_object'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
#
|
5
|
+
# Class representing a Redis hash.
|
6
|
+
#
|
7
|
+
class HashKey < EnumerableObject
|
8
|
+
def initialize(key, *args)
|
9
|
+
super
|
10
|
+
@options[:marshal_keys] ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
# Redis: HSET
|
14
|
+
def store(field, value)
|
15
|
+
allow_expiration do
|
16
|
+
redis.hset(key, field, marshal(value, options[:marshal_keys][field]))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
alias_method :[]=, :store
|
20
|
+
|
21
|
+
# Redis: HGET
|
22
|
+
def hget(field)
|
23
|
+
unmarshal redis.hget(key, field), options[:marshal_keys][field]
|
24
|
+
end
|
25
|
+
alias_method :get, :hget
|
26
|
+
alias_method :[], :hget
|
27
|
+
|
28
|
+
# Verify that a field exists. Redis: HEXISTS
|
29
|
+
def has_key?(field)
|
30
|
+
redis.hexists(key, field)
|
31
|
+
end
|
32
|
+
alias_method :include?, :has_key?
|
33
|
+
alias_method :key?, :has_key?
|
34
|
+
alias_method :member?, :has_key?
|
35
|
+
|
36
|
+
# Delete fields. Redis: HDEL
|
37
|
+
def delete(*field)
|
38
|
+
redis.hdel(key, field)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Fetch a key in a way similar to Ruby's Hash#fetch
|
42
|
+
def fetch(field, *args, &block)
|
43
|
+
value = hget(field)
|
44
|
+
default = args[0]
|
45
|
+
|
46
|
+
return value if value || (!default && !block_given?)
|
47
|
+
|
48
|
+
block_given? ? block.call(field) : default
|
49
|
+
end
|
50
|
+
|
51
|
+
# Return all the keys of the hash. Redis: HKEYS
|
52
|
+
def keys
|
53
|
+
redis.hkeys(key)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Return all the values of the hash. Redis: HVALS
|
57
|
+
def values
|
58
|
+
redis.hvals(key).map{|v| unmarshal(v) }
|
59
|
+
end
|
60
|
+
alias_method :vals, :values
|
61
|
+
|
62
|
+
# Retrieve the entire hash. Redis: HGETALL
|
63
|
+
def all
|
64
|
+
h = redis.hgetall(key) || {}
|
65
|
+
h.each{|k,v| h[k] = unmarshal(v, options[:marshal_keys][k]) }
|
66
|
+
h
|
67
|
+
end
|
68
|
+
alias_method :clone, :all
|
69
|
+
alias_method :value, :all
|
70
|
+
|
71
|
+
# Enumerate through each keys. Redis: HKEYS
|
72
|
+
def each_key(&block)
|
73
|
+
keys.each(&block)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Enumerate through all values. Redis: HVALS
|
77
|
+
def each_value(&block)
|
78
|
+
values.each(&block)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return the size of the dict. Redis: HLEN
|
82
|
+
def size
|
83
|
+
redis.hlen(key)
|
84
|
+
end
|
85
|
+
alias_method :length, :size
|
86
|
+
alias_method :count, :size
|
87
|
+
|
88
|
+
# Returns true if dict is empty
|
89
|
+
def empty?
|
90
|
+
true if size == 0
|
91
|
+
end
|
92
|
+
|
93
|
+
# Set keys in bulk, takes a hash of field/values {'field1' => 'val1'}. Redis: HMSET
|
94
|
+
def bulk_set(*args)
|
95
|
+
raise ArgumentError, "Argument to bulk_set must be hash of key/value pairs" unless args.last.is_a?(::Hash)
|
96
|
+
allow_expiration do
|
97
|
+
redis.hmset(key, *args.last.inject([]){ |arr,kv|
|
98
|
+
arr + [kv[0], marshal(kv[1], options[:marshal_keys][kv[0]])]
|
99
|
+
})
|
100
|
+
end
|
101
|
+
end
|
102
|
+
alias_method :update, :bulk_set
|
103
|
+
|
104
|
+
# Set keys in bulk if they do not exist. Takes a hash of field/values {'field1' => 'val1'}. Redis: HSETNX
|
105
|
+
def fill(pairs={})
|
106
|
+
raise ArgumentError, "Argument to fill must be a hash of key/value pairs" unless pairs.is_a?(::Hash)
|
107
|
+
allow_expiration do
|
108
|
+
pairs.each do |field, value|
|
109
|
+
redis.hsetnx(key, field, marshal(value, options[:marshal_keys][field]))
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Get keys in bulk, takes an array of fields as arguments. Redis: HMGET
|
115
|
+
def bulk_get(*fields)
|
116
|
+
hsh = {}
|
117
|
+
get_fields = *fields.flatten
|
118
|
+
return hsh if get_fields.empty?
|
119
|
+
res = redis.hmget(key, get_fields)
|
120
|
+
get_fields.each do |k|
|
121
|
+
hsh[k] = unmarshal(res.shift, options[:marshal_keys][k])
|
122
|
+
end
|
123
|
+
hsh
|
124
|
+
end
|
125
|
+
|
126
|
+
# Get values in bulk, takes an array of fields as arguments.
|
127
|
+
# Values are returned in a collection in the same order than their keys in *keys Redis: HMGET
|
128
|
+
def bulk_values(*fields)
|
129
|
+
get_fields = *fields.flatten
|
130
|
+
return [] if get_fields.empty?
|
131
|
+
res = redis.hmget(key, get_fields)
|
132
|
+
get_fields.collect{|k| unmarshal(res.shift, options[:marshal_keys][k])}
|
133
|
+
end
|
134
|
+
|
135
|
+
# Increment value by integer at field. Redis: HINCRBY
|
136
|
+
def incrby(field, by=1)
|
137
|
+
allow_expiration do
|
138
|
+
ret = redis.hincrby(key, field, by)
|
139
|
+
ret.to_i
|
140
|
+
end
|
141
|
+
end
|
142
|
+
alias_method :incr, :incrby
|
143
|
+
|
144
|
+
# Decrement value by integer at field. Redis: HINCRBY
|
145
|
+
def decrby(field, by=1)
|
146
|
+
incrby(field, -by)
|
147
|
+
end
|
148
|
+
alias_method :decr, :decrby
|
149
|
+
|
150
|
+
# Increment value by float at field. Redis: HINCRBYFLOAT
|
151
|
+
def incrbyfloat(field, by=1.0)
|
152
|
+
allow_expiration do
|
153
|
+
ret = redis.hincrbyfloat(key, field, by)
|
154
|
+
ret.to_f
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Decrement value by float at field. Redis: HINCRBYFLOAT
|
159
|
+
def decrbyfloat(field, by=1.0)
|
160
|
+
incrbyfloat(field, -by)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
class Redis
|
2
|
+
module Helpers
|
3
|
+
# These are core commands that all types share (rename, etc)
|
4
|
+
module CoreCommands
|
5
|
+
def exists
|
6
|
+
redis.exists key
|
7
|
+
end
|
8
|
+
|
9
|
+
def exists?
|
10
|
+
redis.exists? key
|
11
|
+
end
|
12
|
+
|
13
|
+
# Delete key. Redis: DEL
|
14
|
+
def delete
|
15
|
+
redis.del key
|
16
|
+
end
|
17
|
+
alias_method :del, :delete
|
18
|
+
alias_method :clear, :delete
|
19
|
+
|
20
|
+
def type
|
21
|
+
redis.type key
|
22
|
+
end
|
23
|
+
|
24
|
+
def rename(name, setkey=true)
|
25
|
+
dest = name.is_a?(self.class) ? name.key : name
|
26
|
+
ret = redis.rename key, dest
|
27
|
+
@key = dest if ret && setkey
|
28
|
+
ret
|
29
|
+
end
|
30
|
+
|
31
|
+
def renamenx(name, setkey=true)
|
32
|
+
dest = name.is_a?(self.class) ? name.key : name
|
33
|
+
ret = redis.renamenx key, dest
|
34
|
+
@key = dest if ret && setkey
|
35
|
+
ret
|
36
|
+
end
|
37
|
+
|
38
|
+
def expire(seconds)
|
39
|
+
redis.expire key, seconds
|
40
|
+
end
|
41
|
+
|
42
|
+
def expireat(unixtime)
|
43
|
+
redis.expireat key, unixtime
|
44
|
+
end
|
45
|
+
|
46
|
+
def persist
|
47
|
+
redis.persist key
|
48
|
+
end
|
49
|
+
|
50
|
+
def ttl
|
51
|
+
redis.ttl(@key)
|
52
|
+
end
|
53
|
+
|
54
|
+
def move(dbindex)
|
55
|
+
redis.move key, dbindex
|
56
|
+
end
|
57
|
+
|
58
|
+
def serializer
|
59
|
+
options[:serializer] || Marshal
|
60
|
+
end
|
61
|
+
|
62
|
+
def marshal(value, domarshal=false)
|
63
|
+
if options[:marshal] || domarshal
|
64
|
+
dump_args = options[:marshal_dump_args] || []
|
65
|
+
serializer.dump(value, *(dump_args.is_a?(Array) ? dump_args : [dump_args]))
|
66
|
+
else
|
67
|
+
value
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def unmarshal(value, domarshal=false)
|
72
|
+
if value.nil?
|
73
|
+
nil
|
74
|
+
elsif options[:marshal] || domarshal
|
75
|
+
if value.is_a?(Array)
|
76
|
+
value.map{|v| unmarshal(v, domarshal)}
|
77
|
+
elsif !value.is_a?(String)
|
78
|
+
value
|
79
|
+
else
|
80
|
+
load_args = options[:marshal_load_args] || []
|
81
|
+
serializer.load(value, *(load_args.is_a?(Array) ? load_args : [load_args]))
|
82
|
+
end
|
83
|
+
else
|
84
|
+
value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
data/lib/redis/list.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/enumerable_object'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
#
|
5
|
+
# Class representing a Redis list. Instances of Redis::List are designed to
|
6
|
+
# behave as much like Ruby arrays as possible.
|
7
|
+
#
|
8
|
+
class List < EnumerableObject
|
9
|
+
# Works like push. Can chain together: list << 'a' << 'b'
|
10
|
+
def <<(value)
|
11
|
+
push(value) # marshal in push()
|
12
|
+
self # for << 'a' << 'b'
|
13
|
+
end
|
14
|
+
|
15
|
+
# Add a member before or after pivot in the list. Redis: LINSERT
|
16
|
+
def insert(where,pivot,value)
|
17
|
+
allow_expiration do
|
18
|
+
redis.linsert(key,where,marshal(pivot),marshal(value))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Add a member to the end of the list. Redis: RPUSH
|
23
|
+
def push(*values)
|
24
|
+
allow_expiration do
|
25
|
+
count = redis.rpush(key, values.map{|v| marshal(v) })
|
26
|
+
redis.ltrim(key, -options[:maxlength], -1) if options[:maxlength]
|
27
|
+
count
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Remove a member from the end of the list. Redis: RPOP
|
32
|
+
def pop(n=nil)
|
33
|
+
if n
|
34
|
+
result, = redis.multi do
|
35
|
+
redis.lrange(key, -n, -1)
|
36
|
+
redis.ltrim(key, 0, -n - 1)
|
37
|
+
end
|
38
|
+
unmarshal result
|
39
|
+
else
|
40
|
+
unmarshal redis.rpop(key)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Atomically pops a value from one list, pushes to another and returns the
|
45
|
+
# value. Destination can be a String or a Redis::List
|
46
|
+
#
|
47
|
+
# list.rpoplpush(destination)
|
48
|
+
#
|
49
|
+
# Returns the popped/pushed value.
|
50
|
+
#
|
51
|
+
# Redis: RPOPLPUSH
|
52
|
+
def rpoplpush(destination)
|
53
|
+
unmarshal redis.rpoplpush(key, destination.is_a?(Redis::List) ? destination.key : destination.to_s)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Add a member to the start of the list. Redis: LPUSH
|
57
|
+
def unshift(*values)
|
58
|
+
allow_expiration do
|
59
|
+
count = redis.lpush(key, values.map{|v| marshal(v) })
|
60
|
+
redis.ltrim(key, 0, options[:maxlength] - 1) if options[:maxlength]
|
61
|
+
count
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Remove a member from the start of the list. Redis: LPOP
|
66
|
+
def shift(n=nil)
|
67
|
+
if n
|
68
|
+
result, = redis.multi do
|
69
|
+
redis.lrange(key, 0, n - 1)
|
70
|
+
redis.ltrim(key, n, -1)
|
71
|
+
end
|
72
|
+
unmarshal result
|
73
|
+
else
|
74
|
+
unmarshal redis.lpop(key)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# Return all values in the list. Redis: LRANGE(0,-1)
|
79
|
+
def values
|
80
|
+
vals = range(0, -1)
|
81
|
+
vals.nil? ? [] : vals
|
82
|
+
end
|
83
|
+
alias_method :get, :values
|
84
|
+
alias_method :value, :values
|
85
|
+
|
86
|
+
# Same functionality as Ruby arrays. If a single number is given, return
|
87
|
+
# just the element at that index using Redis: LINDEX. Otherwise, return
|
88
|
+
# a range of values using Redis: LRANGE.
|
89
|
+
def [](index, length=nil)
|
90
|
+
if index.is_a? Range
|
91
|
+
range(index.first, index.max)
|
92
|
+
elsif length
|
93
|
+
case length <=> 0
|
94
|
+
when 1 then range(index, index + length - 1)
|
95
|
+
when 0 then []
|
96
|
+
when -1 then nil # Ruby does this (a bit weird)
|
97
|
+
end
|
98
|
+
else
|
99
|
+
at(index)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
alias_method :slice, :[]
|
103
|
+
|
104
|
+
# Same functionality as Ruby arrays.
|
105
|
+
def []=(index, value)
|
106
|
+
allow_expiration do
|
107
|
+
redis.lset(key, index, marshal(value))
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Delete the element(s) from the list that match name. If count is specified,
|
112
|
+
# only the first-N (if positive) or last-N (if negative) will be removed.
|
113
|
+
# Use .del to completely delete the entire key.
|
114
|
+
# Redis: LREM
|
115
|
+
def delete(name, count=0)
|
116
|
+
redis.lrem(key, count, marshal(name)) # weird api
|
117
|
+
end
|
118
|
+
|
119
|
+
# Return a range of values from +start_index+ to +end_index+. Can also use
|
120
|
+
# the familiar list[start,end] Ruby syntax. Redis: LRANGE
|
121
|
+
def range(start_index, end_index)
|
122
|
+
redis.lrange(key, start_index, end_index).map{|v| unmarshal(v) }
|
123
|
+
end
|
124
|
+
|
125
|
+
# Return the value at the given index. Can also use familiar list[index] syntax.
|
126
|
+
# Redis: LINDEX
|
127
|
+
def at(index)
|
128
|
+
unmarshal redis.lindex(key, index)
|
129
|
+
end
|
130
|
+
|
131
|
+
# Return the first element in the list. Redis: LINDEX(0)
|
132
|
+
def first
|
133
|
+
at(0)
|
134
|
+
end
|
135
|
+
|
136
|
+
# Return the last element in the list. Redis: LINDEX(-1)
|
137
|
+
def last
|
138
|
+
at(-1)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Return the length of the list. Aliased as size. Redis: LLEN
|
142
|
+
def length
|
143
|
+
redis.llen(key)
|
144
|
+
end
|
145
|
+
alias_method :size, :length
|
146
|
+
|
147
|
+
# Returns true if there are no elements in the list. Redis: LLEN == 0
|
148
|
+
def empty?
|
149
|
+
length == 0
|
150
|
+
end
|
151
|
+
|
152
|
+
def ==(x)
|
153
|
+
values == x
|
154
|
+
end
|
155
|
+
|
156
|
+
def to_s
|
157
|
+
values.join(', ')
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/lib/redis/lock.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/base_object'
|
2
|
+
|
3
|
+
class Redis
|
4
|
+
#
|
5
|
+
# Class representing a lock. This functions like a proxy class, in
|
6
|
+
# that you can say @object.lock_name { block } to use the lock and also
|
7
|
+
# @object.counter_name.clear to reset on it. You can use this
|
8
|
+
# directly, but it is better to use the lock :foo class method in your
|
9
|
+
# class to define a lock.
|
10
|
+
#
|
11
|
+
class Lock < BaseObject
|
12
|
+
class LockTimeout < StandardError; end #:nodoc:
|
13
|
+
|
14
|
+
def initialize(key, *args)
|
15
|
+
super(key, *args)
|
16
|
+
@options[:timeout] ||= 5
|
17
|
+
@options[:init] = false if @options[:init].nil? # default :init to false
|
18
|
+
redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
|
19
|
+
end
|
20
|
+
|
21
|
+
def value
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
# Get the lock and execute the code block. Any other code that needs the lock
|
26
|
+
# (on any server) will spin waiting for the lock up to the :timeout
|
27
|
+
# that was specified when the lock was defined.
|
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
|
33
|
+
try_until_timeout do
|
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)
|
39
|
+
|
40
|
+
# Backward compatibility code
|
41
|
+
# TODO: remove at the next major release for performance
|
42
|
+
unless @options[:expiration].nil?
|
43
|
+
old_expiration = redis.get(key).to_f
|
44
|
+
|
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)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
begin
|
56
|
+
yield
|
57
|
+
ensure
|
58
|
+
# We need to be careful when cleaning up the lock key. If we took a really long
|
59
|
+
# time for some reason, and the lock expired, someone else may have it, and
|
60
|
+
# it's not safe for us to remove it. Check how much time has passed since we
|
61
|
+
# wrote the lock key and only delete it if it hasn't expired (or we're not using
|
62
|
+
# lock expiration)
|
63
|
+
if @options[:expiration].nil? || end_time > Time.now.to_f
|
64
|
+
redis.del(key)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Return expiration in milliseconds
|
70
|
+
def generate_expiration
|
71
|
+
((@options[:expiration].nil? ? 1 : @options[:expiration].to_f) * 1000).to_i
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def try_until_timeout
|
77
|
+
if @options[:timeout] == 0
|
78
|
+
yield
|
79
|
+
else
|
80
|
+
start = Time.now
|
81
|
+
while Time.now - start < @options[:timeout]
|
82
|
+
yield
|
83
|
+
sleep 0.1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
raise LockTimeout, "Timeout on lock #{key} exceeded #{@options[:timeout]} sec"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
class Redis
|
2
|
+
module Objects
|
3
|
+
class ConnectionPoolProxy
|
4
|
+
def initialize(pool)
|
5
|
+
raise ArgumentError "Should only proxy ConnectionPool!" unless self.class.should_proxy?(pool)
|
6
|
+
@pool = pool
|
7
|
+
end
|
8
|
+
|
9
|
+
def method_missing(name, *args, &block)
|
10
|
+
@pool.with { |x| x.send(name, *args, &block) }
|
11
|
+
end
|
12
|
+
ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
|
13
|
+
|
14
|
+
def respond_to_missing?(name, include_all = false)
|
15
|
+
@pool.with { |x| x.respond_to?(name, include_all) }
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.should_proxy?(conn)
|
19
|
+
defined?(::ConnectionPool) && conn.is_a?(::ConnectionPool)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.proxy_if_needed(conn)
|
23
|
+
if should_proxy?(conn)
|
24
|
+
ConnectionPoolProxy.new(conn)
|
25
|
+
else
|
26
|
+
conn
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|