Commit 8e1a61fa authored by Hans Fugal's avatar Hans Fugal Committed by dcsommer

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<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
parent ff25b950
/*
* 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 \
}
}}}
/*
* 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);
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment