Packages
livebook
0.15.1
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/apps/deployer.ex
defmodule Livebook.Apps.Deployer do
# Deploys apps on the given node.
#
# Each node runs a single deployer, which deploys apps sequentially.
# This design makes sure that only one app warmup is going to run
# at a time, rather than concurrently, which reduces memory usage
# and prevents race conditions with `Mix.install/2`.
use GenServer
require Logger
alias Livebook.App
alias Livebook.Apps
@doc """
Starts a new deployer process.
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(_opts) do
GenServer.start_link(__MODULE__, {})
end
@doc """
Asynchronously requests an app deployment.
If there is no app process under the requested slug, it is started.
Otherwise the notebook is deployed as a new version into the existing
app.
This function automatically starts monitoring the deployer process
and returns the monitor reference.
When the deployment finished the caller receives a message of the
form `{:deploy_result, ref, result}`, with the same monitor reference
and the result of `Apps.deploy/2`. The caller should then
demonitor the reference.
## Options
* `:start_only` - when `true`, deploys only if the app does not
exist already. If the app does exist, the deployment finishes
with an error. Defaults to `false`
* `:permanent` - whether the app is deployed as permanent. Defaults
to `false`
* `:deployed_by` - the user performing the deployment
"""
@spec deploy_monitor(pid(), Apps.AppSpec.t(), keyword()) :: reference()
def deploy_monitor(pid, app_spec, opts \\ []) do
opts = Keyword.validate!(opts, start_only: false, permanent: false, deployed_by: nil)
ref = Process.monitor(pid)
GenServer.cast(
pid,
{:deploy, app_spec, opts[:start_only], opts[:permanent], opts[:deployed_by], self(), ref}
)
ref
end
@doc """
Returns a list of all deployers present in the cluster.
"""
@spec list_deployers() :: list(pid())
def list_deployers() do
:pg.get_members(Apps.Deployer.PG, Apps.Deployer)
end
@doc """
Returns a node-local deployer.
"""
@spec local_deployer(node()) :: pid()
def local_deployer(node \\ node()) do
list_deployers() |> Enum.find(&(node(&1) == node)) || raise "no local deployer running"
end
@impl true
def init({}) do
:pg.join(Apps.Deployer.PG, __MODULE__, self())
{:ok, %{}}
end
@impl true
def handle_cast({:deploy, app_spec, start_only, permanent, deployed_by, from, ref}, state) do
Logger.info("[app=#{app_spec.slug}] Deploying app")
files_tmp_path = Apps.generate_files_tmp_path(app_spec.slug)
result =
with {:ok, %{notebook: notebook, warnings: warnings}} <-
Apps.AppSpec.load(app_spec, files_tmp_path) do
if Apps.AppSpec.should_warmup?(app_spec) do
with {:error, message} <- Apps.warmup_app(notebook, files_tmp_path) do
Logger.warning("[app=#{app_spec.slug}] App warmup failed, #{message}")
end
end
deployment_bundle = %{
notebook: notebook,
files_tmp_path: files_tmp_path,
app_spec: app_spec,
permanent: permanent,
warnings: warnings,
deployed_by: deployed_by
}
name = Apps.global_name(app_spec.slug)
opts = [start_only: start_only]
with {:error, error} <- start_or_redeploy(name, deployment_bundle, opts) do
File.rm_rf(files_tmp_path)
{:error, error}
end
end
send(from, {:deploy_result, ref, result})
{:noreply, state}
end
defp start_or_redeploy(name, deployment_bundle, opts) do
case :global.whereis_name(name) do
:undefined ->
opts = [deployment_bundle: deployment_bundle]
case DynamicSupervisor.start_child(Livebook.AppSupervisor, {App, opts}) do
{:ok, pid} ->
{:ok, pid}
{:error, :already_started} ->
# We could use a global transaction to prevent the unlikely
# case of multiple nodes starting the app simultaneously.
# However, the global registration can still fail if the
# node joins the cluster while the app process is starting.
# So we handle both cases the same way in this branch.
start_or_redeploy(name, deployment_bundle, opts)
{:error, reason} ->
{:error, "app process failed to start, reason: #{inspect(reason)}"}
end
pid ->
redeploy_app(pid, deployment_bundle, opts)
end
end
defp redeploy_app(pid, deployment_bundle, opts) do
cond do
opts[:start_only] ->
{:error, :already_started}
node(pid) != node() ->
# This is relevant specifically when :files_source points to
# a local directory, but we generally expect redeployments to
# happen within the same node, so it's good to enforce this
{:error, "cannot deploy a new notebook to an app running on a different node"}
true ->
App.deploy(pid, deployment_bundle)
{:ok, pid}
end
end
end