google-cloud-pubsub 0.31.1 → 0.32.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -0
  3. data/README.md +8 -8
  4. data/lib/google/cloud/pubsub/async_publisher.rb +4 -0
  5. data/lib/google/cloud/pubsub/credentials.rb +2 -14
  6. data/lib/google/cloud/pubsub/subscriber.rb +85 -0
  7. data/lib/google/cloud/pubsub/subscriber/async_stream_pusher.rb +24 -19
  8. data/lib/google/cloud/pubsub/subscriber/async_unary_pusher.rb +45 -26
  9. data/lib/google/cloud/pubsub/subscriber/inventory.rb +136 -0
  10. data/lib/google/cloud/pubsub/subscriber/stream.rb +80 -138
  11. data/lib/google/cloud/pubsub/subscription.rb +2 -2
  12. data/lib/google/cloud/pubsub/topic.rb +4 -4
  13. data/lib/google/cloud/pubsub/v1/credentials.rb +38 -0
  14. data/lib/google/cloud/pubsub/v1/doc/google/iam/v1/iam_policy.rb +62 -0
  15. data/lib/google/cloud/pubsub/v1/doc/google/iam/v1/policy.rb +127 -0
  16. data/lib/google/cloud/pubsub/v1/doc/google/protobuf/duration.rb +1 -1
  17. data/lib/google/cloud/pubsub/v1/doc/google/protobuf/empty.rb +28 -0
  18. data/lib/google/cloud/pubsub/v1/doc/google/protobuf/field_mask.rb +1 -1
  19. data/lib/google/cloud/pubsub/v1/doc/google/protobuf/timestamp.rb +1 -1
  20. data/lib/google/cloud/pubsub/v1/doc/google/pubsub/v1/pubsub.rb +113 -31
  21. data/lib/google/cloud/pubsub/v1/publisher_client.rb +180 -109
  22. data/lib/google/cloud/pubsub/v1/subscriber_client.rb +322 -193
  23. data/lib/google/cloud/pubsub/v1/subscriber_client_config.json +1 -1
  24. data/lib/google/cloud/pubsub/version.rb +1 -1
  25. data/lib/google/pubsub/v1/pubsub_pb.rb +21 -0
  26. data/lib/google/pubsub/v1/pubsub_services_pb.rb +87 -73
  27. metadata +23 -5
  28. data/lib/google/cloud/pubsub/v1/doc/overview.rb +0 -75
@@ -15,6 +15,7 @@
15
15
 
16
16
  require "google/cloud/pubsub/subscriber/async_unary_pusher"
17
17
  require "google/cloud/pubsub/subscriber/enumerator_queue"
18
+ require "google/cloud/pubsub/subscriber/inventory"
18
19
  require "google/cloud/pubsub/service"
19
20
  require "google/cloud/errors"
20
21
  require "monitor"
@@ -67,7 +68,9 @@ module Google
67
68
 
68
69
  def start
69
70
  synchronize do
70
- break if @request_queue
71
+ break if @background_thread
72
+
73
+ @inventory.start
71
74
 
72
75
  start_streaming!
73
76
  end
@@ -79,12 +82,26 @@ module Google
79
82
  synchronize do
80
83
  break if @stopped
81
84
 
85
+ # Close the stream by pushing the sentinel value.
86
+ # The unary pusher does not use the stream, so it can close here.
87
+ @request_queue.push self unless @request_queue.nil?
88
+
89
+ # Signal to the background thread that we are stopped.
82
90
  @stopped = true
91
+ @pause_cond.broadcast
92
+
93
+ # Now that the reception thread is stopped, immediately stop the
94
+ # callback thread pool and purge all pending callbacks.
95
+ @callback_thread_pool.kill
83
96
 
97
+ # Once all the callbacks are stopped, we can stop the inventory.
84
98
  @inventory.stop
85
99
 
