/*
* Copyright 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.cloud.pubsub;
import com.google.auth.Credentials;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.pubsub.Subscriber.MessageReceiver.AckReply;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.Service;
import com.google.pubsub.v1.PubsubMessage;
import io.grpc.ManagedChannelBuilder;
import java.io.IOException;
import java.util.concurrent.ScheduledExecutorService;
import org.joda.time.Duration;
/**
* A Cloud Pub/Sub <a href="https://cloud.google.com/pubsub/docs/subscriber">subscriber</a> that is
* associated with a specific subscription at creation.
*
* <p>A {@link Subscriber} allows you to provide an implementation of a {@link MessageReceiver
* receiver} to which messages are going to be delivered as soon as they are received by the
* subscriber. The delivered messages then can be {@link AckReply#ACK acked} or {@link
* AckReply#NACK nacked} at will as they get processed by the receiver. Nacking a
* messages implies a later redelivery of such message.
*
* <p>The subscriber handles the ack management, by automatically extending the ack deadline while
* the message is being processed, to then issue the ack or nack of such message when the processing
* is done. <strong>Note:</strong> message redelivery is still possible.
*
* <p>It also provides customizable options that control:
*
* <ul>
* <li>Ack deadline extension: such as the amount of time ahead to trigger the extension of
* message acknowledgement expiration.
* <li>Flow control: such as the maximum outstanding messages or maximum outstanding bytes to keep
* in memory before the receiver either ack or nack them.
* </ul>
*
* <p>If no credentials are provided, the {@link Publisher} will use application default
* credentials through {@link GoogleCredentials#getApplicationDefault}.
*
* <p>For example, a {@link Subscriber} can be constructed and used to receive messages as follows:
*
* <pre>
* MessageReceiver receiver =
* message -> {
* // ... process message ...
* return Futures.immediateFuture(AckReply.ACK);
* });
*
* Subscriber subscriber =
* Subscriber.Builder.newBuilder(MY_SUBSCRIPTION, receiver)
* .setMaxBatchAcks(100)
* .build();
*
* subscriber.startAsync();
*
* ... recommended, listen for fatal errors that break the subscriber streaming ...
* subscriber.addListener(
new Listener() {
@Override
public void failed(State from, Throwable failure) {
System.out.println("Subscriber faile with error: " + failure);
}
},
Executors.newSingleThreadExecutor());
*
* ... and when done with the subscriber ...
* subscriber.stopAsync();
* </pre>
*/
public interface Subscriber extends Service {
String PUBSUB_API_ADDRESS = "pubsub.googleapis.com";
String PUBSUB_API_SCOPE = "https://www.googleapis.com/auth/pubsub";
/** Retrieves a snapshot of the current subscriber statistics. */
SubscriberStats getStats();
/** Users of the {@link Subscriber} must implement this interface to receive messages. */
interface MessageReceiver {
public static enum AckReply {
/**
* To be used for acking a message.
*/
ACK,
/**
* To be used for nacking a message.
*/
NACK
}
/**
* Called when a message is received by the subscriber.
*
* @return A future that signals when a message has been processed.
*/
ListenableFuture<AckReply> receiveMessage(PubsubMessage message);
}
/** Subscription for which the subscriber is streaming messages. */
String getSubscription();
/**
* Time before a message is to expire when the subscriber is going to attempt to renew its ack
* deadline.
*/
Duration getAckExpirationPadding();
/**
* Maximum number of outstanding (i.e. pending to process) messages before limits are enforced.
*
* <p><b>When limits are enforced, no more messages will be dispatched to the {@link
* MessageReceiver} but due to the gRPC and HTTP/2 buffering and congestion control window
* management, still some extra bytes could be kept at lower layers.
*/
Optional<Integer> getMaxOutstandingMessages();
/**
* Maximum number of outstanding (i.e. pending to process) bytes before limits are enforced.
*/
Optional<Integer> getMaxOutstandingBytes();
/** Builder of {@link Subscriber Subscribers}. */
final class Builder {
private static final Duration MIN_ACK_EXPIRATION_PADDING = Duration.millis(100);
private static final Duration DEFAULT_ACK_EXPIRATION_PADDING = Duration.millis(500);
String subscription;
Optional<Credentials> credentials;
MessageReceiver receiver;
Duration ackExpirationPadding;
Optional<Integer> maxOutstandingMessages;
Optional<Integer> maxOutstandingBytes;
Optional<ScheduledExecutorService> executor;
Optional<ManagedChannelBuilder<? extends ManagedChannelBuilder<?>>> channelBuilder;
/**
* Constructs a new {@link Builder}.
*
* <p>Once {@link #build()} is called a gRPC stub will be created for use of the {@link
* Publisher}.
*
* @param subscription Cloud Pub/Sub subscription to bind the subscriber to
* @param receiver an implementation of {@link MessageReceiver} used to process the received
* messages
*/
public static Builder newBuilder(String subscription, MessageReceiver receiver) {
return new Builder(subscription, receiver);
}
Builder(String subscription, MessageReceiver receiver) {
setDefaults();
this.subscription = subscription;
this.receiver = receiver;
}
private void setDefaults() {
credentials = Optional.absent();
channelBuilder = Optional.absent();
ackExpirationPadding = DEFAULT_ACK_EXPIRATION_PADDING;
maxOutstandingBytes = Optional.absent();
maxOutstandingMessages = Optional.absent();
executor = Optional.absent();
}
/**
* Credentials to authenticate with.
*
* <p>Must be properly scoped for accessing Cloud Pub/Sub APIs.
*/
public Builder setCredentials(Credentials credentials) {
this.credentials = Optional.of(Preconditions.checkNotNull(credentials));
return this;
}
/**
* ManagedChannelBuilder to use to create Channels.
*
* <p>Must point at Cloud Pub/Sub endpoint.
*/
public Builder setChannelBuilder(
ManagedChannelBuilder<? extends ManagedChannelBuilder<?>> channelBuilder) {
this.channelBuilder =
Optional.<ManagedChannelBuilder<? extends ManagedChannelBuilder<?>>>of(
Preconditions.checkNotNull(channelBuilder));
return this;
}
/**
* Sets the maximum number of outstanding messages; messages delivered to the {@link
* MessageReceiver} that have not been acknowledged or rejected.
*
* @param maxOutstandingMessages must be greater than 0
*/
public Builder setMaxOutstandingMessages(int maxOutstandingMessages) {
Preconditions.checkArgument(
maxOutstandingMessages > 0,
"maxOutstandingMessages limit is disabled by default, but if set it must be set to a "
+ "value greater to 0.");
this.maxOutstandingMessages = Optional.of(maxOutstandingMessages);
return this;
}
/**
* Sets the maximum number of outstanding bytes; bytes delivered to the {@link MessageReceiver}
* that have not been acknowledged or rejected.
*
* @param maxOutstandingBytes must be greater than 0
*/
public Builder setMaxOutstandingBytes(int maxOutstandingBytes) {
Preconditions.checkArgument(
maxOutstandingBytes > 0,
"maxOutstandingBytes limit is disabled by default, but if set it must be set to a value "
+ "greater than 0.");
this.maxOutstandingBytes = Optional.of(maxOutstandingBytes);
return this;
}
/**
* Set acknowledgement expiration padding.
*
* <p>This is the time accounted before a message expiration is to happen, so the
* {@link Subscriber} is able to send an ack extension beforehand.
*
* <p>This padding duration is configurable so you can account for network latency. A reasonable
* number must be provided so messages don't expire because of network latency between when the
* ack extension is required and when it reaches the Pub/Sub service.
*
* @param ackExpirationPadding must be greater or equal to {@link #MIN_ACK_EXPIRATION_PADDING}
*/
public Builder setAckExpirationPadding(Duration ackExpirationPadding) {
Preconditions.checkArgument(ackExpirationPadding.compareTo(MIN_ACK_EXPIRATION_PADDING) >= 0);
this.ackExpirationPadding = ackExpirationPadding;
return this;
}
/** Gives the ability to set a custom executor. */
public Builder setExecutor(ScheduledExecutorService executor) {
this.executor = Optional.of(executor);
return this;
}
public Subscriber build() throws IOException {
return new SubscriberImpl(this);
}
}
}