40
40
import io .grpc .Status ;
41
41
import io .grpc .Status .Code ;
42
42
import java .util .ArrayDeque ;
43
+ import java .util .Deque ;
43
44
import java .util .HashMap ;
44
45
import java .util .List ;
45
46
import java .util .Map ;
46
47
import java .util .Map .Entry ;
47
- import java .util .Queue ;
48
48
49
49
/**
50
50
* RemoteStore handles all interaction with the backend through a simple, clean interface. This
@@ -129,9 +129,21 @@ public interface RemoteStoreCallback {
129
129
@ Nullable private WriteStream writeStream ;
130
130
@ Nullable private WatchChangeAggregator watchChangeAggregator ;
131
131
132
- private int lastBatchSeen ;
133
-
134
- private final Queue <MutationBatch > pendingWrites ;
132
+ /**
133
+ * A list of up to MAX_PENDING_WRITES writes that we have fetched from the LocalStore via
134
+ * fillWritePipeline() and have or will send to the write stream.
135
+ *
136
+ * <p>Whenever writePipeline.length > 0 the RemoteStore will attempt to start or restart the write
137
+ * stream. When the stream is established the writes in the pipeline will be sent in order.
138
+ *
139
+ * <p>Writes remain in writePipeline until they are acknowledged by the backend and thus will
140
+ * automatically be re-sent if the stream is interrupted / restarted before they're acknowledged.
141
+ *
142
+ * <p>Write responses from the backend are linked to their originating request purely based on
143
+ * order, and so we can just poll() writes from the front of the writePipeline as we receive
144
+ * responses.
145
+ */
146
+ private final Deque <MutationBatch > writePipeline ;
135
147
136
148
public RemoteStore (
137
149
RemoteStoreCallback remoteStoreCallback ,
@@ -143,8 +155,7 @@ public RemoteStore(
143
155
this .datastore = datastore ;
144
156
145
157
listenTargets = new HashMap <>();
146
- lastBatchSeen = MutationBatch .UNKNOWN ;
147
- pendingWrites = new ArrayDeque <>();
158
+ writePipeline = new ArrayDeque <>();
148
159
149
160
onlineStateTracker =
150
161
new OnlineStateTracker (workerQueue , remoteStoreCallback ::handleOnlineStateChange );
@@ -188,7 +199,10 @@ private void disableNetworkInternal() {
188
199
writeStream .stop ();
189
200
190
201
cleanUpWatchStreamState ();
191
- cleanUpWriteStreamState ();
202
+ if (!writePipeline .isEmpty ()) {
203
+ Logger .debug (LOG_TAG , "Stopping write stream with %d pending writes" , writePipeline .size ());
204
+ writePipeline .clear ();
205
+ }
192
206
193
207
writeStream = null ;
194
208
watchStream = null ;
@@ -290,7 +304,7 @@ private void sendUnwatchRequest(int targetId) {
290
304
* pending writes.
291
305
*/
292
306
private boolean shouldStartWriteStream () {
293
- return isNetworkEnabled () && !writeStream .isStarted () && !pendingWrites .isEmpty ();
307
+ return isNetworkEnabled () && !writeStream .isStarted () && !writePipeline .isEmpty ();
294
308
}
295
309
296
310
/**
@@ -301,12 +315,6 @@ private boolean shouldStartWatchStream() {
301
315
return isNetworkEnabled () && !watchStream .isStarted () && !listenTargets .isEmpty ();
302
316
}
303
317
304
- private void cleanUpWriteStreamState () {
305
- lastBatchSeen = MutationBatch .UNKNOWN ;
306
- Logger .debug (LOG_TAG , "Stopping write stream with " + pendingWrites .size () + " pending writes" );
307
- pendingWrites .clear ();
308
- }
309
-
310
318
private void cleanUpWatchStreamState () {
311
319
// If the connection is closed then we'll never get a snapshot version for the accumulated
312
320
// changes and so we'll never be able to complete the batch. When we start up again the server
@@ -488,49 +496,46 @@ private void processTargetError(WatchTargetChange targetChange) {
488
496
// Write Stream
489
497
490
498
/**
491
- * Notifies that there are new mutations to process in the queue. This is typically called by
492
- * SyncEngine after it has sent mutations to LocalStore.
499
+ * Attempts to fill our write pipeline with writes from the LocalStore.
493
500
*
494
- * <p>In response the remote store will pull mutations from the local store until the datastore
495
- * instance reports that it cannot accept further in-progress writes. This mechanism serves to
496
- * maintain a pipeline of in-flight requests between the Datastore and the server that applies
497
- * them .
501
+ * <p>Called internally to bootstrap or refill the write pipeline and by SyncEngine whenever there
502
+ * are new mutations to process.
503
+ *
504
+ * <p>Starts the write stream if necessary .
498
505
*/
499
506
public void fillWritePipeline () {
500
507
if (isNetworkEnabled ()) {
501
- while (canWriteMutations ()) {
502
- MutationBatch batch = localStore .getNextMutationBatch (lastBatchSeen );
508
+ int lastBatchIdRetrieved =
509
+ writePipeline .isEmpty () ? MutationBatch .UNKNOWN : writePipeline .getLast ().getBatchId ();
510
+ while (canAddToWritePipeline ()) {
511
+ MutationBatch batch = localStore .getNextMutationBatch (lastBatchIdRetrieved );
503
512
if (batch == null ) {
504
513
break ;
505
514
}
506
- commitBatch (batch );
515
+ addToWritePipeline (batch );
516
+ lastBatchIdRetrieved = batch .getBatchId ();
507
517
}
508
518
509
- if (pendingWrites .isEmpty ()) {
510
- writeStream .markIdle ();
511
- }
519
+ writeStream .markIdle ();
512
520
}
513
521
}
514
522
515
523
/**
516
- * Returns true if the backend can accept additional write requests.
517
- *
518
- * <p>When sending mutations to the write stream (e.g. in fillWritePipeline()), call this method
519
- * first to check if more mutations can be sent.
520
- *
521
- * <p>Currently the only thing that can prevent the backend from accepting write requests is if
522
- * there are too many requests already outstanding. As writes complete the backend will be able to
523
- * accept more.
524
+ * Returns true if we can add to the write pipeline (i.e. it is not full and the network is
525
+ * enabled).
524
526
*/
525
- private boolean canWriteMutations () {
526
- return isNetworkEnabled () && pendingWrites .size () < MAX_PENDING_WRITES ;
527
+ private boolean canAddToWritePipeline () {
528
+ return isNetworkEnabled () && writePipeline .size () < MAX_PENDING_WRITES ;
527
529
}
528
530
529
- private void commitBatch (MutationBatch mutationBatch ) {
530
- hardAssert (canWriteMutations (), "commitBatch called when mutations can't be written" );
531
- lastBatchSeen = mutationBatch .getBatchId ();
531
+ /**
532
+ * Queues additional writes to be sent to the write stream, sending them immediately if the write
533
+ * stream is established, else starting the write stream if it is not yet started.
534
+ */
535
+ private void addToWritePipeline (MutationBatch mutationBatch ) {
536
+ hardAssert (canAddToWritePipeline (), "addToWritePipeline called when pipeline is full" );
532
537
533
- pendingWrites .add (mutationBatch );
538
+ writePipeline .add (mutationBatch );
534
539
535
540
if (shouldStartWriteStream ()) {
536
541
startWriteStream ();
@@ -576,19 +581,8 @@ private void handleWriteStreamHandshakeComplete() {
576
581
// Record the stream token.
577
582
localStore .setLastStreamToken (writeStream .getLastStreamToken ());
578
583
579
- /*
580
- * Drain pending writes.
581
- *
582
- * Note that at this point pendingWrites contains mutations that have already been accepted by
583
- * fillWritePipeline/commitBatch. If the pipeline is full, canWriteMutations will be false,
584
- * despite the fact that we actually need to send mutations over.
585
- *
586
- * This also means that this method indirectly respects the limits imposed by canWriteMutations
587
- * since writes can't be added to the pendingWrites array when canWriteMutations is false. If
588
- * the limits imposed by canWriteMutations actually protect us from DOSing ourselves then those
589
- * limits won't be exceeded here and we'll continue to make progress.
590
- */
591
- for (MutationBatch batch : pendingWrites ) {
584
+ // Send the write pipeline now that stream is established.
585
+ for (MutationBatch batch : writePipeline ) {
592
586
writeStream .writeMutations (batch .getMutations ());
593
587
}
594
588
}
@@ -599,8 +593,8 @@ private void handleWriteStreamHandshakeComplete() {
599
593
private void handleWriteStreamMutationResults (
600
594
SnapshotVersion commitVersion , List <MutationResult > results ) {
601
595
// This is a response to a write containing mutations and should be correlated to the first
602
- // pending write.
603
- MutationBatch batch = pendingWrites .poll ();
596
+ // write in our write pipeline .
597
+ MutationBatch batch = writePipeline .poll ();
604
598
605
599
MutationBatchResult mutationBatchResult =
606
600
MutationBatchResult .create (batch , commitVersion , results , writeStream .getLastStreamToken ());
@@ -617,7 +611,7 @@ private void handleWriteStreamClose(Status status) {
617
611
618
612
// If the write stream closed due to an error, invoke the error callbacks if there are pending
619
613
// writes.
620
- if (!status .isOk () && !pendingWrites .isEmpty ()) {
614
+ if (!status .isOk () && !writePipeline .isEmpty ()) {
621
615
// TODO: handle UNAUTHENTICATED status, see go/firestore-client-errors
622
616
if (writeStream .isHandshakeComplete ()) {
623
617
// This error affects the actual writes
@@ -658,7 +652,7 @@ private void handleWriteError(Status status) {
658
652
if (Datastore .isPermanentWriteError (status )) {
659
653
// If this was a permanent error, the request itself was the problem so it's not going
660
654
// to succeed if we resend it.
661
- MutationBatch batch = pendingWrites .poll ();
655
+ MutationBatch batch = writePipeline .poll ();
662
656
663
657
// In this case it's also unlikely that the server itself is melting down -- this was
664
658
// just a bad request, so inhibit backoff on the next restart
0 commit comments