Skip to content

Commit 887dde6

Browse files
authored
DPL Analysis: send CCDB tables together with the objects they refer to (#16079)
1 parent de3ecdf commit 887dde6

6 files changed

Lines changed: 125 additions & 11 deletions

File tree

‎Framework/CCDBSupport/src/AnalysisCCDBHelpers.cxx‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
#include "AnalysisCCDBHelpers.h"
1313
#include "CCDBFetcherHelper.h"
14+
#include "Framework/ArrowContext.h"
1415
#include "Framework/ArrowTypes.h"
1516
#include "Framework/DataProcessingStats.h"
1617
#include "Framework/DeviceSpec.h"
@@ -25,6 +26,8 @@
2526
#include "Framework/ConfigParamsHelper.h"
2627
#include <fairmq/Version.h>
2728
#include <arrow/array/builder_binary.h>
29+
#include <arrow/io/memory.h>
30+
#include <arrow/ipc/writer.h>
2831
#include <arrow/type.h>
2932
#include <arrow/type_fwd.h>
3033
#include <arrow/util/key_value_metadata.h>
@@ -161,7 +164,7 @@ AlgorithmSpec AnalysisCCDBHelpers::fetchFromCCDB(ConfigContext const& /*ctx*/)
161164
std::unordered_map<std::string, int> bindings;
162165
fillValidRoutes(*helper, spec.outputs, bindings);
163166

164-
return adaptStateless([schemas, bindings, helper, allbuilders, pathTables](InputRecord& inputs, DataTakingContext& dtc, DataAllocator& allocator, TimingInfo& timingInfo, DataProcessingStats& stats) {
167+
return adaptStateless([schemas, bindings, helper, allbuilders, pathTables](InputRecord& inputs, DataTakingContext& dtc, DataAllocator& allocator, ArrowContext& arrowContext, TimingInfo& timingInfo, DataProcessingStats& stats) {
165168
O2_SIGNPOST_ID_GENERATE(sid, ccdb);
166169
O2_SIGNPOST_START(ccdb, sid, "fetchFromAnalysisCCDB", "Fetching CCDB objects for analysis%" PRIu64, (uint64_t)timingInfo.timeslice);
167170
std::ranges::for_each(allbuilders, [](auto& builder) { builder.second->Reset(); });
@@ -232,6 +235,7 @@ AlgorithmSpec AnalysisCCDBHelpers::fetchFromCCDB(ConfigContext const& /*ctx*/)
232235
}
233236

234237
std::vector<DataAllocator::CacheId> lastIds(numBuilders, DataAllocator::CacheId{.value = -1, .handle = -1, .segment = -1});
238+
std::vector<DataAllocator::CacheId> referenced;
235239

236240
// Rows sharing a uniformity value resolve to the same objects, so the query is
237241
// issued once per distinct value and the resulting handles are repeated for the
@@ -321,7 +325,7 @@ AlgorithmSpec AnalysisCCDBHelpers::fetchFromCCDB(ConfigContext const& /*ctx*/)
321325
auto& lastId = lastIds[bi];
322326
if (response.id.value != lastId.value) {
323327
lastId.value = response.id.value;
324-
allocator.adoptFromCache(output, response.id, header::gSerializationMethodCCDB);
328+
referenced.push_back(response.id);
325329
}
326330
result &= builder.second->Append();
327331
auto* value_builder = dynamic_cast<arrow::Int64Builder*>(builder.second->value_builder());
@@ -338,8 +342,25 @@ AlgorithmSpec AnalysisCCDBHelpers::fetchFromCCDB(ConfigContext const& /*ctx*/)
338342
}
339343
arrow::ArrayVector arrays;
340344
std::ranges::for_each(builders, [&arrays](auto& builder) { arrays.push_back(*builder.second->Finish()); });
341-
auto outTable = arrow::Table::Make(schema, arrays);
342-
allocator.adopt(output, outTable);
345+
// The table is sent by the message backend, followed by the objects it refers to,
346+
// so that they all leave in one multipart and the consumer relays them into the
347+
// same slot. Through the arrow backend, the table would leave on its own first.
348+
auto stream = arrow::io::BufferOutputStream::Create().ValueOrDie();
349+
auto writer = arrow::ipc::MakeStreamWriter(stream, schema).ValueOrDie();
350+
if (!writer->WriteRecordBatch(*arrow::RecordBatch::Make(schema, reserveSize, arrays)).ok()) {
351+
LOGP(fatal, "Unable to serialise the table of {}", outBinding);
352+
}
353+
auto buffer = stream->Finish().ValueOrDie();
354+
auto payload = allocator.makeVector<char>(output);
355+
payload.assign(buffer->data(), buffer->data() + buffer->size());
356+
allocator.adoptContainer(output, std::move(payload), DataAllocator::CacheStrategy::Never, header::gSerializationMethodArrow);
357+
// Accounted for like the tables of the arrow backend, for the rate limiting.
358+
allocator.findMessageHeader(output)->serialization = header::gSerializationMethodArrow;
359+
arrowContext.updateBytesSent(buffer->size());
360+
arrowContext.updateMessagesSent(1);
361+
for (auto const& id : referenced) {
362+
allocator.adoptFromCache(output, id, header::gSerializationMethodCCDB);
363+
}
343364
}
344365

345366
stats.updateStats({(int)ProcessingStatsId::CCDB_CACHE_FETCHED_BYTES, DataProcessingStats::Op::Set, (int64_t)helper->totalFetchedBytes});

‎Framework/CCDBSupport/src/CCDBFetcherHelper.cxx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ auto CCDBFetcherHelper::populateCacheWith(std::shared_ptr<CCDBFetcherHelper> con
256256
helper->totalFetchedBytes += size;
257257
helper->totalRequestedBytes += size;
258258
api.appendFlatHeader(v, headers);
259-
auto cacheId = CCDBHelpers::adoptAndReplaceCachedMessage(allocator, helper->mapURL2DPLCache, path, output, std::move(v), header::gSerializationMethodCCDB);
259+
auto cacheId = CCDBHelpers::adoptAndReplaceCachedMessage(allocator, helper->mapURL2DPLCache, path, output, std::move(v), header::gSerializationMethodCCDB, DataAllocator::CacheStrategy::CacheOnly);
260260
helper->mapURL2DPLCache[path] = cacheId;
261261
responses.emplace_back(Response{.id = cacheId, .size = size, .request = nullptr});
262262
O2_SIGNPOST_EVENT_EMIT(ccdb, sid, "populateCacheWith", "Caching %{public}s for %{public}s (DPL id %" PRIu64 ", size %zu)", path.data(), headers["ETag"].data(), cacheId.value, size);
@@ -274,7 +274,7 @@ auto CCDBFetcherHelper::populateCacheWith(std::shared_ptr<CCDBFetcherHelper> con
274274
helper->totalFetchedBytes += size;
275275
helper->totalRequestedBytes += size;
276276
api.appendFlatHeader(v, headers);
277-
auto cacheId = CCDBHelpers::adoptAndReplaceCachedMessage(allocator, helper->mapURL2DPLCache, path, output, std::move(v), header::gSerializationMethodCCDB);
277+
auto cacheId = CCDBHelpers::adoptAndReplaceCachedMessage(allocator, helper->mapURL2DPLCache, path, output, std::move(v), header::gSerializationMethodCCDB, DataAllocator::CacheStrategy::CacheOnly);
278278
helper->mapURL2DPLCache[path] = cacheId;
279279
responses.emplace_back(Response{.id = cacheId, .size = size, .request = nullptr});
280280
O2_SIGNPOST_EVENT_EMIT(ccdb, sid, "populateCacheWith", "Caching %{public}s for %{public}s (DPL id %" PRIu64 ")", path.data(), headers["ETag"].data(), cacheId.value);

‎Framework/CCDBSupport/src/CCDBHelpers.cxx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -255,10 +255,11 @@ DataAllocator::CacheId CCDBHelpers::adoptAndReplaceCachedMessage(
255255
std::string const& path,
256256
Output const& output,
257257
o2::pmr::vector<char>&& v,
258-
o2::header::SerializationMethod method)
258+
o2::header::SerializationMethod method,
259+
DataAllocator::CacheStrategy strategy)
259260
{
260261
auto oldIt = cache.find(path);
261-
auto cacheId = allocator.adoptContainer(output, std::move(v), DataAllocator::CacheStrategy::Always, method);
262+
auto cacheId = allocator.adoptContainer(output, std::move(v), strategy, method);
262263
if (oldIt != cache.end()) {
263264
allocator.pruneFromCache(oldIt->second);
264265
}

‎Framework/CCDBSupport/src/CCDBHelpers.h‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,16 @@ struct CCDBHelpers {
3939
/// @a output DPL Output matcher
4040
/// @a v freshly-fetched CCDB payload; consumed by the call, leaving @a v empty
4141
/// @a method serialization-method tag written into the message header
42+
/// @a strategy CacheOnly to cache the message without sending it
4243
/// @return the new CacheId; the caller must record it in its map
4344
static DataAllocator::CacheId adoptAndReplaceCachedMessage(
4445
DataAllocator& allocator,
4546
std::unordered_map<std::string, DataAllocator::CacheId> const& cache,
4647
std::string const& path,
4748
Output const& output,
4849
o2::pmr::vector<char>&& v,
49-
o2::header::SerializationMethod method);
50+
o2::header::SerializationMethod method,
51+
DataAllocator::CacheStrategy strategy = DataAllocator::CacheStrategy::Always);
5052
};
5153

5254
} // namespace o2::framework

‎Framework/Core/include/Framework/DataAllocator.h‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,9 @@ class DataAllocator
528528

529529
enum struct CacheStrategy : int {
530530
Never = 0,
531-
Always = 1
531+
Always = 1,
532+
/// Cache the message without sending it. Use adoptFromCache to send it.
533+
CacheOnly = 2
532534
};
533535

534536
template <typename ContainerT>
@@ -628,7 +630,7 @@ DataAllocator::CacheId DataAllocator::adoptContainer(const Output& spec, Contain
628630
);
629631

630632
CacheId cacheId{0, 0, 0}; //
631-
if (cache == CacheStrategy::Always) {
633+
if (cache != CacheStrategy::Never) {
632634
// The message will be shallow cloned in the cache. Since the
633635
// clone is indistinguishable from the original, we can keep sending
634636
// the original.
@@ -637,6 +639,9 @@ DataAllocator::CacheId DataAllocator::adoptContainer(const Output& spec, Contain
637639
cacheId.handle = meta.fHandle;
638640
cacheId.segment = meta.fSegmentId;
639641
}
642+
if (cache == CacheStrategy::CacheOnly) {
643+
return cacheId;
644+
}
640645

641646
context.add<MessageContext::TrivialObject>(std::move(headerMessage), std::move(payloadMessage), routeIndex);
642647
return cacheId;

‎Framework/Core/test/test_DataRelayer.cxx‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,91 @@ TEST_CASE("DataRelayer")
11051105
checkContents();
11061106
}
11071107

1108+
// The analysis CCDB fetcher sends a CCDB table followed by the objects it
1109+
// refers to, all for the same route. The consumer must get them in the same
1110+
// slot, with the table as the first part, which holds when they are relayed
1111+
// from one multipart, i.e. without checking for completion in between.
1112+
SECTION("CCDBTableAndObjectsCompleteTogether")
1113+
{
1114+
InputSpec spec0{"bcs", "AOD", "BC"};
1115+
InputSpec spec1{"ccdb", "AOD", "GLOCCDBOBJ", 1};
1116+
1117+
std::vector<InputRoute> inputs = {
1118+
InputRoute{spec0, 0, "Fake0", 0},
1119+
InputRoute{spec1, 1, "Fake1", 0},
1120+
};
1121+
1122+
std::vector<InputChannelInfo> infos{1};
1123+
TimesliceIndex index{1, infos};
1124+
ref.registerService(ServiceRegistryHelpers::handleForService<TimesliceIndex>(&index));
1125+
1126+
auto policy = CompletionPolicyHelpers::consumeWhenAll();
1127+
DataRelayer relayer(policy, inputs, index, {registry}, -1);
1128+
relayer.setPipelineLength(4);
1129+
1130+
auto transport = fair::mq::TransportFactory::CreateTransportFactory("zeromq");
1131+
auto channelAlloc = o2::pmr::getTransportAllocator(transport.get());
1132+
1133+
auto relayOne = [&](o2::header::DataDescription description, uint32_t subSpec, o2::header::SerializationMethod method, size_t timeslice) {
1134+
DataHeader dh;
1135+
dh.dataOrigin = "AOD";
1136+
dh.dataDescription = description;
1137+
dh.subSpecification = subSpec;
1138+
dh.splitPayloadIndex = 0;
1139+
dh.splitPayloadParts = 1;
1140+
dh.payloadSerializationMethod = method;
1141+
std::array<fair::mq::MessagePtr, 2> msgs;
1142+
msgs[0] = o2::pmr::getMessage(Stack{channelAlloc, dh, DataProcessingHeader{timeslice, 1}});
1143+
msgs[1] = transport->CreateMessage(8);
1144+
DataRelayer::InputInfo info{0, 2, DataRelayer::InputType::Data, {ChannelIndex::INVALID}};
1145+
relayer.relay(msgs[0]->GetData(), msgs.data(), info, 2);
1146+
};
1147+
auto relayCCDB = [&](size_t timeslice) {
1148+
relayOne("GLOCCDBOBJ", 1, o2::header::gSerializationMethodArrow, timeslice);
1149+
relayOne("GLOCCDBOBJ", 1, o2::header::gSerializationMethodCCDB, timeslice);
1150+
relayOne("GLOCCDBOBJ", 1, o2::header::gSerializationMethodCCDB, timeslice);
1151+
};
1152+
1153+
// Table and objects in one multipart: one slot, with the table first.
1154+
relayOne("BC", 0, o2::header::gSerializationMethodArrow, 0);
1155+
relayCCDB(0);
1156+
std::vector<RecordAction> ready;
1157+
relayer.getReadyToProcess(ready);
1158+
REQUIRE(ready.size() == 1);
1159+
auto result = relayer.consumeAllInputsForTimeslice(ready[0].slot);
1160+
REQUIRE((result[1] | count_parts{}) == 3);
1161+
for (size_t p = 0; p < 3; ++p) {
1162+
auto* dh = o2::header::get<DataHeader*>((result[1] | get_header{p})->GetData());
1163+
REQUIRE(dh->payloadSerializationMethod == (p == 0 ? o2::header::gSerializationMethodArrow : o2::header::gSerializationMethodCCDB));
1164+
}
1165+
1166+
// The same, with the CCDB parts arriving before the other input.
1167+
relayCCDB(1);
1168+
ready.clear();
1169+
relayer.getReadyToProcess(ready);
1170+
REQUIRE(ready.empty());
1171+
relayOne("BC", 0, o2::header::gSerializationMethodArrow, 1);
1172+
relayer.getReadyToProcess(ready);
1173+
REQUIRE(ready.size() == 1);
1174+
result = relayer.consumeAllInputsForTimeslice(ready[0].slot);
1175+
REQUIRE((result[1] | count_parts{}) == 3);
1176+
1177+
// What happened when the table and the objects were sent separately: the
1178+
// slot completes on the table alone, and the objects open a new slot which
1179+
// can never complete.
1180+
relayOne("BC", 0, o2::header::gSerializationMethodArrow, 2);
1181+
relayOne("GLOCCDBOBJ", 1, o2::header::gSerializationMethodArrow, 2);
1182+
ready.clear();
1183+
relayer.getReadyToProcess(ready);
1184+
REQUIRE(ready.size() == 1);
1185+
result = relayer.consumeAllInputsForTimeslice(ready[0].slot);
1186+
REQUIRE((result[1] | count_parts{}) == 1);
1187+
relayOne("GLOCCDBOBJ", 1, o2::header::gSerializationMethodCCDB, 2);
1188+
ready.clear();
1189+
relayer.getReadyToProcess(ready);
1190+
REQUIRE(ready.empty());
1191+
}
1192+
11081193
// An expiring input is materialised straight into the slot, so with one
11091194
// shared buffer per slot it lands *after* whatever the other inputs already
11101195
// hold -- the cells are then no longer in input order. Check that the data

0 commit comments

Comments
 (0)