Skip to content

Commit 74b39ac

Browse files
authored
Delete shards only when notified via gossip (#6504)
* Delete shards only when notified via gossip * Use queues summary to recover shards
1 parent 2021556 commit 74b39ac

5 files changed

Lines changed: 224 additions & 200 deletions

File tree

quickwit/quickwit-ingest/src/ingest_v2/fetch.rs

Lines changed: 62 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -847,8 +847,15 @@ pub(super) mod tests {
847847
fetch_task_handle.await.unwrap();
848848
}
849849

850-
#[tokio::test]
851-
async fn test_fetch_task_signals_eof() {
850+
/// Spawns a fetch task against a closed shard whose queue holds `num_records` records and whose
851+
/// replication position is `replication_position_inclusive`, then asserts that fetching from
852+
/// `from_position_exclusive` immediately signals EOF at `expected_eof_position`.
853+
async fn check_fetch_task_signals_eof(
854+
num_records: usize,
855+
replication_position_inclusive: Position,
856+
from_position_exclusive: Position,
857+
expected_eof_position: Position,
858+
) {
852859
let tempdir = tempfile::tempdir().unwrap();
853860
let mrecordlog = Arc::new(RwLock::new(Some(
854861
MultiRecordLogAsync::open(tempdir.path()).await.unwrap(),
@@ -868,26 +875,25 @@ pub(super) mod tests {
868875
.await
869876
.unwrap();
870877

871-
mrecordlog_guard
872-
.as_mut()
873-
.unwrap()
874-
.append_records(
875-
&queue_id,
876-
None,
877-
std::iter::once(MRecord::new_doc("test-doc-foo").encode()),
878-
)
879-
.await
880-
.unwrap();
878+
if num_records > 0 {
879+
let records = (0..num_records).map(|_| MRecord::new_doc("test-doc-foo").encode());
880+
mrecordlog_guard
881+
.as_mut()
882+
.unwrap()
883+
.append_records(&queue_id, None, records)
884+
.await
885+
.unwrap();
886+
}
881887
drop(mrecordlog_guard);
882888

883889
let open_fetch_stream_request = OpenFetchStreamRequest {
884-
client_id: client_id.clone(),
890+
client_id,
885891
index_uid: Some(index_uid.clone()),
886892
source_id: source_id.clone(),
887893
shard_id: Some(shard_id.clone()),
888-
from_position_exclusive: Some(Position::offset(0u64)),
894+
from_position_exclusive: Some(from_position_exclusive.clone()),
889895
};
890-
let shard_status = (ShardState::Closed, Position::offset(0u64));
896+
let shard_status = (ShardState::Closed, replication_position_inclusive);
891897
let (_shard_status_tx, shard_status_rx) = watch::channel(shard_status);
892898

893899
let (mut fetch_stream, fetch_task_handle) = FetchStreamTask::spawn(
@@ -906,63 +912,56 @@ pub(super) mod tests {
906912
assert_eq!(fetch_eof.index_uid(), &index_uid);
907913
assert_eq!(fetch_eof.source_id, source_id);
908914
assert_eq!(fetch_eof.shard_id(), shard_id);
909-
assert_eq!(fetch_eof.eof_position, Some(Position::eof(0u64).as_eof()));
915+
assert_eq!(
916+
fetch_eof.eof_position,
917+
Some(expected_eof_position),
918+
"unexpected EOF position when fetching from `{from_position_exclusive:?}`"
919+
);
910920

911921
fetch_task_handle.await.unwrap();
912922
}
913923

914924
#[tokio::test]
915-
async fn test_fetch_task_signals_eof_at_beginning() {
916-
let tempdir = tempfile::tempdir().unwrap();
917-
let mrecordlog = Arc::new(RwLock::new(Some(
918-
MultiRecordLogAsync::open(tempdir.path()).await.unwrap(),
919-
)));
920-
let client_id = "test-client".to_string();
921-
let index_uid: IndexUid = IndexUid::for_test("test-index", 0);
922-
let source_id = "test-source".to_string();
923-
let shard_id = ShardId::from(1);
924-
let queue_id = queue_id(&index_uid, &source_id, &shard_id);
925-
926-
let open_fetch_stream_request = OpenFetchStreamRequest {
927-
client_id: client_id.clone(),
928-
index_uid: Some(index_uid.clone()),
929-
source_id: source_id.clone(),
930-
shard_id: Some(shard_id.clone()),
931-
from_position_exclusive: Some(Position::Beginning),
932-
};
933-
let (shard_status_tx, shard_status_rx) = watch::channel(ShardStatus::default());
934-
let (mut fetch_stream, fetch_task_handle) = FetchStreamTask::spawn(
935-
open_fetch_stream_request,
936-
mrecordlog.clone(),
937-
shard_status_rx,
938-
1024,
939-
);
940-
let mut mrecordlog_guard = mrecordlog.write().await;
941-
942-
mrecordlog_guard
943-
.as_mut()
944-
.unwrap()
945-
.create_queue(&queue_id)
946-
.await
947-
.unwrap();
948-
drop(mrecordlog_guard);
925+
async fn test_fetch_task_signals_eof() {
926+
check_fetch_task_signals_eof(
927+
0,
928+
Position::Beginning,
929+
Position::Beginning,
930+
Position::Beginning.as_eof(),
931+
)
932+
.await;
949933

950-
let shard_status = (ShardState::Closed, Position::Beginning);
951-
shard_status_tx.send(shard_status).unwrap();
934+
check_fetch_task_signals_eof(
935+
0,
936+
Position::Beginning,
937+
Position::offset(0u64),
938+
Position::eof(0u64),
939+
)
940+
.await;
952941

953-
let fetch_message = timeout(Duration::from_millis(100), fetch_stream.next())
954-
.await
955-
.unwrap()
956-
.unwrap()
957-
.unwrap();
958-
let fetch_eof = into_fetch_eof(fetch_message);
942+
check_fetch_task_signals_eof(
943+
0,
944+
Position::Beginning,
945+
Position::offset(42u64),
946+
Position::eof(42u64),
947+
)
948+
.await;
959949

960-
assert_eq!(fetch_eof.index_uid(), &index_uid);
961-
assert_eq!(fetch_eof.source_id, source_id);
962-
assert_eq!(fetch_eof.shard_id(), shard_id);
963-
assert_eq!(fetch_eof.eof_position, Some(Position::Beginning.as_eof()));
950+
check_fetch_task_signals_eof(
951+
1,
952+
Position::offset(0u64),
953+
Position::offset(0u64),
954+
Position::eof(0u64),
955+
)
956+
.await;
964957

965-
fetch_task_handle.await.unwrap();
958+
check_fetch_task_signals_eof(
959+
1,
960+
Position::offset(0u64),
961+
Position::offset(42u64),
962+
Position::eof(42u64),
963+
)
964+
.await;
966965
}
967966

968967
#[tokio::test]

quickwit/quickwit-ingest/src/ingest_v2/ingester.rs

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,13 +1024,17 @@ impl Ingester {
10241024
let queue_id = subrequest.queue_id();
10251025
let truncate_up_to_position_inclusive = subrequest.truncate_up_to_position_inclusive();
10261026

1027-
if truncate_up_to_position_inclusive.is_eof() {
1028-
state_guard.delete_shard(&queue_id, "indexer RPC").await;
1029-
} else {
1030-
state_guard
1031-
.truncate_shard(&queue_id, truncate_up_to_position_inclusive, "indexer RPC")
1032-
.await;
1033-
}
1027+
// We deliberately do NOT delete the shard when the indexer truncates up to EOF over
1028+
// this gRPC path. Shard deletion is driven solely by the `ShardPositionsUpdate` gossip
1029+
// event (see the `EventSubscriber<ShardPositionsUpdate>` impl below), which is the same
1030+
// signal the control plane uses to delete the shard from the metastore and its model.
1031+
//
1032+
// Handling shard deletion through that single, shared signal keeps the ingester and
1033+
// control plane views consistent: the ingester never removes a shard the
1034+
// control plane does not also remove.
1035+
state_guard
1036+
.truncate_shard(&queue_id, truncate_up_to_position_inclusive, "indexer RPC")
1037+
.await;
10341038
}
10351039
let wal_usage = state_guard.mrecordlog.resource_usage();
10361040
report_wal_usage(wal_usage);
@@ -1545,7 +1549,14 @@ mod tests {
15451549
.await;
15461550

15471551
let state_guard = ingester.state.lock_fully("test").await.unwrap();
1548-
assert_eq!(state_guard.shards.len(), 1);
1552+
assert_eq!(state_guard.shards.len(), 3);
1553+
1554+
let solo_shard_01 = state_guard.shards.get(&queue_id_01).unwrap();
1555+
solo_shard_01.assert_is_solo();
1556+
solo_shard_01.assert_is_closed();
1557+
solo_shard_01.assert_replication_position(Position::offset(0u64));
1558+
solo_shard_01.assert_truncation_position(Position::offset(0u64));
1559+
assert!(solo_shard_01.is_advertisable);
15491560

15501561
let solo_shard_02 = state_guard.shards.get(&queue_id_02).unwrap();
15511562
solo_shard_02.assert_is_solo();
@@ -1554,6 +1565,13 @@ mod tests {
15541565
solo_shard_02.assert_truncation_position(Position::offset(0u64));
15551566
assert!(solo_shard_02.is_advertisable);
15561567

1568+
let solo_shard_03 = state_guard.shards.get(&queue_id_03).unwrap();
1569+
solo_shard_03.assert_is_solo();
1570+
solo_shard_03.assert_is_closed();
1571+
solo_shard_03.assert_replication_position(Position::Beginning);
1572+
solo_shard_03.assert_truncation_position(Position::Beginning);
1573+
assert!(solo_shard_03.is_advertisable);
1574+
15571575
state_guard
15581576
.mrecordlog
15591577
.assert_records_eq(&queue_id_02, .., &[(1, [0, 0], "test-doc-bar")]);
@@ -3260,16 +3278,24 @@ mod tests {
32603278
.unwrap();
32613279

32623280
let state_guard = ingester.state.lock_fully("test").await.unwrap();
3263-
3264-
assert_eq!(state_guard.shards.len(), 1);
3265-
assert_eq!(state_guard.doc_mappers.len(), 1);
3281+
assert_eq!(state_guard.shards.len(), 2);
3282+
assert_eq!(state_guard.doc_mappers.len(), 2);
32663283

32673284
assert!(state_guard.shards.contains_key(&queue_id_01));
3285+
assert!(state_guard.shards.contains_key(&queue_id_02));
32683286
assert!(state_guard.doc_mappers.contains_key(&doc_mapping_uid_01));
3287+
assert!(state_guard.doc_mappers.contains_key(&doc_mapping_uid_02));
3288+
3289+
let solo_shard_02 = state_guard.shards.get(&queue_id_02).unwrap();
3290+
solo_shard_02.assert_truncation_position(Position::eof(0u64));
32693291

32703292
state_guard
32713293
.mrecordlog
32723294
.assert_records_eq(&queue_id_01, .., &[(1, [0, 0], "test-doc-bar")]);
3295+
3296+
state_guard
3297+
.mrecordlog
3298+
.assert_records_eq(&queue_id_02, .., &[]);
32733299
}
32743300

32753301
#[tokio::test]

quickwit/quickwit-ingest/src/ingest_v2/mrecordlog_utils.rs

Lines changed: 1 addition & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@
1414

1515
use std::io;
1616
use std::iter::once;
17-
use std::ops::RangeInclusive;
1817

1918
use bytesize::ByteSize;
2019
#[cfg(feature = "failpoints")]
2120
use fail::fail_point;
22-
use mrecordlog::error::{AppendError, DeleteQueueError};
21+
use mrecordlog::error::AppendError;
2322
use quickwit_proto::ingest::DocBatchV2;
2423
use quickwit_proto::types::{Position, QueueId};
2524
use tracing::instrument;
@@ -150,37 +149,6 @@ pub(super) fn check_enough_capacity(
150149
Ok(())
151150
}
152151

153-
/// Deletes a queue from the WAL. Returns without error if the queue does not exist.
154-
pub async fn force_delete_queue(
155-
mrecordlog: &mut MultiRecordLogAsync,
156-
queue_id: &QueueId,
157-
) -> io::Result<()> {
158-
match mrecordlog.delete_queue(queue_id).await {
159-
Ok(_) | Err(DeleteQueueError::MissingQueue(_)) => Ok(()),
160-
Err(DeleteQueueError::IoError(error)) => Err(error),
161-
}
162-
}
163-
164-
/// Returns the first and last position of the records currently stored in the queue. Returns `None`
165-
/// if the queue does not exist or is empty.
166-
pub(super) fn queue_position_range(
167-
mrecordlog: &MultiRecordLogAsync,
168-
queue_id: &QueueId,
169-
) -> Option<RangeInclusive<u64>> {
170-
let first_position = mrecordlog
171-
.range(queue_id, ..)
172-
.ok()?
173-
.next()
174-
.map(|record| record.position)?;
175-
176-
let last_position = mrecordlog
177-
.last_record(queue_id)
178-
.ok()?
179-
.map(|record| record.position)?;
180-
181-
Some(first_position..=last_position)
182-
}
183-
184152
#[cfg(test)]
185153
mod tests {
186154
use super::*;
@@ -267,33 +235,4 @@ mod tests {
267235

268236
check_enough_capacity(&mrecordlog, ByteSize::mb(256), ByteSize(12), ByteSize(12)).unwrap();
269237
}
270-
271-
#[tokio::test]
272-
async fn test_append_queue_position_range() {
273-
let tempdir = tempfile::tempdir().unwrap();
274-
let mut mrecordlog = MultiRecordLogAsync::open(tempdir.path()).await.unwrap();
275-
276-
assert!(queue_position_range(&mrecordlog, &"queue-not-found".to_string()).is_none());
277-
278-
mrecordlog.create_queue("test-queue").await.unwrap();
279-
assert!(queue_position_range(&mrecordlog, &"test-queue".to_string()).is_none());
280-
281-
mrecordlog
282-
.append_records("test-queue", None, std::iter::once(&b"test-doc-foo"[..]))
283-
.await
284-
.unwrap();
285-
let position_range = queue_position_range(&mrecordlog, &"test-queue".to_string()).unwrap();
286-
assert_eq!(position_range, 0..=0);
287-
288-
mrecordlog
289-
.append_records("test-queue", None, std::iter::once(&b"test-doc-bar"[..]))
290-
.await
291-
.unwrap();
292-
let position_range = queue_position_range(&mrecordlog, &"test-queue".to_string()).unwrap();
293-
assert_eq!(position_range, 0..=1);
294-
295-
mrecordlog.truncate("test-queue", 0).await.unwrap();
296-
let position_range = queue_position_range(&mrecordlog, &"test-queue".to_string()).unwrap();
297-
assert_eq!(position_range, 1..=1);
298-
}
299238
}

0 commit comments

Comments
 (0)