View Source Bonfire.Epics (Bonfire v0.9.10-classic-beta.156)

Epics are a extensible way of structuring tasks.

This library is designed to provide a structured way to define and execute complex workflows in Elixir applications. It introduces the concept of "Epics" and "Acts" to organize and run sequences of operations.

Key components and concepts:

  • Bonfire.Epics.Epic: An Epic represents a complete workflow or process. It's a container that holds a sequence of Acts to be executed, along with state information, errors, and assigned values.
  • Bonfire.Epics.Act: An Act is an individual step or operation within an Epic. Each Act is typically a module that implements a specific task or functionality.
  • Execution Flow & Parallel Execution: Epics are executed by running their Acts in sequence. The library provides mechanisms to define, modify, and run these sequences. The library supports running multiple Acts in parallel for improved performance in certain scenarios.
  • Shared State: An Epic can maintain state throughout its execution using the 'assigns' map, allowing data to be passed between Acts.
  • Configurable: Epics can be defined in configuration, including at runtime, making it easy to set up and modify workflows without changing code.
  • Database Transactions: See the Bonfire.Ecto library for helpers to queue changeset operations within Acts and then run them all together in a single transaction: https://github.com/bonfire-networks/bonfire_ecto
  • Error Handling: The library includes built-in error handling, allowing errors to be captured and associated with specific Acts within an Epic.

This library is particularly useful for applications that need to manage complex, multi-step tasks with error handling and state management. It provides a flexible and extensible way to define, configure, and execute these processes, making it easier to maintain and modify complex workflows.

How it works

  1. Each Act is implemented as a module with a run/2 function that performs a specific task.
  2. Users define an Epic, either in code or configuration, as sequences of Acts.
  3. When the Epic is run, it executes each Act in sequence (or with some Acts optionally running in parallel), maintaining state and handling errors along the way. Acts can update the Epic's state, adding errors, and assigning values that can be used by subsequent Acts.
  4. After all Acts are executed, the final state of the Epic is returned, including any errors or assigned values.

1. How to write an Act

Write a module with a run/2 function that takes an Epic and an Act, performs a specific task, and returns an Epic.

defmodule Bonfire.Label.Acts.LabelObject do
  @moduledoc """
  Takes an object and label and returns a changeset for labeling that object. 
  Implements `Bonfire.Epics.Act`.

  Epic Options:
    * `:current_user` - user that will create the page, required.

  Act Options:
    * `:as` - key to where we find the label(s) to add, and then assign changeset to, default: `:label`.
    * `:object` (configurable) - id to use for the thing to label
    * `:attrs` - epic options key to find the attributes at, default: `:attrs`.
  """

  use Arrows
  import Bonfire.Epics

  @doc false
  def run(epic, act) do
    current_user = Bonfire.Common.Utils.current_user(epic.assigns[:options])

    cond do
      epic.errors != [] ->
        maybe_debug(
          epic,
          act,
          length(epic.errors),
          "Skipping due to epic errors"
        )

        epic

      not (is_struct(current_user) or is_binary(current_user)) ->
        maybe_debug(
          epic,
          act,
          current_user,
          "Skipping due to missing current_user"
        )

        epic

      true ->
        as = Keyword.get(act.options, :as) || Keyword.get(act.options, :on, :label)
        object_key = Keyword.get(act.options, :object, :object)

        label = Keyword.get(epic.assigns[:options], as, [])
        object = Keyword.get(epic.assigns[:options], object_key, nil)

        Bonfire.Label.Labelling.label_object(label, object,
          return: :changeset,
          current_user: current_user
        )
        |> Map.put(:action, :insert)
        |> Bonfire.Epics.Epic.assign(epic, as, ...)
        |> Bonfire.Ecto.Acts.Work.add(:label)
    end
  end
end

2. How to define an Epic

Simple Epic where each Act executes sequentially

     @page_act_opts [on: :page, attrs: :page_attrs]

     config :bonfire_pages, Bonfire.Pages,
      epics: [
        create: [
          # Create a changeset for insertion
          {Bonfire.Pages.Acts.Page.Create, @page_act_opts},
          # with a sanitised body and tags extracted,
          {Bonfire.Social.Acts.PostContents, @page_act_opts},
          # a caretaker,
          {Bonfire.Me.Acts.Caretaker, @page_act_opts},
          # and a creator,
          {Bonfire.Me.Acts.Creator, @page_act_opts},
          # and possibly fetch contents of URLs,
          {Bonfire.Files.Acts.URLPreviews, @page_act_opts},
          # possibly with uploaded files,
          {Bonfire.Files.Acts.AttachMedia, @page_act_opts},
          # with extracted tags fully hooked up,
          {Bonfire.Tag.Acts.Tag, @page_act_opts},
          # and the appropriate boundaries established,
          {Bonfire.Boundaries.Acts.SetBoundaries, @page_act_opts},

          # Now we open a Postgres transaction and actually do the insertions in DB
          Bonfire.Ecto.Acts.Begin,
          # Run our inserts
          Bonfire.Ecto.Acts.Work,
          Bonfire.Ecto.Acts.Commit,

          # Enqueue for indexing by meilisearch
          {Bonfire.Search.Acts.Queue, @page_act_opts}
        ]
      ]

Advanced Epic, where some Acts execute in parallel

    config :bonfire_posts, Bonfire.Posts,
      epics: [
        publish: [
          # Create a changeset for insertion
          Bonfire.Posts.Acts.Posts.Publish,
          # These next 3 Acts are run in parallel
          [
            # with a sanitised body and tags extracted,
            {Bonfire.Social.Acts.PostContents, on: :post},

            # assign a caretaker,
            {Bonfire.Me.Acts.Caretaker, on: :post},

            # record the creator,
            {Bonfire.Me.Acts.Creator, on: :post}
          ],
          # These next 4 Acts are run in parallel (they run after the previous 3 because they depend on the outputs of those Acts)
          [
            # possibly fetch contents of URLs,
            {Bonfire.Files.Acts.URLPreviews, on: :post},

            # possibly occurring in a thread,
            {Bonfire.Social.Acts.Threaded, on: :post},

            # with extracted tags/mentions fully hooked up,
            {Bonfire.Tag.Acts.Tag, on: :post},

            # maybe set as sensitive,
            {Bonfire.Social.Acts.Sensitivity, on: :post}
          ],
          # These next 3 Acts are run in parallel (they run after the previous 4 because they depend on the outputs of those Acts)
          [
            # possibly with uploaded/linked media (optionally depends on URLPreviews),
            {Bonfire.Files.Acts.AttachMedia, on: :post},

            # with appropriate boundaries established (depends on Threaded),
            {Bonfire.Boundaries.Acts.SetBoundaries, on: :post},

            # summarised by an activity (possibly appearing in feeds),
            {Bonfire.Social.Acts.Activity, on: :post}
          ],

          # Now we open a Postgres transaction and actually do the insertions in DB
          Bonfire.Ecto.Acts.Begin,
          # Run our inserts
          Bonfire.Ecto.Acts.Work,
          Bonfire.Ecto.Acts.Commit,

          # Preload data & Publish live feed updates via (in-memory) PubSub
          {Bonfire.Social.Acts.LivePush, on: :post},

          # These steps are run in parallel
          [
            # Enqueue for indexing by meilisearch
            {Bonfire.Search.Acts.Queue, on: :post},

            # Prepare JSON for federation and add to queue (oban).
            {Bonfire.Social.Acts.Federate, on: :post}
          ],

          # Once the activity/object exists (depends on federation being done)
          {Bonfire.Tags.Acts.AutoBoost, on: :post}
        ]
      ]

3. How to run an Epic

Bonfire.Epics.run_epic(Bonfire.Posts, :publish, on: :post)

Copyright (c) 2022 Bonfire Contributors

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.

Summary

Functions

Runs a series of Bonfire.Epics.Epic operations based on configured Acts for this module.

Like debug, but will omit fully outputting the inspectable thing and still print the message if only :debug is set

Functions

Link to this macro

maybe_debug(act_or_epic, thing, label \\ nil)

View Source (macro)
Link to this macro

maybe_debug(epic, act, thing, label)

View Source (macro)
Link to this function

run_epic(config_key, type, options \\ [])

View Source

Runs a series of Bonfire.Epics.Epic operations based on configured Acts for this module.

Parameters

  • config_key: The config key from which to load the Epic definition, such as a module name.
  • type: The type of epic operation to run.
  • options: Options for the epic operation, including :on for the key in the epic assigns to return on success (default to :result).

Returns

{:ok, result} on success, {:error, reason} on failure.

Examples

# runs an Epic defined in config at `config :bonfire_posts, Bonfire.Posts, epics: [publish: [...]]`
iex> Bonfire.Epics.run_epic(Bonfire.Posts, :publish, [on: :post])
{:ok, %{}}
Link to this macro

smart(epic, act, thing, label \\ "")

View Source (macro)

Like debug, but will omit fully outputting the inspectable thing and still print the message if only :debug is set