86
- # signal to the background thread that we are unpaused
87
- @pause_cond.broadcast
100
+ # Stop the publisher and send the final batch of changes.
101
+ @async_pusher.stop if @async_pusher # will push current batch
102
+
103
+ # Stop the push thread pool now that the pusher is stopped.
104
+ @push_thread_pool.shutdown
88
105
  end
89
106
 
90
107
  self
@@ -100,21 +117,10 @@ module Google
100
117
 
101
118
  def wait!
102
119
  synchronize do
103
- # Now that the reception thread is dead, make sure all recieved
104
- # messages have had the callback called.
105
- @callback_thread_pool.shutdown
106
- @callback_thread_pool.wait_for_termination
107
-
108
- # Once all the callbacks are complete, we can stop the publisher
109
- # and send the final request to the steeam.
110
- @async_pusher.stop if @async_pusher # will push current batch
111
-
112
- # Close the push thread pool now that the pusher is closed.
113
- @push_thread_pool.shutdown
114
- @push_thread_pool.wait_for_termination
115
-
116
- # Close the stream now that all requests have been made.
117
- @request_queue.push self unless @request_queue.nil?
120
+ # # Wait for the push thread pool to finish pushing all remaining
121
+ # changes. Do not wait indefinitely.
122
+ @push_thread_pool.wait_for_termination 60
123
+ @push_thread_pool.kill if @push_thread_pool.shuttingdown?
118
124
  end
119
125
 
120
126
  self
@@ -127,7 +133,7 @@ module Google
127
133
  return true if ack_ids.empty?
128
134
 
129
135
  synchronize do
130
- @async_pusher ||= AsyncUnaryPusher.new self
136
+ @async_pusher ||= AsyncUnaryPusher.new(self).start
131
137
  @async_pusher.acknowledge ack_ids
132
138
  @inventory.remove ack_ids
133
139
  unpause_streaming!
@@ -143,7 +149,7 @@ module Google
143
149
  return true if mod_ack_ids.empty?
144
150
 
145
151
  synchronize do
146
- @async_pusher ||= AsyncUnaryPusher.new self
152
+ @async_pusher ||= AsyncUnaryPusher.new(self).start
147
153
  @async_pusher.delay deadline, mod_ack_ids
148
154
  @inventory.remove mod_ack_ids
149
155
  unpause_streaming!
@@ -170,7 +176,7 @@ module Google
170
176
  synchronize do
171
177
  return true if @inventory.empty?
172
178
 
173
- @async_pusher ||= AsyncUnaryPusher.new self
179
+ @async_pusher ||= AsyncUnaryPusher.new(self).start
174
180
  @async_pusher.delay subscriber.deadline, @inventory.ack_ids
175
181
  end
176
182
 
@@ -190,9 +196,28 @@ module Google
190
196
 
191
197
  protected
192
198
 
199
+ # @private
200
+ class RestartStream < StandardError; end
201
+
193
202
  # rubocop:disable all
194
203
 
195
- def background_run enum
204
+ def background_run
205
+ # Don't allow a stream to restart if already stopped
206
+ return if @stopped
207
+
208
+ # signal to the previous queue to shut down
209
+ old_queue = []
210
+ old_queue = @request_queue.quit_and_dump_queue if @request_queue
211
+
212
+ # Always create a new request queue and enum
213
+ @request_queue = EnumeratorQueue.new self
214
+ @request_queue.push initial_input_request
215
+ old_queue.each { |obj| @request_queue.push obj }
216
+ enum = subscriber.service.streaming_pull @request_queue.each
217
+
218
+ @stopped = nil
219
+ @paused = nil
220
+
196
221
  loop do
197
222
  synchronize do
198
223
  if @paused && !@stopped
@@ -213,7 +238,7 @@ module Google
213
238
 
214
239
  synchronize do
215
240
  # Create receipt of received messages reception
216
- @async_pusher ||= AsyncUnaryPusher.new self
241
+ @async_pusher ||= AsyncUnaryPusher.new(self).start
217
242
  @async_pusher.delay subscriber.deadline, received_ack_ids
