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

Added Heartbeat to the SHDR adapter to override #351

Merged
merged 3 commits into from
Oct 20, 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
2 changes: 0 additions & 2 deletions .github/workflows/build-docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ env:
on:
# # run on push or pull request events for the master branch
push:
paths-ignore: ["**/*.md", "LICENSE.txt", ".gitignore"]
branches: [ "main", "main-dev" ]
tags:
- "v*.*.*"

Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set(AGENT_VERSION_MAJOR 2)
set(AGENT_VERSION_MINOR 2)
set(AGENT_VERSION_PATCH 0)
set(AGENT_VERSION_BUILD 10)
set(AGENT_VERSION_BUILD 11)
set(AGENT_VERSION_RC "")

# This minimum version is to support Visual Studio 2019 and C++ feature checking and FetchContent
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,11 @@ These can be overridden on a per-adapter basis

*Default*: false

* `Heartbeat` – Overrides the heartbeat interval sent back from the adapter in the
`* PONG <hb>`. The heartbeat will always be this value in milliseconds.

*Default*: _None_

* `IgnoreTimestamps` - Overwrite timestamps with the agent time. This will correct
clock drift but will not give as accurate relative time since it will not take into
consideration network latencies. This can be overridden on a per adapter basis.
Expand Down
1 change: 1 addition & 0 deletions src/mtconnect/configuration/agent_config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,7 @@ namespace mtconnect::configuration {
{{configuration::Url, string()},
{configuration::Device, string()},
{configuration::UUID, string()},
{configuration::Heartbeat, std::chrono::milliseconds()},
{configuration::Uuid, string()}});

