To build:
/opt/zig-0.14/zig build
First of all - this is NOT so called hierarchical state machines (nested states and whatnots) implementation. It is much more convenient to deal with hierarchy of (relatively simple) interacting state machines rather than with a single huge machine having hierarchy of states.
This is epoll based implementation, so in essence events are:
EPOLLIN
(read()
/accept()
will not block)EPOLLOUT
(write()
will not block)EPOLLERR/EPOLLHUP/EPOLLRDHUP
Upon returning from epoll_wait()
these events are transformed into messages
and then delivered to destination state machine (owner of a channel, see below).
Besides messages triggered by 'external world', there are internal messages -
machines can send them to each other directly (see engine/architecture.txt
in the sources).
Event source is anything representable by file descriptor and thus can be used with epoll
facility:
- signals (
EPOLLIN
only) - timers (
EPOLLIN
only) - sockets, terminals, serial devices, fifoes etc.
- file system (via
inotify
facility)
Each event source has an owner (it is some state machine).
M0, M1, M2 ...
: internal messagesS0, S1, S2 ...
: signalsT0, T1, T2 ...
: timersDO, D1, D2
: i/o ('can read', 'can write', 'error')F0, F1, F2 ...
: file system ('writable file was closed' and alike)
These 'tags' are used in the names of state machines 'methods', for example:
fn workD2(me: *StageMachine, _: ?*StageMachine, _: ?*anyopaque) void {
var pd = utils.opaqPtrTo(me.data, *RxData);
me.msgTo(me, M0_IDLE, null);
me.msgTo(pd.customer, M2_FAIL, null);
}
Each state can have enter/leave functions, which can be used to perform some action
in addition to 'regular' actions. For example, RX
machine (see below) stops timeout timer
when leaving WORK
state:
fn workLeave(me: *StageMachine) void {
var pd = utils.opaqPtrTo(me.data, *RxData);
pd.tm0.disable(&me.md.eq) catch unreachable;
}
The server consists of 4 kinds of state machines:
- LISTENER (one instance)
- WORKER (many instances, kept in a pool when idle)
- RX/TX (many instances, also kept in pools)
Thus we have 3-level hierarchy here.
Listener is responsible for accepting incoming connections and also for managing resources, associated with connected client (memory and file descriptor). Has 2 states:
- INIT
- enter: prepare channels
M0
: gotoWORK
state- leave: nothing
- WORK
- enter: enable channels
D0
: accept connection, take WORKER from the pool, send itM1
with ptr to client as payloadM0
: close connection, free memoryS0
(SIGINT): stop event loopS1
(SIGTERM): stop event loop- leave: say goodbye
Worker is a machine which implements message flow pattern. Has 5 states:
- INIT
- enter: send
M0
to self M0
: gotoIDLE
state- leave: nothing
- enter: send
- IDLE
- enter: put self into the pool
M1
: store information about clientM0
: gotoRECV
state- leave: nothing
- RECV
- enter: get
RX
machine from pool, send itM1
with ptr to context M0
: gotoSEND
stateM1
: sendM0
to selfM2
: gotoFAIL
state- leave: nothing
- enter: get
- SEND
- enter: get
TX
machine from pool, send itM1
with ptr to context M0
: gotoRECV
stateM1
: sendM0
to selfM2
: gotoFAIL
state- leave: nothing
- enter: get
- FAIL
- enter: send
M0
to self,M0
toLISTENER
with ptr to client M0
: gotoIDLE
state- leave: nothing
- enter: send
Rx is a machine which knows how to read data. Has 3 states:
- INIT
- enter: init timer and i/o channels, send
M0
to self M0
: gotoIDLE
state- leave: nothing
- enter: init timer and i/o channels, send
- IDLE
- enter: put self into the pool
M0
: gotoWORK
stateM1
: store context given by requester- leave: nothing
- WORK
- enter: enable i/o and timer
D0
: read data, sendM1
to requester when doneD2
: sendM0
to self,M2
to requesterT0
(timeout): sendM0
to self,M2
to requesterM0
: gotoIDLE
state- leave: stop timer
Tx is a machine which knows how to write data. Also has 3 states:
- INIT
- enter: init i/o channel
M0
: gotoIDLE
state- leave: nothing
- IDLE
- enter: put self into the pool
M0
: gotoWORK
stateM1
: store context given by requester- leave: nothing
- WORK
- enter: enable i/o
D1
: write data, sendM1
to requester when doneD2
: sendM0
to self,M2
to requesterM0
: gotoIDLE
state- leave: nothing
- client connected (note
D0
)
LISTENER-1 @ WORK got 'D0' from OS
WORKER-4 @ IDLE got 'M1' from LISTENER-1
WORKER-4 @ IDLE got 'M0' from SELF
RX-4 @ IDLE got 'M1' from WORKER-4
RX-4 @ IDLE got 'M0' from SELF
- client suddenly disconnected (note
D2
)
RX-4 @ IDLE got 'M0' from SELF
RX-4 @ WORK got 'D2' from OS
RX-4 @ WORK got 'M0' from SELF
WORKER-4 @ RECV got 'M2' from RX-4
WORKER-4 @ FAIL got 'M0' from SELF
LISTENER-1 @ WORK got 'M0' from WORKER-4
- normal request-reply (note
D0
andD1
)
RX-4 @ IDLE got 'M0' from SELF
RX-4 @ WORK got 'D0' from OS
<<< 4 bytes: { 49, 50, 51, 10 }
RX-4 @ WORK got 'M0' from SELF
WORKER-4 @ RECV got 'M1' from RX-4
WORKER-4 @ RECV got 'M0' from SELF
TX-4 @ IDLE got 'M1' from WORKER-4
TX-4 @ IDLE got 'M0' from SELF
TX-4 @ WORK got 'D1' from OS
TX-4 @ WORK got 'M0' from SELF
WORKER-4 @ SEND got 'M1' from TX-4
WORKER-4 @ SEND got 'M0' from SELF
- request timeout (note
T0
)
RX-4 @ IDLE got 'M0' from SELF
RX-4 @ WORK got 'T0' from OS
RX-4 @ WORK got 'M0' from SELF
WORKER-4 @ RECV got 'M2' from RX-4
WORKER-4 @ FAIL got 'M0' from SELF
LISTENER-1 @ WORK got 'M0' from WORKER-4
The client also consists of 4 kinds of state machines:
- TERMINATOR (one instance)
- WORKER (many instances)
- RX/TX (many instances, in pools)
However here we have only 2-level machine hierarchy
because TERMINATOR
is stand-alone machine and it's
only purpose is catching SIGTERM
and SIGINT
.
- INIT
- enter: init channels, send 'M0' to self
M0
: gotoIDLE
state- leave: nothing
- IDLE
- enter: enable channels (
SIGINT
andSIGTERM
) S0
andS1
: stop event loop- leave: nothing
- enter: enable channels (
- INIT
- enter: init io and timer channels, send
M0
to self M0
: gotoCONN
state- leave: nothing
- enter: init io and timer channels, send
- CONN
- enter: take
TX
machine, start connect, sendM1
toTX
M1
: sendM0
to self (connection Ok)M2
: sendM3
to self (can not connect)M0
: gotoSEND
stateM3
: gotoWAIT
state- leave: nothing
- enter: take
- SEND
- enter: prepare request, take
TX
machine, send itM1
M1
: sendM0
to selfM2
: sendM3
to selfM0
: gotoRECV
stateM3
: gotoWAIT
state- leave: nothing
- enter: prepare request, take
- RECV
- enter: take
RX
machine, send itM1
M1
: sendM0
to selfM2
: sendM3
to selfM0
: gotoTWIX
stateM3
: gotoWAIT
state- leave: nothing
- enter: take
- TWIX
- enter: start timer (500 msec)
T0
: gotoSEND
state- leave: nothing
- WAIT
- enter: start timer (5000 msec)
T0
: gotoCONN
state- leave: nothing
- successful connection
TX-1 @ IDLE got 'M1' from WORKER-1
TX-1 @ IDLE got 'M0' from SELF
TX-1 @ WORK got 'D1' from OS
TX-1 @ WORK got 'M0' from SELF
WORKER-1 @ CONN got 'M1' from TX-1
WORKER-1 : connected to '127.0.0.1:3333'
WORKER-1 @ CONN got 'M0' from SELF
- failed connection
TX-1 @ IDLE got 'M1' from WORKER-1
TX-1 @ IDLE got 'M0' from SELF
TX-1 @ WORK got 'D2' from OS
TX-1 @ WORK got 'M0' from SELF
WORKER-1 @ CONN got 'M2' from TX-1
WORKER-1 : can not connect to '127.0.0.1:3333': error.ConnectionRefused
WORKER-1 @ CONN got 'M3' from SELF
- request-reply
TX-1 @ IDLE got 'M1' from WORKER-1
TX-1 @ IDLE got 'M0' from SELF
TX-1 @ WORK got 'D1' from OS
TX-1 @ WORK got 'M0' from SELF
WORKER-1 @ SEND got 'M1' from TX-1
WORKER-1 @ SEND got 'M0' from SELF
RX-1 @ IDLE got 'M1' from WORKER-1
RX-1 @ IDLE got 'M0' from SELF
RX-1 @ WORK got 'D0' from OS
RX-1 @ WORK got 'M0' from SELF
WORKER-1 @ RECV got 'M1' from RX-1
reply: WORKER-1-7