Skip to content

Commit e3bb15b

Browse files
committed
ct/l1: Handle exceptions better
This change improves the handling of exceptions in the L1 reader in the following ways: 1. It removes try-catch blocks in favor of ss::coroutine::as_future. 2. It ensures that object readers are closed even in exceptional situations (using #1).
1 parent 015911d commit e3bb15b

2 files changed

Lines changed: 180 additions & 137 deletions

File tree

src/v/cloud_topics/level_one/frontend_reader/BUILD

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@ redpanda_cc_library(
2020
"//src/v/config",
2121
"//src/v/model",
2222
"//src/v/model:timeout_clock",
23+
"@seastar",
2324
],
2425
)

src/v/cloud_topics/level_one/frontend_reader/reader.cc

Lines changed: 179 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include "config/configuration.h"
1515
#include "model/timeout_clock.h"
1616

17+
#include <seastar/coroutine/as_future.hh>
18+
1719
#include <exception>
1820
#include <iterator>
1921
#include <utility>
@@ -90,64 +92,67 @@ ss::future<> level_one_log_reader_impl::fetch_metadata(
9092
co_return;
9193
}
9294

93-
try {
94-
auto response = co_await _metastore->get_first_ge(
95-
_tidp, _config.start_offset);
96-
97-
if (response.has_value()) {
98-
auto& obj = response.value();
99-
100-
vlog(
101-
cd_log.debug,
102-
"Found L1 object {} for {} ({}) at offset {}",
103-
obj.oid,
104-
_ntp,
105-
_tidp,
106-
_config.start_offset);
95+
auto response_fut = co_await ss::coroutine::as_future(
96+
_metastore->get_first_ge(_tidp, _config.start_offset));
97+
if (response_fut.failed()) {
98+
vlog(
99+
cd_log.error,
100+
"Exception fetching metadata from metastore for {} ({}): {}",
101+
_ntp,
102+
_tidp,
103+
response_fut.get_exception());
104+
_state = state::end_of_stream;
105+
co_return;
106+
}
107107

108-
auto footer_result = co_await read_footer(
109-
obj.oid, obj.footer_pos, obj.object_size);
110-
if (!footer_result.has_value()) {
111-
vlog(
112-
cd_log.warn,
113-
"Failed to read footer from L1 object {} for {} ({}): {}",
114-
obj.oid,
115-
_ntp,
116-
_tidp,
117-
std::to_underlying(footer_result.error()));
118-
_state = state::end_of_stream;
119-
co_return;
120-
}
121-
_current_obj = current_object{
122-
.oid = obj.oid, .footer = std::move(footer_result.value())};
123-
_state = state::ready;
124-
} else if (response.error() == l1::metastore::errc::out_of_range) {
108+
auto response = response_fut.get();
109+
if (!response.has_value()) {
110+
if (response.error() == l1::metastore::errc::out_of_range) {
125111
vlog(
126112
cd_log.debug,
127113
"No L1 objects found for {} ({}) at offset {} or later",
128114
_ntp,
129115
_tidp,
130116
_config.start_offset);
131117
_state = state::end_of_stream;
132-
} else {
133-
vlog(
134-
cd_log.warn,
135-
"Failed to query metastore for {} ({}) offset {}: {}",
136-
_ntp,
137-
_tidp,
138-
_config.start_offset,
139-
response.error());
140-
_state = state::end_of_stream;
118+
co_return;
141119
}
142-
} catch (...) {
143120
vlog(
144-
cd_log.error,
145-
"Failed to fetch metadata from metastore for {} ({}): {}",
121+
cd_log.warn,
122+
"Failed to query metastore for {} ({}) offset {}: {}",
123+
_ntp,
124+
_tidp,
125+
_config.start_offset,
126+
response.error());
127+
_state = state::end_of_stream;
128+
co_return;
129+
}
130+
131+
auto& obj = response.value();
132+
vlog(
133+
cd_log.debug,
134+
"Found L1 object {} for {} ({}) at offset {}",
135+
obj.oid,
136+
_ntp,
137+
_tidp,
138+
_config.start_offset);
139+
140+
auto footer_result = co_await read_footer(
141+
obj.oid, obj.footer_pos, obj.object_size);
142+
if (!footer_result.has_value()) {
143+
vlog(
144+
cd_log.warn,
145+
"Failed to read footer from L1 object {} for {} ({}): {}",
146+
obj.oid,
146147
_ntp,
147148
_tidp,
148-
std::current_exception());
149+
std::to_underlying(footer_result.error()));
149150
_state = state::end_of_stream;
151+
co_return;
150152
}
153+
_current_obj = current_object{
154+
.oid = obj.oid, .footer = std::move(footer_result.value())};
155+
_state = state::ready;
151156
}
152157

