GenCycle v0.1.1 GenCycle View Source
An event-driven task manager to design GenServer lifecycles.
The recipes model applies to any process that wants to execute independent tasks (in parallel) without becoming cluttered with task logic and having a state machine of executed tasks.
Recipes are wrappers for simple actions to be performed in parallel.
Each action runs in a supervised task and recipes can be pipelined via events.
Processes like GenServer can import the RecipeManager
to implement the recipes model.
Creating a Recipe
Recipes lie in lib/recipes
. To start create a new file there, e.g., trace_recipe.ex
.
First step is to tell it to follow the GenCycle.RecipeBehavior
:
defmodule GenCycle.Recipe.MyRecipe do
@moduledoc false
@behaviour GenCycle.RecipeBehavior
end
If you check the RecipeBehavior
you will see it requires the following callback function:
@callback start(state, event, message, return) :: :noevent | {:event, event, message} | {:event, event, message, return}
So lets implement it:
defmodule GenCycle.Recipe.MyRecipe do
@moduledoc false
require Logger
@behaviour GenCycle.RecipeBehavior
def start(_state, _event, _message, _return) do
Logger.info "Invoked MyRecipe.start"
:noevent #return this if there isnt another recipe waiting for this to conclude
end
end
Note the four parameters received:
- state - copy of the owner’s state when this got invoked.
- event - event that triggered the start method (e.g.,
:event
) - message - event message that triggered the start method (e.g.,
:done
) - return - if the triggering event passed a return value, then it is passed to start via the return parameter. Otherwise the parameter is set as :no_return. We will discuss this later.
Now we have a recipe ready to be registered. Recipes run whenever the event/message combination they are registered to is triggered. In this case we want them to run whenever an app is put foreground.
Note that putting an app in the foreground is in itself a recipe.
Unlike our MyRecipe
, the ForegroundRecipe
launches an event upon conclusion.
To do so it returns {:event, :foreground, :done}
.
This is how recipes are pipelined between them, via returned events.
So lets register MyRecipe
to be launched on this event in the virtual_device.init1
of VirtualDevice
:
state = state |> add_recipe(:foreground, :done, Patata.Recipe.MyRecipe)
Done! Now whenever an app is put on foreground, MyRecipe
executes.
Recipe Events
With our recipe created and added via add_recipe3
(or add_recipes3
), if we want to launch it ourselves we can do it by launching the event they expect.
For the previous example it could be achieved by:
state |> launch_event({:foreground, :done})
In the case of a timed event, one could do:
state |> schedule_event(time_in_ms, {:foreground, :done})
Occasionally one might want to cancel an event, to do so scheduled events can be named. E.g.:
state |> schedule_event(time_in_ms, {:foreground, :done}, [name: foreground_event_name])
And canceled using their event name:
state |> cancel_event(foreground_event_name)
Pipelining recipes
As we have seen, recipes can wait on one another by generating their own events. E.g., by returning:
{:event, :your-event, :your-message}
and registering the recipe on that event and message/status:
state |> add_recipe(:your-event, :your-message, Patata.Recipe.YourRecipe)
Additionally state can also be passed with the events by returning four fields instead:
{:event, :your-event, :your-message, :your-state}
Two important notes:
- Both the event and message need to be matchable, i.e., something constant, either an atom or a string.
- Recipes’ start method is executed in its own task and its state and return parameters are copies and therefore any change to them is not propagated.
- Do not generate atoms automatically since these are not garbage collected. Try to reuse atoms.
Waiting for recipes
In many cases you might want to wait for a few recipes to conclude before launching a new event/recipe.
To do this we have introduced gather_events4
:
data_events = [ # List of events to be matched
{:event_one, :message_one},
{:event_two, :message_two}
]
state
|> gather_events(
data_events, # List of events to be matched
{:event_start, :message_start}, # Starts matching events from this event
{:event_out, :message_out}) # On matching all events launches this event
This function waits on the completion of group of recipes by listening to their returning events (as specified by data_events
).
It only considering events since the occurrence of a starting event (event_start
) to support repeating gathers over time.
On successfully matching all the events it launches a completion event that can be used to start other recipes.
Internally, the gather_events4
function registers a listener on all the awaited events (data_events
). Whenever one of these events occurs, a specialized recipe (GatherRecipe
) checks for the occurrence of the starting event (event_start
) in the event history and tries to match the awaited events in the subsequent events in history.
Important note:
- If your gather depends on recipes that might fail it is up to you to make sure that either the matching events occur nonetheless, otherwise the gather will never output its event.
Link to this section Summary
Functions
Returns true if an event (e.g., {:event,:status}) occurred before
Method to be invoked with GenServer state in init1, it adds the required state to run GenCycle
Launches an {event, status} that can trigger an existing recipe. If your recipe does not require any state invoke launch_event2, otherwise launch_event3 can be called, where the third argument is the state to be passed to the recipe
Link to this section Functions
Returns true if an event (e.g., {:event,:status}) occurred before.
Examples
iex> event_in_history?(state, {:new_event, :new_status}) false
Method to be invoked with GenServer state in init1, it adds the required state to run GenCycle.
Examples
def init(:ok) do
{:ok, init_recipes(%{})}
end
Launches an {event, status} that can trigger an existing recipe. If your recipe does not require any state invoke launch_event2, otherwise launch_event3 can be called, where the third argument is the state to be passed to the recipe.
Examples
def handle_cast({:launch_recipe}, state) do {:noreply, launch_event(state, {:new_event, :new_status}) end