From 03454345e66f9f991072f76e8be63f76ca6afde6 Mon Sep 17 00:00:00 2001 From: Thomas Markwalder Date: Fri, 16 Feb 2024 16:18:44 -0500 Subject: [PATCH] [#3245] Adds fundamental classes to perfmon src/hooks/dhcp/perfmon/monitored_duration.cc src/hooks/dhcp/perfmon/monitored_duration.h New files that add DurationDataInterval, DurationKey, Alarm, and MonitoredDuration classes src/hooks/dhcp/perfmon/monitored_duration.h New file of unit tests for new classes modified: Makefile.am modified: tests/Makefile.am --- src/hooks/dhcp/perfmon/Makefile.am | 1 + src/hooks/dhcp/perfmon/monitored_duration.cc | 340 +++++++++ src/hooks/dhcp/perfmon/monitored_duration.h | 463 ++++++++++++ src/hooks/dhcp/perfmon/tests/Makefile.am | 2 +- .../tests/monitored_duration_unittests.cc | 697 ++++++++++++++++++ 5 files changed, 1502 insertions(+), 1 deletion(-) create mode 100644 src/hooks/dhcp/perfmon/monitored_duration.cc create mode 100644 src/hooks/dhcp/perfmon/monitored_duration.h create mode 100644 src/hooks/dhcp/perfmon/tests/monitored_duration_unittests.cc diff --git a/src/hooks/dhcp/perfmon/Makefile.am b/src/hooks/dhcp/perfmon/Makefile.am index b5b8c09e79..741cf1a3c8 100644 --- a/src/hooks/dhcp/perfmon/Makefile.am +++ b/src/hooks/dhcp/perfmon/Makefile.am @@ -17,6 +17,7 @@ noinst_LTLIBRARIES = libperfmon.la libperfmon_la_SOURCES = perfmon_callouts.cc libperfmon_la_SOURCES += perfmon_log.cc perfmon_log.h libperfmon_la_SOURCES += perfmon_messages.cc perfmon_messages.h +libperfmon_la_SOURCES += monitored_duration.cc monitored_duration.h libperfmon_la_SOURCES += version.cc libperfmon_la_CXXFLAGS = $(AM_CXXFLAGS) diff --git a/src/hooks/dhcp/perfmon/monitored_duration.cc b/src/hooks/dhcp/perfmon/monitored_duration.cc new file mode 100644 index 0000000000..db210859d1 --- /dev/null +++ b/src/hooks/dhcp/perfmon/monitored_duration.cc @@ -0,0 +1,340 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include +#include +#include +#include +#include + +using namespace isc::dhcp; +using namespace boost::posix_time; + +namespace isc { +namespace perfmon { + +// DurationDataInterval methods + +DurationDataInterval::DurationDataInterval(const Timestamp& start_time /* = PktEvent::now()*/) + : start_time_(start_time), occurrences_(0), + min_duration_(pos_infin), max_duration_(neg_infin), + total_duration_(microseconds(0)) { +} + +void +DurationDataInterval::addDuration(const Duration& duration) { + ++occurrences_; + if (duration < min_duration_) { + min_duration_ = duration; + } + + if (duration > max_duration_) { + max_duration_ = duration; + } + + total_duration_ += duration; +} + +Duration +DurationDataInterval::getAverageDuration() const { + if (!occurrences_) { + return (ZERO_DURATION()); + } + + return (total_duration_ / occurrences_); +} + +// DurationKey methods + +DurationKey::DurationKey(uint16_t family, + uint8_t query_type, + uint8_t response_type, + const std::string& start_event_label, + const std::string& end_event_label, + dhcp::SubnetID subnet_id) + : family_(family), + query_type_(query_type), + response_type_(response_type), + start_event_label_(start_event_label), + end_event_label_(end_event_label), + subnet_id_(subnet_id) { + if (family != AF_INET && family != AF_INET6) { + isc_throw (BadValue, "DurationKey: family must be AF_INET or AF_INET6"); + } + + validateMessagePair(family, query_type, response_type); +} + +void +DurationKey::validateMessagePair(uint16_t family, uint8_t query_type, uint8_t response_type) { + if (family == AF_INET) { + switch(query_type) { + case DHCP_NOTYPE: + if (response_type == DHCP_NOTYPE || + response_type == DHCPOFFER || + response_type == DHCPACK || + response_type == DHCPNAK) { + return; + } + break; + + case DHCPDISCOVER: + if (response_type == DHCP_NOTYPE || + response_type == DHCPOFFER || + response_type == DHCPNAK) { + return; + } + break; + + case DHCPREQUEST: + if (response_type == DHCP_NOTYPE || + response_type == DHCPACK || + response_type == DHCPNAK) { + return; + } + break; + + case DHCPINFORM: + if (response_type == DHCP_NOTYPE || + response_type == DHCPACK) { + return; + } + break; + + default: + isc_throw(BadValue, "Query type not supported by monitoring: " + << Pkt4::getName(query_type)); + break; + } + + isc_throw(BadValue, "Response type: " << Pkt4::getName(response_type) + << " not valid for query type: " << Pkt4::getName(query_type)); + + } else { + switch(query_type) { + case DHCPV6_NOTYPE: + case DHCPV6_SOLICIT: + if (response_type == DHCPV6_NOTYPE || + response_type == DHCPV6_ADVERTISE || + response_type == DHCPV6_REPLY) { + return; + } + break; + + case DHCPV6_REQUEST: + case DHCPV6_RENEW: + case DHCPV6_REBIND: + case DHCPV6_CONFIRM: + if (response_type == DHCPV6_NOTYPE || + response_type == DHCPV6_REPLY) { + return; + } + break; + + default: + isc_throw(BadValue, "Query type not supported by monitoring: " + << Pkt6::getName(query_type)); + break; + } + + isc_throw(BadValue, "Response type: " << Pkt6::getName(response_type) + << " not valid for query type: " << Pkt6::getName(query_type)); + } +} + +std::string +DurationKey::getLabel() const { + std::ostringstream oss; + if (family_ == AF_INET) { + oss << (query_type_ == DHCP_NOTYPE ? "NONE" : Pkt4::getName(query_type_)) << "-" + << (response_type_ == DHCP_NOTYPE ? "NONE" : Pkt4::getName(response_type_)); + } else { + oss << (query_type_ == DHCPV6_NOTYPE ? "NONE" : Pkt6::getName(query_type_)) << "-" + << (response_type_ == DHCPV6_NOTYPE ? "NONE" : Pkt6::getName(response_type_)); + } + + oss << "." << start_event_label_ << "-" << end_event_label_ + << "." << subnet_id_; + + return (oss.str()); +}; + +// Alarm methods + +Alarm::Alarm(uint16_t family, + uint8_t query_type, + uint8_t response_type, + const std::string& start_event_label, + const std::string& end_event_label, + dhcp::SubnetID subnet_id, + const Duration& low_water, + const Duration& high_water, + bool enabled /* = true */) + : DurationKey(family, query_type, response_type, start_event_label, end_event_label, subnet_id), + low_water_(low_water), + high_water_(high_water), + state_(enabled ? CLEAR : DISABLED), + stos_time_(PktEvent::now()), + last_high_water_report_(PktEvent::EMPTY_TIME()) { + if (low_water >= high_water_) { + isc_throw(BadValue, "low water: " << low_water_ + << ", must be less than high water: " << high_water_); + } +} + +Alarm::Alarm(const DurationKey& key, + const Duration& low_water, + const Duration& high_water, + bool enabled /* = true */) + : DurationKey(key), + low_water_(low_water), + high_water_(high_water), + state_(enabled ? CLEAR : DISABLED), + stos_time_(PktEvent::now()), + last_high_water_report_(PktEvent::EMPTY_TIME()) { + if (low_water >= high_water_) { + isc_throw(BadValue, "low water: " << low_water_ + << ", must be less than high water: " << high_water_); + } +} + +void +Alarm::setLowWater(const Duration& low_water) { + if (low_water >= high_water_) { + isc_throw(BadValue, "low water: " << low_water + << ", must be less than high water: " << high_water_); + } + + low_water_ = low_water; +} + +void +Alarm::setHighWater(const Duration& high_water) { + if (high_water <= low_water_) { + isc_throw(BadValue, "high water: " << high_water + << ", must be greater than low water: " << low_water_); + } + + high_water_ = high_water; +} + +void +Alarm::setState(State state) { + state_ = state; + stos_time_ = PktEvent::now(); + last_high_water_report_ = PktEvent::EMPTY_TIME(); +} + +void +Alarm::clear() { + setState(CLEAR); +} + +void +Alarm::disable() { + setState(DISABLED); +} + +bool +Alarm::checkSample(const Duration& sample, const Duration& report_interval) { + if (state_ == DISABLED) { + isc_throw(InvalidOperation, "Alarm::checkSample() " + "- should not be called when alarm is DISABLED"); + } + + // Low water subceeded? + if (sample < low_water_) { + // If the alarm is currently triggered, transition to CLEAR + // state and return true to signal reportable condition. + if (state_ == TRIGGERED) { + setState(CLEAR); + return (true); + } + + // Nothing to report. + return (false); + } + + // High water exceeded? + if (sample > high_water_) { + // If the alarm isn't yet triggered, transition to the TRIGGERED state. + if (state_ != TRIGGERED) { + setState(TRIGGERED); + } + } + + // If we're triggered and have not yet reported it or it is time to report again, + // update the report time and return true. + if (state_ == TRIGGERED) { + auto now = PktEvent::now(); + if ((last_high_water_report_ == PktEvent::EMPTY_TIME()) || + ((now - last_high_water_report_) > report_interval)) { + last_high_water_report_ = now; + return (true); + } + } + + // Nothing to report. + return (false); +} + +// MonitoredDuration methods + +MonitoredDuration::MonitoredDuration(uint16_t family, + uint8_t query_type, + uint8_t response_type, + const std::string& start_event_label, + const std::string& end_event_label, + dhcp::SubnetID subnet_id, + const Duration& interval_duration) + : DurationKey(family, query_type, response_type, start_event_label, end_event_label, subnet_id), + interval_duration_(interval_duration), + current_interval_(0), + previous_interval_(0) { + if (interval_duration_ <= DurationDataInterval::ZERO_DURATION()) { + isc_throw(BadValue, "MonitoredDuration - interval_duration " << interval_duration_ + << ", is invalid, it must be greater than 0"); + } +} + +MonitoredDuration::MonitoredDuration(const DurationKey& key, + const Duration& interval_duration) + : DurationKey(key), + interval_duration_(interval_duration), + current_interval_(0), + previous_interval_(0) { + if (interval_duration_ <= DurationDataInterval::ZERO_DURATION()) { + isc_throw(BadValue, "MonitoredDuration - interval_duration " << interval_duration_ + << ", is invalid, it must be greater than 0"); + } +} + +bool +MonitoredDuration::addSample(const Duration& sample) { + auto now = PktEvent::now(); + bool do_report = false; + if (!current_interval_) { + current_interval_.reset(new DurationDataInterval(now)); + } else if ((now - current_interval_->getStartTime()) > interval_duration_) { + previous_interval_ = current_interval_; + do_report = true; + current_interval_.reset(new DurationDataInterval(now)); + } + + current_interval_->addDuration(sample); + return (do_report); +} + +void +MonitoredDuration::clear() { + current_interval_.reset(); + previous_interval_.reset(); +} + +} // end of namespace perfmon +} // end of namespace isc diff --git a/src/hooks/dhcp/perfmon/monitored_duration.h b/src/hooks/dhcp/perfmon/monitored_duration.h new file mode 100644 index 0000000000..9c630d1756 --- /dev/null +++ b/src/hooks/dhcp/perfmon/monitored_duration.h @@ -0,0 +1,463 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef _MONITORED_DURATION_H +#define _MONITORED_DURATION_H + +#include +#include + +#include + +namespace isc { +namespace perfmon { + +typedef boost::posix_time::ptime Timestamp; +typedef boost::posix_time::time_duration Duration; + +/// @brief Embodies a span of time (i.e. an interval) over which duration data +/// is accumulated. +class DurationDataInterval { +public: + /// @brief Get a duration of zero. + /// + /// @return Duration of zero microseconds. + static const Duration& ZERO_DURATION() { + static Duration duration(boost::posix_time::microseconds(0)); + return (duration); + } + + /// @brief Constructor + explicit DurationDataInterval(const Timestamp& start_time = dhcp::PktEvent::now()); + + /// @brief Destructor + ~DurationDataInterval() = default; + + /// @brief Add a duration to the interval. + /// + /// Given a duration value: + /// -# Increment the number of occurrences + /// -# Add the duration to the total duration + /// -# Update the minimum and/or maxium duration accordingly + /// + /// @param duration Duration to add. + void addDuration(const Duration& duration); + + /// @brief Get the start time of the interval. + /// + /// @return Timestamp containing the start time. + const Timestamp& getStartTime() const { + return (start_time_); + } + + /// @brief Set the interval start time. + /// + /// @param start_time new value for the interval start time. + void setStartTime(const Timestamp& start_time) { + start_time_ = start_time; + } + + /// @brief Get the number of occurrences that have contributed to the + /// interval. + /// + /// @return uint64_t containing the number of occurrences. + uint64_t getOccurrences() const { + return (occurrences_); + }; + + /// @brief Get the minimum duration that has occurred in the interval. + /// + /// @return Duration containing the minimum duration. + Duration getMinDuration() const { + return (min_duration_); + } + + /// @brief Get the maximum duration that has occurred in the interval. + /// + /// @return Duration containing the maximum duration. + Duration getMaxDuration() const { + return (max_duration_); + } + + /// @brief Get the total duration in the interval. + /// + /// @return Duration containing the total duration. + Duration getTotalDuration() const { + return (total_duration_); + } + + /// @brief Get the average duration for the interval. + /// + /// @return Duration containing the average. + Duration getAverageDuration() const; + +private: + /// @brief Timestamp at which this interval began. + Timestamp start_time_; + + /// @brief Number of event-pairs that occurred during the interval. + uint64_t occurrences_; + + /// @brief Minimum duration that occurred during this interval. + Duration min_duration_; + + /// @brief Maximum duration that occurred during this interval. + Duration max_duration_; + + /// @brief Total duration of all the occurrences included in this interval. + Duration total_duration_; +}; + +/// @brief Defines a pointer to a DurationDataInterval instance. +typedef boost::shared_ptr DurationDataIntervalPtr; + +/// @brief Houses the composite key that uniquely identifies a duration: +/// -# Query Packet Type +/// -# Response Packet Type +/// -# Start Event +/// -# End Event +/// -# Subnet ID can be GLOBAL_SUBNET_ID for aggregate durations +class DurationKey { +public: + /// @brief Constructor + /// + /// @param family protocol family AF_INET or AF_INET6 + /// @param query_type message type of the query packet + /// @param response_type_ message type of the response packet + /// @param start_event_label label of the start event + /// @param end_event_label label of the end event + /// @param SubnetID subnet_id id of the selected subnet + DurationKey(uint16_t family, uint8_t query_type_, uint8_t response_type_, + const std::string& start_event_label, const std::string& end_event_label, + dhcp::SubnetID subnet_id_); + + /// @brief Destructor + virtual ~DurationKey() = default; + + /// @brief Get protocol family + /// + /// @return uint16_t containing the family (AF_INET or AF_INET6) + uint16_t getFamily() { + return(family_); + } + + /// @brief Get the query packet type. + /// + /// @return uint8_t containing the query packet type. + uint8_t getQueryType() const { + return (query_type_); + } + + /// @brief Get the response packet type. + /// + /// @return uint8_t containing the response packet type. + uint8_t getResponseType() const { + return (response_type_); + }; + + /// @brief Get the start event label. + /// + /// @return String containing the start event label. + std::string getStartEventLabel() const { + return (start_event_label_); + } + + /// @brief Get the end event label. + /// + /// @return String containing the end event label. + std::string getEndEventLabel() const { + return (end_event_label_); + } + + /// @brief Get the subnet id. + /// + /// @return SubnetID of the selected subnet. + dhcp::SubnetID getSubnetId() const { + return (subnet_id_); + } + + /// @brief Get a composite label of the member values with text message types. + /// + /// @param family Protocol family of the key (AF_INET or AF_INET6) + /// The format of the string: + /// + /// @code + /// -.-. + /// + /// Example: + /// + /// "DHCPDISCOVER-DHCPOFFER.socket_received.buffer_read.12" + /// + /// or + /// + /// "DHCPV6_SOLICIT-DHCPV6_ADVERTISE.socket_received.buffer_read.12" + /// + /// @endcode + /// + /// @return String containing the composite label. + std::string getLabel() const; + + /// @brief Validates that a query and response message type pair is sane. + /// + /// @param family Protocol family of the key (AF_INET or AF_INET6) + /// The format of the string: + /// @param query_type message type of the query packet + /// @param response_type_ message type of the response packet + /// + /// @throw BadValue is the pairing does not make sense. + static void validateMessagePair(uint16_t family, uint8_t query_type, uint8_t response_type); + +protected: + /// @brief Protocol family AF_INET or AF_INET6 + uint16_t family_; + + /// @brief Query message type (e.g. DHCPDISCOVER, DHCP6_SOLICIT) + uint8_t query_type_; + + /// @brief Response message type. (e.g. DHCPOFFER, DHCP6_ADVERTISE) + uint8_t response_type_; + + /// @brief Label of the start event which begins the duration. + std::string start_event_label_; + + /// @brief Label of the end event which ends the duration. + std::string end_event_label_; + + /// @brief Subnet ID of the subnet selected during query fulfillment. + isc::dhcp::SubnetID subnet_id_; +}; + +/// @brief Defines a pointer to a DurationKey instance. +typedef boost::shared_ptr DurationKeyPtr; + +/// @brief Defines an alarm for a duration. +class Alarm : public DurationKey { +public: + /// @brief Defines Alarm states + enum State { + CLEAR, // Enabled and not currently triggered + TRIGGERED, // High water has been exceeded + DISABLED // Disabled + }; + + /// @brief Constructor + /// + /// @param family protocol family AF_INET or AF_INET6 + /// @param query_type message type of the query packet + /// @param response_type_ message type of the response packet + /// @param start_event_label label of the start event + /// @param end_event_label label of the end event + /// @param SubnetID subnet_id id of the selected subnet + /// @param low_water threshold below which the average duration must fall to clear the alarm + /// @brief high_water threshold above which the average duration must rise to trigger the alarm. + /// @brief enabled true sets state to CLEAR, otherwise DISABLED, defaults to true. + Alarm(uint16_t family, uint8_t query_type_, uint8_t response_type_, + const std::string& start_event_label, const std::string& end_event_label, + dhcp::SubnetID subnet_id_, + const Duration& low_water, const Duration& high_water, bool enabled = true); + + /// @brief Constructor + /// + /// param key composite key that identifies the alarm + /// @param low_water threshold below which the average duration must fall to clear the alarm + /// @brief high_water threshold above which the average duration must rise to trigger the alarm. + /// @brief enabled true sets state to CLEAR, otherwise DISABLED, defaults to true. + Alarm(const DurationKey& key, const Duration& low_water, const Duration& high_water, bool enabled = true); + + /// @brief Destructor + virtual ~Alarm() = default; + + /// @brief Get the low water threshold. + /// + /// @return Duration containing the low water threshold. + Duration getLowWater() const { + return (low_water_); + } + + /// @brief Set the low water threshold. + /// + /// @param low_water new value for the low water threshold. + /// @throw BadValue if new value is greater than the current value + /// of high water. + void setLowWater(const Duration& low_water); + + /// @brief Get the high water threshold. + /// + /// @return Duration containing the high water threshold. + Duration getHighWater() const { + return (high_water_); + } + + /// @brief Set the high water threshold. + /// + /// @param high_water new value for the high water threshold. + /// @throw BadValue if new value is less than the current value + /// of low water. + void setHighWater(const Duration& high_water); + + /// @brief Get the alarm's state. + State getState() { + return (state_); + } + + /// @brief Sets the alarm state. + /// + /// Sets the alarm's state to the given value, + /// sets the start of state time to the current time, + /// and resets the last high water report. + /// + /// @param state new state to which to transition. + void setState(State state); + + /// @brief Get the time the current state began. + /// + /// @return Timestamp the alarm entered it's current state. + Timestamp getStosTime() { + return (stos_time_); + } + + /// @brief Get the timestamp of the last high water report. + /// + /// @return Timestamp containing the last high water report time. + Timestamp getLastHighWaterReport() { + return (last_high_water_report_); + } + + /// @brief Set the timestamp of the last high water report. + /// + /// This function is provided for test purposes only. + /// + /// @param timestamp new value of the last high water report, defaults to + /// the current time. + void setLastHighWaterReport(const Timestamp& timestamp = dhcp::PktEvent::now()) { + last_high_water_report_ = timestamp; + } + + /// @brief Sets the alarm back to the CLEAR state. + void clear(); + + /// @brief Disables the alarm by setting the state to DISABLED. + void disable(); + + /// @brief Checks a duration against the high and low water thresholds + /// and calls the appropriate event handler. + /// + /// -# If the sample is less than the low water threshold: + /// If the state is TRIGGERED, transition to CLEAR and return true otherwise + /// return false. + /// -# If the sample is greater than high water threshold: + /// If the state is not TRIGGERED, transition to TRIGGERED + /// -# If the state is TRIGGERED and the last report time either not set or + /// is more than report interval old, update the last report time to current + /// time and return true. + /// -# Otherwise return false. + /// + /// @param sample duration to test against the thresholds. + /// @param report_interval amount of time that must elapse between high + /// water reports. + /// + /// @return True if alarm state should be reported. + /// + /// @throw InvalidOperation if called when the state is DISABLED. + bool checkSample(const Duration& sample, const Duration& report_interval); + +private: + /// @brief Threshold below which the average duration must fall to clear the alarm. + Duration low_water_; + + /// @brief Threshold above which the average duration must rise to trigger the alarm. + Duration high_water_; + + /// @brief Current alarm state. + State state_; + + /// @brief Timestamp of the beginning of the current state. + Timestamp stos_time_; + + /// @brief Last time the high water breach was reported. + Timestamp last_high_water_report_; +}; + +/// @brief Defines a pointer to an Alarm instance. +typedef boost::shared_ptr AlarmPtr; + +class MonitoredDuration : public DurationKey { +public: + /// @brief Constructor + /// + /// @param family protocol family AF_INET or AF_INET6 + /// @param query_type message type of the query packet + /// @param response_type_ message type of the response packet + /// @param start_event_label label of the start event + /// @param end_event_label label of the end event + /// @param SubnetID subnet_id id of the selected subnet + /// @param Duration interval_duration_; + MonitoredDuration(uint16_t family, uint8_t query_type_, uint8_t response_type_, + const std::string& start_event_label, const std::string& end_event_label, + dhcp::SubnetID subnet_id_, const Duration& interval_duration_); + + /// @brief Constructor + /// + /// param key composite key that identifies the alarm + /// @param Duration interval_duration_; + MonitoredDuration(const DurationKey& key, const Duration& interval_duration_); + + /// @brief Destructor + virtual ~MonitoredDuration() = default; + + /// @brief Get the interval duration + /// + /// @return Duration containing the interval duration. + Duration getIntervalDuration() const { + return(interval_duration_); + } + + /// @brief Get the previous interval + /// + /// @return Pointer to the previous interval if it exists or an empty pointer. + DurationDataIntervalPtr getPreviousInterval() const { + return (previous_interval_); + } + + /// @brief Get the current interval + /// + /// @return Pointer to the current interval if it exists or an empty pointer. + DurationDataIntervalPtr getCurrentInterval() const { + return (current_interval_); + } + + /// @brief Add a sample to the duration's current interval. + /// + /// If there is no current interval start a new one otherwise if the current + /// interval has expired move it to the previous interval, set the return flag + /// to true, then start a new interval. + /// Add the sample to the current interval. + /// + /// @param sample duration value to add + /// + /// @return True if there is a newly completed (i.e. previous) interval to report. + bool addSample(const Duration& sample); + + /// @brief Deletes the current and previous intervals. + void clear(); + +private: + /// @brief Length of the time of a single data interval. + Duration interval_duration_; + + /// @brief Data interval into which samples are currently accumulating. + DurationDataIntervalPtr current_interval_; + + /// @brief Closed data interval immediately prior to the current interval. + DurationDataIntervalPtr previous_interval_; +}; + +typedef boost::shared_ptr MonitoredDurationPtr; + +} // end of namespace isc::perfmon +} // end of namespace isc + +#endif diff --git a/src/hooks/dhcp/perfmon/tests/Makefile.am b/src/hooks/dhcp/perfmon/tests/Makefile.am index 050fb155ea..b6cb195a09 100644 --- a/src/hooks/dhcp/perfmon/tests/Makefile.am +++ b/src/hooks/dhcp/perfmon/tests/Makefile.am @@ -27,7 +27,7 @@ if HAVE_GTEST TESTS += perfmon_unittests perfmon_unittests_SOURCES = run_unittests.cc -#perfmon_unittests_SOURCES += perfmon_unittests.cc +perfmon_unittests_SOURCES += monitored_duration_unittests.cc perfmon_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) $(LOG4CPLUS_INCLUDES) diff --git a/src/hooks/dhcp/perfmon/tests/monitored_duration_unittests.cc b/src/hooks/dhcp/perfmon/tests/monitored_duration_unittests.cc new file mode 100644 index 0000000000..c417f3af32 --- /dev/null +++ b/src/hooks/dhcp/perfmon/tests/monitored_duration_unittests.cc @@ -0,0 +1,697 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include +#include +#include + +#include +#include +#include + +using namespace isc; +using namespace isc::dhcp; +using namespace isc::perfmon; +using namespace boost::posix_time; + +namespace { + +/// @brief Exercises the basic functions of DurationDataInterval. +TEST(DurationDataInterval, basics) { + auto start_time = PktEvent::now(); + + DurationDataIntervalPtr interval; + + // Default Construct a interval. + interval.reset(new DurationDataInterval()); + ASSERT_TRUE(interval); + + // Verify contents. + // Start time is set to current time by default. + EXPECT_GE(interval->getStartTime(), start_time); + EXPECT_EQ(interval->getOccurrences(), 0); + EXPECT_EQ(interval->getMinDuration(), pos_infin); + EXPECT_EQ(interval->getMaxDuration(), neg_infin); + EXPECT_EQ(interval->getTotalDuration(), DurationDataInterval::ZERO_DURATION()); + EXPECT_EQ(interval->getAverageDuration(), DurationDataInterval::ZERO_DURATION()); + + // Verify that start time can be specified. + interval.reset(new DurationDataInterval(start_time + milliseconds(5000))); + ASSERT_TRUE(interval); + EXPECT_GE(interval->getStartTime() - start_time, milliseconds(5000)); + + // Add 100ms duration and check contents. + Duration d100(milliseconds(100)); + interval->addDuration(d100); + EXPECT_EQ(interval->getOccurrences(), 1); + EXPECT_EQ(interval->getMinDuration(), d100); + EXPECT_EQ(interval->getMaxDuration(), d100); + EXPECT_EQ(interval->getTotalDuration(), d100); + EXPECT_EQ(interval->getAverageDuration(), d100); + + // Add 300ms duration and check contents. + Duration d300(milliseconds(300)); + interval->addDuration(d300); + EXPECT_EQ(interval->getOccurrences(), 2); + EXPECT_EQ(interval->getMinDuration(), d100); + EXPECT_EQ(interval->getMaxDuration(), d300); + EXPECT_EQ(interval->getTotalDuration(), d100 + d300); + EXPECT_EQ(interval->getAverageDuration(), Duration(milliseconds(200))); + + // Add 50ms duration and check contents. + Duration d50(milliseconds(50)); + interval->addDuration(d50); + EXPECT_EQ(interval->getOccurrences(), 3); + EXPECT_EQ(interval->getMinDuration(), d50); + EXPECT_EQ(interval->getMaxDuration(), d300); + EXPECT_EQ(interval->getTotalDuration(), d100 + d300 + d50); + EXPECT_EQ(interval->getAverageDuration(), Duration(milliseconds(150))); + + // Add a zero duration and check contents. + interval->addDuration(DurationDataInterval::ZERO_DURATION()); + EXPECT_EQ(interval->getOccurrences(), 4); + EXPECT_EQ(interval->getMinDuration(), DurationDataInterval::ZERO_DURATION()); + EXPECT_EQ(interval->getMaxDuration(), d300); + EXPECT_EQ(interval->getTotalDuration(), d100 + d300 + d50); + EXPECT_EQ(interval->getAverageDuration(), Duration(microseconds(112500))); +} + +/// @brief Exercises the basic functions of DurationDataInterval. +TEST(DurationKey, basics) { + DurationKeyPtr key; + + // Create valid v4 key, verify contents and label. + ASSERT_NO_THROW_LOG(key.reset(new DurationKey(AF_INET, DHCPDISCOVER, DHCPOFFER, + "process_started", "process_completed", + SUBNET_ID_GLOBAL))); + ASSERT_TRUE(key); + EXPECT_EQ(key->getFamily(), AF_INET); + EXPECT_EQ(key->getQueryType(), DHCPDISCOVER); + EXPECT_EQ(key->getResponseType(), DHCPOFFER); + EXPECT_EQ(key->getStartEventLabel(), "process_started"); + EXPECT_EQ(key->getEndEventLabel(), "process_completed"); + EXPECT_EQ(key->getSubnetId(), SUBNET_ID_GLOBAL); + EXPECT_EQ("DHCPDISCOVER-DHCPOFFER.process_started-process_completed.0", key->getLabel()); + + // Create valid v6 key, verify contents and label. + ASSERT_NO_THROW_LOG(key.reset(new DurationKey(AF_INET6, DHCPV6_SOLICIT, DHCPV6_ADVERTISE, + "mt_queued", "process_started", 77))); + ASSERT_TRUE(key); + EXPECT_EQ(key->getFamily(), AF_INET6); + EXPECT_EQ(key->getQueryType(), DHCPV6_SOLICIT); + EXPECT_EQ(key->getResponseType(), DHCPV6_ADVERTISE); + EXPECT_EQ(key->getStartEventLabel(), "mt_queued"); + EXPECT_EQ(key->getEndEventLabel(), "process_started"); + EXPECT_EQ(key->getSubnetId(), 77); + EXPECT_EQ("SOLICIT-ADVERTISE.mt_queued-process_started.77", key->getLabel()); + + // Make sure constructor catches an insane message pairing. + ASSERT_THROW_MSG(key.reset(new DurationKey(AF_INET6, DHCPV6_ADVERTISE, DHCPV6_SOLICIT, + "mt_queued", "process_started", 77)), BadValue, + "Query type not supported by monitoring: ADVERTISE"); +} + +/// @brief Verify v4 message pair validation works. +TEST(DurationKey, validateMessagePairs4) { + // Defines a test scenario. + struct Scenario { + // Query type to use in the scenario. + uint8_t query_type_; + // Valid response types for query type (if any). + std::unordered_set valid_responses_; + }; + + // List of scenarios to test, one per v4 message type. + std::list scenarios { + { DHCP_NOTYPE, {DHCP_NOTYPE, DHCPOFFER, DHCPACK, DHCPNAK}}, + { DHCPDISCOVER, {DHCP_NOTYPE, DHCPOFFER, DHCPNAK}}, + { DHCPOFFER, {}}, + { DHCPREQUEST, {DHCP_NOTYPE, DHCPACK, DHCPNAK}}, + { DHCPDECLINE, {}}, + { DHCPACK, {}}, + { DHCPNAK, {}}, + { DHCPRELEASE, {}}, + { DHCPINFORM, {DHCP_NOTYPE, DHCPACK}}, +// { DHCPFORCERENEW, {}}, commented out in dhcp4.h + { DHCPLEASEQUERY, {}}, + { DHCPLEASEUNASSIGNED, {}}, + { DHCPLEASEUNKNOWN, {}}, + { DHCPLEASEACTIVE, {}}, + { DHCPBULKLEASEQUERY, {}}, + { DHCPLEASEQUERYDONE, {}}, +// { DHCPACTIVELEASEQUERY, {}}, commented out in dhcp4.h + { DHCPLEASEQUERYSTATUS, {}}, + { DHCPTLS, {}}, + }; + + // Iterate over the scenarios. Attempt to pair each scenario query type with every v6 message + // type as a response type. If the response type is in the scenario's valid list, the pair + // should validate, otherwise it should throw. + for (auto const& scenario : scenarios) { + for (uint8_t response_type = DHCP_NOTYPE; response_type < DHCP_TYPES_EOF; ++response_type) { + if (scenario.valid_responses_.count(response_type)) { + ASSERT_NO_THROW_LOG( + DurationKey::validateMessagePair(AF_INET, scenario.query_type_, response_type)); + } else { + ASSERT_THROW( + DurationKey::validateMessagePair(AF_INET, scenario.query_type_, response_type), + BadValue); + } + } + } +} + +/// @brief Verify v4 message pair validation works. +TEST(DurationKey, validateMessagePairs6) { + // Defines a test scenario. + struct Scenario { + // Query type to use in the scenario. + uint8_t query_type_; + // Valid response types for query type (if any). + std::unordered_set valid_responses_; + }; + + // List of scenarios to test, one per v6 message type. + std::list scenarios { + {DHCPV6_NOTYPE, {DHCPV6_NOTYPE, DHCPV6_ADVERTISE, DHCPV6_REPLY}}, + {DHCPV6_SOLICIT, {DHCPV6_NOTYPE, DHCPV6_ADVERTISE, DHCPV6_REPLY}}, + {DHCPV6_ADVERTISE, {}}, + {DHCPV6_REQUEST, {DHCPV6_NOTYPE, DHCPV6_REPLY}}, + {DHCPV6_CONFIRM, {DHCPV6_NOTYPE, DHCPV6_REPLY}}, + {DHCPV6_RENEW, {DHCPV6_NOTYPE, DHCPV6_REPLY}}, + {DHCPV6_REBIND, {DHCPV6_NOTYPE, DHCPV6_REPLY}}, + {DHCPV6_REPLY, {}}, + {DHCPV6_RELEASE, {}}, + {DHCPV6_DECLINE, {}}, + {DHCPV6_RECONFIGURE, {}}, + {DHCPV6_INFORMATION_REQUEST, {}}, + {DHCPV6_RELAY_FORW, {}}, + {DHCPV6_RELAY_REPL, {}}, + {DHCPV6_LEASEQUERY, {}}, + {DHCPV6_LEASEQUERY_REPLY, {}}, + {DHCPV6_LEASEQUERY_DONE, {}}, + {DHCPV6_LEASEQUERY_DATA, {}}, + {DHCPV6_RECONFIGURE_REQUEST, {}}, + {DHCPV6_RECONFIGURE_REPLY, {}}, + {DHCPV6_DHCPV4_QUERY, {}}, + {DHCPV6_DHCPV4_RESPONSE, {}}, + {DHCPV6_ACTIVELEASEQUERY, {}}, + {DHCPV6_STARTTLS, {}}, + {DHCPV6_BNDUPD, {}}, + {DHCPV6_BNDREPLY, {}}, + {DHCPV6_POOLREQ, {}}, + {DHCPV6_POOLRESP, {}}, + {DHCPV6_UPDREQ, {}}, + {DHCPV6_UPDREQALL, {}}, + {DHCPV6_UPDDONE, {}}, + {DHCPV6_CONNECT, {}}, + {DHCPV6_CONNECTREPLY, {}}, + {DHCPV6_DISCONNECT, {}}, + {DHCPV6_STATE, {}}, + {DHCPV6_CONTACT, {}} + }; + + // Iterate over the scenarios. Attempt to pair each scenario query type with every v6 message + // type as a response type. If the response type is in the scenario's valid list, the pair + // should validate, otherwise it should throw. + for (auto const& scenario : scenarios) { + for (uint8_t response_type = DHCPV6_NOTYPE; response_type < DHCPV6_TYPES_EOF; ++response_type) { + if (scenario.valid_responses_.count(response_type)) { + ASSERT_NO_THROW_LOG( + DurationKey::validateMessagePair(AF_INET6, scenario.query_type_, response_type)); + } else { + ASSERT_THROW( + DurationKey::validateMessagePair(AF_INET6, scenario.query_type_, response_type), + BadValue); + } + } + } +} + +/// @brief Verifies Alarm construction. +TEST(Alarm, validConstructors) { + AlarmPtr alarm; + + auto start_time = PktEvent::now(); + + // Create valid v4 alarm, verify contents and label. + Duration low_water(milliseconds(50)); + Duration high_water(milliseconds(250)); + ASSERT_NO_THROW_LOG(alarm.reset(new Alarm(AF_INET, DHCPDISCOVER, DHCPOFFER, + "process_started", "process_completed", + SUBNET_ID_GLOBAL, + low_water, high_water))); + ASSERT_TRUE(alarm); + EXPECT_EQ(alarm->getFamily(), AF_INET); + EXPECT_EQ(alarm->getQueryType(), DHCPDISCOVER); + EXPECT_EQ(alarm->getResponseType(), DHCPOFFER); + EXPECT_EQ(alarm->getStartEventLabel(), "process_started"); + EXPECT_EQ(alarm->getEndEventLabel(), "process_completed"); + EXPECT_EQ(alarm->getSubnetId(), SUBNET_ID_GLOBAL); + EXPECT_EQ("DHCPDISCOVER-DHCPOFFER.process_started-process_completed.0", alarm->getLabel()); + EXPECT_EQ(alarm->getSubnetId(), SUBNET_ID_GLOBAL); + EXPECT_EQ(alarm->getLowWater(), low_water); + EXPECT_EQ(alarm->getHighWater(), high_water); + EXPECT_EQ(alarm->getState(), Alarm::CLEAR); + EXPECT_GE(alarm->getStosTime(), start_time); + + start_time = PktEvent::now(); + + // Create valid v6 key and use that to create an alarm. Verify contents and label. + DurationKeyPtr key; + ASSERT_NO_THROW_LOG(key.reset(new DurationKey(AF_INET6, DHCPV6_SOLICIT, DHCPV6_ADVERTISE, + "mt_queued", "process_started", 77))); + + ASSERT_NO_THROW_LOG(alarm.reset(new Alarm(*key, low_water, high_water, false))); + ASSERT_TRUE(alarm); + EXPECT_EQ(alarm->getFamily(), AF_INET6); + EXPECT_EQ(alarm->getQueryType(), DHCPV6_SOLICIT); + EXPECT_EQ(alarm->getResponseType(), DHCPV6_ADVERTISE); + EXPECT_EQ(alarm->getStartEventLabel(), "mt_queued"); + EXPECT_EQ(alarm->getEndEventLabel(), "process_started"); + EXPECT_EQ(alarm->getSubnetId(), 77); + EXPECT_EQ("SOLICIT-ADVERTISE.mt_queued-process_started.77", alarm->getLabel()); + EXPECT_EQ(alarm->getLowWater(), low_water); + EXPECT_EQ(alarm->getHighWater(), high_water); + EXPECT_EQ(alarm->getState(), Alarm::DISABLED); + EXPECT_GE(alarm->getStosTime(), start_time); +} + +/// @brief Verifies Alarm invalid construction. +TEST(Alarm, invalidConstructors) { + AlarmPtr alarm; + + // Make sure we catch an invalid message pairing. + Duration low_water(milliseconds(50)); + Duration high_water(milliseconds(250)); + ASSERT_THROW_MSG(alarm.reset(new Alarm(AF_INET, DHCPDISCOVER, DHCPDISCOVER, + "process_started", "process_completed", + SUBNET_ID_GLOBAL, low_water, high_water)), + BadValue, + "Response type: DHCPDISCOVER not valid for query type: DHCPDISCOVER"); + + // Low water too high, should throw. + ASSERT_THROW_MSG(alarm.reset(new Alarm(AF_INET, DHCPDISCOVER, DHCPOFFER, + "process_started", "process_completed", + SUBNET_ID_GLOBAL, high_water, low_water)), + BadValue, + "low water: 00:00:00.250000, must be less than high water:" + " 00:00:00.050000"); + + // Create valid v6 key. + DurationKeyPtr key; + ASSERT_NO_THROW_LOG(key.reset(new DurationKey(AF_INET6, DHCPV6_SOLICIT, DHCPV6_ADVERTISE, + "mt_queued", "process_started", 77))); + + // Low water too high, should throw. + ASSERT_THROW_MSG(alarm.reset(new Alarm(*key, high_water, low_water)), + BadValue, + "low water: 00:00:00.250000, must be less than high water:" + " 00:00:00.050000"); +} + +TEST(Alarm, lowWaterHighWaterSetters) { + // Create valid v4 alarm. + Duration low_water(milliseconds(50)); + Duration high_water(milliseconds(250)); + AlarmPtr alarm; + ASSERT_NO_THROW_LOG(alarm.reset(new Alarm(AF_INET, DHCPDISCOVER, DHCPOFFER, + "process_started", "process_completed", + SUBNET_ID_GLOBAL, + low_water, high_water))); + + // Should be able to set thresholds to new, valid values. + low_water += milliseconds(50); + high_water -= milliseconds(100); + ASSERT_NO_THROW(alarm->setLowWater(low_water)); + EXPECT_EQ(alarm->getLowWater(), low_water); + ASSERT_NO_THROW(alarm->setHighWater(high_water)); + EXPECT_EQ(alarm->getHighWater(), high_water); + + // Setting low too high should fail and leave Alarm intact. + ASSERT_THROW_MSG(alarm->setLowWater(high_water), BadValue, + "low water: 00:00:00.150000, must be less than high water: 00:00:00.150000"); + EXPECT_EQ(alarm->getLowWater(), low_water); + + // Setting high too low should fail and leave Alarm intact. + ASSERT_THROW_MSG(alarm->setHighWater(low_water), BadValue, + "high water: 00:00:00.100000, must be greater than low water: 00:00:00.100000"); + EXPECT_EQ(alarm->getHighWater(), high_water); +} + +TEST(Alarm, clearAndDisable) { + auto start_time = PktEvent::now(); + AlarmPtr alarm; + ASSERT_NO_THROW_LOG(alarm.reset(new Alarm(AF_INET, DHCPDISCOVER, DHCPOFFER, + "process_started", "process_completed", + SUBNET_ID_GLOBAL, milliseconds(100), milliseconds(200)))); + + // Initial state should be CLEAR, stos_time_ should be close to now, no report time. + EXPECT_EQ(alarm->getState(), Alarm::CLEAR); + EXPECT_GE(alarm->getStosTime(), start_time); + EXPECT_EQ(alarm->getLastHighWaterReport(), PktEvent::EMPTY_TIME()); + + // Save stos then nap. + auto prev_time = alarm->getStosTime(); + usleep(100); + + // Change the state to DISABLED. Should have a later stos_time_. + ASSERT_NO_THROW(alarm->disable()); + EXPECT_EQ(alarm->getState(), Alarm::DISABLED); + EXPECT_GE(alarm->getStosTime(), prev_time); + EXPECT_EQ(alarm->getLastHighWaterReport(), PktEvent::EMPTY_TIME()); + + // While we're disabled verify operations that are not allowed. + ASSERT_THROW_MSG(alarm->checkSample(milliseconds(75), seconds(60)), InvalidOperation, + "Alarm::checkSample() - should not be called when alarm is DISABLED"); + + // Save stos then nap. + prev_time = alarm->getStosTime(); + usleep(100); + + // Restore the alarm to CLEAR. + ASSERT_NO_THROW(alarm->clear()); + EXPECT_EQ(alarm->getState(), Alarm::CLEAR); + EXPECT_GE(alarm->getStosTime(), prev_time); + EXPECT_EQ(alarm->getLastHighWaterReport(), PktEvent::EMPTY_TIME()); +} + +/// @brief Verifies the result of Alarm::checkSample() over the range of scenarios. +/// The alarm is created in either the CLEAR or TRIGGERED state and then checkSample() +/// is invoked. The scenarios tested are described by the table below: +/// +/// ``` +/// INPUT | OUTPUT +/// Test sample relationship Input Report Int.| +// to the thresholds State Elapsed | Report State Stos Last Report +/// -------------------------------------------------|---------------------------------- +/// sample < low_water C false | false C - - +/// sample < low_water C true | false C - - +/// sample < low_water T false | true C updated reset +/// sample < low_water T true | true C updated reset +/// | +/// sample == low_water C false | false C - - +/// sample == low_water C true | false C - - +/// sample == low_water T false | false T - - +/// sample == low_water T true | true T updated +/// | +/// low_water < sample < high_water C false | false C - - +/// low_water < sample < high_water C true | false C - - +/// low_water < sample < high_water T false | false T - - +/// low_water < sample < high_water T true | true T - updated +/// | +/// sample == high water C false | false C - - +/// sample == high water C true | false C - - +/// sample == high water T false | false T - - +/// sample == high water T true | true T - updated +/// | +/// sample > high water C false | true T updated set +/// sample > high water C true | true T updated set +/// sample > high water T false | false T - - +/// sample > high water T true | true T - updated +/// ``` +TEST(Alarm, checkSample) { + // Create mnemonic constants. + Duration low_water(milliseconds(100)); + Duration high_water(milliseconds(200)); + Duration lt_low_water(milliseconds(50)); + Duration eq_low_water = low_water; + Duration mid_range(milliseconds(150)); + Duration eq_high_water = high_water; + Duration gt_high_water(milliseconds(250)); + Duration report_interval(milliseconds(25)); + + bool report_elapsed = true; + bool should_report = true; + + // Enumerates possible outcomes for last_high_water_report. + enum TimeChange { + none, // no change + set, // from empty time to time + updated, // updated to a more recent time + reset // reset to empty time + }; + + // Embodies a test scenario based on the table in the commentary. It does not + // include a column for stos_time_ changes as they are easily inferred. + struct Scenario { + Duration sample_; // duration to test the alarm with + Alarm::State input_state_; // Starting state of the Alarm (CLEAR or TRIGGERED) + bool report_interval_elapsed_; // True if report interval has elapsed + bool should_report_; // True if checkSample() should return true + Alarm::State output_state_; // Alarm state after calling checkSample() + TimeChange last_report_chg_; // Expected change to last_high_water_report_ + }; + + // Scenarios as described in the commentary. + std::list scenarios = { + { lt_low_water, Alarm::CLEAR, !report_elapsed, !should_report, Alarm::CLEAR, TimeChange::none }, + { lt_low_water, Alarm::CLEAR, report_elapsed, !should_report, Alarm::CLEAR, TimeChange::none }, + { lt_low_water, Alarm::TRIGGERED, !report_elapsed, should_report, Alarm::CLEAR, TimeChange::reset }, + { lt_low_water, Alarm::TRIGGERED, report_elapsed, should_report, Alarm::CLEAR, TimeChange::reset }, + + { eq_low_water, Alarm::CLEAR, !report_elapsed, !should_report, Alarm::CLEAR, TimeChange::none }, + { eq_low_water, Alarm::CLEAR, report_elapsed, !should_report, Alarm::CLEAR, TimeChange::none }, + { eq_low_water, Alarm::TRIGGERED, !report_elapsed, !should_report, Alarm::TRIGGERED, TimeChange::none }, + { eq_low_water, Alarm::TRIGGERED, report_elapsed, should_report, Alarm::TRIGGERED, TimeChange::updated }, + + { mid_range, Alarm::CLEAR, !report_elapsed, !should_report, Alarm::CLEAR, TimeChange::none }, + { mid_range, Alarm::CLEAR, report_elapsed, !should_report, Alarm::CLEAR, TimeChange::none }, + { mid_range, Alarm::TRIGGERED, !report_elapsed, !should_report, Alarm::TRIGGERED, TimeChange::none }, + { mid_range, Alarm::TRIGGERED, report_elapsed, should_report, Alarm::TRIGGERED, TimeChange::updated }, + + { eq_high_water, Alarm::CLEAR, !report_elapsed, !should_report, Alarm::CLEAR, TimeChange::none }, + { eq_high_water, Alarm::CLEAR, report_elapsed, !should_report, Alarm::CLEAR, TimeChange::none }, + { eq_high_water, Alarm::TRIGGERED, !report_elapsed, !should_report, Alarm::TRIGGERED, TimeChange::none }, + { eq_high_water, Alarm::TRIGGERED, report_elapsed, should_report, Alarm::TRIGGERED, TimeChange::updated }, + + { gt_high_water, Alarm::CLEAR, !report_elapsed, should_report, Alarm::TRIGGERED, TimeChange::set }, + { gt_high_water, Alarm::CLEAR, report_elapsed, should_report, Alarm::TRIGGERED, TimeChange::set }, + { gt_high_water, Alarm::TRIGGERED, !report_elapsed, !should_report, Alarm::TRIGGERED, TimeChange::none }, + { gt_high_water, Alarm::TRIGGERED, report_elapsed, should_report, Alarm::TRIGGERED, TimeChange::updated }, + }; + + AlarmPtr alarm; + DurationKey key(AF_INET, DHCPDISCOVER, DHCPOFFER, + "process_started", "process_completed", SUBNET_ID_GLOBAL); + size_t pass = 0; + for (auto const& scenario : scenarios) { + std::ostringstream oss; + oss << "scenario: " << pass++; + SCOPED_TRACE(oss.str()); + + auto start_time = PktEvent::now(); + + // Create an Alarm with the scenario starting characteristics. + ASSERT_NO_THROW_LOG(alarm.reset(new Alarm(key, low_water, high_water))); + if (scenario.input_state_ == Alarm::TRIGGERED) { + alarm->setState(Alarm::TRIGGERED); + alarm->setLastHighWaterReport(!scenario.report_interval_elapsed_ ? + PktEvent::now() : start_time - (report_interval * 2)); + } + + // Save the current timestamps. + auto prev_stos_time = alarm->getStosTime(); + auto prev_report_time = alarm->getLastHighWaterReport(); + + // Take a little nap. + usleep(50); + + // Invoke checkSample() with the scenario sample duration. It should not throw. + bool should_report; + ASSERT_NO_THROW_LOG(should_report = alarm->checkSample(scenario.sample_, report_interval)); + + // Verify that we returned the expected value for a reportable event (or not). + EXPECT_EQ(should_report, scenario.should_report_); + + // Verify we ended up in the expected state. + ASSERT_EQ(alarm->getState(), scenario.output_state_); + + // If the state changed, stos_time_ should have been updated. + if (scenario.input_state_ != scenario.output_state_) { + EXPECT_GT(alarm->getStosTime(), prev_stos_time); + } else { + EXPECT_EQ(alarm->getStosTime(), prev_stos_time); + } + + // Verify the last_high_water_report_ outcome. + switch(scenario.last_report_chg_) { + case TimeChange::none: + EXPECT_EQ(alarm->getLastHighWaterReport(), prev_report_time); + break; + case TimeChange::set: + EXPECT_EQ(prev_report_time, PktEvent::EMPTY_TIME()); + EXPECT_GE(alarm->getLastHighWaterReport(), alarm->getStosTime()); + break; + case TimeChange::updated: + EXPECT_GT(alarm->getLastHighWaterReport(), prev_report_time); + break; + case TimeChange::reset: + EXPECT_EQ(alarm->getLastHighWaterReport(), PktEvent::EMPTY_TIME()); + break; + } + } +} + +/// @brief Verifies MonitoredDuration valid construction. +TEST(MonitoredDuration, validConstructors) { + MonitoredDurationPtr mond; + Duration interval_duration(seconds(60)); + + // Create valid v4 duration, verify contents and label. + ASSERT_NO_THROW_LOG(mond.reset(new MonitoredDuration(AF_INET, DHCPDISCOVER, DHCPOFFER, + "process_started", "process_completed", + SUBNET_ID_GLOBAL, interval_duration))); + ASSERT_TRUE(mond); + EXPECT_EQ(mond->getFamily(), AF_INET); + EXPECT_EQ(mond->getQueryType(), DHCPDISCOVER); + EXPECT_EQ(mond->getResponseType(), DHCPOFFER); + EXPECT_EQ(mond->getStartEventLabel(), "process_started"); + EXPECT_EQ(mond->getEndEventLabel(), "process_completed"); + EXPECT_EQ(mond->getSubnetId(), SUBNET_ID_GLOBAL); + EXPECT_EQ("DHCPDISCOVER-DHCPOFFER.process_started-process_completed.0", mond->getLabel()); + EXPECT_EQ(mond->getIntervalDuration(), interval_duration); + EXPECT_FALSE(mond->getCurrentInterval()); + EXPECT_FALSE(mond->getPreviousInterval()); + + // Create valid v6 key and use that to create an alarm. Verify contents and label. + DurationKeyPtr key; + ASSERT_NO_THROW_LOG(key.reset(new DurationKey(AF_INET6, DHCPV6_SOLICIT, DHCPV6_ADVERTISE, + "mt_queued", "process_started", 77))); + + ASSERT_NO_THROW_LOG(mond.reset(new MonitoredDuration(*key, interval_duration))); + ASSERT_TRUE(mond); + EXPECT_EQ(mond->getFamily(), AF_INET6); + EXPECT_EQ(mond->getQueryType(), DHCPV6_SOLICIT); + EXPECT_EQ(mond->getResponseType(), DHCPV6_ADVERTISE); + EXPECT_EQ(mond->getStartEventLabel(), "mt_queued"); + EXPECT_EQ(mond->getEndEventLabel(), "process_started"); + EXPECT_EQ(mond->getSubnetId(), 77); + EXPECT_EQ("SOLICIT-ADVERTISE.mt_queued-process_started.77", mond->getLabel()); + EXPECT_EQ(mond->getIntervalDuration(), interval_duration); + EXPECT_FALSE(mond->getCurrentInterval()); + EXPECT_FALSE(mond->getPreviousInterval()); +} + +/// @brief Verifies MonitoredDuration invalid construction. +TEST(MonitoredDuration, invalidConstructors) { + MonitoredDurationPtr mond; + + // Make sure we catch an invalid message pairing. + Duration interval_duration = seconds(60); + ASSERT_THROW_MSG(mond.reset(new MonitoredDuration(AF_INET, DHCPDISCOVER, DHCPDISCOVER, + "process_started", "process_completed", + SUBNET_ID_GLOBAL, interval_duration)), + BadValue, + "Response type: DHCPDISCOVER not valid for query type: DHCPDISCOVER"); + + // Interval duration cannot be than zero. + interval_duration = DurationDataInterval::ZERO_DURATION(); + ASSERT_THROW_MSG(mond.reset(new MonitoredDuration(AF_INET, DHCPDISCOVER, DHCPOFFER, + "process_started", "process_completed", + SUBNET_ID_GLOBAL, interval_duration)), + BadValue, + "MonitoredDuration - interval_duration 00:00:00," + " is invalid, it must be greater than 0"); + + // Interval duration cannot be negative. + ASSERT_THROW_MSG(mond.reset(new MonitoredDuration(AF_INET, DHCPDISCOVER, DHCPOFFER, + "process_started", "process_completed", + SUBNET_ID_GLOBAL, seconds(-5))), + BadValue, + "MonitoredDuration - interval_duration -00:00:05," + " is invalid, it must be greater than 0"); + + // Create valid v6 key. + DurationKeyPtr key; + ASSERT_NO_THROW_LOG(key.reset(new DurationKey(AF_INET6, DHCPV6_SOLICIT, DHCPV6_ADVERTISE, + "mt_queued", "process_started", 77))); + + // Interval duration cannot be than zero. + ASSERT_THROW_MSG(mond.reset(new MonitoredDuration(*key, interval_duration)), + BadValue, + "MonitoredDuration - interval_duration 00:00:00," + " is invalid, it must be greater than 0"); + + // Interval duration cannot be negative. + ASSERT_THROW_MSG(mond.reset(new MonitoredDuration(*key, seconds(-5))), + BadValue, + "MonitoredDuration - interval_duration -00:00:05," + " is invalid, it must be greater than 0"); +} + +// Exercises MonitoredDuration::addSample() and MonitoredDuration::clear(). +TEST(MonitoredDuration, addSampleAndClear) { + MonitoredDurationPtr mond; + Duration interval_duration(milliseconds(50)); + + // Create valid v4 duration with interval duration of 50ms. + ASSERT_NO_THROW_LOG(mond.reset(new MonitoredDuration(AF_INET, DHCPDISCOVER, DHCPOFFER, + "process_started", "process_completed", + SUBNET_ID_GLOBAL, interval_duration))); + ASSERT_TRUE(mond); + + // Initially there are no intervals. + EXPECT_FALSE(mond->getCurrentInterval()); + EXPECT_FALSE(mond->getPreviousInterval()); + + // Iterate over a 60ms period, adding a 10ms sample to a duration + // on each pass. Sleep for 10ms in between iterations. + DurationDataIntervalPtr original_interval; + DurationDataIntervalPtr current_interval; + DurationDataIntervalPtr previous_interval; + auto ten_ms = milliseconds(10); + bool should_report; + for (int i = 0; i < 6; ++i) { + ASSERT_NO_THROW(should_report = mond->addSample(ten_ms)); + current_interval = mond->getCurrentInterval(); + ASSERT_TRUE(current_interval); + switch(i) { + case 0: + // First pass, we should only have a current interval, + // nothing to report, one occurrence and a total duration of 10ms. + original_interval = current_interval; + EXPECT_FALSE(mond->getPreviousInterval()); + EXPECT_FALSE(should_report); + EXPECT_EQ(current_interval->getOccurrences(), 1); + EXPECT_EQ(current_interval->getTotalDuration(), ten_ms); + break; + default: + // On passes that occur during the duration interval, we should + // still only have a current interval and nothing to report. + // Current interval occurrences and total duration should be increasing. + EXPECT_EQ(current_interval, original_interval); + EXPECT_FALSE(mond->getPreviousInterval()); + EXPECT_FALSE(should_report); + EXPECT_EQ(current_interval->getOccurrences(), (i + 1)); + EXPECT_EQ(current_interval->getTotalDuration(), (ten_ms * (i + 1))); + break; + case 5: + // On the last pass we should have crossed the interval boundary. + // Previous interval should be equal to the original interval and + // should_report should be true. The new current interval should + // have 1 occurrence and a total of 10ms. + previous_interval = mond->getPreviousInterval(); + EXPECT_TRUE(previous_interval); + EXPECT_EQ(previous_interval, original_interval); + EXPECT_TRUE(should_report); + EXPECT_EQ(current_interval->getOccurrences(), 1); + EXPECT_EQ(current_interval->getTotalDuration(), ten_ms); + break; + } + + // Sleep for 10ms. + usleep(10000); + } + + // Verify that clear wipes the intervals. + ASSERT_NO_THROW_LOG(mond->clear()); + EXPECT_FALSE(mond->getCurrentInterval()); + EXPECT_FALSE(mond->getPreviousInterval()); +} + +} // end of anonymous namespace -- 2.47.2