amqp 0.6.7 → 0.7.0.pre

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,132 @@
1
+ # encoding: utf-8
2
+
3
+ class MQ
4
+ class Collection < ::Array
5
+ class IncompatibleItemError < ArgumentError
6
+ def initialize(item)
7
+ super("Instance of #{item.class} doesn't respond to #name!")
8
+ end
9
+ end
10
+
11
+ def [](name)
12
+ self.find do |object|
13
+ object.name == name
14
+ end
15
+ end
16
+
17
+ # Collection#[]= doesn't really make any sense, as we can't
18
+ # redefine already existing Queues and Exchanges (we can declare
19
+ # them multiple times, but if the queue resp. exchange is already
20
+ # in the collection, it doesn't make sense to do so and we can't
21
+ # run declare twice in order to change options, because the AMQP
22
+ # broker closes the connection if we try to do so).
23
+
24
+ # Use Collection#<< for adding items to the collection.
25
+ undef_method :[]=
26
+
27
+ def <<(item)
28
+ if (item.name rescue nil).nil? || ! self[item.name]
29
+ self.add!(item)
30
+ end
31
+
32
+ return item
33
+ end
34
+
35
+ alias_method :__push__, :push
36
+ alias_method :push, :<<
37
+
38
+ def add!(item)
39
+ unless item.respond_to?(:name)
40
+ raise IncompatibleItemError.new(item)
41
+ end
42
+
43
+ __push__(item)
44
+ return item
45
+ end
46
+ end
47
+ end
48
+
49
+ if $0 =~ /bacon/ or $0 == __FILE__
50
+ require "bacon"
51
+
52
+ Item = Struct.new(:name)
53
+
54
+ describe MQ::Collection do
55
+ before do
56
+ @items = 3.times.map { |int| Item.new("name-#{int}") }
57
+ @collection = MQ::Collection.new(@items)
58
+ end
59
+
60
+ describe "accessors" do
61
+ should "be accessible by its name" do
62
+ @collection["name-1"].should.not.be.nil
63
+ @collection["name-1"].should.eql(@items[1])
64
+ end
65
+
66
+ should "not allow to change already existing object" do
67
+ lambda { @collection["name-1"] = Item.new("test") }.should.raise(NoMethodError)
68
+ end
69
+ end
70
+
71
+ describe "#<<" do
72
+ should "raise IncompatibleItemError if the argument doesn't have method :name" do
73
+ lambda { @collection << nil }.should.raise(MQ::Collection::IncompatibleItemError)
74
+ end
75
+
76
+ should "add an item into the collection" do
77
+ length = @collection.length
78
+ @collection << Item.new("test")
79
+ @collection.length.should.eql(length + 1)
80
+ end
81
+
82
+ should "not add an item to the collection if another item with given name already exists and the name IS NOT nil" do
83
+ @collection << Item.new("test")
84
+ length = @collection.length
85
+ @collection << Item.new("test")
86
+ @collection.length.should.eql(length)
87
+ end
88
+
89
+ should "add an item to the collection if another item with given name already exists and the name IS nil" do
90
+ @collection << Item.new(nil)
91
+ length = @collection.length
92
+ @collection << Item.new(nil)
93
+ @collection.length.should.eql(length + 1)
94
+ end
95
+
96
+ should "return the item" do
97
+ item = Item.new("test")
98
+ (@collection << item).should.eql item
99
+ end
100
+
101
+ should "return the item even if it already existed" do
102
+ item = Item.new("test")
103
+ @collection << item
104
+ (@collection << item).should.eql item
105
+ end
106
+ end
107
+
108
+ describe "#add!" do
109
+ should "raise IncompatibleItemError if the argument doesn't have method :name" do
110
+ lambda { @collection << nil }.should.raise(MQ::Collection::IncompatibleItemError)
111
+ end
112
+
113
+ should "add an item into the collection" do
114
+ length = @collection.length
115
+ @collection << Item.new("test")
116
+ @collection.length.should.eql(length + 1)
117
+ end
118
+
119
+ should "add an item to the collection if another item with given name already exists" do
120
+ @collection.add! Item.new("test")
121
+ length = @collection.length
122
+ @collection.add! Item.new("test")
123
+ @collection.length.should.eql(length + 1)
124
+ end
125
+
126
+ should "return the item" do
127
+ item = Item.new("test")
128
+ (@collection << item).should.eql item
129
+ end
130
+ end
131
+ end
132
+ end
@@ -10,7 +10,7 @@ class MQ
10
10
  # There are three (3) supported Exchange types: direct, fanout and topic.
