bunny 0.6.3.rc2 → 0.7
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.
- data/.gitignore +8 -0
- data/.rspec +3 -0
- data/.travis.yml +15 -0
- data/.yardopts +9 -0
- data/CHANGELOG +3 -0
- data/Gemfile +39 -0
- data/Gemfile.lock +34 -0
- data/LICENSE +5 -4
- data/README.textile +54 -0
- data/Rakefile +15 -13
- data/bunny.gemspec +42 -61
- data/examples/simple_08.rb +4 -2
- data/examples/simple_09.rb +4 -2
- data/examples/simple_ack_08.rb +3 -1
- data/examples/simple_ack_09.rb +3 -1
- data/examples/simple_consumer_08.rb +4 -2
- data/examples/simple_consumer_09.rb +4 -2
- data/examples/simple_fanout_08.rb +3 -1
- data/examples/simple_fanout_09.rb +3 -1
- data/examples/simple_headers_08.rb +5 -3
- data/examples/simple_headers_09.rb +5 -3
- data/examples/simple_publisher_08.rb +3 -1
- data/examples/simple_publisher_09.rb +3 -1
- data/examples/simple_topic_08.rb +5 -3
- data/examples/simple_topic_09.rb +5 -3
- data/ext/amqp-0.8.json +616 -0
- data/ext/amqp-0.9.1.json +388 -0
- data/ext/config.yml +4 -0
- data/ext/qparser.rb +463 -0
- data/lib/bunny.rb +88 -66
- data/lib/bunny/channel08.rb +38 -38
- data/lib/bunny/channel09.rb +37 -37
- data/lib/bunny/client08.rb +184 -206
- data/lib/bunny/client09.rb +277 -363
- data/lib/bunny/consumer.rb +35 -0
- data/lib/bunny/exchange08.rb +37 -41
- data/lib/bunny/exchange09.rb +106 -124
- data/lib/bunny/queue08.rb +216 -202
- data/lib/bunny/queue09.rb +256 -326
- data/lib/bunny/subscription08.rb +30 -29
- data/lib/bunny/subscription09.rb +84 -83
- data/lib/bunny/version.rb +5 -0
- data/lib/qrack/amq-client-url.rb +165 -0
- data/lib/qrack/channel.rb +19 -17
- data/lib/qrack/client.rb +152 -151
- data/lib/qrack/errors.rb +5 -0
- data/lib/qrack/protocol/protocol08.rb +132 -130
- data/lib/qrack/protocol/protocol09.rb +133 -131
- data/lib/qrack/protocol/spec08.rb +2 -0
- data/lib/qrack/protocol/spec09.rb +2 -0
- data/lib/qrack/qrack08.rb +7 -10
- data/lib/qrack/qrack09.rb +7 -10
- data/lib/qrack/queue.rb +27 -40
- data/lib/qrack/subscription.rb +102 -101
- data/lib/qrack/transport/buffer08.rb +266 -264
- data/lib/qrack/transport/buffer09.rb +268 -264
- data/lib/qrack/transport/frame08.rb +13 -11
- data/lib/qrack/transport/frame09.rb +9 -7
- data/spec/spec_08/bunny_spec.rb +48 -45
- data/spec/spec_08/connection_spec.rb +10 -7
- data/spec/spec_08/exchange_spec.rb +145 -143
- data/spec/spec_08/queue_spec.rb +161 -161
- data/spec/spec_09/bunny_spec.rb +46 -44
- data/spec/spec_09/connection_spec.rb +15 -8
- data/spec/spec_09/exchange_spec.rb +147 -145
- data/spec/spec_09/queue_spec.rb +182 -184
- metadata +60 -41
- data/README.rdoc +0 -66
@@ -0,0 +1,35 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
####################################
|
4
|
+
# NOTE: THIS CLASS IS HERE TO MAKE #
|
5
|
+
# TRANSITION TO AMQ CLIENT EASIER #
|
6
|
+
####################################
|
7
|
+
|
8
|
+
require "qrack/subscription"
|
9
|
+
|
10
|
+
# NOTE: This file is rather a temporary hack to fix
|
11
|
+
# https://github.com/ruby-amqp/bunny/issues/9 then
|
12
|
+
# some permanent solution. It's mostly copied from
|
13
|
+
# the AMQP and AMQ Client gems. Later on we should
|
14
|
+
# use AMQ Client directly and just inherit from
|
15
|
+
# the AMQ::Client::Sync::Consumer class.
|
16
|
+
|
17
|
+
module Bunny
|
18
|
+
|
19
|
+
# AMQP consumers are entities that handle messages delivered
|
20
|
+
# to them ("push API" as opposed to "pull API") by AMQP broker.
|
21
|
+
# Every consumer is associated with a queue. Consumers can be
|
22
|
+
# exclusive (no other consumers can be registered for the same
|
23
|
+
# queue) or not (consumers share the queue). In the case of
|
24
|
+
# multiple consumers per queue, messages are distributed in
|
25
|
+
# round robin manner with respect to channel-level prefetch
|
26
|
+
# setting).
|
27
|
+
class Consumer < Qrack::Subscription
|
28
|
+
def initialize(*args)
|
29
|
+
super(*args)
|
30
|
+
@consumer_tag ||= (1..32).to_a.shuffle.join
|
31
|
+
end
|
32
|
+
|
33
|
+
alias_method :consume, :start
|
34
|
+
end
|
35
|
+
end
|
data/lib/bunny/exchange08.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
1
3
|
module Bunny
|
2
|
-
|
4
|
+
|
3
5
|
=begin rdoc
|
4
6
|
|
5
7
|
=== DESCRIPTION:
|
@@ -28,7 +30,7 @@ target broker/server or visit the {AMQP website}[http://www.amqp.org] to find th
|
|
28
30
|
specification that applies to your target broker/server.
|
29
31
|
|
30
32
|
=end
|
31
|
-
|
33
|
+
|
32
34
|
class Exchange
|
33
35
|
|
34
36
|
attr_reader :client, :type, :name, :opts, :key
|
@@ -36,38 +38,35 @@ specification that applies to your target broker/server.
|
|
36
38
|
def initialize(client, name, opts = {})
|
37
39
|
# check connection to server
|
38
40
|
raise Bunny::ConnectionError, 'Not connected to server' if client.status == :not_connected
|
39
|
-
|
41
|
+
|
40
42
|
@client, @name, @opts = client, name, opts
|
41
|
-
|
43
|
+
|
42
44
|
# set up the exchange type catering for default names
|
43
|
-
if name
|
44
|
-
|
45
|
+
if name =~ /^amq\.(.+)$/
|
46
|
+
predeclared = true
|
47
|
+
new_type = $1
|
45
48
|
# handle 'amq.match' default
|
46
49
|
new_type = 'headers' if new_type == 'match'
|
47
50
|
@type = new_type.to_sym
|
48
51
|
else
|
49
52
|
@type = opts[:type] || :direct
|
50
53
|
end
|
51
|
-
|
54
|
+
|
52
55
|
@key = opts[:key]
|
53
56
|
@client.exchanges[@name] ||= self
|
54
|
-
|
57
|
+
|
55
58
|
# ignore the :nowait option if passed, otherwise program will hang waiting for a
|
56
59
|
# response that will not be sent by the server
|
57
60
|
opts.delete(:nowait)
|
58
|
-
|
59
|
-
unless name == "amq.#{type}" or name == ''
|
60
|
-
client.send_frame(
|
61
|
-
Qrack::Protocol::Exchange::Declare.new(
|
62
|
-
{ :exchange => name, :type => type, :nowait => false }.merge(opts)
|
63
|
-
)
|
64
|
-
)
|
65
61
|
|
66
|
-
|
62
|
+
unless predeclared or name == ''
|
63
|
+
opts = { :exchange => name, :type => type, :nowait => false }.merge(opts)
|
64
|
+
|
65
|
+
client.send_frame(Qrack::Protocol::Exchange::Declare.new(opts))
|
67
66
|
|
68
|
-
|
69
|
-
"Error declaring exchange #{name}: type = #{type}")
|
67
|
+
method = client.next_method
|
70
68
|
|
69
|
+
client.check_response(method, Qrack::Protocol::Exchange::DeclareOk, "Error declaring exchange #{name}: type = #{type}")
|
71
70
|
end
|
72
71
|
end
|
73
72
|
|
@@ -95,14 +94,11 @@ if successful. If an error occurs raises _Bunny_::_ProtocolError_.
|
|
95
94
|
# response that will not be sent by the server
|
96
95
|
opts.delete(:nowait)
|
97
96
|
|
98
|
-
client.send_frame(
|
99
|
-
Qrack::Protocol::Exchange::Delete.new({ :exchange => name, :nowait => false }.merge(opts))
|
100
|
-
)
|
97
|
+
client.send_frame(Qrack::Protocol::Exchange::Delete.new({ :exchange => name, :nowait => false }.merge(opts)))
|
101
98
|
|
102
|
-
|
99
|
+
method = client.next_method
|
103
100
|
|
104
|
-
|
105
|
-
"Error deleting exchange #{name}")
|
101
|
+
client.check_response(method, Qrack::Protocol::Exchange::DeleteOk, "Error deleting exchange #{name}")
|
106
102
|
|
107
103
|
client.exchanges.delete(name)
|
108
104
|
|
@@ -122,6 +118,7 @@ if any, is committed.
|
|
122
118
|
|
123
119
|
* <tt>:key => 'routing_key'</tt> - Specifies the routing key for the message. The routing key is
|
124
120
|
used for routing messages depending on the exchange configuration.
|
121
|
+
* <tt>:content_type => 'content/type'</tt> - Specifies the content type to use for the message.
|
125
122
|
* <tt>:mandatory => true or false (_default_)</tt> - Tells the server how to react if the message
|
126
123
|
cannot be routed to a queue. If set to _true_, the server will return an unroutable message
|
127
124
|
with a Return method. If this flag is zero, the server silently drops the message.
|
@@ -130,8 +127,8 @@ if any, is committed.
|
|
130
127
|
undeliverable message with a Return method. If set to _false_, the server will queue the message,
|
131
128
|
but with no guarantee that it will ever be consumed.
|
132
129
|
* <tt>:persistent => true or false (_default_)</tt> - Tells the server whether to persist the message
|
133
|
-
If set to _true_, the message will be persisted to disk and not lost if the server restarts.
|
134
|
-
If set to _false_, the message will not be persisted across server restart. Setting to _true_
|
130
|
+
If set to _true_, the message will be persisted to disk and not lost if the server restarts.
|
131
|
+
If set to _false_, the message will not be persisted across server restart. Setting to _true_
|
135
132
|
incurs a performance penalty as there is an extra cost associated with disk access.
|
136
133
|
|
137
134
|
==== RETURNS:
|
@@ -144,25 +141,24 @@ nil
|
|
144
141
|
opts = opts.dup
|
145
142
|
out = []
|
146
143
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
)
|
144
|
+
# Set up options
|
145
|
+
routing_key = opts.delete(:key) || key
|
146
|
+
mandatory = opts.delete(:mandatory)
|
147
|
+
immediate = opts.delete(:immediate)
|
148
|
+
delivery_mode = opts.delete(:persistent) ? 2 : 1
|
149
|
+
content_type = opts.delete(:content_type) || 'application/octet-stream'
|
150
|
+
|
151
|
+
out << Qrack::Protocol::Basic::Publish.new({ :exchange => name,
|
152
|
+
:routing_key => routing_key,
|
153
|
+
:mandatory => mandatory,
|
154
|
+
:immediate => immediate })
|
159
155
|
data = data.to_s
|
160
156
|
out << Qrack::Protocol::Header.new(
|
161
157
|
Qrack::Protocol::Basic,
|
162
158
|
data.bytesize, {
|
163
|
-
:content_type =>
|
159
|
+
:content_type => content_type,
|
164
160
|
:delivery_mode => delivery_mode,
|
165
|
-
:priority => 0
|
161
|
+
:priority => 0
|
166
162
|
}.merge(opts)
|
167
163
|
)
|
168
164
|
out << Qrack::Transport::Body.new(data)
|
@@ -171,5 +167,5 @@ nil
|
|
171
167
|
end
|
172
168
|
|
173
169
|
end
|
174
|
-
|
170
|
+
|
175
171
|
end
|
data/lib/bunny/exchange09.rb
CHANGED
@@ -1,34 +1,29 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
=begin rdoc
|
4
|
-
|
5
|
-
=== DESCRIPTION:
|
6
|
-
|
7
|
-
*Exchanges* are the routing and distribution hub of AMQP. All messages that Bunny sends
|
8
|
-
to an AMQP broker/server _have_ to pass through an exchange in order to be routed to a
|
9
|
-
destination queue. The AMQP specification defines the types of exchange that you can create.
|
10
|
-
|
11
|
-
At the time of writing there are four (4) types of exchange defined -
|
1
|
+
# encoding: utf-8
|
12
2
|
|
13
|
-
|
14
|
-
* <tt>:fanout</tt>
|
15
|
-
* <tt>:topic</tt>
|
16
|
-
* <tt>:headers</tt>
|
17
|
-
|
18
|
-
AMQP-compliant brokers/servers are required to provide default exchanges for the _direct_ and
|
19
|
-
_fanout_ exchange types. All default exchanges are prefixed with <tt>'amq.'</tt>, for example -
|
20
|
-
|
21
|
-
* <tt>amq.direct</tt>
|
22
|
-
* <tt>amq.fanout</tt>
|
23
|
-
* <tt>amq.topic</tt>
|
24
|
-
* <tt>amq.match</tt> or <tt>amq.headers</tt>
|
25
|
-
|
26
|
-
If you want more information about exchanges, please consult the documentation for your
|
27
|
-
target broker/server or visit the {AMQP website}[http://www.amqp.org] to find the version of the
|
28
|
-
specification that applies to your target broker/server.
|
3
|
+
module Bunny
|
29
4
|
|
30
|
-
|
31
|
-
|
5
|
+
# *Exchanges* are the routing and distribution hub of AMQP. All messages that Bunny sends
|
6
|
+
# to an AMQP broker/server @have_to pass through an exchange in order to be routed to a
|
7
|
+
# destination queue. The AMQP specification defines the types of exchange that you can create.
|
8
|
+
#
|
9
|
+
# At the time of writing there are four (4) types of exchange defined:
|
10
|
+
#
|
11
|
+
# * @:direct@
|
12
|
+
# * @:fanout@
|
13
|
+
# * @:topic@
|
14
|
+
# * @:headers@
|
15
|
+
#
|
16
|
+
# AMQP-compliant brokers/servers are required to provide default exchanges for the @direct@ and
|
17
|
+
# @fanout@ exchange types. All default exchanges are prefixed with @'amq.'@, for example:
|
18
|
+
#
|
19
|
+
# * @amq.direct@
|
20
|
+
# * @amq.fanout@
|
21
|
+
# * @amq.topic@
|
22
|
+
# * @amq.match@ or @amq.headers@
|
23
|
+
#
|
24
|
+
# If you want more information about exchanges, please consult the documentation for your
|
25
|
+
# target broker/server or visit the "AMQP website":http://www.amqp.org to find the version of the
|
26
|
+
# specification that applies to your target broker/server.
|
32
27
|
class Exchange09
|
33
28
|
|
34
29
|
attr_reader :client, :type, :name, :opts, :key
|
@@ -36,74 +31,63 @@ specification that applies to your target broker/server.
|
|
36
31
|
def initialize(client, name, opts = {})
|
37
32
|
# check connection to server
|
38
33
|
raise Bunny::ConnectionError, 'Not connected to server' if client.status == :not_connected
|
39
|
-
|
34
|
+
|
40
35
|
@client, @name, @opts = client, name, opts
|
41
|
-
|
36
|
+
|
42
37
|
# set up the exchange type catering for default names
|
43
|
-
if name
|
44
|
-
|
38
|
+
if name =~ /^amq\.(.+)$/
|
39
|
+
predeclared = true
|
40
|
+
new_type = $1
|
45
41
|
# handle 'amq.match' default
|
46
42
|
new_type = 'headers' if new_type == 'match'
|
47
43
|
@type = new_type.to_sym
|
48
44
|
else
|
49
45
|
@type = opts[:type] || :direct
|
50
46
|
end
|
51
|
-
|
47
|
+
|
52
48
|
@key = opts[:key]
|
53
49
|
@client.exchanges[@name] ||= self
|
54
|
-
|
50
|
+
|
55
51
|
# ignore the :nowait option if passed, otherwise program will hang waiting for a
|
56
52
|
# response that will not be sent by the server
|
57
53
|
opts.delete(:nowait)
|
58
|
-
|
59
|
-
unless
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
)
|
54
|
+
|
55
|
+
unless predeclared or name == ''
|
56
|
+
opts = {
|
57
|
+
:exchange => name, :type => type, :nowait => false,
|
58
|
+
:deprecated_ticket => 0, :deprecated_auto_delete => false, :deprecated_internal => false
|
59
|
+
}.merge(opts)
|
60
|
+
|
61
|
+
client.send_frame(Qrack::Protocol09::Exchange::Declare.new(opts))
|
66
62
|
|
67
63
|
method = client.next_method
|
68
64
|
|
69
|
-
|
70
|
-
"Error declaring exchange #{name}: type = #{type}")
|
71
|
-
|
65
|
+
client.check_response(method, Qrack::Protocol09::Exchange::DeclareOk, "Error declaring exchange #{name}: type = #{type}")
|
72
66
|
end
|
73
67
|
end
|
74
68
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
delete the exchange if it has no queue bindings. If the exchange has queue bindings the
|
86
|
-
server does not delete it but raises a channel exception instead.
|
87
|
-
* <tt>:nowait => true or false (_default_)</tt> - Ignored by Bunny, always _false_.
|
88
|
-
|
89
|
-
==== Returns:
|
90
|
-
|
91
|
-
<tt>:delete_ok</tt> if successful
|
92
|
-
=end
|
93
|
-
|
69
|
+
# Requests that an exchange is deleted from broker/server. Removes reference from exchanges
|
70
|
+
# if successful. If an error occurs raises {Bunny::ProtocolError}.
|
71
|
+
#
|
72
|
+
# @option opts [Boolean] :if_unused (false)
|
73
|
+
# If set to @true@, the server will only delete the exchange if it has no queue bindings. If the exchange has queue bindings the server does not delete it but raises a channel exception instead.
|
74
|
+
#
|
75
|
+
# @option opts [Boolean] :nowait (false)
|
76
|
+
# Ignored by Bunny, always @false@.
|
77
|
+
#
|
78
|
+
# @return [Symbol] @:delete_ok@ if successful.
|
94
79
|
def delete(opts = {})
|
95
80
|
# ignore the :nowait option if passed, otherwise program will hang waiting for a
|
96
81
|
# response that will not be sent by the server
|
97
82
|
opts.delete(:nowait)
|
98
83
|
|
99
|
-
|
100
|
-
|
101
|
-
)
|
84
|
+
opts = { :exchange => name, :nowait => false, :deprecated_ticket => 0 }.merge(opts)
|
85
|
+
|
86
|
+
client.send_frame(Qrack::Protocol09::Exchange::Delete.new(opts))
|
102
87
|
|
103
88
|
method = client.next_method
|
104
89
|
|
105
|
-
|
106
|
-
"Error deleting exchange #{name}")
|
90
|
+
client.check_response(method, Qrack::Protocol09::Exchange::DeleteOk, "Error deleting exchange #{name}")
|
107
91
|
|
108
92
|
client.exchanges.delete(name)
|
109
93
|
|
@@ -111,67 +95,65 @@ if successful. If an error occurs raises _Bunny_::_ProtocolError_.
|
|
111
95
|
:delete_ok
|
112
96
|
end
|
113
97
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
98
|
+
# Publishes a message to a specific exchange. The message will be routed to queues as defined
|
99
|
+
# by the exchange configuration and distributed to any active consumers when the transaction,
|
100
|
+
# if any, is committed.
|
101
|
+
#
|
102
|
+
# @option opts [String] :key
|
103
|
+
# Specifies the routing key for the message. The routing key is
|
104
|
+
# used for routing messages depending on the exchange configuration.
|
105
|
+
#
|
106
|
+
# @option opts [String] :content_type
|
107
|
+
# Specifies the content type for the message.
|
108
|
+
#
|
109
|
+
# @option opts [Boolean] :mandatory (false)
|
110
|
+
# Tells the server how to react if the message cannot be routed to a queue.
|
111
|
+
# If set to @true@, the server will return an unroutable message
|
112
|
+
# with a Return method. If this flag is zero, the server silently drops the message.
|
113
|
+
#
|
114
|
+
# @option opts [Boolean] :immediate (false)
|
115
|
+
# Tells the server how to react if the message cannot be routed to a queue consumer
|
116
|
+
# immediately. If set to @true@, the server will return an undeliverable message with
|
117
|
+
# a Return method. If set to @false@, the server will queue the message, but with no
|
118
|
+
# guarantee that it will ever be consumed.
|
119
|
+
#
|
120
|
+
# @option opts [Boolean] :persistent (false)
|
121
|
+
# Tells the server whether to persist the message. If set to @true@, the message will
|
122
|
+
# be persisted to disk and not lost if the server restarts. If set to @false@, the message
|
123
|
+
# will not be persisted across server restart. Setting to @true@ incurs a performance penalty
|
124
|
+
# as there is an extra cost associated with disk access.
|
125
|
+
#
|
126
|
+
# @return [NilClass] nil
|
144
127
|
def publish(data, opts = {})
|
145
128
|
opts = opts.dup
|
146
129
|
out = []
|
147
130
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
out << Qrack::Transport09::Body.new(data)
|
131
|
+
# Set up options
|
132
|
+
routing_key = opts.delete(:key) || key
|
133
|
+
mandatory = opts.delete(:mandatory)
|
134
|
+
immediate = opts.delete(:immediate)
|
135
|
+
delivery_mode = opts.delete(:persistent) ? 2 : 1
|
136
|
+
content_type = opts.delete(:content_type) || 'application/octet-stream'
|
137
|
+
|
138
|
+
out << Qrack::Protocol09::Basic::Publish.new({ :exchange => name,
|
139
|
+
:routing_key => routing_key,
|
140
|
+
:mandatory => mandatory,
|
141
|
+
:immediate => immediate,
|
142
|
+
:deprecated_ticket => 0 })
|
143
|
+
data = data.to_s
|
144
|
+
out << Qrack::Protocol09::Header.new(
|
145
|
+
Qrack::Protocol09::Basic,
|
146
|
+
data.bytesize, {
|
147
|
+
:content_type => content_type,
|
148
|
+
:delivery_mode => delivery_mode,
|
149
|
+
:priority => 0
|
150
|
+
}.merge(opts)
|
151
|
+
)
|
152
|
+
out << Qrack::Transport09::Body.new(data)
|
171
153
|
|
172
154
|
client.send_frame(*out)
|
173
155
|
end
|
174
156
|
|
175
157
|
end
|
176
|
-
|
158
|
+
|
177
159
|
end
|