Skip to content

Commit f62a329

Browse files
franzpoeschelax3l
andauthored
ADIOS2 Append Mode (#1007)
* error::Internal * Frontend implementation of appending Also use switch statement in many frontend places for the Access enum. With 4 variants, if statements become increasingly unreadable and it's good to have compiler warnings if a case is forgotten. Try to avoid default branches. Also refactor `autoDetectPadding()` into a separate function since it is now needed in two places. * Backend implementation 1) Make backends aware of Append mode 2) In ADIOS1, fix use of namespaces, only use #include statements outside of namespaces * Documentation and testing * Extend tests: also use variable-based encoding * Allow overwriting old datasets in HDF5 * Test that RW-mode in group-based mode still works * Document extended meaning of createFile task * Apply suggestions from code review Co-authored-by: Axel Huebl <[email protected]> * Remove special test paths for ADIOS2 * C++17 and other CI fixes * Add missing throw * Apply suggestions from code review Co-authored-by: Axel Huebl <[email protected]> * Use H5Lexists to be more efficient in lookup Co-authored-by: Axel Huebl <[email protected]>
1 parent 33a189c commit f62a329

19 files changed

+747
-133
lines changed

docs/source/usage/workflow.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,39 @@
11
.. _workflow:
22

3+
Access modes
4+
============
5+
6+
The openPMD-api distinguishes between a number of different access modes:
7+
8+
* **Create mode**: Used for creating a new Series from scratch.
9+
Any file possibly existing in the specified location will be overwritten.
10+
* **Read-only mode**: Used for reading from an existing Series.
11+
No modifications will be made.
12+
* **Read/Write mode**: Creates a new Series if not existing, otherwise opens an existing Series for reading and writing.
13+
New datasets and iterations will be inserted as needed.
14+
Not fully supported by all backends:
15+
16+
* ADIOS1: Automatically coerced to *Create* mode if the file does not exist yet and to *Read-only* mode if it exists.
17+
* ADIOS2: Automatically coerced to *Create* mode if the file does not exist yet and to *Read-only* mode if it exists.
18+
Since this happens on a per-file level, this mode allows to read from existing iterations and write to new iterations at the same time in file-based iteration encoding.
19+
* **Append mode**: Restricted mode for appending new iterations to an existing Series that is supported by all backends at least in file-based iteration encoding, and by all but ADIOS1 in other encodings.
20+
The API is equivalent to that of the *Create* mode, meaning that no reading is supported whatsoever.
21+
If the Series does not exist yet, this behaves equivalently to the *Create* mode.
22+
Existing iterations will not be deleted, newly-written iterations will be inserted.
23+
24+
**Warning:** When writing an iteration that already exists, the behavior is implementation-defined and depends on the chosen backend and iteration encoding:
25+
26+
* The new iteration might fully replace the old one.
27+
* The new iteration might be merged into the old one.
28+
* (To be removed in a future update) The old and new iteration might coexist in the resulting dataset.
29+
30+
We suggest to fully define iterations when using Append mode (i.e. as if using Create mode) to avoid implementation-specific behavior.
31+
Appending to an openPMD Series is only supported on a per-iteration level.
32+
33+
**Warning:** There is no reading involved in using Append mode.
34+
It is a user's responsibility to ensure that the appended dataset and the appended-to dataset are compatible with each other.
35+
The results of using incompatible backend configurations are undefined.
36+
337
Workflow
438
========
539

include/openPMD/Error.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,5 +69,16 @@ namespace error
6969

7070
BackendConfigSchema(std::vector<std::string>, std::string what);
7171
};
72+
73+
/**
74+
* @brief Internal errors that should not happen. Please report.
75+
*
76+
* Example: A nullpointer is observed somewhere.
77+
*/
78+
class Internal : public Error
79+
{
80+
public:
81+
Internal(std::string const &what);
82+
};
7283
} // namespace error
7384
} // namespace openPMD

