Skip to content

Commit 783174f

Browse files
authored
Skip invalid timeframes during ROOT input (#15576)
Handle corrupt reads as recoverable and discard the affected timeframe when DPL_AOD_READER_SKIP_INVALID is enabled.
1 parent a3e5117 commit 783174f

5 files changed

Lines changed: 172 additions & 55 deletions

File tree

Framework/AnalysisSupport/src/AODJAlienReaderHelpers.cxx

Lines changed: 131 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,31 @@
1010
// or submit itself to any jurisdiction.
1111

1212
#include "AODJAlienReaderHelpers.h"
13+
#include <algorithm>
1314
#include <charconv>
15+
#include <cctype>
16+
#include <cstdlib>
17+
#include <exception>
1418
#include <memory>
1519
#include <ranges>
20+
#include <string>
21+
#include <string_view>
1622
#include <vector>
1723
#include "Framework/TableTreeHelpers.h"
1824
#include "Framework/AnalysisHelpers.h"
1925
#include "Framework/DataProcessingStats.h"
2026
#include "Framework/RootArrowFilesystem.h"
2127
#include "Framework/AlgorithmSpec.h"
28+
#include "Framework/ArrowContext.h"
2229
#include "Framework/ConfigParamRegistry.h"
2330
#include "Framework/ControlService.h"
2431
#include "Framework/CallbackService.h"
2532
#include "Framework/EndOfStreamContext.h"
2633
#include "Framework/DeviceSpec.h"
2734
#include "Framework/RawDeviceService.h"
2835
#include "Framework/DataSpecUtils.h"
36+
#include "Framework/MessageContext.h"
37+
#include "Framework/StringContext.h"
2938
#include "Framework/ConfigContext.h"
3039
#include "DataInputDirector.h"
3140
#include "Framework/SourceInfoHeader.h"
@@ -101,6 +110,31 @@ using o2::monitoring::tags::Value;
101110

102111
namespace o2::framework::readers
103112
{
113+
static bool shouldSkipInvalidReads()
114+
{
115+
auto const* envValue = getenv("DPL_AOD_READER_SKIP_INVALID");
116+
if (envValue == nullptr) {
117+
return false;
118+
}
119+
120+
std::string value{envValue};
121+
std::ranges::transform(value, value.begin(), [](unsigned char c) { return std::tolower(c); });
122+
return !value.empty() && value != "0" && value != "false";
123+
}
124+
125+
static std::string describeException(std::exception const& exception)
126+
{
127+
std::string description{exception.what()};
128+
try {
129+
std::rethrow_if_nested(exception);
130+
} catch (std::exception const& nested) {
131+
description += ": " + describeException(nested);
132+
} catch (...) {
133+
description += ": unknown exception";
134+
}
135+
return description;
136+
}
137+
104138
AlgorithmSpec AODJAlienReaderHelpers::rootFileReaderCallback(ConfigContext const& ctx)
105139
{
106140
// aod-parent-base-path-replacement is now a workflow option, so it needs to be
@@ -193,14 +227,16 @@ AlgorithmSpec AODJAlienReaderHelpers::rootFileReaderCallback(ConfigContext const
193227
int level = originLevelMapping.empty() ? -1 : 0;
194228
auto fileCounter = std::make_shared<int>(0);
195229
auto numTF = std::make_shared<int>(-1);
230+
bool const skipInvalidReads = shouldSkipInvalidReads();
196231
return adaptStateless([TFNumberHeader,
197232
TFFileNameHeader,
198233
requestedTables,
199234
fileCounter,
200235
numTF,
201236
watchdog,
202237
maxRate,
203-
didir, reportTFN, reportTFFileName, level](Monitoring& monitoring, DataAllocator& outputs, ControlService& control, DeviceSpec const& device, DataProcessingStats& dpstats) {
238+
skipInvalidReads,
239+
didir, reportTFN, reportTFFileName, level](Monitoring& monitoring, DataAllocator& outputs, ControlService& control, DeviceSpec const& device, DataProcessingStats& dpstats, ArrowContext& arrowContext, MessageContext& messageContext, StringContext& stringContext) {
204240
// Each parallel reader device.inputTimesliceId reads the files fileCounter*device.maxInputTimeslices+device.inputTimesliceId
205241
// the TF to read is numTF
206242
assert(device.inputTimesliceId < device.maxInputTimeslices);
@@ -214,10 +250,10 @@ AlgorithmSpec AODJAlienReaderHelpers::rootFileReaderCallback(ConfigContext const
214250
}
215251

216252
// loop over requested tables
217-
bool first = true;
218253
static size_t totalSizeUncompressed = 0;
219254
static size_t totalSizeCompressed = 0;
220255
static uint64_t totalDFSent = 0;
256+
static uint64_t totalInvalidReadSkipped = 0;
221257

222258
// check if RuntimeLimit is reached
223259
if (!watchdog->update()) {
@@ -232,19 +268,98 @@ AlgorithmSpec AODJAlienReaderHelpers::rootFileReaderCallback(ConfigContext const
232268

233269
int64_t startTime = uv_hrtime();
234270
int64_t startSize = totalSizeCompressed;
235-
for (auto& route : requestedTables) {
236-
if ((device.inputTimesliceId % route.maxTimeslices) != route.timeslice) {
237-
continue;
271+
auto skipInvalidRead = [&](o2::header::DataOrigin const& origin, InvalidAODReadError const& e) {
272+
auto skippedTimeframes = ++totalInvalidReadSkipped;
273+
LOGP(error, "Invalid AOD read for table {}: fileCounter {}, timeFrame {}. Skipping timeframe (skipped timeframes: {}). Reason: {}",
274+
origin.as<std::string>(), fcnt, ntf, skippedTimeframes, describeException(e));
275+
arrowContext.clear();
276+
messageContext.discard();
277+
stringContext.clear();
278+
dpstats.updateStats({static_cast<short>(ProcessingStatsId::AOD_INVALID_READ_SKIPPED_TIMEFRAMES), DataProcessingStats::Op::Add, 1});
279+
*fileCounter = (fcnt - device.inputTimesliceId) / device.maxInputTimeslices;
280+
*numTF = ntf;
281+
};
282+
enum class TFReaderState {
283+
READ_FIRST_TABLE,
284+
READ_FIRST_TABLE_FROM_NEXT_FILE,
285+
READ_NEXT_TABLE,
286+
TRY_NEXT_FILE,
287+
TIMEFRAME_READ,
288+
INVALID_TIMEFRAME,
289+
};
290+
auto readState = TFReaderState::READ_FIRST_TABLE;
291+
size_t routeIndex = 0;
292+
auto reportTimeframe = [&didir, &fcnt, &ntf, &outputs, &TFNumberHeader, &TFFileNameHeader, reportTFN, reportTFFileName](header::DataHeader const& dh) {
293+
if (reportTFN) {
294+
// TF number
295+
auto timeFrameNumber = didir->getTimeFrameNumber(dh, fcnt, ntf);
296+
auto o = Output(TFNumberHeader);
297+
outputs.make<uint64_t>(o) = timeFrameNumber;
298+
}
299+
300+
if (reportTFFileName) {
301+
// Origin file name for derived output map
302+
auto o2 = Output(TFFileNameHeader);
303+
auto fileAndFolder = didir->getFileFolder(dh, fcnt, ntf);
304+
auto rootFS = std::dynamic_pointer_cast<TFileFileSystem>(fileAndFolder.filesystem());
305+
auto* f = dynamic_cast<TFile*>(rootFS->GetFile());
306+
std::string currentFilename(f->GetFile()->GetName());
307+
if (strcmp(f->GetEndpointUrl()->GetProtocol(), "file") == 0 && f->GetEndpointUrl()->GetFile()[0] != '/') {
308+
// This is not an absolute local path. Make it absolute.
309+
static std::string pwd = gSystem->pwd() + std::string("/");
310+
currentFilename = pwd + std::string(f->GetName());
311+
}
312+
outputs.make<std::string>(o2) = currentFilename;
313+
}
314+
};
315+
auto tryReadTable = [&device, &didir, &fcnt, &ntf, &outputs, &reportTimeframe, &requestedTables, &routeIndex, &skipInvalidRead, skipInvalidReads](TFReaderState currentState) -> TFReaderState {
316+
while (routeIndex < requestedTables.size() &&
317+
(device.inputTimesliceId % requestedTables[routeIndex].maxTimeslices) != requestedTables[routeIndex].timeslice) {
318+
++routeIndex;
319+
}
320+
if (routeIndex == requestedTables.size()) {
321+
return TFReaderState::TIMEFRAME_READ;
238322
}
239323

240-
// create header
324+
auto& route = requestedTables[routeIndex];
241325
auto concrete = DataSpecUtils::asConcreteDataMatcher(route.matcher);
242326
auto dh = header::DataHeader(concrete.description, concrete.origin, concrete.subSpec);
243327
bool wasAOD = std::ranges::any_of(route.matcher.metadata, [](ConfigParamSpec const& p) { return p.name.starts_with("aod-origin-replaced"); });
244328

245-
if (!didir->readTree(outputs, dh, fcnt, ntf, totalSizeCompressed, totalSizeUncompressed, wasAOD)) {
246-
if (first) {
247-
// check if there is a next file to read
329+
try {
330+
if (!didir->readTree(outputs, dh, fcnt, ntf, totalSizeCompressed, totalSizeUncompressed, wasAOD)) {
331+
return TFReaderState::TRY_NEXT_FILE;
332+
}
333+
} catch (InvalidAODReadError const& e) {
334+
if (!skipInvalidReads) {
335+
throw;
336+
}
337+
skipInvalidRead(concrete.origin, e);
338+
return TFReaderState::INVALID_TIMEFRAME;
339+
}
340+
341+
if (currentState == TFReaderState::READ_FIRST_TABLE || currentState == TFReaderState::READ_FIRST_TABLE_FROM_NEXT_FILE) {
342+
reportTimeframe(dh);
343+
}
344+
++routeIndex;
345+
return TFReaderState::READ_NEXT_TABLE;
346+
};
347+
while (readState != TFReaderState::TIMEFRAME_READ) {
348+
switch (readState) {
349+
case TFReaderState::READ_FIRST_TABLE:
350+
readState = tryReadTable(readState);
351+
break;
352+
case TFReaderState::READ_FIRST_TABLE_FROM_NEXT_FILE:
353+
case TFReaderState::READ_NEXT_TABLE:
354+
readState = tryReadTable(readState);
355+
if (readState == TFReaderState::TRY_NEXT_FILE) {
356+
// Once a file has been selected, every requested table must exist.
357+
auto concrete = DataSpecUtils::asConcreteDataMatcher(requestedTables[routeIndex].matcher);
358+
LOGP(fatal, "Can not retrieve tree for table {}: fileCounter {}, timeFrame {}", concrete.origin.as<std::string>(), fcnt, ntf);
359+
throw std::runtime_error("Processing is stopped!");
360+
}
361+
break;
362+
case TFReaderState::TRY_NEXT_FILE:
248363
fcnt += device.maxInputTimeslices;
249364
if (didir->atEnd(fcnt)) {
250365
LOGP(info, "No input files left to read for reader {}!", device.inputTimesliceId);
@@ -254,42 +369,15 @@ AlgorithmSpec AODJAlienReaderHelpers::rootFileReaderCallback(ConfigContext const
254369
control.readyToQuit(QuitRequest::Me);
255370
return;
256371
}
257-
// get first folder of next file
258372
ntf = 0;
259-
if (!didir->readTree(outputs, dh, fcnt, ntf, totalSizeCompressed, totalSizeUncompressed, wasAOD)) {
260-
LOGP(fatal, "Can not retrieve tree for table {}: fileCounter {}, timeFrame {}", concrete.origin.as<std::string>(), fcnt, ntf);
261-
throw std::runtime_error("Processing is stopped!");
262-
}
263-
} else {
264-
LOGP(fatal, "Can not retrieve tree for table {}: fileCounter {}, timeFrame {}", concrete.origin.as<std::string>(), fcnt, ntf);
265-
throw std::runtime_error("Processing is stopped!");
266-
}
267-
}
268-
269-
if (first) {
270-
if (reportTFN) {
271-
// TF number
272-
auto timeFrameNumber = didir->getTimeFrameNumber(dh, fcnt, ntf);
273-
auto o = Output(TFNumberHeader);
274-
outputs.make<uint64_t>(o) = timeFrameNumber;
275-
}
276-
277-
if (reportTFFileName) {
278-
// Origin file name for derived output map
279-
auto o2 = Output(TFFileNameHeader);
280-
auto fileAndFolder = didir->getFileFolder(dh, fcnt, ntf);
281-
auto rootFS = std::dynamic_pointer_cast<TFileFileSystem>(fileAndFolder.filesystem());
282-
auto* f = dynamic_cast<TFile*>(rootFS->GetFile());
283-
std::string currentFilename(f->GetFile()->GetName());
284-
if (strcmp(f->GetEndpointUrl()->GetProtocol(), "file") == 0 && f->GetEndpointUrl()->GetFile()[0] != '/') {
285-
// This is not an absolute local path. Make it absolute.
286-
static std::string pwd = gSystem->pwd() + std::string("/");
287-
currentFilename = pwd + std::string(f->GetName());
288-
}
289-
outputs.make<std::string>(o2) = currentFilename;
290-
}
373+
routeIndex = 0;
374+
readState = TFReaderState::READ_FIRST_TABLE_FROM_NEXT_FILE;
375+
break;
376+
case TFReaderState::INVALID_TIMEFRAME:
377+
return;
378+
case TFReaderState::TIMEFRAME_READ:
379+
break;
291380
}
292-
first = false;
293381
}
294382
int64_t stopSize = totalSizeCompressed;
295383
int64_t bytesDelta = stopSize - startSize;

Framework/AnalysisSupport/src/DataInputDirector.cxx

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
#include <arrow/dataset/file_base.h>
3535
#include <arrow/dataset/dataset.h>
3636
#include <uv.h>
37+
#include <exception>
3738
#include <memory>
3839

3940
#if __has_include(<TJAlienFile.h>)
@@ -536,18 +537,23 @@ bool DataInputDescriptor::readTree(DataAllocator& outputs, header::DataHeader dh
536537
if (!format) {
537538
t.deactivate();
538539
LOGP(debug, "Could not find tree {}. Trying in parent file.", fullpath.path());
539-
auto parentFile = getParentFile(counter, numTF, treename, wantedLevel, wantedOrigin);
540-
if (parentFile != nullptr) {
541-
int parentNumTF = parentFile->findDFNumber(0, folder.path());
542-
if (parentNumTF == -1) {
543-
auto parentRootFS = std::dynamic_pointer_cast<TFileFileSystem>(parentFile->mCurrentFilesystem);
544-
throw std::runtime_error(fmt::format(R"(DF {} listed in parent file map but not found in the corresponding file "{}")", folder.path(), parentRootFS->GetFile()->GetName()));
545-
}
546-
// first argument is 0 as the parent file object contains only 1 file
547-
return parentFile->readTree(outputs, dh, 0, parentNumTF, treename, totalSizeCompressed, totalSizeUncompressed);
540+
std::shared_ptr<DataInputDescriptor> parentFile;
541+
try {
542+
parentFile = getParentFile(counter, numTF, treename, wantedLevel, wantedOrigin);
543+
} catch (...) {
544+
std::throw_with_nested(InvalidAODReadError(fmt::format("Unable to resolve parent file for tree {}", treename)));
545+
}
546+
if (parentFile == nullptr) {
547+
auto rootFS = std::dynamic_pointer_cast<TFileFileSystem>(mCurrentFilesystem);
548+
throw std::runtime_error(fmt::format(R"(Couldn't get TTree "{}" from "{}". Please check https://aliceo2group.github.io/analysis-framework/docs/troubleshooting/#tree-not-found for more information.)", fullpath.path(), rootFS->GetFile()->GetName()));
548549
}
549-
auto rootFS = std::dynamic_pointer_cast<TFileFileSystem>(mCurrentFilesystem);
550-
throw std::runtime_error(fmt::format(R"(Couldn't get TTree "{}" from "{}". Please check https://aliceo2group.github.io/analysis-framework/docs/troubleshooting/#tree-not-found for more information.)", fullpath.path(), rootFS->GetFile()->GetName()));
550+
int parentNumTF = parentFile->findDFNumber(0, folder.path());
551+
if (parentNumTF == -1) {
552+
auto parentRootFS = std::dynamic_pointer_cast<TFileFileSystem>(parentFile->mCurrentFilesystem);
553+
throw InvalidAODReadError(fmt::format(R"(DF {} listed in parent file map but not found in the corresponding file "{}")", folder.path(), parentRootFS->GetFile()->GetName()));
554+
}
555+
// first argument is 0 as the parent file object contains only 1 file
556+
return parentFile->readTree(outputs, dh, 0, parentNumTF, treename, totalSizeCompressed, totalSizeUncompressed);
551557
}
552558

553559
auto schemaOpt = format->Inspect(fullpath);
@@ -573,7 +579,15 @@ bool DataInputDescriptor::readTree(DataAllocator& outputs, header::DataHeader dh
573579
//// add branches to read
574580
//// fill the table
575581
f2b->setLabel(treename.c_str());
576-
f2b->fill(datasetSchema, format);
582+
char const* operation = "read";
583+
try {
584+
f2b->fill(datasetSchema, format);
585+
operation = "finalize";
586+
f2b.release();
587+
} catch (...) {
588+
f2b.discard();
589+
std::throw_with_nested(InvalidAODReadError(fmt::format("Unable to {} tree {}", operation, treename)));
590+
}
577591

578592
return true;
579593
}

Framework/AnalysisSupport/src/DataInputDirector.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <arrow/dataset/dataset.h>
2222

2323
#include <regex>
24+
#include <stdexcept>
2425
#include <vector>
2526
#include "rapidjson/fwd.h"
2627

@@ -32,6 +33,12 @@ class Monitoring;
3233
namespace o2::framework
3334
{
3435

36+
class InvalidAODReadError : public std::runtime_error
37+
{
38+
public:
39+
using std::runtime_error::runtime_error;
40+
};
41+
3542
struct FileNameHolder {
3643
std::string fileName;
3744
int numberOfTimeFrames = 0;

Framework/Core/include/Framework/DataProcessingStats.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ enum struct ProcessingStatsId : short {
7474
CCDB_CACHE_FAILURE,
7575
CCDB_CACHE_FETCHED_BYTES,
7676
CCDB_CACHE_REQUESTED_BYTES,
77+
AOD_INVALID_READ_SKIPPED_TIMEFRAMES,
7778
AVAILABLE_MANAGED_SHM_BASE = 512,
7879
};
7980

Framework/Core/src/CommonServices.cxx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,13 @@ o2::framework::ServiceSpec CommonServices::dataProcessingStats()
11201120
MetricSpec{.name = "dropped_computations", .metricId = static_cast<short>(ProcessingStatsId::DROPPED_COMPUTATIONS), .kind = Kind::UInt64, .minPublishInterval = quickUpdateInterval},
11211121
MetricSpec{.name = "dropped_incoming_messages", .metricId = static_cast<short>(ProcessingStatsId::DROPPED_INCOMING_MESSAGES), .kind = Kind::UInt64, .minPublishInterval = quickUpdateInterval},
11221122
MetricSpec{.name = "relayed_messages", .metricId = static_cast<short>(ProcessingStatsId::RELAYED_MESSAGES), .kind = Kind::UInt64, .minPublishInterval = quickUpdateInterval},
1123+
MetricSpec{.name = "aod-invalid-read-skipped-timeframes",
1124+
.metricId = static_cast<short>(ProcessingStatsId::AOD_INVALID_READ_SKIPPED_TIMEFRAMES),
1125+
.kind = Kind::UInt64,
1126+
.scope = Scope::DPL,
1127+
.minPublishInterval = 0,
1128+
.maxRefreshLatency = 10000,
1129+
.sendInitialValue = true},
11231130
MetricSpec{.name = "arrow-bytes-destroyed",
11241131
.enabled = arrowAndResourceLimitingMetrics,
11251132
.metricId = static_cast<short>(ProcessingStatsId::ARROW_BYTES_DESTROYED),

0 commit comments

Comments
 (0)