Skip to content

Commit 3cc2fd6

Browse files
committed
Improve skill to migrate to the new CCDB fetcher
New Analysis Framework features now supported.
1 parent 26d73c3 commit 3cc2fd6

1 file changed

Lines changed: 169 additions & 1 deletion

File tree

.claude/commands/migrate-ccdb.md

Lines changed: 169 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,20 @@ The old approach uses `Service<o2::ccdb::BasicCCDBManager>` and calls `ccdb->get
1010
// In namespace o2::aod (or a sub-namespace):
1111
DECLARE_SOA_CCDB_COLUMN(StructName, getterName, ConcreteType, "CCDB/Object/Path");
1212

13+
// ... or, when the object needs fixing up after deserialisation, the _FULL form, whose
14+
// trailing argument is the finaliser (see "Objects needing post-deserialisation fixup"):
15+
DECLARE_SOA_CCDB_COLUMN_FULL(StructName, "fStructName", getterName, ConcreteType, "CCDB/Object/Path",
16+
[](ConcreteType* o) { return fixUp(o); });
17+
1318
DECLARE_SOA_TIMESTAMPED_TABLE(TableName, aod::Timestamps, o2::aod::timestamp::Timestamp, 1, "TABLEDESC",
1419
ns::StructName, ns::OtherColumn);
1520

21+
// ... or, when the object is constant across something coarser than a timestamp, the
22+
// uniform form (see "Uniformity: how often the object can change"):
23+
DECLARE_SOA_UNIFORM_TABLE(TableName, aod::Timestamps, o2::aod::timestamp::Timestamp,
24+
aod::BCs, o2::aod::bc::RunNumber, 1, "TABLEDESC",
25+
ns::StructName);
26+
1627
// In the task — basic usage:
1728
using MyBCs = soa::Join<aod::BCsWithTimestamps, aod::TableName>;
1829
void process(MyBCs const& bcs) {
@@ -84,6 +95,17 @@ DECLARE_SOA_TIMESTAMPED_TABLE(MyTaskCCDBObjects, aod::Timestamps, o2::aod::times
8495
} // namespace o2::aod
8596
```
8697

98+
Before writing the declaration, settle three things per column — each has its own section
99+
below, and getting them wrong is silent rather than loud:
100+
101+
1. **Does the object need fixing up after deserialisation?** If so use `DECLARE_SOA_CCDB_COLUMN_FULL`
102+
with a finaliser — see "Objects needing post-deserialisation fixup".
103+
2. **How often can the object change?** Timestamp (the default) or run — see "Uniformity: how
104+
often the object can change". Choose from the object's validity, not from how the old code
105+
happened to fetch it.
106+
3. **Is the path the same for every run?** If it varies by period, declare the mapping in the
107+
query string instead of porting the run-range `if/else` — see "Paths that vary by run".
108+
87109
Rules for naming:
88110
- `StructName` / `getterName`: derive from the type name, e.g. `GRPMagField` / `grpMagField`, `MeanVertex` / `meanVertex`
89111
- Table name: `<TaskStruct>CCDBObjects`, e.g. `SkimmerDalitzEECCDBObjects`
@@ -141,8 +163,154 @@ After making changes:
141163
- **`getRunDuration()` calls**: these use `BasicCCDBManager` statically and are unrelated to per-BC fetching — do not touch them.
142164
- **`ctpRateFetcher` / other helpers**: out of scope.
143165
- **Multiple tasks in one file**: tasks can share a single CCDB table declaration if they need the same objects; otherwise each task gets its own with a unique `_Desc_`.
144-
- **Non-BC timestamps**: if the timestamp comes from something other than a BC (e.g. computed manually), the migration is non-trivial — flag it instead of forcing it.
166+
- **Non-BC timestamps**: if the timestamp comes from something other than a BC, the migration is non-trivial — flag it instead of forcing it. This is the single most common blocker in practice. `Common/Tools/EventSelectionModule.h:243` computes `ts = sorTimestamp / 2 + eorTimestamp / 2` (mid-run, from `getRunDuration` / `AggregatedRunInfo`) and fetches `EventSelectionParams`, `ITS/Config/AlpideParam`, `TriggerAliases` and `ITS/Calib/TimeDeadMap` at it. A BC-keyed column fetches at each BC's own timestamp instead, so migrating these silently changes which object version is served whenever an object is revised mid-run. They need a run-keyed table before they can move.
145167
- **Global/init-time fetches** (e.g. `efficiencyGlobal.cxx` style): not migratable — the timestamped-table mechanism requires a row in a BC-keyed table.
146168
- **Magnetic-field side effects**: tasks that compute `d_bz` from a fetched `GRPMagField` and seed a propagator can keep that logic, just sourcing the object from `bc.grpMagField()` instead of `ccdb->getForTimeStamp(...)`.
147169