218
243
 
219
244
  # Add received messages to inventory
@@ -232,57 +257,56 @@ module Google
232
257
  break
233
258
  end
234
259
  end
260
+
235
261
  # Has the loop broken but we aren't stopped?
236
262
  # Could be GRPC has thrown an internal error, so restart.
237
- synchronize { raise "restart thread" unless @stopped }
238
- rescue GRPC::DeadlineExceeded, GRPC::Unavailable, GRPC::Cancelled,
239
- GRPC::ResourceExhausted, GRPC::Internal, GRPC::Core::CallError
240
- # The GAPIC layer will raise DeadlineExceeded when stream is opened
241
- # longer than the timeout value it is configured for. When this
242
- # happends, restart the stream stealthly.
243
- # Also stealthly restart the stream on Unavailable, Cancelled,
244
- # ResourceExhausted, and Internal.
245
- # Also, also stealthly restart the stream when GRPC raises the
246
- # internal CallError.
247
- synchronize { start_streaming! }
263
+ raise RestartStream unless synchronize { @stopped }
264
+
265
+ # We must be stopped, tell the stream to quit.
266
+ @request_queue.push self
267
+ rescue GRPC::Cancelled, GRPC::DeadlineExceeded, GRPC::Internal,
268
+ GRPC::ResourceExhausted, GRPC::Unauthenticated,
269
+ GRPC::Unavailable, GRPC::Core::CallError
270
+ # Restart the stream with an incremental back for a retriable error.
271
+ # Also when GRPC raises the internal CallError.
272
+
273
+ retry
274
+ rescue RestartStream
275
+ retry
248
276
  rescue StandardError => e
249
277
  synchronize do
250
- if @stopped
251
- raise Google::Cloud::Error.from_error(e)
252
- else
253
- start_streaming!
254
- end
278
+ subscriber.error! e
279
+ start_streaming! unless @stopped
255
280
  end
281
+
282
+ retry
256
283
  end
257
284
 
258
285
  # rubocop:enable all
259
286
 
260
287
  def perform_callback_async rec_msg
288
+ return unless callback_thread_pool.running?
289
+
261
290
  Concurrent::Future.new(executor: callback_thread_pool) do
262
- subscriber.callback.call rec_msg
291
+ begin
292
+ subscriber.callback.call rec_msg
293
+ rescue StandardError => callback_error
294
+ subscriber.error! callback_error
295
+ end
263
296
  end.execute
264
297
  end
265
298
 
266
299
  def start_streaming!
267
- # Don't allow a stream to restart if already stopped
268
- return if @stopped
269
-
270
- # signal to the previous queue to shut down
271
- old_queue = []
272
- old_queue = @request_queue.quit_and_dump_queue if @request_queue
273
-
274
- @request_queue = EnumeratorQueue.new self
275
- @request_queue.push initial_input_request
276
- old_queue.each { |obj| @request_queue.push obj }
277
- output_enum = subscriber.service.streaming_pull @request_queue.each
300
+ # A Stream will only ever have one background thread. If the thread
301
+ # dies because it was stopped, or because of an unhandled error that
302
+ # could not be recovered from, so be it.
303
+ return if @background_thread
278
304
 
279
- @stopped = nil
280
- @paused = nil
305
+ @stopped = false
306
+ @paused = false
281
307
 
282
308
  # create new background thread to handle new enumerator
283
- @background_thread = Thread.new(output_enum) do |enum|
284
- background_run enum
285
- end
309
+ @background_thread = Thread.new { background_run }
286
310
  end
287
311
 
288
312
  def pause_streaming!
@@ -340,88 +364,6 @@ module Google
340
364
  return "stopped" if status == false
341
365
  status
342
366
  end
343
-
344
- ##
345
- # @private
346
- class Inventory
347
- include MonitorMixin
348
-
349
- attr_reader :stream, :limit
350
-
351
- def initialize stream, limit
352
- @stream = stream
353
- @limit = limit
354
- @_ack_ids = []
355
- @wait_cond = new_cond
356
-
357
- super()
358
- end
359
-
360
- def ack_ids
361
- @_ack_ids
362
- end
363
-
364
- def add *ack_ids
365
- ack_ids = Array(ack_ids).flatten
366
- synchronize do
367
- @_ack_ids += ack_ids
368
- unless @stopped
369
- @background_thread ||= Thread.new { background_run }
370
- end
371
- end
372
- end
373
-
374
- def remove *ack_ids
375
- ack_ids = Array(ack_ids).flatten
376
- synchronize do
377
- @_ack_ids -= ack_ids
378
- if @_ack_ids.empty?
379
- if @background_thread
380
- @background_thread.kill
381
- @background_thread = nil
382
- end
383
- end
384
- end
385
- end
386
-
387
- def count
388
- synchronize do
389
- @_ack_ids.count
390
- end
391
- end
392
-
393
- def empty?
394
- synchronize do
395
- @_ack_ids.empty?
396
- end
397
- end
398
-
399
- def stop
400
- synchronize do
401
- @stopped = true
402
- @background_thread.kill if @background_thread
403
- end
404
- end
405
-
406
- def full?
407
- count >= limit
408
- end
409
-
410
- protected
411
-
412
- def background_run
413
- until synchronize { @stopped }
414
- delay = calc_delay
415
- synchronize { @wait_cond.wait delay }
416
-
417
- stream.delay_inventory!
418
- end
419
- end
420
-
421
- def calc_delay
422
- (stream.subscriber.deadline - 3) * rand(0.8..0.9)
423
- end
424
- end
425
367
  end
426
368
  end
427
369
  end
@@ -108,8 +108,8 @@ module Google
108
108
  ##
109
109
  # Indicates whether to retain acknowledged messages. If `true`, then
110
110
  # messages are not expunged from the subscription's backlog, even if
111
- # they are acknowledged, until they fall out of the
112
- # {#retention_duration} window. Default is `false`.
111
+ # they are acknowledged, until they fall out of the {#retention} window.
112
+ # Default is `false`.
113
113
  #
114
114
  # @return [Boolean] Returns `true` if acknowledged messages are
115
115
  # retained.
@@ -121,14 +121,14 @@ module Google
121
121
  # @param [Boolean] retain_acked Indicates whether to retain acknowledged
122
122
  # messages. If `true`, then messages are not expunged from the
123
123
  # subscription's backlog, even if they are acknowledged, until they
124
- # fall out of the `retention_duration` window. Default is `false`.
124
+ # fall out of the `retention` window. Default is `false`.
125
125
  # @param [Numeric] retention How long to retain unacknowledged messages
126
126
  # in the subscription's backlog, from the moment a message is
127
127
  # published. If `retain_acked` is `true`, then this also configures
128
128
  # the retention of acknowledged messages, and thus configures how far
129
- # back in time a {#seek} can be done. Cannot be more than 604,800
130
- # seconds (7 days) or less than 600 seconds (10 minutes). Default is
131
- # 604,800 seconds (7 days).
129
+ # back in time a {Subscription#seek} can be done. Cannot be more than
130
+ # 604,800 seconds (7 days) or less than 600 seconds (10 minutes).
131
+ # Default is 604,800 seconds (7 days).
132
132
  # @param [String] endpoint A URL locating the endpoint to which messages
133
133
  # should be pushed.
134
134
  #