11
11
  #
12
12
  # As part of the standard, the server _must_ predeclare the direct exchange
13
- # 'amq.direct' and the fanout exchange 'amq.fanout' (all exchange names
13
+ # 'amq.direct' and the fanout exchange 'amq.fanout' (all exchange names
14
14
  # starting with 'amq.' are reserved). Attempts to declare an exchange using
15
15
  # 'amq.' as the name will raise an MQ:Error and fail. In practice these
16
16
  # default exchanges are never used directly by client code.
@@ -28,7 +28,7 @@ class MQ
28
28
  # There are three (3) supported Exchange types: direct, fanout and topic.
29
29
  #
30
30
  # As part of the standard, the server _must_ predeclare the direct exchange
31
- # 'amq.direct' and the fanout exchange 'amq.fanout' (all exchange names
31
+ # 'amq.direct' and the fanout exchange 'amq.fanout' (all exchange names
32
32
  # starting with 'amq.' are reserved). Attempts to declare an exchange using
33
33
  # 'amq.' as the name will raise an MQ:Error and fail. In practice these
34
34
  # default exchanges are never used directly by client code.
@@ -36,8 +36,8 @@ class MQ
36
36
  # == Direct
37
37
  # A direct exchange is useful for 1:1 communication between a publisher and
38
38
  # subscriber. Messages are routed to the queue with a binding that shares
39
- # the same name as the exchange. Alternately, the messages are routed to
40
- # the bound queue that shares the same name as the routing key used for
39
+ # the same name as the exchange. Alternately, the messages are routed to
40
+ # the bound queue that shares the same name as the routing key used for
41
41
  # defining the exchange. This exchange type does not honor the :key option
42
42
  # when defining a new instance with a name. It _will_ honor the :key option
43
43
  # if the exchange name is the empty string. This is because an exchange
@@ -57,14 +57,14 @@ class MQ
57
57
  # queue.pop { |data| puts "received data [#{data}]" }
58
58
  #
59
59
  # == Fanout
60
- # A fanout exchange is useful for 1:N communication where one publisher
61
- # feeds multiple subscribers. Like direct exchanges, messages published
62
- # to a fanout exchange are delivered to queues whose name matches the
63
- # exchange name (or are bound to that exchange name). Each queue gets
60
+ # A fanout exchange is useful for 1:N communication where one publisher
61
+ # feeds multiple subscribers. Like direct exchanges, messages published
62
+ # to a fanout exchange are delivered to queues whose name matches the
63
+ # exchange name (or are bound to that exchange name). Each queue gets
64
64
  # its own copy of the message.
65
65
  #
66
- # Like the direct exchange type, this exchange type does not honor the
67
- # :key option when defining a new instance with a name. It _will_ honor
66
+ # Like the direct exchange type, this exchange type does not honor the
67
+ # :key option when defining a new instance with a name. It _will_ honor
68
68
  # the :key option if the exchange name is the empty string. Fanout exchanges
69
69
  # defined with the empty string as the name use the default 'amq.fanout'.
70
70
  # In this case it needs to use :key to do its matching.
@@ -91,20 +91,20 @@ class MQ
91
91
  # end
92
92
  #
93
93
  # == Topic
94
- # A topic exchange allows for messages to be published to an exchange
94
+ # A topic exchange allows for messages to be published to an exchange
95
95
  # tagged with a specific routing key. The Exchange uses the routing key
96
- # to determine which queues to deliver the message. Wildcard matching
97
- # is allowed. The topic must be declared using dot notation to separate
96
+ # to determine which queues to deliver the message. Wildcard matching
97
+ # is allowed. The topic must be declared using dot notation to separate
98
98
  # each subtopic.
99
99
  #
100
100
  # This is the only exchange type to honor the :key parameter.
101
101
  #
102
- # As part of the AMQP standard, each server _should_ predeclare a topic
102
+ # As part of the AMQP standard, each server _should_ predeclare a topic
103
103
  # exchange called 'amq.topic' (this is not required by the standard).
