gcloud 0.1.1 → 0.2.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.
@@ -0,0 +1,605 @@
1
+ #--
2
+ # Copyright 2015 Google Inc. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ require "gcloud/pubsub/errors"
17
+ require "gcloud/pubsub/subscription/list"
18
+ require "gcloud/pubsub/received_message"
19
+
20
+ module Gcloud
21
+ module Pubsub
22
+ ##
23
+ # = Subscription
24
+ #
25
+ # A named resource representing the stream of messages from a single,
26
+ # specific topic, to be delivered to the subscribing application.
27
+ #
28
+ # require "gcloud"
29
+ #
30
+ # gcloud = Gcloud.new
31
+ # pubsub = gcloud.pubsub
32
+ #
33
+ # sub = pubsub.subscription "my-topic-sub"
34
+ # msgs = sub.pull
35
+ # msgs.each { |msg| msg.acknowledge! }
36
+ #
37
+ class Subscription
38
+ ##
39
+ # The Connection object.
40
+ attr_accessor :connection #:nodoc:
41
+
42
+ ##
43
+ # The Google API Client object.
44
+ attr_accessor :gapi #:nodoc:
45
+
46
+ ##
47
+ # Create an empty Subscription object.
48
+ def initialize #:nodoc:
49
+ @connection = nil
50
+ @gapi = {}
51
+ @name = nil
52
+ @exists = nil
53
+ end
54
+
55
+ ##
56
+ # New lazy Topic object without making an HTTP request.
57
+ def self.new_lazy name, conn, options = {} #:nodoc:
58
+ sub = new.tap do |f|
59
+ f.gapi = nil
60
+ f.connection = conn
61
+ end
62
+ sub.instance_eval do
63
+ @name = conn.subscription_path(name, options)
64
+ end
65
+ sub
66
+ end
67
+
68
+ ##
69
+ # The name of the subscription.
70
+ def name
71
+ @gapi ? @gapi["name"] : @name
72
+ end
73
+
74
+ ##
75
+ # The Topic from which this subscription receives messages.
76
+ #
77
+ # === Returns
78
+ #
79
+ # Topic
80
+ #
81
+ # === Example
82
+ #
83
+ # require "gcloud"
84
+ #
85
+ # gcloud = Gcloud.new
86
+ # pubsub = gcloud.pubsub
87
+ #
88
+ # sub = pubsub.subscription "my-topic-sub"
89
+ # sub.topic.name #=> "projects/my-project/topics/my-topic"
90
+ #
91
+ def topic
92
+ ensure_gapi!
93
+ # Always disable autocreate, we don't want to recreate a topic that
94
+ # was intentionally deleted.
95
+ Topic.new_lazy @gapi["topic"], connection, autocreate: false
96
+ end
97
+
98
+ ##
99
+ # This value is the maximum number of seconds after a subscriber receives
100
+ # a message before the subscriber should acknowledge the message.
101
+ def deadline
102
+ ensure_gapi!
103
+ @gapi["ackDeadlineSeconds"]
104
+ end
105
+
106
+ ##
107
+ # Returns the URL locating the endpoint to which messages should be
108
+ # pushed.
109
+ def endpoint
110
+ ensure_gapi!
111
+ @gapi["pushConfig"]["pushEndpoint"] if @gapi["pushConfig"]
112
+ end
113
+
114
+ ##
115
+ # Sets the URL locating the endpoint to which messages should be pushed.
116
+ def endpoint= new_endpoint
117
+ ensure_connection!
118
+ resp = connection.modify_push_config name, new_endpoint, {}
119
+ if resp.success?
120
+ @gapi["pushConfig"]["pushEndpoint"] = new_endpoint if @gapi
121
+ else
122
+ fail ApiError.from_response(resp)
123
+ end
124
+ end
125
+
126
+ ##
127
+ # Determines whether the subscription exists in the Pub/Sub service.
128
+ #
129
+ # === Example
130
+ #
131
+ # require "gcloud"
132
+ #
133
+ # gcloud = Gcloud.new
134
+ # pubsub = gcloud.pubsub
135
+ #
136
+ # sub = pubsub.subscription "my-topic-sub"
137
+ # sub.exists? #=> true
138
+ #
139
+ def exists?
140
+ # Always true if we have a gapi object
141
+ return true unless @gapi.nil?
142
+ # If we have a value, return it
143
+ return @exists unless @exists.nil?
144
+ ensure_gapi!
145
+ @exists = !@gapi.nil?
146
+ rescue NotFoundError
147
+ @exists = false
148
+ end
149
+
150
+ ##
151
+ # Determines whether the subscription object was created with an
152
+ # HTTP call.
153
+ #
154
+ # === Example
155
+ #
156
+ # require "gcloud"
157
+ #
158
+ # gcloud = Gcloud.new
159
+ # pubsub = gcloud.pubsub
160
+ #
161
+ # sub = pubsub.get_subscription "my-topic-sub"
162
+ # sub.lazy? #=> false
163
+ #
164
+ def lazy? #:nodoc:
165
+ @gapi.nil?
166
+ end
167
+
168
+ ##
169
+ # Deletes an existing subscription.
170
+ # All pending messages in the subscription are immediately dropped.
171
+ #
172
+ # === Returns
173
+ #
174
+ # +true+ if the subscription was deleted.
175
+ #
176
+ # === Example
177
+ #
178
+ # require "gcloud"
179
+ #
180
+ # gcloud = Gcloud.new
181
+ # pubsub = gcloud.pubsub
182
+ #
183
+ # sub = pubsub.subscription "my-topic-sub"
184
+ # sub.delete
185
+ #
186
+ def delete
187
+ ensure_connection!
188
+ resp = connection.delete_subscription name
189
+ if resp.success?
190
+ true
191
+ else
192
+ fail ApiError.from_response(resp)
193
+ end
194
+ end
195
+
196
+ # rubocop:disable Metrics/MethodLength
197
+ # Disabled rubocop because these lines are needed.
198
+
199
+ ##
200
+ # Pulls messages from the server. Returns an empty list if there are no
201
+ # messages available in the backlog. Raises an ApiError with status
202
+ # +UNAVAILABLE+ if there are too many concurrent pull requests pending
203
+ # for the given subscription.
204
+ #
205
+ # === Parameters
206
+ #
207
+ # +options+::
208
+ # An optional Hash for controlling additional behavior. (+Hash+)
209
+ # <code>options[:immediate]</code>::
210
+ # When +true+ the system will respond immediately even if it is not able
211
+ # to return messages. When +false+ the system is allowed to wait until
212
+ # it can return least one message. No messages are returned when a
213
+ # request times out. The default value is +true+. (+Boolean+)
214
+ # <code>options[:max]</code>::
215
+ # The maximum number of messages to return for this request. The Pub/Sub
216
+ # system may return fewer than the number specified. The default value
217
+ # is +100+, the maximum value is +1000+. (+Integer+)
218
+ # <code>options[:autoack]</code>::
219
+ # Automatically acknowledge the message as it is pulled. The default
220
+ # value is +false+. (+Boolean+)
221
+ #
222
+ # === Returns
223
+ #
224
+ # Array of Gcloud::Pubsub::ReceivedMessage
225
+ #
226
+ # === Examples
227
+ #
228
+ # require "gcloud"
229
+ #
230
+ # gcloud = Gcloud.new
231
+ # pubsub = gcloud.pubsub
232
+ #
233
+ # sub = pubsub.subscription "my-topic-sub"
234
+ # sub.pull.each { |msg| msg.acknowledge! }
235
+ #
236
+ # A maximum number of messages returned can also be specified:
237
+ #
238
+ # require "gcloud"
239
+ #
240
+ # gcloud = Gcloud.new
241
+ # pubsub = gcloud.pubsub
242
+ #
243
+ # sub = pubsub.subscription "my-topic-sub", max: 10
244
+ # sub.pull.each { |msg| msg.acknowledge! }
245
+ #
246
+ # The call can block until messages are available by setting the
247
+ # +:immediate+ option to +false+:
248
+ #
249
+ # require "gcloud"
250
+ #
251
+ # gcloud = Gcloud.new
252
+ # pubsub = gcloud.pubsub
253
+ #
254
+ # sub = pubsub.subscription "my-topic-sub"
255
+ # msgs = sub.pull immediate: false
256
+ # msgs.each { |msg| msg.acknowledge! }
257
+ #
258
+ def pull options = {}
259
+ ensure_connection!
260
+ resp = connection.pull name, options
261
+ if resp.success?
262
+ messages = Array(resp.data["receivedMessages"]).map do |gapi|
263
+ ReceivedMessage.from_gapi gapi, self
264
+ end
265
+ acknowledge messages if options[:autoack]
266
+ messages
267
+ else
268
+ fail ApiError.from_response(resp)
269
+ end
270
+ rescue Faraday::TimeoutError
271
+ []
272
+ end
273
+
274
+ # rubocop:enable Metrics/MethodLength
275
+
276
+ ##
277
+ # Pulls from the server while waiting for messages to become available.
278
+ # This is the same as:
279
+ #
280
+ # subscription.pull immediate: false
281
+ #
282
+ # === Parameters
283
+ #
284
+ # +options+::
285
+ # An optional Hash for controlling additional behavior. (+Hash+)
286
+ # <code>options[:max]</code>::
287
+ # The maximum number of messages to return for this request. The Pub/Sub
288
+ # system may return fewer than the number specified. The default value
289
+ # is +100+, the maximum value is +1000+. (+Integer+)
290
+ # <code>options[:autoack]</code>::
291
+ # Automatically acknowledge the message as it is pulled. The default
292
+ # value is +false+. (+Boolean+)
293
+ #
294
+ # === Returns
295
+ #
296
+ # Array of Gcloud::Pubsub::ReceivedMessage
297
+ #
298
+ # === Example
299
+ #
300
+ # require "gcloud"
301
+ #
302
+ # gcloud = Gcloud.new
303
+ # pubsub = gcloud.pubsub
304
+ #
305
+ # sub = pubsub.subscription "my-topic-sub"
306
+ # msgs = sub.wait_for_messages
307
+ # msgs.each { |msg| msg.acknowledge! }
308
+ #
309
+ def wait_for_messages options = {}
310
+ pull options.merge(immediate: false)
311
+ end
312
+
313
+ ##
314
+ # Poll the backend for new messages. This runs a loop to ping the API,
315
+ # blocking indefinitely, yielding retrieved messages as they are received.
316
+ #
317
+ # === Parameters
318
+ #
319
+ # +options+::
320
+ # An optional Hash for controlling additional behavior. (+Hash+)
321
+ # <code>options[:max]</code>::
322
+ # The maximum number of messages to return for this request. The Pub/Sub
323
+ # system may return fewer than the number specified. The default value
324
+ # is +100+, the maximum value is +1000+. (+Integer+)
325
+ # <code>options[:autoack]</code>::
326
+ # Automatically acknowledge the message as it is pulled. The default
327
+ # value is +false+. (+Boolean+)
328
+ # <code>options[:delay]</code>::
329
+ # The number of seconds to pause between requests when the Google Cloud
330
+ # service has no messages to return. The default value is +1+.
331
+ # (+Number+)
332
+ #
333
+ # === Examples
334
+ #
335
+ # require "gcloud"
336
+ #
337
+ # gcloud = Gcloud.new
338
+ # pubsub = gcloud.pubsub
339
+ #
340
+ # sub = pubsub.subscription "my-topic-sub"
341
+ # sub.listen do |msg|
342
+ # # process msg
343
+ # end
344
+ #
345
+ # The number of messages pulled per batch can be set with the +max+
346
+ # option:
347
+ #
348
+ # require "gcloud"
349
+ #
350
+ # gcloud = Gcloud.new
351
+ # pubsub = gcloud.pubsub
352
+ #
353
+ # sub = pubsub.subscription "my-topic-sub"
354
+ # sub.listen max: 20 do |msg|
355
+ # # process msg
356
+ # end
357
+ #
358
+ # Messages can be automatically acknowledged as they are pulled with the
359
+ # +autoack+ option:
360
+ #
361
+ # require "gcloud"
362
+ #
363
+ # gcloud = Gcloud.new
364
+ # pubsub = gcloud.pubsub
365
+ #
366
+ # sub = pubsub.subscription "my-topic-sub"
367
+ # sub.listen autoack: true do |msg|
368
+ # # process msg
369
+ # end
370
+ #
371
+ def listen options = {}
372
+ delay = options.fetch(:delay, 1)
373
+ loop do
374
+ msgs = wait_for_messages options
375
+ if msgs.any?
376
+ msgs.each { |msg| yield msg }
377
+ else
378
+ sleep delay
379
+ end
380
+ end
381
+ end
382
+
383
+ ##
384
+ # Acknowledges receipt of a message. After an ack,
385
+ # the Pub/Sub system can remove the message from the subscription.
386
+ # Acknowledging a message whose ack deadline has expired may succeed,
387
+ # although the message may have been sent again.
388
+ # Acknowledging a message more than once will not result in an error.
389
+ # This is only used for messages received via pull.
390
+ #
391
+ # === Parameters
392
+ #
393
+ # +messages+::
394
+ # One or more ReceivedMessage objects or ack_id values.
395
+ # (+ReceivedMessage+ or +ack_id+)
396
+ #
397
+ # === Example
398
+ #
399
+ # require "gcloud"
400
+ #
401
+ # gcloud = Gcloud.new
402
+ # pubsub = gcloud.pubsub
403
+ #
404
+ # sub = pubsub.subscription "my-topic-sub"
405
+ # messages = sub.pull
406
+ # sub.acknowledge messages
407
+ #
408
+ def acknowledge *messages
409
+ ack_ids = coerce_ack_ids messages
410
+ ensure_connection!
411
+ resp = connection.acknowledge name, *ack_ids
412
+ if resp.success?
413
+ true
414
+ else
415
+ fail ApiError.from_response(resp)
416
+ end
417
+ end
418
+ alias_method :ack, :acknowledge
419
+
420
+ ##
421
+ # Modifies the acknowledge deadline for messages.
422
+ #
423
+ # This indicates that more time is needed to process the messages, or to
424
+ # make the messages available for redelivery if the processing was
425
+ # interrupted.
426
+ #
427
+ # === Parameters
428
+ #
429
+ # +new_deadline+::
430
+ # The new ack deadline in seconds from the time this request is sent
431
+ # to the Pub/Sub system. Must be >= 0. For example, if the value is
432
+ # +10+, the new ack deadline will expire 10 seconds after the call is
433
+ # made. Specifying +0+ may immediately make the messages available for
434
+ # another pull request. (+Integer+)
435
+ # +messages+::
436
+ # One or more ReceivedMessage objects or ack_id values.
437
+ # (+ReceivedMessage+ or +ack_id+)
438
+ #
439
+ # === Example
440
+ #
441
+ # require "gcloud"
442
+ #
443
+ # gcloud = Gcloud.new
444
+ # pubsub = gcloud.pubsub
445
+ #
446
+ # sub = pubsub.subscription "my-topic-sub"
447
+ # messages = sub.pull
448
+ # sub.delay 120, messages
449
+ #
450
+ def delay new_deadline, *messages
451
+ ack_ids = coerce_ack_ids messages
452
+ ensure_connection!
453
+ resp = connection.modify_ack_deadline name, ack_ids, new_deadline
454
+ if resp.success?
455
+ true
456
+ else
457
+ fail ApiError.from_response(resp)
458
+ end
459
+ end
460
+
461
+ ##
462
+ # Gets the access control policy.
463
+ #
464
+ # === Parameters
465
+ #
466
+ # +options+::
467
+ # An optional Hash for controlling additional behavior. (+Hash+)
468
+ # <code>options[:force]</code>::
469
+ # Force the latest policy to be retrieved from the Pub/Sub service when
470
+ # +true. Otherwise the policy will be memoized to reduce the number of
471
+ # API calls made to the Pub/Sub service. The default is +false+.
472
+ # (+Boolean+)
473
+ #
474
+ # === Returns
475
+ #
476
+ # A hash that conforms to the following structure:
477
+ #
478
+ # {
479
+ # "bindings" => [{
480
+ # "role" => "roles/viewer",
481
+ # "members" => ["serviceAccount:your-service-account"]
482
+ # }],
483
+ # "rules" => []
484
+ # }
485
+ #
486
+ # === Examples
487
+ #
488
+ # By default, the policy values are memoized to reduce the number of API
489
+ # calls to the Pub/Sub service.
490
+ #
491
+ # require "gcloud"
492
+ #
493
+ # gcloud = Gcloud.new
494
+ # pubsub = gcloud.pubsub
495
+ #
496
+ # subscription = pubsub.subscription "my-subscription"
497
+ # puts subscription.policy["bindings"]
498
+ # puts subscription.policy["rules"]
499
+ #
500
+ # To retrieve the latest policy from the Pub/Sub service, use the +force+
501
+ # flag.
502
+ #
503
+ # require "gcloud"
504
+ #
505
+ # gcloud = Gcloud.new
506
+ # pubsub = gcloud.pubsub
507
+ #
508
+ # subscription = pubsub.subscription "my-subscription"
509
+ # policy = subscription.policy force: true
510
+ #
511
+ def policy options = {}
512
+ @policy = nil if options[:force]
513
+ @policy ||= begin
514
+ ensure_connection!
515
+ resp = connection.get_subscription_policy name
516
+ policy = resp.data["policy"]
517
+ policy = policy.to_hash if policy.respond_to? :to_hash
518
+ policy
519
+ end
520
+ end
521
+
522
+ ##
523
+ # Sets the access control policy.
524
+ #
525
+ # === Parameters
526
+ #
527
+ # +new_policy+::
528
+ # A hash that conforms to the following structure:
529
+ #
530
+ # {
531
+ # "bindings" => [{
532
+ # "role" => "roles/viewer",
533
+ # "members" => ["serviceAccount:your-service-account"]
534
+ # }],
535
+ # "rules" => []
536
+ # }
537
+ #
538
+ # === Example
539
+ #
540
+ # require "gcloud"
541
+ #
542
+ # gcloud = Gcloud.new
543
+ # pubsub = gcloud.pubsub
544
+ #
545
+ # viewer_policy = {
546
+ # "bindings" => [{
547
+ # "role" => "roles/viewer",
548
+ # "members" => ["serviceAccount:your-service-account"]
549
+ # }]
550
+ # }
551
+ # subscription = pubsub.subscription "my-subscription"
552
+ # subscription.policy = viewer_policy
553
+ #
554
+ def policy= new_policy
555
+ ensure_connection!
556
+ resp = connection.set_subscription_policy name, new_policy
557
+ if resp.success?
558
+ @policy = resp.data["policy"]
559
+ @policy = @policy.to_hash if @policy.respond_to? :to_hash
560
+ else
561
+ fail ApiError.from_response(resp)
562
+ end
563
+ end
564
+
565
+ ##
566
+ # New Subscription from a Google API Client object.
567
+ def self.from_gapi gapi, conn #:nodoc:
568
+ new.tap do |f|
569
+ f.gapi = gapi
570
+ f.connection = conn
571
+ end
572
+ end
573
+
574
+ protected
575
+
576
+ ##
577
+ # Raise an error unless an active connection is available.
578
+ def ensure_connection!
579
+ fail "Must have active connection" unless connection
580
+ end
581
+
582
+ ##
583
+ # Ensures a Google API object exists.
584
+ def ensure_gapi!
585
+ ensure_connection!
586
+ return @gapi if @gapi
587
+ resp = connection.get_subscription @name
588
+ if resp.success?
589
+ @gapi = resp.data
590
+ else
591
+ fail ApiError.from_response(resp)
592
+ end
593
+ end
594
+
595
+ ##
596
+ # Makes sure the values are the +ack_id+.
597
+ # If given several ReceivedMessage objects extract the +ack_id+ values.
598
+ def coerce_ack_ids messages
599
+ Array(messages).flatten.map do |msg|
600
+ msg.respond_to?(:ack_id) ? msg.ack_id : msg.to_s
601
+ end
602
+ end
603
+ end
604
+ end
605
+ end