From: Hans Fugal Date: Wed, 15 Oct 2014 16:32:03 +0000 (-0700) Subject: wangle::detail::FSM X-Git-Tag: v0.22.0~280 X-Git-Url: http://plrg.eecs.uci.edu/git/?a=commitdiff_plain;h=8e1a61fac79b35d7fbcb18a82af2d6307850f889;p=folly.git wangle::detail::FSM 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(&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 --- diff --git a/folly/wangle/detail/FSM.h b/folly/wangle/detail/FSM.h new file mode 100644 index 00000000..96c6c280 --- /dev/null +++ b/folly/wangle/detail/FSM.h @@ -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 +#include +#include + +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 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 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 + bool updateState(Enum A, Enum B, F const& action) { + std::lock_guard 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 index 00000000..85fc5d05 --- /dev/null +++ b/folly/wangle/test/FSM.cpp @@ -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 +#include + +using namespace folly::wangle::detail; + +enum class State { A, B }; + +TEST(FSM, ctor) { + FSM fsm(State::A); + EXPECT_EQ(State::A, fsm.getState()); +} + +TEST(FSM, update) { + FSM fsm(State::A); + EXPECT_TRUE(fsm.updateState(State::A, State::B, []{})); + EXPECT_EQ(State::B, fsm.getState()); +} + +TEST(FSM, badUpdate) { + FSM fsm(State::A); + EXPECT_FALSE(fsm.updateState(State::B, State::A, []{})); +} + +TEST(FSM, actionOnUpdate) { + FSM fsm(State::A); + size_t count = 0; + fsm.updateState(State::A, State::B, [&]{ count++; }); + EXPECT_EQ(1, count); +} + +TEST(FSM, noActionOnBadUpdate) { + FSM 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 { + size_t count = 0; + MyFSM() : FSM(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 { + size_t count = 0; + size_t count2 = 0; + MyFSM() : FSM(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); +}