You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
+
87
109
Rules for naming:
88
110
-`StructName` / `getterName`: derive from the type name, e.g. `GRPMagField` / `grpMagField`, `MeanVertex` / `meanVertex`
89
111
- Table name: `<TaskStruct>CCDBObjects`, e.g. `SkimmerDalitzEECCDBObjects`
@@ -141,8 +163,154 @@ After making changes:
141
163
-**`getRunDuration()` calls**: these use `BasicCCDBManager` statically and are unrelated to per-BC fetching — do not touch them.
142
164
-**`ctpRateFetcher` / other helpers**: out of scope.
143
165
-**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.
145
167
-**Global/init-time fetches** (e.g. `efficiencyGlobal.cxx` style): not migratable — the timestamped-table mechanism requires a row in a BC-keyed table.
146
168
-**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(...)`.
147
169
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):
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:
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.
0 commit comments