104
104
  #
105
105
  # The classic example is delivering market data. When publishing market
106
- # data for stocks, we may subdivide the stream based on 2
107
- # characteristics: nation code and trading symbol. The topic tree for
106
+ # data for stocks, we may subdivide the stream based on 2
107
+ # characteristics: nation code and trading symbol. The topic tree for
108
108
  # Apple Computer would look like:
109
109
  # 'stock.us.aapl'
110
110
  # For a foreign stock, it may look like:
@@ -139,10 +139,10 @@ class MQ
139
139
  # end
140
140
  # end
141
141
  #
142
- # For matching, the '*' (asterisk) wildcard matches against one
143
- # dot-separated item only. The '#' wildcard (hash or pound symbol)
144
- # matches against 0 or more dot-separated items. If none of these
145
- # symbols are used, the exchange performs a comparison looking for an
142
+ # For matching, the '*' (asterisk) wildcard matches against one
143
+ # dot-separated item only. The '#' wildcard (hash or pound symbol)
144
+ # matches against 0 or more dot-separated items. If none of these
145
+ # symbols are used, the exchange performs a comparison looking for an
146
146
  # exact match.
147
147
  #
148
148
  # == Options
@@ -150,12 +150,12 @@ class MQ
150
150
  # If set, the server will not create the exchange if it does not
151
151
  # already exist. The client can use this to check whether an exchange
152
152
  # exists without modifying the server state.
153
- #
153
+ #
154
154
  # * :durable => true | false (default false)
155
155
  # If set when creating a new exchange, the exchange will be marked as
156
156
  # durable. Durable exchanges remain active when a server restarts.
157
157
  # Non-durable exchanges (transient exchanges) are purged if/when a
158
- # server restarts.
158
+ # server restarts.
159
159
  #
160
160
  # A transient exchange (the default) is stored in memory-only
161
161
  # therefore it is a good choice for high-performance and low-latency
@@ -183,26 +183,56 @@ class MQ
183
183
  # not wait for a reply method. If the server could not complete the
184
184
  # method it will raise a channel or connection exception.
185
185
  #
186
+ # * :no_declare => true | false (default false)
187
+ # If set, the exchange will not be declared to the
188
+ # AMQP broker at instantiation-time. This allows the AMQP
189
+ # client to send messages to exchanges that were
190
+ # already declared by someone else, e.g. if the client
191
+ # does not have sufficient privilege to declare (create)
192
+ # an exchange. Use with caution, as binding to an exchange
193
+ # with the no-declare option causes your system to become
194
+ # sensitive to the ordering of clients' actions!
195
+ #
186
196
  # == Exceptions
187
197
  # Doing any of these activities are illegal and will raise MQ:Error.
188
198
  # * redeclare an already-declared exchange to a different type
189
199
  # * :passive => true and the exchange does not exist (NOT_FOUND)
190
200
  #
191
- def initialize mq, type, name, opts = {}
201
+ def initialize mq, type, name, opts = {}, &block
192
202
  @mq = mq
193
- @type, @name, @opts = type, name, opts
194
- @mq.exchanges[@name = name] ||= self
203
+ @type, @opts = type, opts
204
+ @opts = { :exchange => name, :type => type, :nowait => block.nil? }.merge(opts)
195
205
  @key = opts[:key]
196
-
206
+ @name = name unless name.empty?
207
+ @status = :unknown
208
+
209
+ # The AMQP 0.8 specification (as well as 0.9.1) in 1.1.4.2 mentiones
210
+ # that Exchange.Declare-Ok confirms the name of the exchange (because
211
+ # of automatically­named), which is logical to interpret that this
212
+ # functionality should be the same as for Queue (though it isn't
213
+ # explicitely told in the specification). In fact, RabbitMQ (and
214
+ # probably other implementations as well) doesn't support it and
215
+ # there is a default exchange with an empty name (so-called default
216
+ # or nameless exchange), so if we'd send Exchange.Declare(exchange=""),
217
+ # then RabbitMQ interpret it as if we'd try to redefine this default
218
+ # exchange so it'd produce an error.
197
219
  unless name == "amq.#{type}" or name == '' or opts[:no_declare]
