Skip to content

Commit 8ab03f5

Browse files
committed
Fixes from codex: leads to aod-writer waiting for inputs
1 parent 8e64c6f commit 8ab03f5

4 files changed

Lines changed: 153 additions & 21 deletions

File tree

Framework/Core/src/CompletionPolicyHelpers.cxx

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,25 @@ CompletionPolicy CompletionPolicyHelpers::consumeWhenAll(const char* name, Compl
175175

176176
CompletionPolicy CompletionPolicyHelpers::consumeWhenAllOrdered(const char* name, CompletionPolicy::Matcher matcher)
177177
{
178-
auto callbackFull = [](InputSpan const& inputs, std::vector<InputSpec> const&, ServiceRegistryRef& ref) -> CompletionPolicy::CompletionOp {
178+
auto callbackFull = [](InputSpan const& inputs, std::vector<InputSpec> const& specs, ServiceRegistryRef& ref) -> CompletionPolicy::CompletionOp {
179+
assert(inputs.size() == specs.size());
179180
auto& decongestionService = ref.get<DecongestionService>();
180181
decongestionService.orderedCompletionPolicyActive = true;
182+
bool hasPresentSporadic = false;
183+
bool hasOrderedInput = false;
184+
bool missingOrderedInput = false;
185+
size_t si = 0;
181186
for (auto& input : inputs) {
187+
auto const& spec = specs[si++];
188+
if (spec.lifetime == Lifetime::Sporadic) {
189+
hasPresentSporadic |= input.header != nullptr;
190+
continue;
191+
}
182192
if (input.header == nullptr) {
183-
return CompletionPolicy::CompletionOp::Wait;
193+
missingOrderedInput = true;
194+
continue;
184195
}
196+
hasOrderedInput = true;
185197
long int startTime = framework::DataRefUtils::getHeader<o2::framework::DataProcessingHeader*>(input)->startTime;
186198
if (startTime == 0) {
187199
LOGP(debug, "startTime is 0, which means we have the first message, so we can process it.");
@@ -191,6 +203,14 @@ CompletionPolicy CompletionPolicyHelpers::consumeWhenAllOrdered(const char* name
191203
return CompletionPolicy::CompletionOp::Retry;
192204
}
193205
}
206+
// Sporadic side inputs, like metadata collected for the AOD writer, are not
207+
// produced for every TF. Process them when they are present, but do not let
208+
// them block or advance the ordered timeframe progression. The device loop
209+
// removes only the sporadic parts for ConsumeExisting, leaving any partial
210+
// ordered TF record in place.
211+
if (missingOrderedInput || !hasOrderedInput) {
212+
return hasPresentSporadic ? CompletionPolicy::CompletionOp::ConsumeExisting : CompletionPolicy::CompletionOp::Wait;
213+
}
194214
decongestionService.nextTimeslice++;
195215
return CompletionPolicy::CompletionOp::ConsumeAndRescan;
196216
};

Framework/Core/src/DataProcessingDevice.cxx

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2263,6 +2263,17 @@ bool DataProcessingDevice::tryDispatchComputation(ServiceRegistryRef ref, std::v
22632263
}
22642264
};
22652265

2266+
auto cleanSporadic = [&currentSetOfInputs](TimesliceSlot, InputRecord& record) {
2267+
assert(record.size() == currentSetOfInputs.size());
2268+
for (size_t ii = 0, ie = record.size(); ii < ie; ++ii) {
2269+
DataRef input = record.getByPos(ii);
2270+
if (input.spec == nullptr || input.spec->lifetime != Lifetime::Sporadic || input.header == nullptr) {
2271+
continue;
2272+
}
2273+
currentSetOfInputs[ii].clear();
2274+
}
2275+
};
2276+
22662277
// Function to cleanup record. For the moment we
22672278
// simply use it to keep track of input messages
22682279
// which are not needed, to display them in the GUI.
@@ -2448,7 +2459,7 @@ bool DataProcessingDevice::tryDispatchComputation(ServiceRegistryRef ref, std::v
24482459

24492460
static bool noCatch = getenv("O2_NO_CATCHALL_EXCEPTIONS") && strcmp(getenv("O2_NO_CATCHALL_EXCEPTIONS"), "0");
24502461

2451-
auto runNoCatch = [&context, ref, &processContext](DataRelayer::RecordAction& action) mutable {
2462+
auto runNoCatch = [&context, ref, &processContext, &record](DataRelayer::RecordAction& action) mutable {
24522463
auto& state = ref.get<DeviceState>();
24532464
auto& spec = ref.get<DeviceSpec const>();
24542465
auto& streamContext = ref.get<StreamContext>();
@@ -2498,8 +2509,18 @@ bool DataProcessingDevice::tryDispatchComputation(ServiceRegistryRef ref, std::v
24982509
}
24992510
}
25002511

2501-
// Notify the sink we just consumed some timeframe data
2502-
if (context.isSink && action.op == CompletionPolicy::CompletionOp::Consume) {
2512+
// Notify the sink we just consumed some timeframe data. A sink can also
2513+
// consume sporadic side inputs, e.g. AOD metadata, without consuming a
2514+
// TF; those records must not emit DPL/SUMMARY feedback.
2515+
auto consumedTimeframeInput = [&record, &spec]() {
2516+
for (size_t ii = 0; ii < spec.inputs.size(); ++ii) {
2517+
if (spec.inputs[ii].matcher.lifetime != Lifetime::Sporadic && record.isValid(ii)) {
2518+
return true;
2519+
}
2520+
}
2521+
return false;
2522+
};
2523+
if (context.isSink && action.op == CompletionPolicy::CompletionOp::Consume && consumedTimeframeInput()) {
25032524
O2_SIGNPOST_EVENT_EMIT(device, pcid, "device", "Sending dpl-summary");
25042525
auto& allocator = ref.get<DataAllocator>();
25052526
allocator.make<int>(OutputRef{"dpl-summary", runtime_hash(spec.name.c_str())}, 1);
@@ -2558,6 +2579,9 @@ bool DataProcessingDevice::tryDispatchComputation(ServiceRegistryRef ref, std::v
25582579
context.postDispatchingCallbacks(processContext);
25592580
ref.get<CallbackService>().call<CallbackService::Id::DataConsumed>(o2::framework::ServiceRegistryRef{ref});
25602581
}
2582+
if (action.op == CompletionPolicy::CompletionOp::ConsumeExisting) {
2583+
cleanSporadic(action.slot, record);
2584+
}
25612585
if ((context.forwardPolicy == ForwardPolicy::AfterProcessing) && hasForwards && consumeSomething) {
25622586
O2_SIGNPOST_EVENT_EMIT(device, aid, "device", "Late forwarding");
25632587
auto& timesliceIndex = ref.get<TimesliceIndex>();

Framework/Core/src/WorkflowHelpers.cxx

Lines changed: 60 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -586,9 +586,27 @@ void WorkflowHelpers::injectServiceDevices(WorkflowSpec& workflow, ConfigContext
586586
extraSpecs.push_back(rootSink);
587587
}
588588

589-
// Inject a collector which merges all META messages and republishes them as
590-
// the AOD metadata keys/vals the AOD writer writes into the AO2D file.
591-
if (hasMetaOutput) {
589+
auto hasWritableAODOutput = std::ranges::any_of(workflow, [](DataProcessorSpec const& spec) {
590+
return std::ranges::any_of(spec.outputs, [](OutputSpec const& output) {
591+
return DataSpecUtils::partialMatch(output, writableAODOrigins);
592+
});
593+
});
594+
auto hasTFNumberOutput = std::ranges::any_of(workflow, [](DataProcessorSpec const& spec) {
595+
return std::ranges::any_of(spec.outputs, [](OutputSpec const& output) {
596+
return DataSpecUtils::match(output, "TFN", "TFNumber", 0);
597+
});
598+
});
599+
auto hasTFFileNameOutput = std::ranges::any_of(workflow, [](DataProcessorSpec const& spec) {
600+
return std::ranges::any_of(spec.outputs, [](OutputSpec const& output) {
601+
return DataSpecUtils::match(output, "TFF", "TFFilename", 0);
602+
});
603+
});
604+
605+
// Inject a collector which merges META messages only when the same topology
606+
// can also write the resulting AMD metadata to an AO2D. Otherwise split
607+
// detector subworkflows get an internal collector whose AMD output has no
608+
// meaningful AOD writer route and the service device can hang the workflow.
609+
if (hasMetaOutput && (hasWritableAODOutput || (hasTFNumberOutput && hasTFFileNameOutput))) {
592610
extraSpecs.push_back(AnalysisSupportHelpers::getMetadataCollectorSink(ctx));
593611
}
594612

@@ -753,13 +771,38 @@ void WorkflowHelpers::injectAODWriter(WorkflowSpec& workflow, ConfigContext cons
753771
// create DataOutputDescriptor
754772
std::shared_ptr<DataOutputDirector> dod = AnalysisSupportHelpers::getDataOutputDirector(ctx);
755773

774+
auto hasWritableAOD = std::ranges::any_of(dec.outputsInputs, [](InputSpec const& spec) {
775+
return DataSpecUtils::partialMatch(spec, writableAODOrigins);
776+
});
777+
auto hasTFNumber = std::ranges::any_of(dec.outputsInputs, [](InputSpec const& spec) {
778+
return DataSpecUtils::partialMatch(spec, header::DataOrigin{"TFN"}) &&
779+
DataSpecUtils::partialMatch(spec, header::DataDescription{"TFNumber"});
780+
});
781+
auto hasTFFileName = std::ranges::any_of(dec.outputsInputs, [](InputSpec const& spec) {
782+
return DataSpecUtils::partialMatch(spec, header::DataOrigin{"TFF"}) &&
783+
DataSpecUtils::partialMatch(spec, header::DataDescription{"TFFilename"});
784+
});
785+
auto canWriteMetadataOnly = hasTFNumber && hasTFFileName;
786+
756787
// select outputs of type AOD which need to be saved
757788
dec.outputsInputsAOD.clear();
758789
for (auto ii = 0u; ii < dec.outputsInputs.size(); ii++) {
759-
if (DataSpecUtils::partialMatch(dec.outputsInputs[ii], AODOrigins)) {
760-
auto ds = dod->getDataOutputDescriptors(dec.outputsInputs[ii]);
761-
if (ds.size() > 0 || dec.isDangling[ii]) {
762-
dec.outputsInputsAOD.emplace_back(dec.outputsInputs[ii]);
790+
auto const& input = dec.outputsInputs[ii];
791+
if (!DataSpecUtils::partialMatch(input, AODOrigins)) {
792+
continue;
793+
}
794+
if (DataSpecUtils::partialMatch(input, header::DataOrigin{"AMD"}) && !hasWritableAOD && !canWriteMetadataOnly) {
795+
continue;
796+
}
797+
auto ds = dod->getDataOutputDescriptors(input);
798+
if (ds.size() > 0 || dec.isDangling[ii]) {
799+
dec.outputsInputsAOD.emplace_back(input);
800+
if (DataSpecUtils::partialMatch(input, header::DataOrigin{"AMD"})) {
801+
// Metadata collected from META outputs is republished as AMD sporadic
802+
// data. Keep the AOD writer input sporadic so the ordered completion
803+
// policy can consume it as side information without requiring it for
804+
// every TF.
805+
dec.outputsInputsAOD.back().lifetime = Lifetime::Sporadic;
763806
}
764807
}
765808
}
@@ -772,15 +815,16 @@ void WorkflowHelpers::injectAODWriter(WorkflowSpec& workflow, ConfigContext cons
772815
auto fileSink = AnalysisSupportHelpers::getGlobalAODSink(ctx);
773816
workflow.push_back(fileSink);
774817

775-
auto it = std::find_if(dec.outputsInputs.begin(), dec.outputsInputs.end(), [](InputSpec const& spec) -> bool {
776-
return DataSpecUtils::partialMatch(spec, o2::header::DataOrigin("TFN"));
777-
});
778-
dec.isDangling[std::distance(dec.outputsInputs.begin(), it)] = false;
779-
780-
it = std::find_if(dec.outputsInputs.begin(), dec.outputsInputs.end(), [](InputSpec const& spec) -> bool {
781-
return DataSpecUtils::partialMatch(spec, o2::header::DataOrigin("TFF"));
782-
});
783-
dec.isDangling[std::distance(dec.outputsInputs.begin(), it)] = false;
818+
auto markAsMatched = [&](header::DataOrigin origin) {
819+
auto it = std::find_if(dec.outputsInputs.begin(), dec.outputsInputs.end(), [&](InputSpec const& spec) -> bool {
820+
return DataSpecUtils::partialMatch(spec, origin);
821+
});
822+
if (it != dec.outputsInputs.end()) {
823+
dec.isDangling[std::distance(dec.outputsInputs.begin(), it)] = false;
824+
}
825+
};
826+
markAsMatched(header::DataOrigin{"TFN"});
827+
markAsMatched(header::DataOrigin{"TFF"});
784828
}
785829
}
786830

Framework/Core/test/test_WorkflowHelpers.cxx

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <algorithm>
2121
#include <memory>
2222
#include <list>
23+
#include <string_view>
2324

2425
using namespace o2::framework;
2526

@@ -494,6 +495,49 @@ TEST_CASE("TestExternalInput")
494495
availableForwardsInfo);
495496
}
496497

498+
TEST_CASE("TestMetadataOnlyAODWriterInjection")
499+
{
500+
auto hasProcessor = [](WorkflowSpec const& workflow, std::string_view name) {
501+
return std::ranges::any_of(workflow, [name](DataProcessorSpec const& spec) {
502+
return spec.name == name;
503+
});
504+
};
505+
auto findProcessor = [](WorkflowSpec const& workflow, std::string_view name) {
506+
return std::ranges::find_if(workflow, [name](DataProcessorSpec const& spec) {
507+
return spec.name == name;
508+
});
509+
};
510+
511+
WorkflowSpec metadataOnlyWorkflow{
512+
{.name = "metadata-producer",
513+
.outputs = {OutputSpec{"META", "TRACKER", 0, Lifetime::Sporadic}}}};
514+
515+
auto context = makeEmptyConfigContext();
516+
WorkflowHelpers::injectServiceDevices(metadataOnlyWorkflow, *context);
517+
518+
REQUIRE_FALSE(hasProcessor(metadataOnlyWorkflow, "internal-dpl-metadata-collector"));
519+
REQUIRE_FALSE(hasProcessor(metadataOnlyWorkflow, "internal-dpl-aod-writer"));
520+
521+
WorkflowSpec metadataWithTFSourceWorkflow{
522+
{.name = "metadata-producer",
523+
.outputs = {OutputSpec{"META", "TRACKER", 0, Lifetime::Sporadic}}},
524+
{.name = "tf-source",
525+
.outputs = {OutputSpec{"TFN", "TFNumber"}, OutputSpec{"TFF", "TFFilename"}}}};
526+
527+
context = makeEmptyConfigContext();
528+
WorkflowHelpers::injectServiceDevices(metadataWithTFSourceWorkflow, *context);
529+
530+
REQUIRE(hasProcessor(metadataWithTFSourceWorkflow, "internal-dpl-metadata-collector"));
531+
REQUIRE(hasProcessor(metadataWithTFSourceWorkflow, "internal-dpl-aod-writer"));
532+
auto writer = findProcessor(metadataWithTFSourceWorkflow, "internal-dpl-aod-writer");
533+
REQUIRE(writer != metadataWithTFSourceWorkflow.end());
534+
for (auto const& input : writer->inputs) {
535+
if (DataSpecUtils::partialMatch(input, o2::header::DataOrigin{"AMD"})) {
536+
REQUIRE(input.lifetime == Lifetime::Sporadic);
537+
}
538+
}
539+
}
540+
497541
TEST_CASE("DetermineDanglingOutputs")
498542
{
499543
WorkflowSpec workflow0{

0 commit comments

Comments
 (0)