if (HasOption(adapterOptions, configuration::Uuid) &&
Expand Down
14 changes: 10 additions & 4 deletions src/mtconnect/source/adapter/shdr/connector.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ namespace sys = boost::system;
namespace mtconnect::source::adapter::shdr {
// Connector public methods
Connector::Connector(asio::io_context::strand &strand, string server, unsigned int port,
seconds legacyTimeout, seconds reconnectInterval)
seconds legacyTimeout, seconds reconnectInterval,
std::optional<std::chrono::milliseconds> heartbeat)
: m_server(std::move(server)),
m_strand(strand),
m_socket(strand.context()),
Expand All @@ -57,6 +58,7 @@ namespace mtconnect::source::adapter::shdr {
m_connected(false),
m_disconnecting(false),
m_realTime(false),
m_heartbeatOverride(heartbeat),
m_legacyTimeout(duration_cast<milliseconds>(legacyTimeout)),
m_reconnectInterval(duration_cast<milliseconds>(reconnectInterval)),
m_receiveTimeLimit(m_legacyTimeout)
Expand Down Expand Up @@ -399,10 +401,14 @@ namespace mtconnect::source::adapter::shdr {
NAMED_SCOPE("Connector::startHeartbeats");

size_t pos;
if (arg.length() > 7 && arg[6] == ' ' &&
(pos = arg.find_first_of("0123456789", 7)) != string::npos)
if (m_heartbeatOverride || (arg.length() > 7 && arg[6] == ' ' &&
(pos = arg.find_first_of("0123456789", 7)) != string::npos))
{
auto freq = milliseconds {atoi(arg.substr(pos).c_str())};
std::chrono::milliseconds freq;
if (m_heartbeatOverride)
freq = *m_heartbeatOverride;
else
freq = milliseconds {atoi(arg.substr(pos).c_str())};
constexpr minutes maxTimeOut = minutes {30}; // Make the maximum timeout 30 minutes.

if (freq > 0ms && freq < maxTimeOut)
Expand Down
8 changes: 6 additions & 2 deletions src/mtconnect/source/adapter/shdr/connector.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ namespace mtconnect::source::adapter::shdr {
/// @param reconnectInterval time between reconnection attempts (defaults to 10 seconds)
Connector(boost::asio::io_context::strand &strand, std::string server, unsigned int port,
std::chrono::seconds legacyTimout = std::chrono::seconds {600},
std::chrono::seconds reconnectInterval = std::chrono::seconds {10});
std::chrono::seconds reconnectInterval = std::chrono::seconds {10},
std::optional<std::chrono::milliseconds> heartbeat = std::nullopt);

virtual ~Connector();

Expand Down Expand Up @@ -94,6 +95,8 @@ namespace mtconnect::source::adapter::shdr {
}

void setRealTime(bool realTime = true) { m_realTime = realTime; }

const auto &getHeartbeatOverride() const { return m_heartbeatOverride; }

protected:
void close();
Expand Down Expand Up @@ -146,7 +149,8 @@ namespace mtconnect::source::adapter::shdr {

// Heartbeats
bool m_heartbeats = false;
std::chrono::milliseconds m_heartbeatFrequency = std::chrono::milliseconds {HEARTBEAT_FREQ};
std::chrono::milliseconds m_heartbeatFrequency {HEARTBEAT_FREQ};
std::optional<std::chrono::milliseconds> m_heartbeatOverride;
std::chrono::milliseconds m_legacyTimeout;
std::chrono::milliseconds m_reconnectInterval;
std::chrono::milliseconds m_receiveTimeLimit;
Expand Down
12 changes: 6 additions & 6 deletions src/mtconnect/source/adapter/shdr/shdr_adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ namespace mtconnect::source::adapter::shdr {
{
GetOptions(block, m_options, options);
AddOptions(block, m_options,
{
{configuration::UUID, string()},
{configuration::Manufacturer, string()},
{configuration::Station, string()},
{configuration::Url, string()},
});
{{configuration::Heartbeat, Milliseconds {0}},
{configuration::UUID, string()},
{configuration::Manufacturer, string()},
{configuration::Station, string()},
{configuration::Url, string()}});

m_options.erase(configuration::Host);
m_options.erase(configuration::Port);
m_heartbeatOverride = GetOption<Milliseconds>(m_options, configuration::Heartbeat);

AddDefaultedOptions(block, m_options,
{{configuration::Host, "localhost"s},
Expand Down
28 changes: 27 additions & 1 deletion test_package/adapter_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
// limitations under the License.
//

/// @file
/// SHDR Adapter tests

// Ensure that gtest is the first header otherwise Windows raises an error
#include <gtest/gtest.h>
// Keep this comment to keep gtest.h above. (clang-format off/on is not working here!)
Expand Down Expand Up @@ -45,7 +48,8 @@ int main(int argc, char *argv[])
return RUN_ALL_TESTS();
}

TEST(AdapterTest, MultilineData)
/// @test check if multiline data is getting treated as a single chunk
TEST(AdapterTest, should_handle_multiline_data)
{
asio::io_context ioc;
asio::io_context::strand strand(ioc);
Expand Down Expand Up @@ -76,6 +80,7 @@ Another Line...
EXPECT_EQ(exp, data);
}

/// @test verify that commands can also be multiline
TEST(AdapterTest, should_forward_multiline_command)
{
asio::io_context ioc;
Expand Down Expand Up @@ -108,6 +113,7 @@ TEST(AdapterTest, should_forward_multiline_command)
EXPECT_EQ(exp, value);
}

/// @test Check that the options are reset when given as a command
TEST(AdapterTest, should_set_options_from_commands)
{
asio::io_context ioc;
Expand All @@ -122,3 +128,23 @@ TEST(AdapterTest, should_set_options_from_commands)
auto v = GetOption<int>(adapter->getOptions(), "ShdrVersion");
ASSERT_EQ(int64_t(3), *v);
}

/// @test validate heartbeat override is set properly
TEST(AdapterTest, should_set_heartbeat_override_from_configuration)
{
namespace pt = boost::property_tree;
asio::io_context ioc;
asio::io_context::strand strand(ioc);
ConfigOptions options;
pt::ptree tree;
tree.push_back(make_pair<string, pt::ptree>(configuration::Host, pt::ptree("locahost"s)));
tree.push_back(make_pair<string, pt::ptree>(configuration::Port, pt::ptree("7878"s)));
tree.push_back(make_pair<string, pt::ptree>(configuration::Heartbeat, pt::ptree("123"s)));
pipeline::PipelineContextPtr context = make_shared<pipeline::PipelineContext>();
auto adapter = make_unique<ShdrAdapter>(ioc, context, options, tree);

const auto &over = adapter->getHeartbeatOverride();
ASSERT_TRUE(over);
ASSERT_EQ(123ms, *over);
}

64 changes: 57 additions & 7 deletions test_package/connector_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
// limitations under the License.
//

/// @file
/// Test the SHDR connector behavior. The connector handles the lower level communications for the
/// adapter.

// Ensure that gtest is the first header otherwise Windows raises an error
#include <gtest/gtest.h>
// Keep this comment to keep gtest.h above. (clang-format off/on is not working here!)
Expand Down Expand Up @@ -44,19 +48,26 @@ using tcp = boost::asio::ip::tcp;
namespace ip = boost::asio::ip;
namespace sys = boost::system;

using namespace chrono_literals;

// main
int main(int argc, char *argv[])
{
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

/// @brief Mock adapter for this connector.
class TestConnector : public Connector
{
public:
TestConnector(boost::asio::io_context::strand &strand, const std::string &server,
unsigned int port, std::chrono::seconds legacyTimeout = std::chrono::seconds {5})
: Connector(strand, server, port, legacyTimeout), m_disconnected(false), m_strand(strand)
unsigned int port, std::chrono::seconds legacyTimeout = std::chrono::seconds {5},
std::chrono::seconds reconnectInterval = std::chrono::seconds {10},
std::optional<std::chrono::milliseconds> heartbeat = std::nullopt)
: Connector(strand, server, port, legacyTimeout, reconnectInterval, heartbeat),
m_disconnected(false),
m_strand(strand)
{}

bool start(unsigned short port)
Expand Down Expand Up @@ -97,6 +108,7 @@ class TestConnector : public Connector
boost::asio::io_context::strand &m_strand;
};

/// @brief Connector test runner
class ConnectorTest : public testing::Test
{
protected:
Expand Down Expand Up @@ -198,22 +210,27 @@ class ConnectorTest : public testing::Test
string m_line;
};

TEST_F(ConnectorTest, Connection)
/// @test check if the connector is able to connect to a socket. Make sure it sends a ping.
TEST_F(ConnectorTest, should_be_able_to_connect)
{
ASSERT_TRUE(m_connector->m_disconnected);

startServer();

/// - Start the connector
m_connector->start(m_port);

/// - Wait for 5 seconds for a connection
runUntil(5s, [this]() -> bool { return m_connected && m_connector->isConnected(); });

/// - Check for a ping
EXPECT_FALSE(m_connector->m_disconnected);
auto line = read(1s);
ASSERT_EQ("* PING", line);
}

TEST_F(ConnectorTest, DataCapture)
/// @test check if the connector can receive data from the connector
TEST_F(ConnectorTest, should_be_able_to_read_data_from_a_socket)
{
// Start the accept thread
ASSERT_TRUE(m_connector->m_disconnected);
Expand All @@ -230,7 +247,8 @@ TEST_F(ConnectorTest, DataCapture)
ASSERT_EQ("Hello Connector", m_connector->m_data);
}

TEST_F(ConnectorTest, Disconnect)
/// @test check if the connector disconnects
TEST_F(ConnectorTest, should_disconnect_when_server_closes_the_connection)
{
// Start the accept thread
startServer();
Expand All @@ -247,7 +265,8 @@ TEST_F(ConnectorTest, Disconnect)
ASSERT_TRUE(m_connector->m_disconnected);
}

TEST_F(ConnectorTest, ProtocolCommand)
/// @test check if the connector correctly handles a protocol command starting with a `*`
TEST_F(ConnectorTest, should_process_a_protocol_command)
{
// Start the accept thread
startServer();
Expand Down Expand Up @@ -479,7 +498,7 @@ TEST_F(ConnectorTest, IPV6Connection)
#endif
}

TEST_F(ConnectorTest, StartHeartbeats)
TEST_F(ConnectorTest, should_start_heartbeats_when_a_valid_pong_is_received)
{
ASSERT_TRUE(!m_connector->heartbeats());

Expand Down Expand Up @@ -543,3 +562,34 @@ TEST_F(ConnectorTest, test_trimming_trailing_white_space)
ASSERT_EQ((string) "fourth", m_connector->m_list[3]);
ASSERT_EQ((string) "r", m_connector->m_list[4]);
}

/// @test validate the heartbeat override works for the connector.
TEST_F(ConnectorTest, should_override_adapter_heartbeat_in_pong)
{
boost::asio::io_context::strand strand(m_context);
m_connector = std::make_unique<TestConnector>(strand, "127.0.0.1", m_port, 600s, 10s, 123ms);
m_connector->m_disconnected = true;
m_connected = false;

ASSERT_TRUE(!m_connector->heartbeats());

string line = "* PONG 888";
m_connector->startHeartbeats(line);

ASSERT_TRUE(m_connector->heartbeats());
ASSERT_EQ(std::chrono::milliseconds {123}, m_connector->heartbeatFrequency());

m_connector->resetHeartbeats();

line = "* PONG 456 ";
m_connector->startHeartbeats(line);

ASSERT_TRUE(m_connector->heartbeats());
ASSERT_EQ(std::chrono::milliseconds {123}, m_connector->heartbeatFrequency());

line = "* PONG 323";
m_connector->startHeartbeats(line);

ASSERT_TRUE(m_connector->heartbeats());
ASSERT_EQ(std::chrono::milliseconds {123}, m_connector->heartbeatFrequency());
}