@@ -2,9 +2,10 @@ mod test_file;
2
2
3
3
use std:: { sync:: Arc , time:: Duration } ;
4
4
5
+ use bson:: Bson ;
5
6
use futures:: stream:: TryStreamExt ;
6
7
use semver:: VersionReq ;
7
- use tokio:: sync:: { RwLockReadGuard , RwLockWriteGuard } ;
8
+ use tokio:: sync:: { Mutex , RwLockReadGuard , RwLockWriteGuard } ;
8
9
9
10
use test_file:: { TestFile , TestResult } ;
10
11
@@ -13,11 +14,11 @@ use crate::{
13
14
error:: { ErrorKind , Result , RETRYABLE_WRITE_ERROR } ,
14
15
event:: {
15
16
cmap:: { CmapEvent , CmapEventHandler , ConnectionCheckoutFailedReason } ,
16
- command:: CommandEventHandler ,
17
+ command:: { CommandEvent , CommandEventHandler } ,
17
18
} ,
18
19
options:: { ClientOptions , FindOptions , InsertManyOptions } ,
19
20
runtime,
20
- runtime:: AsyncJoinHandle ,
21
+ runtime:: { spawn , AcknowledgedMessage , AsyncJoinHandle } ,
21
22
sdam:: MIN_HEARTBEAT_FREQUENCY ,
22
23
test:: {
23
24
assert_matches,
@@ -35,6 +36,7 @@ use crate::{
35
36
CLIENT_OPTIONS ,
36
37
LOCK ,
37
38
} ,
39
+ Client ,
38
40
} ;
39
41
40
42
#[ cfg_attr( feature = "tokio-runtime" , tokio:: test( flavor = "multi_thread" ) ) ]
@@ -499,3 +501,95 @@ async fn retry_write_pool_cleared() {
499
501
500
502
assert_eq ! ( handler. get_command_started_events( & [ "insert" ] ) . len( ) , 3 ) ;
501
503
}
504
+
505
+ /// Prose test from retryable writes spec verifying that the original error is returned after
506
+ /// encountering a WriteConcernError with a RetryableWriteError label.
507
+ #[ cfg_attr( feature = "tokio-runtime" , tokio:: test( flavor = "multi_thread" ) ) ]
508
+ #[ cfg_attr( feature = "async-std-runtime" , async_std:: test) ]
509
+ async fn retry_write_retryable_write_error ( ) {
510
+ let _guard: RwLockWriteGuard < ( ) > = LOCK . run_exclusively ( ) . await ;
511
+
512
+ let mut client_options = CLIENT_OPTIONS . get ( ) . await . clone ( ) ;
513
+ client_options. retry_writes = Some ( true ) ;
514
+ let ( event_tx, event_rx) = tokio:: sync:: mpsc:: channel :: < AcknowledgedMessage < CommandEvent > > ( 1 ) ;
515
+ // The listener needs to be active on client startup, but also needs a handle to the client
516
+ // itself for the trigger action.
517
+ let listener_client: Arc < Mutex < Option < TestClient > > > = Arc :: new ( Mutex :: new ( None ) ) ;
518
+ // Set up event listener
519
+ let ( fp_tx, mut fp_rx) = tokio:: sync:: mpsc:: unbounded_channel ( ) ;
520
+ {
521
+ let client = listener_client. clone ( ) ;
522
+ let mut event_rx = event_rx;
523
+ let fp_tx = fp_tx. clone ( ) ;
524
+ // Spawn a task to watch the event channel
525
+ spawn ( async move {
526
+ while let Some ( msg) = event_rx. recv ( ) . await {
527
+ if let CommandEvent :: Succeeded ( ev) = & * msg {
528
+ if let Some ( Bson :: Document ( wc_err) ) = ev. reply . get ( "writeConcernError" ) {
529
+ if ev. command_name == "insert" && wc_err. get_i32 ( "code" ) == Ok ( 91 ) {
530
+ // Spawn a new task so events continue to process
531
+ let client = client. clone ( ) ;
532
+ let fp_tx = fp_tx. clone ( ) ;
533
+ spawn ( async move {
534
+ // Enable the failpoint.
535
+ let fp_guard = {
536
+ let client = client. lock ( ) . await ;
537
+ FailPoint :: fail_command (
538
+ & [ "insert" ] ,
539
+ FailPointMode :: Times ( 1 ) ,
540
+ FailCommandOptions :: builder ( )
541
+ . error_code ( 10107 )
542
+ . error_labels ( vec ! [
543
+ "RetryableWriteError" . to_string( ) ,
544
+ "NoWritesPerformed" . to_string( ) ,
545
+ ] )
546
+ . build ( ) ,
547
+ )
548
+ . enable ( client. as_ref ( ) . unwrap ( ) , None )
549
+ . await
550
+ . unwrap ( )
551
+ } ;
552
+ fp_tx. send ( fp_guard) . unwrap ( ) ;
553
+ // Defer acknowledging the message until the failpoint has been set
554
+ // up so the retry hits it.
555
+ msg. acknowledge ( ( ) ) ;
556
+ } ) ;
557
+ }
558
+ }
559
+ }
560
+ }
561
+ } ) ;
562
+ }
563
+ client_options. test_options_mut ( ) . async_event_listener = Some ( event_tx) ;
564
+ let client = Client :: test_builder ( ) . options ( client_options) . build ( ) . await ;
565
+ * listener_client. lock ( ) . await = Some ( client. clone ( ) ) ;
566
+
567
+ if !client. is_replica_set ( ) || client. server_version_lt ( 6 , 0 ) {
568
+ log_uncaptured ( "skipping retry_write_retryable_write_error: invalid topology" ) ;
569
+ return ;
570
+ }
571
+
572
+ let _fp_guard = FailPoint :: fail_command (
573
+ & [ "insert" ] ,
574
+ FailPointMode :: Times ( 1 ) ,
575
+ FailCommandOptions :: builder ( )
576
+ . write_concern_error ( doc ! {
577
+ "code" : 91 ,
578
+ "errorLabels" : [ "RetryableWriteError" ] ,
579
+ } )
580
+ . build ( ) ,
581
+ )
582
+ . enable ( & client, None )
583
+ . await
584
+ . unwrap ( ) ;
585
+
586
+ let result = client
587
+ . database ( "test" )
588
+ . collection :: < Document > ( "test" )
589
+ . insert_one ( doc ! { "hello" : "there" } , None )
590
+ . await ;
591
+ assert_eq ! ( result. unwrap_err( ) . code( ) , Some ( 91 ) ) ;
592
+
593
+ // Consume failpoint guard.
594
+ let _ = fp_rx. recv ( ) . await ;
595
+ }
0 commit comments