Packages
livebook
0.9.3
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/runtime/erl_dist/node_manager.ex
defmodule Livebook.Runtime.ErlDist.NodeManager do
@moduledoc false
# The primary Livebook process started on a remote node.
#
# This process is responsible for initializing the node
# with necessary runtime configuration and then starting
# runtime server processes, one per runtime.
# This approach allows for multiple runtimes connected
# to the same node, while preserving the necessary
# cleanup semantics.
#
# The manager process terminates as soon as the last runtime
# server terminates. Upon termination the manager reverts the
# runtime configuration back to the initial state.
use GenServer
alias Livebook.Runtime.ErlDist
@name __MODULE__
@doc """
Starts the node manager.
## Options
* `:unload_modules_on_termination` - whether to unload all
Livebook related modules from the node on termination.
Defaults to `true`
* `:anonymous` - configures whether manager should
be registered under a global name or not.
In most cases we enforce a single manager per node
and identify it by a name, but this can be opted-out
from by using this option. Defaults to `false`
* `:auto_termination` - whether to terminate the manager
when the last runtime server terminates. Defaults to `true`
* `:parent_node` - indicates which node spawned the node manager.
It is used to disconnect the node when the server terminates,
which happens when the last session using the node disconnects.
Defaults to `nil`
* `:capture_orphan_logs` - whether to capture logs out of Livebook
evaluator's scope. Defaults to `true`
"""
def start(opts \\ []) do
{opts, gen_opts} = split_opts(opts)
GenServer.start(__MODULE__, opts, gen_opts)
end
@doc """
Starts the node manager with link.
See `start/1` for available options.
"""
def start_link(opts \\ []) do
{opts, gen_opts} = split_opts(opts)
GenServer.start_link(__MODULE__, opts, gen_opts)
end
defp split_opts(opts) do
{anonymous?, opts} = Keyword.pop(opts, :anonymous, false)
gen_opts = [
name: if(anonymous?, do: nil, else: @name)
]
{opts, gen_opts}
end
@doc """
Starts a new `Livebook.Runtime.ErlDist.RuntimeServer` for evaluation.
"""
@spec start_runtime_server(node() | pid(), keyword()) :: pid()
def start_runtime_server(node_or_pid, opts \\ []) do
GenServer.call(server(node_or_pid), {:start_runtime_server, opts})
end
defp server(pid) when is_pid(pid), do: pid
defp server(node) when is_atom(node), do: {@name, node}
@impl true
def init(opts) do
unload_modules_on_termination = Keyword.get(opts, :unload_modules_on_termination, true)
auto_termination = Keyword.get(opts, :auto_termination, true)
parent_node = Keyword.get(opts, :parent_node)
capture_orphan_logs = Keyword.get(opts, :capture_orphan_logs, true)
## Initialize the node
Process.flag(:trap_exit, true)
{:ok, server_supervisor} = DynamicSupervisor.start_link(strategy: :one_for_one)
# Register our own standard error IO device that proxies
# to sender's group leader.
original_standard_error = Process.whereis(:standard_error)
{:ok, io_forward_gl_pid} = ErlDist.IOForwardGL.start_link()
Process.unregister(:standard_error)
Process.register(io_forward_gl_pid, :standard_error)
Logger.add_backend(Livebook.Runtime.ErlDist.LoggerGLBackend)
# Set `ignore_module_conflict` only for the NodeManager lifetime.
initial_ignore_module_conflict = Code.compiler_options()[:ignore_module_conflict]
Code.compiler_options(ignore_module_conflict: true)
initial_ansi_syntax_colors = Application.get_env(:elixir, :ansi_syntax_colors)
Application.put_env(
:elixir,
:ansi_syntax_colors,
Livebook.Runtime.Evaluator.DefaultFormatter.syntax_colors()
)
tmp_dir = make_tmp_dir()
if ebin_path = ebin_path(tmp_dir) do
File.mkdir_p!(ebin_path)
Code.prepend_path(ebin_path)
end
{:ok,
%{
unload_modules_on_termination: unload_modules_on_termination,
auto_termination: auto_termination,
server_supervisor: server_supervisor,
runtime_servers: [],
initial_ignore_module_conflict: initial_ignore_module_conflict,
initial_ansi_syntax_colors: initial_ansi_syntax_colors,
original_standard_error: original_standard_error,
parent_node: parent_node,
capture_orphan_logs: capture_orphan_logs,
tmp_dir: tmp_dir
}}
end
@impl true
def terminate(_reason, state) do
Code.compiler_options(ignore_module_conflict: state.initial_ignore_module_conflict)
if ansi_syntax_colors = state.initial_ansi_syntax_colors do
Application.put_env(:elixir, :ansi_syntax_colors, ansi_syntax_colors)
else
Application.delete_env(:elixir, :ansi_syntax_colors)
end
Process.unregister(:standard_error)
Process.register(state.original_standard_error, :standard_error)
Logger.remove_backend(Livebook.Runtime.ErlDist.LoggerGLBackend)
if state.unload_modules_on_termination do
ErlDist.unload_required_modules()
end
if state.parent_node do
Node.disconnect(state.parent_node)
end
if ebin_path = ebin_path(state.tmp_dir) do
Code.delete_path(ebin_path)
end
if tmp_dir = state.tmp_dir do
File.rm_rf!(tmp_dir)
end
:ok
end
@impl true
def handle_info({:DOWN, _, :process, pid, _}, state) do
if pid in state.runtime_servers do
case update_in(state.runtime_servers, &List.delete(&1, pid)) do
%{runtime_servers: [], auto_termination: true} = state ->
{:stop, :shutdown, state}
state ->
{:noreply, state}
end
else
{:noreply, state}
end
end
def handle_info({:orphan_log, _output} = message, state) do
if state.capture_orphan_logs do
for pid <- state.runtime_servers, do: send(pid, message)
end
{:noreply, state}
end
def handle_info(_message, state), do: {:noreply, state}
@impl true
def handle_call({:start_runtime_server, opts}, _from, state) do
opts =
opts
|> Keyword.put_new(:ebin_path, ebin_path(state.tmp_dir))
|> Keyword.put_new(:tmp_dir, child_tmp_dir(state.tmp_dir))
|> Keyword.put_new(:base_path_env, System.get_env("PATH", ""))
{:ok, server_pid} =
DynamicSupervisor.start_child(state.server_supervisor, {ErlDist.RuntimeServer, opts})
Process.monitor(server_pid)
state = update_in(state.runtime_servers, &[server_pid | &1])
{:reply, server_pid, state}
end
defp make_tmp_dir() do
path = Path.join([System.tmp_dir!(), "livebook_runtime", random_id()])
if File.mkdir_p(path) == :ok do
path
end
end
defp ebin_path(nil), do: nil
defp ebin_path(tmp_dir), do: Path.join(tmp_dir, "ebin")
defp child_tmp_dir(nil), do: nil
defp child_tmp_dir(tmp_dir), do: Path.join(tmp_dir, random_id())
defp random_id() do
:crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower)
end
end