@@ -160,16 +160,6 @@ impl Ingester {
160160 Ok ( ingester)
161161 }
162162
163- /// Checks whether the ingester is fully decommissioned and updates its status accordingly.
164- async fn check_decommissioning_status ( & self , state : & mut InnerIngesterState ) {
165- if state. status ( ) != IngesterStatus :: Decommissioning {
166- return ;
167- }
168- if state. shards . values ( ) . all ( |shard| shard. is_indexed ( ) ) {
169- state. set_status ( IngesterStatus :: Decommissioned ) . await ;
170- }
171- }
172-
173163 /// Initializes a primary shard by creating a queue in the write-ahead log and inserting a new
174164 /// [`IngesterShard`] into the ingester state. If replication is enabled, this method will
175165 /// also:
@@ -1039,7 +1029,7 @@ impl Ingester {
10391029 let wal_usage = state_guard. mrecordlog . resource_usage ( ) ;
10401030 report_wal_usage ( wal_usage) ;
10411031
1042- self . check_decommissioning_status ( & mut state_guard ) . await ;
1032+ state_guard . check_decommissioning_status ( ) . await ;
10431033 let truncate_response = TruncateShardsResponse { } ;
10441034 Ok ( truncate_response)
10451035 }
@@ -1199,7 +1189,7 @@ impl IngesterService for Ingester {
11991189 . delete_shard ( & queue_id, "control-plane-retain-shards-rpc" )
12001190 . await ;
12011191 }
1202- self . check_decommissioning_status ( & mut state_guard ) . await ;
1192+ state_guard . check_decommissioning_status ( ) . await ;
12031193 Ok ( RetainShardsResponse { } )
12041194 }
12051195
@@ -1258,9 +1248,7 @@ impl IngesterService for Ingester {
12581248 for shard in state_guard. shards . values_mut ( ) {
12591249 shard. close ( ) ;
12601250 }
1261- self_clone
1262- . check_decommissioning_status ( & mut state_guard)
1263- . await ;
1251+ state_guard. check_decommissioning_status ( ) . await ;
12641252 } ) ;
12651253 Ok ( DecommissionResponse { } )
12661254 }
@@ -1291,6 +1279,7 @@ impl EventSubscriber<ShardPositionsUpdate> for WeakIngesterState {
12911279 . await ;
12921280 }
12931281 }
1282+ state_guard. check_decommissioning_status ( ) . await ;
12941283 }
12951284}
12961285
@@ -3298,6 +3287,61 @@ mod tests {
32983287 . assert_records_eq ( & queue_id_02, .., & [ ] ) ;
32993288 }
33003289
3290+ #[ tokio:: test]
3291+ async fn test_ingester_truncate_empty_shard_to_eof ( ) {
3292+ let ( ingester_ctx, ingester) = IngesterForTest :: default ( ) . build ( ) . await ;
3293+
3294+ let index_uid = IndexUid :: for_test ( "test-index" , 0 ) ;
3295+ let source_id = SourceId :: from ( "test-source" ) ;
3296+ let queue_id = queue_id ( & index_uid, & source_id, & ShardId :: from ( 1 ) ) ;
3297+
3298+ let doc_mapping_uid = DocMappingUid :: random ( ) ;
3299+ let doc_mapping_json = format ! ( r#"{{ "doc_mapping_uid": "{doc_mapping_uid}" }}"# ) ;
3300+ let shard = Shard {
3301+ index_uid : Some ( index_uid. clone ( ) ) ,
3302+ source_id : source_id. clone ( ) ,
3303+ shard_id : Some ( ShardId :: from ( 1 ) ) ,
3304+ shard_state : ShardState :: Open as i32 ,
3305+ doc_mapping_uid : Some ( doc_mapping_uid) ,
3306+ ..Default :: default ( )
3307+ } ;
3308+
3309+ let mut state_guard = ingester. state . lock_fully ( "test" ) . await . unwrap ( ) ;
3310+ ingester
3311+ . init_primary_shard (
3312+ & mut state_guard. inner ,
3313+ & mut state_guard. mrecordlog ,
3314+ shard,
3315+ & doc_mapping_json,
3316+ Instant :: now ( ) ,
3317+ true ,
3318+ )
3319+ . await
3320+ . unwrap ( ) ;
3321+ state_guard. shards . get_mut ( & queue_id) . unwrap ( ) . close ( ) ;
3322+ drop ( state_guard) ;
3323+
3324+ let truncate_shards_request = TruncateShardsRequest {
3325+ ingester_id : ingester_ctx. node_id . to_string ( ) ,
3326+ subrequests : vec ! [ TruncateShardsSubrequest {
3327+ index_uid: Some ( index_uid. clone( ) ) ,
3328+ source_id: source_id. clone( ) ,
3329+ shard_id: Some ( ShardId :: from( 1 ) ) ,
3330+ truncate_up_to_position_inclusive: Some ( Position :: Beginning . as_eof( ) ) ,
3331+ } ] ,
3332+ } ;
3333+ ingester
3334+ . truncate_shards ( truncate_shards_request. clone ( ) )
3335+ . await
3336+ . unwrap ( ) ;
3337+
3338+ let state_guard = ingester. state . lock_fully ( "test" ) . await . unwrap ( ) ;
3339+ let shard = state_guard. shards . get ( & queue_id) . unwrap ( ) ;
3340+ shard. assert_truncation_position ( Position :: Beginning . as_eof ( ) ) ;
3341+ assert ! ( state_guard. mrecordlog. queue_exists( & queue_id) ) ;
3342+ state_guard. mrecordlog . assert_records_eq ( & queue_id, .., & [ ] ) ;
3343+ }
3344+
33013345 #[ tokio:: test]
33023346 async fn test_ingester_truncate_shards_deletes_dangling_shards ( ) {
33033347 let ( ingester_ctx, ingester) = IngesterForTest :: default ( ) . build ( ) . await ;
@@ -3666,7 +3710,9 @@ mod tests {
36663710 let index_uid = IndexUid :: for_test ( "test-index" , 0 ) ;
36673711 let source_id = SourceId :: from ( "test-source" ) ;
36683712
3669- let shard = IngesterShard :: new_solo ( index_uid, source_id, ShardId :: from ( 1 ) ) . build ( ) ;
3713+ let shard = IngesterShard :: new_solo ( index_uid, source_id, ShardId :: from ( 1 ) )
3714+ . with_replication_position_inclusive ( Position :: offset ( 12u64 ) )
3715+ . build ( ) ;
36703716 let queue_id = shard. queue_id ( ) ;
36713717
36723718 state_guard. shards . insert ( queue_id. clone ( ) , shard) ;
@@ -3701,17 +3747,13 @@ mod tests {
37013747 let ( _ingester_ctx, ingester) = IngesterForTest :: default ( ) . build ( ) . await ;
37023748 let mut state_guard = ingester. state . lock_fully ( "test" ) . await . unwrap ( ) ;
37033749
3704- ingester
3705- . check_decommissioning_status ( & mut state_guard)
3706- . await ;
3750+ state_guard. check_decommissioning_status ( ) . await ;
37073751 assert_eq ! ( state_guard. status( ) , IngesterStatus :: Ready ) ;
37083752
37093753 state_guard
37103754 . set_status ( IngesterStatus :: Decommissioning )
37113755 . await ;
3712- ingester
3713- . check_decommissioning_status ( & mut state_guard)
3714- . await ;
3756+ state_guard. check_decommissioning_status ( ) . await ;
37153757 assert_eq ! ( state_guard. status( ) , IngesterStatus :: Decommissioned ) ;
37163758
37173759 state_guard
@@ -3728,20 +3770,70 @@ mod tests {
37283770 let queue_id = solo_shard. queue_id ( ) ;
37293771
37303772 state_guard. shards . insert ( queue_id. clone ( ) , solo_shard) ;
3731- ingester
3732- . check_decommissioning_status ( & mut state_guard)
3733- . await ;
3773+ state_guard. check_decommissioning_status ( ) . await ;
37343774 assert_eq ! ( state_guard. status( ) , IngesterStatus :: Decommissioning ) ;
37353775
37363776 let shard = state_guard. shards . get_mut ( & queue_id) . unwrap ( ) ;
37373777 shard. truncation_position_inclusive = Position :: Beginning . as_eof ( ) ;
3778+ state_guard. check_decommissioning_status ( ) . await ;
3779+ assert_eq ! ( state_guard. status( ) , IngesterStatus :: Decommissioning ) ;
37383780
3739- ingester
3740- . check_decommissioning_status ( & mut state_guard)
3741- . await ;
3781+ state_guard. shards . remove ( & queue_id) ;
3782+ state_guard. check_decommissioning_status ( ) . await ;
37423783 assert_eq ! ( state_guard. status( ) , IngesterStatus :: Decommissioned ) ;
37433784 }
37443785
3786+ #[ tokio:: test]
3787+ async fn test_decommission_completes_when_empty_shard_deleted_via_gossip ( ) {
3788+ let ( _ingester_ctx, ingester) = IngesterForTest :: default ( ) . build ( ) . await ;
3789+
3790+ let event_broker = EventBroker :: default ( ) ;
3791+ ingester. subscribe ( & event_broker) ;
3792+
3793+ let index_uid = IndexUid :: for_test ( "test-index" , 0 ) ;
3794+ let source_id = SourceId :: from ( "test-source" ) ;
3795+ let shard_id = ShardId :: from ( 1 ) ;
3796+ let queue_id = queue_id ( & index_uid, & source_id, & shard_id) ;
3797+
3798+ let empty_shard =
3799+ IngesterShard :: new_solo ( index_uid. clone ( ) , source_id. clone ( ) , shard_id. clone ( ) )
3800+ . with_state ( ShardState :: Closed )
3801+ . build ( ) ;
3802+
3803+ let mut status_rx = ingester. state . status_rx . clone ( ) ;
3804+ {
3805+ let mut state_guard = ingester. state . lock_fully ( "test" ) . await . unwrap ( ) ;
3806+ state_guard. shards . insert ( queue_id. clone ( ) , empty_shard) ;
3807+ state_guard
3808+ . set_status ( IngesterStatus :: Decommissioning )
3809+ . await ;
3810+ }
3811+
3812+ assert_eq ! (
3813+ * status_rx. borrow_and_update( ) ,
3814+ IngesterStatus :: Decommissioning
3815+ ) ;
3816+
3817+ event_broker. publish ( ShardPositionsUpdate {
3818+ source_uid : SourceUid {
3819+ index_uid : index_uid. clone ( ) ,
3820+ source_id : source_id. clone ( ) ,
3821+ } ,
3822+ updated_shard_positions : vec ! [ ( shard_id, Position :: Beginning . as_eof( ) ) ] ,
3823+ } ) ;
3824+
3825+ tokio:: time:: timeout ( Duration :: from_secs ( 5 ) , async {
3826+ while * status_rx. borrow_and_update ( ) != IngesterStatus :: Decommissioned {
3827+ status_rx. changed ( ) . await . unwrap ( ) ;
3828+ }
3829+ } )
3830+ . await
3831+ . expect ( "ingester should reach Decommissioned once its empty shard is deleted via gossip" ) ;
3832+
3833+ let state_guard = ingester. state . lock_fully ( "test" ) . await . unwrap ( ) ;
3834+ assert ! ( !state_guard. shards. contains_key( & queue_id) ) ;
3835+ }
3836+
37453837 #[ tokio:: test]
37463838 async fn test_ingester_truncate_on_shard_positions_update ( ) {
37473839 let ( _ingester_ctx, ingester) = IngesterForTest :: default ( ) . build ( ) . await ;
0 commit comments