All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.cloud.pubsub.v1.Publisher Maven / Gradle / Ivy

/*
 * Copyright 2016 Google LLC
 *
 * 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.v1;

import static com.google.common.util.concurrent.MoreExecutors.directExecutor;

import com.google.api.core.ApiFunction;
import com.google.api.core.ApiFuture;
import com.google.api.core.ApiFutureCallback;
import com.google.api.core.ApiFutures;
import com.google.api.core.BetaApi;
import com.google.api.core.SettableApiFuture;
import com.google.api.gax.batching.BatchingSettings;
import com.google.api.gax.core.BackgroundResource;
import com.google.api.gax.core.BackgroundResourceAggregation;
import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.core.ExecutorAsBackgroundResource;
import com.google.api.gax.core.ExecutorProvider;
import com.google.api.gax.core.FixedExecutorProvider;
import com.google.api.gax.core.InstantiatingExecutorProvider;
import com.google.api.gax.retrying.RetrySettings;
import com.google.api.gax.rpc.HeaderProvider;
import com.google.api.gax.rpc.NoHeaderProvider;
import com.google.api.gax.rpc.StatusCode;
import com.google.api.gax.rpc.TransportChannelProvider;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.cloud.pubsub.v1.stub.GrpcPublisherStub;
import com.google.cloud.pubsub.v1.stub.PublisherStub;
import com.google.cloud.pubsub.v1.stub.PublisherStubSettings;
import com.google.common.base.Preconditions;
import com.google.pubsub.v1.PublishRequest;
import com.google.pubsub.v1.PublishResponse;
import com.google.pubsub.v1.PubsubMessage;
import com.google.pubsub.v1.TopicName;
import com.google.pubsub.v1.TopicNames;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.threeten.bp.Duration;

/**
 * A Cloud Pub/Sub publisher, that is
 * associated with a specific topic at creation.
 *
 * 

A {@link Publisher} provides built-in capabilities to automatically handle batching of * messages, controlling memory utilization, and retrying API calls on transient errors. * *

With customizable options that control: * *

    *
  • Message batching: such as number of messages or max batch byte size. *
  • Retries: such as the maximum duration of retries for a failing batch of messages. *
* *

{@link Publisher} will use the credentials set on the channel, which uses application default * credentials through {@link GoogleCredentials#getApplicationDefault} by default. */ public class Publisher { private static final Logger logger = Logger.getLogger(Publisher.class.getName()); private final String topicName; private final BatchingSettings batchingSettings; private final Lock messagesBatchLock; private MessagesBatch messagesBatch; private final AtomicBoolean activeAlarm; private final PublisherStub publisherStub; private final ScheduledExecutorService executor; private final AtomicBoolean shutdown; private final BackgroundResource backgroundResources; private final MessageWaiter messagesWaiter; private ScheduledFuture currentAlarmFuture; private final ApiFunction messageTransform; /** The maximum number of messages in one request. Defined by the API. */ public static long getApiMaxRequestElementCount() { return 1000L; } /** The maximum size of one request. Defined by the API. */ public static long getApiMaxRequestBytes() { return 10L * 1000L * 1000L; // 10 megabytes (https://en.wikipedia.org/wiki/Megabyte) } private Publisher(Builder builder) throws IOException { topicName = builder.topicName; this.batchingSettings = builder.batchingSettings; this.messageTransform = builder.messageTransform; messagesBatch = new MessagesBatch(batchingSettings); messagesBatchLock = new ReentrantLock(); activeAlarm = new AtomicBoolean(false); executor = builder.executorProvider.getExecutor(); List backgroundResourceList = new ArrayList<>(); if (builder.executorProvider.shouldAutoClose()) { backgroundResourceList.add(new ExecutorAsBackgroundResource(executor)); } // Publisher used to take maxAttempt == 0 to mean infinity, but to GAX it means don't retry. // We post-process this here to keep backward-compatibility. RetrySettings retrySettings = builder.retrySettings; if (retrySettings.getMaxAttempts() == 0) { retrySettings = retrySettings.toBuilder().setMaxAttempts(Integer.MAX_VALUE).build(); } PublisherStubSettings.Builder stubSettings = PublisherStubSettings.newBuilder() .setCredentialsProvider(builder.credentialsProvider) .setExecutorProvider(FixedExecutorProvider.create(executor)) .setTransportChannelProvider(builder.channelProvider); stubSettings .publishSettings() .setRetryableCodes( StatusCode.Code.ABORTED, StatusCode.Code.CANCELLED, StatusCode.Code.DEADLINE_EXCEEDED, StatusCode.Code.INTERNAL, StatusCode.Code.RESOURCE_EXHAUSTED, StatusCode.Code.UNKNOWN, StatusCode.Code.UNAVAILABLE) .setRetrySettings(retrySettings) .setBatchingSettings(BatchingSettings.newBuilder().setIsEnabled(false).build()); this.publisherStub = GrpcPublisherStub.create(stubSettings.build()); backgroundResourceList.add(publisherStub); backgroundResources = new BackgroundResourceAggregation(backgroundResourceList); shutdown = new AtomicBoolean(false); messagesWaiter = new MessageWaiter(); } /** Topic which the publisher publishes to. */ public TopicName getTopicName() { return TopicNames.parse(topicName); } /** Topic which the publisher publishes to. */ public String getTopicNameString() { return topicName; } /** * Schedules the publishing of a message. The publishing of the message may occur immediately or * be delayed based on the publisher batching options. * *

Example of publishing a message. * *

{@code
   * String message = "my_message";
   * ByteString data = ByteString.copyFromUtf8(message);
   * PubsubMessage pubsubMessage = PubsubMessage.newBuilder().setData(data).build();
   * ApiFuture messageIdFuture = publisher.publish(pubsubMessage);
   * ApiFutures.addCallback(messageIdFuture, new ApiFutureCallback() {
   *   public void onSuccess(String messageId) {
   *     System.out.println("published with message id: " + messageId);
   *   }
   *
   *   public void onFailure(Throwable t) {
   *     System.out.println("failed to publish: " + t);
   *   }
   * });
   * }
* * @param message the message to publish. * @return the message ID wrapped in a future. */ public ApiFuture publish(PubsubMessage message) { Preconditions.checkState(!shutdown.get(), "Cannot publish on a shut-down publisher."); final OutstandingPublish outstandingPublish = new OutstandingPublish(messageTransform.apply(message)); List batchesToSend; messagesBatchLock.lock(); try { batchesToSend = messagesBatch.add(outstandingPublish); // Setup the next duration based delivery alarm if there are messages batched. setupAlarm(); } finally { messagesBatchLock.unlock(); } messagesWaiter.incrementPendingMessages(1); if (!batchesToSend.isEmpty()) { for (final OutstandingBatch batch : batchesToSend) { logger.log(Level.FINER, "Scheduling a batch for immediate sending."); executor.execute( new Runnable() { @Override public void run() { publishOutstandingBatch(batch); } }); } } return outstandingPublish.publishResult; } private void setupAlarm() { if (!messagesBatch.isEmpty()) { if (!activeAlarm.getAndSet(true)) { long delayThresholdMs = getBatchingSettings().getDelayThreshold().toMillis(); logger.log(Level.FINER, "Setting up alarm for the next {0} ms.", delayThresholdMs); currentAlarmFuture = executor.schedule( new Runnable() { @Override public void run() { logger.log(Level.FINER, "Sending messages based on schedule."); activeAlarm.getAndSet(false); publishAllOutstanding(); } }, delayThresholdMs, TimeUnit.MILLISECONDS); } } else if (currentAlarmFuture != null) { logger.log(Level.FINER, "Cancelling alarm, no more messages"); if (activeAlarm.getAndSet(false)) { currentAlarmFuture.cancel(false); } } } /** * Publish any outstanding batches if non-empty. This method sends buffered messages, but does not * wait for the send operations to complete. To wait for messages to send, call {@code get} on the * futures returned from {@code publish}. */ public void publishAllOutstanding() { messagesBatchLock.lock(); OutstandingBatch batchToSend; try { if (messagesBatch.isEmpty()) { return; } batchToSend = messagesBatch.popOutstandingBatch(); } finally { messagesBatchLock.unlock(); } publishOutstandingBatch(batchToSend); } private ApiFuture publishCall(OutstandingBatch outstandingBatch) { return publisherStub .publishCallable() .futureCall( PublishRequest.newBuilder() .setTopic(topicName) .addAllMessages(outstandingBatch.getMessages()) .build()); } private void publishOutstandingBatch(final OutstandingBatch outstandingBatch) { ApiFutureCallback futureCallback = new ApiFutureCallback() { @Override public void onSuccess(PublishResponse result) { try { if (result.getMessageIdsCount() != outstandingBatch.size()) { outstandingBatch.onFailure( new IllegalStateException( String.format( "The publish result count %s does not match " + "the expected %s results. Please contact Cloud Pub/Sub support " + "if this frequently occurs", result.getMessageIdsCount(), outstandingBatch.size()))); } else { outstandingBatch.onSuccess(result.getMessageIdsList()); } } finally { messagesWaiter.incrementPendingMessages(-outstandingBatch.size()); } } @Override public void onFailure(Throwable t) { try { outstandingBatch.onFailure(t); } finally { messagesWaiter.incrementPendingMessages(-outstandingBatch.size()); } } }; ApiFutures.addCallback(publishCall(outstandingBatch), futureCallback, directExecutor()); } private static final class OutstandingBatch { final List outstandingPublishes; final long creationTime; int attempt; int batchSizeBytes; OutstandingBatch(List outstandingPublishes, int batchSizeBytes) { this.outstandingPublishes = outstandingPublishes; attempt = 1; creationTime = System.currentTimeMillis(); this.batchSizeBytes = batchSizeBytes; } int size() { return outstandingPublishes.size(); } private List getMessages() { List results = new ArrayList<>(outstandingPublishes.size()); for (OutstandingPublish outstandingPublish : outstandingPublishes) { results.add(outstandingPublish.message); } return results; } private void onFailure(Throwable t) { for (OutstandingPublish outstandingPublish : outstandingPublishes) { outstandingPublish.publishResult.setException(t); } } private void onSuccess(Iterable results) { Iterator messagesResultsIt = outstandingPublishes.iterator(); for (String messageId : results) { messagesResultsIt.next().publishResult.set(messageId); } } } private static final class OutstandingPublish { final SettableApiFuture publishResult; final PubsubMessage message; final int messageSize; OutstandingPublish(PubsubMessage message) { this.publishResult = SettableApiFuture.create(); this.message = message; this.messageSize = message.getSerializedSize(); } } /** The batching settings configured on this {@code Publisher}. */ public BatchingSettings getBatchingSettings() { return batchingSettings; } /** * Schedules immediate publishing of any outstanding messages and waits until all are processed. * *

Sends remaining outstanding messages and prevents future calls to publish. This method * should be invoked prior to deleting the {@link Publisher} object in order to ensure that no * pending messages are lost. */ public void shutdown() { Preconditions.checkState( !shutdown.getAndSet(true), "Cannot shut down a publisher already shut-down."); if (currentAlarmFuture != null && activeAlarm.getAndSet(false)) { currentAlarmFuture.cancel(false); } publishAllOutstanding(); backgroundResources.shutdown(); } /** * Wait for all work has completed execution after a {@link #shutdown()} request, or the timeout * occurs, or the current thread is interrupted. * *

Call this method to make sure all resources are freed properly. */ public boolean awaitTermination(long duration, TimeUnit unit) throws InterruptedException { return backgroundResources.awaitTermination(duration, unit); } /** * Constructs a new {@link Builder} using the given topic. * *

Example of creating a {@code Publisher}. * *

{@code
   * String projectName = "my_project";
   * String topicName = "my_topic";
   * ProjectTopicName topic = ProjectTopicName.create(projectName, topicName);
   * Publisher publisher = Publisher.newBuilder(topic).build();
   * try {
   *   // ...
   * } finally {
   *   // When finished with the publisher, make sure to shutdown to free up resources.
   *   publisher.shutdown();
   *   publisher.awaitTermination(1, TimeUnit.MINUTES);
   * }
   * }
*/ public static Builder newBuilder(TopicName topicName) { return newBuilder(topicName.toString()); } /** * Constructs a new {@link Builder} using the given topic. * *

Example of creating a {@code Publisher}. * *

{@code
   * String topic = "projects/my_project/topics/my_topic";
   * Publisher publisher = Publisher.newBuilder(topic).build();
   * try {
   *   // ...
   * } finally {
   *   // When finished with the publisher, make sure to shutdown to free up resources.
   *   publisher.shutdown();
   *   publisher.awaitTermination(1, TimeUnit.MINUTES);
   * }
   * }
*/ public static Builder newBuilder(String topicName) { return new Builder(topicName); } /** A builder of {@link Publisher}s. */ public static final class Builder { static final Duration MIN_TOTAL_TIMEOUT = Duration.ofSeconds(10); static final Duration MIN_RPC_TIMEOUT = Duration.ofMillis(10); // Meaningful defaults. static final long DEFAULT_ELEMENT_COUNT_THRESHOLD = 100L; static final long DEFAULT_REQUEST_BYTES_THRESHOLD = 1000L; // 1 kB static final Duration DEFAULT_DELAY_THRESHOLD = Duration.ofMillis(1); private static final Duration DEFAULT_RPC_TIMEOUT = Duration.ofSeconds(10); private static final Duration DEFAULT_TOTAL_TIMEOUT = MIN_TOTAL_TIMEOUT; static final BatchingSettings DEFAULT_BATCHING_SETTINGS = BatchingSettings.newBuilder() .setDelayThreshold(DEFAULT_DELAY_THRESHOLD) .setRequestByteThreshold(DEFAULT_REQUEST_BYTES_THRESHOLD) .setElementCountThreshold(DEFAULT_ELEMENT_COUNT_THRESHOLD) .build(); static final RetrySettings DEFAULT_RETRY_SETTINGS = RetrySettings.newBuilder() .setTotalTimeout(DEFAULT_TOTAL_TIMEOUT) .setInitialRetryDelay(Duration.ofMillis(5)) .setRetryDelayMultiplier(2) .setMaxRetryDelay(Duration.ofMillis(Long.MAX_VALUE)) .setInitialRpcTimeout(DEFAULT_RPC_TIMEOUT) .setRpcTimeoutMultiplier(2) .setMaxRpcTimeout(DEFAULT_RPC_TIMEOUT) .build(); private static final int THREADS_PER_CPU = 5; static final ExecutorProvider DEFAULT_EXECUTOR_PROVIDER = InstantiatingExecutorProvider.newBuilder() .setExecutorThreadCount(THREADS_PER_CPU * Runtime.getRuntime().availableProcessors()) .build(); String topicName; // Batching options BatchingSettings batchingSettings = DEFAULT_BATCHING_SETTINGS; RetrySettings retrySettings = DEFAULT_RETRY_SETTINGS; private TransportChannelProvider channelProvider = TopicAdminSettings.defaultGrpcTransportProviderBuilder().setChannelsPerCpu(1).build(); private HeaderProvider headerProvider = new NoHeaderProvider(); private HeaderProvider internalHeaderProvider = TopicAdminSettings.defaultApiClientHeaderProviderBuilder().build(); ExecutorProvider executorProvider = DEFAULT_EXECUTOR_PROVIDER; private CredentialsProvider credentialsProvider = TopicAdminSettings.defaultCredentialsProviderBuilder().build(); private ApiFunction messageTransform = new ApiFunction() { @Override public PubsubMessage apply(PubsubMessage input) { return input; } }; private Builder(String topic) { this.topicName = Preconditions.checkNotNull(topic); } /** * {@code ChannelProvider} to use to create Channels, which must point at Cloud Pub/Sub * endpoint. * *

For performance, this client benefits from having multiple underlying connections. See * {@link com.google.api.gax.grpc.InstantiatingGrpcChannelProvider.Builder#setPoolSize(int)}. */ public Builder setChannelProvider(TransportChannelProvider channelProvider) { this.channelProvider = Preconditions.checkNotNull(channelProvider); return this; } /** * Sets the static header provider. The header provider will be called during client * construction only once. The headers returned by the provider will be cached and supplied as * is for each request issued by the constructed client. Some reserved headers can be overridden * (e.g. Content-Type) or merged with the default value (e.g. User-Agent) by the underlying * transport layer. * * @param headerProvider the header provider * @return the builder */ @BetaApi public Builder setHeaderProvider(HeaderProvider headerProvider) { this.headerProvider = Preconditions.checkNotNull(headerProvider); return this; } /** * Sets the static header provider for getting internal (library-defined) headers. The header * provider will be called during client construction only once. The headers returned by the * provider will be cached and supplied as is for each request issued by the constructed client. * Some reserved headers can be overridden (e.g. Content-Type) or merged with the default value * (e.g. User-Agent) by the underlying transport layer. * * @param internalHeaderProvider the internal header provider * @return the builder */ Builder setInternalHeaderProvider(HeaderProvider internalHeaderProvider) { this.internalHeaderProvider = Preconditions.checkNotNull(internalHeaderProvider); return this; } /** {@code CredentialsProvider} to use to create Credentials to authenticate calls. */ public Builder setCredentialsProvider(CredentialsProvider credentialsProvider) { this.credentialsProvider = Preconditions.checkNotNull(credentialsProvider); return this; } // Batching options public Builder setBatchingSettings(BatchingSettings batchingSettings) { Preconditions.checkNotNull(batchingSettings); Preconditions.checkNotNull(batchingSettings.getElementCountThreshold()); Preconditions.checkArgument(batchingSettings.getElementCountThreshold() > 0); Preconditions.checkNotNull(batchingSettings.getRequestByteThreshold()); Preconditions.checkArgument(batchingSettings.getRequestByteThreshold() > 0); Preconditions.checkNotNull(batchingSettings.getDelayThreshold()); Preconditions.checkArgument(batchingSettings.getDelayThreshold().toMillis() > 0); this.batchingSettings = batchingSettings; return this; } /** Configures the Publisher's retry parameters. */ public Builder setRetrySettings(RetrySettings retrySettings) { Preconditions.checkArgument( retrySettings.getTotalTimeout().compareTo(MIN_TOTAL_TIMEOUT) >= 0); Preconditions.checkArgument( retrySettings.getInitialRpcTimeout().compareTo(MIN_RPC_TIMEOUT) >= 0); this.retrySettings = retrySettings; return this; } /** Gives the ability to set a custom executor to be used by the library. */ public Builder setExecutorProvider(ExecutorProvider executorProvider) { this.executorProvider = Preconditions.checkNotNull(executorProvider); return this; } /** * Gives the ability to set an {@link ApiFunction} that will transform the {@link PubsubMessage} * before it is sent */ @BetaApi public Builder setTransform(ApiFunction messageTransform) { this.messageTransform = Preconditions.checkNotNull(messageTransform, "The messageTransform cannnot be null."); return this; } public Publisher build() throws IOException { return new Publisher(this); } } private static class MessagesBatch { private List messages; private int batchedBytes; private final BatchingSettings batchingSettings; public MessagesBatch(BatchingSettings batchingSettings) { this.batchingSettings = batchingSettings; reset(); } private OutstandingBatch popOutstandingBatch() { OutstandingBatch batch = new OutstandingBatch(messages, batchedBytes); reset(); return batch; } private void reset() { messages = new LinkedList<>(); batchedBytes = 0; } private boolean isEmpty() { return messages.isEmpty(); } private int getBatchedBytes() { return batchedBytes; } private int getMessagesCount() { return messages.size(); } private boolean hasBatchingBytes() { return getMaxBatchBytes() > 0; } private long getMaxBatchBytes() { return batchingSettings.getRequestByteThreshold(); } private List add(OutstandingPublish outstandingPublish) { List batchesToSend = new ArrayList<>(); // Check if the next message makes the current batch exceed the max batch byte size. if (!isEmpty() && hasBatchingBytes() && getBatchedBytes() + outstandingPublish.messageSize >= getMaxBatchBytes()) { batchesToSend.add(popOutstandingBatch()); } messages.add(outstandingPublish); batchedBytes += outstandingPublish.messageSize; // Border case: If the message to send is greater or equals to the max batch size then send it // immediately. // Alternatively if after adding the message we have reached the batch max messages then we // have a batch to send. if ((hasBatchingBytes() && outstandingPublish.messageSize >= getMaxBatchBytes()) || getMessagesCount() == batchingSettings.getElementCountThreshold()) { batchesToSend.add(popOutstandingBatch()); } return batchesToSend; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy