Skip to content

Commit fdd9434

Browse files
feat: Add flow control support to publisher (#119)
* feat: Add flow control support to publisher
1 parent e7c007b commit fdd9434

File tree

3 files changed

+364
-3
lines changed

3 files changed

+364
-3
lines changed

google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/v1/Publisher.java

+170-3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import com.google.api.core.BetaApi;
2626
import com.google.api.core.SettableApiFuture;
2727
import com.google.api.gax.batching.BatchingSettings;
28+
import com.google.api.gax.batching.FlowControlSettings;
29+
import com.google.api.gax.batching.FlowController;
2830
import com.google.api.gax.core.BackgroundResource;
2931
import com.google.api.gax.core.BackgroundResourceAggregation;
3032
import com.google.api.gax.core.CredentialsProvider;
@@ -55,6 +57,7 @@
5557
import java.util.List;
5658
import java.util.Map;
5759
import java.util.concurrent.Callable;
60+
import java.util.concurrent.CountDownLatch;
5861
import java.util.concurrent.ScheduledExecutorService;
5962
import java.util.concurrent.ScheduledFuture;
6063
import java.util.concurrent.TimeUnit;
@@ -108,6 +111,8 @@ public class Publisher {
108111
private ScheduledFuture<?> currentAlarmFuture;
109112
private final ApiFunction<PubsubMessage, PubsubMessage> messageTransform;
110113

114+
private MessageFlowController flowController = null;
115+
111116
/** The maximum number of messages in one request. Defined by the API. */
112117
public static long getApiMaxRequestElementCount() {
113118
return 1000L;
@@ -122,6 +127,16 @@ private Publisher(Builder builder) throws IOException {
122127
topicName = builder.topicName;
123128

124129
this.batchingSettings = builder.batchingSettings;
130+
FlowControlSettings flowControl = this.batchingSettings.getFlowControlSettings();
131+
if (flowControl != null
132+
&& flowControl.getLimitExceededBehavior() != FlowController.LimitExceededBehavior.Ignore) {
133+
this.flowController =
134+
new MessageFlowController(
135+
flowControl.getMaxOutstandingElementCount(),
136+
flowControl.getMaxOutstandingRequestBytes(),
137+
flowControl.getLimitExceededBehavior());
138+
}
139+
125140
this.enableMessageOrdering = builder.enableMessageOrdering;
126141
this.messageTransform = builder.messageTransform;
127142

@@ -221,6 +236,19 @@ public ApiFuture<String> publish(PubsubMessage message) {
221236

222237
final OutstandingPublish outstandingPublish =
223238
new OutstandingPublish(messageTransform.apply(message));
239+
240+
if (flowController != null) {
241+
try {
242+
flowController.acquire(outstandingPublish.messageSize);
243+
} catch (FlowController.FlowControlException e) {
244+
if (!orderingKey.isEmpty()) {
245+
sequentialExecutor.stopPublish(orderingKey);
246+
}
247+
outstandingPublish.publishResult.setException(e);
248+
return outstandingPublish.publishResult;
249+
}
250+
}
251+
224252
List<OutstandingBatch> batchesToSend;
225253
messagesBatchLock.lock();
226254
try {
@@ -454,7 +482,7 @@ public ApiFuture<PublishResponse> call() {
454482
ApiFutures.addCallback(future, futureCallback, directExecutor());
455483
}
456484

457-
private static final class OutstandingBatch {
485+
private final class OutstandingBatch {
458486
final List<OutstandingPublish> outstandingPublishes;
459487
final long creationTime;
460488
int attempt;
@@ -484,14 +512,21 @@ private List<PubsubMessage> getMessages() {
484512

485513
private void onFailure(Throwable t) {
486514
for (OutstandingPublish outstandingPublish : outstandingPublishes) {
515+
if (flowController != null) {
516+
flowController.release(outstandingPublish.messageSize);
517+
}
487518
outstandingPublish.publishResult.setException(t);
488519
}
489520
}
490521

491522
private void onSuccess(Iterable<String> results) {
492523
Iterator<OutstandingPublish> messagesResultsIt = outstandingPublishes.iterator();
493524
for (String messageId : results) {
494-
messagesResultsIt.next().publishResult.set(messageId);
525+
OutstandingPublish nextPublish = messagesResultsIt.next();
526+
if (flowController != null) {
527+
flowController.release(nextPublish.messageSize);
528+
}
529+
nextPublish.publishResult.set(messageId);
495530
}
496531
}
497532
}
@@ -602,6 +637,10 @@ public static final class Builder {
602637
.setDelayThreshold(DEFAULT_DELAY_THRESHOLD)
603638
.setRequestByteThreshold(DEFAULT_REQUEST_BYTES_THRESHOLD)
604639
.setElementCountThreshold(DEFAULT_ELEMENT_COUNT_THRESHOLD)
640+
.setFlowControlSettings(
641+
FlowControlSettings.newBuilder()
642+
.setLimitExceededBehavior(FlowController.LimitExceededBehavior.Ignore)
643+
.build())
605644
.build();
606645
static final RetrySettings DEFAULT_RETRY_SETTINGS =
607646
RetrySettings.newBuilder()
@@ -759,7 +798,135 @@ public Publisher build() throws IOException {
759798
}
760799
}
761800

762-
private static class MessagesBatch {
801+
private static class MessageFlowController {
802+
private final Lock lock;
803+
private final Long messageLimit;
804+
private final Long byteLimit;
805+
private final FlowController.LimitExceededBehavior limitBehavior;
806+
807+
private Long outstandingMessages;
808+
private Long outstandingBytes;
809+
private LinkedList<CountDownLatch> awaitingMessageAcquires;
810+
private LinkedList<CountDownLatch> awaitingBytesAcquires;
811+
812+
MessageFlowController(
813+
Long messageLimit, Long byteLimit, FlowController.LimitExceededBehavior limitBehavior) {
814+
this.messageLimit = messageLimit;
815+
this.byteLimit = byteLimit;
816+
this.limitBehavior = limitBehavior;
817+
this.lock = new ReentrantLock();
818+
819+
this.outstandingMessages = 0L;
820+
this.outstandingBytes = 0L;
821+
822+
this.awaitingMessageAcquires = new LinkedList<CountDownLatch>();
823+
this.awaitingBytesAcquires = new LinkedList<CountDownLatch>();
824+
}
825+
826+
void acquire(long messageSize) throws FlowController.FlowControlException {
827+
lock.lock();
828+
try {
829+
if (outstandingMessages >= messageLimit
830+
&& limitBehavior == FlowController.LimitExceededBehavior.ThrowException) {
831+
throw new FlowController.MaxOutstandingElementCountReachedException(messageLimit);
832+
}
833+
if (outstandingBytes + messageSize >= byteLimit
834+
&& limitBehavior == FlowController.LimitExceededBehavior.ThrowException) {
835+
throw new FlowController.MaxOutstandingRequestBytesReachedException(byteLimit);
836+
}
837+
838+
// We can acquire or we should wait until we can acquire.
839+
// Start by acquiring a slot for a message.
840+
CountDownLatch messageWaiter = null;
841+
while (outstandingMessages >= messageLimit) {
842+
if (messageWaiter == null) {
843+
// This message gets added to the back of the line.
844+
messageWaiter = new CountDownLatch(1);
845+
awaitingMessageAcquires.addLast(messageWaiter);
846+
} else {
847+
// This message already in line stays at the head of the line.
848+
messageWaiter = new CountDownLatch(1);
849+
awaitingMessageAcquires.set(0, messageWaiter);
850+
}
851+
lock.unlock();
852+
try {
853+
messageWaiter.await();
854+
} catch (InterruptedException e) {
855+
logger.log(Level.WARNING, "Interrupted while waiting to acquire flow control tokens");
856+
}
857+
lock.lock();
858+
}
859+
++outstandingMessages;
860+
if (messageWaiter != null) {
861+
awaitingMessageAcquires.removeFirst();
862+
}
863+
864+
// There may be some surplus messages left; let the next message waiting for a token have
865+
// one.
866+
if (!awaitingMessageAcquires.isEmpty() && outstandingMessages < messageLimit) {
867+
awaitingMessageAcquires.getFirst().countDown();
868+
}
869+
870+
// Now acquire space for bytes.
871+
CountDownLatch bytesWaiter = null;
872+
Long bytesRemaining = messageSize;
873+
while (outstandingBytes + bytesRemaining >= byteLimit) {
874+
// Take what is available.
875+
Long available = byteLimit - outstandingBytes;
876+
bytesRemaining -= available;
877+
outstandingBytes = byteLimit;
878+
if (bytesWaiter == null) {
879+
// This message gets added to the back of the line.
880+
bytesWaiter = new CountDownLatch(1);
881+
awaitingBytesAcquires.addLast(bytesWaiter);
882+
} else {
883+
// This message already in line stays at the head of the line.
884+
bytesWaiter = new CountDownLatch(1);
885+
awaitingBytesAcquires.set(0, bytesWaiter);
886+
}
887+
lock.unlock();
888+
try {
889+
bytesWaiter.await();
890+
} catch (InterruptedException e) {
891+
logger.log(Level.WARNING, "Interrupted while waiting to acquire flow control tokens");
892+
}
893+
lock.lock();
894+
}
895+
896+
outstandingBytes += bytesRemaining;
897+
if (bytesWaiter != null) {
898+
awaitingBytesAcquires.removeFirst();
899+
}
900+
// There may be some surplus bytes left; let the next message waiting for bytes have some.
901+
if (!awaitingBytesAcquires.isEmpty() && outstandingBytes < byteLimit) {
902+
awaitingBytesAcquires.getFirst().countDown();
903+
}
904+
} finally {
905+
lock.unlock();
906+
}
907+
}
908+
909+
private void notifyNextAcquires() {
910+
if (!awaitingMessageAcquires.isEmpty()) {
911+
CountDownLatch awaitingAcquire = awaitingMessageAcquires.getFirst();
912+
awaitingAcquire.countDown();
913+
}
914+
if (!awaitingBytesAcquires.isEmpty()) {
915+
CountDownLatch awaitingAcquire = awaitingBytesAcquires.getFirst();
916+
awaitingAcquire.countDown();
917+
}
918+
}
919+
920+
void release(long messageSize) {
921+
lock.lock();
922+
--outstandingMessages;
923+
outstandingBytes -= messageSize;
924+
notifyNextAcquires();
925+
lock.unlock();
926+
}
927+
}
928+
929+
private class MessagesBatch {
763930
private List<OutstandingPublish> messages;
764931
private int batchedBytes;
765932
private String orderingKey;

google-cloud-pubsub/src/main/java/com/google/cloud/pubsub/v1/SequentialExecutorService.java

+4
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,10 @@ void resumePublish(String key) {
247247
keysWithErrors.remove(key);
248248
}
249249

250+
void stopPublish(String key) {
251+
keysWithErrors.add(key);
252+
}
253+
250254
/** Cancels every task in the queue associated with {@code key}. */
251255
private void cancelQueuedTasks(final String key, Throwable e) {
252256
keysWithErrors.add(key);

0 commit comments

Comments
 (0)