Skip to content

Commit f9284a6

Browse files
Fix persistency flaw when loading (#60)
* Test the problem * Fix test * mf * Fix tests * write cleaner * fix tests * Fix wrong arities
1 parent a2585f4 commit f9284a6

File tree

3 files changed

+40
-10
lines changed

3 files changed

+40
-10
lines changed

examples/ecto_integration/test/ecto_integration_test.exs

+25-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule EctoIntegration.Test do
55
alias EctoIntegration.Data.{Post, Post.EventLog}
66
alias EctoIntegration.Repo
77

8-
setup_all do
8+
setup do
99
uuid = Ecto.UUID.generate()
1010
post = Post.create(%{id: uuid, title: "Post 1", body: "Body 1"})
1111

@@ -91,4 +91,28 @@ defmodule EctoIntegration.Test do
9191
}
9292
] = post.(uuid).event_log |> Enum.sort()
9393
end
94+
95+
test "loading a persisted struct starts the FSM with correct current state", setup_attrs do
96+
%{uuid: uuid, post: _post} = setup_attrs
97+
post = fn uuid -> Post |> Repo.get(uuid) |> Repo.preload(:event_log) end
98+
state = &Finitomata.state/1
99+
100+
# update data in database directly
101+
{:ok, published_post} =
102+
post.(uuid)
103+
|> Ecto.Changeset.cast(%{state: :published}, [:state])
104+
|> Repo.update()
105+
106+
assert published_post.state == :published
107+
108+
# kill the FSM process
109+
pid = Finitomata.fqn(nil, uuid) |> GenServer.whereis()
110+
assert is_pid(pid)
111+
assert Process.exit(pid, :kill)
112+
Process.sleep(100)
113+
114+
# re-fetch data, which will start the FSM
115+
assert post.(uuid).state == :published
116+
assert state.(uuid).current == :published
117+
end
94118
end

lib/finitomata.ex

+13-7
Original file line numberDiff line numberDiff line change
@@ -803,13 +803,15 @@ defmodule Finitomata do
803803
:ignore -> {lifecycle, payload}
804804
end
805805

806-
state = %State{
807-
name: name,
808-
lifecycle: lifecycle,
809-
persistency: Map.get(init_arg, :persistency, nil),
810-
timer: @__config__[:timer],
811-
payload: payload
812-
}
806+
state =
807+
%State{
808+
name: name,
809+
lifecycle: lifecycle,
810+
persistency: Map.get(init_arg, :persistency, nil),
811+
timer: @__config__[:timer],
812+
payload: payload
813+
}
814+
|> put_current_state_if_loaded(lifecycle, payload)
813815

814816
:persistent_term.put({Finitomata, state.name}, state.payload)
815817

@@ -822,6 +824,10 @@ defmodule Finitomata do
822824
{:ok, state, {:continue, {:transition, event_payload({@__config__[:entry], nil})}}}
823825
end
824826

827+
defp put_current_state_if_loaded(state, lifecycle, payload) do
828+
if lifecycle == :loaded, do: Map.put(state, :current, payload.state), else: state
829+
end
830+
825831
@doc false
826832
@impl GenServer
827833
def handle_call(:state, _from, state), do: {:reply, state, state}

stuff/fsm.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ I’m a big fan of [Finite Automata](https://en.wikipedia.org/wiki/Finite-state_
1616

1717
This library leverages the power of _callbacks_ to not only completely cover the _FSM_ implementation, but also provide a compile-time proof the _FSM_ is valid and functional. One of the most important things this library provides is the _FSM_ description itself, that is fault-tolerant, not error-prone, and easy to grasp. The _FSM_ definition, which is currently supported in both [PlantUML](https://plantuml.com/en/state-diagram) and [Mermaid](https://mermaid.live) syntaxes, would be drawn in the generated docs of the project using this library.
1818

19-
The consumer of this library initiates a transition by calling somewhat like `transition(object, event)`, then the `GenServer` does its magic and the callback `on_transition/4` gets called. From inside this callback, the consumer implements the business logic and returns the result (the next state to move the _FSM_ to.) There are also syntactic sugar callbacks `on_enter/2`, `on_exit/2`, `on_failure/2`, and `on_terminate/1` to allow easy state change reactions, handling of errors, and a final cleanup respectively.
19+
The consumer of this library initiates a transition by calling somewhat like `transition(object, event)`, then the `GenServer` does its magic and the callback `on_transition/4` gets called. From inside this callback, the consumer implements the business logic and returns the result (the next state to move the _FSM_ to.) There are also syntactic sugar callbacks `on_enter/2`, `on_exit/2`, `on_failure/3`, and `on_terminate/1` to allow easy state change reactions, handling of errors, and a final cleanup respectively.
2020

2121
All the callbacks do have a default implementation, which would perfectly handle transitions having a single to state and not requiring any additional business logic attached. When needed, this might be turned off.
2222

@@ -26,7 +26,7 @@ Upon start, _FSM_ moves to its _initial state_ and sits there awaiting for the t
2626

2727
This library has a compilation-time guarantee the _FSM_ is valid, e. g. has the only one begin state, has at least one end state, all states can be reached, _and_ all the necessary callbacks are defined. That said, if we an _FSM_ has an event initiating transitions from the same state to two different states, and there is no `on_transition/4` clause covering that case, the compile-time error would be raised. On the other hand, if the transition is predetermined and might lead to the only one state, the callback implementation is not mandatory, because there is no trolley problem between these two states.
2828

29-
The _FSM_ definition allows event names, terminated with bangs and/or question marks. If the event name is terminated with a bang (`init!`,) **and this event is the only one possible from this state,** the event will be called automatically once _FSM_ enters this state. This is handy for moving through initialization or through states which do not require a consumer intervention and might be done immediately after _FSM_ reaches the respective state. If the transition failed in any way (the state has not been left either due to `{:error, any()}` response received from `on_transition/4` _or_ due to other unexpected issue, like if `on_transition/4` raised,) the `on_failure/2` callback would be called and the warning would be printed to the log. To suppress this behaviour and to allow a transition silently fail, the event should have ended with a question mark (`try_call?`.) The event cannot have both a bang and a question mark in its name.
29+
The _FSM_ definition allows event names, terminated with bangs and/or question marks. If the event name is terminated with a bang (`init!`,) **and this event is the only one possible from this state,** the event will be called automatically once _FSM_ enters this state. This is handy for moving through initialization or through states which do not require a consumer intervention and might be done immediately after _FSM_ reaches the respective state. If the transition failed in any way (the state has not been left either due to `{:error, any()}` response received from `on_transition/4` _or_ due to other unexpected issue, like if `on_transition/4` raised,) the `on_failure/3` callback would be called and the warning would be printed to the log. To suppress this behaviour and to allow a transition silently fail, the event should have ended with a question mark (`try_call?`.) The event cannot have both a bang and a question mark in its name.
3030

3131
## Wiki Example
3232

0 commit comments

Comments
 (0)