FLAME Framework
repository·main·Indexed 22 days ago
https://github.com/phoenixframework/flameA framework for running modular parts of an Elixir application on short-lived, elastic infrastructure. FLAME treats the entire application as a 'lambda', allowing developers to offload heavy tasks to temporary copies of the app with full access to the environment and database. It provides interfaces for synchronous calls (FLAME.call/3), asynchronous casts (FLAME.cast/3), and remote child processes (FLAME.place_child/3), with built-in backends for local development and Fly.io.
What's inside FLAME
- FLAME (Fleeting Lambda Application for Modular Execution) is a pattern where you treat your entire application as a lambda. Instead of just running a function, you run a modular part of your application inside a temporary, short-lived copy of the app on elastic infrastructure. This allows you to scale specific, heavy workloads (like video processing or heavy computations) without scaling your entire main application cluster.
Configure a FLAME Pool
mainTo enable elastic scaling, start a
FLAME.Poolin your application's supervision tree (typically inlib/my_app/application.ex). The pool manages scaling runners up and down and monitors remote resources.Common configuration options include:
name: The name of the pool.backend: The backend used for provisioning (e.g.,FLAME.FlyBackend).min: Minimum number of runners.max: Maximum number of runners.max_concurrency: Maximum concurrent executions.idle_shutdown_after: Time in milliseconds after which an idle runner is shut down.log: Logging level.
children = [ {FLAME.Pool, name: MyApp.SamplePool, backend: FLAME.FlyBackend, min: 0, max: 10, max_concurrency: 5, idle_shutdown_after: 30_000, log: :debug} ]Install FLAME
mainAdd
:flameto your project's dependencies in yourmix.exsfile.Note for Erlang/OTP 26 and earlier: You must also include
:jason(version>= 0.0.0) as a dependency for compatibility.defp deps do [ # For Erlang/OTP 26 and earlier, you also need Jason # {:jason, ">= 0.0.0"}, {:flame, "~> 0.5"} ] endManage remote nodes with FLAME.Runner
mainA
FLAME.Runneris responsible for booting a new node and executing concurrent functions on it. It is typically managed via aFLAME.Poolof runners. When a caller exits or crashes, the remote node is automatically terminated (e.g., viaFLAME.Terminatorwhen usingFLAME.FlyBackend).To use a runner, you typically follow this lifecycle:
- Start the runner with a specific backend.
- Boot the remote node.
- Execute functions using
call/3orcast/3(thoughcall/3is the primary entry point for synchronous execution). - Shutdown the runner when finished.
{:ok, runner} = Runner.start_link(backend: FLAME.FlyBackend) :ok = Runner.remote_boot(runner) Runner.call(runner, fn -> :operation1 end) Runner.shutdown(runner)How FLAME Pools manage concurrency and scaling
mainFLAME Pools are designed for elastic scaling based on demand and concurrency limits:
- Scaling Up: When a
callorplace_childis made and no runners have available capacity (up to:max_concurrency), the pool checks if it can grow. If the current runner count is less than:max, it initiates an asynchronous boot of a new runner. - Concurrency Control: Each runner has a
:max_concurrencylimit. If all runners are busy, new requests are queued in aWaitingStatequeue. - Scaling Down: Runners that become idle for a period exceeding
:idle_shutdown_afterare shut down. - Scale to Zero: By setting
:minto0, the pool can shut down all runners when there is no demand, saving resources. - Resource Tracking: If
:track_resourcesis enabled, the pool monitors returned results for any data implementing theFLAME.Trackableprotocol. This ensures the remote node does not terminate while these resources are still in use.
- Scaling Up: When a
Handle remote node lifecycle messages in a FLAME backend
mainWhen a remote node is booted, a
FLAME.Terminatorprocess starts automatically on that node. This process communicates lifecycle events back to the parent node via thehandle_info/2callback in your backend implementation.Your backend can react to these specific message patterns:
Node Booted: When the remote terminator starts, it sends:
{ref, {:remote_up, remote_terminator_pid}}Note:refis the reference generated by the backend and encoded into theFLAME.Parent.encode/1string.Graceful Shutdown: When the remote terminator is shutting down gracefully, it sends:
{ref, {:remote_shutdown, :idle}}
Implementing
handle_info/2allows your backend to manage the state of provisioned instances as they come online or go offline.@impl true def handle_info({ref, {:remote_up, terminator_pid}}, state) do # Handle the new remote node being ready {:noreply, %{state | remote_nodes: Map.put(state.remote_nodes, ref, terminator_pid)}} end @impl true def handle_info({ref, {:remote_shutdown, :idle}}, state) do # Handle the remote node shutting down {:noreply, %{state | remote_nodes: Map.delete(state.remote_nodes, ref)}} endUse FLAME.LocalBackend for development and testing
mainThe
FLAME.LocalBackendis a implementation of theFLAME.Backendbehaviour designed for local execution on the current node. It is primarily intended for development and testing environments where you do not need to distribute tasks to remote servers.Configuration
You can configure the backend using the
init/1function. It accepts anoptskeyword list and can also be configured via the application environment using the:flameapplication with theFLAME.LocalBackendmodule as the key.Required Options:
:terminator_sup: A supervisor (typically aDynamicSupervisor) used to manage the lifecycle of terminators created duringremote_boot/1.
# Example configuration via Application environment Application.put_env(:flame, FLAME.LocalBackend, [terminator_sup: MySupervisor]) # Or passing options directly to init/1 # (Note: init/1 is called internally by the Flame supervisor/manager)Track resources with FLAME.Trackable
mainIf you allocate long-lived resources in a FLAME node, the node might terminate before the resource is finished. By using the:track_resourcesoption inFLAME.call/3, FLAME will traverse the returned result looking for data that implements theFLAME.Trackableprotocol. FLAME will then ensure the node does not terminate until all tracked PIDs have terminated.Conditionally start services in FLAME runners
mainBecause FLAME nodes clone your entire application, you may want to disable certain services (like Phoenix endpoints or large database connection pools) when the node is running as a FLAME child rather than the main application node. Use
FLAME.Parent.get/0to detect this state.Example: Conditional Endpoint
def start(_type, _args) do flame_parent = FLAME.Parent.get() children = [ ..., {FLAME.Pool, name: Thumbs.FFMpegRunner, ...}, !flame_parent && ThumbsWeb.Endpoint ] |> Enum.filter(& &1) Supervisor.start_link(children, [strategy: :one_for_one, name: Thumbs.Supervisor]) endExample: Conditional Database Pool Size
pool_size = if FLAME.Parent.get() do 1 else String.to_integer(System.get_env("POOL_SIZE") || "10") end config :thumbs, Thumbs.Repo, pool_size: pool_sizeFLAME execution interfaces
mainFLAME provides three primary interfaces for elastically scaled operations:
FLAME.call/3: Used for synchronous calls where you wait for the result.FLAME.cast/3: Used for asynchronous casts where you do not need to wait for the results.FLAME.place_child/3: Used to place a child specification to run in a remote instance, serving as a replacement for standard supervision calls likeDynamicSupervisor.start_child/2orTask.Supervisor.start_child/2.
Execute code in a temporary app instance with FLAME.call/3
mainUse
FLAME.call/3to execute a block of code in a temporary copy of your entire application. This is ideal for CPU-intensive or resource-heavy tasks.Because the code runs in a full copy of your application, you have access to your entire environment, including database connections and all application modules. FLAME automatically handles variables that the function closes over.
def generate_thumbnails(%Video{} = vid, interval) do FLAME.call(MyApp.FFMpegRunner, fn -> # I'm runner on a short-lived, temporary server tmp_dir = Path.join(System.tmp_dir!(), Ecto.UUID.generate()) File.mkdir!(tmp_dir) System.cmd("ffmpeg", ~w(-i #{vid.url} -vf fps=1/#{interval} #{tmp_dir}/%02d.png)) urls = VideoStore.put_thumbnails(vid, Path.wildcard(tmp_dir <> "/*.png")) Repo.insert_all(Thumbnail, Enum.map(urls, &%{video_id: vid.id, url: &1})) end) endConfigure FLAME shutdown timeout
mainFLAME includes a termination process that allows remote functions time to complete before the node is destroyed. The default shutdown timeout is 30 seconds. You can adjust this in your application configuration (e.g.,
config/runtime.exs):config :flame, :terminator, shutdown_timeout: :timer.seconds(10)