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