153158
ss::future<std::expected<l1::footer, l1::io::errc>>
@@ -165,13 +170,41 @@ level_one_log_reader_impl::read_footer(
165170
auto* abort_source = _config.abort_source
166171
? &_config.abort_source.value().get()
167172
: &default_abort_source;
168-
auto stream_result = co_await _io->read_object(extent, abort_source);
173+
auto stream_fut = co_await ss::coroutine::as_future(
174+
_io->read_object(extent, abort_source));
175+
if (stream_fut.failed()) {
176+
vlog(
177+
cd_log.error,
178+
"Exception opening stream for footer from object {} (pos {} object "
179+
"size {}): {}",
180+
oid,
181+
extent.position,
182+
object_size,
183+
stream_fut.get_exception());
184+
co_return std::unexpected(l1::io::errc::file_io_error);
185+
}
186+
auto stream_result = stream_fut.get();
169187
if (!stream_result.has_value()) {
170188
co_return std::unexpected(stream_result.error());
171189
}
172190

173191
auto& stream = stream_result.value();
174-
iobuf footer_buf = co_await read_iobuf_exactly(stream, footer_total_size);
192+
193+
auto footer_fut = co_await ss::coroutine::as_future(
194+
read_iobuf_exactly(stream, footer_total_size));
195+
if (footer_fut.failed()) {
196+
vlog(
197+
cd_log.error,
198+
"Exception reading footer from object {} (pos {} object size {}): {}",
199+
oid,
200+
extent.position,
201+
object_size,
202+
footer_fut.get_exception());
203+
co_await stream.close();
204+
co_return std::unexpected(l1::io::errc::file_io_error);
205+
}
206+
207+
iobuf footer_buf = footer_fut.get();
175208
co_await stream.close();
176209

177210
// Parse the footer - we have the complete footer so this should succeed.
@@ -213,112 +246,121 @@ ss::future<> level_one_log_reader_impl::materialize_batches(
213246
_current_obj.has_value(),
214247
"Expected to have current object in ready state");
215248

216-
try {
217-
size_t start_pos
218-
= _current_obj->footer.file_position_before_kafka_offset(
219-
_tidp, _config.start_offset);
220-
if (start_pos == l1::footer::npos) {
221-
// This shouldn't happen- the L1 metastore told us there's
222-
// data for this NTP, and we checked after the latest consume
223-
// that the object isn't exhausted of data.
224-
vlog(
225-
cd_log.error,
226-
"Reader in ready state but no data for {} ({}) in object {}",
227-
_ntp,
228-
_tidp,
229-
_current_obj->oid);
230-
_state = state::end_of_stream;
231-
co_return;
232-
}
249+
size_t start_pos = _current_obj->footer.file_position_before_kafka_offset(
250+
_tidp, _config.start_offset);
251+
if (start_pos == l1::footer::npos) {
252+
// This shouldn't happen- the L1 metastore told us there's
253+
// data for this NTP, and we checked after the latest consume
254+
// that the object isn't exhausted of data.
255+
vlog(
256+
cd_log.error,
257+
"Reader in ready state but no data for {} ({}) in object {}",
258+
_ntp,
259+
_tidp,
260+
_current_obj->oid);
261+
_state = state::end_of_stream;
262+
co_return;
263+
}
233264

234-
l1::object_extent extent{
235-
.id = _current_obj->oid,
236-
.position = start_pos,
237-
.size = L1_max_bytes_per_object_fetch,
238-
};
239-
ss::abort_source default_abort_source;
240-
auto* abort_source = _config.abort_source
241-
? &_config.abort_source.value().get()
242-
: &default_abort_source;
243-
auto stream_result = co_await _io->read_object(extent, abort_source);
244-
if (!stream_result.has_value()) {
265+
l1::object_extent extent{
266+
.id = _current_obj->oid,
267+
.position = start_pos,
268+
.size = L1_max_bytes_per_object_fetch,
269+
};
270+
ss::abort_source default_abort_source;
271+
auto* abort_source = _config.abort_source
272+
? &_config.abort_source.value().get()
273+
: &default_abort_source;
274+
auto stream_fut = co_await ss::coroutine::as_future(
275+
_io->read_object(extent, abort_source));
276+
if (stream_fut.failed()) {
277+
vlog(
278+
cd_log.error,
279+
"Exception opening stream for L1 object {} for {} ({}): {}",
280+
_current_obj->oid,
281+
_ntp,
282+
_tidp,
283+
stream_fut.get_exception());
284+
_state = state::end_of_stream;
285+
co_return;
286+
}
287+
auto stream_result = stream_fut.get();
288+
if (!stream_result.has_value()) {
289+
vlog(
290+
cd_log.warn,
291+
"Failed to open stream for L1 object {} for {} ({}): {}",
292+
_current_obj->oid,
293+
_ntp,
294+
_tidp,
295+
std::to_underlying(stream_result.error()));
296+
_state = state::end_of_stream;
297+
co_return;
298+
}
299+
300+
auto reader = l1::object_reader::create(std::move(stream_result).value());
301+
while (true) {
302+
auto result_fut = co_await ss::coroutine::as_future(
303+
reader->read_next());
304+
if (result_fut.failed()) {
245305
vlog(
246-
cd_log.warn,
247-
"Failed to open stream for L1 object {} for {} ({}): {}",
306+
cd_log.error,
307+
"Exception reading L1 object {} for {} ({}): {}",
248308
_current_obj->oid,
249309
_ntp,
250310
_tidp,
251-
std::to_underlying(stream_result.error()));
311+
result_fut.get_exception());
312+
co_await reader->close();
252313
_state = state::end_of_stream;
253314
co_return;
254315
}
255316

256-
auto reader = l1::object_reader::create(
257-
std::move(stream_result).value());
258-
while (true) {
259-
auto result = co_await reader->read_next();
260-
261-
if (std::holds_alternative<model::record_batch>(result)) {
262-
auto batch = std::move(std::get<model::record_batch>(result));
263-
264-
// Skip batches before our start offset.
265-
if (
266-
batch.last_offset()
267-
< kafka::offset_cast(_config.start_offset)) {
268-
continue;
269-
}
270-
271-
// Stop if we've reached our max offset.
272-
if (
273-
batch.base_offset()
274-
>= kafka::offset_cast(_config.max_offset)) {
275-
break;
276-
}
277-
278-
// Check if adding this batch would exceed our byte limit.
279-
size_t batch_size = batch.size_bytes();
280-
if (
281-
(_config.strict_max_bytes || _config.bytes_consumed > 0)
282-
&& _config.bytes_consumed + batch_size >= _config.max_bytes) {
283-
_config.over_budget = true;
284-
break;
285-
}
286-
_config.bytes_consumed += batch_size;
287-
288-
// If we make it past all that, emit the batch.
289-
_batches.push_back(std::move(batch));
290-
} else if (std::holds_alternative<model::topic_id_partition>(
291-
result)) {
292-
// Partition marker. Done with this ntp's partition.
317+
auto result = result_fut.get();
318+
if (std::holds_alternative<model::record_batch>(result)) {
319+
auto batch = std::move(std::get<model::record_batch>(result));
320+
321+
// Skip batches before our start offset.
322+
if (
323+
batch.last_offset() < kafka::offset_cast(_config.start_offset)) {
324+
continue;
325+
}
326+
327+
// Stop if we've reached our max offset.
328+
if (batch.base_offset() >= kafka::offset_cast(_config.max_offset)) {
293329
break;
294-
} else if (
295-
std::holds_alternative<l1::footer>(result)
296-
|| std::holds_alternative<l1::object_reader::eof>(result)) {
297-
// End of data.
330+
}
331+
332+
// Check if adding this batch would exceed our byte limit.
333+
size_t batch_size = batch.size_bytes();
334+
if (
335+
(_config.strict_max_bytes || _config.bytes_consumed > 0)
336+
&& _config.bytes_consumed + batch_size >= _config.max_bytes) {
337+
_config.over_budget = true;
298338
break;
299339
}
340+
_config.bytes_consumed += batch_size;
341+
342+
// If we make it past all that, emit the batch.
343+
_batches.push_back(std::move(batch));
344+
} else if (std::holds_alternative<model::topic_id_partition>(result)) {
345+
// Partition marker. Done with this ntp's partition.
346+
break;
347+
} else if (
348+
std::holds_alternative<l1::footer>(result)
349+
|| std::holds_alternative<l1::object_reader::eof>(result)) {
350+
// End of data.
351+
break;
300352
}
353+
}
301354

302-
co_await reader->close();
355+
co_await reader->close();
303356

304-
vlog(
305-
cd_log.debug,
306-
"Materialized {} batches from L1 object {} for {} ({})",
307-
_batches.size(),
308-
_current_obj->oid,
309-
_ntp,
310-
_tidp);
311-
} catch (...) {
312-
vlog(
313-
cd_log.error,
314-
"Failed to materialize batches from L1 object {} for {} ({}): {}",
315-
_current_obj->oid,
316-
_ntp,
317-
_tidp,
318-
std::current_exception());
319-
_state = state::end_of_stream;
320-
co_return;
321-
}
357+
vlog(
358+
cd_log.debug,
359+
"Materialized {} batches from L1 object {} for {} ({})",
360+
_batches.size(),
361+
_current_obj->oid,
362+
_ntp,
363+
_tidp);
322364

323365
_state = _batches.empty() ? state::end_of_stream : state::materialized;
324366
}

0 commit comments

Comments
 (0)