198
- @mq.callback{
199
- @mq.send Protocol::Exchange::Declare.new({ :exchange => name,
200
- :type => type,
201
- :nowait => true }.merge(opts))
220
+ @status = :unfinished
221
+ @mq.callback {
222
+ @mq.send Protocol::Exchange::Declare.new(@opts)
202
223
  }
224
+ else
225
+ # Call the callback immediately, as given exchange is already
226
+ # declared.
227
+ @status = :finished
228
+ block.call(self)
203
229
  end
230
+
231
+ self.callback = block
204
232
  end
205
- attr_reader :name, :type, :key
233
+
234
+ attr_reader :name, :type, :key, :status
235
+ attr_accessor :opts, :callback
206
236
 
207
237
  # This method publishes a staged file message to a specific exchange.
208
238
  # The file message will be routed to queues as defined by the exchange
@@ -212,7 +242,7 @@ class MQ
212
242
  # exchange = MQ.direct('name', :key => 'foo.bar')
213
243
  # exchange.publish("some data")
214
244
  #
215
- # The method takes several hash key options which modify the behavior or
245
+ # The method takes several hash key options which modify the behavior or
216
246
  # lifecycle of the message.
217
247
  #
218
248
  # * :routing_key => 'string'
@@ -236,7 +266,7 @@ class MQ
236
266
  # no guarantee that it will ever be consumed.
237
267
  #
238
268
  # * :persistent
239
- # True or False. When true, this message will remain in the queue until
269
+ # True or False. When true, this message will remain in the queue until
240
270
  # it is consumed (if the queue is durable). When false, the message is
241
271
  # lost if the server restarts and the queue is recreated.
242
272
  #
@@ -254,7 +284,7 @@ class MQ
254
284
  data = data.to_s
255
285
 
256
286
  out << Protocol::Header.new(Protocol::Basic,
257
- data.length, { :content_type => 'application/octet-stream',
287
+ data.bytesize, { :content_type => 'application/octet-stream',
258
288
  :delivery_mode => (opts[:persistent] ? 2 : 1),
259
289
  :priority => 0 }.merge(opts))
260
290
 
@@ -286,7 +316,7 @@ class MQ
286
316
  # If set, the server will only delete the exchange if it has no queue
287
317
  # bindings. If the exchange has queue bindings the server does not
288
318
  # delete it but raises a channel exception instead (MQ:Error).
289
- #
319
+ #
290
320
  def delete opts = {}
291
321
  @mq.callback{
292
322
  @mq.send Protocol::Exchange::Delete.new({ :exchange => name,
@@ -300,5 +330,9 @@ class MQ
300
330
  @deferred_status = nil
301
331
  initialize @mq, @type, @name, @opts
302
332
  end
333
+
334
+ def receive_response response
335
+ self.callback && self.callback.call(self)
336
+ end
303
337
  end
304
- end
338
+ end
@@ -1,7 +1,7 @@
1
1
  class MQ
2
2
  class Queue
3
3
  include AMQP
4
-
4
+
5
5
  # Queues store and forward messages. Queues can be configured in the server
6
6
  # or created at runtime. Queues must be attached to at least one exchange
7
7
  # in order to receive messages from publishers.
@@ -10,7 +10,7 @@ class MQ
10
10
  # internal use. Attempts to create queue names in violation of this
11
11
  # reservation will raise MQ:Error (ACCESS_REFUSED).
12
12
  #
13
- # When a queue is created without a name, the server will generate a
13
+ # When a queue is created without a name, the server will generate a
14
14
  # unique name internally (not currently supported in this library).
15
15
  #
16
16
  # == Options
@@ -18,7 +18,7 @@ class MQ
18
18
  # If set, the server will not create the exchange if it does not
19
19
  # already exist. The client can use this to check whether an exchange
20
20
  # exists without modifying the server state.
21
- #
21
+ #
22
22
  # * :durable => true | false (default false)
23
23
  # If set when creating a new queue, the queue will be marked as
24
24
  # durable. Durable queues remain active when a server restarts.
@@ -47,7 +47,7 @@ class MQ
47
47
  # If set, the queue is deleted when all consumers have finished
48
48
  # using it. Last consumer can be cancelled either explicitly or because
49
49
  # its channel is closed. If there was no consumer ever on the queue, it
50
- # won't be deleted.
50
+ # won't be deleted.
51
51
  #
52
52
  # The server waits for a short period of time before
53
53
  # determining the queue is unused to give time to the client code
@@ -61,17 +61,21 @@ class MQ
61
61
  # not wait for a reply method. If the server could not complete the
62
62
  # method it will raise a channel or connection exception.
63
63
  #
64
- def initialize mq, name, opts = {}
64
+ def initialize mq, name, opts = {}, &block
65
65
  @mq = mq
66
- @opts = opts
66
+ @opts = { :queue => name, :nowait => block.nil? }.merge(opts)
67
67
  @bindings ||= {}
68
- @mq.queues[@name = name] ||= self
69
- @mq.callback{
70
- @mq.send Protocol::Queue::Declare.new({ :queue => name,
71
- :nowait => true }.merge(opts))
68
+ @name = name unless name.empty?
69
+ @status = @opts[:nowait] ? :unknown : :unfinished
70
+ @mq.callback {
71
+ @mq.send Protocol::Queue::Declare.new(@opts)
72
72
  }
73
+
74
+ self.callback = block
73
75
  end
76
+
74
77
  attr_reader :name
78
+ attr_accessor :opts, :callback, :bind_callback
75
79
 
76
80
  # This method binds a queue to an exchange. Until a queue is
77
81
  # bound it will not receive any messages. In a classic messaging
@@ -106,7 +110,8 @@ class MQ
106
110
  # not wait for a reply method. If the server could not complete the
107
111
  # method it will raise a channel or connection exception.
108
112
  #
109
- def bind exchange, opts = {}
113
+ def bind exchange, opts = {}, &block
114
+ @status = :unbound
110
115
  exchange = exchange.respond_to?(:name) ? exchange.name : exchange
111
116
  @bindings[exchange] = opts
112
117
 
@@ -114,13 +119,14 @@ class MQ
114
119
  @mq.send Protocol::Queue::Bind.new({ :queue => name,
115
120
  :exchange => exchange,
116
121
  :routing_key => opts[:key],
117
- :nowait => true }.merge(opts))
122
+ :nowait => block.nil? }.merge(opts))
118
123
  }
124
+ self.bind_callback = block
119
125
  self
120
126
  end
121
127
 
122
128
  # Remove the binding between the queue and exchange. The queue will
123
- # not receive any more messages until it is bound to another
129
+ # not receive any more messages until it is bound to another
124
130
  # exchange.
125
131
  #
126
132
  # Due to the asynchronous nature of the protocol, it is possible for
@@ -197,7 +203,7 @@ class MQ
197
203
  # EM.add_periodic_timer(1) do
198
204
  # exchange.publish("random number #{rand(1000)}")
199
205
  # end
200
- #
206
+ #
201
207
  # # note that #bind is never called; it is implicit because
202
208
  # # the exchange and queue names match
203
209
  # queue = MQ.queue('foo queue')
@@ -215,9 +221,9 @@ class MQ
215
221
  # EM.add_periodic_timer(1) do
216
222
  # exchange.publish("random number #{rand(1000)}")
217
223
  # end
218
- #
224
+ #
219
225
  # queue = MQ.queue('foo queue')
220
- # queue.pop do |header, body|
226
+ # queue.pop do |header, body|
221
227
  # p header
222
228
  # puts "received payload [#{body}]"
223
229
  # end
@@ -268,7 +274,7 @@ class MQ
268
274
  # EM.add_periodic_timer(1) do
269
275
  # exchange.publish("random number #{rand(1000)}")
270
276
  # end
271
- #
277
+ #
272
278
  # queue = MQ.queue('foo queue')
273
279
  # queue.subscribe { |body| puts "received payload [#{body}]" }
274
280
  # end
@@ -282,11 +288,11 @@ class MQ
282
288
  # EM.add_periodic_timer(1) do
283
289
  # exchange.publish("random number #{rand(1000)}")
284
290
  # end
285
- #
291
+ #
286
292
  # # note that #bind is never called; it is implicit because
287
293
  # # the exchange and queue names match
288
294
  # queue = MQ.queue('foo queue')
289
- # queue.subscribe do |header, body|
295
+ # queue.subscribe do |header, body|
290
296
  # p header
291
297
  # puts "received payload [#{body}]"
292
298
  # end
@@ -340,11 +346,11 @@ class MQ
340
346
  # Those messages will be serviced by the last block used in a
341
347
  # #subscribe or #pop call.
342
348
  #
343
- # Additionally, if the queue was created with _autodelete_ set to
349
+ # Additionally, if the queue was created with _autodelete_ set to
344
350
  # true, the server will delete the queue after its wait period
345
351
  # has expired unless the queue is bound to an active exchange.
346
352
  #
347
- # The method accepts a block which will be executed when the
353
+ # The method accepts a block which will be executed when the
348
354
  # unsubscription request is acknowledged as complete by the server.
349
355
  #
350
356
  # * :nowait => true | false (default true)
@@ -363,21 +369,21 @@ class MQ
363
369
  def publish data, opts = {}
364
370
  exchange.publish(data, opts)
365
371
  end
366
-
372
+
367
373
  # Boolean check to see if the current queue has already been subscribed
368
- # to an exchange.
374
+ # to an exchange.
369
375
  #
370
376
  # Attempts to #subscribe multiple times to any exchange will raise an
371
- # Exception. Only a single block at a time can be associated with any
377
+ # Exception. Only a single block at a time can be associated with any
372
378
  # one queue for processing incoming messages.
373
379
  #
374
380
  def subscribed?
375
381
  !!@on_msg
376
382
  end
377
383
 
378
- # Passes the message to the block passed to pop or subscribe.
384
+ # Passes the message to the block passed to pop or subscribe.
379
385
  #
380
- # Performs an arity check on the block's parameters. If arity == 1,
386
+ # Performs an arity check on the block's parameters. If arity == 1,
381
387
  # pass only the message body. If arity != 1, pass the headers and
382
388
  # the body to the block.
383
389
  #
@@ -385,7 +391,7 @@ class MQ
385
391
  # the headers parameter. See #pop or #subscribe for a code example.
386
392
  #
387
393
  def receive headers, body
388
- headers = MQ::Header.new(@mq, headers)
394
+ headers = MQ::Header.new(@mq, headers) unless headers.nil?
389
395
 
390
396
  if cb = (@on_msg || @on_pop)
391
397
  cb.call *(cb.arity == 1 ? [body] : [headers, body])
@@ -399,6 +405,8 @@ class MQ
399
405
  # }
400
406
  #
401
407
  def status opts = {}, &blk
408
+ return @status if opts.empty? && blk.nil?
409
+
402
410
  @on_status = blk
403
411
  @mq.callback{
404
412
  @mq.send Protocol::Queue::Declare.new({ :queue => name,
@@ -408,6 +416,13 @@ class MQ
408
416
  end
409
417
 
410
418
  def receive_status declare_ok
419
+ @name = declare_ok.queue
420
+ @status = :finished
421
+
422
+ if self.callback
423
+ self.callback.call(self, declare_ok.message_count, declare_ok.consumer_count)
424
+ end
425
+
411
426
  if @on_status
412
427
  m, c = declare_ok.message_count, declare_ok.consumer_count
413
428
  @on_status.call *(@on_status.arity == 1 ? [m] : [m, c])
@@ -415,6 +430,13 @@ class MQ
415
430
  end
416
431
  end
417
432
 
433
+ def after_bind bind_ok
434
+ @status = :bound
435
+ if self.bind_callback
436
+ self.bind_callback.call(self)
437
+ end
438
+ end
439
+
418
440
  def confirm_subscribe
419
441
  @on_confirm_subscribe.call if @on_confirm_subscribe
420
442
  @on_confirm_subscribe = nil
@@ -424,6 +446,7 @@ class MQ
424
446
  @on_cancel.call if @on_cancel
425
447
  @on_cancel = @on_msg = nil
426
448
  @mq.consumers.delete @consumer_tag
449
+ @mq.queues.delete(@name) if @opts[:auto_delete]
427
450
  @consumer_tag = nil
428
451
  end
429
452
 
@@ -444,11 +467,11 @@ class MQ
444
467
  pop @on_pop_opts, &@on_pop
445
468
  end
446
469
  end
447
-
470
+
448
471
  private
449
-
472
+
450
473
  def exchange
451
474
  @exchange ||= Exchange.new(@mq, :direct, '', :key => name)
452
475
  end
453
476
  end
454
- end
477
+ end