From c376381a4f23614c3f607bb1883567576f0e5a66 Mon Sep 17 00:00:00 2001 From: Matthew Whitlock Date: Thu, 3 Oct 2024 16:08:10 -0400 Subject: [PATCH] Initial doxygen setup --- CMakeLists.txt | 69 ++++- doc/DoxygenLayout.xml | 269 +++++++++++++++++ doc/DoxygenStyle.css | 41 +++ doc/Introduction.md | 51 ++++ doc/examples/DataRecovery.md | 114 +++++++ doc/examples/IMR.md | 6 + doc/examples/ProcessRecovery.md | 19 ++ doc/fake_init.h | 4 + doc/img/fenix_process_flow.png | Bin 0 -> 67332 bytes include/fenix.h | 517 ++++++++++++++++++++++++++++++-- 10 files changed, 1044 insertions(+), 46 deletions(-) create mode 100644 doc/DoxygenLayout.xml create mode 100644 doc/DoxygenStyle.css create mode 100644 doc/Introduction.md create mode 100644 doc/examples/DataRecovery.md create mode 100644 doc/examples/IMR.md create mode 100644 doc/examples/ProcessRecovery.md create mode 100644 doc/fake_init.h create mode 100644 doc/img/fenix_process_flow.png diff --git a/CMakeLists.txt b/CMakeLists.txt index 170b576..1c9c93c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,7 +17,8 @@ set(FENIX_VERSION_MINOR 0) option(BUILD_EXAMPLES "Builds example programs from the examples directory" OFF) option(BUILD_TESTING "Builds tests and test modes of files" ON) - +option(BUILD_DOCS "Builds documentation if is doxygen found" ON) +option(DOCS_ONLY "Only build documentation" OFF) #Solves an issue with some system environments putting their MPI headers before #the headers CMake includes. Forces non-system MPI headers when incorrect headers @@ -25,26 +26,68 @@ option(BUILD_TESTING "Builds tests and test modes of files" ON) option(FENIX_SYSTEM_INC_FIX "Attempts to force overriding any system MPI headers" ON) option(FENIX_PROPAGATE_INC_FIX "Attempt overriding system MPI headers in linking projects" ON) -find_package(MPI REQUIRED) -if(${FENIX_SYSTEM_INC_FIX}) - include(cmake/systemMPIOverride.cmake) -endif() +if(NOT DOCS_ONLY) + find_package(MPI REQUIRED) + if(${FENIX_SYSTEM_INC_FIX}) + include(cmake/systemMPIOverride.cmake) + endif() -add_subdirectory(src) + add_subdirectory(src) + include(CTest) + list(APPEND MPIEXEC_PREFLAGS "--with-ft;mpi") -include(CTest) -list(APPEND MPIEXEC_PREFLAGS "--with-ft;mpi") + if(BUILD_EXAMPLES) + add_subdirectory(examples) + endif() -if(BUILD_EXAMPLES) - add_subdirectory(examples) -endif() + if(BUILD_TESTING) + add_subdirectory(test) + endif() +endif() -if(BUILD_TESTING) - add_subdirectory(test) +if(BUILD_DOCS) + find_package(Doxygen) + if(DOXYGEN_FOUND) + list(APPEND DOXYGEN_EXAMPLE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/examples) + list(APPEND DOXYGEN_EXAMPLE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/doc/examples) + list(APPEND DOXYGEN_IMAGE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/doc/img) + set(DOXYGEN_USE_MDFILE_AS_MAINPAGE doc/Introduction.md) + set(DOXYGEN_TOC_INCLUDE_HEADINGS 0) + set(DOXYGEN_DISABLE_INDEX YES) + set(DOXYGEN_GENERATE_TREEVIEW YES) + set(DOXYGEN_FULL_SIDEBAR NO) + set(DOXYGEN_HTML_EXTRA_STYLESHEET ${CMAKE_CURRENT_SOURCE_DIR}/doc/DoxygenStyle.css) + set(DOXYGEN_LAYOUT_FILE ${CMAKE_CURRENT_SOURCE_DIR}/doc/DoxygenLayout.xml) + set(DOXYGEN_OUTPUT_DIRECTORY doc) + + set(DOXYGEN_GENERATE_MAN YES) + set(DOXYGEN_GENERATE_HTML YES) + set(DOXYGEN_GENERATE_LATEX YES) + + set(DOXYGEN_QUIET YES) + set(DOXYGEN_WARN_IF_UNDOCUMENTED NO) + set(DOXYGEN_WARN_IF_DOC_ERROR YES) + set(DOXYGEN_WARN_NO_PARAMDOC YES) + set(DOXYGEN_SHOW_INCLUDE_FILES NO) + set(DOXYGEN_WARN_IF_UNDOC_ENUM_VAL NO) + list(APPEND DOXYGEN_ALIASES "returnstatus=@return FENIX_SUCCESS if successful, any [return code](@ref ReturnCodes) otherwise.") + list(APPEND DOXYGEN_ALIASES "unimplemented=@qualifier UNIMPLEMENTED @brief @htmlonly @endhtmlonly UNIMPLEMENTED @htmlonly @endhtmlonly") + + doxygen_add_docs(doc + doc/Introduction.md doc/fake_init.h include src + ALL + COMMENT "Generate Fenix documentation") + message(STATUS "Run `make doc` to build documentation") + + install(DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/doc DESTINATION ${CMAKE_INSTALL_PREFIX}) + + else() + message(STATUS "Doxygen not found, `make docs` disabled") + endif() endif() diff --git a/doc/DoxygenLayout.xml b/doc/DoxygenLayout.xml new file mode 100644 index 0000000..535b044 --- /dev/null +++ b/doc/DoxygenLayout.xml @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/DoxygenStyle.css b/doc/DoxygenStyle.css new file mode 100644 index 0000000..770b1c6 --- /dev/null +++ b/doc/DoxygenStyle.css @@ -0,0 +1,41 @@ +/*Move qualifiers (e.g. collective, unimplemented) to being above function name instead of bottom right*/ +/* It's too easy to miss as-is, especially the unimplemented tag.*/ +table.mlabels { + direction: rtl; + writing-mode: vertical-rl; +} +/*Undo the weird writing-mode changes at each mlabels table member*/ +table.mlabels td.mlabels-right { + writing-mode: horizontal-tb; + text-align: left; + width: auto; +} +table.mlabels td.mlabels-left { + writing-mode: horizontal-tb; + text-align: left; + width: auto; +} +/*Undo the table direction change in the subtable of function parameters*/ +table.mlabels table.memname { + float: left; + direction: ltr; +} + +/*Make the qualifier labels slightly larger, and bold.*/ +table.mlabels td.mlabels-right span.mlabel { + font-weight: bold; + font-size: 12px; +} + + +/* + * Hide the "UNIMPLEMENTED" tag within the function's detailed description + * It's visible already. +*/ +div.memdoc span.mlabel { + display: none; +} + +table.params { + word-wrap: break-all; +} diff --git a/doc/Introduction.md b/doc/Introduction.md new file mode 100644 index 0000000..1e8d68b --- /dev/null +++ b/doc/Introduction.md @@ -0,0 +1,51 @@ +Fenix is a software library compatible with the Message Passing +Interface (MPI) to support fault recovery without application +shutdown. Fenix has two components: process recovery and data +recovery. Process recovery is used to repair communicators whose +ranks suffered failure detected by the MPI runtime. Data recovery +is an optional feature that can be used to implement a +high-performance in-memory checkpoint/restart mechanism. + +Below is a brief overview of these two components, but see the +[Process Recovery](@ref ProcessRecovery) and [Data Recovery](@ref DataRecovery) +topics for more details. + +## Process Recovery + +The core feature of process recovery is creation of a resilient +communicator that will automatically repair itself. This recovery +is achieved by setting aside some number of ranks as *spare ranks*. +When a failure is detected, the spare ranks are used to replace +the failed ranks. + +The exact process of recovery is subject to some nuances of the OpenMPI +ULFM specification, which Fenix is implemented on top of. For example, +messages may have locally succeeded while failing on other participating +ranks. + +![An example process flow diagram for recovery using Fenix](fenix_process_flow.png){html: width=300px} + +The default recovery pattern is to perform a `longjmp` to the location of +#Fenix_Init following communicator repairs. This emulates the typical offline +checkpoint/restart pattern, but without the need to restart the application. +However, `longjmp` has some nebulous behavior in many applications. Fenix also +supports a non-jumping recovery pattern. This is more predictable across compilers +and optimizations, but requires checking the return value of every MPI call to +detect failed operations (though communicator repair is still automatic). A +good practice for C++ applications is to use the non-jumping pattern, but add +a Fenix error-handler callback to throw an exception on failure. + +## Data Recovery + +Fenix provides its own redundant data storage API to facilitate +data recovery along with process recovery, but the user can choose +other data recovery options to meet a variety of application needs. +For example, data could be recovered by approximately interpolating +values from unaffected, topologically neighboring ranks instead of +by reading stored redundant data. In addition, the user may decide +to use external libraries such as +[VeloC](https://veloc.readthedocs.io/en/latest/). + +> Any Fenix function without a return type, e.g. #Fenix_Init, may be +> implemented via macros, in which case it cannot be used to resolve +> function pointers. diff --git a/doc/examples/DataRecovery.md b/doc/examples/DataRecovery.md new file mode 100644 index 0000000..e2f8ddf --- /dev/null +++ b/doc/examples/DataRecovery.md @@ -0,0 +1,114 @@ +Fenix provides options for redundant storage of application data +to facilitate application data recovery in a transparent manner. +Fenix contains functions to control consistency of collections of +such data, as well as their level of persistence. Functions with +the prefix \c Fenix\_Data\_ perform store, versioning, restore, +and other relevant operations and form the Fenix data recovery API. +The user can select a specific set of application data, identified +by its location in memory, label it using [Fenix_Data_member_create](@ref Fenix_Data_member_create), +and copy it into Fenix's redundant storage space through +[Fenix_Data_member(i)store(v)](@ref Fenix_Data_member_store) at a +point in time. Subsequently, #Fenix_Data_commit finalizes all +preceding Fenix store operations involving this data group and +assigns a unique time stamp to the resulting data *snapshot*, +marking the data as potentially recoverable after a loss of ranks. +Individual pieces of data can then be restored whenever they are +needed with #Fenix_Data_member_restore, for example after a failure +occurs. We note that Fenix's data storage and recovery facility +aims primarily to support in-memory recovery. + +Populating redundant data storage using Fenix may involve the +dispersion of data created by one rank to other ranks within the +system, making the store operation semantically a collective +operation. However, Fenix does not require store operations to be +globally synchronizing. For example, execution of + #Fenix_Data_member_store for a particular collection of data +could potentially be finished in some ranks, but not yet in others. +And if certain ranks nominally participating in the storage +operations have no actual data movement responsibility, Fenix is +allowerd to let them exit the operation immediately. Consequently, +Fenix data storage functions should not be used for synchronization +purposes. + +Multiple distinct pieces (members) of data assigned to Fenix-managed +redundant storage, can be associated with a specific instance of +a Fenix *data group* to form a semantic unit. Committing such a +group ensures that the data involved is available for recovery. + +## Data Groups + +----- +A Fenix *data group* provides dual functionality. First, it serves +as a container for a set of data objects (*members*) that are +committed together, and hence provides transaction semantics. +Second, it recognizes that #Fenix_Data_member_store is an operation +carried out collectively by a group of ranks, but not necessarily +by all active ranks in the MPI environment. Hence, it adopts the +convenient MPI vehicle of \c communicators to indicate the subset +of ranks involved. Data groups are composed of members that +describe the actual application data and the redundancy policy +to be used for securely storing the members. + +Data groups can and should be recreated after each failure (i.e. do not +conditionally skip the creation after initialization). + +See #Fenix_Data_group_create +for creating a data group. + +## Data Redundancy Policies + +----- +Fenix internally uses an extensible system for defining data +policies to keep the door open to easily adding new data policies +and configuring them on a per-data-group basis. We currently +support a single, configurable, memory-based policy. + +### In Memory Redundancy Policy (IMR) + +IMR is referenced with the FENIX_DATA_POLICY_IN_MEMORY_RAID definition, +and takes as input an array of integers with the following usage: + +* Mode: (1 or 5) Chooses storage mimicking the given RAID style. +* Separation: Sets the rank separation for groups used to store redundant data. + Users should choose a separation that attempts to ensure the ranks + chosen for grouping are not colocated on nodes/racks to minimize the + chance of multiple ranks in a group +* GroupSize: For Mode 5 only, sets the size of the parity groups, minimum 3. + +The policy is designed to localize recovery as much as possible. Communication +amongst group members is required (as failure during recovery operations +can lead to inconsistent beliefs about which ranks have recovered data), +but groups without recovering ranks may then all recover locally rather +than communicating further. Groups need not wait for ranks outside of +their group to enter or exit recovery. + +* **Mode 1**: Groups ranks into dyadically paired partners of Rank N and + Rank (N+Separation). For odd-size communicators, a single + group of size 3 will also form of the first, middle, and last + ranks. Each rank stores a copy of its own data and a copy of + its partner's. For groups of three, partner data storage is + chained. Should both partners fail (or any two for groups of + three) before recovery operations have completed, data will be + unrecoverable. + + **Memory Usage**: Each rank stores a copy of its own data and of its + partner's data for each timestamp, where checkpoint depth D + stores D+1 checkpoints. Therefore for data size M, + (D+1)*M*2 bytes are used. + + **Computation**: None. + +* **Mode 5**: Groups ranks into parity groups of size GroupSize. + Groups are formed of Rank N, N+Separation, N+2*Separation. + If any two ranks in a group fail before recovery operations + have completed, data will be unrecoverable. + + **Memory Usage**: Each rank stores a copy of its own data and + M/(GroupSize-1) parity bytes per timestamp. Therefore, + (D+1)*M*(GroupSize/(GroupSize-1)) bytes are used. + + **Computation**: O(M) parity bit calculations. + +These options enable users to trade reliability and computation for memory +space, which may be necessary for applications with large memory usage. + diff --git a/doc/examples/IMR.md b/doc/examples/IMR.md new file mode 100644 index 0000000..76f852a --- /dev/null +++ b/doc/examples/IMR.md @@ -0,0 +1,6 @@ +# In Memory Redundancy (IMR) {#md_IMR} + +Fenix supports one data storage policy, IMR, +which stores data through either a RAID-1-like +buddy rank mechanism or a RAID-5-like parity +mechanism. diff --git a/doc/examples/ProcessRecovery.md b/doc/examples/ProcessRecovery.md new file mode 100644 index 0000000..e2d8e1f --- /dev/null +++ b/doc/examples/ProcessRecovery.md @@ -0,0 +1,19 @@ +Functions and types for process recovery. + +* Only communicators derived from the communicator returned by +Fenix_Init are eligible for reconstruction. +After communicators have been repaired, they contain the same +number of ranks as before the failure occurred, unless the user +did not allocate sufficient redundant resources (*spare ranks*) +and instructed Fenix not to create new ranks. In this case +communicators will still be repaired, but will contain fewer +ranks than before the failure occurred. + +* To ease adoption of MPI fault tolerance, Fenix automatically +captures any errors resulting from MPI library calls that are a +result of a damaged communicator (other errors reported by the +MPI runtime are ignored by Fenix and are returned to the +application, for handling by the application writer). In other +words, programmers do not need to replace calls to the MPI library +with calls to Fenix (for example, *Fenix_Send* instead of +*MPI_Send*). diff --git a/doc/fake_init.h b/doc/fake_init.h new file mode 100644 index 0000000..a9afa16 --- /dev/null +++ b/doc/fake_init.h @@ -0,0 +1,4 @@ +//!@weakgroup ProcessRecovery +//!@{ +void Fenix_Init(int* role, MPI_Comm comm, MPI_Comm* newcomm, int** argc, char*** argv, int spare_ranks, int spawn, MPI_Info info, int* error); +//!@} diff --git a/doc/img/fenix_process_flow.png b/doc/img/fenix_process_flow.png new file mode 100644 index 0000000000000000000000000000000000000000..e94029a9dbfacc7ed40755d1b1c387165200b7d4 GIT binary patch literal 67332 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 + * Used to signal that a non-fatal error or special condition was encountered in the execution of + * Fenix_Init, or FENIX_SUCCESS otherwise. It has the same value across all ranks released by + * Fenix_Init. If spawning is explicitly disabled (_spawn equals false) and spare ranks have been + * depleted, Fenix will repair resilience communicators by shrinking them and will report such + * shrinkage in this return parameter through the value FENIX_WARNING_SPARE_RANKS_DEPLETED. + */ + +//!@internal #define Fenix_Init(_role, _comm, _newcomm, _argc, _argv, _spare_ranks, \ _spawn, _info, _error) \ { \ @@ -138,98 +221,466 @@ extern const Fenix_Data_subset FENIX_DATA_SUBSET_EMPTY; __fenix_postinit( _error ); \ } -int Fenix_Initialized(int *); +/** + * @brief Sets flag to true if Fenix_Init has been called, else false. + * @param[out] flag Pointer to the flag to be set. + * @returnstatus + */ +int Fenix_Initialized(int *flag); + +/** + * @brief Register a callback to be invoked after failure process recovery. + * + * This function registers a callback to be invoked after a failure has been recovered by Fenix, + * and right before resuming application execution (e.g. returning from #Fenix_Init by default). + * If this function is called more than once, the different callbacks will be called in the + * reverse order that they were registered (i.e. as a callback stack). + * + * Callback functions are passed the newly-repaired resilient communicator, the error code returned + * by MPI in the communication action which caused a failure recovery, and the user-provided \c void* + * callback data. + * + * Callbacks will only be invoked by survivor ranks, since spare ranks or respawned ranks had no way + * to register them before a failure. + * + * @param[in] recover the callback function to register. + * @param[in] callback_data The user-provided data which will be passed to the callback. + * + * @returnstatus + */ int Fenix_Callback_register(void (*recover)(MPI_Comm, int, void *), void *callback_data); +/** + * @brief Pop the most recently registered callback from the callback stack. + * @returnstatus + */ int Fenix_Callback_pop(); +//!@unimplemented Returns the number of ranks with a given #Fenix_Rank_role int Fenix_get_number_of_ranks_with_role(int, int *); +//!@unimplemented Returns the #Fenix_Rank_role for a given rank int Fenix_get_role(MPI_Comm comm, int rank, int *role); +/** + * @brief Get the list of ranks that failed in the most recent failure. + * @param[out] fail_list Set to a list of failed ranks. + * @return The number of failed ranks. + */ +int Fenix_Process_fail_list(int** fail_list); + +/** + * @brief Check a pre-recovery request without error + * @param[in] request The request to check + * @param[out] status The status of the request + * @return True if the request was cancelled or has unknown completion status, + * false if it completed successfully. + */ +int Fenix_check_cancelled(MPI_Request *request, MPI_Status *status); + + +/** + * @brief Clean up Fenix state. Each active rank must call \c Fenix_Finalize before exiting. + * + * This function cleans up all Fenix state, if any. If an MPI program using the Fenix library terminates + * normally (i.e. not due to a call to \c MPI_Abort, or an unrecoverable error) then each rank must call + * \c Fenix_Finalize before it exits. It must be called before \c MPI_Finalize, and after #Fenix_Init. + * There shall be no function calls after this function, except #Fenix_Initialized. + * + * As noted in the description of #Fenix_Init, all spare ranks that have not been used to + * recover from failures (and therefore are still reserved by Fenix and kept inside #Fenix_Init) will call + * \c MPI_Finalize and exit when all active ranks have called \c Fenix_Finalize. + * + * **Advice**: Sometimes users may want to remove ranks proactively from the execution, for example because + * monitoring data shows that failure of a rank is imminent or that a rank is executing un-manageably slowly. + * This can be accomplished by calling \c exit on the targeted ranks, followed by an invocation of MPI_Barrier. + * The removed ranks will be reported as failed and error handling will progress appropriately. No calls to finalize + * are needed in this case. + */ int Fenix_Finalize(); -int Fenix_Data_group_create(int group_id, MPI_Comm, int start_time_stamp, +/**@}*/ + + +/** + * @defgroup DataRecovery Data Recovery + * @brief Functions for storing and restoring data in Fenix. + * @details @include{doc} DataRecovery.md + * + * @{ + */ +#define FENIX_DATA_GROUP_WORLD_ID 10 +#define FENIX_GROUP_ID_MAX 11 +#define FENIX_TIME_STAMP_MAX 12 +#define FENIX_DATA_MEMBER_ALL 15 +#define FENIX_DATA_MEMBER_ATTRIBUTE_BUFFER 11 +#define FENIX_DATA_MEMBER_ATTRIBUTE_COUNT 12 +#define FENIX_DATA_MEMBER_ATTRIBUTE_DATATYPE 13 +#define FENIX_DATA_MEMBER_ATTRIBUTE_SIZE 14 +#define FENIX_DATA_SNAPSHOT_LATEST -1 +#define FENIX_DATA_SNAPSHOT_ALL 16 +#define FENIX_DATA_SUBSET_CREATED 2 + +#define FENIX_DATA_POLICY_IN_MEMORY_RAID 13 + +/** + * @unimplemented As MPI_Request, but for Fenix asynchronous data recovery calls + */ +typedef struct { + MPI_Request mpi_send_req; + MPI_Request mpi_recv_req; +} Fenix_Request; + +//!@brief A standin for checkpointing/recovering all available data in a member. +extern const Fenix_Data_subset FENIX_DATA_SUBSET_FULL; + +//!@brief A standin for checkpointing/recovering none of the available data in a member. +extern const Fenix_Data_subset FENIX_DATA_SUBSET_EMPTY; + + +/** + * @brief Create a Data Group + * @qualifier collective + * + * If a group with this group_id was already created in the past and has not been deleted, the + * parameters of this call are ignored and this function simply serves to coordinate with any + * ranks that have not yet created this group (e.g. due to a failure). + * + * All calling ranks must pass the same values for the parameters \c group_id, \c comm, + * \c start_time_stamp, \c policy_name, and \c policy_value. + * + * @param group_id A unique identifier to this group. + * @param comm A resilient communicator on which the group is formed. + * @param start_time_stamp The time_stamp to be used for the first commit in this group. + * @param depth + * @parblock + * The number of successive snapshots of this group that are retained by Fenix, in + * addition to the most recent one, and that can be recovered by calling Fenix data member + * restore functions. + * + * For example, a depth of 0 means Fenix will keep only the necessary data to restore the + * most recent snapshot, freeing or overwriting older snapshots automatically. A depth + * of -1 is currently not supported, but would ordinarily indicate that no snapshots should + * be removed automatically. + * @endparblock + * @param policy_name Currently, may only be FENIX_DATA_POLICY_IN_MEMORY_RAID + * @param policy_value Pointer to data passed along to the policy. + * See the specific policy for more information. + * @param flag pointer to store policy-specific status or errors + * @return FENIX_SUCCESS, or an error value. + */ +int Fenix_Data_group_create(int group_id, MPI_Comm comm, int start_time_stamp, int depth, int policy_name, void* policy_value, int* flag); +/** + * @brief Create a data member for store/restore operations + * @qualifier collective + * @qualifier local + * + * All calling ranks in the group's communicator must pass the same values for the parameters + * \c member_id, \c datatype, and \c group_id. + * + * @param group_id Identifier to a data group within which to create the member. + * @param member_id An integer unique within the data group that identifies the data in + * \c source_buffer. Must be nonnegative and less than FENIX_MEMBER_ID_MAX, which is + * guaranteed to be at least 2^30. + * @param buffer Address of the data to be copied to redundant storage maintained by Fenix. + * Note that this parameter may also be specified using #Fenix_Data_member_attr_set, which + * is critical for non-survivor ranks after a failure which will have an invalid address + * which was generated on the failed rank and must update. + * @param count The maximum number of contiguous elements of type \c datatype of the data to be + * stored. Need not be the same in all calling ranks. + * @param datatype The MPI_Datatype of the elements in \c source_buffer + * + * @return FENIX_SUCCESS, or an error value. + */ int Fenix_Data_member_create(int group_id, int member_id, void *buffer, int count, MPI_Datatype datatype); +/** + * @brief Get the storage policy of a data group + * + * @param group_id Identified to the data group to query + * @param policy_name The identifier of the policy name of the data group. + * @param policy_value A location within which to store the policy_values this group's + * policy was configured with. + * @param flag A location set to true if a policy value was extracted, else false. + * @return FENIX_SUCCESS, or an error value. + */ int Fenix_Data_group_get_redundancy_policy(int group_id, int* policy_name, void *policy_value, int *flag); +//!@unimplemented Block on completion of the store operation specified by the request. int Fenix_Data_wait(Fenix_Request request); + +//!@unimplemented Query completion of the store operation specified by the request. int Fenix_Data_test(Fenix_Request request, int *flag); + +/** + * @brief Store a particular group member into the group's resilient storage space, in uncommitted storage. + * @qualifier collective + * + * The user can safely modify the member's data buffer after this call, as the current state is copied immediately. + * Multiple calls may be used to incrementally store data (using subset_specifiers), or overwrite old data prior to a commit. + * + * @param group_id All ranks must provide the same group_id + * @param member_id All ranks must provide the same member_id + * @param subset_specifier Which subset of the data to store. It is always valid for every rank to provide the same + * subset_specifier; depending on the group's policy, varying combinations of specifiers may be possible. + * @return FENIX_SUCCESS, or an error value. + */ int Fenix_Data_member_store(int group_id, int member_id, Fenix_Data_subset subset_specifier); -int Fenix_Data_member_storev(int member_id, int group_id, + +//!@unimplemented As [store](#Fenix_Data_member_store), but subsets may vary rank-to-rank. +int Fenix_Data_member_storev(int group_id, int member_id, Fenix_Data_subset subset_specifier); -int Fenix_Data_member_istore(int member_id, int group_id, +//!@unimplemented As [store](#Fenix_Data_member_store), but asynchronous. +int Fenix_Data_member_istore(int group_id, int member_id, Fenix_Data_subset subset_specifier, Fenix_Request *request); -int Fenix_Data_member_istorev(int member_id, int group_id, +//!@unimplemented As [istore](#Fenix_Data_member_istore), but asynchronous. +int Fenix_Data_member_istorev(int group_id, int member_id, Fenix_Data_subset subset_specifier, Fenix_Request *request); +/** + * @brief Commit stored data members to the group's next snapshot. + * @qualifier collective + * @qualifier local + * + * This function is used to freeze the current state of a data group, + * together with all its application data that has been stored in Fenix’ + * redundant storage, and label it with a time stamp, thus creating a + * snapshot of the stored application data. Only data that has been + * committed is eligible for recovery through #Fenix_Data_member_restore. + * An application needs to call #Fenix_Data_wait for all pending asynchronous + * [Fenix_Data_member_istore(v)](@ref Fenix_Data_member_istore) operations + * in the group before committing. + * + * @param[in] group_id The group to commit + * @param[out] time_stamp The time stamp of the new snapshot + * @returnstatus + */ int Fenix_Data_commit(int group_id, int *time_stamp); +/** + * @brief As [commit](#Fenix_Data_commit), but ensures a globally consistent commit. + * @qualifier collective + * + * This function does not function as a traditional barrier. + * The commit will proceed if all *non-failed* ranks reach the barrier. + * This allows for commits to be made when a rank fails after storing all + * of its data into resilient storage. + * + * @param[in] group_id The group to commit + * @param[out] time_stamp The time stamp of the new snapshot + * @returnstatus + */ int Fenix_Data_commit_barrier(int group_id, int *time_stamp); +//!@unimplemented Block until all ranks in the group have reached this point. int Fenix_Data_barrier(int group_id); +/** + * @brief Restore the data of a group member from a snapshot. + * @qualifier collective + * + * All ranks in the group’s resilient communicator must pass the + * same values for the parameters group_id, member_id, and time_stamp. + * This function is used to retrieve data from consistent snapshot + * members. This function can only be used if the size of the + * communicator used to store the data is the same as that at the time + * of data recovery (this implies non-shrinking communicator recovery + * in case of a rank loss). + * + * If the size of the buffer needing to receive the recovery data is + * unknown for a particular rank, it can be queried using + * #Fenix_Data_member_attr_get. + * + * @param[in] group_id The group to restore from + * @param[in] member_id The member to restore + * @param[out] target_buffer The buffer to store the restored data + * @param[in] max_count The maximum number of elements to restore + * @param[in] time_stamp The time stamp of the snapshot to restore from + * @param[out] found_data The subset of the data that was found in the snapshot + * @returnstatus + */ int Fenix_Data_member_restore(int group_id, int member_id, void *target_buffer, int max_count, int time_stamp, Fenix_Data_subset* found_data); +/** + * @brief Local-only version of Fenix_Data_member_restore + * + * This function restores the data of a group member from the local + * snapshot. + * + * @param[in] group_id The group to restore from + * @param[in] member_id The member to restore + * @param[out] target_buffer The buffer to store the restored data + * @param[in] max_count The maximum number of elements to restore + * @param[in] time_stamp The time stamp of the snapshot to restore from + * @param[out] found_data The subset of the data that was found in the snapshot + * @returnstatus + */ int Fenix_Data_member_lrestore(int group_id, int member_id, void *target_buffer, int max_count, int time_stamp, Fenix_Data_subset* found_data); +//!@unimplemented As #Fenix_Data_member_restore, but restores from a specific rank's data. int Fenix_Data_member_restore_from_rank(int member_id, void *data, int max_count, int time_stamp, int group_id, int source_rank); +/** + * @brief Create a data subset for use in store operations. + * + * Creates a subset based on num_blocks pairs of + * {start_offset,end_offset}, + * {start_offset+stride,end_offset+stride}, + * {start_offset+2*stride,end_offset+2*stride}, + * etc. + * + * The value of start_offset must be smaller than or equal + * to the value of end_offset to indicate non-negative block + * size. Otherwise, the function returns an error code. + * + * Created subsets must be deleted with #Fenix_Data_subset_delete + * to free memory. + * + * @param[in] num_blocks The number of contiguous data blocks. + * @param[in] start_offset The index of the first element in the first data block. + * @param[in] end_offset The index of the last element in the first data block. + * @param[in] stride Regular shift between successive data blocks. + * @param[out] subset_specifier The created subset. + * @returnstatus + */ int Fenix_Data_subset_create(int num_blocks, int start_offset, int end_offset, int stride, Fenix_Data_subset *subset_specifier); +/** + * @brief As #Fenix_Data_subset_create, but with varying start and end offsets. + * + * Creates a subset based on num_blocks pairs of {start_offset,end_offset}. + * The value of start_offset must be smaller than or equal to end_offset + * to indicate non-negative block size. Otherwise, the function returns an + * error code. + * + * Created subsets must be deleted with #Fenix_Data_subset_delete + * to free memory. + * + * @param[in] num_blocks The number of contiguous data blocks. + * @param[in] array_start_offsets The index of the first element in each data block. + * @param[in] array_end_offsets The index of the last element in each data block. + * @param[out] subset_specifier The created subset. + */ int Fenix_Data_subset_createv(int num_blocks, int *array_start_offsets, int *array_end_offsets, Fenix_Data_subset *subset_specifier); +/** + * @brief Delete a data subset. + * + * Frees the memory associated with a data subset object. + * + * @param[in] subset_specifier The subset to delete. + * @returnstatus + */ int Fenix_Data_subset_delete(Fenix_Data_subset *subset_specifier); +//!@unimplemented Get the number of members in a data group. int Fenix_Data_group_get_number_of_members(int group_id, int *number_of_members); -int Fenix_Data_group_get_member_at_position(int position, int *member_id, - int group_id); - +//!@unimplemented Get member ID based on member index +int Fenix_Data_group_get_member_at_position(int group_id, int *member_id, + int position); + +/** + * @brief Get the number of locally-available snapshots in a data group. + * + * May include snapshots that are inconsistent across the group. + * + * @param[in] group_id The group to query + * @param[out] number_of_snapshots The number of snapshots in the group + * @returnstatus + */ int Fenix_Data_group_get_number_of_snapshots(int group_id, int *number_of_snapshots); +/** + * @brief Get the time stamp of a snapshot at a given index. + * + * Snapshots are indexed in reverse order in which the user committed them + * (e.g. the most recent available snapshot has position=0). + * + * @param[in] group_id The group to query + * @param[in] position The index of the snapshot, which must be [0, number_of_snapshots) + * @param[out] time_stamp The time stamp of the snapshot + * + */ int Fenix_Data_group_get_snapshot_at_position(int group_id, int position, int *time_stamp); +//!@unimplemented Get the value of a member's attribute. int Fenix_Data_member_attr_get(int group_id, int member_id, int attributename, void *attributevalue, int *flag, int source_rank); +/** + * @brief Set the value of a member's attribute. + * + * Valid names are #FENIX_DATA_MEMBER_ATTRIBUTE_BUFFER, #FENIX_DATA_MEMBER_ATTRIBUTE_COUNT, + * and #FENIX_DATA_MEMBER_ATTRIBUTE_DATATYPE. + * + * The COUNT and DATATYPE attributes may only be set before the first store operation. + * Contrary to the Fenix specification, returning to #Fenix_Init after a failure does not + * allow the user to set these attributes again. + * + * @param[in] group_id The group to update + * @param[in] member_id The member to update + * @param[in] attribute_name The attribute to update + * @param[in] attribute_value The new value of the attribute + * @param[out] flag Set to true if the attribute was set, else false + * @returnstatus + */ int Fenix_Data_member_attr_set(int group_id, int member_id, int attribute_name, void *attribute_value, int *flag); +/** + * @brief Delete a snapshot from a data group. + * @qualifier local + * + * @param[in] group_id The group to delete from + * @param[in] time_stamp The time stamp of the snapshot to delete + * @returnstatus + */ int Fenix_Data_snapshot_delete(int group_id, int time_stamp); +/** + * @brief Delete a data group. + * @qualifier local + * + * @param[in] group_id The group to delete + * @returnstatus + */ int Fenix_Data_group_delete(int group_id); +/** + * @brief Delete a data member. + * @qualifier local + * + * @param[in] group_id The group to delete from + * @param[in] member_id The member to delete + * @returnstatus + */ int Fenix_Data_member_delete(int group_id, int member_id); - -int Fenix_Process_fail_list(int** fail_list); - -int Fenix_check_cancelled(MPI_Request *request, MPI_Status *status); +/**@}*/ int Fenix_Process_detect_failures(int do_recovery);