2525import com .google .api .core .ApiFutures ;
2626import com .google .api .core .BetaApi ;
2727import com .google .api .core .SettableApiFuture ;
28+ import com .google .api .gax .rpc .ApiException ;
2829import com .google .api .gax .rpc .StatusCode .Code ;
2930import com .google .cloud .firestore .v1 .FirestoreSettings ;
3031import com .google .common .annotations .VisibleForTesting ;
3132import com .google .common .util .concurrent .MoreExecutors ;
33+ import java .util .ArrayList ;
34+ import java .util .List ;
3235import java .util .Map ;
3336import java .util .Set ;
3437import java .util .concurrent .ExecutionException ;
@@ -110,6 +113,14 @@ enum OperationType {
110113 */
111114 private static final int RATE_LIMITER_MULTIPLIER_MILLIS = 5 * 60 * 1000 ;
112115
116+ /**
117+ * The default maximum number of pending operations that can be enqueued onto a BulkWriter
118+ * instance. An operation is considered pending if BulkWriter has sent it via RPC and is awaiting
119+ * the result. BulkWriter buffers additional writes after this many pending operations in order to
120+ * avoiding going OOM.
121+ */
122+ private static final int DEFAULT_MAXIMUM_PENDING_OPERATIONS_COUNT = 500 ;
123+
113124 /**
114125 * The default jitter to apply to the exponential backoff used in retries. For example, a factor
115126 * of 0.3 means a 30% jitter is applied.
@@ -158,6 +169,26 @@ public boolean onError(BulkWriterException error) {
158169 @ GuardedBy ("lock" )
159170 private final RateLimiter rateLimiter ;
160171
172+ /**
173+ * The number of pending operations enqueued on this BulkWriter instance. An operation is
174+ * considered pending if BulkWriter has sent it via RPC and is awaiting the result.
175+ */
176+ @ GuardedBy ("lock" )
177+ private int pendingOpsCount = 0 ;
178+
179+ /**
180+ * An array containing buffered BulkWriter operations after the maximum number of pending
181+ * operations has been enqueued.
182+ */
183+ @ GuardedBy ("lock" )
184+ private final List <Runnable > bufferedOperations = new ArrayList <>();
185+
186+ /**
187+ * The maximum number of pending operations that can be enqueued onto this BulkWriter instance.
188+ * Once the this number of writes have been enqueued, subsequent writes are buffered.
189+ */
190+ private int maxPendingOpCount = DEFAULT_MAXIMUM_PENDING_OPERATIONS_COUNT ;
191+
161192 /**
162193 * The batch that is currently used to schedule operations. Once this batch reaches maximum
163194 * capacity, a new batch is created.
@@ -627,7 +658,7 @@ private ApiFuture<WriteResult> executeWrite(
627658 final DocumentReference documentReference ,
628659 final OperationType operationType ,
629660 final ApiFunction <BulkCommitBatch , ApiFuture <WriteResult >> enqueueOperationOnBatchCallback ) {
630- BulkWriterOperation operation =
661+ final BulkWriterOperation operation =
631662 new BulkWriterOperation (
632663 documentReference ,
633664 operationType ,
@@ -660,10 +691,73 @@ public ApiFuture<Boolean> apply(BulkWriterException e) {
660691 synchronized (lock ) {
661692 verifyNotClosedLocked ();
662693 writesEnqueued = true ;
663- sendOperationLocked (enqueueOperationOnBatchCallback , operation );
694+
695+ // Advance the lastOperation pointer. This ensures that lastOperation only completes when
696+ // both the previous and the current write complete.
697+ lastOperation =
698+ ApiFutures .transformAsync (
699+ lastOperation ,
700+ new ApiAsyncFunction <Void , Void >() {
701+ @ Override
702+ public ApiFuture <Void > apply (Void aVoid ) {
703+ return silenceFuture (operation .getFuture ());
704+ }
705+ },
706+ MoreExecutors .directExecutor ());
707+
708+ // Schedule the operation if the BulkWriter has fewer than the maximum number of allowed
709+ // pending operations, or add the operation to the buffer.
710+ if (pendingOpsCount < maxPendingOpCount ) {
711+ pendingOpsCount ++;
712+ sendOperationLocked (enqueueOperationOnBatchCallback , operation );
713+ } else {
714+ bufferedOperations .add (
715+ new Runnable () {
716+ @ Override
717+ public void run () {
718+ synchronized (lock ) {
719+ pendingOpsCount ++;
720+ sendOperationLocked (enqueueOperationOnBatchCallback , operation );
721+ }
722+ }
723+ });
724+ }
664725 }
665726
666- return operation .getFuture ();
727+ ApiFuture <WriteResult > processedOperationFuture =
728+ ApiFutures .transformAsync (
729+ operation .getFuture (),
730+ new ApiAsyncFunction <WriteResult , WriteResult >() {
731+ public ApiFuture <WriteResult > apply (WriteResult result ) throws Exception {
732+ pendingOpsCount --;
733+ processBufferedOperations ();
734+ return ApiFutures .immediateFuture (result );
735+ }
736+ },
737+ MoreExecutors .directExecutor ());
738+
739+ return ApiFutures .catchingAsync (
740+ processedOperationFuture ,
741+ ApiException .class ,
742+ new ApiAsyncFunction <ApiException , WriteResult >() {
743+ public ApiFuture <WriteResult > apply (ApiException e ) throws Exception {
744+ pendingOpsCount --;
745+ processBufferedOperations ();
746+ throw e ;
747+ }
748+ },
749+ MoreExecutors .directExecutor ());
750+ }
751+
752+ /**
753+ * Manages the pending operation counter and schedules the next BulkWriter operation if we're
754+ * under the maximum limit.
755+ */
756+ private void processBufferedOperations () {
757+ if (pendingOpsCount < maxPendingOpCount && bufferedOperations .size () > 0 ) {
758+ Runnable nextOp = bufferedOperations .remove (0 );
759+ nextOp .run ();
760+ }
667761 }
668762
669763 /**
@@ -927,6 +1021,16 @@ RateLimiter getRateLimiter() {
9271021 return rateLimiter ;
9281022 }
9291023
1024+ @ VisibleForTesting
1025+ int getBufferedOperationsCount () {
1026+ return bufferedOperations .size ();
1027+ }
1028+
1029+ @ VisibleForTesting
1030+ void setMaxPendingOpCount (int newMax ) {
1031+ maxPendingOpCount = newMax ;
1032+ }
1033+
9301034 /**
9311035 * Schedules the provided operations on the current BulkCommitBatch. Sends the BulkCommitBatch if
9321036 * it reaches maximum capacity.
@@ -946,17 +1050,6 @@ private void sendOperationLocked(
9461050 bulkCommitBatch .enqueueOperation (op );
9471051 enqueueOperationOnBatchCallback .apply (bulkCommitBatch );
9481052
949- lastOperation =
950- ApiFutures .transformAsync (
951- lastOperation ,
952- new ApiAsyncFunction <Void , Void >() {
953- @ Override
954- public ApiFuture <Void > apply (Void aVoid ) {
955- return silenceFuture (op .getFuture ());
956- }
957- },
958- MoreExecutors .directExecutor ());
959-
9601053 if (bulkCommitBatch .getMutationsSize () == maxBatchSize ) {
9611054 scheduleCurrentBatchLocked (/* flush= */ false );
9621055 }
0 commit comments