gcloud 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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