include/openPMD/IO/AbstractIOHandler.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,23 @@ namespace internal
121121
*/
122122
class AbstractIOHandler
123123
{
124+
friend class Series;
125+
126+
private:
127+
void setIterationEncoding(IterationEncoding encoding)
128+
{
129+
/*
130+
* In file-based iteration encoding, the APPEND mode is handled entirely
131+
* by the frontend, the backend should just treat it as CREATE mode
132+
*/
133+
if (encoding == IterationEncoding::fileBased &&
134+
m_backendAccess == Access::APPEND)
135+
{
136+
// do we really want to have those as const members..?
137+
*const_cast<Access *>(&m_backendAccess) = Access::CREATE;
138+
}
139+
}
140+
124141
public:
125142
#if openPMD_HAVE_MPI
126143
AbstractIOHandler(std::string path, Access at, MPI_Comm)
@@ -153,6 +170,7 @@ class AbstractIOHandler
153170
virtual std::string backendName() const = 0;
154171

155172
std::string const directory;
173+
// why do these need to be separate?
156174
Access const m_backendAccess;
157175
Access const m_frontendAccess;
158176
std::queue<IOTask> m_work;

include/openPMD/IO/AbstractIOHandlerImpl.hpp

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -266,14 +266,17 @@ class AbstractIOHandlerImpl
266266
* file.
267267
*
268268
* The operation should fail if m_handler->m_frontendAccess is
269-
* Access::READ_ONLY. The new file should be located in
270-
* m_handler->directory. The new file should have the filename
271-
* parameters.name. The filename should include the correct corresponding
272-
* filename extension. Any existing file should be overwritten if
273-
* m_handler->m_frontendAccess is Access::CREATE. The Writables file
274-
* position should correspond to the root group "/" of the hierarchy. The
275-
* Writable should be marked written when the operation completes
276-
* successfully.
269+
* Access::READ_ONLY. If m_handler->m_frontendAccess is Access::APPEND, a
270+
* possibly existing file should not be overwritten. Instead, written
271+
* updates should then either occur in-place or in form of new IO steps.
272+
* Support for reading is not necessary in Append mode.
273+
* The new file should be located in m_handler->directory.
274+
* The new file should have the filename parameters.name.
275+
* The filename should include the correct corresponding filename extension.
276+
* Any existing file should be overwritten if m_handler->m_frontendAccess is
277+
* Access::CREATE. The Writables file position should correspond to the root
278+
* group "/" of the hierarchy. The Writable should be marked written when
279+
* the operation completes successfully.
277280
*/
278281
virtual void
279282
createFile(Writable *, Parameter<Operation::CREATE_FILE> const &) = 0;

include/openPMD/IO/Access.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ enum class Access
2828
{
2929
READ_ONLY, //!< open series as read-only, fails if series is not found
3030
READ_WRITE, //!< open existing series as writable
31-
CREATE //!< create new series and truncate existing (files)
31+
CREATE, //!< create new series and truncate existing (files)
32+
APPEND //!< write new iterations to an existing series without reading
3233
}; // Access
3334

3435
// deprecated name (used prior to 0.12.0)

src/Error.cpp

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,12 @@ namespace error
4545
concatVector(errorLocation_in) + "': " + std::move(what))
4646
, errorLocation(std::move(errorLocation_in))
4747
{}
48+
49+
Internal::Internal(std::string const &what)
50+
: Error(
51+
"Internal error: " + what +
52+
"\nThis is a bug. Please report at ' "
53+
"https://github.com/openPMD/openPMD-api/issues'.")
54+
{}
4855
} // namespace error
4956
} // namespace openPMD

src/IO/ADIOS/ADIOS2IOHandler.cpp

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -315,9 +315,6 @@ void ADIOS2IOHandlerImpl::createFile(
315315
m_iterationEncoding = parameters.encoding;
316316
associateWithFile(writable, shared_name);
317317
this->m_dirty.emplace(shared_name);
318-
getFileData(shared_name, IfFileNotOpen::OpenImplicitly).m_mode =
319-
adios2::Mode::Write; // WORKAROUND
320-
// ADIOS2 does not yet implement ReadWrite Mode
321318

322319
writable->written = true;
323320
writable->abstractFilePosition = std::make_shared<ADIOS2FilePosition>();
@@ -1074,21 +1071,16 @@ adios2::Mode ADIOS2IOHandlerImpl::adios2AccessMode(std::string const &fullPath)
10741071
if (auxiliary::directory_exists(fullPath) ||
10751072
auxiliary::file_exists(fullPath))
10761073
{
1077-
std::cerr << "ADIOS2 does currently not yet implement ReadWrite "
1078-
"(Append) mode. "
1079-
<< "Replacing with Read mode." << std::endl;
10801074
return adios2::Mode::Read;
10811075
}
10821076
else
10831077
{
1084-
std::cerr << "ADIOS2 does currently not yet implement ReadWrite "
1085-
"(Append) mode. "
1086-
<< "Replacing with Write mode." << std::endl;
10871078
return adios2::Mode::Write;
10881079
}
1089-
default:
1090-
return adios2::Mode::Undefined;
1080+
case Access::APPEND:
1081+
return adios2::Mode::Append;
10911082
}
1083+
throw std::runtime_error("Unreachable!");
10921084
}
10931085