170+
## Lessons learned (established in-tree, with references)
171+
172+
### Why this migration matters beyond tidiness
173+
174+
The per-task path Configurable is a silent-divergence trap. `propagationService` and `propagationServiceV2` share the identical `ccdb.lutPath` Configurable (`Common/Tools/StandardCCDBLoader.h:45`, default `GLO/Param/MatLUT`), but config JSONs key overrides by *device name*. Every config in the tree carries a `propagation-service` block setting `GLO/Param/MatLUTInner` and no `propagation-service-v2` block, so V2 silently fell back to the full LUT — different material corrections, no warning. After migration the path is one option on the fetcher device, and two tasks disagreeing produces a warning (`ArrowSupport.cxx:641-666`) instead of silence.
175+
176+
### Objects needing post-deserialisation fixup
177+
178+
Some objects are not usable straight out of the ROOT streamer. `MatLayerCylSet` is a `FlatObject`: its internal pointers are unfixed and its voxel lookup unbuilt until `MatLayerCylSet::rectifyPtrFromFile()` runs. Use the `_FULL` form, which carries the finaliser (the plain `DECLARE_SOA_CCDB_COLUMN` passes an identity one):
179+
180+
```cpp
181+
DECLARE_SOA_CCDB_COLUMN_FULL(MatLUT, "fMatLUT", matLUT, o2::base::MatLayerCylSet, "GLO/Param/MatLUT", //!
182+
[](o2::base::MatLayerCylSet* lut) { return o2::base::MatLayerCylSet::rectifyPtrFromFile(lut); });
183+
```
184+
185+
The finaliser must be the **last** macro argument (commas in a lambda body are absorbed by `__VA_ARGS__`), has signature `T* (*)(T*)`, and runs on the receiving device once per (re)deserialisation, before the object is ever handed out. Ownership contract: whatever it returns is what the column cache later `delete`s, so a finaliser returning a *different* instance must dispose of the one it was given.
186+
187+
Do **not** put this fixup in the task. There is no `finaliseCCDB` hook on the analysis path (`adaptAnalysisTask` wires only `EndOfStream`, `AnalysisTask.h:610-619`; grep confirms zero uses of `finaliseCCDB` in O2Physics), and even if there were, an opt-in hook means a task that forgets it gets a silently broken object.
188+
189+
### Uniformity: how often the object can change
190+
191+
Every CCDB table declares a *uniformity column*: rows sharing its value resolve to the same
192+
object, so the fetcher queries once per distinct value instead of once per row.
193+
`DECLARE_SOA_TIMESTAMPED_TABLE` defaults it to the timestamp column, which is the
194+
pre-existing behaviour — every distinct timestamp may yield a different object.
195+
196+
Pick it from the object's real validity, and only then:
197+
198+
| Object changes ... | Uniformity | Declare with |
199+
| --- | --- | --- |
200+
| within a run (calibrations, drift velocity) | timestamp (default) | `DECLARE_SOA_TIMESTAMPED_TABLE` |
201+
| per run or per period (geometry, material, per-period calibrations) | `aod::BCs` / `aod::bc::RunNumber` | `DECLARE_SOA_UNIFORM_TABLE` |
202+
203+
Worked examples in the tree: `aod::TpcCalibCCDBObjects` keeps the timestamp default because
204+
the TPC drift velocity genuinely varies within a run; `aod::GeomCCDBObjects` and
205+
`aod::TrackTunerCCDBObjects` are run-uniform.
206+
207+
Two consequences worth knowing before choosing:
208+
209+
- The uniformity column may live in a **different table** from the timestamp — the run number
210+
is on `aod::BCs`, the timestamp on `aod::Timestamps`. Both are handed to the fetcher
211+
automatically (the table's `generateSources()` merges their originals) and read positionally.
212+
- Positional reading is only sound if the two sources are **row-aligned**. ASoA encodes no
213+
type-level relation between tables that merely have equal row counts, so this cannot be a
214+
`static_assert`; the fetcher compares the two column lengths and fatals on a mismatch.
215+
Anything joinable with the BCs is fine.
216+
217+
### Paths that vary by run: declare a mapping, not code
218+
219+
A column's path may be a plain path, or a mapping from uniformity value to path:
220+
221+
```
222+
"520259-529691=…/pp2023/pass4/vsPhi;559348-559387=…/ppRef/polarity_positive;fallback"
223+
```
224+
225+
Ranges are inclusive; either bound may be omitted (`-hi=path`, `lo-=path`); entries are
226+
separated by `;`; an entry without `=` is an explicit fallback. **A value matching no range
227+
is fatal**, deliberately — silently substituting another period's calibration is the failure
228+
mode this whole mechanism exists to prevent. A string with no `=` is a plain path, so
229+
existing columns are unaffected.
230+
231+
The mapping is *data*, carried in the schema metadata. That matters: the CCDB fetcher is a
232+
separate device and must not depend on code from the task that declared the column, so a
233+
resolver lambda would not do. It also means the run ranges stop being compiled in — the whole
234+
mapping is replaceable at runtime through the `ccdb:fXxx` option.
235+
236+
This replaces hand-written run-range tables. `TrackTuner::getPathInputFileAutomaticFromCCDB()`
237+
is the model case: ~50 lines of `else if (lo <= runNumber && runNumber <= hi)` became the
238+
declaration in `Common/DataModel/TrackTunerCCDBObjects.h`. When porting one, **derive the
239+
mapping mechanically and diff it against the source** — first-match-wins must reproduce the
240+
`if/else` order, which matters whenever ranges overlap (in TrackTuner, one PbPb range sits
241+
inside a pp range and must stay *after* it).
242+
243+
### Serving migrated and un-migrated callers from one module
244+
245+
Shared modules must keep working for tasks that have not migrated. Detect the capability
246+
rather than adding a configuration flag:
247+
248+
```cpp
249+
auto const& bc = collision.template bc_as<TBCs>();
250+
if constexpr (requires { bc.vdriftTgl(); }) {
251+
mVDriftMgr.update(bc.vdriftTgl()); // column path
252+
} else {
253+
mVDriftMgr.update(bc.timestamp()); // legacy CCDB query
254+
}
255+
```
256+
257+
The discarded branch is not instantiated, so an un-migrated caller compiles exactly as before
258+
and a migrated one never references the CCDB manager. `strangenessBuilderModule::updateVDrift`
259+
uses this. Where a whole function parameter falls away, add an overload of different arity
260+
that forwards (see "Shared module signatures") and put a `static_assert` with a readable
261+
message on the ccdb-free one, so calling it with an unjoined BC table names the missing table
262+
instead of failing somewhere inside the template.
263+
264+
### Two path settings must never both be live
265+
266+
After migration the column is the single source of truth for a path. If the task still has an
267+
old `Configurable<std::string>` for the same object, **fail loudly when both are set** rather
268+
than silently preferring one — that divergence is exactly the bug this migration exists to
269+
kill. `TrackPropagationModule::init` fatals when `trackTuner.pathInputFile` is non-empty while
270+
the calibrations come from columns, naming the option to use instead (`ccdb:fTrackTunerDca`).
271+
272+
Caveat: this test only works for Configurables whose default is empty. One with a non-empty
273+
default cannot be distinguished from an unset one, so that hole stays open until the framework
274+
can report whether an option was explicitly set.
275+
276+
### Grouping columns into tables
277+
278+
One table per **family of objects used together with similar validity intervals** — not one per consuming task. Geometry and material description (`GLO/Param/MatLUT`, and later `GLO/Config/GeometryAligned`, `GLO/Config/Geometry`, `<DET>/Calib/Align`; see `GRPGeomRequest` in `O2/Detectors/Base/src/GRPGeomHelper.cxx:44-60`) is one family with essentially static validity. The GRP family changes per run, and `GRPMagField` is requested per timeframe in O2 (`GRPGeomHelper.cxx:72`). Splitting on that boundary keeps a task from fetching a multi-hundred-MB LUT it never asked for.
279+
280+
**Several timestamped tables can be joined onto the same BCs.** `soa::Join<aod::BCsWithTimestamps, aod::GloCCDBObjects, aod::GeomCCDBObjects>` works: the duplicated `aod::Timestamps` is deduplicated when `originals` is merged (`ASoA.h:172-186`), giving 4 originals, and every accessor resolves. Do not invent per-use-case tables to work around a limitation that does not exist.
281+
282+
### Global state is not a lookup
283+
284+
Migrating removes CCDB *queries*, not side effects. Two things stay:
285+
286+
- `Propagator::initFieldFromGRP()` rebuilds or rescales a `MagneticField`, attaches it to `TGeoGlobalMagField::Instance()` and locks it (`O2/Detectors/Base/src/Propagator.cxx:107-149`). Keep it guarded on run change.
287+
- `Propagator::Instance()->setMatLUT()` is a pointer store, so it is cheaper to redo unconditionally every timeframe — and doing so picks up a relocated column buffer for free instead of dangling.
288+
289+
Everything else (mean vertex, run number) should become a direct read at the point of use, with no cached member and no `initCCDB()` helper. A cached pointer plus a "did the buffer move?" check is strictly worse than reading the column fresh.
290+
291+
`Propagator` cannot itself become a column value: private constructor, deleted copy/move, singleton `Instance()` (`Propagator.h:157-201`).
292+
293+
### Shared module signatures
294+
295+
If a shared module takes a `StandardCCDBLoader`, change it to take the values it actually uses (`int runNumber`, `MeanVertexObject const*`) and keep a thin forwarding overload for un-migrated callers, so V1 tasks stay byte-identical. `TrackPropagationModule::fillTrackTables` does this — the two overloads differ in arity, so overload resolution is unambiguous.
296+
297+
### What the migration does and does not buy
298+
299+
The fetcher downloads once into a shm cache and the column stores `(handle, segment, size)` (`AnalysisCCDBHelpers.cxx:213-222`). What is shared is the **serialised blob**; each consumer still streams its own heap copy in the column getter. So expect fewer downloads, one configuration point and cross-device consistency — but not a per-device RSS reduction. For a `FlatObject` like the LUT, real memory sharing needs a zero-copy path (`FlatObject::setActualBufferAddress`) that does not exist yet.
300+
301+
### Known gaps in the mechanism
302+
303+
- **Run-dependent objects are not served correctly.** The analysis fetcher still hardcodes `.runNumber = 1, .runDependent = 0` for every column, even though `CCDBFetcherHelper.cxx:189-195` implements the run-dependent query paths. `GLO/Config/GRPECS` is marked "Run dependent !!!" in O2 and already has a column — verify before relying on it. Now that a run-uniform table gives the fetcher a run number per row, wiring this through is small and worth doing.
304+
- **`getForRun` is not the same query.** `BasicCCDBManager::getForRun` resolves the run duration and queries at *mid-run* (`BasicCCDBManager.h:364-374`); a column queries at each BC's timestamp. Identical for objects with one version per run, divergent otherwise.
305+
- **Row cardinality, not query count.** The uniformity column already collapses the *queries* to one per distinct value, but the table still carries one row per BC per column — a `FixedSizeList<int64,3>`, 24 B, rebuilt every timeframe. Collapsing the rows too needs a non-extension table plus lookup by value at the consumer, which does not exist yet. So a run-uniform table costs the same arrow memory as before; what it saves is the fetching.
306+
- **Multi-run dataframes.** Skimmed datasets can span runs. Every existing consumer configures from `bcs.begin()` and applies it to the whole DF (`propagationServiceV2.cxx`, `StandardCCDBLoader.h:70-77`, `strangenessBuilderModule.h:850`), which is wrong for such a DF. Migrating preserves this bug unless it is fixed deliberately — do not claim the migration fixes it.
307+
308+
### Practical gotchas
309+
310+
- `DECLARE_SOA_CCDB_COLUMN` expands to code using `TClass` and `TBufferFile`, but `ASoA.h` only sees them forward-declared. A translation unit that includes the column header without otherwise pulling in `<TClass.h>` and `<TBufferFile.h>` fails to compile. Include them if needed.
311+
- A failed fetch is fatal, not silent: if `extractCCDBPayload` returns null the getter aborts naming the type, the path and the `ccdb:` option to check. A mistyped path therefore stops the job rather than dereferencing null.
312+
- Do not add a `sources` member to a table's metadata struct. It makes the struct satisfy both `soa::with_sources` and `soa::with_sources_generator`, and `getInputMetadata` becomes ambiguous.
313+
- Device options are matched by device *name*. Never look a task's own option up by a hardcoded name (`device.name == "propagation-service"` silently matched nothing in `propagation-service-v2`); take the running device from `initContext.services().get<DeviceSpec const>()`. Spell the type out rather than using `auto`, or the pre-existing `option.defaultValue.get<bool>()` becomes a dependent name and needs `template`.
314+
- Verify with the *control*: when changing a shared header, compile an un-migrated consumer too. A new error appearing in both is yours; the same errors in both means you changed nothing for them.
315+
148316
$ARGUMENTS

0 commit comments

Comments
 (0)