Skip to content

Commit cc2b07e

Browse files
committed
ENH: Add UserDataFilePath redirect to WriteRecoveryFile
Adds an optional userDataFilePath parameter to WriteRecoveryFile. When set, the writer emits a minimal HDF5 file containing only the file- version tag plus a root-level "UserDataFilePath" string attribute. The DataStructure and Pipeline parameters are ignored in that mode — the user's authoritative `.dream3d` output (produced by a trailing WriteDREAM3DFilter in the pipeline) is the real data carrier. The recovery scanner reads the attribute back at relaunch time to redirect the load. Also adds the companion reader DREAM3D::ReadUserDataFilePathAttribute and a round-trip unit test covering both the redirect and the standard (no-attribute) cases. Existing WriteRecoveryFile call sites are source-compatible — the new parameter defaults to std::nullopt, which preserves today's behavior (full DataStructure + Pipeline write with OOC placeholder overrides). Signed-off-by: Joey Kleingers <joey.kleingers@bluequartz.net>
1 parent 0ab87f5 commit cc2b07e

3 files changed

Lines changed: 151 additions & 34 deletions

File tree

src/simplnx/Utilities/Parsing/DREAM3D/Dream3dIO.cpp

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
#include <nlohmann/json.hpp>
3232

3333
#include <fstream>
34+
#include <optional>
3435
#include <set>
3536
#include <sstream>
3637
#include <stdexcept>
@@ -45,6 +46,7 @@ namespace
4546
constexpr StringLiteral k_DataStructureGroupTag = "DataStructure";
4647
constexpr StringLiteral k_LegacyDataStructureGroupTag = "DataContainers";
4748
constexpr StringLiteral k_FileVersionTag = "FileVersion";
49+
constexpr StringLiteral k_UserDataFilePathTag = "UserDataFilePath";
4850
constexpr StringLiteral k_PipelineJsonTag = "Pipeline";
4951
constexpr StringLiteral k_PipelineNameTag = "Current Pipeline";
5052
constexpr StringLiteral k_PipelineVersionTag = "Pipeline Version";
@@ -2364,25 +2366,64 @@ Result<> DREAM3D::WriteFile(const fs::path& path, const DataStructure& dataStruc
23642366
return {};
23652367
}
23662368

2367-
Result<> DREAM3D::WriteRecoveryFile(const fs::path& path, const DataStructure& dataStructure, const Pipeline& pipeline)
2369+
Result<> DREAM3D::WriteRecoveryFile(const fs::path& path, const DataStructure& dataStructure, const Pipeline& pipeline, std::optional<fs::path> userDataFilePath)
23682370
{
2369-
// Obtain the global DataIOCollection so we can activate the write-array-override.
2370-
// The SimplnxOoc plugin registers a callback on this collection at startup that
2371-
// knows how to write OOC array placeholders instead of full data.
2372-
auto& ioCollection = DataStoreUtilities::GetIOCollection();
2371+
// Minimal-redirect variant: the user's pipeline ends with a WriteDREAM3DFilter,
2372+
// so the authoritative data is at userDataFilePath on disk. Write a tiny HDF5
2373+
// file that only carries the file-version tag plus a root-level attribute
2374+
// pointing to the user's file. dataStructure and pipeline are intentionally
2375+
// ignored — the user's file already has them (or the caller has recorded the
2376+
// pipeline separately as a paired `.d3dpipeline`).
2377+
if(userDataFilePath.has_value())
2378+
{
2379+
auto fileWriter = nx::core::HDF5::FileIO::WriteFile(path);
2380+
if(!fileWriter.isValid())
2381+
{
2382+
return MakeErrorResult(-9046, fmt::format("Failed to create recovery file at path {}", path.string()));
2383+
}
2384+
auto versionResult = WriteFileVersion(fileWriter);
2385+
if(versionResult.invalid())
2386+
{
2387+
return versionResult;
2388+
}
2389+
// Canonicalize to an absolute path at write time so a later cwd change
2390+
// doesn't invalidate the recovery redirect.
2391+
const fs::path absUserPath = fs::absolute(*userDataFilePath);
2392+
return fileWriter.writeStringAttribute(k_UserDataFilePathTag.str(), absUserPath.string());
2393+
}
23732394

2374-
// The RAII guard sets the override active on construction. While active,
2375-
// HDF5::DataStructureWriter will check each DataArray against the override
2376-
// callback before writing. The guard deactivates the override on destruction,
2377-
// ensuring it does not leak into subsequent normal WriteFile calls.
2395+
// Standard recovery variant: drive WriteFile with the WriteArrayOverrideGuard
2396+
// active so OOC stores write placeholder + recovery metadata instead of data.
2397+
auto& ioCollection = DataStoreUtilities::GetIOCollection();
23782398
WriteArrayOverrideGuard guard(ioCollection);
2379-
2380-
// Delegate to the standard WriteFile path. The only difference is that the
2381-
// override is now active, so OOC arrays get placeholder writes. XDMF output
2382-
// is disabled (false) for recovery files since they are transient.
23832399
return WriteFile(path, dataStructure, pipeline, false);
23842400
}
23852401

