moped 1.5.3 → 2.0.0.beta
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.
Potentially problematic release.
This version of moped might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +42 -5
- data/README.md +1 -1
- data/lib/moped.rb +10 -13
- data/lib/moped/address.rb +56 -0
- data/lib/moped/authenticatable.rb +89 -0
- data/lib/moped/cluster.rb +169 -136
- data/lib/moped/collection.rb +53 -19
- data/lib/moped/connection.rb +69 -10
- data/lib/moped/connection/manager.rb +49 -0
- data/lib/moped/connection/pool.rb +198 -0
- data/lib/moped/connection/queue.rb +93 -0
- data/lib/moped/connection/reaper.rb +52 -0
- data/lib/moped/connection/socket.rb +4 -0
- data/lib/moped/connection/socket/connectable.rb +169 -0
- data/lib/moped/connection/socket/ssl.rb +52 -0
- data/lib/moped/connection/socket/tcp.rb +25 -0
- data/lib/moped/connection/sockets.rb +4 -0
- data/lib/moped/cursor.rb +3 -5
- data/lib/moped/database.rb +18 -24
- data/lib/moped/errors.rb +35 -6
- data/lib/moped/executable.rb +96 -0
- data/lib/moped/failover.rb +41 -0
- data/lib/moped/failover/disconnect.rb +31 -0
- data/lib/moped/failover/ignore.rb +29 -0
- data/lib/moped/failover/reconfigure.rb +34 -0
- data/lib/moped/failover/retry.rb +37 -0
- data/lib/moped/indexes.rb +4 -1
- data/lib/moped/instrumentable.rb +39 -0
- data/lib/moped/instrumentable/log.rb +43 -0
- data/lib/moped/instrumentable/noop.rb +31 -0
- data/lib/moped/loggable.rb +110 -0
- data/lib/moped/node.rb +316 -297
- data/lib/moped/operation.rb +3 -0
- data/lib/moped/operation/read.rb +62 -0
- data/lib/moped/operation/write.rb +57 -0
- data/lib/moped/protocol/command.rb +65 -4
- data/lib/moped/protocol/commands/authenticate.rb +1 -2
- data/lib/moped/protocol/delete.rb +16 -0
- data/lib/moped/protocol/get_more.rb +102 -31
- data/lib/moped/protocol/insert.rb +17 -0
- data/lib/moped/protocol/message.rb +44 -46
- data/lib/moped/protocol/query.rb +175 -92
- data/lib/moped/protocol/reply.rb +19 -8
- data/lib/moped/protocol/update.rb +18 -0
- data/lib/moped/query.rb +43 -17
- data/lib/moped/read_preference.rb +49 -0
- data/lib/moped/read_preference/nearest.rb +55 -0
- data/lib/moped/read_preference/primary.rb +60 -0
- data/lib/moped/read_preference/primary_preferred.rb +55 -0
- data/lib/moped/read_preference/secondary.rb +50 -0
- data/lib/moped/read_preference/secondary_preferred.rb +53 -0
- data/lib/moped/read_preference/selectable.rb +79 -0
- data/lib/moped/readable.rb +55 -0
- data/lib/moped/session.rb +122 -70
- data/lib/moped/{mongo_uri.rb → uri.rb} +75 -31
- data/lib/moped/version.rb +1 -1
- data/lib/moped/write_concern.rb +33 -0
- data/lib/moped/write_concern/propagate.rb +38 -0
- data/lib/moped/write_concern/unverified.rb +28 -0
- metadata +79 -44
- data/lib/moped/bson.rb +0 -45
- data/lib/moped/bson/binary.rb +0 -137
- data/lib/moped/bson/code.rb +0 -112
- data/lib/moped/bson/document.rb +0 -41
- data/lib/moped/bson/extensions.rb +0 -91
- data/lib/moped/bson/extensions/array.rb +0 -37
- data/lib/moped/bson/extensions/boolean.rb +0 -16
- data/lib/moped/bson/extensions/false_class.rb +0 -19
- data/lib/moped/bson/extensions/float.rb +0 -22
- data/lib/moped/bson/extensions/hash.rb +0 -39
- data/lib/moped/bson/extensions/integer.rb +0 -36
- data/lib/moped/bson/extensions/nil_class.rb +0 -19
- data/lib/moped/bson/extensions/object.rb +0 -11
- data/lib/moped/bson/extensions/regexp.rb +0 -38
- data/lib/moped/bson/extensions/string.rb +0 -45
- data/lib/moped/bson/extensions/symbol.rb +0 -33
- data/lib/moped/bson/extensions/time.rb +0 -23
- data/lib/moped/bson/extensions/true_class.rb +0 -19
- data/lib/moped/bson/max_key.rb +0 -51
- data/lib/moped/bson/min_key.rb +0 -51
- data/lib/moped/bson/object_id.rb +0 -301
- data/lib/moped/bson/timestamp.rb +0 -38
- data/lib/moped/bson/types.rb +0 -67
- data/lib/moped/logging.rb +0 -58
- data/lib/moped/session/context.rb +0 -115
- data/lib/moped/sockets/connectable.rb +0 -167
- data/lib/moped/sockets/ssl.rb +0 -50
- data/lib/moped/sockets/tcp.rb +0 -23
- data/lib/moped/threaded.rb +0 -69
data/lib/moped/errors.rb
CHANGED
@@ -5,6 +5,18 @@ module Moped
|
|
5
5
|
# source of information on error codes.
|
6
6
|
ERROR_REFERENCE = "https://github.com/mongodb/mongo/blob/master/docs/errors.md"
|
7
7
|
|
8
|
+
# Raised when the connection pool is saturated and no new connection is
|
9
|
+
# reaped during the wait time.
|
10
|
+
class PoolSaturated < RuntimeError; end
|
11
|
+
|
12
|
+
# Raised when attempting to checkout a connection on a thread that already
|
13
|
+
# has a connection checked out.
|
14
|
+
class ConnectionInUse < RuntimeError; end
|
15
|
+
|
16
|
+
# Raised when attempting to checkout a pinned connection from the pool but
|
17
|
+
# it is already in use by another object on the same thread.
|
18
|
+
class PoolTimeout < RuntimeError; end
|
19
|
+
|
8
20
|
# Generic error class for exceptions related to connection failures.
|
9
21
|
class ConnectionFailure < StandardError; end
|
10
22
|
|
@@ -100,10 +112,7 @@ module Moped
|
|
100
112
|
class PotentialReconfiguration < MongoError
|
101
113
|
|
102
114
|
# Not master error codes.
|
103
|
-
NOT_MASTER = [ 13435, 13436
|
104
|
-
|
105
|
-
# Error codes received around reconfiguration
|
106
|
-
CONNECTION_ERRORS_RECONFIGURATION = [ 15988, 10276, 11600, 9001, 13639, 10009, 11002 ]
|
115
|
+
NOT_MASTER = [ 13435, 13436 ]
|
107
116
|
|
108
117
|
# Replica set reconfigurations can be either in the form of an operation
|
109
118
|
# error with code 13435, or with an error message stating the server is
|
@@ -113,8 +122,28 @@ module Moped
|
|
113
122
|
NOT_MASTER.include?(details["code"]) || err.include?("not master")
|
114
123
|
end
|
115
124
|
|
116
|
-
|
117
|
-
|
125
|
+
# Is the error due to a namespace not being found?
|
126
|
+
#
|
127
|
+
# @example Is the namespace not found?
|
128
|
+
# error.ns_not_found?
|
129
|
+
#
|
130
|
+
# @return [ true, false ] If the namespace was not found.
|
131
|
+
#
|
132
|
+
# @since 2.0.0
|
133
|
+
def ns_not_found?
|
134
|
+
details["errmsg"] == "ns not found"
|
135
|
+
end
|
136
|
+
|
137
|
+
# Is the error due to a namespace not existing?
|
138
|
+
#
|
139
|
+
# @example Doest the namespace not exist?
|
140
|
+
# error.ns_not_exists?
|
141
|
+
#
|
142
|
+
# @return [ true, false ] If the namespace was not found.
|
143
|
+
#
|
144
|
+
# @since 2.0.0
|
145
|
+
def ns_not_exists?
|
146
|
+
details["errmsg"] =~ /namespace does not exist/
|
118
147
|
end
|
119
148
|
end
|
120
149
|
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Moped
|
3
|
+
|
4
|
+
# Provides common behavior around executing a thread local stack safely.
|
5
|
+
#
|
6
|
+
# @since 2.0.0
|
7
|
+
module Executable
|
8
|
+
|
9
|
+
# Given the name of a thread local stack, ensure that execution happens by
|
10
|
+
# starting and ending the stack execution cleanly.
|
11
|
+
#
|
12
|
+
# @example Ensure execution of a pipeline.
|
13
|
+
# execute(:pipeline) do
|
14
|
+
# yield(self)
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# @param [ Symbol ] name The name of the stack.
|
18
|
+
#
|
19
|
+
# @return [ Object ] The result of the yield.
|
20
|
+
#
|
21
|
+
# @since 2.0.0
|
22
|
+
def execute(name)
|
23
|
+
begin_execution(name)
|
24
|
+
begin
|
25
|
+
yield(self)
|
26
|
+
ensure
|
27
|
+
end_execution(name)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Are we currently executing a stack on the thread?
|
32
|
+
#
|
33
|
+
# @example Are we executing a pipeline?
|
34
|
+
# executing?(:pipeline)
|
35
|
+
#
|
36
|
+
# @param [ Symbol ] name The name of the stack.
|
37
|
+
#
|
38
|
+
# @return [ true, false ] If we are executing the stack.
|
39
|
+
#
|
40
|
+
# @since 2.0.0
|
41
|
+
def executing?(name)
|
42
|
+
!stack(name).empty?
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# Begin entry into a named thread local stack.
|
48
|
+
#
|
49
|
+
# @api private
|
50
|
+
#
|
51
|
+
# @example Begin entry into the stack.
|
52
|
+
# executable.begin_execution(:create)
|
53
|
+
#
|
54
|
+
# @param [ String ] name The name of the stack.
|
55
|
+
#
|
56
|
+
# @return [ true ] True.
|
57
|
+
#
|
58
|
+
# @since 1.0.0
|
59
|
+
def begin_execution(name)
|
60
|
+
stack(name).push(true)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Exit from a named thread local stack.
|
64
|
+
#
|
65
|
+
# @api private
|
66
|
+
#
|
67
|
+
# @example Exit from the stack.
|
68
|
+
# executable.end_execution(:create)
|
69
|
+
#
|
70
|
+
# @param [ Symbol ] name The name of the stack
|
71
|
+
#
|
72
|
+
# @return [ true ] True.
|
73
|
+
#
|
74
|
+
# @since 1.0.0
|
75
|
+
def end_execution(name)
|
76
|
+
stack(name).pop
|
77
|
+
end
|
78
|
+
|
79
|
+
# Get the named stack.
|
80
|
+
#
|
81
|
+
# @api private
|
82
|
+
#
|
83
|
+
# @example Get a stack by name
|
84
|
+
# executable.stack(:create)
|
85
|
+
#
|
86
|
+
# @param [ Symbol ] name The name of the stack
|
87
|
+
#
|
88
|
+
# @return [ Array ] The stack.
|
89
|
+
#
|
90
|
+
# @since 1.0.0
|
91
|
+
def stack(name)
|
92
|
+
stacks = (Thread.current[:"moped-stacks"] ||= {})
|
93
|
+
stacks[name] ||= []
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "moped/failover/disconnect"
|
3
|
+
require "moped/failover/ignore"
|
4
|
+
require "moped/failover/reconfigure"
|
5
|
+
require "moped/failover/retry"
|
6
|
+
|
7
|
+
module Moped
|
8
|
+
|
9
|
+
# Provides behaviour around failover scenarios for different types of
|
10
|
+
# exceptions that get raised on connection and execution of operations.
|
11
|
+
#
|
12
|
+
# @since 2.0.0
|
13
|
+
module Failover
|
14
|
+
extend self
|
15
|
+
|
16
|
+
# Hash lookup for the failover classes based off the exception type.
|
17
|
+
#
|
18
|
+
# @since 2.0.0
|
19
|
+
STRATEGIES = {
|
20
|
+
Errors::AuthenticationFailure => Ignore,
|
21
|
+
Errors::ConnectionFailure => Retry,
|
22
|
+
Errors::CursorNotFound => Ignore,
|
23
|
+
Errors::OperationFailure => Reconfigure,
|
24
|
+
Errors::QueryFailure => Reconfigure
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
# Get the appropriate failover handler given the provided exception.
|
28
|
+
#
|
29
|
+
# @example Get the failover handler for an IOError.
|
30
|
+
# Moped::Failover.get(IOError)
|
31
|
+
#
|
32
|
+
# @param [ Exception ] exception The raised exception.
|
33
|
+
#
|
34
|
+
# @return [ Object ] The failover handler.
|
35
|
+
#
|
36
|
+
# @since 2.0.0
|
37
|
+
def get(exception)
|
38
|
+
STRATEGIES.fetch(exception.class, Disconnect)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Moped
|
3
|
+
module Failover
|
4
|
+
|
5
|
+
# Disconnect is for the case when we get exceptions we do not know about,
|
6
|
+
# and need to disconnect the node to cleanup the problem.
|
7
|
+
#
|
8
|
+
# @since 2.0.0
|
9
|
+
module Disconnect
|
10
|
+
extend self
|
11
|
+
|
12
|
+
# Executes the failover strategy. In the case of disconnect, we just re-raise
|
13
|
+
# the exception that was thrown previously extending a socket error and
|
14
|
+
# disconnect.
|
15
|
+
#
|
16
|
+
# @example Execute the disconnect strategy.
|
17
|
+
# Moped::Failover::Disconnect.execute(exception, node)
|
18
|
+
#
|
19
|
+
# @param [ Exception ] exception The raised exception.
|
20
|
+
# @param [ Node ] node The node the exception got raised on.
|
21
|
+
#
|
22
|
+
# @raise [ Errors::SocketError ] The extended exception that was thrown.
|
23
|
+
#
|
24
|
+
# @since 2.0.0
|
25
|
+
def execute(exception, node)
|
26
|
+
node.disconnect
|
27
|
+
raise(exception.extend(Errors::SocketError))
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Moped
|
3
|
+
module Failover
|
4
|
+
|
5
|
+
# Ignore is for the case when we get exceptions we deem are proper user
|
6
|
+
# or datbase errors and should be re-raised.
|
7
|
+
#
|
8
|
+
# @since 2.0.0
|
9
|
+
module Ignore
|
10
|
+
extend self
|
11
|
+
|
12
|
+
# Executes the failover strategy. In the case of ignore, we just re-raise
|
13
|
+
# the exception that was thrown previously.
|
14
|
+
#
|
15
|
+
# @example Execute the ignore strategy.
|
16
|
+
# Moped::Failover::Ignore.execute(exception, node)
|
17
|
+
#
|
18
|
+
# @param [ Exception ] exception The raised exception.
|
19
|
+
# @param [ Node ] node The node the exception got raised on.
|
20
|
+
#
|
21
|
+
# @raise [ Exception ] The exception that was previously thrown.
|
22
|
+
#
|
23
|
+
# @since 2.0.0
|
24
|
+
def execute(exception, node)
|
25
|
+
raise(exception)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Moped
|
3
|
+
module Failover
|
4
|
+
|
5
|
+
# Reconfigure is for exceptions that indicate that a replica set was
|
6
|
+
# potentially reconfigured in the middle of an operation.
|
7
|
+
#
|
8
|
+
# @since 2.0.0
|
9
|
+
module Reconfigure
|
10
|
+
extend self
|
11
|
+
|
12
|
+
# Executes the failover strategy. In the case of reconfigure, we check if
|
13
|
+
# the failure was due to a replica set reconfiguration mid operation and
|
14
|
+
# raise a new error if appropriate.
|
15
|
+
#
|
16
|
+
# @example Execute the reconfigure strategy.
|
17
|
+
# Moped::Failover::Reconfigure.execute(exception, node)
|
18
|
+
#
|
19
|
+
# @param [ Exception ] exception The raised exception.
|
20
|
+
# @param [ Node ] node The node the exception got raised on.
|
21
|
+
#
|
22
|
+
# @raise [ Exception, Errors::ReplicaSetReconfigure ] The exception that
|
23
|
+
# was previously thrown or a reconfiguration error.
|
24
|
+
#
|
25
|
+
# @since 2.0.0
|
26
|
+
def execute(exception, node)
|
27
|
+
if exception.reconfiguring_replica_set?
|
28
|
+
raise(Errors::ReplicaSetReconfigured.new(exception.command, exception.details))
|
29
|
+
end
|
30
|
+
raise(exception)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Moped
|
3
|
+
module Failover
|
4
|
+
|
5
|
+
# Retry is for the case when we get exceptions around the connection, and
|
6
|
+
# want to make another attempt to try and resolve the issue.
|
7
|
+
#
|
8
|
+
# @since 2.0.0
|
9
|
+
module Retry
|
10
|
+
extend self
|
11
|
+
|
12
|
+
# Executes the failover strategy. In the case of retyr, we disconnect and
|
13
|
+
# reconnect, then try the operation one more time.
|
14
|
+
#
|
15
|
+
# @example Execute the retry strategy.
|
16
|
+
# Moped::Failover::Retry.execute(exception, node)
|
17
|
+
#
|
18
|
+
# @param [ Exception ] exception The raised exception.
|
19
|
+
# @param [ Node ] node The node the exception got raised on.
|
20
|
+
#
|
21
|
+
# @raise [ Errors::ConnectionFailure ] If the retry fails.
|
22
|
+
#
|
23
|
+
# @return [ Object ] The result of the block yield.
|
24
|
+
#
|
25
|
+
# @since 2.0.0
|
26
|
+
def execute(exception, node)
|
27
|
+
node.disconnect
|
28
|
+
begin
|
29
|
+
yield if block_given?
|
30
|
+
rescue Exception => e
|
31
|
+
node.down!
|
32
|
+
raise(e)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/moped/indexes.rb
CHANGED
@@ -55,7 +55,10 @@ module Moped
|
|
55
55
|
def create(key, options = {})
|
56
56
|
spec = options.merge(ns: namespace, key: key)
|
57
57
|
spec[:name] ||= key.to_a.join("_")
|
58
|
-
|
58
|
+
|
59
|
+
database.session.with(write: { w: 1 }) do |_s|
|
60
|
+
_s[:"system.indexes"].insert(spec)
|
61
|
+
end
|
59
62
|
end
|
60
63
|
|
61
64
|
# Drop an index, or all indexes.
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "moped/instrumentable/log"
|
3
|
+
require "moped/instrumentable/noop"
|
4
|
+
|
5
|
+
module Moped
|
6
|
+
module Instrumentable
|
7
|
+
|
8
|
+
# The name of the topic of operations for Moped.
|
9
|
+
#
|
10
|
+
# @since 2.0.0
|
11
|
+
TOPIC = "moped.operations"
|
12
|
+
|
13
|
+
# Topic for warning instrumentation.
|
14
|
+
#
|
15
|
+
# @since 2.0.0
|
16
|
+
WARN = "moped.warn"
|
17
|
+
|
18
|
+
# @!attribute instrumenter
|
19
|
+
# @return [ Object ] The instrumenter
|
20
|
+
attr_reader :instrumenter
|
21
|
+
|
22
|
+
# Instrument and execute the provided block.
|
23
|
+
#
|
24
|
+
# @example Instrument and execute.
|
25
|
+
# instrument("moped.noop") do
|
26
|
+
# node.connect
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# @param [ String ] name The name of the operation.
|
30
|
+
# @param [ Hash ] payload The payload.
|
31
|
+
#
|
32
|
+
# @return [ Object ] The result of the yield.
|
33
|
+
#
|
34
|
+
# @since 2.0.0
|
35
|
+
def instrument(name, payload = {}, &block)
|
36
|
+
instrumenter.instrument(name, payload, &block)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Moped
|
3
|
+
module Instrumentable
|
4
|
+
|
5
|
+
# Provides logging instrumentation for compatibility with active support
|
6
|
+
# notifications.
|
7
|
+
#
|
8
|
+
# @since 2.0.0
|
9
|
+
class Log
|
10
|
+
|
11
|
+
class << self
|
12
|
+
|
13
|
+
# Instrument the log payload.
|
14
|
+
#
|
15
|
+
# @example Instrument the log payload.
|
16
|
+
# Log.instrument("moped.ops", {})
|
17
|
+
#
|
18
|
+
# @param [ String ] name The name of the logging type.
|
19
|
+
# @param [ Hash ] payload The log payload.
|
20
|
+
#
|
21
|
+
# @return [ Object ] The result of the yield.
|
22
|
+
#
|
23
|
+
# @since 2.0.0
|
24
|
+
def instrument(name, payload = {})
|
25
|
+
started = Time.new
|
26
|
+
begin
|
27
|
+
yield if block_given?
|
28
|
+
rescue Exception => e
|
29
|
+
payload[:exception] = [ e.class.name, e.message ]
|
30
|
+
raise e
|
31
|
+
ensure
|
32
|
+
runtime = ("%.4fms" % (1000 * (Time.now.to_f - started.to_f)))
|
33
|
+
if name == TOPIC
|
34
|
+
Moped::Loggable.log_operations(payload[:prefix], payload[:ops], runtime)
|
35
|
+
else
|
36
|
+
Moped::Loggable.debug(payload[:prefix], payload.reject { |k,v| k == :prefix }, runtime)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
module Moped
|
3
|
+
module Instrumentable
|
4
|
+
|
5
|
+
# Does not instrument anything, just yields.
|
6
|
+
#
|
7
|
+
# @since 2.0.0
|
8
|
+
class Noop
|
9
|
+
|
10
|
+
class << self
|
11
|
+
|
12
|
+
# Do not instrument anything.
|
13
|
+
#
|
14
|
+
# @example Do not instrument.
|
15
|
+
# Noop.instrument("moped.noop") do
|
16
|
+
# node.connect
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# @param [ String ] name The name of the operation.
|
20
|
+
# @param [ Hash ] payload The payload.
|
21
|
+
#
|
22
|
+
# @return [ Object ] The result of the yield.
|
23
|
+
#
|
24
|
+
# @since 2.0.0
|
25
|
+
def instrument(name, payload = {})
|
26
|
+
yield payload if block_given?
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|