@@ -41,6 +41,17 @@ export interface MongoSyncBucketStorageOptions {
41
41
checksumOptions ?: MongoChecksumOptions ;
42
42
}
43
43
44
+ /**
45
+ * Only keep checkpoints around for a minute, before fetching a fresh one.
46
+ *
47
+ * The reason is that we keep a MongoDB snapshot reference (clusterTime) with the checkpoint,
48
+ * and they expire after 5 minutes by default. This is an issue if the checkpoint stream is idle,
49
+ * but new clients connect and use an outdated checkpoint snapshot for parameter queries.
50
+ *
51
+ * These will be filtered out for existing clients, so should not create significant overhead.
52
+ */
53
+ const CHECKPOINT_TIMEOUT_MS = 60_000 ;
54
+
44
55
export class MongoSyncBucketStorage
45
56
extends BaseObserver < storage . SyncRulesBucketStorageListener >
46
57
implements storage . SyncRulesBucketStorage
@@ -680,25 +691,45 @@ export class MongoSyncBucketStorage
680
691
681
692
// We only watch changes to the active sync rules.
682
693
// If it changes to inactive, we abort and restart with the new sync rules.
683
- let lastOp : storage . ReplicationCheckpoint | null = null ;
694
+ try {
695
+ while ( true ) {
696
+ // If the stream is idle, we wait a max of a minute (CHECKPOINT_TIMEOUT_MS)
697
+ // before we get another checkpoint, to avoid stale checkpoint snapshots.
698
+ const timeout = timers
699
+ . setTimeout ( CHECKPOINT_TIMEOUT_MS , { done : false } , { signal } )
700
+ . catch ( ( ) => ( { done : true } ) ) ;
701
+ try {
702
+ const result = await Promise . race ( [ stream . next ( ) , timeout ] ) ;
703
+ if ( result . done ) {
704
+ break ;
705
+ }
706
+ } catch ( e ) {
707
+ if ( e . name == 'AbortError' ) {
708
+ break ;
709
+ }
710
+ throw e ;
711
+ }
684
712
685
- for await ( const _ of stream ) {
686
- if ( signal . aborted ) {
687
- break ;
688
- }
713
+ if ( signal . aborted ) {
714
+ // Would likely have been caught by the signal on the timeout or the upstream stream, but we check here anyway
715
+ break ;
716
+ }
689
717
690
- const op = await this . getCheckpointInternal ( ) ;
691
- if ( op == null ) {
692
- // Sync rules have changed - abort and restart.
693
- // We do a soft close of the stream here - no error
694
- break ;
695
- }
718
+ const op = await this . getCheckpointInternal ( ) ;
719
+ if ( op == null ) {
720
+ // Sync rules have changed - abort and restart.
721
+ // We do a soft close of the stream here - no error
722
+ break ;
723
+ }
696
724
697
- // Check for LSN / checkpoint changes - ignore other metadata changes
698
- if ( lastOp == null || op . lsn != lastOp . lsn || op . checkpoint != lastOp . checkpoint ) {
699
- lastOp = op ;
725
+ // Previously, we only yielded when the checkpoint or lsn changed.
726
+ // However, we always want to use the latest snapshotTime, so we skip that filtering here.
727
+ // That filtering could be added in the per-user streams if needed, but in general the capped collection
728
+ // should already only contain useful changes in most cases.
700
729
yield op ;
701
730
}
731
+ } finally {
732
+ await stream . return ( null ) ;
702
733
}
703
734
}
704
735
0 commit comments