2402+
Result<std::optional<fs::path>> DREAM3D::ReadUserDataFilePathAttribute(const fs::path& recoveryFilePath)
2403+
{
2404+
auto fileReader = nx::core::HDF5::FileIO::ReadFile(recoveryFilePath);
2405+
if(!fileReader.isValid())
2406+
{
2407+
return MakeErrorResult<std::optional<fs::path>>(-9047, fmt::format("Failed to open recovery file at path {}", recoveryFilePath.string()));
2408+
}
2409+
2410+
// Absent attribute is not an error — it simply means the file is a
2411+
// standard (data-carrying) recovery file. Distinguish by peeking before
2412+
// reading: readStringAttribute returns an error result if the attribute
2413+
// doesn't exist, so probe presence explicitly.
2414+
if(!fileReader.hasAttribute(k_UserDataFilePathTag.str()))
2415+
{
2416+
return {std::optional<fs::path>{}};
2417+
}
2418+
2419+
auto attrResult = fileReader.readStringAttribute(k_UserDataFilePathTag.str());
2420+
if(attrResult.invalid())
2421+
{
2422+
return ConvertInvalidResult<std::optional<fs::path>>(std::move(attrResult));
2423+
}
2424+
return {std::optional<fs::path>{fs::path(attrResult.value())}};
2425+
}
2426+
23862427
Result<> DREAM3D::AppendFile(const fs::path& path, const DataStructure& dataStructure, const DataPath& dataPath)
23872428
{
23882429
auto file = nx::core::HDF5::FileIO::AppendFile(path);

src/simplnx/Utilities/Parsing/DREAM3D/Dream3dIO.hpp

Lines changed: 41 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include <filesystem>
88
#include <memory>
9+
#include <optional>
910
#include <string>
1011
#include <utility>
1112

@@ -131,32 +132,51 @@ SIMPLNX_EXPORT Result<> WriteFile(nx::core::HDF5::FileIO& fileWriter, const Pipe
131132
SIMPLNX_EXPORT Result<> WriteFile(const std::filesystem::path& path, const DataStructure& dataStructure, const Pipeline& pipeline = {}, bool writeXdmf = false);
132133

133134
/**
134-
* @brief Writes a recovery .dream3d file with optimized handling of OOC arrays.
135+
* @brief Writes a recovery snapshot of @p dataStructure to @p path.
135136
*
136-
* A recovery file captures the current pipeline state so that execution can
137-
* be resumed after a crash or interruption. For OOC arrays, materializing
138-
* the full data into HDF5 would be extremely expensive (potentially hundreds
139-
* of GB). Instead, this function activates the DataIOCollection's
140-
* write-array-override hook via a WriteArrayOverrideGuard RAII object.
137+
* When @p userDataFilePath is unset (default), the full recovery file is
138+
* written: in-core arrays get their data payload, OOC-backed arrays get a
139+
* placeholder plus their getRecoveryMetadata() key/value attributes so the
140+
* recovery loader can reconstruct the backing store on load.
141141
*
142-
* When the override is active, the HDF5 DataStructureWriter checks each
143-
* DataArray against the registered override callback (set by the SimplnxOoc
144-
* plugin). For OOC-backed arrays, the callback writes a lightweight
145-
* placeholder (just the store metadata: file path, chunk layout, shape)
146-
* instead of the full data. For in-core arrays, the callback returns
147-
* std::nullopt, causing the writer to fall through to the standard HDF5
148-
* write path.
142+
* When @p userDataFilePath is set, @p dataStructure and @p pipeline are
143+
* ignored and a minimal HDF5 file is written containing only the file-
144+
* version attribute and a root-level string attribute named
145+
* "UserDataFilePath" whose value is the absolute path of the user's
146+
* authoritative `.dream3d` output. The recovery scanner uses that attribute
147+
* at relaunch time to redirect the load at the user's file.
149148
*
150-
* If no write-array-override callback is registered (i.e., the OOC plugin
151-
* is not loaded), the guard is a no-op and the function behaves identically
152-
* to WriteFile.
149+
* @param path Target path of the recovery file ("{uuid}.dream3d").
150+
* @param dataStructure Pipeline's final DataStructure (ignored when
151+
* @p userDataFilePath is set).
152+
* @param pipeline Pipeline JSON to embed (ignored when
153+
* @p userDataFilePath is set).
154+
* @param userDataFilePath Optional absolute path to the user's own
155+
* `.dream3d` file. When set, switches the writer
156+
* to minimal redirect mode.
157+
* @return Result<> ok on success; error payload on HDF5-level failure
158+
* (file open or version-tag write).
159+
*/
160+
SIMPLNX_EXPORT Result<> WriteRecoveryFile(const std::filesystem::path& path, const DataStructure& dataStructure, const Pipeline& pipeline = {},
161+
std::optional<std::filesystem::path> userDataFilePath = std::nullopt);
162+
163+
/**
164+
* @brief Reads the "UserDataFilePath" root-level HDF5 string attribute
165+
* from a recovery file.
153166
*
154-
* @param path Output file path for the recovery .dream3d file
155-
* @param dataStructure The DataStructure to write
156-
* @param pipeline The Pipeline to serialize alongside the data (default empty)
157-
* @return Result<> with any errors from the write operation
167+
* The recovery scanner calls this on every `{uuid}.dream3d` it finds at
168+
* startup; when a value comes back it means the pipeline ended with a
169+
* WriteDREAM3DFilter and the returned path is the user's authoritative
170+
* output. Absent attribute is NOT an error — it just means this recovery
171+
* file carries its own data (the standard case).
172+
*
173+
* @param recoveryFilePath Path to the `{uuid}.dream3d` to inspect.
174+
* @return Result<std::optional<std::filesystem::path>>
175+
* - ok + nullopt: attribute absent, this is a standard recovery file
176+
* - ok + path: attribute set, caller should redirect to that path
177+
* - error: HDF5 open/read failure (corrupt file, missing, etc.)
158178
*/
159-
SIMPLNX_EXPORT Result<> WriteRecoveryFile(const std::filesystem::path& path, const DataStructure& dataStructure, const Pipeline& pipeline = {});
179+
SIMPLNX_EXPORT Result<std::optional<std::filesystem::path>> ReadUserDataFilePathAttribute(const std::filesystem::path& recoveryFilePath);
160180

161181
/**
162182
* @brief Appends the object at the path in the data structure to the dream3d file

test/Dream3dLoadingApiTest.cpp

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,26 @@ const DataPath k_ArrayB2Path({k_GroupBName, k_AttrMatBName, k_ArrayB2Name});
6262
// Helpers
6363
// ---------------------------------------------------------------------------
6464

65+
/**
66+
* @brief RAII guard that removes a temporary file on destruction.
67+
*
68+
* Ensures test-output files are cleaned up even when a REQUIRE assertion
69+
* throws and skips the remaining test body.
70+
*/
71+
struct ScopedTempFile
72+
{
73+
explicit ScopedTempFile(fs::path p)
74+
: path(std::move(p))
75+
{
76+
}
77+
~ScopedTempFile()
78+
{
79+
std::error_code ec;
80+
fs::remove(path, ec);
81+
}
82+
fs::path path;
83+
};
84+
6585
fs::path GetTestOutputDir()
6686
{
6787
return fs::path(unit_test::k_BinaryTestOutputDir.view());
@@ -360,6 +380,42 @@ TEST_CASE("Dream3dLoadingApi: LoadDataStructureArrays prune verification")
360380
CHECK(ds.getDataAs<Float32Array>(k_ArrayB2Path) == nullptr);
361381
}
362382

383+
TEST_CASE("Dream3dLoadingApi: Recovery file with user data path redirect")
384+
{
385+
// RAII guards ensure cleanup even when a REQUIRE throws on failure.
386+
ScopedTempFile filePathGuard{GetTestOutputDir() / "Dream3dLoadingApiTest_RecoveryRedirect.dream3d"};
387+
const fs::path& filePath = filePathGuard.path;
388+
const fs::path userDataPath = GetTestOutputDir() / "my_user_output.dream3d";
389+
390+
// Write the minimal redirect variant — dataStructure/pipeline ignored.
391+
DataStructure emptyDs;
392+
Result<> writeResult = DREAM3D::WriteRecoveryFile(filePath, emptyDs, {}, userDataPath);
393+
REQUIRE(writeResult.valid());
394+
395+
// File exists and is small (minimal variant is kilobytes, full variant is
396+
// MB-GB with real data). The sanity check is "< 64 KB" to catch a
397+
// regression where WriteRecoveryFile silently falls through to the full
398+
// path despite userDataFilePath being set.
399+
REQUIRE(fs::exists(filePath));
400+
REQUIRE(fs::file_size(filePath) < 64 * 1024);
401+
402+
// Read back: attribute should contain the ABSOLUTE form of userDataPath.
403+
auto readResult = DREAM3D::ReadUserDataFilePathAttribute(filePath);
404+
REQUIRE(readResult.valid());
405+
REQUIRE(readResult.value().has_value());
406+
REQUIRE(readResult.value().value() == fs::absolute(userDataPath));
407+
408+
// A standard recovery file (no redirect) should return nullopt.
409+
ScopedTempFile standardFilePathGuard{GetTestOutputDir() / "Dream3dLoadingApiTest_RecoveryStandard.dream3d"};
410+
const fs::path& standardFilePath = standardFilePathGuard.path;
411+
DataStructure simpleDs = CreateSimpleTestDataStructure();
412+
REQUIRE(DREAM3D::WriteRecoveryFile(standardFilePath, simpleDs).valid());
413+
414+
auto readStandard = DREAM3D::ReadUserDataFilePathAttribute(standardFilePath);
415+
REQUIRE(readStandard.valid());
416+
REQUIRE_FALSE(readStandard.value().has_value());
417+
}
418+
363419
TEST_CASE("Dream3dLoadingApi: Recovery file with all in-core data")
364420
{
365421
DataStructure srcDs = CreateSimpleTestDataStructure();

0 commit comments

Comments
 (0)