Fix #88: Timeout handling. (#91)

fix timeout handling

* Despite what is documented in sd_bus_get_timeout(3), the timeout
  returned is actually an absolute time point of Linux's CLOCK_MONOTONIC
  clock. Hence, we first have to subtract the current time from the
  timeout in order to get a relative time that can be passed to poll.

* For async call timeouts to reliably work, we need a way to notify the
  event loop of a connection that is currently blocked waiting in poll.
  I.e. assume the event loop thread entered poll with a timeout set to
  T1. Afterwards, the main thread starts an async call C with a timeout
  T2 < T1. In order for C to be canceled after its timeout T1 has
  elapsed, we have to be able to notify the event loop so that it can
  update its poll data.

Co-authored-by: Urs Ritzmann <ursritzmann@protonmail.ch>
Co-authored-by: Lukasz Marcul <lukasz.marcul@onemeter.com>
This commit is contained in:
Benjamin Kaufmann
2021-12-20 10:00:29 +01:00
committed by GitHub
parent 0b8f2d9752
commit bb0f3f0242
13 changed files with 249 additions and 30 deletions

View File

@ -42,6 +42,7 @@
using ::testing::Eq;
using ::testing::DoubleEq;
using ::testing::Gt;
using ::testing::Le;
using ::testing::AnyOf;
using ::testing::ElementsAre;
using ::testing::SizeIs;
@ -56,6 +57,7 @@ using SdbusTestObject = TestFixture;
TEST_F(SdbusTestObject, ThrowsTimeoutErrorWhenClientSideAsyncMethodTimesOut)
{
std::chrono::time_point<std::chrono::steady_clock> start;
try
{
std::promise<uint32_t> promise;
@ -68,7 +70,8 @@ TEST_F(SdbusTestObject, ThrowsTimeoutErrorWhenClientSideAsyncMethodTimesOut)
promise.set_exception(std::make_exception_ptr(*err));
});
m_proxy->doOperationClientSideAsyncWith500msTimeout(1000); // The operation will take 1s, but the timeout is 500ms, so we should time out
start = std::chrono::steady_clock::now();
m_proxy->doOperationClientSideAsyncWithTimeout(1us, 1000); // The operation will take 1s, but the timeout is 500ms, so we should time out
future.get();
FAIL() << "Expected sdbus::Error exception";
@ -77,6 +80,8 @@ TEST_F(SdbusTestObject, ThrowsTimeoutErrorWhenClientSideAsyncMethodTimesOut)
{
ASSERT_THAT(e.getName(), AnyOf("org.freedesktop.DBus.Error.Timeout", "org.freedesktop.DBus.Error.NoReply"));
ASSERT_THAT(e.getMessage(), AnyOf("Connection timed out", "Method call timed out"));
auto measuredTimeout = std::chrono::steady_clock::now() - start;
ASSERT_THAT(measuredTimeout, Le(50ms));
}
catch(...)
{

View File

@ -37,9 +37,11 @@
// STL
#include <thread>
#include <chrono>
using ::testing::Eq;
using namespace sdbus::test;
using namespace std::chrono_literals;
/*-------------------------------------*/
/* -- TEST CASES -- */
@ -88,3 +90,44 @@ TEST(Connection, CanEnterAndLeaveEventLoop)
t.join();
}
TEST(Connection, PollDataGetZeroTimeout)
{
sdbus::IConnection::PollData pd{};
pd.timeout_usec = 0;
ASSERT_TRUE(pd.getRelativeTimeout().has_value());
EXPECT_THAT(pd.getRelativeTimeout().value(), Eq(std::chrono::microseconds::zero()));
EXPECT_THAT(pd.getPollTimeout(), Eq(0));
}
TEST(Connection, PollDataGetInfiniteTimeout)
{
sdbus::IConnection::PollData pd{};
pd.timeout_usec = UINT64_MAX;
ASSERT_FALSE(pd.getRelativeTimeout().has_value());
EXPECT_THAT(pd.getPollTimeout(), Eq(-1));
}
TEST(Connection, PollDataGetZeroRelativeTimeoutForPast)
{
sdbus::IConnection::PollData pd{};
auto past = std::chrono::steady_clock::now() - 10s;
pd.timeout_usec = std::chrono::duration_cast<std::chrono::microseconds>(past.time_since_epoch()).count();
ASSERT_TRUE(pd.getRelativeTimeout().has_value());
EXPECT_THAT(pd.getRelativeTimeout().value(), Eq(0us));
EXPECT_THAT(pd.getPollTimeout(), Eq(0));
}
TEST(Connection, PollDataGetRelativeTimeoutInTolerance)
{
sdbus::IConnection::PollData pd{};
constexpr auto TIMEOUT = 1s;
constexpr auto TOLERANCE = 100ms;
auto future = std::chrono::steady_clock::now() + TIMEOUT;
pd.timeout_usec = std::chrono::duration_cast<std::chrono::microseconds>(future.time_since_epoch()).count();
ASSERT_TRUE(pd.getRelativeTimeout().has_value());
EXPECT_GE(pd.getRelativeTimeout().value(), TIMEOUT - TOLERANCE);
EXPECT_LE(pd.getRelativeTimeout().value(), TIMEOUT + TOLERANCE);
EXPECT_GE(pd.getPollTimeout(), 900);
EXPECT_LE(pd.getPollTimeout(), 1100);
}

