diff --git a/src/utils/LimitTimer.cxxtest b/src/utils/LimitTimer.cxxtest new file mode 100644 index 000000000..4e1e9ae1f --- /dev/null +++ b/src/utils/LimitTimer.cxxtest @@ -0,0 +1,136 @@ +/** \copyright + * Copyright (c) 2021, Stuart Baker + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @file LimitTimer.cxxtest + * + * Unit tests for LimitTimer. + * + * @author Stuart Baker + * @date 10 January 2021 + */ + +#include "utils/test_main.hxx" + +#include "utils/LimitTimer.hxx" +#include "os/FakeClock.hxx" + +class MockCallback +{ +public: + MOCK_METHOD0(callback, void()); +}; + +class MyLimitTimer +{ +public: + /// Constructor. + /// @param update_delay_msec cooldown time delay in milliseconds + /// @param max_tokens number of available tokens + MyLimitTimer(uint16_t update_delay_msec, + uint8_t max_tokens) + : mockCallback_() + , limitTimer_(&g_executor, update_delay_msec, max_tokens, + std::bind(&MockCallback::callback, &mockCallback_)) + { + } + + /// Destructor. + ~MyLimitTimer() + { + wait_for_main_executor(); + while (!g_executor.active_timers()->empty()) + { + sleep_helper(20); + } + } + + /// Helper function for sleeping. + /// @param clk fake clock or nullptr if no fake clock exists + /// @param len_msec how long to sleep + /// @param step_msec what granularity to use for sleeping wiht fake clock. + void sleep_helper(unsigned len_msec, unsigned step_msec = 50) + { + for (unsigned i = 0; i < len_msec; i += step_msec) + { + clk_.advance(MSEC_TO_NSEC(step_msec)); + wait_for_main_executor(); + } + } + + FakeClock clk_; + + ::testing::StrictMock mockCallback_; + LimitTimer limitTimer_; +}; + +TEST(LimitTimerTest, Create) +{ + MyLimitTimer(200, 3); +} + +TEST(LimitTimerTest, TryTake) +{ + MyLimitTimer lt(200, 3); + + ::testing::MockFunction check; + + ::testing::InSequence s; + + // flush out the tokens + EXPECT_CALL(lt.mockCallback_, callback()).Times(0); + EXPECT_TRUE(lt.limitTimer_.try_take()); + EXPECT_TRUE(lt.limitTimer_.try_take()); + EXPECT_TRUE(lt.limitTimer_.try_take()); + EXPECT_CALL(check, Call("1")); + check.Call("1"); + + // try to pull one more token out (that is not available) + EXPECT_CALL(lt.mockCallback_, callback()).Times(0); + EXPECT_FALSE(lt.limitTimer_.try_take()); + EXPECT_CALL(check, Call("2")); + check.Call("2"); + + // verify the callback after timeout + EXPECT_CALL(lt.mockCallback_, callback()).Times(1); + EXPECT_CALL(check, Call("3")); + lt.sleep_helper(200); + check.Call("3"); + + // timer should still be running as bucket gets refilled + lt.sleep_helper(200); + EXPECT_FALSE(g_executor.active_timers()->empty()); + + // pull a token from the bucket + lt.limitTimer_.take_no_callback(); + + // timer should still be running as bucket gets refilled + lt.sleep_helper(200); + EXPECT_FALSE(g_executor.active_timers()->empty()); + + // timer should stop running once bucket gets refilled + lt.sleep_helper(200); + EXPECT_TRUE(g_executor.active_timers()->empty()); +} diff --git a/src/utils/LimitTimer.hxx b/src/utils/LimitTimer.hxx new file mode 100644 index 000000000..d2a1cf83f --- /dev/null +++ b/src/utils/LimitTimer.hxx @@ -0,0 +1,135 @@ +/** @copyright + * Copyright (c) 2020, Balazs Racz; 2021 Stuart Baker + * All rights reserved + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * - Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @file LimitTimer.hxx + * + * Limits the number of updates per unit time. Initial version written by + * Balazs Racz, modified for generalization by Stuart Baker + * + * @author Balazs Racz, Stuart Baker + * @date 9 January 2021 + */ + +#include "executor/Timer.hxx" + +/// This timer takes care of limiting the number of speed updates we send +/// out in a second. It is a token bucket filter. +class LimitTimer : public Timer +{ +public: + /// Constructor. + /// @param ex executor to run on + /// @param update_delay_msec cooldown time delay in milliseconds + /// @param max_tokens number of available tokens, <= 127 max + /// @param callback callback called once after cooldown time delay + LimitTimer(ExecutorBase *ex, uint16_t update_delay_msec, uint8_t max_tokens, + std::function callback) + : Timer(ex->active_timers()) + , updateDelayMsec_(update_delay_msec) + , bucket_(max_tokens > 127 ? 127 : max_tokens) + , bucketMax_(max_tokens) + , needUpdate_(false) + , callback_(callback) + { + HASSERT(callback); + } + + /// Destructor. + ~LimitTimer() + { + cancel(); + } + + /// Attempts to take a token out of the bucket. Must be called from the + /// same executor that was passed in the object construction. + /// @return true if the take is successful, false if there are no available + /// tokens, in which case there will be a callback generated when + /// tokens become available. + bool try_take() + { + if (bucket_ == bucketMax_) + { + start(MSEC_TO_NSEC(updateDelayMsec_)); + } + if (bucket_ > 0) + { + --bucket_; + return true; + } + else + { + needUpdate_ = true; + return false; + } + } + + /// Takes one entry from the bucket, and does not give a callback if + /// there is no entry left. Must be called from the + /// same executor that was passed in the object construction. + void take_no_callback() + { + if (bucket_ > 0) + { + --bucket_; + } + } + +private: + /// Callback from the timer infrastructure. Called periodically. + long long timeout() override + { + ++bucket_; + if (needUpdate_) + { + callback_(); + needUpdate_ = false; + } + if (bucket_ >= bucketMax_) + { + return NONE; + } + else + { + return RESTART; + } + } + + /// cooldown delay in msec + uint16_t updateDelayMsec_; + + /// number of available tokens + uint8_t bucket_ ; + + /// maximum number of tokens in the bucket + uint8_t bucketMax_ : 7; + + /// if non-zero, wake up parent when token is available. + uint8_t needUpdate_ : 1; + + /// callback after cooldown period. + std::function callback_; +}; +