1
1
import { strict as assert } from 'assert' ;
2
+ import { UUID } from 'bson' ;
2
3
import { expect } from 'chai' ;
3
4
import { on , once } from 'events' ;
4
5
import { gte , lt } from 'semver' ;
@@ -14,6 +15,7 @@ import {
14
15
type CommandStartedEvent ,
15
16
type Db ,
16
17
isHello ,
18
+ LEGACY_HELLO_COMMAND ,
17
19
Long ,
18
20
MongoAPIError ,
19
21
MongoChangeStreamError ,
@@ -45,6 +47,19 @@ const pipeline = [
45
47
{ $addFields : { comment : 'The documentKey field has been projected out of this document.' } }
46
48
] ;
47
49
50
+ async function forcePrimaryStepDown ( client : MongoClient ) {
51
+ await client
52
+ . db ( 'admin' )
53
+ . command ( { replSetFreeze : 0 } , { readPreference : ReadPreference . SECONDARY } ) ;
54
+ await client
55
+ . db ( 'admin' )
56
+ . command ( { replSetStepDown : 15 , secondaryCatchUpPeriodSecs : 10 , force : true } ) ;
57
+
58
+ // wait for secondary to become primary but also allow previous primary to become next primary
59
+ // in subsequent test runs
60
+ await sleep ( 15_000 ) ;
61
+ }
62
+
48
63
describe ( 'Change Streams' , function ( ) {
49
64
let client : MongoClient ;
50
65
let collection : Collection ;
@@ -2003,9 +2018,11 @@ describe('Change Streams', function () {
2003
2018
2004
2019
describe ( 'ChangeStream resumability' , function ( ) {
2005
2020
let client : MongoClient ;
2021
+ let utilClient : MongoClient ;
2006
2022
let collection : Collection ;
2007
2023
let changeStream : ChangeStream ;
2008
2024
let aggregateEvents : CommandStartedEvent [ ] = [ ] ;
2025
+ let appName : string ;
2009
2026
2010
2027
const changeStreamResumeOptions : ChangeStreamOptions = {
2011
2028
fullDocument : 'updateLookup' ,
@@ -2055,22 +2072,36 @@ describe('ChangeStream resumability', function () {
2055
2072
beforeEach ( async function ( ) {
2056
2073
const dbName = 'resumabilty_tests' ;
2057
2074
const collectionName = 'foo' ;
2058
- const utilClient = this . configuration . newClient ( ) ;
2075
+
2076
+ utilClient = this . configuration . newClient ( ) ;
2077
+
2059
2078
// 3.6 servers do not support creating a change stream on a database that doesn't exist
2060
2079
await utilClient
2061
2080
. db ( dbName )
2062
2081
. dropDatabase ( )
2063
2082
. catch ( e => e ) ;
2064
2083
await utilClient . db ( dbName ) . createCollection ( collectionName ) ;
2065
- await utilClient . close ( ) ;
2066
2084
2067
- client = this . configuration . newClient ( { monitorCommands : true } ) ;
2085
+ // we are going to switch primary in tests and cleanup of failpoints is difficult,
2086
+ // so generating unique appname instead of cleaning for each test is an easier solution
2087
+ appName = new UUID ( ) . toString ( ) ;
2088
+
2089
+ client = this . configuration . newClient (
2090
+ { } ,
2091
+ {
2092
+ monitorCommands : true ,
2093
+ serverSelectionTimeoutMS : 10_000 ,
2094
+ heartbeatFrequencyMS : 5_000 ,
2095
+ appName : appName
2096
+ }
2097
+ ) ;
2068
2098
client . on ( 'commandStarted' , filterForCommands ( [ 'aggregate' ] , aggregateEvents ) ) ;
2069
2099
collection = client . db ( dbName ) . collection ( collectionName ) ;
2070
2100
} ) ;
2071
2101
2072
2102
afterEach ( async function ( ) {
2073
2103
await changeStream . close ( ) ;
2104
+ await utilClient . close ( ) ;
2074
2105
await client . close ( ) ;
2075
2106
aggregateEvents = [ ] ;
2076
2107
} ) ;
@@ -2228,6 +2259,38 @@ describe('ChangeStream resumability', function () {
2228
2259
expect ( changeStream . closed ) . to . be . true ;
2229
2260
} ) ;
2230
2261
} ) ;
2262
+
2263
+ context ( 'when the error is not a server error' , function ( ) {
2264
+ it (
2265
+ 'should resume on ServerSelectionError' ,
2266
+ { requires : { topology : [ 'replicaset' ] } } ,
2267
+ async function ( ) {
2268
+ changeStream = collection . watch ( [ ] ) ;
2269
+ await initIteratorMode ( changeStream ) ;
2270
+
2271
+ await collection . insertOne ( { a : 1 } ) ;
2272
+
2273
+ await utilClient . db ( 'admin' ) . command ( {
2274
+ configureFailPoint : 'failCommand' ,
2275
+ mode : 'alwaysOn' ,
2276
+ data : {
2277
+ failCommands : [ 'ping' , 'hello' , LEGACY_HELLO_COMMAND ] ,
2278
+ closeConnection : true ,
2279
+ appName : appName
2280
+ }
2281
+ } as FailCommandFailPoint ) ;
2282
+
2283
+ await forcePrimaryStepDown ( utilClient ) ;
2284
+
2285
+ const change = await changeStream . next ( ) ;
2286
+ expect ( change ) . to . containSubset ( { operationType : 'insert' , fullDocument : { a : 1 } } ) ;
2287
+
2288
+ expect ( aggregateEvents ) . to . have . lengthOf ( 2 ) ;
2289
+ const [ e1 , e2 ] = aggregateEvents ;
2290
+ expect ( e1 . address ) . to . not . equal ( e2 . address ) ;
2291
+ }
2292
+ ) ;
2293
+ } ) ;
2231
2294
} ) ;
2232
2295
2233
2296
context ( '#hasNext' , function ( ) {
@@ -2541,6 +2604,37 @@ describe('ChangeStream resumability', function () {
2541
2604
expect ( changeStream . closed ) . to . be . true ;
2542
2605
} ) ;
2543
2606
} ) ;
2607
+
2608
+ context ( 'when the error is not a server error' , function ( ) {
2609
+ it (
2610
+ 'should resume on ServerSelectionError' ,
2611
+ { requires : { topology : [ 'replicaset' ] } } ,
2612
+ async function ( ) {
2613
+ changeStream = collection . watch ( [ ] ) ;
2614
+ await initIteratorMode ( changeStream ) ;
2615
+
2616
+ await collection . insertOne ( { a : 1 } ) ;
2617
+
2618
+ await utilClient . db ( 'admin' ) . command ( {
2619
+ configureFailPoint : 'failCommand' ,
2620
+ mode : 'alwaysOn' ,
2621
+ data : {
2622
+ failCommands : [ 'ping' , 'hello' , LEGACY_HELLO_COMMAND ] ,
2623
+ closeConnection : true ,
2624
+ appName : appName
2625
+ }
2626
+ } as FailCommandFailPoint ) ;
2627
+ await forcePrimaryStepDown ( utilClient ) ;
2628
+
2629
+ const change = await changeStream . tryNext ( ) ;
2630
+ expect ( change ) . to . containSubset ( { operationType : 'insert' , fullDocument : { a : 1 } } ) ;
2631
+
2632
+ expect ( aggregateEvents ) . to . have . lengthOf ( 2 ) ;
2633
+ const [ e1 , e2 ] = aggregateEvents ;
2634
+ expect ( e1 . address ) . to . not . equal ( e2 . address ) ;
2635
+ }
2636
+ ) ;
2637
+ } ) ;
2544
2638
} ) ;
2545
2639
2546
2640
context ( '#asyncIterator' , function ( ) {
@@ -2677,6 +2771,41 @@ describe('ChangeStream resumability', function () {
2677
2771
}
2678
2772
} ) ;
2679
2773
} ) ;
2774
+
2775
+ context ( 'when the error is not a server error' , function ( ) {
2776
+ it (
2777
+ 'should resume on ServerSelectionError' ,
2778
+ { requires : { topology : [ 'replicaset' ] } } ,
2779
+ async function ( ) {
2780
+ changeStream = collection . watch ( [ ] ) ;
2781
+ await initIteratorMode ( changeStream ) ;
2782
+ const changeStreamIterator = changeStream [ Symbol . asyncIterator ] ( ) ;
2783
+
2784
+ await collection . insertOne ( { a : 1 } ) ;
2785
+
2786
+ await utilClient . db ( 'admin' ) . command ( {
2787
+ configureFailPoint : 'failCommand' ,
2788
+ mode : 'alwaysOn' ,
2789
+ data : {
2790
+ failCommands : [ 'ping' , 'hello' , LEGACY_HELLO_COMMAND ] ,
2791
+ closeConnection : true ,
2792
+ appName : appName
2793
+ }
2794
+ } as FailCommandFailPoint ) ;
2795
+ await forcePrimaryStepDown ( utilClient ) ;
2796
+
2797
+ const change = await changeStreamIterator . next ( ) ;
2798
+ expect ( change . value ) . to . containSubset ( {
2799
+ operationType : 'insert' ,
2800
+ fullDocument : { a : 1 }
2801
+ } ) ;
2802
+
2803
+ expect ( aggregateEvents ) . to . have . lengthOf ( 2 ) ;
2804
+ const [ e1 , e2 ] = aggregateEvents ;
2805
+ expect ( e1 . address ) . to . not . equal ( e2 . address ) ;
2806
+ }
2807
+ ) ;
2808
+ } ) ;
2680
2809
} ) ;
2681
2810
} ) ;
2682
2811
@@ -2866,6 +2995,50 @@ describe('ChangeStream resumability', function () {
2866
2995
expect ( changeStream . closed ) . to . be . true ;
2867
2996
} ) ;
2868
2997
} ) ;
2998
+
2999
+ context ( 'when the error is not a server error' , function ( ) {
3000
+ it (
3001
+ 'should resume on ServerSelectionError' ,
3002
+ { requires : { topology : [ 'replicaset' ] } } ,
3003
+ async function ( ) {
3004
+ changeStream = collection . watch ( [ ] ) ;
3005
+
3006
+ const changes = on ( changeStream , 'change' ) ;
3007
+ await once ( changeStream . cursor , 'init' ) ;
3008
+
3009
+ await collection . insertOne ( { a : 1 } ) ;
3010
+
3011
+ const change = await changes . next ( ) ;
3012
+ expect ( change . value [ 0 ] ) . to . containSubset ( {
3013
+ operationType : 'insert' ,
3014
+ fullDocument : { a : 1 }
3015
+ } ) ;
3016
+
3017
+ await utilClient . db ( 'admin' ) . command ( {
3018
+ configureFailPoint : 'failCommand' ,
3019
+ mode : 'alwaysOn' ,
3020
+ data : {
3021
+ failCommands : [ 'ping' , 'hello' , LEGACY_HELLO_COMMAND ] ,
3022
+ closeConnection : true ,
3023
+ appName : appName
3024
+ }
3025
+ } as FailCommandFailPoint ) ;
3026
+ await forcePrimaryStepDown ( utilClient ) ;
3027
+
3028
+ await collection . insertOne ( { a : 2 } ) ;
3029
+
3030
+ const change2 = await changes . next ( ) ;
3031
+ expect ( change2 . value [ 0 ] ) . to . containSubset ( {
3032
+ operationType : 'insert' ,
3033
+ fullDocument : { a : 2 }
3034
+ } ) ;
3035
+
3036
+ expect ( aggregateEvents ) . to . have . lengthOf ( 2 ) ;
3037
+ const [ e1 , e2 ] = aggregateEvents ;
3038
+ expect ( e1 . address ) . to . not . equal ( e2 . address ) ;
3039
+ }
3040
+ ) ;
3041
+ } ) ;
2869
3042
} ) ;
2870
3043
2871
3044
it (
0 commit comments