View File

@ -42,6 +42,7 @@
using ::testing::Eq;
using ::testing::DoubleEq;
using ::testing::Gt;
using ::testing::Le;
using ::testing::AnyOf;
using ::testing::ElementsAre;
using ::testing::SizeIs;
@ -162,21 +163,24 @@ TEST_F(SdbusTestObject, CallsMultiplyMethodWithNoReplyFlag)
TEST_F(SdbusTestObject, CallsMethodWithCustomTimeoutSuccessfully)
{
auto res = m_proxy->doOperationWith500msTimeout(20); // The operation will take 20ms, but the timeout is 500ms, so we are fine
auto res = m_proxy->doOperationWithTimeout(500ms, 20); // The operation will take 20ms, but the timeout is 500ms, so we are fine
ASSERT_THAT(res, Eq(20));
}
TEST_F(SdbusTestObject, ThrowsTimeoutErrorWhenMethodTimesOut)
{
auto start = std::chrono::steady_clock::now();
try
{
m_proxy->doOperationWith500msTimeout(1000); // The operation will take 1s, but the timeout is 500ms, so we should time out
m_proxy->doOperationWithTimeout(1us, 1000); // The operation will take 1s, but the timeout is 1us, so we should time out
FAIL() << "Expected sdbus::Error exception";
}
catch (const sdbus::Error& e)
{
ASSERT_THAT(e.getName(), AnyOf("org.freedesktop.DBus.Error.Timeout", "org.freedesktop.DBus.Error.NoReply"));
ASSERT_THAT(e.getMessage(), AnyOf("Connection timed out", "Method call timed out"));
auto measuredTimeout = std::chrono::steady_clock::now() - start;
ASSERT_THAT(measuredTimeout, Le(50ms));
}
catch(...)
{

View File

@ -28,6 +28,8 @@
#define SDBUS_CPP_INTEGRATIONTESTS_DEFS_H_
#include "sdbus-c++/Types.h"
#include <chrono>
#include <ostream>
namespace sdbus { namespace test {
@ -54,6 +56,20 @@ const bool DEFAULT_BLOCKING_VALUE{true};
constexpr const double DOUBLE_VALUE{3.24L};
/** Duration stream operator for human readable gtest value output.
*
* Note that the conversion to double is lossy if the input type has 64 or more bits.
* This is ok for our integration tests because they don't have very
* accurate timing requirements.
*
* @return human readable duration in seconds
*/
template< class Rep, class Period >
static std::ostream& operator<<(std::ostream& os, const std::chrono::duration<Rep, Period>& d)
{
auto seconds = std::chrono::duration_cast<std::chrono::duration<double>>(d);
return os << seconds.count() << " s";
}
}}
#endif /* SDBUS_CPP_INTEGRATIONTESTS_DEFS_H_ */

View File

@ -97,11 +97,11 @@ void TestProxy::installDoOperationClientSideAsyncReplyHandler(std::function<void
m_DoOperationClientSideAsyncReplyHandler = std::move(handler);
}
uint32_t TestProxy::doOperationWith500msTimeout(uint32_t param)
uint32_t TestProxy::doOperationWithTimeout(const std::chrono::microseconds &timeout, uint32_t param)
{
using namespace std::chrono_literals;
uint32_t result;
getProxy().callMethod("doOperation").onInterface(sdbus::test::INTERFACE_NAME).withTimeout(500000us).withArguments(param).storeResultsTo(result);
getProxy().callMethod("doOperation").onInterface(sdbus::test::INTERFACE_NAME).withTimeout(timeout).withArguments(param).storeResultsTo(result);
return result;
}
@ -126,12 +126,12 @@ void TestProxy::doErroneousOperationClientSideAsync()
});
}
void TestProxy::doOperationClientSideAsyncWith500msTimeout(uint32_t param)
void TestProxy::doOperationClientSideAsyncWithTimeout(const std::chrono::microseconds &timeout, uint32_t param)
{
using namespace std::chrono_literals;
getProxy().callMethodAsync("doOperation")
.onInterface(sdbus::test::INTERFACE_NAME)
.withTimeout(500000us)
.withTimeout(timeout)
.withArguments(param)
.uponReplyInvoke([this](const sdbus::Error* error, uint32_t returnValue)
{

View File

@ -91,10 +91,10 @@ protected:
public:
void installDoOperationClientSideAsyncReplyHandler(std::function<void(uint32_t res, const sdbus::Error* err)> handler);
uint32_t doOperationWith500msTimeout(uint32_t param);
uint32_t doOperationWithTimeout(const std::chrono::microseconds &timeout, uint32_t param);
sdbus::PendingAsyncCall doOperationClientSideAsync(uint32_t param);
void doErroneousOperationClientSideAsync();
void doOperationClientSideAsyncWith500msTimeout(uint32_t param);
void doOperationClientSideAsyncWithTimeout(const std::chrono::microseconds &timeout, uint32_t param);
int32_t callNonexistentMethod();
int32_t callMethodOnNonexistentInterface();
void setStateProperty(const std::string& value);