]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[5674] Implemented state model pausing.
authorMarcin Siodelski <marcin@isc.org>
Wed, 18 Jul 2018 14:17:05 +0000 (16:17 +0200)
committerMarcin Siodelski <marcin@isc.org>
Wed, 18 Jul 2018 14:17:05 +0000 (16:17 +0200)
src/lib/util/state_model.cc
src/lib/util/state_model.h
src/lib/util/tests/state_model_unittest.cc

index dc647b1e1f61aac38e17b550c085d8710cc0244e..38ac9d73f1ff9b7179b55339d35c47ca668f306b 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2018 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
@@ -14,8 +14,10 @@ namespace util {
 
 /********************************** State *******************************/
 
-State::State(const int value, const std::string& label, StateHandler handler)
-        : LabeledValue(value, label), handler_(handler) {
+State::State(const int value, const std::string& label, StateHandler handler,
+             const StatePausing& state_pausing)
+    : LabeledValue(value, label), handler_(handler), pausing_(state_pausing),
+      was_paused_(false) {
 }
 
 State::~State() {
@@ -26,6 +28,16 @@ State::run() {
         (handler_)();
 }
 
+bool
+State::shouldPause() {
+    if ((pausing_ == STATE_PAUSE_ALWAYS) ||
+        ((pausing_ == STATE_PAUSE_ONCE) && (!was_paused_))) {
+        was_paused_ = true;
+        return (true);
+    }
+    return (false);
+}
+
 /********************************** StateSet *******************************/
 
 StateSet::StateSet() {
@@ -35,9 +47,11 @@ StateSet::~StateSet() {
 }
 
 void
-StateSet::add(const int value, const std::string& label, StateHandler handler) {
+StateSet::add(const int value, const std::string& label, StateHandler handler,
+              const StatePausing& state_pausing) {
     try {
-        LabeledValueSet::add(LabeledValuePtr(new State(value, label, handler)));
+        LabeledValueSet::add(LabeledValuePtr(new State(value, label, handler,
+                                                       state_pausing)));
     } catch (const std::exception& ex) {
         isc_throw(StateModelError, "StateSet: cannot add state :" << ex.what());
     }
@@ -74,9 +88,10 @@ const int StateModel::FAIL_EVT;
 const int StateModel::SM_DERIVED_EVENT_MIN;
 
 StateModel::StateModel() : events_(), states_(), dictionaries_initted_(false),
-                          curr_state_(NEW_ST), prev_state_(NEW_ST),
-                          last_event_(NOP_EVT), next_event_(NOP_EVT),
-                          on_entry_flag_(false), on_exit_flag_(false) {
+                           curr_state_(NEW_ST), prev_state_(NEW_ST),
+                           last_event_(NOP_EVT), next_event_(NOP_EVT),
+                           on_entry_flag_(false), on_exit_flag_(false),
+                           paused_(false) {
 }
 
 StateModel::~StateModel(){
@@ -177,7 +192,7 @@ StateModel::getEvent(unsigned int event_value) {
 
 void
 StateModel::defineState(unsigned int state_value, const std::string& label,
-    StateHandler handler) {
+                        StateHandler handler, const StatePausing& state_pausing) {
     if (!isModelNew()) {
         // Don't allow for self-modifying maps.
         isc_throw(StateModelError, "States may only be added to a new model."
@@ -186,7 +201,7 @@ StateModel::defineState(unsigned int state_value, const std::string& label,
 
     // Attempt to add the state to the set.
     try {
-        states_.add(state_value, label, handler);
+        states_.add(state_value, label, handler, state_pausing);
     } catch (const std::exception& ex) {
         isc_throw(StateModelError, "Error adding state: " << ex.what());
     }
@@ -248,6 +263,11 @@ StateModel::endModel() {
     transition(END_ST, END_EVT);
 }
 
+void
+StateModel::unpauseModel() {
+    paused_ = false;
+}
+
 void
 StateModel::abortModel(const std::string& explanation) {
     transition(END_ST, FAIL_EVT);
@@ -272,6 +292,12 @@ StateModel::setState(unsigned int state) {
 
     // At this time they are calculated the same way.
     on_exit_flag_ = on_entry_flag_;
+
+    // If we're entering the new state we need to see if we should
+    // pause the state model in this state.
+    if (on_entry_flag_ && !paused_ && (getState(state)->shouldPause())) {
+        paused_ = true;
+    }
 }
 
 void
@@ -345,6 +371,11 @@ StateModel::didModelFail() const {
     return (isModelDone() && (next_event_ == FAIL_EVT));
 }
 
+bool
+StateModel::isModelPaused() const {
+    return (paused_);
+}
+
 std::string
 StateModel::getStateLabel(const int state) const {
     return (states_.getLabel(state));
index 938be57227c7f4d26e0e1d1f50047ad15183ea71..0299dbaebc423dc2472c364e3781bd81b06f2e3e 100644 (file)
@@ -35,12 +35,28 @@ typedef LabeledValuePtr EventPtr;
 /// @brief Defines a pointer to an instance method for handling a state.
 typedef boost::function<void()> StateHandler;
 
+/// @brief State machine pausing modes.
+///
+/// Supported modes are:
+/// - always pause in the given state,
+/// - never pause in the given state,
+/// - pause upon first transition to the given state.
+enum StatePausing {
+    STATE_PAUSE_ALWAYS,
+    STATE_PAUSE_NEVER,
+    STATE_PAUSE_ONCE
+};
+
 /// @brief Defines a State within the State Model.
 ///
 /// This class provides the means to define a state within a set or dictionary
 /// of states, and assign the state an handler method to execute the state's
 /// actions.  It derives from LabeledValue which allows a set of states to be
 /// keyed by integer constants.
+///
+/// Because a state model can be paused in selected states, this class also
+/// provides the means for specifying a pausing mode and for checking whether
+/// the state model should be paused when entering this state.
 class State : public LabeledValue {
 public:
     /// @brief Constructor
@@ -49,6 +65,8 @@ public:
     /// @param label is the text label to assign to the state
     /// @param handler is the bound instance method which handles the state's
     /// action.
+    /// @param state_pausing pausing mode selected for the given state. The
+    /// default value is @c STATE_PAUSE_NEVER.
     ///
     /// A typical invocation might look this:
     ///
@@ -58,7 +76,8 @@ public:
     /// @endcode
     ///
     /// @throw StateModelError if label is null or blank.
-    State(const int value, const std::string& label, StateHandler handler);
+    State(const int value, const std::string& label, StateHandler handler,
+          const StatePausing& state_pausing = STATE_PAUSE_NEVER);
 
     /// @brief Destructor
     virtual ~State();
@@ -66,9 +85,25 @@ public:
     /// @brief Invokes the State's handler.
     void run();
 
+    /// @brief Indicates if the state model should pause upon entering
+    /// this state.
+    ///
+    /// It modifies the @c was_paused_ flag if the state model should
+    /// pause. That way, it keeps track of visits in this particular state,
+    /// making it possible to pause only upon the first transition to the
+    /// state when @c STATE_PAUSE_ONCE mode is used.
+    bool shouldPause();
+
 private:
     /// @brief Bound instance method pointer to the state's handler method.
     StateHandler handler_;
+
+    /// @brief Specifies selected pausing mode for a state.
+    StatePausing pausing_;
+
+    /// @brief Indicates if the state machine was already paused in this
+    /// state.
+    bool was_paused_;
 };
 
 /// @brief Defines a shared pointer to a State.
@@ -92,10 +127,12 @@ public:
     /// @param value is the numeric value of the state
     /// @param label is the text label to assign to the state
     /// @param handler is the bound instance method which handles the state's
+    /// @param state_pausing state pausing mode for the given state.
     ///
     /// @throw StateModelError if the value is already defined in the set, or
     /// if the label is null or blank.
-    void add(const int value, const std::string& label, StateHandler handler);
+    void add(const int value, const std::string& label, StateHandler handler,
+             const StatePausing& state_pausing);
 
     /// @brief Fetches a state for the given value.
     ///
@@ -223,6 +260,14 @@ public:
 /// which transitions the model to END_ST with END_EVT.  Bringing the model to
 /// an abnormal end is done via the abortModel method, which transitions the
 /// model to END_ST with FAILED_EVT.
+///
+/// The model can be paused in the selected states. The states in which the
+/// state model should pause (always or only once) are determined within the
+/// @c StateModel::defineStates method. The state handlers can check whether
+/// the state machine is paused or not by calling @c StateModel::isModelPaused
+/// and act accordingy. Typically, the state handler would simply post the
+/// @c NOP_EVT when it finds that the state model is paused. The model
+/// remains paused until @c StateModel::unpauseModel is called.
 class StateModel {
 public:
 
@@ -307,6 +352,9 @@ public:
     /// handler should call endModel.
     void endModel();
 
+    /// @brief Unpauses state model.
+    void unpauseModel();
+
     /// @brief An empty state handler.
     ///
     /// This method is primarily used to permit special states, NEW_ST and
@@ -421,11 +469,14 @@ protected:
     /// exceptions.
     /// @param handler is the bound instance method which implements the state's
     /// actions.
+    /// @param state_pausing pausing mode selected for the given state. The
+    /// default value is @c STATE_PAUSE_NEVER.
     ///
     /// @throw StateModelError if the model has already been started, if
     /// the value is already defined, or if the label is empty.
     void defineState(unsigned int value, const std::string& label,
-                     StateHandler handler);
+                     StateHandler handler,
+                     const StatePausing& state_pausing = STATE_PAUSE_NEVER);
 
     /// @brief Fetches the state referred to by value.
     ///
@@ -593,6 +644,11 @@ public:
     /// @return Boolean true if the model has reached the END_ST.
     bool isModelDone() const;
 
+    /// @brief Returns whether or not the model is paused.
+    ///
+    /// @return Boolean true if the model is paused, false otherwise.
+    bool isModelPaused() const;
+
     /// @brief Returns whether or not the model failed.
     ///
     /// @return Boolean true if the model has reached the END_ST and the last
@@ -662,6 +718,9 @@ private:
 
     /// @brief Indicates if state exit logic should be executed.
     bool on_exit_flag_;
+
+    /// @brief Indicates if the state model is paused.
+    bool paused_;
 };
 
 /// @brief Defines a pointer to a StateModel.
index c6b2b6733952f6078a87bc5d828b29b5888e506e..b8b261150944d3cc564f1b4d00355225dcfd39b5 100644 (file)
@@ -40,6 +40,12 @@ public:
     ///@brief State which finishes off processing.
     static const int DONE_ST = SM_DERIVED_STATE_MIN + 4;
 
+    ///@brief State in which model is always paused.
+    static const int PAUSE_ALWAYS_ST = SM_DERIVED_STATE_MIN + 5;
+
+    ///@brief State in which model is paused at most once.
+    static const int PAUSE_ONCE_ST = SM_DERIVED_STATE_MIN + 6;
+
     // StateModelTest events
     ///@brief Event used to trigger initiation of asynchronous work.
     static const int WORK_START_EVT = SM_DERIVED_EVENT_MIN + 1;
@@ -56,6 +62,9 @@ public:
     ///@brief Event used to trigger an attempt to transition to bad state
     static const int SIMULATE_ERROR_EVT = SM_DERIVED_EVENT_MIN + 5;
 
+    ///@brief Event used to indicate that state machine is unpaused.
+    static const int UNPAUSED_EVT = SM_DERIVED_EVENT_MIN + 6;
+
     /// @brief Constructor
     ///
     /// Parameters match those needed by StateModel.
@@ -159,6 +168,11 @@ public:
         }
     }
 
+    /// @brief State handler for PAUSE_ALWAYS_ST and PAUSE_ONCE_ST.
+    void pauseHandler() {
+        postNextEvent(NOP_EVT);
+    }
+
     /// @brief Construct the event dictionary.
     virtual void defineEvents() {
         // Invoke the base call implementation first.
@@ -170,6 +184,7 @@ public:
         defineEvent(ALL_DONE_EVT, "ALL_DONE_EVT");
         defineEvent(FORCE_UNDEFINED_ST_EVT, "FORCE_UNDEFINED_ST_EVT");
         defineEvent(SIMULATE_ERROR_EVT, "SIMULATE_ERROR_EVT");
+        defineEvent(UNPAUSED_EVT, "UNPAUSED_EVT");
     }
 
     /// @brief Verify the event dictionary.
@@ -183,6 +198,7 @@ public:
         getEvent(ALL_DONE_EVT);
         getEvent(FORCE_UNDEFINED_ST_EVT);
         getEvent(SIMULATE_ERROR_EVT);
+        getEvent(UNPAUSED_EVT);
     }
 
     /// @brief Construct the state dictionary.
@@ -202,6 +218,14 @@ public:
 
         defineState(DONE_ST, "DONE_ST",
             boost::bind(&StateModelTest::doneWorkHandler, this));
+
+        defineState(PAUSE_ALWAYS_ST, "PAUSE_ALWAYS_ST",
+            boost::bind(&StateModelTest::pauseHandler, this),
+                    STATE_PAUSE_ALWAYS);
+
+        defineState(PAUSE_ONCE_ST, "PAUSE_ONCE_ST",
+            boost::bind(&StateModelTest::pauseHandler, this),
+                    STATE_PAUSE_ONCE);
     }
 
     /// @brief Verify the state dictionary.
@@ -214,6 +238,8 @@ public:
         getState(READY_ST);
         getState(DO_WORK_ST);
         getState(DONE_ST);
+        getState(PAUSE_ALWAYS_ST);
+        getState(PAUSE_ONCE_ST);
     }
 
     /// @brief  Manually construct the event and state dictionaries.
@@ -279,6 +305,8 @@ const int StateModelTest::DONE_ST;
 const int StateModelTest::WORK_START_EVT;
 const int StateModelTest::WORK_DONE_EVT;
 const int StateModelTest::ALL_DONE_EVT;
+const int StateModelTest::PAUSE_ALWAYS_ST;
+const int StateModelTest::PAUSE_ONCE_ST;
 
 /// @brief Checks the fundamentals of defining and retrieving events.
 TEST_F(StateModelTest, eventDefinition) {
@@ -830,4 +858,61 @@ TEST_F(StateModelTest, stateModelTest) {
     EXPECT_TRUE(getWorkCompleted());
 }
 
+// This test verifies the pausing and un-pausing capabilities of the state
+// model.
+TEST_F(StateModelTest, stateModelPause) {
+    // Verify that status methods are correct: model is new.
+    EXPECT_TRUE(isModelNew());
+    EXPECT_FALSE(isModelRunning());
+    EXPECT_FALSE(isModelWaiting());
+    EXPECT_FALSE(isModelDone());
+    EXPECT_FALSE(isModelPaused());
+
+    // Verify that the failure explanation is empty and work is not done.
+    EXPECT_TRUE(getFailureExplanation().empty());
+    EXPECT_FALSE(getWorkCompleted());
+
+    // Transition straight to the state in which the model should always
+    // pause.
+    ASSERT_NO_THROW(startModel(PAUSE_ALWAYS_ST));
+
+    // Verify it was successful and that the model is paused.
+    EXPECT_EQ(PAUSE_ALWAYS_ST, getCurrState());
+    EXPECT_TRUE(isModelPaused());
+
+    // Run the model again. It should still be paused.
+    ASSERT_NO_THROW(runModel(NOP_EVT));
+    EXPECT_TRUE(isModelPaused());
+
+    // Unpause the model and transition to the state in which the model
+    // should be paused at most once.
+    unpauseModel();
+    transition(PAUSE_ONCE_ST, NOP_EVT);
+    EXPECT_EQ(PAUSE_ONCE_ST, getCurrState());
+    EXPECT_TRUE(isModelPaused());
+
+    // The model should still be paused until explicitly unpaused.
+    ASSERT_NO_THROW(runModel(NOP_EVT));
+    EXPECT_EQ(PAUSE_ONCE_ST, getCurrState());
+    EXPECT_TRUE(isModelPaused());
+
+    unpauseModel();
+
+    // Transition back to the first state. The model should pause again.
+    transition(PAUSE_ALWAYS_ST, NOP_EVT);
+    EXPECT_EQ(PAUSE_ALWAYS_ST, getCurrState());
+    EXPECT_TRUE(isModelPaused());
+
+    ASSERT_NO_THROW(runModel(NOP_EVT));
+    EXPECT_EQ(PAUSE_ALWAYS_ST, getCurrState());
+    EXPECT_TRUE(isModelPaused());
+
+    // Unpause the model and transition to the state in which the model
+    // should pause only once. This time it should not pause.
+    unpauseModel();
+    transition(PAUSE_ONCE_ST, NOP_EVT);
+    EXPECT_EQ(PAUSE_ONCE_ST, getCurrState());
+    EXPECT_FALSE(isModelPaused());
+}
+
 }