com.google.cloud.pubsub.PollingSubscriberConnection.java Source code

Java tutorial

Introduction

Here is the source code for com.google.cloud.pubsub.PollingSubscriberConnection.java

Source

/*
 * 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.cloud.pubsub.Subscriber.MessageReceiver;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.pubsub.v1.AcknowledgeRequest;
import com.google.pubsub.v1.GetSubscriptionRequest;
import com.google.pubsub.v1.ModifyAckDeadlineRequest;
import com.google.pubsub.v1.PullRequest;
import com.google.pubsub.v1.PullResponse;
import com.google.pubsub.v1.SubscriberGrpc;
import com.google.pubsub.v1.SubscriberGrpc.SubscriberFutureStub;
import com.google.pubsub.v1.Subscription;
import io.grpc.Channel;
import io.grpc.StatusRuntimeException;
import io.grpc.auth.MoreCallCredentials;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Implementation of {@link AbstractSubscriberConnection} based on Cloud Pub/Sub pull and
 * acknowledge operations.
 */
final class PollingSubscriberConnection extends AbstractSubscriberConnection {
    private static final int MAX_PER_REQUEST_CHANGES = 1000;
    private static final Duration DEFAULT_TIMEOUT = Duration.standardSeconds(10);
    private static final int DEFAULT_MAX_MESSAGES = 1000;
    private static final Duration INITIAL_BACKOFF = Duration.millis(100); // 100ms
    private static final Duration MAX_BACKOFF = Duration.standardSeconds(10); // 10s

    private static final Logger logger = LoggerFactory.getLogger(PollingSubscriberConnection.class);

    private final SubscriberFutureStub stub;

    public PollingSubscriberConnection(String subscription, Credentials credentials, MessageReceiver receiver,
            Duration ackExpirationPadding, Distribution ackLatencyDistribution, Channel channel,
            FlowController flowController, ScheduledExecutorService executor) {
        super(subscription, receiver, ackExpirationPadding, ackLatencyDistribution, flowController, executor);
        stub = SubscriberGrpc.newFutureStub(channel).withCallCredentials(MoreCallCredentials.from(credentials));
    }

    @Override
    void initialize() {
        ListenableFuture<Subscription> subscriptionInfo = stub
                .withDeadlineAfter(DEFAULT_TIMEOUT.getMillis(), TimeUnit.MILLISECONDS)
                .getSubscription(GetSubscriptionRequest.newBuilder().setSubscription(subscription).build());

        Futures.addCallback(subscriptionInfo, new FutureCallback<Subscription>() {
            @Override
            public void onSuccess(Subscription result) {
                setMessageDeadlineSeconds(result.getAckDeadlineSeconds());
                pullMessages(INITIAL_BACKOFF);
            }

            @Override
            public void onFailure(Throwable cause) {
                notifyFailed(cause);
            }
        });
    }

    private void pullMessages(final Duration backoff) {
        ListenableFuture<PullResponse> pullResult = stub
                .withDeadlineAfter(DEFAULT_TIMEOUT.getMillis(), TimeUnit.MILLISECONDS)
                .pull(PullRequest.newBuilder().setSubscription(subscription).setMaxMessages(DEFAULT_MAX_MESSAGES)
                        .setReturnImmediately(true).build());

        Futures.addCallback(pullResult, new FutureCallback<PullResponse>() {
            @Override
            public void onSuccess(PullResponse pullResponse) {
                processReceivedMessages(pullResponse.getReceivedMessagesList());
                if (pullResponse.getReceivedMessagesCount() == 0) {
                    // No messages in response, possibly caught up in backlog, we backoff to avoid 
                    // slamming the server.
                    executor.schedule(new Runnable() {
                        @Override
                        public void run() {
                            Duration newBackoff = backoff.multipliedBy(2);
                            if (newBackoff.isLongerThan(MAX_BACKOFF)) {
                                newBackoff = MAX_BACKOFF;
                            }
                            pullMessages(newBackoff);
                        }
                    }, backoff.getMillis(), TimeUnit.MILLISECONDS);
                    return;
                }
                pullMessages(INITIAL_BACKOFF);
            }

            @Override
            public void onFailure(Throwable cause) {
                if (!(cause instanceof StatusRuntimeException)
                        || isRetryable(((StatusRuntimeException) cause).getStatus())) {
                    logger.error("Failed to pull messages (recoverable): " + cause.getMessage(), cause);
                    executor.schedule(new Runnable() {
                        @Override
                        public void run() {
                            Duration newBackoff = backoff.multipliedBy(2);
                            if (newBackoff.isLongerThan(MAX_BACKOFF)) {
                                newBackoff = MAX_BACKOFF;
                            }
                            pullMessages(newBackoff);
                        }
                    }, backoff.getMillis(), TimeUnit.MILLISECONDS);
                    return;
                }
                notifyFailed(cause);
            }
        });
    }

    @Override
    void sendAckOperations(List<String> acksToSend, List<PendingModifyAckDeadline> ackDeadlineExtensions) {
        // Send the modify ack deadlines in batches as not to exceed the max request
        // size.
        List<List<PendingModifyAckDeadline>> modifyAckDeadlineChunks = Lists.partition(ackDeadlineExtensions,
                MAX_PER_REQUEST_CHANGES);
        for (List<PendingModifyAckDeadline> modAckChunk : modifyAckDeadlineChunks) {
            for (PendingModifyAckDeadline modifyAckDeadline : modAckChunk) {
                stub.withDeadlineAfter(DEFAULT_TIMEOUT.getMillis(), TimeUnit.MILLISECONDS)
                        .modifyAckDeadline(ModifyAckDeadlineRequest.newBuilder().setSubscription(subscription)
                                .addAllAckIds(modifyAckDeadline.ackIds)
                                .setAckDeadlineSeconds(modifyAckDeadline.deadlineExtensionSeconds).build());
            }
        }

        List<List<String>> ackChunks = Lists.partition(acksToSend, MAX_PER_REQUEST_CHANGES);
        Iterator<List<String>> ackChunksIt = ackChunks.iterator();
        while (ackChunksIt.hasNext()) {
            List<String> ackChunk = ackChunksIt.next();
            stub.withDeadlineAfter(DEFAULT_TIMEOUT.getMillis(), TimeUnit.MILLISECONDS).acknowledge(
                    AcknowledgeRequest.newBuilder().setSubscription(subscription).addAllAckIds(ackChunk).build());
        }
    }
}