wangle::detail::FSM
authorHans Fugal <fugalh@fb.com>
Wed, 15 Oct 2014 16:32:03 +0000 (09:32 -0700)
committerdcsommer <dcsommer@fb.com>
Fri, 17 Oct 2014 18:42:35 +0000 (11:42 -0700)
Summary:
spaghetti-monster

Finagle uses a nifty state machine pattern in `Promise.scala`. Each state carries its data with it, then you switch statements with a simple state `cas`, within the functions (which are the transition inputs). See https://github.com/twitter/util/blob/master/util-core/src/main/scala/com/twitter/util/Promise.scala#L133-L139 and https://github.com/twitter/util/blob/master/util-core/src/main/scala/com/twitter/util/Promise.scala#L604-L635 for example.

I was thinking we could do something similar in C++, though we don't quite have the same luxury of type cases like scala. (Although as an aside I found this really cool paper on implementing type cases in C++, it's kind of evil but very cool. http://www.stroustrup.com/OOPSLA-typeswitch-draft.pdf)

I was looking at having a union of the different state classes, e.g.

union U {
State base;
Done done;
Waiting waiting;
...
};

and then you could use the memoized `dynamic_cast` trick in that paper to make a fairly efficient type case (and fairly readable, depending on how evil you want to get with the macros). However, `dynamic_cast<Done*>(&u.base)` blissfully segfaults. I'm not sure if this is something that should work and it's a compiler bug, or whether trying to (ab)use a union this way is against some arcane C++ rules. @hannesr suggested maybe a variant type might work. We can also do memory mangling on our own if it comes to it - however those are all more advanced techniques to play with at a later optimization time.

So instead, I went for a this-is-context helper base class. The mutable context is just `this`, and you inherit from `FSM`.

The magic macros make it more succint to lay out state transitions. See the tests for examples. Maybe in the future we can get really clever and find a way to generate state machine diagrams from code using this, especially when macros are being used.

Test Plan: unit tests were written and pass

Reviewed By: davejwatson@fb.com

Subscribers: meisner, trunkagent, net-systems@, fugalh, exa, njormrod, hannesr

FB internal diff: D1613497

folly/wangle/detail/FSM.h [new file with mode: 0644]
folly/wangle/test/FSM.cpp [new file with mode: 0644]

diff --git a/folly/wangle/detail/FSM.h b/folly/wangle/detail/FSM.h
new file mode 100644 (file)
index 0000000..96c6c28
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2014 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <atomic>
+#include <mutex>
+#include <folly/SmallLocks.h>
+
+namespace folly { namespace wangle { namespace detail {
+
+/// Finite State Machine helper base class.
+/// Inherit from this.
+/// For best results, use an "enum class" for Enum.
+template <class Enum>
+class FSM {
+private:
+  // I am not templatizing this because folly::MicroSpinLock needs to be
+  // zero-initialized (or call init) which isn't generic enough for something
+  // that behaves like std::mutex. :(
+  using Mutex = folly::MicroSpinLock;
+  Mutex mutex_ {0};
+
+  // This might not be necessary for all Enum types, e.g. anything
+  // that is atomically updated in practice on this CPU and there's no risk
+  // of returning a bogus state because of tearing.
+  // An optimization would be to use a static conditional on the Enum type.
+  std::atomic<Enum> state_;
+
+public:
+  FSM(Enum startState) : state_(startState) {}
+
+  Enum getState() const {
+    return state_.load(std::memory_order_relaxed);
+  }
+
+  // transition from state A to state B, and then perform action while the
+  // lock is still held.
+  //
+  // If the current state is not A, returns false.
+  template <class F>
+  bool updateState(Enum A, Enum B, F const& action) {
+    std::lock_guard<Mutex> lock(mutex_);
+    if (state_ != A) return false;
+    state_ = B;
+    action();
+    return true;
+  }
+};
+
+#define FSM_START \
+  retry: \
+    switch (getState()) {
+
+#define FSM_UPDATE2(a, b, action, unlocked_code) \
+    case a: \
+      if (!updateState((a), (b), (action))) goto retry; \
+      { unlocked_code ; } \
+      break;
+
+#define FSM_UPDATE(a, b, action) FSM_UPDATE2((a), (b), (action), {})
+
+#define FSM_END \
+    }
+
+
+}}}
diff --git a/folly/wangle/test/FSM.cpp b/folly/wangle/test/FSM.cpp
new file mode 100644 (file)
index 0000000..85fc5d0
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2014 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gtest/gtest.h>
+#include <folly/wangle/detail/FSM.h>
+
+using namespace folly::wangle::detail;
+
+enum class State { A, B };
+
+TEST(FSM, ctor) {
+  FSM<State> fsm(State::A);
+  EXPECT_EQ(State::A, fsm.getState());
+}
+
+TEST(FSM, update) {
+  FSM<State> fsm(State::A);
+  EXPECT_TRUE(fsm.updateState(State::A, State::B, []{}));
+  EXPECT_EQ(State::B, fsm.getState());
+}
+
+TEST(FSM, badUpdate) {
+  FSM<State> fsm(State::A);
+  EXPECT_FALSE(fsm.updateState(State::B, State::A, []{}));
+}
+
+TEST(FSM, actionOnUpdate) {
+  FSM<State> fsm(State::A);
+  size_t count = 0;
+  fsm.updateState(State::A, State::B, [&]{ count++; });
+  EXPECT_EQ(1, count);
+}
+
+TEST(FSM, noActionOnBadUpdate) {
+  FSM<State> fsm(State::A);
+  size_t count = 0;
+  fsm.updateState(State::B, State::A, [&]{ count++; });
+  EXPECT_EQ(0, count);
+}
+
+TEST(FSM, magicMacros) {
+  struct MyFSM : public FSM<State> {
+    size_t count = 0;
+    MyFSM() : FSM<State>(State::A) {}
+    void twiddle() {
+      FSM_START
+        FSM_UPDATE(State::A, State::B, [&]{ count++; });
+        FSM_UPDATE(State::B, State::A, [&]{ count--; });
+      FSM_END
+    }
+  };
+
+  MyFSM fsm;
+
+  fsm.twiddle();
+  EXPECT_EQ(State::B, fsm.getState());
+  EXPECT_EQ(1, fsm.count);
+
+  fsm.twiddle();
+  EXPECT_EQ(State::A, fsm.getState());
+  EXPECT_EQ(0, fsm.count);
+}
+
+TEST(FSM, magicMacros2) {
+  struct MyFSM : public FSM<State> {
+    size_t count = 0;
+    size_t count2 = 0;
+    MyFSM() : FSM<State>(State::A) {}
+    void twiddle() {
+      FSM_START
+        FSM_UPDATE2(State::A, State::B, [&]{ count++; }, count2++);
+        FSM_UPDATE2(State::B, State::A, [&]{ count--; }, count2--);
+      FSM_END
+    }
+  };
+
+  MyFSM fsm;
+
+  fsm.twiddle();
+  EXPECT_EQ(State::B, fsm.getState());
+  EXPECT_EQ(1, fsm.count);
+  EXPECT_EQ(1, fsm.count2);
+
+  fsm.twiddle();
+  EXPECT_EQ(State::A, fsm.getState());
+  EXPECT_EQ(0, fsm.count);
+  EXPECT_EQ(0, fsm.count2);
+}