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

Link to this function add_recipe(state, arg, action) View Source
Link to this function add_recipes(state, arg, actions) View Source
Link to this function cancel_all_events(state) View Source
Link to this function cancel_event(state, name) View Source
Link to this function event_in_history?(state, event) View Source

Returns true if an event (e.g., {:event,:status}) occurred before.

Examples

iex> event_in_history?(state, {:new_event, :new_status}) false

Link to this function gather_all_events(state, event_in, event_out) View Source
Link to this function gather_events(state, events, event_start, event_out) View Source
Link to this function init_recipes(state, pid) View Source
init_recipes(map(), pid()) :: map()

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
Link to this function launch_event(state, arg, return \\ :no_return) View Source

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

Link to this function on_event(state, arg, return) View Source
Link to this function schedule_event(state, period, arg, options \\ []) View Source