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
@@ -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
|