redis-objects-legacy 1.6.0

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