com.spotify.google.cloud.pubsub.client.Puller Maven / Gradle / Ivy
/*
* Copyright (c) 2011-2015 Spotify AB
*
* 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.spotify.google.cloud.pubsub.client;
import com.google.common.util.concurrent.MoreExecutors;
import com.swrve.ratelimitedlogger.RateLimitedLog;
import org.joda.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
public class Puller implements Closeable {
/**
* A handler for received messages.
*/
public interface MessageHandler {
/**
* Called when a {@link Puller} receives a message.
*
* @param puller The {@link Puller}
* @param subscription The subscription that the message was received on.
* @param message The message.
* @param ackId The ack id.
* @return A future that should be completed with the ack id when the message has been consumed.
*/
CompletionStage handleMessage(Puller puller, String subscription, Message message, String ackId);
}
private static final int MAX_LOG_RATE = 3;
private static final Duration MAX_LOG_DURATION = Duration.millis(2000);
private static final Logger logger = LoggerFactory.getLogger(Puller.class);
private static final Logger LOG = RateLimitedLog.withRateLimit(logger)
.maxRate(MAX_LOG_RATE)
.every(MAX_LOG_DURATION)
.build();
private final ScheduledExecutorService scheduler =
MoreExecutors.getExitingScheduledExecutorService(new ScheduledThreadPoolExecutor(1));
private final Acker acker;
private final Pubsub pubsub;
private final String project;
private final String subscription;
private final MessageHandler handler;
private final int concurrency;
private final int batchSize;
private final int maxOutstandingMessages;
private final int maxAckQueueSize;
private final long pullIntervalMillis;
private final AtomicInteger outstandingRequests = new AtomicInteger();
private final AtomicInteger outstandingMessages = new AtomicInteger();
public Puller(final Builder builder) {
this.pubsub = Objects.requireNonNull(builder.pubsub, "pubsub");
this.project = Objects.requireNonNull(builder.project, "project");
this.subscription = Objects.requireNonNull(builder.subscription, "subscription");
this.handler = Objects.requireNonNull(builder.handler, "handler");
this.concurrency = builder.concurrency;
this.batchSize = builder.batchSize;
this.maxOutstandingMessages = builder.maxOutstandingMessages;
this.maxAckQueueSize = builder.maxAckQueueSize;
this.pullIntervalMillis = builder.pullIntervalMillis;
// Set up a batching acker for sending acks
this.acker = Acker.builder()
.pubsub(pubsub)
.project(project)
.subscription(subscription)
.batchSize(batchSize)
.concurrency(concurrency)
.queueSize(maxAckQueueSize)
.build();
// Start pulling
pull();
// Schedule pulling to compensate for failures and exceeding the outstanding message limit
scheduler.scheduleWithFixedDelay(this::pull, pullIntervalMillis, pullIntervalMillis, MILLISECONDS);
}
@Override
public void close() throws IOException {
scheduler.shutdownNow();
try {
scheduler.awaitTermination(30, SECONDS);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
public int maxAckQueueSize() {
return maxAckQueueSize;
}
public int maxOutstandingMessages() {
return maxOutstandingMessages;
}
public int outstandingMessages() {
return outstandingMessages.get();
}
public int concurrency() {
return concurrency;
}
public int outstandingRequests() {
return outstandingRequests.get();
}
public int batchSize() {
return batchSize;
}
public String subscription() {
return subscription;
}
public String project() {
return project;
}
public long pullIntervalMillis() {
return pullIntervalMillis;
}
private void pull() {
while (outstandingRequests.get() < concurrency &&
outstandingMessages.get() < maxOutstandingMessages) {
pullBatch();
}
}
private void pullBatch() {
outstandingRequests.incrementAndGet();
pubsub.pull(project, subscription, true, batchSize)
.whenComplete((messages, ex) -> {
outstandingRequests.decrementAndGet();
// Bail if pull failed
if (ex != null) {
LOG.error("Pull failed", ex);
return;
}
// Add entire batch to outstanding message count
outstandingMessages.addAndGet(messages.size());
// Call handler for each received message
for (final ReceivedMessage message : messages) {
final CompletionStage handlerFuture;
try {
handlerFuture = handler.handleMessage(this, subscription, message.message(), message.ackId());
} catch (Exception e) {
outstandingMessages.decrementAndGet();
LOG.error("Message handler threw exception", e);
continue;
}
if (handlerFuture == null) {
outstandingMessages.decrementAndGet();
LOG.error("Message handler returned null");
continue;
}
// Decrement the number of outstanding messages when handling is complete
handlerFuture.whenComplete((ignore, throwable) -> outstandingMessages.decrementAndGet());
// Ack when the message handling successfully completes
handlerFuture.thenAccept(acker::acknowledge).exceptionally(throwable -> {
if (!(throwable instanceof CancellationException)) {
LOG.error("Acking pubsub threw exception", throwable);
}
return null;
});
}
});
}
/**
* Create a builder that can be used to build a {@link Puller}.
*/
public static Builder builder() {
return new Builder();
}
/**
* A builder that can be used to build a {@link Puller}.
*/
public static class Builder {
private Pubsub pubsub;
private String project;
private String subscription;
private MessageHandler handler;
private int concurrency = 64;
private int batchSize = 1000;
private int maxOutstandingMessages = 64_000;
private int maxAckQueueSize = 10 * batchSize;
private long pullIntervalMillis = 1000;
/**
* Set the {@link Pubsub} client to use. The client will be closed when this {@link Puller} is closed.
*
* Note: The client should be configured to at least allow as many connections as the concurrency level of this
* {@link Puller}.
*/
public Builder pubsub(final Pubsub pubsub) {
this.pubsub = pubsub;
return this;
}
/**
* Set the Google Cloud project to pull from.
*/
public Builder project(final String project) {
this.project = project;
return this;
}
/**
* The subscription to pull from.
*/
public Builder subscription(final String subscription) {
this.subscription = subscription;
return this;
}
/**
* The handler to call for received messages.
*/
public Builder messageHandler(final MessageHandler messageHandler) {
this.handler = messageHandler;
return this;
}
/**
* Set the Google Cloud Pub/Sub request concurrency level. Default is {@code 64}.
*/
public Builder concurrency(final int concurrency) {
this.concurrency = concurrency;
return this;
}
/**
* Set the Google Cloud Pub/Sub pull batch size. Default is {@code 1000}.
*/
public Builder batchSize(final int batchSize) {
this.batchSize = batchSize;
return this;
}
/**
* Set the limit of outstanding messages pending handling. Pulling is throttled when this limit is hit. Default is
* {@code 64000}.
*/
public Builder maxOutstandingMessages(final int maxOutstandingMessages) {
this.maxOutstandingMessages = maxOutstandingMessages;
return this;
}
/**
* Set the max size for the queue of acks back to Google Cloud Pub/Sub. Default is {@code 10 * batchSize}.
*/
public Builder maxAckQueueSize(final int maxAckQueueSize) {
this.maxAckQueueSize = maxAckQueueSize;
return this;
}
/**
* Set the pull interval in millis. Default is {@code 1000} millis.
*/
public Builder pullIntervalMillis(final long pullIntervalMillis) {
this.pullIntervalMillis = pullIntervalMillis;
return this;
}
/**
* Build a {@link Puller}.
*/
public Puller build() {
return new Puller(this);
}
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy