1616
1717package com .google .cloud .firestore ;
1818
19+ import static com .google .cloud .firestore .BulkWriterOperation .DEFAULT_BACKOFF_MAX_DELAY_MS ;
20+
1921import com .google .api .core .ApiAsyncFunction ;
2022import com .google .api .core .ApiFunction ;
2123import com .google .api .core .ApiFuture ;
@@ -108,6 +110,12 @@ enum OperationType {
108110 */
109111 private static final int RATE_LIMITER_MULTIPLIER_MILLIS = 5 * 60 * 1000 ;
110112
113+ /**
114+ * The default jitter to apply to the exponential backoff used in retries. For example, a factor
115+ * of 0.3 means a 30% jitter is applied.
116+ */
117+ static final double DEFAULT_JITTER_FACTOR = 0.3 ;
118+
111119 private static final WriteResultCallback DEFAULT_SUCCESS_LISTENER =
112120 new WriteResultCallback () {
113121 public void onResult (DocumentReference documentReference , WriteResult result ) {}
@@ -681,7 +689,7 @@ public ApiFuture<Void> flush() {
681689
682690 private ApiFuture <Void > flushLocked () {
683691 verifyNotClosedLocked ();
684- sendCurrentBatchLocked (/* flush= */ true );
692+ scheduleCurrentBatchLocked (/* flush= */ true );
685693 return lastOperation ;
686694 }
687695
@@ -846,37 +854,61 @@ public void addWriteErrorListener(@Nonnull Executor executor, WriteErrorCallback
846854 * This allows retries to resolve as part of a {@link BulkWriter#flush()} or {@link
847855 * BulkWriter#close()} call.
848856 */
849- private void sendCurrentBatchLocked (final boolean flush ) {
857+ private void scheduleCurrentBatchLocked (final boolean flush ) {
850858 if (bulkCommitBatch .getMutationsSize () == 0 ) return ;
851- BulkCommitBatch pendingBatch = bulkCommitBatch ;
859+
860+ final BulkCommitBatch pendingBatch = bulkCommitBatch ;
852861 bulkCommitBatch = new BulkCommitBatch (firestore , bulkWriterExecutor );
853862
854- // Send the batch if it is under the rate limit, or schedule another attempt after the
863+ // Use the write with the longest backoff duration when determining backoff.
864+ int highestBackoffDuration = 0 ;
865+ for (BulkWriterOperation op : pendingBatch .pendingOperations ) {
866+ if (op .getBackoffDuration () > highestBackoffDuration ) {
867+ highestBackoffDuration = op .getBackoffDuration ();
868+ }
869+ }
870+ final int backoffMsWithJitter = applyJitter (highestBackoffDuration );
871+
872+ bulkWriterExecutor .schedule (
873+ new Runnable () {
874+ @ Override
875+ public void run () {
876+ synchronized (lock ) {
877+ sendBatchLocked (pendingBatch , flush );
878+ }
879+ }
880+ },
881+ backoffMsWithJitter ,
882+ TimeUnit .MILLISECONDS );
883+ }
884+
885+ /** Sends the provided batch once the rate limiter does not require any delay. */
886+ private void sendBatchLocked (final BulkCommitBatch batch , final boolean flush ) {
887+ // Send the batch if it is does not require any delay, or schedule another attempt after the
855888 // appropriate timeout.
856- boolean underRateLimit = rateLimiter .tryMakeRequest (pendingBatch .getMutationsSize ());
889+ boolean underRateLimit = rateLimiter .tryMakeRequest (batch .getMutationsSize ());
857890 if (underRateLimit ) {
858- pendingBatch
891+ batch
859892 .bulkCommit ()
860893 .addListener (
861894 new Runnable () {
862895 @ Override
863896 public void run () {
864897 synchronized (lock ) {
865- sendCurrentBatchLocked (flush );
898+ scheduleCurrentBatchLocked (flush );
866899 }
867900 }
868901 },
869902 bulkWriterExecutor );
870-
871903 } else {
872- long delayMs = rateLimiter .getNextRequestDelayMs (pendingBatch .getMutationsSize ());
904+ long delayMs = rateLimiter .getNextRequestDelayMs (batch .getMutationsSize ());
873905 logger .log (Level .FINE , String .format ("Backing off for %d seconds" , delayMs / 1000 ));
874906 bulkWriterExecutor .schedule (
875907 new Runnable () {
876908 @ Override
877909 public void run () {
878910 synchronized (lock ) {
879- sendCurrentBatchLocked ( flush );
911+ sendBatchLocked ( batch , flush );
880912 }
881913 }
882914 },
@@ -905,7 +937,7 @@ private void sendOperationLocked(
905937 if (bulkCommitBatch .has (op .getDocumentReference ())) {
906938 // Create a new batch since the backend doesn't support batches with two writes to the same
907939 // document.
908- sendCurrentBatchLocked (/* flush= */ false );
940+ scheduleCurrentBatchLocked (/* flush= */ false );
909941 }
910942
911943 // Run the operation on the current batch and advance the `lastOperation` pointer. This
@@ -926,7 +958,7 @@ public ApiFuture<Void> apply(Void aVoid) {
926958 MoreExecutors .directExecutor ());
927959
928960 if (bulkCommitBatch .getMutationsSize () == maxBatchSize ) {
929- sendCurrentBatchLocked (/* flush= */ false );
961+ scheduleCurrentBatchLocked (/* flush= */ false );
930962 }
931963 }
932964
@@ -989,4 +1021,11 @@ public void onSuccess(T writeResult) {
9891021 MoreExecutors .directExecutor ());
9901022 return flushCallback ;
9911023 }
1024+
1025+ private int applyJitter (int backoffMs ) {
1026+ if (backoffMs == 0 ) return 0 ;
1027+ // Random value in [-0.3, 0.3].
1028+ double jitter = DEFAULT_JITTER_FACTOR * (Math .random () * 2 - 1 );
1029+ return (int ) Math .min (DEFAULT_BACKOFF_MAX_DELAY_MS , backoffMs + jitter * backoffMs );
1030+ }
9921031}
0 commit comments