10941086
json::TracingJSON ADIOS2IOHandlerImpl::nullvalue = {
@@ -2235,6 +2227,7 @@ namespace detail
22352227
delayOpeningTheFirstStep = true;
22362228
break;
22372229
case adios2::Mode::Write:
2230+
case adios2::Mode::Append:
22382231
/*
22392232
* File engines, write mode:
22402233
* Default for old layout is no steps.
@@ -2442,6 +2435,7 @@ namespace detail
24422435
{
24432436
switch (m_mode)
24442437
{
2438+
case adios2::Mode::Append:
24452439
case adios2::Mode::Write: {
24462440
// usesSteps attribute only written upon ::advance()
24472441
// this makes sure that the attribute is only put in case
@@ -2686,17 +2680,14 @@ namespace detail
26862680
switch (ba.m_mode)
26872681
{
26882682
case adios2::Mode::Write:
2683+
case adios2::Mode::Append:
26892684
eng.PerformPuts();
26902685
break;
26912686
case adios2::Mode::Read:
26922687
eng.PerformGets();
26932688
break;
2694-
case adios2::Mode::Append:
2695-
// TODO order?
2696-
eng.PerformGets();
2697-
eng.PerformPuts();
2698-
break;
26992689
default:
2690+
throw error::Internal("[ADIOS2] Unexpected access mode.");
27002691
break;
27012692
}
27022693
},

src/IO/ADIOS/CommonADIOS1IOHandler.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
*/
2121

2222
#include "openPMD/IO/ADIOS/CommonADIOS1IOHandler.hpp"
23+
#include "openPMD/Error.hpp"
2324

2425
#if openPMD_HAVE_ADIOS1
2526

@@ -406,6 +407,15 @@ void CommonADIOS1IOHandlerImpl<ChildClass>::createFile(
406407
if (!auxiliary::ends_with(name, ".bp"))
407408
name += ".bp";
408409

410+
if (m_handler->m_backendAccess == Access::APPEND &&
411+
auxiliary::file_exists(name))
412+
{
413+
throw error::OperationUnsupportedInBackend(
414+
"ADIOS1",
415+
"Appending to existing file on disk (use Access::CREATE to "
416+
"overwrite)");
417+
}
418+
409419
writable->written = true;
410420
writable->abstractFilePosition =
411421
std::make_shared<ADIOS1FilePosition>("/");

src/IO/HDF5/HDF5IOHandler.cpp

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,41 @@ void HDF5IOHandlerImpl::createFile(
237237
std::string name = m_handler->directory + parameters.name;
238238
if (!auxiliary::ends_with(name, ".h5"))
239239
name += ".h5";
240-
unsigned flags;
241-
if (m_handler->m_backendAccess == Access::CREATE)
240+
unsigned flags{};
241+
switch (m_handler->m_backendAccess)
242+
{
243+
case Access::CREATE:
242244
flags = H5F_ACC_TRUNC;
243-
else
245+
break;
246+
case Access::APPEND:
247+
if (auxiliary::file_exists(name))
248+
{
249+
flags = H5F_ACC_RDWR;
250+
}
251+
else
252+
{
253+
flags = H5F_ACC_TRUNC;
254+
}
255+
break;
256+
case Access::READ_WRITE:
244257
flags = H5F_ACC_EXCL;
245-
hid_t id =
246-
H5Fcreate(name.c_str(), flags, H5P_DEFAULT, m_fileAccessProperty);
258+
break;
259+
case Access::READ_ONLY:
260+
// condition has been checked above
261+
throw std::runtime_error(
262+
"[HDF5] Control flow error in createFile backend access mode.");
263+
}
264+
265+
hid_t id{};
266+
if (flags == H5F_ACC_RDWR)
267+
{
268+
id = H5Fopen(name.c_str(), flags, m_fileAccessProperty);
269+
}
270+
else
271+
{
272+
id = H5Fcreate(
273+
name.c_str(), flags, H5P_DEFAULT, m_fileAccessProperty);
274+
}
247275
VERIFY(id >= 0, "[HDF5] Internal error: Failed to create HDF5 file");
248276

249277
writable->written = true;
@@ -409,6 +437,36 @@ void HDF5IOHandlerImpl::createDataset(
409437
"[HDF5] Internal error: Failed to open HDF5 group during dataset "
410438
"creation");
411439

440+
if (m_handler->m_backendAccess == Access::APPEND)
441+
{
442+
// The dataset might already exist in the file from a previous run
443+
// We delete it, otherwise we could not create it again with
444+
// possibly different parameters.
445+
if (htri_t link_id = H5Lexists(node_id, name.c_str(), H5P_DEFAULT);
446+
link_id > 0)
447+
{
448+
// This only unlinks, but does not delete the dataset
449+
// Deleting the actual dataset physically is now up to HDF5:
450+
// > when removing an object with H5Ldelete, the HDF5 library
451+
// > should be able to detect and recycle the file space when no
452+
// > other reference to the deleted object exists
453+
// https://github.com/openPMD/openPMD-api/pull/1007#discussion_r867223316
454+
herr_t status = H5Ldelete(node_id, name.c_str(), H5P_DEFAULT);
455+
VERIFY(
456+
status == 0,
457+
"[HDF5] Internal error: Failed to delete old dataset '" +
458+
name + "' from group for overwriting.");
459+
}
460+
else if (link_id < 0)
461+
{
462+
throw std::runtime_error(
463+
"[HDF5] Internal error: Failed to check for link existence "
464+
"of '" +
465+
name + "' inside group for overwriting.");
466+
}
467+
// else: link_id == 0: Link does not exist, nothing to do
468+
}
469+
412470
Datatype d = parameters.dtype;
413471
if (d == Datatype::UNDEFINED)
414472
{
@@ -702,7 +760,14 @@ void HDF5IOHandlerImpl::openFile(
702760
Access at = m_handler->m_backendAccess;
703761
if (at == Access::READ_ONLY)
704762
flags = H5F_ACC_RDONLY;
705-
else if (at == Access::READ_WRITE || at == Access::CREATE)
763+
/*
764+
* Within the HDF5 backend, APPEND and READ_WRITE mode are
765+
* equivalent, but the openPMD frontend exposes no reading
766+
* functionality in APPEND mode.
767+
*/
768+
else if (
769+
at == Access::READ_WRITE || at == Access::CREATE ||
770+
at == Access::APPEND)
706771
flags = H5F_ACC_RDWR;
707772
else
708773
throw std::runtime_error("[HDF5] Unknown file Access");

src/IO/JSON/JSONIOHandlerImpl.cpp

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ void JSONIOHandlerImpl::createFile(
117117
file.invalidate();
118118
}
119119

120-
std::string const dir(m_handler->directory);
120+
std::string const &dir(m_handler->directory);
121121
if (!auxiliary::directory_exists(dir))
122122
{
123123
auto success = auxiliary::create_directories(dir);
@@ -126,8 +126,14 @@ void JSONIOHandlerImpl::createFile(
126126

127127
associateWithFile(writable, shared_name);
128128
this->m_dirty.emplace(shared_name);
129-
// make sure to overwrite!
130-
this->m_jsonVals[shared_name] = std::make_shared<nlohmann::json>();
129+
130+
if (m_handler->m_backendAccess != Access::APPEND)
131+
{
132+
// make sure to overwrite!
133+
this->m_jsonVals[shared_name] = std::make_shared<nlohmann::json>();
134+
}
135+
// else: the JSON value is not available in m_jsonVals and will be
136+
// read from the file later on before overwriting
131137

132138
writable->written = true;
133139
writable->abstractFilePosition = std::make_shared<JSONFilePosition>();
@@ -910,6 +916,14 @@ JSONIOHandlerImpl::getFilehandle(File fileName, Access access)
910916
{
911917
case Access::CREATE:
912918
case Access::READ_WRITE:
919+
case Access::APPEND:
920+
/*
921+
* Always truncate when writing, we alway write entire JSON
922+
* datasets, never partial ones.
923+
* Within the JSON backend, APPEND and READ_WRITE mode are
924+
* equivalent, but the openPMD frontend exposes no reading
925+
* functionality in APPEND mode.
926+
*/
913927
fs->open(path, std::ios_base::out | std::ios_base::trunc);
914928
break;
915929
case Access::READ_ONLY:

0 commit comments

Comments
 (0)