17
17
import { User } from '../auth/user' ;
18
18
import { SnapshotVersion } from '../core/snapshot_version' ;
19
19
import { Transaction } from '../core/transaction' ;
20
- import { BatchId , OnlineState , TargetId } from '../core/types' ;
20
+ import { OnlineState , TargetId } from '../core/types' ;
21
21
import { LocalStore } from '../local/local_store' ;
22
22
import { QueryData , QueryPurpose } from '../local/query_data' ;
23
23
import { MutationResult } from '../model/mutation' ;
@@ -77,8 +77,24 @@ const MAX_PENDING_WRITES = 10;
77
77
* - acking mutations to the SyncEngine once they are accepted or rejected.
78
78
*/
79
79
export class RemoteStore implements TargetMetadataProvider {
80
- private pendingWrites : MutationBatch [ ] = [ ] ;
81
- private lastBatchSeen : BatchId = BATCHID_UNKNOWN ;
80
+ /**
81
+ * A list of up to MAX_PENDING_WRITES writes that we have fetched from the
82
+ * LocalStore via fillWritePipeline() and have or will send to the write
83
+ * stream.
84
+ *
85
+ * Whenever writePipeline.length > 0 the RemoteStore will attempt to start or
86
+ * restart the write stream. When the stream is established the writes in the
87
+ * pipeline will be sent in order.
88
+ *
89
+ * Writes remain in writePipeline until they are acknowledged by the backend
90
+ * and thus will automatically be re-sent if the stream is interrupted /
91
+ * restarted before they're acknowledged.
92
+ *
93
+ * Write responses from the backend are linked to their originating request
94
+ * purely based on order, and so we can just shift() writes from the front of
95
+ * the writePipeline as we receive responses.
96
+ */
97
+ private writePipeline : MutationBatch [ ] = [ ] ;
82
98
83
99
/**
84
100
* A mapping of watched targets that the client cares about tracking and the
@@ -177,7 +193,16 @@ export class RemoteStore implements TargetMetadataProvider {
177
193
this . writeStream . stop ( ) ;
178
194
179
195
this . cleanUpWatchStreamState ( ) ;
180
- this . cleanUpWriteStreamState ( ) ;
196
+
197
+ log . debug (
198
+ LOG_TAG ,
199
+ 'Stopping write stream with ' +
200
+ this . writePipeline . length +
201
+ ' pending writes'
202
+ ) ;
203
+ // TODO(mikelehen): We only actually need to clear the write pipeline if
204
+ // this is being called as part of handleUserChange(). Consider reworking.
205
+ this . writePipeline = [ ] ;
181
206
182
207
this . writeStream = null ;
183
208
this . watchStream = null ;
@@ -439,72 +464,61 @@ export class RemoteStore implements TargetMetadataProvider {
439
464
return promiseChain ;
440
465
}
441
466
442
- cleanUpWriteStreamState ( ) : void {
443
- this . lastBatchSeen = BATCHID_UNKNOWN ;
444
- log . debug (
445
- LOG_TAG ,
446
- 'Stopping write stream with ' +
447
- this . pendingWrites . length +
448
- ' pending writes'
449
- ) ;
450
- this . pendingWrites = [ ] ;
451
- }
452
-
453
467
/**
454
- * Notifies that there are new mutations to process in the queue. This is
455
- * typically called by SyncEngine after it has sent mutations to LocalStore.
468
+ * Attempts to fill our write pipeline with writes from the LocalStore.
469
+ *
470
+ * Called internally to bootstrap or refill the write pipeline and by
471
+ * SyncEngine whenever there are new mutations to process.
472
+ *
473
+ * Starts the write stream if necessary.
456
474
*/
457
475
async fillWritePipeline ( ) : Promise < void > {
458
- if ( this . canWriteMutations ( ) ) {
476
+ if ( this . canAddToWritePipeline ( ) ) {
477
+ const lastBatchIdRetrieved =
478
+ this . writePipeline . length > 0
479
+ ? this . writePipeline [ this . writePipeline . length - 1 ] . batchId
480
+ : BATCHID_UNKNOWN ;
459
481
return this . localStore
460
- . nextMutationBatch ( this . lastBatchSeen )
482
+ . nextMutationBatch ( lastBatchIdRetrieved )
461
483
. then ( batch => {
462
484
if ( batch === null ) {
463
- if ( this . pendingWrites . length === 0 ) {
485
+ if ( this . writePipeline . length === 0 ) {
464
486
this . writeStream . markIdle ( ) ;
465
487
}
466
488
} else {
467
- this . commit ( batch ) ;
489
+ this . addToWritePipeline ( batch ) ;
468
490
return this . fillWritePipeline ( ) ;
469
491
}
470
492
} ) ;
471
493
}
472
494
}
473
495
474
496
/**
475
- * Returns true if the backend can accept additional write requests.
476
- *
477
- * When sending mutations to the write stream (e.g. in fillWritePipeline),
478
- * call this method first to check if more mutations can be sent.
479
- *
480
- * Currently the only thing that can prevent the backend from accepting
481
- * write requests is if there are too many requests already outstanding. As
482
- * writes complete the backend will be able to accept more.
497
+ * Returns true if we can add to the write pipeline (i.e. it is not full and
498
+ * the network is enabled).
483
499
*/
484
- canWriteMutations ( ) : boolean {
500
+ private canAddToWritePipeline ( ) : boolean {
485
501
return (
486
- this . isNetworkEnabled ( ) && this . pendingWrites . length < MAX_PENDING_WRITES
502
+ this . isNetworkEnabled ( ) && this . writePipeline . length < MAX_PENDING_WRITES
487
503
) ;
488
504
}
489
505
490
506
// For testing
491
507
outstandingWrites ( ) : number {
492
- return this . pendingWrites . length ;
508
+ return this . writePipeline . length ;
493
509
}
494
510
495
511
/**
496
- * Given mutations to commit, actually commits them to the Datastore. Note
497
- * that this does *not* return a Promise specifically because the AsyncQueue
498
- * should not block operations for this .
512
+ * Queues additional writes to be sent to the write stream, sending them
513
+ * immediately if the write stream is established, else starting the write
514
+ * stream if it is not yet started .
499
515
*/
500
- private commit ( batch : MutationBatch ) : void {
516
+ private addToWritePipeline ( batch : MutationBatch ) : void {
501
517
assert (
502
- this . canWriteMutations ( ) ,
503
- "commit called when batches can't be written"
518
+ this . canAddToWritePipeline ( ) ,
519
+ 'addToWritePipeline called when pipeline is full'
504
520
) ;
505
- this . lastBatchSeen = batch . batchId ;
506
-
507
- this . pendingWrites . push ( batch ) ;
521
+ this . writePipeline . push ( batch ) ;
508
522
509
523
if ( this . shouldStartWriteStream ( ) ) {
510
524
this . startWriteStream ( ) ;
@@ -517,7 +531,7 @@ export class RemoteStore implements TargetMetadataProvider {
517
531
return (
518
532
this . isNetworkEnabled ( ) &&
519
533
! this . writeStream . isStarted ( ) &&
520
- this . pendingWrites . length > 0
534
+ this . writePipeline . length > 0
521
535
) ;
522
536
}
523
537
@@ -543,20 +557,8 @@ export class RemoteStore implements TargetMetadataProvider {
543
557
return this . localStore
544
558
. setLastStreamToken ( this . writeStream . lastStreamToken )
545
559
. then ( ( ) => {
546
- // Drain any pending writes.
547
- //
548
- // Note that at this point pendingWrites contains mutations that
549
- // have already been accepted by fillWritePipeline/commitBatch. If
550
- // the pipeline is full, canWriteMutations will be false, despite
551
- // the fact that we actually need to send mutations over.
552
- //
553
- // This also means that this method indirectly respects the limits
554
- // imposed by canWriteMutations since writes can't be added to the
555
- // pendingWrites array when canWriteMutations is false. If the
556
- // limits imposed by canWriteMutations actually protect us from
557
- // DOSing ourselves then those limits won't be exceeded here and
558
- // we'll continue to make progress.
559
- for ( const batch of this . pendingWrites ) {
560
+ // Send the write pipeline now that the stream is established.
561
+ for ( const batch of this . writePipeline ) {
560
562
this . writeStream . writeMutations ( batch . mutations ) ;
561
563
}
562
564
} ) ;
@@ -567,12 +569,12 @@ export class RemoteStore implements TargetMetadataProvider {
567
569
results : MutationResult [ ]
568
570
) : Promise < void > {
569
571
// This is a response to a write containing mutations and should be
570
- // correlated to the first pending write.
572
+ // correlated to the first write in our write pipeline .
571
573
assert (
572
- this . pendingWrites . length > 0 ,
573
- 'Got result for empty pending writes '
574
+ this . writePipeline . length > 0 ,
575
+ 'Got result for empty write pipeline '
574
576
) ;
575
- const batch = this . pendingWrites . shift ( ) ! ;
577
+ const batch = this . writePipeline . shift ( ) ! ;
576
578
const success = MutationBatchResult . from (
577
579
batch ,
578
580
commitVersion ,
@@ -594,11 +596,7 @@ export class RemoteStore implements TargetMetadataProvider {
594
596
595
597
// If the write stream closed due to an error, invoke the error callbacks if
596
598
// there are pending writes.
597
- if ( error && this . pendingWrites . length > 0 ) {
598
- assert (
599
- ! ! error ,
600
- 'We have pending writes, but the write stream closed without an error'
601
- ) ;
599
+ if ( error && this . writePipeline . length > 0 ) {
602
600
// A promise that is resolved after we processed the error
603
601
let errorHandling : Promise < void > ;
604
602
if ( this . writeStream . handshakeComplete ) {
@@ -644,7 +642,7 @@ export class RemoteStore implements TargetMetadataProvider {
644
642
if ( isPermanentError ( error . code ) ) {
645
643
// This was a permanent error, the request itself was the problem
646
644
// so it's not going to succeed if we resend it.
647
- const batch = this . pendingWrites . shift ( ) ! ;
645
+ const batch = this . writePipeline . shift ( ) ! ;
648
646
649
647
// In this case it's also unlikely that the server itself is melting
650
648
// down -- this was just a bad request so inhibit backoff on the next
0 commit comments