Centralize schema-safe writable ROOT I/O (preserve StreamerInfo on ROOT ops) - #567
cmargalejo wants to merge 12 commits into
Conversation
|
@AlvaroEzq, @lobis, @hgmaluenda, @juanangp , small ping on this PR. We want to get a new stable REST release ready, and this is one of the PRs still waiting for review. Could you have a look when you get a chance? |
Introduce a move-only TRestRootFileHandle that inventories the exact on-disk StreamerInfo tuples and schema rules before a writable open. UPDATE now starts in READ mode, prepares loaded and emulated classes, preflights every historical entry, verifies that the file identity is unchanged, and only then reopens the same TFile handle for mutation. Make the policy explicit: remote files remain valid read sources but cannot be writable destinations, local file URLs are supported, and close failures are observable. Exact class/version/checksum entries are marked instead of broadly mutating unrelated StreamerInfo state. Add a transactional ROOT merge helper that validates recursive key/class manifests, TTree entry totals, and schema inventories before and after atomic installation. Existing targets are protected by rollback backups, input files are removed only after complete validation, and cleanup or rollback failures are surfaced to callers.
Replace ad-hoc writable TFile opens in framework macros, data-set helpers, runs, process runners, and threads with TRestRootFileHandle. Keep transient owning handles beside legacy raw aliases where ROOT reflection requires them, and make TRestRun non-copyable so its file state cannot be shallow-copied. Use the transactional merge path for TRestRun outputs, including the create-if-absent case, and leave source files intact whenever validation, replacement, backup cleanup, or input cleanup fails. Analysis-chain attachment is restored to read-only operation. Write split process metadata to an explicit destination rather than relying on gDirectory, and make input mode handling explicit: READ/OPEN and UPDATE are accepted while destructive or unsupported modes fail clearly.
Build independent version-1 and version-2 ROOT dictionaries in which fSamples keeps the same member name while changing from vector<float> to vector<double>. Deliberately provide no pragma for that member: the version-2 reader first proves ROOT's automatic same-name collection conversion before REST mutates the file, and repeats the value checks after unloaded and loaded update paths. Exercise embedded schema-rule preservation independently with a renamed scalar, mapping the legacy fLegacyCode member to fRenamedCode through a pragma. Assert that the on-disk rule inventory contains this renamed-field rule and no rule for fSamples, verify the rule-based value conversion before and after updates, and continue checking the original payload with the matching version-1 dictionary. Preserve both exact historical payload StreamerInfo tuples while covering preflight byte preservation, remote mutation policy, file URL and move-only lifecycles, unsupported run modes, and explicit split-metadata destinations. Compile-time assertions protect the intended ownership semantics. Exercise transactional merging with recursive target-only and input-only keys, aggregate TTree entry checks, incompatible same-path classes, missing sources, and a forced post-install validation failure. The failure tests require byte-identical rollback and retention of every source file.
Add a developer guide for TRestRootFileHandle and PrepareBorrowedUpdate, including the read-first schema preflight, preservation of historical StreamerInfo and embedded rules, local-only mutation policy, move-only ownership, and explicit Close/error handling. Document transactional merge and replacement behavior, how existingTarget participates, when inputs are removed, and how callers must interpret cleanup and rollback failures. Bound the guarantee to the implemented platform filesystem operations rather than promising crash durability or atomicity on every mounted filesystem. Explain why TRestRun is intentionally non-copyable and give practical migration paths through references, pointers, unique ownership, or reconstruction. Link the guide from CONTRIBUTING.md so it is discoverable from the repository's developer documentation entry point.
520b1fa to
e0e156d
Compare
for more information, see https://pre-commit.ci
StreamerInfo on ROOT ops)
Vindaar
left a comment
There was a problem hiding this comment.
The addition of the new changes should make it so that REST files should remain stable and readable in the future even under further schema changes. That's a big win for us, because there's nothing worse than breaking backwards compatibility for old data files. Especially given the context of the likely changes still to come for BabyIAXO once we get closer to actual data taking. At some point development will speed up and we are going to be happy this is done now and not later.
PR #567 recreated an existing destination by feeding it back to TFileMerger as an ordinary input. On ROOT 6.26.06 that rewrote target-only TRestAnalysisTree objects and left their REST observable bindings unusable even though the raw branch data remained present. It also made schema preflight depend on finding an exact already-registered TStreamerInfo identity, which rejected valid on-disk metadata such as ROOT::TIOFeatures. Seed the same-directory transaction file with a byte-for-byte copy of the existing target, prepare that copy through the checked UPDATE path, and give the prepared TFile to TFileMerger while adding only new worker inputs. This restores the historical UPDATE contract: target-only keys remain opaque, new inputs merge with one another, and same-named incoming objects replace target objects. Atomic replacement, rollback, remote-path rejection, permission preservation, and post-write validation remain intact. Resolve on-disk StreamerInfos with the same BuildCheck and ownership protocol used by ROOT itself, then register embedded schema rules before any writable transition or merge. Continue requiring exact historical user schemas and rules, while accepting only ROOT-defined normalization of standard-library implementation descriptors identified through TClassEdit. The developer guide now documents these invariants and the resulting merge semantics.
Add focused regressions for every failure class found while validating #567. A realistic tree-bearing fixture supplies ROOT::TIOFeatures and verifies that UPDATE succeeds even when automatic StreamerInfo registration is disabled. A schema-only worker subprocess proves that historical user schemas and embedded evolution rules arriving from merge inputs are retained, while a frozen pair-base fixture covers ROOT normalization of generated standard-library metadata. Exercise existing-target transactions with real TRestAnalysisTree observables using both one and two worker files. A fresh verifier process checks raw TTree::Draw values and TRestRun/GetObservableValue results, and the test also requires the target-only AnalysisTree key payload to remain byte-position and size identical. This catches the ROOT 6.26.06 binding regression that entry-count and key-name checks missed. Retain no-mutation and rollback coverage through an explicit test-only preflight failure hook, and align merge assertions with ROOT historical UPDATE behavior: worker trees merge together and replace same-named target trees while target-only objects stay untouched. The helper fixtures keep loaded and unloaded dictionary states isolated across processes so the tests do not accidentally pass through shared ROOT class state.
|
Hey @Vindaar, reading doc/developer/Safe writable ROOT IO.md, two small thoughts.
What do you think? |
Combine schema inventory and ROOT registry resolution into one on-disk read, collect required class-index entries directly, and share schema-rule registration and output validation. Use one internal exception boundary to unwind file ownership before candidate cleanup while preserving the public boolean/error interface and rollback after publication failures. Reject incompatible same-version cached user schemas before making files writable, comparing ROOT internal versions as they are serialized. Restore ROOT UPDATE create-if-absent semantics with non-overwriting CREATE, honor local path expansion, and resolve destination aliases so successful input cleanup cannot delete the published output. Retain every existing regression and add seven focused cases, including independent old/new conflicting dictionaries, byte-identical failure, gain-map export, path aliases and new-output rollback. The production implementation is 187 lines smaller; tests and developer guidance are measured separately. Validation: all 78 framework/library/restG4 tests pass in official serial order, all 105 replayed workflow run/build steps pass on modern and reference dependencies, 26 focused host tests pass, and configured pre-commit checks pass. CI-only submodule checkouts are deliberately excluded from this commit.
Keep experiment evidence separate from the implementation: architecture and line-count comparison, exact dependency identities, all 78 serial CTest results, 105 passing workflow run/build steps, benchmark limits and remaining design risks. Include the workflow replay runner and small comparison benchmark so future reviewers can reproduce the checks in the persistent modern/reference containers. Generated scripts/logs, downloaded data, installed libraries and CI-only submodule checkout changes remain outside this evidence commit.
Carry the remaining clarity improvements from the independent PR #567 review into the compact implementation. Remove redundant TRestRun closes where CloseFile has already cleared both owned handles. Track create-for-update explicitly, reject unresolved local destinations, and distinguish failed CREATE opens from read-only UPDATE preflight errors. Include the input filename when schema or content inspection fails during a multi-source merge. Validation: incremental ROOT 6.26/10 build and 26/26 focused non-network CTest cases pass; git diff --check passes. Existing full-matrix evidence belongs to the preceding compact implementation. No validation submodule checkouts are staged.
NormalizeSchemaRule is now used only to serialize rules into a comparable form. The compact registration loop gets the target directly from its parsed rule, so remove the unused optional output parameter and assignment without changing schema registration or validation. Validation: focused ROOT build, 26/26 non-network CTest cases, configured pre-commit checks and git diff --check pass.
Make the UPDATE migration warning visible in the repository README and contribution guide, rather than limiting it to developer documentation. Clarify that direct ROOT calls in existing library code and private macros are not intercepted automatically, and that read-only macros do not need this migration. Add a user tutorial with a complete checked-write example, explicit Close/error handling, borrowed-pointer lifetimes, search guidance and warnings against bypassing failed preflight. Explain missing-file creation, local destinations and the distinction between schema preservation, recovery and transaction durability. Qualify the README's blanket historical compatibility claim. Validation: pre-commit and whitespace checks passed; migration-guide links resolve. The C++ example compiled and passed a create/update/readback smoke test against the focused build (the standalone build-tree run emitted ROOT dictionary-location warnings). No production code or submodule gitlinks changed.
|
I've added a few more commits which mostly simplify the PR a little bit. The main points remain unchanged though. Given the large size of the PR, here is an HTML page to review the code more easily, where we have a clearer overview of which files contain what kind of change, what to focus on when reviewing etc. You might find it useful: https://pages.vindaar.de/rest-for-physics/pr567_compact_review.html
Yes, there seem to be ~4 existing usages in detectorlib and tracklib that we still have to update.
Yes, this is a good idea in principle. We can have a small script that checks for existing lines that use the old style model and then write a warning or even CI failure if they are detected. The thing is, lines using |
Fixes #568
UPDATE 2026-08-19:
Also updating this PR significantly. The idea was essentially correct, but it didn't go far enough. The issue is that with the PR in the original form there would be many edge cases where we could have lost the
StreamerInfoin the future. Either through existing code paths or more importantly due to the way the developers of REST need to use the ROOT I/O in order to not lose that information.TRestRootFileHandle.StreamerInfoand schema rules, including classes whose dictionaries are not currently loaded.TFilecall sites are migrated to this checked path.TRestRunbecomes non-copyable because it now owns file handles and contains non-owning aliases into their contents.vector<float> → vector<double>evolution, explicit renamed-member rules, update failures, merges, rollback, and remote paths.So #566 repairs the specific damaged legacy detector-signal files; #567 prevents normal REST updates and merges from discarding the historical schema information needed to read old data safely.
Important note for developers: In the future it is mandatory that all ROOT I/O happens through the new safe interop. See the documentation about this here:
https://github.com/rest-for-physics/framework/blob/07c720c6cd89f594c2047a3bbdff609c2e7f3c89/doc/developer/Safe%20writable%20ROOT%20IO.md
In principle this should make our lives easier.
The addition of the new I/O interface and updating of all existing ROOT file I/O code is the reason the PR exploded in size now.
Original PR message
The problem
When a ROOT file is closed,
TFile::WriteStreamerInfo()replaces the file'sStreamerInfo record, keeping only the infos of the classes streamed during that
session.
TRestRun::MergeToOutputFilemerges the threads' output files (whichpreserves the infos), but then reopens the merged file in UPDATE mode to write
the metadata: closing that session wipes the StreamerInfos of all event classes,
keeping only the metadata classes.
As a consequence, no restManager output file contains event-class
StreamerInfos, which silently breaks ROOT schema evolution whenever an event
class definition changes. This is what made files written before the
TRestDetectorSignalvector<Float_t>→vector<Double_t>change unreadable(rest-for-physics/detectorlib#125): without the on-disk layout description,
ROOT misreads the float payload as doubles and allocates GBs of garbage.
(restG4 files are unaffected: they are written in a single session.)
The fix
New
TRestTools::PreserveStreamerInfos(TFile*): called right after opening afile in UPDATE mode, it re-tags every StreamerInfo already stored in the file
(same marking as the deprecated
TStreamerInfo::TagFile) so thatTFile::WriteStreamerInfowrites them out again on close. Called at the UPDATEsessions that write into REST data files:
TRestRun::MergeToOutputFile(the main restManager output path)TRestRun::UpdateOutputFileTRestProcessRunnersplit-file metadata update of the main fileTRestDataSet::ExportVerification
Merging two files containing
TRestDetectorSignalEventtrees viaTRestRun::MergeToOutputFile: without the fix the merged file keeps onlyTRestRun/TRestMetadatainfos; with it,TRestDetectorSignalEvent,TRestEventandTRestDetectorSignalsurvive the metadata session and thefile reads back correctly.
Note: this protects files written from now on. Existing files have already
lost their StreamerInfos; for the detector signal case the data is recoverable
with the tool in #566.