google-cloud-pubsub 0.31.1 → 0.32.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.
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