Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement result type for nonblocking operations. #517

Merged
merged 8 commits into from
Jul 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 138 additions & 2 deletions include/kamping/result.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This file is part of KaMPIng.
//
// Copyright 2021-2022 The KaMPIng Authors
// Copyright 2021-2023 The KaMPIng Authors
//
// KaMPIng is free software : you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
Expand All @@ -16,6 +16,7 @@
/// @file
/// @brief Some functions and types simplifying/enabling the development of wrapped \c MPI calls in KaMPIng.

#include <optional>
#include <utility>

#include "kamping/has_member.hpp"
Expand Down Expand Up @@ -57,7 +58,17 @@ struct ResultCategoryNotUsed {};
/// elements.
template <class StatusObject, class RecvBuf, class RecvCounts, class RecvDispls, class SendCounts, class SendDispls>
class MPIResult {
private:
/// @brief Helper for implementing \ref is_empty. Returns \c true if all template arguments passed are equal to \ref
/// internal::ResultCategoryNotUsed.
template <typename... Args>
static constexpr bool is_empty_impl = std::conjunction_v<std::is_same<Args, internal::ResultCategoryNotUsed>...>;

public:
/// @brief \c true, if the result does not encapsulate any data.
static constexpr bool is_empty =
is_empty_impl<StatusObject, RecvBuf, RecvCounts, RecvDispls, SendCounts, SendDispls>;

/// @brief Constructor of MPIResult.
///
/// If any of the buffer categories are not used by the wrapped \c MPI call or if the caller has provided (and still
Expand Down Expand Up @@ -175,7 +186,7 @@ class MPIResult {
/// Makes an MPIResult from all arguments passed and inserts internal::ResultCategoryNotUsed when no fitting parameter
/// type is passed as argument.
///
/// @tparam Args Automaticcaly deducted template parameters.
/// @tparam Args Automatically deducted template parameters.
/// @param args All parameter that should be included in the MPIResult.
/// @return MPIResult encapsulating all passed parameters.
template <typename... Args>
Expand Down Expand Up @@ -233,4 +244,129 @@ auto make_mpi_result(Args... args) {
);
}

/// @brief NonBlockingResult contains the result of a non-blocking \c MPI call wrapped by KaMPIng. It encapsulates a
/// \ref kamping::MPIResult and a \ref kamping::Request.
///
///
/// @tparam MPIResultType The underlying result type.
/// @tparam RequestDataBuffer Container encapsulating the underlying request.
template <typename MPIResultType, typename RequestDataBuffer>
class NonBlockingResult {
public:
/// @brief Constructor for \c NonBlockingResult.
/// @param result The underlying \ref kamping::MPIResult.
/// @param request A \ref kamping::internal::DataBuffer containing the associated \ref kamping::Request.
NonBlockingResult(MPIResultType result, RequestDataBuffer request)
: _mpi_result(std::move(result)),
_request(std::move(request)) {}

/// @brief \c true if the result object owns the underlying \ref kamping::Request.
static constexpr bool owns_request = internal::has_extract_v<RequestDataBuffer>;

/// @brief Extracts the components of this results, leaving the user responsible.
///
/// If this result owns the underlying request, returns a \c std::tuple containing the \ref Request and \ref
/// MPIResult. If the request is owned by the user, just return the underlying \ref MPIResult.
///
/// Note that the result may be in an undefined state because the associated operations is still underway and it is
/// the user's responsibilty to ensure that the corresponding request has been completed before accessing the
/// result.
auto extract() {
if constexpr (owns_request) {
auto result = extract_result(); // we try to extract the result first, so that we get a nice error message
return std::make_tuple(_request.extract(), std::move(result));
} else {
return extract_result();
}
}

/// @brief Waits for the underlying \ref Request to complete by calling \ref Request::wait() and returns an \ref
/// MPIResult upon completion or nothing if the result is empty (see \ref MPIResult::is_empty).
///
/// This method is only available if this result owns the underlying request. If this is not the case, the user must
/// manually wait on the request that they own and manually obtain the result via \ref extract().
niklas-uhl marked this conversation as resolved.
Show resolved Hide resolved
template <
typename NonBlockingResulType_ = NonBlockingResult<MPIResultType, RequestDataBuffer>,
typename std::enable_if<NonBlockingResulType_::owns_request, bool>::type = true>
[[nodiscard]] std::conditional_t<!MPIResultType::is_empty, MPIResultType, void> wait() {
kassert_not_extracted("The result of this request has already been extracted.");
_request.underlying().wait();
if constexpr (!MPIResultType::is_empty) {
return extract_result();
} else {
return;
}
}

/// @brief Tests the underlying \ref Request for completion by calling \ref Request::test() and returns an optional
/// containing the underlying \ref MPIResult on success. If the associated operation has not completed yet, returns
/// \c std::nullopt.
///
/// Returns a \c bool indicated if the test succeeded in case the result is empty (see \ref MPIResult::is_empty).
///
/// This method is only available if this result owns the underlying request. If this is not the case, the user must
/// manually test the request that they own and manually obtain the result via \ref extract().
template <
typename NonBlockingResulType_ = NonBlockingResult<MPIResultType, RequestDataBuffer>,
typename std::enable_if<NonBlockingResulType_::owns_request, bool>::type = true>
auto test() {
kassert_not_extracted("The result of this request has already been extracted.");
if constexpr (!MPIResultType::is_empty) {
if (_request.underlying().test()) {
return std::optional{extract_result()};
} else {
return std::optional<MPIResultType>{};
}
} else {
return _request.underlying().test();
}
}

private:
/// @brief Moves the wrapped \ref MPIResult out of this object.
MPIResultType extract_result() {
kassert_not_extracted("The result of this request has already been extracted.");
auto extracted = std::move(_mpi_result);
set_extracted();
return extracted;
}

void set_extracted() {
#if KASSERT_ENABLED(KAMPING_ASSERTION_LEVEL_NORMAL)
is_extracted = true;
#endif
}

/// @brief Throws an assertion if the extracted flag is set, i.e. the underlying status has been moved out.
///
/// @param message The message for the assertion.
void kassert_not_extracted(std::string const message [[maybe_unused]]) const {
#if KASSERT_ENABLED(KAMPING_ASSERTION_LEVEL_NORMAL)
KASSERT(!is_extracted, message, assert::normal);
#endif
}
MPIResultType _mpi_result; ///< The wrapped \ref MPIResult.
RequestDataBuffer _request; ///< DataBuffer containing the wrapped \ref Request.
#if KASSERT_ENABLED(KAMPING_ASSERTION_LEVEL_NORMAL)
bool is_extracted = false; ///< Has the status been extracted and is therefore in an invalid state?
#endif
};

/// @brief Factory for creating a \ref kamping::NonBlockingResult.
///
/// Makes an \ref kamping::NonBlockingResult from all arguments passed and inserts internal::ResultCategoryNotUsed when
/// no fitting parameter type is passed as argument.
///
/// Note that an argument of with type \ref kamping::internal::ParameterType::request is required.
///
/// @tparam Args Automatically deducted template parameters.
/// @param args All parameter that should be included in the MPIResult.
/// @return \ref kamping::NonBlockingResult encapsulating all passed parameters.
template <typename... Args>
auto make_nonblocking_result(Args... args) {
auto&& request = internal::select_parameter_type<internal::ParameterType::request>(args...);
auto result = make_mpi_result(std::forward<Args>(args)...);
return NonBlockingResult(std::move(result), std::move(request));
}

} // namespace kamping
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ option(KAMPING_TEST_ENABLE_SANITIZERS "Enable undefined behavior sanitizer and a
# Registering tests without MPI:
kamping_register_test(test_checking_casts FILES checking_casts_test.cpp)
kamping_register_test(test_result FILES result_test.cpp)
kamping_register_test(test_nonblocking_result FILES nonblocking_result_test.cpp)
kamping_register_test(test_mpi_operations FILES mpi_operations_test.cpp)
kamping_register_test(test_named_parameter_check FILES named_parameter_check_test.cpp)
kamping_register_test(test_named_parameter_selection FILES named_parameter_selection_test.cpp)
Expand Down
215 changes: 215 additions & 0 deletions tests/nonblocking_result_test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// This file is part of KaMPIng.
//
// Copyright 2023 The KaMPIng Authors
//
// KaMPIng is free software : you can redistribute it and/or modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
// version. KaMPIng is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
// for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with KaMPIng. If not, see
// <https://www.gnu.org/licenses/>.

#include <gtest/gtest.h>
#include <mpi.h>

#include "helpers_for_testing.hpp"
#include "kamping/named_parameters.hpp"
#include "kamping/result.hpp"

using namespace kamping;

static bool test_succeed = false;
static size_t num_wait_calls = 0;

KAMPING_MAKE_HAS_MEMBER(wait)
KAMPING_MAKE_HAS_MEMBER(test)

int MPI_Wait(MPI_Request*, MPI_Status*) {
// we have to do something useful here, because else clang wants us to make this function const, which fails the
// build.
num_wait_calls++;
return MPI_SUCCESS;
}

int MPI_Test(MPI_Request*, int* flag, MPI_Status*) {
*flag = test_succeed;
return MPI_SUCCESS;
}

class NonBlockingResultTest : public ::testing::Test {
void SetUp() override {
test_succeed = false;
num_wait_calls = 0;
}
void TearDown() override {
test_succeed = false;
num_wait_calls = 0;
}
};

TEST_F(NonBlockingResultTest, owning_request_and_result_types_match) {
auto recv_buf_obj = recv_buf(alloc_new<std::vector<int>>);
using expected_result_type = MPIResult<
internal::ResultCategoryNotUsed,
decltype(recv_buf_obj),
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed>;
auto request_obj = request();
auto result = kamping::make_nonblocking_result(std::move(recv_buf_obj), std::move(request_obj));

EXPECT_TRUE(has_member_test_v<decltype(result)>);
using test_return_type = decltype(result.test());
EXPECT_TRUE((internal::is_specialization<test_return_type, std::optional>::value));
EXPECT_TRUE((std::is_same_v<test_return_type::value_type, expected_result_type>));

EXPECT_TRUE(has_member_wait_v<decltype(result)>);
using wait_return_type = decltype(result.wait());
EXPECT_TRUE((std::is_same_v<wait_return_type, expected_result_type>));
}

TEST_F(NonBlockingResultTest, owning_request_and_result_wait_works) {
auto recv_buf_obj = recv_buf(alloc_new<std::vector<int>>);
recv_buf_obj.underlying().push_back(42);
recv_buf_obj.underlying().push_back(43);
recv_buf_obj.underlying().push_back(44);
auto request_obj = request();
auto result = kamping::make_nonblocking_result(std::move(recv_buf_obj), std::move(request_obj));
EXPECT_EQ(num_wait_calls, 0);
auto data = result.wait().extract_recv_buffer();
EXPECT_EQ(num_wait_calls, 1);
auto expected_data = std::vector{42, 43, 44};
EXPECT_EQ(data, expected_data);
EXPECT_KASSERT_FAILS(result.extract(), "The result of this request has already been extracted.");
}

TEST_F(NonBlockingResultTest, owning_request_and_result_test_works) {
auto recv_buf_obj = recv_buf(alloc_new<std::vector<int>>);
recv_buf_obj.underlying().push_back(42);
recv_buf_obj.underlying().push_back(43);
recv_buf_obj.underlying().push_back(44);
auto request_obj = request();
auto result = kamping::make_nonblocking_result(std::move(recv_buf_obj), std::move(request_obj));
test_succeed = false;
EXPECT_FALSE(result.test().has_value());
test_succeed = true;
auto data = result.test();
EXPECT_TRUE(data.has_value());
auto expected_data = std::vector{42, 43, 44};
EXPECT_EQ(data.value().extract_recv_buffer(), expected_data);
}

TEST_F(NonBlockingResultTest, owning_request_and_result_extract_works) {
auto recv_buf_obj = recv_buf(alloc_new<std::vector<int>>);
using expected_result_type = MPIResult<
internal::ResultCategoryNotUsed,
decltype(recv_buf_obj),
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed>;
recv_buf_obj.underlying().push_back(42);
recv_buf_obj.underlying().push_back(43);
recv_buf_obj.underlying().push_back(44);
auto request_obj = request();
auto nonblocking_result = kamping::make_nonblocking_result(std::move(recv_buf_obj), std::move(request_obj));
auto [req, result] = nonblocking_result.extract();
EXPECT_TRUE((std::is_same_v<decltype(req), Request>));
EXPECT_TRUE((std::is_same_v<decltype(result), expected_result_type>));

auto expected_data = std::vector{42, 43, 44};
EXPECT_EQ(result.extract_recv_buffer(), expected_data);
EXPECT_KASSERT_FAILS(nonblocking_result.extract(), "The result of this request has already been extracted.");
}

TEST_F(NonBlockingResultTest, owning_request_and_empty_result_types_match) {
auto request_obj = request();
auto result = kamping::make_nonblocking_result(std::move(request_obj));
EXPECT_TRUE(has_member_test_v<decltype(result)>);
using test_return_type = decltype(result.test());
EXPECT_TRUE((std::is_same_v<test_return_type, bool>));
EXPECT_TRUE(has_member_wait_v<decltype(result)>);
using wait_return_type = decltype(result.wait());
EXPECT_TRUE((std::is_same_v<wait_return_type, void>));
}

TEST_F(NonBlockingResultTest, owning_request_and_empty_result_test_works) {
auto request_obj = request();
auto result = kamping::make_nonblocking_result(std::move(request_obj));
test_succeed = false;
EXPECT_FALSE(result.test());
test_succeed = true;
EXPECT_TRUE(result.test());
}

TEST_F(NonBlockingResultTest, owning_request_and_empty_result_extract_works) {
using expected_result_type = MPIResult<
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed>;
auto request_obj = request();
auto nonblocking_result = kamping::make_nonblocking_result(std::move(request_obj));
auto [req, result] = nonblocking_result.extract();
EXPECT_TRUE((std::is_same_v<decltype(req), Request>));
EXPECT_TRUE((std::is_same_v<decltype(result), expected_result_type>));

EXPECT_KASSERT_FAILS(nonblocking_result.extract(), "The result of this request has already been extracted.");
}

TEST_F(NonBlockingResultTest, non_owning_request_and_result_types_match) {
auto recv_buf_obj = recv_buf(alloc_new<std::vector<int>>);
Request req;
auto request_obj = request(req);
auto result = kamping::make_nonblocking_result(std::move(recv_buf_obj), std::move(request_obj));
EXPECT_FALSE(has_member_test_v<decltype(result)>)
<< "The result does not own the request, so test() should not be available.";
EXPECT_FALSE(has_member_wait_v<decltype(result)>)
<< "The result does not own the request, so wait() should not be available.";
}

TEST_F(NonBlockingResultTest, non_owning_request_and_result_extract_works) {
auto recv_buf_obj = recv_buf(alloc_new<std::vector<int>>);
using expected_result_type = MPIResult<
internal::ResultCategoryNotUsed,
decltype(recv_buf_obj),
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed,
internal::ResultCategoryNotUsed>;
recv_buf_obj.underlying().push_back(42);
recv_buf_obj.underlying().push_back(43);
recv_buf_obj.underlying().push_back(44);
Request req;
auto request_obj = request(req);
auto nonblocking_result = kamping::make_nonblocking_result(std::move(recv_buf_obj), std::move(request_obj));
auto result = nonblocking_result.extract();
EXPECT_TRUE((std::is_same_v<decltype(result), expected_result_type>));

auto expected_data = std::vector{42, 43, 44};
EXPECT_EQ(result.extract_recv_buffer(), expected_data);
EXPECT_KASSERT_FAILS(nonblocking_result.extract(), "The result of this request has already been extracted.");
}

TEST_F(NonBlockingResultTest, wait_on_extracted_request) {
auto request_obj = request();
auto result = kamping::make_nonblocking_result(std::move(request_obj));
auto [req, empty_result] = result.extract();
(void)req;
(void)empty_result;
EXPECT_KASSERT_FAILS(result.wait(), "The result of this request has already been extracted.");
}

TEST_F(NonBlockingResultTest, test_on_extracted_request) {
auto request_obj = request();
auto result = kamping::make_nonblocking_result(std::move(request_obj));
auto [req, empty_result] = result.extract();
(void)req;
(void)empty_result;
EXPECT_KASSERT_FAILS(result.test(), "The result of this request has already been extracted.");
}
Loading