Project
Loading...
Searching...
No Matches
WorkflowHelpers.cxx
Go to the documentation of this file.
1// Copyright 2019-2020 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
11#include "WorkflowHelpers.h"
28#include "Framework/Signpost.h"
30
31#include "Framework/Variant.h"
32#include "Headers/DataHeader.h"
33#include <algorithm>
34#include <list>
35#include <set>
36#include <utility>
37#include <vector>
38#include <climits>
39#include <numeric>
40
41O2_DECLARE_DYNAMIC_LOG(workflow_helpers);
42
43namespace o2::framework
44{
45std::ostream& operator<<(std::ostream& out, TopoIndexInfo const& info)
46{
47 out << "(" << info.index << ", " << info.layer << ")";
48 return out;
49}
50
51std::vector<TopoIndexInfo>
53 int const* edgeIn,
54 int const* edgeOut,
55 size_t byteStride,
56 size_t edgesCount)
57{
58 size_t stride = byteStride / sizeof(int);
59 using EdgeIndex = int;
60 // Create the index which will be returned.
61 std::vector<TopoIndexInfo> index(nodeCount);
62 for (auto wi = 0; static_cast<size_t>(wi) < nodeCount; ++wi) {
63 index[wi] = {wi, 0};
64 }
65 std::vector<EdgeIndex> remainingEdgesIndex(edgesCount);
66 for (EdgeIndex ei = 0; static_cast<size_t>(ei) < edgesCount; ++ei) {
67 remainingEdgesIndex[ei] = ei;
68 }
69
70 // Create a vector where at each position we have true
71 // if the vector has dependencies, false otherwise
72 std::vector<bool> nodeDeps(nodeCount, false);
73 for (EdgeIndex ei = 0; static_cast<size_t>(ei) < edgesCount; ++ei) {
74 nodeDeps[*(edgeOut + ei * stride)] = true;
75 }
76
77 // We start with all those which do not have any dependencies
78 // They are layer 0.
79 std::list<TopoIndexInfo> L;
80 for (auto ii = 0; static_cast<size_t>(ii) < index.size(); ++ii) {
81 if (nodeDeps[ii] == false) {
82 L.push_back({ii, 0});
83 }
84 }
85
86 // The final result.
87 std::vector<TopoIndexInfo> S;
88 // The set of vertices which can be reached by the current node
89 std::set<TopoIndexInfo> nextVertex;
90 // The set of edges which are not related to the current node.
91 std::vector<EdgeIndex> nextEdges;
92 while (!L.empty()) {
93 auto node = L.front();
94 S.push_back(node);
95 L.pop_front();
96 nextVertex.clear();
97 nextEdges.clear();
98
99 // After this, nextVertex will contain all the vertices
100 // which have the current node as incoming.
101 // nextEdges will contain all the edges which are not related
102 // to the current node.
103 for (auto const& ei : remainingEdgesIndex) {
104 if (*(edgeIn + ei * stride) == node.index) {
105 nextVertex.insert({*(edgeOut + ei * stride), node.layer + 1});
106 } else {
107 nextEdges.push_back(ei);
108 }
109 }
110 remainingEdgesIndex.swap(nextEdges);
111
112 // Of all the vertices which have node as incoming,
113 // check if there is any other incoming node.
114 std::set<TopoIndexInfo> hasPredecessors;
115 for (auto const& ei : remainingEdgesIndex) {
116 for (auto& m : nextVertex) {
117 if (m.index == *(edgeOut + ei * stride)) {
118 hasPredecessors.insert({m.index, m.layer});
119 }
120 }
121 }
122 std::vector<TopoIndexInfo> withPredecessor;
123 std::set_difference(nextVertex.begin(), nextVertex.end(),
124 hasPredecessors.begin(), hasPredecessors.end(),
125 std::back_inserter(withPredecessor));
126 std::copy(withPredecessor.begin(), withPredecessor.end(), std::back_inserter(L));
127 }
128 return S;
129}
130
131// get the default value for condition-backend
133{
134 static bool explicitBackend = getenv("DPL_CONDITION_BACKEND");
135 static DeploymentMode deploymentMode = DefaultsHelpers::deploymentMode();
136 if (explicitBackend) {
137 return getenv("DPL_CONDITION_BACKEND");
138 } else if (deploymentMode == DeploymentMode::OnlineDDS || deploymentMode == DeploymentMode::OnlineECS) {
139 return "http://o2-ccdb.internal";
140 } else {
141 return "http://alice-ccdb.cern.ch";
142 }
143}
144
145// get the default value for condition query rate
147{
148 return getenv("DPL_CONDITION_QUERY_RATE") ? std::stoi(getenv("DPL_CONDITION_QUERY_RATE")) : 0;
149}
150
151// get the default value for condition query rate multiplier
153{
154 return getenv("DPL_CONDITION_QUERY_RATE_MULTIPLIER") ? std::stoi(getenv("DPL_CONDITION_QUERY_RATE_MULTIPLIER")) : 1;
155}
156
158{
159 int rateLimitingIPCID = std::stoi(ctx.options().get<std::string>("timeframes-rate-limit-ipcid"));
160 DataProcessorSpec ccdbBackend{
161 .name = "internal-dpl-ccdb-backend",
162 .outputs = {},
163 .options = {{"condition-backend", VariantType::String, defaultConditionBackend(), {"URL for CCDB"}},
164 {"condition-timestamp", VariantType::Int64, 0ll, {"Force timestamp for CCDB lookup"}},
165 {"condition-not-before", VariantType::Int64, 0ll, {"do not fetch from CCDB objects created before provide timestamp"}},
166 {"condition-not-after", VariantType::Int64, 3385078236000ll, {"do not fetch from CCDB objects created after the timestamp"}},
167 {"condition-remap", VariantType::String, "", {"remap condition path in CCDB based on the provided string."}},
168 {"condition-tf-per-query", VariantType::Int, defaultConditionQueryRate(), {"check condition validity per requested number of TFs, fetch only once if <=0"}},
169 {"condition-tf-per-query-multiplier", VariantType::Int, defaultConditionQueryRateMultiplier(), {"check conditions once per this amount of nominal checks (>0) or on module of TFcounter (<0)"}},
170 {"condition-use-slice-for-prescaling", VariantType::Int, 0, {"use TFslice instead of TFcounter to control validation frequency. If > query rate, do not allow TFCounter excursion exceeding it"}},
171 {"condition-time-tolerance", VariantType::Int64, 5000ll, {"prefer creation time if its difference to orbit-derived time exceeds threshold (ms), impose if <0"}},
172 {"orbit-offset-enumeration", VariantType::Int64, 0ll, {"initial value for the orbit"}},
173 {"orbit-multiplier-enumeration", VariantType::Int64, 0ll, {"multiplier to get the orbit from the counter"}},
174 {"start-value-enumeration", VariantType::Int64, 0ll, {"initial value for the enumeration"}},
175 {"end-value-enumeration", VariantType::Int64, -1ll, {"final value for the enumeration"}},
176 {"step-value-enumeration", VariantType::Int64, 1ll, {"step between one value and the other"}}},
177 };
178 DataProcessorSpec analysisCCDBBackend{
179 .name = "internal-dpl-aod-ccdb",
180 .inputs = {},
181 .outputs = {},
182 .algorithm = AlgorithmSpec::dummyAlgorithm(),
183 .options = {{"condition-backend", VariantType::String, defaultConditionBackend(), {"URL for CCDB"}},
184 {"condition-not-before", VariantType::Int64, 0ll, {"do not fetch from CCDB objects created before provide timestamp"}},
185 {"condition-not-after", VariantType::Int64, 3385078236000ll, {"do not fetch from CCDB objects created after the timestamp"}},
186 {"condition-remap", VariantType::String, "", {"remap condition path in CCDB based on the provided string."}},
187 {"condition-tf-per-query", VariantType::Int, defaultConditionQueryRate(), {"check condition validity per requested number of TFs, fetch only once if <=0"}},
188 {"condition-tf-per-query-multiplier", VariantType::Int, defaultConditionQueryRateMultiplier(), {"check conditions once per this amount of nominal checks (>0) or on module of TFcounter (<0)"}},
189 {"condition-use-slice-for-prescaling", VariantType::Int, 0, {"use TFslice instead of TFcounter to control validation frequency. If > query rate, do not allow TFCounter excursion exceeding it"}},
190 {"condition-time-tolerance", VariantType::Int64, 5000ll, {"prefer creation time if its difference to orbit-derived time exceeds threshold (ms), impose if <0"}},
191 {"start-value-enumeration", VariantType::Int64, 0ll, {"initial value for the enumeration"}},
192 {"end-value-enumeration", VariantType::Int64, -1ll, {"final value for the enumeration"}},
193 {"step-value-enumeration", VariantType::Int64, 1ll, {"step between one value and the other"}}}};
194 DataProcessorSpec transientStore{"internal-dpl-transient-store",
195 {},
196 {},
198 DataProcessorSpec qaStore{"internal-dpl-qa-store",
199 {},
200 {},
202 DataProcessorSpec timer{"internal-dpl-clock",
203 {},
204 {},
206
207 // In case InputSpec of origin AOD are
208 // requested but not available as part of the workflow,
209 // we insert in the configuration something which
210 // reads them from file.
211 //
212 // FIXME: source branch is DataOrigin, for the moment. We should
213 // make it configurable via ConfigParamsOptions
214 auto aodLifetime = Lifetime::Enumeration;
215
216 DataProcessorSpec aodReader{
217 .name = "internal-dpl-aod-reader",
218 .inputs = {InputSpec{"enumeration",
219 "DPL",
220 "ENUM",
221 static_cast<DataAllocator::SubSpecificationType>(compile_time_hash("internal-dpl-aod-reader")),
222 aodLifetime}},
223 .algorithm = AlgorithmSpec::dummyAlgorithm(),
224 .options = {ConfigParamSpec{"aod-file-private", VariantType::String, ctx.options().get<std::string>("aod-file"), {"AOD file"}},
225 ConfigParamSpec{"aod-max-io-rate", VariantType::Float, 0.f, {"Maximum I/O rate in MB/s"}},
226 ConfigParamSpec{"aod-reader-json", VariantType::String, {"json configuration file"}},
227 ConfigParamSpec{"time-limit", VariantType::Int64, 0ll, {"Maximum run time limit in seconds"}},
228 ConfigParamSpec{"orbit-offset-enumeration", VariantType::Int64, 0ll, {"initial value for the orbit"}},
229 ConfigParamSpec{"orbit-multiplier-enumeration", VariantType::Int64, 0ll, {"multiplier to get the orbit from the counter"}},
230 ConfigParamSpec{"start-value-enumeration", VariantType::Int64, 0ll, {"initial value for the enumeration"}},
231 ConfigParamSpec{"end-value-enumeration", VariantType::Int64, -1ll, {"final value for the enumeration"}},
232 ConfigParamSpec{"step-value-enumeration", VariantType::Int64, 1ll, {"step between one value and the other"}}},
233 .requiredServices = CommonServices::defaultServices("O2FrameworkAnalysisSupport:RunSummary")};
234
235 ctx.services().registerService(ServiceRegistryHelpers::handleForService<DanglingEdgesContext>(new DanglingEdgesContext));
236 auto& dec = ctx.services().get<DanglingEdgesContext>();
237
238 std::vector<InputSpec> requestedCCDBs;
239 std::vector<OutputSpec> providedCCDBs;
240
241 for (size_t wi = 0; wi < workflow.size(); ++wi) {
242 auto& processor = workflow[wi];
243 auto name = processor.name;
244 uint32_t hash = runtime_hash(name.c_str());
245 dec.outTskMap.push_back({hash, name});
246
247 std::string prefix = "internal-dpl-";
248 if (processor.inputs.empty() && processor.name.compare(0, prefix.size(), prefix) != 0) {
249 processor.inputs.push_back(InputSpec{"enumeration", "DPL", "ENUM", static_cast<DataAllocator::SubSpecificationType>(runtime_hash(processor.name.c_str())), Lifetime::Enumeration});
250 ConfigParamsHelper::addOptionIfMissing(processor.options, ConfigParamSpec{"orbit-offset-enumeration", VariantType::Int64, 0ll, {"1st injected orbit"}});
251 ConfigParamsHelper::addOptionIfMissing(processor.options, ConfigParamSpec{"orbit-multiplier-enumeration", VariantType::Int64, 0ll, {"orbits/TForbit"}});
252 processor.options.push_back(ConfigParamSpec{"start-value-enumeration", VariantType::Int64, 0ll, {"initial value for the enumeration"}});
253 processor.options.push_back(ConfigParamSpec{"end-value-enumeration", VariantType::Int64, -1ll, {"final value for the enumeration"}});
254 processor.options.push_back(ConfigParamSpec{"step-value-enumeration", VariantType::Int64, 1ll, {"step between one value and the other"}});
255 }
256 bool hasTimeframeInputs = std::ranges::any_of(processor.inputs, [](auto const& input) { return input.lifetime == Lifetime::Timeframe; });
257 bool hasTimeframeOutputs = std::ranges::any_of(processor.outputs, [](auto const& output) { return output.lifetime == Lifetime::Timeframe; });
258
259 // A timeframeSink consumes timeframes without creating new
260 // timeframe data.
261 bool timeframeSink = hasTimeframeInputs && !hasTimeframeOutputs;
262 if (rateLimitingIPCID != -1) {
263 if (timeframeSink && processor.name.find("internal-dpl-injected-dummy-sink") == std::string::npos) {
264 O2_SIGNPOST_ID_GENERATE(sid, workflow_helpers);
265 bool hasMatch = false;
266 ConcreteDataMatcher summaryMatcher = ConcreteDataMatcher{"DPL", "SUMMARY", static_cast<DataAllocator::SubSpecificationType>(hash)};
267 auto summaryOutput = std::ranges::find_if(processor.outputs, [&summaryMatcher](auto const& output) { return DataSpecUtils::match(output, summaryMatcher); });
268 if (summaryOutput != processor.outputs.end()) {
269 O2_SIGNPOST_EVENT_EMIT(workflow_helpers, sid, "output enumeration", "%{public}s already there in %{public}s",
270 DataSpecUtils::describe(*summaryOutput).c_str(), processor.name.c_str());
271 hasMatch = true;
272 }
273
274 if (!hasMatch) {
275 O2_SIGNPOST_EVENT_EMIT(workflow_helpers, sid, "output enumeration", "Adding DPL/SUMMARY/%d to %{public}s", hash, processor.name.c_str());
276 processor.outputs.push_back(OutputSpec{{"dpl-summary"}, ConcreteDataMatcher{"DPL", "SUMMARY", static_cast<DataAllocator::SubSpecificationType>(hash)}});
277 }
278 }
279 }
280 bool hasConditionOption = false;
281 for (size_t ii = 0; ii < processor.inputs.size(); ++ii) {
282 auto& input = processor.inputs[ii];
283 bool hasProjectors = false;
284 bool hasIndexRecords = false;
285 bool hasCCDBURLs = false;
286 bool wasAOD = false;
287 // all three options are exclusive
288 for (auto const& p : input.metadata) {
289 if (p.name.starts_with("aod-origin-replaced")) {
290 wasAOD = true;
291 }
292 if (p.name.compare("projectors") == 0) {
293 hasProjectors = true;
294 break;
295 }
296 if (p.name.compare("index-records") == 0) {
297 hasIndexRecords = true;
298 break;
299 }
300 if (p.name.starts_with("ccdb:")) {
301 hasCCDBURLs = true;
302 break;
303 }
304 }
305 switch (input.lifetime) {
306 case Lifetime::Timer: {
307 auto concrete = DataSpecUtils::asConcreteDataMatcher(input);
308 auto hasOption = std::ranges::any_of(processor.options, [&input](auto const& option) { return (option.name == "period-" + input.binding); });
309 if (hasOption == false) {
310 processor.options.push_back(ConfigParamSpec{"period-" + input.binding, VariantType::Int, 1000, {"period of the timer in milliseconds"}});
311 }
312 timer.outputs.emplace_back(OutputSpec{concrete.origin, concrete.description, concrete.subSpec, Lifetime::Timer});
313 } break;
314 case Lifetime::Signal: {
315 auto concrete = DataSpecUtils::asConcreteDataMatcher(input);
316 timer.outputs.emplace_back(OutputSpec{concrete.origin, concrete.description, concrete.subSpec, Lifetime::Signal});
317 } break;
318 case Lifetime::Enumeration: {
319 auto concrete = DataSpecUtils::asConcreteDataMatcher(input);
320 timer.outputs.emplace_back(OutputSpec{concrete.origin, concrete.description, concrete.subSpec, Lifetime::Enumeration});
321 } break;
322 case Lifetime::Condition: {
323 requestedCCDBs.emplace_back(input);
324 if ((hasConditionOption == false) && std::ranges::none_of(processor.options, [](auto const& option) { return (option.name.compare("condition-backend") == 0); })) {
325 processor.options.emplace_back(ConfigParamSpec{"condition-backend", VariantType::String, defaultConditionBackend(), {"URL for CCDB"}});
326 processor.options.emplace_back(ConfigParamSpec{"condition-timestamp", VariantType::Int64, 0ll, {"Force timestamp for CCDB lookup"}});
327 hasConditionOption = true;
328 }
329 } break;
330 case Lifetime::OutOfBand: {
331 auto concrete = DataSpecUtils::asConcreteDataMatcher(input);
332 auto hasOption = std::ranges::any_of(processor.options, [&input](auto const& option) { return (option.name == "out-of-band-channel-name-" + input.binding); });
333 if (hasOption == false) {
334 processor.options.push_back(ConfigParamSpec{"out-of-band-channel-name-" + input.binding, VariantType::String, "out-of-band", {"channel to listen for out of band data"}});
335 }
336 timer.outputs.emplace_back(OutputSpec{concrete.origin, concrete.description, concrete.subSpec, Lifetime::Enumeration});
337 } break;
338 case Lifetime::QA:
339 case Lifetime::Transient:
340 case Lifetime::Timeframe:
341 case Lifetime::Optional:
342 break;
343 }
344 if (hasProjectors) {
345 DataSpecUtils::updateInputList(dec.requestedDYNs, InputSpec{input});
346 } else if (hasIndexRecords) {
347 DataSpecUtils::updateInputList(dec.requestedIDXs, InputSpec{input});
348 } else if (hasCCDBURLs) {
349 DataSpecUtils::updateInputList(dec.requestedTIMs, InputSpec{input});
350 } else if (DataSpecUtils::partialMatch(input, AODOrigins) || wasAOD) {
351 DataSpecUtils::updateInputList(dec.requestedAODs, InputSpec{input});
352 }
353 }
354
355 std::ranges::stable_sort(timer.outputs, [](OutputSpec const& a, OutputSpec const& b) { return *DataSpecUtils::getOptionalSubSpec(a) < *DataSpecUtils::getOptionalSubSpec(b); });
356
357 for (auto& output : processor.outputs) {
358 bool hasProjectors = false;
359 bool hasIndexRecords = false;
360 bool hasCCDBURLs = false;
361 bool wasAOD = false;
362 // all three options are exclusive
363 for (auto const& p : output.metadata) {
364 if (p.name.starts_with("aod-origin-replaced")) {
365 wasAOD = true;
366 }
367 if (p.name.compare("projectors") == 0) {
368 hasProjectors = true;
369 break;
370 }
371 if (p.name.compare("index-records") == 0) {
372 hasIndexRecords = true;
373 break;
374 }
375 if (p.name.starts_with("ccdb:")) {
376 hasCCDBURLs = true;
377 break;
378 }
379 }
380 if (hasProjectors) {
381 dec.providedDYNs.emplace_back(output);
382 } else if (hasCCDBURLs) {
383 dec.providedTIMs.emplace_back(output);
384 } else if (hasIndexRecords) {
385 dec.providedIDXs.emplace_back(output);
386 } else if (DataSpecUtils::partialMatch(output, AODOrigins) || wasAOD) {
387 dec.providedAODs.emplace_back(output);
389 dec.providedOutputObjHist.emplace_back(output);
390 auto it = std::ranges::find_if(dec.outObjHistMap, [&](auto&& x) { return x.id == hash; });
391 if (it == dec.outObjHistMap.end()) {
392 dec.outObjHistMap.push_back({hash, {output.binding.value}});
393 } else {
394 it->bindings.push_back(output.binding.value);
395 }
396 }
397
398 if (output.lifetime == Lifetime::Condition) {
399 providedCCDBs.push_back(output);
400 }
401 }
402 }
403
404 auto inputSpecLessThan = [](InputSpec const& lhs, InputSpec const& rhs) { return DataSpecUtils::describe(lhs) < DataSpecUtils::describe(rhs); };
405 auto outputSpecLessThan = [](OutputSpec const& lhs, OutputSpec const& rhs) { return DataSpecUtils::describe(lhs) < DataSpecUtils::describe(rhs); };
406
407 DataProcessorSpec indexBuilder{
408 "internal-dpl-aod-index-builder",
409 {},
410 {},
411 AlgorithmSpec::dummyAlgorithm(), // real algorithm will be set in adjustTopology
412 {}};
413 std::ranges::sort(dec.requestedIDXs, inputSpecLessThan);
414 std::ranges::sort(dec.providedIDXs, outputSpecLessThan);
415 dec.requestedIDXs | views::filter_not_matching(dec.providedIDXs) | sinks::append_to{dec.builderInputs};
416 AnalysisSupportHelpers::addMissingOutputsToBuilder(dec.builderInputs, dec.requestedAODs, dec.requestedDYNs, indexBuilder);
417
418 std::ranges::sort(dec.requestedTIMs, inputSpecLessThan);
419 std::ranges::sort(dec.providedTIMs, outputSpecLessThan);
420 dec.requestedTIMs | views::filter_not_matching(dec.providedTIMs) | sinks::append_to{dec.analysisCCDBInputs};
421 AnalysisSupportHelpers::addMissingOutputsToBuilder(dec.analysisCCDBInputs, dec.requestedAODs, dec.requestedDYNs, analysisCCDBBackend);
422
423 std::ranges::sort(dec.requestedDYNs, inputSpecLessThan);
424 std::ranges::sort(dec.providedDYNs, outputSpecLessThan);
425 dec.requestedDYNs | views::filter_not_matching(dec.providedDYNs) | sinks::append_to{dec.spawnerInputs};
426
427 DataProcessorSpec aodSpawner{
428 "internal-dpl-aod-spawner",
429 {},
430 {},
431 AlgorithmSpec::dummyAlgorithm(), // real algorithm will be set in adjustTopology
432 {}};
433 AnalysisSupportHelpers::addMissingOutputsToSpawner({}, dec.spawnerInputs, dec.requestedAODs, aodSpawner);
434
435 std::ranges::sort(dec.requestedAODs, inputSpecLessThan);
436 std::ranges::sort(dec.providedAODs, outputSpecLessThan);
437 AnalysisSupportHelpers::addMissingOutputsToReader(dec.providedAODs, dec.requestedAODs, aodReader);
438
439 std::ranges::sort(requestedCCDBs, inputSpecLessThan);
440 std::ranges::sort(providedCCDBs, outputSpecLessThan);
441 AnalysisSupportHelpers::addMissingOutputsToReader(providedCCDBs, requestedCCDBs, ccdbBackend);
442
443 std::vector<DataProcessorSpec> extraSpecs;
444
445 if (transientStore.outputs.empty() == false) {
446 extraSpecs.push_back(transientStore);
447 }
448 if (qaStore.outputs.empty() == false) {
449 extraSpecs.push_back(qaStore);
450 }
451
452 if (aodSpawner.outputs.empty() == false) {
453 extraSpecs.push_back(timePipeline(aodSpawner, ctx.options().get<int64_t>("spawners")));
454 }
455
456 if (indexBuilder.outputs.empty() == false) {
457 extraSpecs.push_back(indexBuilder);
458 }
459
460 // add the Analysys CCDB backend which reads CCDB objects using a provided table
462 if (deploymentMode != DeploymentMode::OnlineDDS && deploymentMode != DeploymentMode::OnlineECS) {
463 if (analysisCCDBBackend.outputs.empty() == false) {
464 extraSpecs.push_back(analysisCCDBBackend);
465 }
466 }
467
468 auto tfnsource = std::ranges::find_if(workflow, [](DataProcessorSpec const& spec) {
469 return std::ranges::any_of(spec.outputs, [](OutputSpec const& output) {
470 return DataSpecUtils::match(output, "TFN", "TFNumber", 0);
471 });
472 });
473
474 // add the reader
475 if (aodReader.outputs.empty() == false) {
476 if (tfnsource == workflow.end()) {
477 // add normal reader
478 aodReader.outputs.emplace_back(OutputSpec{"TFN", "TFNumber"});
479 aodReader.outputs.emplace_back(OutputSpec{"TFF", "TFFilename"});
480 } else {
481 // AODs are being injected the tfnsource is the entry point, add error-handler reader
482 aodReader.algorithm = AlgorithmSpec{
484 [](DeviceSpec const& spec) {
485 LOGP(warn, "Workflow with injected AODs has unsatisfied inputs:");
486 for (auto const& output : spec.outputs) {
487 LOGP(warn, " {}", DataSpecUtils::describe(output.matcher));
488 }
489 LOGP(fatal, "Stopping.");
490 // to ensure the output type for adaptStateful
491 return adaptStateless([](DataAllocator&) {});
492 })};
493 }
494 auto concrete = DataSpecUtils::asConcreteDataMatcher(aodReader.inputs[0]);
495 timer.outputs.emplace_back(concrete.origin, concrete.description, concrete.subSpec, Lifetime::Enumeration);
496 extraSpecs.push_back(timePipeline(aodReader, ctx.options().get<int64_t>("readers")));
497 }
498
499 InputSpec matcher{"dstf", "FLP", "DISTSUBTIMEFRAME", 0xccdb};
500 auto& dstf = std::get<ConcreteDataMatcher>(matcher.matcher);
501 // Check if any of the provided outputs is a DISTSTF
502 // Check if any of the requested inputs is for a 0xccdb message
503 bool providesDISTSTF = std::ranges::any_of(workflow,
504 [&matcher](auto const& dp) {
505 return std::any_of(dp.outputs.begin(), dp.outputs.end(), [&matcher](auto const& output) {
506 return DataSpecUtils::match(matcher, output);
507 });
508 });
509
510 // If there is no CCDB requested, but we still ask for a FLP/DISTSUBTIMEFRAME/0xccdb
511 // we add to the first data processor which has no inputs (apart from
512 // enumerations / timers) the responsibility to provide the DISTSUBTIMEFRAME
513 bool requiresDISTSUBTIMEFRAME = std::ranges::any_of(workflow,
514 [&dstf](auto const& dp) {
515 return std::any_of(dp.inputs.begin(), dp.inputs.end(), [&dstf](auto const& input) {
516 return DataSpecUtils::match(input, dstf);
517 });
518 });
519
520 // We find the first device which has either just enumerations or
521 // just timers, and we will add the DISTSUBTIMEFRAME to it.
522 // Notice how we do so in a stable manner by sorting the devices
523 // by name.
524 int enumCandidate = -1;
525 int timerCandidate = -1;
526 for (auto wi = 0U; wi < workflow.size(); ++wi) {
527 auto& dp = workflow[wi];
528 if (dp.inputs.size() != 1) {
529 continue;
530 }
531 auto lifetime = dp.inputs[0].lifetime;
532 if (lifetime == Lifetime::Enumeration && (enumCandidate == -1 || workflow[enumCandidate].name > dp.name)) {
533 enumCandidate = wi;
534 }
535 if (lifetime == Lifetime::Timer && (timerCandidate == -1 || workflow[timerCandidate].name > dp.name)) {
536 timerCandidate = wi;
537 }
538 }
539
540 // * If there are AOD outputs we use TFNumber as the CCDB clock
541 // * If one device provides a DISTSTF we use that as the CCDB clock
542 // * If one of the devices provides a timer we use that as the CCDB clock
543 // * If none of the above apply, add to the first data processor
544 // which has no inputs apart from enumerations the responsibility
545 // to provide the DISTSUBTIMEFRAME.
546 if (ccdbBackend.outputs.empty() == false) {
547 if (aodReader.outputs.empty() == false) {
548 // fetcher clock follows AOD source (TFNumber)
549 ccdbBackend.inputs.push_back(InputSpec{"tfn", "TFN", "TFNumber"});
550 } else if (providesDISTSTF) {
551 // fetcher clock follows DSTF/ccdb source (DISTSUBTIMEFRAME)
552 ccdbBackend.inputs.push_back(InputSpec{"tfn", dstf, Lifetime::Timeframe});
553 } else {
554 if (enumCandidate != -1) {
555 // add DSTF/ccdb source to the enumeration-driven source explicitly
556 // fetcher clock is provided by enumeration-driven source (DISTSUBTIMEFRAME)
557 DataSpecUtils::updateOutputList(workflow[enumCandidate].outputs, OutputSpec{{"ccdb-diststf"}, dstf, Lifetime::Timeframe});
558 ccdbBackend.inputs.push_back(InputSpec{"tfn", dstf, Lifetime::Timeframe});
559 } else if (timerCandidate != -1) {
560 // fetcher clock is proived by timer source
561 auto timer_dstf = DataSpecUtils::asConcreteDataMatcher(workflow[timerCandidate].outputs[0]);
562 ccdbBackend.inputs.push_back(InputSpec{"tfn", timer_dstf, Lifetime::Timeframe});
563 }
564 }
565
566 ccdbBackend.outputs.push_back(OutputSpec{"CTP", "OrbitReset", 0});
567 // Load the CCDB backend from the plugin
568 ccdbBackend.algorithm = PluginManager::loadAlgorithmFromPlugin("O2FrameworkCCDBSupport", "CCDBFetcherPlugin", ctx);
569 extraSpecs.push_back(timePipeline(ccdbBackend, ctx.options().get<int64_t>("ccdb-fetchers")));
570 } else if (requiresDISTSUBTIMEFRAME && enumCandidate != -1) {
571 // add DSTF/ccdb source to the enumeration-driven source explicitly if it is required in the workflow
572 DataSpecUtils::updateOutputList(workflow[enumCandidate].outputs, OutputSpec{{"ccdb-diststf"}, dstf, Lifetime::Timeframe});
573 }
574
575 // add the timer
576 if (timer.outputs.empty() == false) {
577 extraSpecs.push_back(timer);
578 }
579
580 // This is to inject a file sink so that any dangling ATSK object is written
581 // to a ROOT file.
582 if (dec.providedOutputObjHist.empty() == false) {
584 extraSpecs.push_back(rootSink);
585 }
586
587 workflow.insert(workflow.end(), extraSpecs.begin(), extraSpecs.end());
588 extraSpecs.clear();
589
590 injectAODWriter(workflow, ctx);
591
592 // Select dangling outputs which are not of type AOD
593 std::vector<InputSpec> redirectedOutputsInputs;
594 for (auto ii = 0u; ii < dec.outputsInputs.size(); ii++) {
595 if (ctx.options().get<std::string>("forwarding-policy") == "none") {
596 continue;
597 }
598 // We forward to the output proxy all the inputs only if they are dangling
599 // or if the forwarding policy is "proxy".
600 if (!dec.isDangling[ii] && (ctx.options().get<std::string>("forwarding-policy") != "all")) {
601 continue;
602 }
603 // AODs are skipped in any case.
604 if (DataSpecUtils::partialMatch(dec.outputsInputs[ii], AODOrigins)) {
605 continue;
606 }
607 redirectedOutputsInputs.emplace_back(dec.outputsInputs[ii]);
608 }
609
610 std::vector<InputSpec> unmatched;
611 auto forwardingDestination = ctx.options().get<std::string>("forwarding-destination");
612 // update tfnsource iterator (could be aod-reader)
613 tfnsource = std::ranges::find_if(workflow, [](DataProcessorSpec const& spec) {
614 return std::ranges::any_of(spec.outputs, [](OutputSpec const& output) {
615 return DataSpecUtils::match(output, "TFN", "TFNumber", 0);
616 });
617 });
618 if (redirectedOutputsInputs.size() > 0 && forwardingDestination == "file") {
619 auto fileSink = CommonDataProcessors::getGlobalFileSink(redirectedOutputsInputs, unmatched);
620 if (unmatched.size() != redirectedOutputsInputs.size()) {
621 extraSpecs.push_back(fileSink);
622 }
623 } else if (redirectedOutputsInputs.size() > 0 && forwardingDestination == "fairmq") {
624 auto fairMQSink = CommonDataProcessors::getGlobalFairMQSink(redirectedOutputsInputs);
625 extraSpecs.push_back(fairMQSink);
626 } else if (forwardingDestination != "drop") {
627 throw runtime_error_f("Unknown forwarding destination %s", forwardingDestination.c_str());
628 }
629 if ((unmatched.size() > 0) || (redirectedOutputsInputs.size() > 0) || (tfnsource != workflow.end())) {
630 std::vector<InputSpec> ignored = unmatched;
631 ignored.insert(ignored.end(), redirectedOutputsInputs.begin(), redirectedOutputsInputs.end());
632 for (auto& ignoredInput : ignored) {
633 ignoredInput.lifetime = Lifetime::Sporadic;
634 }
635
636 // Use the new dummy sink when the AOD reader is there
637 O2_SIGNPOST_ID_GENERATE(sid, workflow_helpers);
638 if (tfnsource != workflow.end()) {
639 DataSpecUtils::updateInputList(ignored, InputSpec{"tfn", "TFN", "TFNumber", 0, Lifetime::Sporadic});
640 DataSpecUtils::updateInputList(ignored, InputSpec{"tff", "TFF", "TFFilename", 0, Lifetime::Sporadic});
641 }
642
643 if (tfnsource != workflow.end() && !tfnsource->name.starts_with("aod-producer-workflow")) { // any tfnsource except the aod-producer should use scheduled sink
644 O2_SIGNPOST_EVENT_EMIT(workflow_helpers, sid, "injectServiceDevices", "Injecting scheduled dummy sink");
645 // if there is a tfnsource, make sure the sink gets TFN/TFF
646 extraSpecs.push_back(CommonDataProcessors::getScheduledDummySink(ignored));
647 } else { // if there is no tfn source or if that source is aod-producer-workflow, out-of-band channel is used to propagate the number of consumed timeframes
648 O2_SIGNPOST_EVENT_EMIT(workflow_helpers, sid, "injectServiceDevices", "Injecting rate limited dummy sink");
649 std::string rateLimitingChannelConfigOutput;
650 if (rateLimitingIPCID != -1) {
651 rateLimitingChannelConfigOutput = fmt::format("name=metric-feedback,type=push,method=bind,address=ipc://{}metric-feedback-{},transport=shmem,rateLogging=0", ChannelSpecHelpers::defaultIPCFolder(), rateLimitingIPCID);
652 }
653 extraSpecs.push_back(CommonDataProcessors::getDummySink(ignored, rateLimitingChannelConfigOutput));
654 }
655 }
656
657 workflow.insert(workflow.end(), extraSpecs.begin(), extraSpecs.end());
658 extraSpecs.clear();
659}
660
662{
663 unsigned int distSTFCount = 0;
664 for (auto& spec : workflow) {
665 auto& inputs = spec.inputs;
666 bool allSporadic = true;
667 bool hasTimer = false;
668 bool hasSporadic = false;
669 bool hasOptionals = false;
670 for (auto& input : inputs) {
671 if (input.lifetime == Lifetime::Optional) {
672 hasOptionals = true;
673 }
674 }
675 for (auto& input : inputs) {
676 // Any InputSpec that is DPL/DISTSUBTIMEFRAME/0 will actually be replaced by one
677 // which looks like DPL/DISTSUBTIMEFRAME/<incremental number> for devices that
678 // have Optional inputs as well.
679 // This is done to avoid the race condition where the DISTSUBTIMEFRAME/0 gets
680 // forwarded before actual RAWDATA arrives.
681 if (DataSpecUtils::match(input, ConcreteDataTypeMatcher{"FLP", "DISTSUBTIMEFRAME"}) &&
682 !DataSpecUtils::match(input, ConcreteDataMatcher{"FLP", "DISTSUBTIMEFRAME", 0})) {
683 LOGP(error,
684 "Only FLP/DISTSUBTIMEFRAME/0 is supported as input "
685 "provided by the user. Please replace {} with FLP/DISTSUBTIMEFRAME/0 in {}.",
686 DataSpecUtils::describe(input), input.binding);
687 }
688 if (hasOptionals && DataSpecUtils::match(input, ConcreteDataMatcher{"FLP", "DISTSUBTIMEFRAME", 0})) {
689 // The first one remains unchanged, therefore we use the postincrement
690 DataSpecUtils::updateMatchingSubspec(input, distSTFCount++);
691 continue;
692 }
693 // Timers are sporadic only when they are not
694 // alone.
695 if (input.lifetime == Lifetime::Timer) {
696 hasTimer = true;
697 continue;
698 }
699 if (input.lifetime == Lifetime::Sporadic) {
700 hasSporadic = true;
701 } else {
702 allSporadic = false;
703 }
704 }
705
706 LOGP(debug, "WorkflowHelpers::adjustTopology: spec {} hasTimer {} hasSporadic {} allSporadic {}", spec.name, hasTimer, hasSporadic, allSporadic);
707
708 // If they are not all sporadic (excluding timers)
709 // we leave things as they are.
710 if (allSporadic == false) {
711 continue;
712 }
713 // A timer alone is not sporadic.
714 if (hasSporadic == false) {
715 continue;
716 }
720 for (auto& output : spec.outputs) {
721 if (output.lifetime == Lifetime::Timeframe) {
722 output.lifetime = Lifetime::Sporadic;
723 }
724 }
725 }
726
727 if (distSTFCount > 0) {
728 for (auto& spec : workflow) {
729 if (std::ranges::any_of(spec.outputs, [](auto const& output) { return DataSpecUtils::match(output, ConcreteDataMatcher{"FLP", "DISTSUBTIMEFRAME", 0}); })) {
730 for (unsigned int i = 1; i < distSTFCount; ++i) {
731 spec.outputs.emplace_back(OutputSpec{ConcreteDataMatcher{"FLP", "DISTSUBTIMEFRAME", i}, Lifetime::Timeframe});
732 }
733 break;
734 }
735 }
736 }
737}
738
740{
741 auto& dec = ctx.services().get<DanglingEdgesContext>();
743 std::tie(dec.outputsInputs, dec.isDangling) = analyzeOutputs(workflow);
744
745 // create DataOutputDescriptor
746 std::shared_ptr<DataOutputDirector> dod = AnalysisSupportHelpers::getDataOutputDirector(ctx);
747
748 // select outputs of type AOD which need to be saved
749 dec.outputsInputsAOD.clear();
750 for (auto ii = 0u; ii < dec.outputsInputs.size(); ii++) {
751 if (DataSpecUtils::partialMatch(dec.outputsInputs[ii], AODOrigins)) {
752 auto ds = dod->getDataOutputDescriptors(dec.outputsInputs[ii]);
753 if (ds.size() > 0 || dec.isDangling[ii]) {
754 dec.outputsInputsAOD.emplace_back(dec.outputsInputs[ii]);
755 }
756 }
757 }
758
759 // file sink for any AOD output
760 if (dec.outputsInputsAOD.size() > 0) {
761 // add TFNumber and TFFilename as input to the writer
762 DataSpecUtils::updateInputList(dec.outputsInputsAOD, InputSpec{"tfn", "TFN", "TFNumber"});
763 DataSpecUtils::updateInputList(dec.outputsInputsAOD, InputSpec{"tff", "TFF", "TFFilename"});
764 auto fileSink = AnalysisSupportHelpers::getGlobalAODSink(ctx);
765 workflow.push_back(fileSink);
766
767 auto it = std::find_if(dec.outputsInputs.begin(), dec.outputsInputs.end(), [](InputSpec const& spec) -> bool {
768 return DataSpecUtils::partialMatch(spec, o2::header::DataOrigin("TFN"));
769 });
770 dec.isDangling[std::distance(dec.outputsInputs.begin(), it)] = false;
771
772 it = std::find_if(dec.outputsInputs.begin(), dec.outputsInputs.end(), [](InputSpec const& spec) -> bool {
773 return DataSpecUtils::partialMatch(spec, o2::header::DataOrigin("TFF"));
774 });
775 dec.isDangling[std::distance(dec.outputsInputs.begin(), it)] = false;
776 }
777}
778
780 std::vector<DeviceConnectionEdge>& logicalEdges,
781 std::vector<OutputSpec>& outputs,
782 std::vector<LogicalForwardInfo>& forwardedInputsInfo)
783{
784 // In case the workflow is empty, we do not have anything to do.
785 if (workflow.empty()) {
786 return;
787 }
788
789 // This is the state. Oif is the iterator I use for the searches.
790 std::vector<LogicalOutputInfo> availableOutputsInfo;
791 auto const& constOutputs = outputs; // const version of the outputs
792 // Forwards is a local cache to avoid adding forwards before time.
793 std::vector<LogicalOutputInfo> forwards;
794
795 // Notice that availableOutputsInfo MUST be updated first, since it relies on
796 // the size of outputs to be the one before the update.
797 auto enumerateAvailableOutputs = [&workflow, &outputs, &availableOutputsInfo]() {
798 O2_SIGNPOST_ID_GENERATE(sid, workflow_helpers);
799 for (size_t wi = 0; wi < workflow.size(); ++wi) {
800 auto& producer = workflow[wi];
801 if (producer.outputs.empty()) {
802 O2_SIGNPOST_EVENT_EMIT(workflow_helpers, sid, "output enumeration", "No outputs for [%zu] %{public}s", wi, producer.name.c_str());
803 }
804 O2_SIGNPOST_START(workflow_helpers, sid, "output enumeration", "Enumerating outputs for producer [%zu] %{}s public", wi, producer.name.c_str());
805
806 for (size_t oi = 0; oi < producer.outputs.size(); ++oi) {
807 auto& out = producer.outputs[oi];
808 auto uniqueOutputId = outputs.size();
809 availableOutputsInfo.emplace_back(LogicalOutputInfo{wi, uniqueOutputId, false});
810 O2_SIGNPOST_EVENT_EMIT(workflow_helpers, sid, "output enumeration", "- [%zu, %zu] %{public}s",
811 oi, uniqueOutputId, DataSpecUtils::describe(out).c_str());
812 outputs.push_back(out);
813 }
814 O2_SIGNPOST_END(workflow_helpers, sid, "output enumeration", "");
815 }
816 };
817
818 auto errorDueToMissingOutputFor = [&workflow, &constOutputs](size_t ci, size_t ii) {
819 auto input = workflow[ci].inputs[ii];
820 std::ostringstream str;
821 str << "No matching output found for "
822 << DataSpecUtils::describe(input) << " as requested by data processor \"" << workflow[ci].name << "\". Candidates:\n";
823
824 for (auto& output : constOutputs) {
825 str << "-" << DataSpecUtils::describe(output) << "\n";
826 }
827
828 throw std::runtime_error(str.str());
829 };
830
831 // This is the outer loop
832 //
833 // Here we iterate over dataprocessor items in workflow and we consider them
834 // as consumer, since we are interested in their inputs.
835 // Notice also we need to search for all the matching inputs, since
836 // we could have more than one source that matches (e.g. in the
837 // case of a time merger).
838 // Once consumed, an output is not actually used anymore, however
839 // we append it as a forward.
840 // Finally, If a device has n-way pipelining, we need to create one node per
841 // parallel pipeline and add an edge for each.
842 enumerateAvailableOutputs();
843
844 std::vector<bool> matches(constOutputs.size());
845 for (size_t consumer = 0; consumer < workflow.size(); ++consumer) {
846 O2_SIGNPOST_ID_GENERATE(sid, workflow_helpers);
847 O2_SIGNPOST_START(workflow_helpers, sid, "input matching", "Matching inputs of consumer [%zu] %{}s public", consumer, workflow[consumer].name.c_str());
848 for (size_t input = 0; input < workflow[consumer].inputs.size(); ++input) {
849 forwards.clear();
850 for (size_t i = 0; i < constOutputs.size(); i++) {
851 matches[i] = DataSpecUtils::match(workflow[consumer].inputs[input], constOutputs[i]);
852 if (matches[i]) {
853 O2_SIGNPOST_EVENT_EMIT(workflow_helpers, sid, "output", "Input %{public}s matches %{public}s",
854 DataSpecUtils::describe(workflow[consumer].inputs[input]).c_str(),
855 DataSpecUtils::describe(constOutputs[i]).c_str());
856 }
857 }
858
859 for (size_t i = 0; i < availableOutputsInfo.size(); i++) {
860 // Notice that if the output is actually a forward, we need to store that information so that when we add it at device level we know which output channel we need to connect it too.
861 if (!matches[availableOutputsInfo[i].outputGlobalIndex]) {
862 continue;
863 }
864 auto* oif = &availableOutputsInfo[i];
865 if (oif->forward) {
866 forwardedInputsInfo.emplace_back(LogicalForwardInfo{consumer, input, oif->outputGlobalIndex});
867 }
868 auto producer = oif->specIndex;
869 auto uniqueOutputId = oif->outputGlobalIndex;
870 for (size_t tpi = 0; tpi < workflow[consumer].maxInputTimeslices; ++tpi) {
871 for (size_t ptpi = 0; ptpi < workflow[producer].maxInputTimeslices; ++ptpi) {
872 O2_SIGNPOST_EVENT_EMIT(workflow_helpers, sid, "output", "Adding edge between %{public}s and %{public}s", workflow[consumer].name.c_str(),
873 workflow[producer].name.c_str());
874 logicalEdges.emplace_back(DeviceConnectionEdge{producer, consumer, tpi, ptpi, uniqueOutputId, input, oif->forward});
875 }
876 }
877 forwards.push_back(LogicalOutputInfo{consumer, uniqueOutputId, true});
878 // We have consumed the input, therefore we remove it from the list. We will insert the forwarded inputs only at the end of the iteration.
879 oif->enabled = false;
880 }
881 if (forwards.empty()) {
882 errorDueToMissingOutputFor(consumer, input);
883 }
884 availableOutputsInfo.erase(std::remove_if(availableOutputsInfo.begin(), availableOutputsInfo.end(), [](auto const& info) { return info.enabled == false; }), availableOutputsInfo.end());
885 std::ranges::copy(forwards, std::back_inserter(availableOutputsInfo));
886 }
887 O2_SIGNPOST_END(workflow_helpers, sid, "input matching", "");
888 }
889}
890
891std::vector<EdgeAction>
893 const std::vector<DeviceConnectionEdge>& edges,
894 const std::vector<size_t>& index)
895{
896 DeviceConnectionEdge last{ULONG_MAX, ULONG_MAX, ULONG_MAX, ULONG_MAX, ULONG_MAX, ULONG_MAX};
897
898 assert(edges.size() == index.size());
899 std::vector<EdgeAction> actions(edges.size(), EdgeAction{false, false});
900 for (size_t i : index) {
901 auto& edge = edges[i];
902 auto& action = actions[i];
903 action.requiresNewDevice = last.producer != edge.producer || last.producerTimeIndex != edge.producerTimeIndex;
904 action.requiresNewChannel = last.consumer != edge.consumer || last.producer != edge.producer || last.timeIndex != edge.timeIndex || last.producerTimeIndex != edge.producerTimeIndex;
905 last = edge;
906 }
907 return actions;
908}
909
910std::vector<EdgeAction>
912 const std::vector<DeviceConnectionEdge>& edges,
913 const std::vector<size_t>& index)
914{
915 DeviceConnectionEdge last{ULONG_MAX, ULONG_MAX, ULONG_MAX, ULONG_MAX, ULONG_MAX, ULONG_MAX};
916
917 assert(edges.size() == index.size());
918 std::vector<EdgeAction> actions(edges.size(), EdgeAction{false, false});
919 for (size_t i : index) {
920 auto& edge = edges[i];
921 auto& action = actions[i];
922 // Calculate which actions need to be taken for this edge.
923 action.requiresNewDevice = last.consumer != edge.consumer || last.timeIndex != edge.timeIndex;
924 action.requiresNewChannel =
925 last.consumer != edge.consumer || last.timeIndex != edge.timeIndex || last.producer != edge.producer || last.producerTimeIndex != edge.producerTimeIndex;
926
927 last = edge;
928 }
929 return actions;
930}
931
932void WorkflowHelpers::sortEdges(std::vector<size_t>& inEdgeIndex,
933 std::vector<size_t>& outEdgeIndex,
934 const std::vector<DeviceConnectionEdge>& edges)
935{
936 inEdgeIndex.resize(edges.size());
937 outEdgeIndex.resize(edges.size());
938 std::iota(inEdgeIndex.begin(), inEdgeIndex.end(), 0);
939 std::iota(outEdgeIndex.begin(), outEdgeIndex.end(), 0);
940
941 // Two indexes, one to bind the outputs, the other
942 // one to connect the inputs. The
943 auto outSorter = [&edges](size_t i, size_t j) {
944 auto& a = edges[i];
945 auto& b = edges[j];
946 return std::tie(a.producer, a.producerTimeIndex, a.timeIndex, a.consumer) < std::tie(b.producer, b.producerTimeIndex, b.timeIndex, b.consumer);
947 };
948 auto inSorter = [&edges](size_t i, size_t j) {
949 auto& a = edges[i];
950 auto& b = edges[j];
951 return std::tie(a.consumer, a.timeIndex, a.producer, a.producerTimeIndex) < std::tie(b.consumer, b.timeIndex, b.producer, b.producerTimeIndex);
952 };
953
954 std::sort(inEdgeIndex.begin(), inEdgeIndex.end(), inSorter);
955 std::sort(outEdgeIndex.begin(), outEdgeIndex.end(), outSorter);
956}
957
959{
960 if (workflow.empty()) {
962 }
963 std::set<std::string> validNames;
964 // std::vector<OutputSpec> availableOutputs;
965 // std::vector<InputSpec> requiredInputs;
966
967 // An index many to one index to go from a given input to the
968 // associated spec
969 // std::map<size_t, size_t> inputToSpec;
970 // A one to one index to go from a given output to the Spec emitting it
971 // std::map<size_t, size_t> outputToSpec;
972
973 std::ostringstream ss;
974
975 for (auto& spec : workflow) {
976 if (spec.name.empty()) {
977 throw std::runtime_error("Invalid DataProcessorSpec name");
978 }
979 if (strpbrk(spec.name.data(), ",;:\"'$") != nullptr) {
980 throw std::runtime_error("Cannot use any of ,;:\"'$ as DataProcessor name");
981 }
982 if (validNames.find(spec.name) != validNames.end()) {
983 throw std::runtime_error("Name " + spec.name + " is used twice.");
984 }
985 validNames.insert(spec.name);
986 for (auto& option : spec.options) {
987 if (option.defaultValue.type() != VariantType::Empty &&
988 option.type != option.defaultValue.type()) {
989 ss << "Mismatch between declared option type (" << (int)option.type << ") and default value type (" << (int)option.defaultValue.type()
990 << ") for " << option.name << " in DataProcessorSpec of "
991 << spec.name;
992 throw std::runtime_error(ss.str());
993 }
994 }
995 for (size_t ii = 0; ii < spec.inputs.size(); ++ii) {
996 InputSpec const& input = spec.inputs[ii];
997 if (DataSpecUtils::validate(input) == false) {
998 ss << "In spec " << spec.name << " input specification "
999 << ii << " requires binding, description and origin"
1000 " to be fully specified (found "
1001 << input.binding << ":" << DataSpecUtils::describe(input) << ")";
1002 throw std::runtime_error(ss.str());
1003 }
1004 }
1005 }
1007}
1008
1009using UnifiedDataSpecType = std::variant<InputSpec, OutputSpec>;
1012 size_t id;
1013};
1014
1015std::tuple<std::vector<InputSpec>, std::vector<bool>> WorkflowHelpers::analyzeOutputs(WorkflowSpec const& workflow)
1016{
1017 // compute total number of input/output
1018 size_t totalInputs = 0;
1019 size_t totalOutputs = 0;
1020 for (auto& spec : workflow) {
1021 totalInputs += spec.inputs.size();
1022 totalOutputs += spec.outputs.size();
1023 }
1024
1025 std::vector<DataMatcherId> inputs;
1026 std::vector<DataMatcherId> outputs;
1027 inputs.reserve(totalInputs);
1028 outputs.reserve(totalOutputs);
1029
1030 std::vector<InputSpec> results;
1031 std::vector<bool> isDangling;
1032 results.reserve(totalOutputs);
1033 isDangling.reserve(totalOutputs);
1034
1036 for (size_t wi = 0, we = workflow.size(); wi != we; ++wi) {
1037 auto& spec = workflow[wi];
1038 for (size_t ii = 0, ie = spec.inputs.size(); ii != ie; ++ii) {
1039 inputs.emplace_back(DataMatcherId{wi, ii});
1040 }
1041 for (size_t oi = 0, oe = spec.outputs.size(); oi != oe; ++oi) {
1042 outputs.emplace_back(DataMatcherId{wi, oi});
1043 }
1044 }
1045
1046 for (size_t oi = 0, oe = outputs.size(); oi != oe; ++oi) {
1047 auto& output = outputs[oi];
1048 auto& outputSpec = workflow[output.workflowId].outputs[output.id];
1049
1050 // is dangling output?
1051 bool matched = false;
1052 for (size_t ii = 0, ie = inputs.size(); ii != ie; ++ii) {
1053 auto& input = inputs[ii];
1054 // Inputs of the same workflow cannot match outputs
1055 if (output.workflowId == input.workflowId) {
1056 continue;
1057 }
1058 auto& inputSpec = workflow[input.workflowId].inputs[input.id];
1059 if (DataSpecUtils::match(inputSpec, outputSpec)) {
1060 matched = true;
1061 break;
1062 }
1063 }
1064
1065 auto input = DataSpecUtils::matchingInput(outputSpec);
1066 char buf[64];
1067 input.binding = (snprintf(buf, 63, "output_%zu_%zu", output.workflowId, output.id), buf);
1068
1069 // make sure that entries are unique
1070 if (std::ranges::find(results, input) == results.end()) {
1071 results.emplace_back(input);
1072 isDangling.emplace_back(matched == false);
1073 }
1074 }
1075
1076 // make sure that results is unique
1077 return std::make_tuple(results, isDangling);
1078}
1079
1080std::vector<InputSpec> WorkflowHelpers::computeDanglingOutputs(WorkflowSpec const& workflow)
1081{
1082
1083 auto [outputsInputs, isDangling] = analyzeOutputs(workflow);
1084
1085 std::vector<InputSpec> results;
1086 for (auto ii = 0u; ii < outputsInputs.size(); ii++) {
1087 if (isDangling[ii]) {
1088 results.emplace_back(outputsInputs[ii]);
1089 }
1090 }
1091
1092 return results;
1093}
1094
1095bool validateLifetime(std::ostream& errors,
1096 DataProcessorSpec const& producer, OutputSpec const& output, DataProcessorPoliciesInfo const& producerPolicies,
1097 DataProcessorSpec const& consumer, InputSpec const& input, DataProcessorPoliciesInfo const& consumerPolicies)
1098{
1099 // In case the completion policy is consume-any, we do not need to check anything.
1100 if (consumerPolicies.completionPolicyName == "consume-any") {
1101 return true;
1102 }
1103 if (input.lifetime == Lifetime::Timeframe && output.lifetime == Lifetime::Sporadic) {
1104 errors << fmt::format("Input {} of {} has lifetime Timeframe, but output {} of {} has lifetime Sporadic\n",
1105 DataSpecUtils::describe(input).c_str(), consumer.name,
1106 DataSpecUtils::describe(output).c_str(), producer.name);
1107 return false;
1108 }
1109 return true;
1110}
1111
1112bool validateExpendable(std::ostream& errors,
1113 DataProcessorSpec const& producer, OutputSpec const& output, DataProcessorPoliciesInfo const& producerPolicies,
1114 DataProcessorSpec const& consumer, InputSpec const& input, DataProcessorPoliciesInfo const& consumerPolicies)
1115{
1116 auto isExpendable = [](DataProcessorLabel const& label) {
1117 return label.value == "expendable";
1118 };
1119 auto isResilient = [](DataProcessorLabel const& label) {
1120 return label.value == "expendable" || label.value == "resilient";
1121 };
1122 bool producerExpendable = std::find_if(producer.labels.begin(), producer.labels.end(), isExpendable) != producer.labels.end();
1123 bool consumerCritical = std::find_if(consumer.labels.begin(), consumer.labels.end(), isResilient) == consumer.labels.end();
1124 if (producerExpendable && consumerCritical) {
1125 errors << fmt::format("Critical consumer {} depends on expendable producer {}\n",
1126 consumer.name,
1127 producer.name);
1128 return false;
1129 }
1130 return true;
1131}
1132
1133using Validator = std::function<bool(std::ostream& errors,
1134 DataProcessorSpec const& producer, OutputSpec const& output, DataProcessorPoliciesInfo const& producerPolicies,
1135 DataProcessorSpec const& consumer, InputSpec const& input, DataProcessorPoliciesInfo const& consumerPolicies)>;
1136
1138 std::vector<DataProcessorPoliciesInfo> const& policies,
1139 std::vector<DeviceConnectionEdge> const& edges,
1140 std::vector<OutputSpec> const& outputs)
1141{
1142 static bool disableLifetimeCheck = getenv("DPL_WORKAROUND_DO_NOT_CHECK_FOR_CORRECT_WORKFLOW_LIFETIMES") && atoi(getenv("DPL_WORKAROUND_DO_NOT_CHECK_FOR_CORRECT_WORKFLOW_LIFETIMES"));
1143 std::vector<Validator> defaultValidators = {validateExpendable};
1144 if (!disableLifetimeCheck) {
1145 defaultValidators.emplace_back(validateLifetime);
1146 }
1147 std::stringstream errors;
1148 // Iterate over all the edges.
1149 // Get the input lifetime and the output lifetime.
1150 // Output lifetime must be Timeframe if the input lifetime is Timeframe.
1151 bool hasErrors = false;
1152 for (auto const& edge : edges) {
1153 DataProcessorSpec const& producer = workflow[edge.producer];
1154 DataProcessorSpec const& consumer = workflow[edge.consumer];
1155 DataProcessorPoliciesInfo const& producerPolicies = policies[edge.producer];
1156 DataProcessorPoliciesInfo const& consumerPolicies = policies[edge.consumer];
1157 OutputSpec const& output = outputs[edge.outputGlobalIndex];
1158 InputSpec const& input = consumer.inputs[edge.consumerInputIndex];
1159 for (auto const& validator : defaultValidators) {
1160 hasErrors |= !validator(errors, producer, output, producerPolicies, consumer, input, consumerPolicies);
1161 }
1162 }
1163 if (hasErrors) {
1164 throw std::runtime_error(errors.str());
1165 }
1166}
1167
1168} // namespace o2::framework
uint32_t hash
std::ostringstream debug
std::unique_ptr< expressions::Node > node
int32_t i
void output(const std::map< std::string, ChannelStat > &channels)
Definition rawdump.cxx:197
std::vector< SidecarEdge > edges
uint32_t j
Definition RawData.h:0
#define O2_DECLARE_DYNAMIC_LOG(name)
Definition Signpost.h:490
#define O2_SIGNPOST_END(log, id, name, format,...)
Definition Signpost.h:609
#define O2_SIGNPOST_ID_GENERATE(name, log)
Definition Signpost.h:507
#define O2_SIGNPOST_EVENT_EMIT(log, id, name, format,...)
Definition Signpost.h:523
#define O2_SIGNPOST_START(log, id, name, format,...)
Definition Signpost.h:603
constexpr uint32_t runtime_hash(char const *str)
consteval uint32_t compile_time_hash(Ts... Vs)
o2::header::DataHeader::SubSpecificationType SubSpecificationType
GLint GLenum GLint x
Definition glcorearb.h:403
const GLfloat * m
Definition glcorearb.h:4066
GLuint index
Definition glcorearb.h:781
GLuint const GLchar * name
Definition glcorearb.h:781
GLboolean GLboolean GLboolean b
Definition glcorearb.h:1233
GLuint GLsizei const GLchar * label
Definition glcorearb.h:2519
GLint GLenum GLboolean GLsizei stride
Definition glcorearb.h:867
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
GLenum GLuint GLenum GLsizei const GLchar * buf
Definition glcorearb.h:2514
constexpr framework::ConcreteDataMatcher matcher()
Definition ASoA.h:388
Defining ITS Vertex explicitly as messageable.
Definition Cartesian.h:288
int defaultConditionQueryRate()
bool validateLifetime(std::ostream &errors, DataProcessorSpec const &producer, OutputSpec const &output, DataProcessorPoliciesInfo const &producerPolicies, DataProcessorSpec const &consumer, InputSpec const &input, DataProcessorPoliciesInfo const &consumerPolicies)
std::ostream & operator<<(std::ostream &s, ChannelType const &type)
Stream operators so that we can use ChannelType with Boost.Test.
std::string defaultConditionBackend()
int defaultConditionQueryRateMultiplier()
bool validateExpendable(std::ostream &errors, DataProcessorSpec const &producer, OutputSpec const &output, DataProcessorPoliciesInfo const &producerPolicies, DataProcessorSpec const &consumer, InputSpec const &input, DataProcessorPoliciesInfo const &consumerPolicies)
std::vector< DataProcessorSpec > WorkflowSpec
DataProcessorSpec timePipeline(DataProcessorSpec original, size_t count)
AlgorithmSpec::ProcessCallback adaptStateless(LAMBDA l)
std::function< bool(std::ostream &errors, DataProcessorSpec const &producer, OutputSpec const &output, DataProcessorPoliciesInfo const &producerPolicies, DataProcessorSpec const &consumer, InputSpec const &input, DataProcessorPoliciesInfo const &consumerPolicies)> Validator
RuntimeErrorRef runtime_error_f(const char *,...)
std::variant< InputSpec, OutputSpec > UnifiedDataSpecType
AlgorithmSpec::InitCallback adaptStateful(LAMBDA l)
Descriptor< gSizeDataOriginString > DataOrigin
Definition DataHeader.h:553
const track::TrackFitContext< NLayers > const TrackFollowContext< NLayers > & ctx
D const SVectorGPU< T, D > & rhs
Definition SMatrixGPU.h:193
static AlgorithmSpec dummyAlgorithm()
static void addMissingOutputsToBuilder(std::vector< InputSpec > const &requestedSpecials, std::vector< InputSpec > &requestedAODs, std::vector< InputSpec > &requestedDYNs, DataProcessorSpec &publisher)
static std::shared_ptr< DataOutputDirector > getDataOutputDirector(ConfigContext const &ctx)
Get the data director.
static void addMissingOutputsToReader(std::vector< OutputSpec > const &providedOutputs, std::vector< InputSpec > const &requestedInputs, DataProcessorSpec &publisher)
static void addMissingOutputsToSpawner(std::vector< OutputSpec > const &providedSpecials, std::vector< InputSpec > const &requestedSpecials, std::vector< InputSpec > &requestedAODs, DataProcessorSpec &publisher)
static DataProcessorSpec getGlobalAODSink(ConfigContext const &)
writes inputs of kind AOD to file
static DataProcessorSpec getOutputObjHistSink(ConfigContext const &)
static DataProcessorSpec getScheduledDummySink(std::vector< InputSpec > const &danglingInputs)
static DataProcessorSpec getGlobalFileSink(std::vector< InputSpec > const &danglingInputs, std::vector< InputSpec > &unmatched)
static DataProcessorSpec getGlobalFairMQSink(std::vector< InputSpec > const &danglingInputs)
static DataProcessorSpec getDummySink(std::vector< InputSpec > const &danglingInputs, std::string rateLimitingChannelConfig)
static std::vector< ServiceSpec > defaultServices(std::string extraPlugins="", int numWorkers=0)
Split a string into a vector of strings using : as a separator.
static void addOptionIfMissing(std::vector< ConfigParamSpec > &specs, const ConfigParamSpec &spec)
A label that can be associated to a DataProcessorSpec.
std::vector< DataProcessorLabel > labels
static bool partialMatch(InputSpec const &spec, o2::header::DataOrigin const &origin)
static std::string describe(InputSpec const &spec)
static void updateOutputList(std::vector< OutputSpec > &list, OutputSpec &&input)
Updates list of OutputSpecs by merging metadata (or adding output).
static bool validate(InputSpec const &input)
static ConcreteDataMatcher asConcreteDataMatcher(InputSpec const &input)
static InputSpec matchingInput(OutputSpec const &spec)
static void updateMatchingSubspec(InputSpec &in, header::DataHeader::SubSpecificationType subSpec)
static void updateInputList(std::vector< InputSpec > &list, InputSpec &&input)
Updates list of InputSpecs by merging metadata.
static bool match(InputSpec const &spec, ConcreteDataMatcher const &target)
static DeploymentMode deploymentMode()
enum Lifetime lifetime
Definition InputSpec.h:73
std::string binding
A mnemonic name for the input spec.
Definition InputSpec.h:66
static auto loadAlgorithmFromPlugin(std::string library, std::string plugin, ConfigContext const &context) -> AlgorithmSpec
Helper struct to keep track of the results of the topological sort.
static void validateEdges(WorkflowSpec const &workflow, std::vector< DataProcessorPoliciesInfo > const &policiesInfos, std::vector< DeviceConnectionEdge > const &edges, std::vector< OutputSpec > const &outputs)
static void adjustTopology(WorkflowSpec &workflow, ConfigContext const &ctx)
static void constructGraph(const WorkflowSpec &workflow, std::vector< DeviceConnectionEdge > &logicalEdges, std::vector< OutputSpec > &outputs, std::vector< LogicalForwardInfo > &availableForwardsInfo)
static void injectServiceDevices(WorkflowSpec &workflow, ConfigContext &ctx)
static WorkflowParsingState verifyWorkflow(const WorkflowSpec &workflow)
static std::vector< InputSpec > computeDanglingOutputs(WorkflowSpec const &workflow)
returns only dangling outputs
static std::vector< EdgeAction > computeOutEdgeActions(const std::vector< DeviceConnectionEdge > &edges, const std::vector< size_t > &index)
static std::tuple< std::vector< InputSpec >, std::vector< bool > > analyzeOutputs(WorkflowSpec const &workflow)
static void sortEdges(std::vector< size_t > &inEdgeIndex, std::vector< size_t > &outEdgeIndex, const std::vector< DeviceConnectionEdge > &edges)
static std::vector< EdgeAction > computeInEdgeActions(const std::vector< DeviceConnectionEdge > &edges, const std::vector< size_t > &index)
static std::vector< TopoIndexInfo > topologicalSort(size_t nodeCount, int const *edgeIn, int const *edgeOut, size_t byteStride, size_t edgesCount)
static void injectAODWriter(WorkflowSpec &workflow, ConfigContext const &ctx)
o2::mch::DsIndex ds
const std::string str