Skip to content

Commit 02dc8b9

Browse files
author
Apurva Nisal
committed
fix(disk buffer): recover from decode errors during initialization seek
Add ReaderError::Decode to is_bad_read() so seek_to_next_record during buffer startup skips decode failures like checksum and partial_write errors, instead of failing topology build with InvalidProtobufPayload.
1 parent 0f80faf commit 02dc8b9

4 files changed

Lines changed: 122 additions & 1 deletion

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Treat decode failures as recoverable bad reads during disk buffer initialization seek, preventing permanent startup failure when corrupted records are encountered during `seek_to_next_record`.
2+
3+
authors: apurvanisal5

lib/vector-buffers/src/test/messages.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,71 @@ impl FixedEncodable for UndecodableRecord {
231231
}
232232
}
233233

234+
/// Like [`UndecodableRecord`], but the decode failure is controlled by the encoded flag.
235+
///
236+
/// `SelectiveDecodeRecord(true)` always fails to decode; `SelectiveDecodeRecord(false)` decodes
237+
/// successfully. Used to simulate corrupt middle records while keeping the last on-disk record
238+
/// valid for writer initialization.
239+
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
240+
pub(crate) struct SelectiveDecodeRecord(pub bool);
241+
242+
impl AddBatchNotifier for SelectiveDecodeRecord {
243+
fn add_batch_notifier(&mut self, batch: BatchNotifier) {
244+
drop(batch);
245+
}
246+
}
247+
248+
impl ByteSizeOf for SelectiveDecodeRecord {
249+
fn allocated_bytes(&self) -> usize {
250+
0
251+
}
252+
}
253+
254+
impl EventCount for SelectiveDecodeRecord {
255+
fn event_count(&self) -> usize {
256+
1
257+
}
258+
}
259+
260+
impl FixedEncodable for SelectiveDecodeRecord {
261+
type EncodeError = io::Error;
262+
type DecodeError = io::Error;
263+
264+
fn encode<B>(self, buffer: &mut B) -> Result<(), Self::EncodeError>
265+
where
266+
B: BufMut,
267+
{
268+
if buffer.remaining_mut() < 1 {
269+
return Err(io::Error::new(
270+
io::ErrorKind::Other,
271+
"not enough capacity to encode record",
272+
));
273+
}
274+
275+
buffer.put_u8(u8::from(self.0));
276+
Ok(())
277+
}
278+
279+
fn decode<B>(mut buffer: B) -> Result<Self, Self::DecodeError>
280+
where
281+
B: Buf,
282+
{
283+
if buffer.remaining() < 1 {
284+
return Err(io::Error::new(
285+
io::ErrorKind::Other,
286+
"not enough data to decode record",
287+
));
288+
}
289+
290+
let fail_decode = buffer.get_u8() != 0;
291+
if fail_decode {
292+
return Err(io::Error::new(io::ErrorKind::Other, "failed to decode"));
293+
}
294+
295+
Ok(SelectiveDecodeRecord(false))
296+
}
297+
}
298+
234299
message_wrapper!(MultiEventRecord: u32, |m: &Self| m.0);
235300

236301
impl MultiEventRecord {

lib/vector-buffers/src/variants/disk_v2/reader.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ where
134134
self,
135135
ReaderError::Checksum { .. }
136136
| ReaderError::Deserialization { .. }
137+
| ReaderError::Decode { .. }
137138
| ReaderError::PartialWrite
138139
)
139140
}

lib/vector-buffers/src/variants/disk_v2/tests/initialization.rs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ use tokio::time::timeout;
44
use tracing::Instrument;
55

66
use crate::{
7-
test::{SizedRecord, acknowledge, install_tracing_helpers, with_temp_dir},
7+
test::{
8+
acknowledge, install_tracing_helpers, with_temp_dir, SelectiveDecodeRecord, SizedRecord,
9+
},
810
variants::disk_v2::tests::{create_default_buffer_v2, set_file_length},
911
};
1012

@@ -182,3 +184,53 @@ async fn reader_doesnt_block_when_ahead_of_last_record_in_current_data_file() {
182184
let parent = trace_span!("reader_doesnt_block_when_ahead_of_last_record_in_current_data_file");
183185
fut.instrument(parent.or_current()).await;
184186
}
187+
188+
#[tokio::test]
189+
async fn reader_recovers_from_decode_error_during_initialization_seek() {
190+
// LOG-9468: if seek_to_next_record hits a decode failure (e.g. InvalidProtobufPayload),
191+
// buffer open must not fail permanently — same recovery as partial_write/checksum.
192+
let _a = install_tracing_helpers();
193+
194+
let fut = with_temp_dir(|dir| {
195+
let data_dir = dir.to_path_buf();
196+
197+
async move {
198+
let (mut writer, reader, ledger) = create_default_buffer_v2(data_dir.clone()).await;
199+
200+
// First record fails decode; second record decodes OK so writer startup validation passes.
201+
writer
202+
.write_record(SelectiveDecodeRecord(true))
203+
.await
204+
.expect("should not fail to write");
205+
writer.flush().await.expect("flush should not fail");
206+
207+
writer
208+
.write_record(SelectiveDecodeRecord(false))
209+
.await
210+
.expect("should not fail to write");
211+
writer.flush().await.expect("flush should not fail");
212+
writer.close();
213+
214+
// Simulate a restart where the reader had acknowledged through record ID 1.
215+
unsafe { ledger.state().unsafe_set_reader_last_record_id(1) };
216+
ledger.flush().expect("should not fail to flush ledger");
217+
218+
drop(reader);
219+
drop(writer);
220+
drop(ledger);
221+
222+
let reopen = timeout(
223+
Duration::from_millis(500),
224+
create_default_buffer_v2::<_, SelectiveDecodeRecord>(data_dir),
225+
)
226+
.await;
227+
assert!(
228+
reopen.is_ok(),
229+
"buffer open should succeed after decode error during initialization seek"
230+
);
231+
}
232+
});
233+
234+
let parent = trace_span!("reader_recovers_from_decode_error_during_initialization_seek");
235+
fut.instrument(parent.or_current()).await;
236+
}

0 commit comments

Comments
 (0)