@@ -41,6 +41,17 @@ export interface MongoSyncBucketStorageOptions {
4141 checksumOptions ?: MongoChecksumOptions ;
4242}
4343
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+
4455export class MongoSyncBucketStorage
4556 extends BaseObserver < storage . SyncRulesBucketStorageListener >
4657 implements storage . SyncRulesBucketStorage
@@ -680,25 +691,45 @@ export class MongoSyncBucketStorage
680691
681692 // We only watch changes to the active sync rules.
682693 // 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+ }
684712
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+ }
689717
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+ }
696724
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.
700729 yield op ;
701730 }
731+ } finally {
732+ await stream . return ( null ) ;
702733 }
703734 }
704735
0 commit comments