Project
Loading...
Searching...
No Matches
AnalysisCCDBHelpers.cxx
Go to the documentation of this file.
1// Copyright 2019-2025 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
12#include "AnalysisCCDBHelpers.h"
13#include "CCDBFetcherHelper.h"
21#include "Framework/Output.h"
22#include "Framework/Signpost.h"
26#include <fairmq/Version.h>
27#include <arrow/array/builder_binary.h>
28#include <arrow/type.h>
29#include <arrow/type_fwd.h>
30#include <arrow/util/key_value_metadata.h>
31#include <arrow/table.h>
32#include <arrow/array.h>
33#include <arrow/builder.h>
34#include <fmt/base.h>
35#include <ctime>
36#include <memory>
37#include "CCDBPathTable.h"
38
39#include <string>
40#include <unordered_map>
41#include <vector>
42
44
45namespace o2::framework
46{
47// Fill valid routes. Notice that for analysis the timestamps are associated to
48// a ATIM table and there might be multiple CCDB objects of the same kind for
49// dataframe.
50// For this reason rather than matching the Lifetime::Condition, we match the
51// origin.
52namespace
53{
54void fillValidRoutes(CCDBFetcherHelper& helper, std::vector<o2::framework::OutputRoute> const& outputRoutes, std::unordered_map<std::string, int>& bindings)
55{
56 for (auto& route : outputRoutes) {
57 if (std::ranges::none_of(route.matcher.metadata, [](auto const& m) { return m.name.starts_with("ccdb:"); })) {
58 continue;
59 }
60 auto specStr = DataSpecUtils::describe(route.matcher);
61 if (bindings.find(specStr) != bindings.end()) {
62 continue;
63 }
64 bindings[specStr] = helper.routes.size();
65 helper.routes.push_back(route);
66 LOGP(info, "The following route needs condition objects {} ", DataSpecUtils::describe(route.matcher));
67 for (auto& metadata : route.matcher.metadata) {
68 if (metadata.type == VariantType::String) {
69 LOGP(info, "- {}: {}", metadata.name, metadata.defaultValue.asString());
70 }
71 }
72 }
73}
74} // namespace
75
77{
78 return adaptStateful([](ConfigParamRegistry const& options, DeviceSpec const& spec, InitContext& ic) {
79 auto& dec = ic.services().get<DanglingEdgesContext>();
80 // The effective default for each ccdb: option was already resolved at topology
81 // time by ArrowSupport (consulting task Configurables) and registered on this
82 // device's options. Here we just read the final value — honouring any further
83 // runtime override supplied via CLI or JSON config.
84 std::unordered_map<std::string, std::string> ccdbUrls;
85 std::unordered_map<std::string, std::string> runDependent;
86 for (auto& input : dec.analysisCCDBInputs) {
87 for (auto& m : input.metadata) {
88 if (m.name.starts_with("ccdb-run-dependent:")) {
89 runDependent.emplace(m.name, m.defaultValue.asString());
90 continue;
91 }
92 if (!m.name.starts_with("ccdb:") || ccdbUrls.count(m.name)) {
93 continue;
94 }
95 std::string url = m.defaultValue.asString();
96 if (ConfigParamsHelper::hasOption(spec.options, m.name)) {
97 url = options.get<std::string>(m.name.c_str());
98 }
99 LOGP(info, "CCDB path resolved for {}: {}", m.name, url);
100 ccdbUrls.emplace(m.name, std::move(url));
101 }
102 }
103 std::vector<std::shared_ptr<arrow::Schema>> schemas;
104 for (auto& input : dec.analysisCCDBInputs) {
105 auto schemaMetadata = std::make_shared<arrow::KeyValueMetadata>();
106 std::vector<std::shared_ptr<arrow::Field>> fields;
107 schemaMetadata->Append("outputRoute", DataSpecUtils::describe(input));
108 schemaMetadata->Append("outputBinding", input.binding);
109 for (auto& m : input.metadata) {
110 if (m.name.starts_with("input:")) {
111 auto name = m.name.substr(6);
112 schemaMetadata->Append("sourceTable", name);
113 schemaMetadata->Append("sourceMatcher", DataSpecUtils::describe(std::get<ConcreteDataMatcher>(DataSpecUtils::fromMetadataString(m.defaultValue.get<std::string>()).matcher)));
114 continue;
115 }
116 if (m.name == "timestamp-column" || m.name == "uniformity-column") {
117 schemaMetadata->Append(m.name, m.defaultValue.asString());
118 continue;
119 }
120 if (!m.name.starts_with("ccdb:")) {
121 continue;
122 }
123 auto fieldMetadata = std::make_shared<arrow::KeyValueMetadata>();
124 auto it = ccdbUrls.find(m.name);
125 fieldMetadata->Append("url", it != ccdbUrls.end() ? it->second : m.defaultValue.asString());
126 auto runDep = runDependent.find("ccdb-run-dependent:" + m.name.substr(strlen("ccdb:")));
127 fieldMetadata->Append("runDependent", runDep != runDependent.end() ? runDep->second : "0");
128 auto pos = m.name.rfind(std::string_view{"::"});
129 auto columnName = fmt::format("f{}", m.name.substr(pos + 2, m.name.size()));
130 fields.emplace_back(std::make_shared<arrow::Field>(columnName, soa::asArrowDataType<int64_t[3]>(), false, fieldMetadata));
131 }
132 schemas.emplace_back(std::make_shared<arrow::Schema>(fields, schemaMetadata));
133 }
134
135 // Parse the declared path mappings once; they are fixed for the run of the workflow.
136 std::vector<std::vector<PathTable>> pathTables;
137 for (auto const& schema : schemas) {
138 auto& tables = pathTables.emplace_back();
139 for (auto const& field : schema->fields()) {
140 tables.push_back(PathTable::parse(*field->metadata()->Get("url")));
141 }
142 }
143
144 std::vector<std::pair<uint32_t, std::shared_ptr<arrow::FixedSizeListBuilder>>> allbuilders;
145 allbuilders.resize([&schemas]() { size_t size = 0; for (auto& schema : schemas) { size += schema->num_fields(); }; return size; }());
146 auto* pool = arrow::default_memory_pool();
147
148 int idx = 0;
149 int sidx = 0;
150 for (auto const& schema : schemas) {
151 for (auto const& _ : schema->fields()) {
152 auto value_builder = std::make_shared<arrow::Int64Builder>();
153 allbuilders[idx] = std::make_pair(sidx, std::make_shared<arrow::FixedSizeListBuilder>(pool, std::move(value_builder), 3));
154 ++idx;
155 }
156 ++sidx;
157 }
158
159 std::shared_ptr<CCDBFetcherHelper> helper = std::make_shared<CCDBFetcherHelper>();
160 CCDBFetcherHelper::initialiseHelper(*helper, options);
161 std::unordered_map<std::string, int> bindings;
162 fillValidRoutes(*helper, spec.outputs, bindings);
163
164 return adaptStateless([schemas, bindings, helper, allbuilders, pathTables](InputRecord& inputs, DataTakingContext& dtc, DataAllocator& allocator, TimingInfo& timingInfo, DataProcessingStats& stats) {
165 O2_SIGNPOST_ID_GENERATE(sid, ccdb);
166 O2_SIGNPOST_START(ccdb, sid, "fetchFromAnalysisCCDB", "Fetching CCDB objects for analysis%" PRIu64, (uint64_t)timingInfo.timeslice);
167 std::ranges::for_each(allbuilders, [](auto& builder) { builder.second->Reset(); });
168 for (auto i = 0U; i < schemas.size(); ++i) {
169 auto& schema = schemas[i];
170 std::vector<CCDBFetcherHelper::FetchOp> ops;
171 auto inputBinding = *schema->metadata()->Get("sourceTable");
172 auto outRouteDesc = *schema->metadata()->Get("outputRoute");
173 std::string outBinding = *schema->metadata()->Get("outputBinding");
174 auto timestampColumnName = schema->metadata()->Contains("timestamp-column") ? *schema->metadata()->Get("timestamp-column") : std::string{"fTimestamp"};
175 auto uniformityColumnName = schema->metadata()->Contains("uniformity-column") ? *schema->metadata()->Get("uniformity-column") : timestampColumnName;
176 O2_SIGNPOST_EVENT_EMIT_INFO(ccdb, sid, "fetchFromAnalysisCCDB",
177 "Fetching CCDB objects for %{public}s's columns with timestamps from %{public}s and putting them in route %{public}s",
178 outBinding.c_str(), inputBinding.c_str(), outRouteDesc.c_str());
179 // The timestamp and uniformity columns may live in different source tables (the
180 // run number is on aod::BCs, the timestamp on aod::Timestamps). Locate each by
181 // name across every declared source, and read them positionally.
182 std::shared_ptr<arrow::ChunkedArray> timestampColumn;
183 std::shared_ptr<arrow::ChunkedArray> uniformityColumn;
184 auto const& schemaKeys = schema->metadata()->keys();
185 auto const& schemaValues = schema->metadata()->values();
186 for (size_t mi = 0; mi < schemaKeys.size(); ++mi) {
187 if (schemaKeys[mi] != "sourceMatcher") {
188 continue;
189 }
190 auto sourceTable = inputs.get<TableConsumer>(DataSpecUtils::fromString(schemaValues[mi]))->asArrowTable();
191 if (auto column = sourceTable->GetColumnByName(timestampColumnName); column && !timestampColumn) {
192 timestampColumn = column;
193 }
194 if (auto column = sourceTable->GetColumnByName(uniformityColumnName); column && !uniformityColumn) {
195 uniformityColumn = column;
196 }
197 }
198 if (!timestampColumn) {
199 LOGP(fatal, "No source table of {} provides the timestamp column \"{}\"", outBinding, timestampColumnName);
200 }
201 if (!uniformityColumn) {
202 LOGP(fatal, "No source table of {} provides the uniformity column \"{}\"", outBinding, uniformityColumnName);
203 }
204 // Positional reading is only sound if the two sources are row-aligned; ASoA has
205 // no type-level way to state that, so it is checked here.
206 if (uniformityColumn->length() != timestampColumn->length()) {
207 LOGP(fatal, "Uniformity column \"{}\" has {} rows but timestamp column \"{}\" has {}; the two sources of {} are not row-aligned",
208 uniformityColumnName, uniformityColumn->length(), timestampColumnName, timestampColumn->length(), outBinding);
209 }
210 auto reserveSize = timestampColumn->length();
211 O2_SIGNPOST_EVENT_EMIT_INFO(ccdb, sid, "fetchFromAnalysisCCDB",
212 "There are %zu bindings available", bindings.size());
213 for (auto const& binding : bindings) {
214 O2_SIGNPOST_EVENT_EMIT_INFO(ccdb, sid, "fetchFromAnalysisCCDB",
215 "* %{public}s: %d",
216 binding.first.c_str(), binding.second);
217 }
218 int outputRouteIndex = bindings.at(outRouteDesc);
219 auto& spec = helper->routes[outputRouteIndex].matcher;
220 auto concrete = DataSpecUtils::asConcreteDataMatcher(spec);
221 Output output{concrete.origin, concrete.description, concrete.subSpec};
222 auto builders = allbuilders | std::views::filter([&i](auto const& builder) { return builder.first == i; });
223 unsigned int numBuilders = std::ranges::count_if(allbuilders, [&i](auto const& builder) { return builder.first == i; });
224 arrow::Status status;
225 std::ranges::for_each(builders, [&status, &reserveSize](auto& builder) {
226 if (reserveSize > builder.second->capacity()) {
227 status &= builder.second->Reserve(reserveSize - builder.second->capacity());
228 }
229 });
230 if (!status.ok()) {
231 throw framework::runtime_error_f("Failed to reserve arrays: ", status.ToString().c_str());
232 }
233
234 std::vector<DataAllocator::CacheId> lastIds(numBuilders, DataAllocator::CacheId{.value = -1, .handle = -1, .segment = -1});
235
236 // Rows sharing a uniformity value resolve to the same objects, so the query is
237 // issued once per distinct value and the resulting handles are repeated for the
238 // rest of the run. When uniformity is the timestamp itself (the default) this
239 // degenerates to the previous behaviour, one query per row.
240 std::vector<int64_t> uniformity;
241 bool const shortCircuit = uniformityColumn.get() != timestampColumn.get();
242 if (shortCircuit) {
243 uniformity.reserve(reserveSize);
244 for (auto uci = 0; uci < uniformityColumn->num_chunks(); ++uci) {
245 auto uchunk = uniformityColumn->chunk(uci);
246 auto const length = uchunk->data()->length;
247 switch (uchunk->type_id()) {
248 case arrow::Type::INT32:
249 for (int64_t ui = 0; ui < length; ++ui) {
250 uniformity.push_back(uchunk->data()->GetValuesSafe<int32_t>(1)[ui]);
251 }
252 break;
253 case arrow::Type::INT64:
254 case arrow::Type::UINT64:
255 for (int64_t ui = 0; ui < length; ++ui) {
256 uniformity.push_back(uchunk->data()->GetValuesSafe<int64_t>(1)[ui]);
257 }
258 break;
259 default:
260 LOGP(fatal, "Uniformity column \"{}\" of {} has unsupported arrow type {}",
261 uniformityColumnName, outBinding, uchunk->type()->ToString());
262 }
263 }
264 }
265 int64_t row = -1;
266 int64_t previousUniformity = 0;
267 bool haveResponses = false;
268 std::vector<CCDBFetcherHelper::Response> responses;
269
270 for (auto ci = 0; ci < timestampColumn->num_chunks(); ++ci) {
271 std::shared_ptr<arrow::Array> chunk = timestampColumn->chunk(ci);
272 auto const* timestamps = chunk->data()->GetValuesSafe<size_t>(1);
273
274 for (int64_t ri = 0; ri < chunk->data()->length; ri++) {
275 ++row;
276 bool const sameAsPrevious = shortCircuit && haveResponses && uniformity[row] == previousUniformity;
277 if (shortCircuit) {
278 previousUniformity = uniformity[row];
279 }
280 ops.clear();
281 int64_t timestamp = timestamps[ri];
282 // Key the path lookup on the uniformity value; when uniformity is the
283 // timestamp itself the mapping expresses validity intervals instead.
284 int64_t const uniformityKey = shortCircuit ? uniformity[row] : timestamp;
285 int fi = 0;
286 for (auto& field : schema->fields()) {
287 auto const& url = pathTables[i][fi++].resolve(uniformityKey, field->name());
288 // Time to actually populate the blob
289 // A run-dependent object is queried with the run number rather than by
290 // timestamp alone. The run comes from the uniformity value, so the column's
291 // table has to be uniform in the run number for this to mean anything.
292 int const fieldRunDependent = field->metadata()->Contains("runDependent")
293 ? std::stoi(*field->metadata()->Get("runDependent"))
294 : 0;
295 if (fieldRunDependent != 0 && uniformityColumnName != "fRunNumber") {
296 LOGP(fatal, R"(Column "{}" of {} is declared run-dependent, but its table is uniform in "{}" rather than fRunNumber, so no run number is available to query with. Declare the table with DECLARE_SOA_UNIFORM_TABLE(..., aod::BCs, o2::aod::bc::RunNumber, ...).)",
297 field->name(), outBinding, uniformityColumnName);
298 }
299 ops.push_back({
300 .spec = spec,
301 .url = url,
302 .timestamp = timestamp,
303 .runNumber = fieldRunDependent != 0 ? static_cast<int>(uniformityKey) : 1,
304 .runDependent = fieldRunDependent,
305 .queryRate = 0,
306 });
307 }
308 if (!sameAsPrevious) {
309 responses = CCDBFetcherHelper::populateCacheWith(helper, ops, timingInfo, dtc, allocator);
310 haveResponses = true;
311 }
312 O2_SIGNPOST_START(ccdb, sid, "handlingResponses",
313 "Got %zu responses from server.",
314 responses.size());
315 if (numBuilders != responses.size()) {
316 LOGP(fatal, "Not enough responses (expected {}, found {})", numBuilders, responses.size());
317 }
318 arrow::Status result;
319
320 int bi = 0;
321 for (auto& builder : builders) {
322 auto& response = responses[bi];
323 auto& lastId = lastIds[bi];
324 if (response.id.value != lastId.value) {
325 lastId.value = response.id.value;
327 }
328 result &= builder.second->Append();
329 auto* value_builder = dynamic_cast<arrow::Int64Builder*>(builder.second->value_builder());
330 result &= value_builder->Append(response.id.handle);
331 result &= value_builder->Append(response.id.segment);
332 result &= value_builder->Append(response.size);
333 ++bi;
334 }
335 if (!result.ok()) {
336 LOGP(fatal, "Error adding results from CCDB");
337 }
338 O2_SIGNPOST_END(ccdb, sid, "handlingResponses", "Done processing responses");
339 }
340 }
341 arrow::ArrayVector arrays;
342 std::ranges::for_each(builders, [&arrays](auto& builder) { arrays.push_back(*builder.second->Finish()); });
343 auto outTable = arrow::Table::Make(schema, arrays);
344 allocator.adopt(output, outTable);
345 }
346
347 stats.updateStats({(int)ProcessingStatsId::CCDB_CACHE_FETCHED_BYTES, DataProcessingStats::Op::Set, (int64_t)helper->totalFetchedBytes});
348 stats.updateStats({(int)ProcessingStatsId::CCDB_CACHE_REQUESTED_BYTES, DataProcessingStats::Op::Set, (int64_t)helper->totalRequestedBytes});
349 O2_SIGNPOST_END(ccdb, sid, "fetchFromAnalysisCCDB", "Fetching CCDB objects");
350 });
351 });
352}
353
354} // namespace o2::framework
std::string binding
std::string url
std::shared_ptr< arrow::Schema > schema
std::vector< std::shared_ptr< arrow::Field > > fields
int32_t i
void output(const std::map< std::string, ChannelStat > &channels)
Definition rawdump.cxx:197
uint16_t pos
Definition RawData.h:3
#define O2_DECLARE_DYNAMIC_LOG(name)
Definition Signpost.h:490
#define O2_SIGNPOST_EVENT_EMIT_INFO(log, id, name, format,...)
Definition Signpost.h:532
#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_START(log, id, name, format,...)
Definition Signpost.h:603
void adopt(const Output &spec, std::string *)
void adoptFromCache(Output const &spec, CacheId id, header::SerializationMethod method=header::gSerializationMethodNone)
Adopt an already cached message, using an already provided CacheId.
ServiceRegistryRef services()
Definition InitContext.h:34
The input API of the Data Processing Layer This class holds the inputs which are valid for processing...
decltype(auto) get(R binding, int part=0) const
const GLfloat * m
Definition glcorearb.h:4066
GLuint64EXT * result
Definition glcorearb.h:5662
GLsizeiptr size
Definition glcorearb.h:659
GLuint const GLchar * name
Definition glcorearb.h:781
GLuint GLsizei GLsizei * length
Definition glcorearb.h:790
const GLuint * arrays
Definition glcorearb.h:1314
constexpr framework::ConcreteDataMatcher matcher()
Definition ASoA.h:388
Defining ITS Vertex explicitly as messageable.
Definition Cartesian.h:288
AlgorithmSpec::ProcessCallback adaptStateless(LAMBDA l)
RuntimeErrorRef runtime_error_f(const char *,...)
AlgorithmSpec::InitCallback adaptStateful(LAMBDA l)
constexpr o2::header::SerializationMethod gSerializationMethodCCDB
Definition DataHeader.h:332
std::shared_ptr< arrow::DataType > asArrowDataType(int list_size=1)
Definition ArrowTypes.h:174
static AlgorithmSpec fetchFromCCDB(ConfigContext const &)
static auto populateCacheWith(std::shared_ptr< CCDBFetcherHelper > const &helper, std::vector< FetchOp > const &ops, TimingInfo &timingInfo, DataTakingContext &dtc, DataAllocator &allocator) -> std::vector< Response >
static void initialiseHelper(CCDBFetcherHelper &helper, ConfigParamRegistry const &options)
static bool hasOption(const std::vector< ConfigParamSpec > &specs, const std::string &optName)
Check if option is defined.
Helper struct to hold statistics about the data processing happening.
static InputSpec fromMetadataString(std::string s)
Create an InputSpec from metadata string.
static std::string describe(InputSpec const &spec)
static ConcreteDataMatcher asConcreteDataMatcher(InputSpec const &input)
static ConcreteDataMatcher fromString(std::string s)
Create a concrete data matcher from serialized string.
std::vector< ConfigParamSpec > options
Definition DeviceSpec.h:57
std::vector< OutputRoute > outputs
Definition DeviceSpec.h:63
std::variant< ConcreteDataMatcher, data_matcher::DataDescriptorMatcher > matcher
The actual matcher for the input spec.
Definition InputSpec.h:70
header::DataOrigin origin
Definition Output.h:28
static PathTable parse(std::string const &spec)
std::vector< int > row