@@ -0,0 +1,38 @@
1
+ # Copyright 2018 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require "googleauth"
16
+
17
+ module Google
18
+ module Cloud
19
+ module Pubsub
20
+ module V1
21
+ class Credentials < Google::Auth::Credentials
22
+ SCOPE = ["https://www.googleapis.com/auth/pubsub"].freeze
23
+ PATH_ENV_VARS = %w(PUBSUB_CREDENTIALS
24
+ PUBSUB_KEYFILE
25
+ GOOGLE_CLOUD_CREDENTIALS
26
+ GOOGLE_CLOUD_KEYFILE
27
+ GCLOUD_KEYFILE)
28
+ JSON_ENV_VARS = %w(PUBSUB_CREDENTIALS_JSON
29
+ PUBSUB_KEYFILE_JSON
30
+ GOOGLE_CLOUD_CREDENTIALS_JSON
31
+ GOOGLE_CLOUD_KEYFILE_JSON
32
+ GCLOUD_KEYFILE_JSON)
33
+ DEFAULT_PATHS = ["~/.config/gcloud/application_default_credentials.json"]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,62 @@
1
+ # Copyright 2018 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Google
16
+ module Iam
17
+ module V1
18
+ # Request message for +SetIamPolicy+ method.
19
+ # @!attribute [rw] resource
20
+ # @return [String]
21
+ # REQUIRED: The resource for which the policy is being specified.
22
+ # +resource+ is usually specified as a path. For example, a Project
23
+ # resource is specified as +projects/{project}+.
24
+ # @!attribute [rw] policy
25
+ # @return [Google::Iam::V1::Policy]
26
+ # REQUIRED: The complete policy to be applied to the +resource+. The size of
27
+ # the policy is limited to a few 10s of KB. An empty policy is a
28
+ # valid policy but certain Cloud Platform services (such as Projects)
29
+ # might reject them.
30
+ class SetIamPolicyRequest; end
31
+
32
+ # Request message for +GetIamPolicy+ method.
33
+ # @!attribute [rw] resource
34
+ # @return [String]
35
+ # REQUIRED: The resource for which the policy is being requested.
36
+ # +resource+ is usually specified as a path. For example, a Project
37
+ # resource is specified as +projects/{project}+.
38
+ class GetIamPolicyRequest; end
39
+
40
+ # Request message for +TestIamPermissions+ method.
41
+ # @!attribute [rw] resource
42
+ # @return [String]
43
+ # REQUIRED: The resource for which the policy detail is being requested.
44
+ # +resource+ is usually specified as a path. For example, a Project
45
+ # resource is specified as +projects/{project}+.
46
+ # @!attribute [rw] permissions
47
+ # @return [Array<String>]
48
+ # The set of permissions to check for the +resource+. Permissions with
49
+ # wildcards (such as '*' or 'storage.*') are not allowed. For more
50
+ # information see
51
+ # [IAM Overview](https://cloud.google.com/iam/docs/overview#permissions).
52
+ class TestIamPermissionsRequest; end
53
+
54
+ # Response message for +TestIamPermissions+ method.
55
+ # @!attribute [rw] permissions
56
+ # @return [Array<String>]
57
+ # A subset of +TestPermissionsRequest.permissions+ that the caller is
58
+ # allowed.
59
+ class TestIamPermissionsResponse; end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,127 @@
1
+ # Copyright 2018 Google LLC
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # https://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ module Google
16
+ module Iam
17
+ module V1
18
+ # Defines an Identity and Access Management (IAM) policy. It is used to
19
+ # specify access control policies for Cloud Platform resources.
20
+ #
21
+ #
22
+ # A +Policy+ consists of a list of +bindings+. A +Binding+ binds a list of
23
+ # +members+ to a +role+, where the members can be user accounts, Google groups,
24
+ # Google domains, and service accounts. A +role+ is a named list of permissions
25
+ # defined by IAM.
26
+ #
27
+ # **Example**
28
+ #
29
+ # {
30
+ # "bindings": [
31
+ # {
32
+ # "role": "roles/owner",
33
+ # "members": [
34
+ # "user:mike@example.com",
35
+ # "group:admins@example.com",
36
+ # "domain:google.com",
37
+ # "serviceAccount:my-other-app@appspot.gserviceaccount.com",
38
+ # ]
39
+ # },
40
+ # {
41
+ # "role": "roles/viewer",
42
+ # "members": ["user:sean@example.com"]
43
+ # }
44
+ # ]
45
+ # }
46
+ #
47
+ # For a description of IAM and its features, see the
48
+ # [IAM developer's guide](https://cloud.google.com/iam).
49
+ # @!attribute [rw] version
50
+ # @return [Integer]
51
+ # Version of the +Policy+. The default version is 0.
52
+ # @!attribute [rw] bindings
53
+ # @return [Array<Google::Iam::V1::Binding>]
54
+ # Associates a list of +members+ to a +role+.
55
+ # Multiple +bindings+ must not be specified for the same +role+.
56
+ # +bindings+ with no members will result in an error.
57
+ # @!attribute [rw] etag
58
+ # @return [String]
59
+ # +etag+ is used for optimistic concurrency control as a way to help
60
+ # prevent simultaneous updates of a policy from overwriting each other.
61
+ # It is strongly suggested that systems make use of the +etag+ in the
62
+ # read-modify-write cycle to perform policy updates in order to avoid race
63
+ # conditions: An +etag+ is returned in the response to +getIamPolicy+, and
64
+ # systems are expected to put that etag in the request to +setIamPolicy+ to
65
+ # ensure that their change will be applied to the same version of the policy.
66
+ #
67
+ # If no +etag+ is provided in the call to +setIamPolicy+, then the existing
68
+ # policy is overwritten blindly.
69
+ class Policy; end
70
+
71
+ # Associates +members+ with a +role+.
72
+ # @!attribute [rw] role
73
+ # @return [String]
74
+ # Role that is assigned to +members+.
75
+ # For example, +roles/viewer+, +roles/editor+, or +roles/owner+.
76
+ # Required
77
+ # @!attribute [rw] members
78
+ # @return [Array<String>]
79
+ # Specifies the identities requesting access for a Cloud Platform resource.
80
+ # +members+ can have the following values:
81
+ #
82
+ # * +allUsers+: A special identifier that represents anyone who is
83
+ # on the internet; with or without a Google account.
84
+ #
85
+ # * +allAuthenticatedUsers+: A special identifier that represents anyone
86
+ # who is authenticated with a Google account or a service account.
87
+ #
88
+ # * +user:{emailid}+: An email address that represents a specific Google
89
+ # account. For example, +alice@gmail.com+ or +joe@example.com+.
90
+ #
91
+ #
92
+ # * +serviceAccount:{emailid}+: An email address that represents a service
93
+ # account. For example, +my-other-app@appspot.gserviceaccount.com+.
94
+ #
95
+ # * +group:{emailid}+: An email address that represents a Google group.
96
+ # For example, +admins@example.com+.
97
+ #
98
+ # * +domain:{domain}+: A Google Apps domain name that represents all the
99
+ # users of that domain. For example, +google.com+ or +example.com+.
100
+ class Binding; end
101
+
102
+ # The difference delta between two policies.
103
+ # @!attribute [rw] binding_deltas
104
+ # @return [Array<Google::Iam::V1::BindingDelta>]
105
+ # The delta for Bindings between two policies.
106
+ class PolicyDelta; end
107
+
108
+ # One delta entry for Binding. Each individual change (only one member in each
109
+ # entry) to a binding will be a separate entry.
110
+ # @!attribute [rw] action
111
+ # @return [Google::Iam::V1::BindingDelta::Action]
112
+ # The action that was performed on a Binding.
113
+ # Required
114
+ # @!attribute [rw] role
115
+ # @return [String]
116
+ # Role that is assigned to +members+.
117
+ # For example, +roles/viewer+, +roles/editor+, or +roles/owner+.
118
+ # Required
119
+ # @!attribute [rw] member
120
+ # @return [String]
121
+ # A single identity requesting access for a Cloud Platform resource.
122
+ # Follows the same format of Binding.members.
123
+ # Required
124
+ class BindingDelta; end
125
+ end
126
+ end
127
+ end