mirror of
https://github.com/livebook-dev/livebook.git
synced 2024-09-20 10:05:57 +08:00
Introduce Fly.io runtime (#2708)
This commit is contained in:
parent
b7ca4d6135
commit
c5ba8f8f81
|
@ -1,5 +1,5 @@
|
||||||
[
|
[
|
||||||
import_deps: [:phoenix, :ecto],
|
import_deps: [:phoenix, :ecto],
|
||||||
plugins: [Phoenix.LiveView.HTMLFormatter],
|
plugins: [Phoenix.LiveView.HTMLFormatter],
|
||||||
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}"]
|
inputs: ["*.{heex,ex,exs}", "{config,lib,test}/**/*.{heex,ex,exs}", "rel/*/overlays/**/*.exs"]
|
||||||
]
|
]
|
||||||
|
|
36
.github/workflows/test.yml
vendored
36
.github/workflows/test.yml
vendored
|
@ -10,6 +10,7 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
MIX_ENV: test
|
MIX_ENV: test
|
||||||
|
ELIXIR_ERL_OPTIONS: "-epmd_module Elixir.Livebook.EPMD"
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
@ -59,41 +60,6 @@ jobs:
|
||||||
- name: Run assets tests
|
- name: Run assets tests
|
||||||
run: npm test --prefix assets
|
run: npm test --prefix assets
|
||||||
|
|
||||||
epmdless:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.event_name == 'push'
|
|
||||||
env:
|
|
||||||
MIX_ENV: test
|
|
||||||
LIVEBOOK_EPMDLESS: true
|
|
||||||
ELIXIR_ERL_OPTIONS: "-epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0"
|
|
||||||
steps:
|
|
||||||
- name: Checkout git repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: Read ./versions
|
|
||||||
run: |
|
|
||||||
. versions
|
|
||||||
echo "elixir=$elixir" >> $GITHUB_ENV
|
|
||||||
echo "otp=$otp" >> $GITHUB_ENV
|
|
||||||
echo "openssl=$openssl" >> $GITHUB_ENV
|
|
||||||
- name: Install Erlang & Elixir
|
|
||||||
uses: erlef/setup-beam@v1
|
|
||||||
with:
|
|
||||||
otp-version: ${{ env.otp }}
|
|
||||||
elixir-version: ${{ env.elixir }}
|
|
||||||
- name: Cache Mix
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
deps
|
|
||||||
_build
|
|
||||||
key: ${{ runner.os }}-mix-${{ env.elixir }}-${{ env.otp }}-${{ hashFiles('**/mix.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-mix-${{ env.elixir }}-${{ env.otp }}-
|
|
||||||
- name: Install mix dependencies
|
|
||||||
run: mix deps.get
|
|
||||||
- name: Run tests
|
|
||||||
run: mix test
|
|
||||||
|
|
||||||
windows:
|
windows:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -33,6 +33,3 @@ npm-debug.log
|
||||||
|
|
||||||
# The built Escript
|
# The built Escript
|
||||||
/livebook
|
/livebook
|
||||||
|
|
||||||
# The priv directory with the EPMD file
|
|
||||||
/priv/epmd
|
|
||||||
|
|
|
@ -217,12 +217,9 @@ The following environment variables can be used to configure Livebook on boot:
|
||||||
|
|
||||||
* `LIVEBOOK_DEFAULT_RUNTIME` - sets the runtime type that is used by default
|
* `LIVEBOOK_DEFAULT_RUNTIME` - sets the runtime type that is used by default
|
||||||
when none is started explicitly for the given notebook. Must be either
|
when none is started explicitly for the given notebook. Must be either
|
||||||
"standalone" (Elixir standalone), "attached:NODE:COOKIE" (Attached node)
|
"standalone" (Standalone), "attached:NODE:COOKIE" (Attached node)
|
||||||
or "embedded" (Embedded). Defaults to "standalone".
|
or "embedded" (Embedded). Defaults to "standalone".
|
||||||
|
|
||||||
* `LIVEBOOK_EPMDLESS` - if set to "true", it disables the usage of EPMD. This is
|
|
||||||
only supported within releases and defaults to true for the Desktop app.
|
|
||||||
|
|
||||||
* `LIVEBOOK_FIPS` - if set to "true", it enables the FIPS mode on startup.
|
* `LIVEBOOK_FIPS` - if set to "true", it enables the FIPS mode on startup.
|
||||||
See more details in [the documentation](https://hexdocs.pm/livebook/fips.html).
|
See more details in [the documentation](https://hexdocs.pm/livebook/fips.html).
|
||||||
|
|
||||||
|
|
|
@ -61,6 +61,8 @@ export function registerGlobalEventHandlers() {
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("lb:scroll_into_view", (event) => {
|
window.addEventListener("lb:scroll_into_view", (event) => {
|
||||||
|
const options = event.detail || {};
|
||||||
|
|
||||||
// If the element is going to be shown, we want to wait for that
|
// If the element is going to be shown, we want to wait for that
|
||||||
waitUntilVisible(event.target).then(() => {
|
waitUntilVisible(event.target).then(() => {
|
||||||
scrollIntoView(event.target, {
|
scrollIntoView(event.target, {
|
||||||
|
@ -68,6 +70,7 @@ export function registerGlobalEventHandlers() {
|
||||||
behavior: "smooth",
|
behavior: "smooth",
|
||||||
block: "nearest",
|
block: "nearest",
|
||||||
inline: "nearest",
|
inline: "nearest",
|
||||||
|
...options,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -30,7 +30,6 @@ config :livebook,
|
||||||
app_service_url: nil,
|
app_service_url: nil,
|
||||||
authentication: :token,
|
authentication: :token,
|
||||||
aws_credentials: false,
|
aws_credentials: false,
|
||||||
epmdless: false,
|
|
||||||
feature_flags: [],
|
feature_flags: [],
|
||||||
force_ssl_host: nil,
|
force_ssl_host: nil,
|
||||||
learn_notebooks: [],
|
learn_notebooks: [],
|
||||||
|
|
|
@ -149,22 +149,19 @@ defmodule Livebook do
|
||||||
config :livebook, :aws_credentials, true
|
config :livebook, :aws_credentials, true
|
||||||
end
|
end
|
||||||
|
|
||||||
if Livebook.Config.boolean!("LIVEBOOK_EPMDLESS", false) do
|
|
||||||
config :livebook, :epmdless, true
|
|
||||||
end
|
|
||||||
|
|
||||||
config :livebook,
|
config :livebook,
|
||||||
:default_runtime,
|
:default_runtime,
|
||||||
Livebook.Config.default_runtime!("LIVEBOOK_DEFAULT_RUNTIME") ||
|
Livebook.Config.default_runtime!("LIVEBOOK_DEFAULT_RUNTIME") ||
|
||||||
Livebook.Runtime.ElixirStandalone.new()
|
Livebook.Runtime.Standalone.new()
|
||||||
|
|
||||||
config :livebook, :default_app_runtime, Livebook.Runtime.ElixirStandalone.new()
|
config :livebook, :default_app_runtime, Livebook.Runtime.Standalone.new()
|
||||||
|
|
||||||
config :livebook,
|
config :livebook,
|
||||||
:runtime_modules,
|
:runtime_modules,
|
||||||
[
|
[
|
||||||
Livebook.Runtime.ElixirStandalone,
|
Livebook.Runtime.Standalone,
|
||||||
Livebook.Runtime.Attached
|
Livebook.Runtime.Attached,
|
||||||
|
Livebook.Runtime.Fly
|
||||||
]
|
]
|
||||||
|
|
||||||
if home = Livebook.Config.writable_dir!("LIVEBOOK_HOME") do
|
if home = Livebook.Config.writable_dir!("LIVEBOOK_HOME") do
|
||||||
|
|
|
@ -7,14 +7,8 @@ defmodule Livebook.Application do
|
||||||
ensure_directories!()
|
ensure_directories!()
|
||||||
set_local_file_system!()
|
set_local_file_system!()
|
||||||
|
|
||||||
if Livebook.Config.epmdless?() do
|
validate_epmd_module!()
|
||||||
validate_epmdless!()
|
start_distribution!()
|
||||||
ensure_distribution!()
|
|
||||||
else
|
|
||||||
ensure_epmd!()
|
|
||||||
ensure_distribution!()
|
|
||||||
end
|
|
||||||
|
|
||||||
set_cookie()
|
set_cookie()
|
||||||
|
|
||||||
children =
|
children =
|
||||||
|
@ -48,6 +42,8 @@ defmodule Livebook.Application do
|
||||||
Livebook.EPMD.NodePool,
|
Livebook.EPMD.NodePool,
|
||||||
# Start the server responsible for associating files with sessions
|
# Start the server responsible for associating files with sessions
|
||||||
Livebook.Session.FileGuard,
|
Livebook.Session.FileGuard,
|
||||||
|
# Start the supervisor dynamically managing runtimes
|
||||||
|
{DynamicSupervisor, name: Livebook.RuntimeSupervisor, strategy: :one_for_one},
|
||||||
# Start the supervisor dynamically managing sessions
|
# Start the supervisor dynamically managing sessions
|
||||||
{DynamicSupervisor, name: Livebook.SessionSupervisor, strategy: :one_for_one},
|
{DynamicSupervisor, name: Livebook.SessionSupervisor, strategy: :one_for_one},
|
||||||
# Start the registry for managing unique connections
|
# Start the registry for managing unique connections
|
||||||
|
@ -124,45 +120,22 @@ defmodule Livebook.Application do
|
||||||
:persistent_term.put(:livebook_local_file_system, local_file_system)
|
:persistent_term.put(:livebook_local_file_system, local_file_system)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_epmdless!() do
|
defp validate_epmd_module!() do
|
||||||
with {:ok, [[~c"Elixir.Livebook.EPMD"]]} <- :init.get_argument(:epmd_module),
|
# We use a custom EPMD module. In releases and Escript, we make
|
||||||
{:ok, [[~c"false"]]} <- :init.get_argument(:start_epmd),
|
# sure the necessary erl flags are set. When running from source,
|
||||||
{:ok, [[~c"0"]]} <- :init.get_argument(:erl_epmd_port) do
|
# those need to be passed explicitly.
|
||||||
:ok
|
case :init.get_argument(:epmd_module) do
|
||||||
else
|
{:ok, [[~c"Elixir.Livebook.EPMD"]]} ->
|
||||||
_ ->
|
|
||||||
Livebook.Config.abort!("""
|
|
||||||
You must specify ELIXIR_ERL_OPTIONS=\"-epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0\" with LIVEBOOK_EPMDLESS. \
|
|
||||||
The epmd module can be found inside #{Application.app_dir(:livebook, "priv/ebin")}.
|
|
||||||
""")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ensure_epmd!() do
|
|
||||||
unless Node.alive?() do
|
|
||||||
case System.cmd("epmd", ["-daemon"]) do
|
|
||||||
{_, 0} ->
|
|
||||||
:ok
|
:ok
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
Livebook.Config.abort!("""
|
Livebook.Config.abort!("""
|
||||||
Could not start epmd (Erlang Port Mapper Daemon). Livebook uses epmd to \
|
You must set the environment variable ELIXIR_ERL_OPTIONS="-epmd_module Elixir.Livebook.EPMD"
|
||||||
talk to different runtimes. You may have to start epmd explicitly by calling:
|
|
||||||
|
|
||||||
epmd -daemon
|
|
||||||
|
|
||||||
Or by calling:
|
|
||||||
|
|
||||||
elixir --sname test -e "IO.puts node()"
|
|
||||||
|
|
||||||
Then you can try booting Livebook again
|
|
||||||
""")
|
""")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
defp ensure_distribution!() do
|
defp start_distribution!() do
|
||||||
unless Node.alive?() do
|
|
||||||
node = get_node_name()
|
node = get_node_name()
|
||||||
|
|
||||||
case Node.start(node, :longnames) do
|
case Node.start(node, :longnames) do
|
||||||
|
@ -173,10 +146,6 @@ defmodule Livebook.Application do
|
||||||
Livebook.Config.abort!("Could not start distributed node: #{inspect(reason)}")
|
Livebook.Config.abort!("Could not start distributed node: #{inspect(reason)}")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
import Record
|
|
||||||
defrecordp :hostent, Record.extract(:hostent, from_lib: "kernel/include/inet.hrl")
|
|
||||||
|
|
||||||
defp set_cookie() do
|
defp set_cookie() do
|
||||||
cookie = Application.fetch_env!(:livebook, :cookie)
|
cookie = Application.fetch_env!(:livebook, :cookie)
|
||||||
|
@ -356,10 +325,10 @@ defmodule Livebook.Application do
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
# We set ELIXIR_ERL_OPTIONS when LIVEBOOK_EPMDLESS is set to true.
|
# We set ELIXIR_ERL_OPTIONS to set our custom EPMD module when
|
||||||
# By design, we don't allow ELIXIR_ERL_OPTIONS to pass through.
|
# running from source. By design, we don't allow ELIXIR_ERL_OPTIONS
|
||||||
# Use ERL_AFLAGS and ERL_ZFLAGS if you want to configure both
|
# to pass through. Use ERL_AFLAGS and ERL_ZFLAGS if you want to
|
||||||
# Livebook and spawned runtimes.
|
# configure both Livebook and spawned runtimes.
|
||||||
defp config_env_var?("ELIXIR_ERL_OPTIONS"), do: true
|
defp config_env_var?("ELIXIR_ERL_OPTIONS"), do: true
|
||||||
defp config_env_var?("LIVEBOOK_" <> _), do: true
|
defp config_env_var?("LIVEBOOK_" <> _), do: true
|
||||||
defp config_env_var?("RELEASE_" <> _), do: true
|
defp config_env_var?("RELEASE_" <> _), do: true
|
||||||
|
|
|
@ -60,12 +60,26 @@ defmodule Livebook.Config do
|
||||||
})
|
})
|
||||||
def docker_images() do
|
def docker_images() do
|
||||||
version = app_version()
|
version = app_version()
|
||||||
base = if version =~ "dev", do: "latest", else: version
|
|
||||||
|
{version, version_cuda} =
|
||||||
|
if version =~ "dev" do
|
||||||
|
{"edge", "latest"}
|
||||||
|
else
|
||||||
|
{version, version}
|
||||||
|
end
|
||||||
|
|
||||||
[
|
[
|
||||||
%{tag: base, name: "Livebook", env: []},
|
%{tag: version, name: "Livebook", env: []},
|
||||||
%{tag: "#{base}-cuda11.8", name: "Livebook + CUDA 11.8", env: [{"XLA_TARGET", "cuda118"}]},
|
%{
|
||||||
%{tag: "#{base}-cuda12.1", name: "Livebook + CUDA 12.1", env: [{"XLA_TARGET", "cuda120"}]}
|
tag: "#{version_cuda}-cuda11.8",
|
||||||
|
name: "Livebook + CUDA 11.8",
|
||||||
|
env: [{"XLA_TARGET", "cuda118"}]
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
tag: "#{version_cuda}-cuda12.1",
|
||||||
|
name: "Livebook + CUDA 12.1",
|
||||||
|
env: [{"XLA_TARGET", "cuda120"}]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -158,7 +172,7 @@ defmodule Livebook.Config do
|
||||||
@spec tmp_path() :: String.t()
|
@spec tmp_path() :: String.t()
|
||||||
def tmp_path() do
|
def tmp_path() do
|
||||||
tmp_dir = System.tmp_dir!() |> Path.expand()
|
tmp_dir = System.tmp_dir!() |> Path.expand()
|
||||||
Path.join(tmp_dir, "livebook")
|
Path.join([tmp_dir, "livebook", app_version()])
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -353,13 +367,6 @@ defmodule Livebook.Config do
|
||||||
Application.fetch_env!(:livebook, :update_instructions_url)
|
Application.fetch_env!(:livebook, :update_instructions_url)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Returns a boolean if epmdless mode is configured.
|
|
||||||
"""
|
|
||||||
def epmdless? do
|
|
||||||
Application.fetch_env!(:livebook, :epmdless)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns the force ssl host if any.
|
Returns the force ssl host if any.
|
||||||
"""
|
"""
|
||||||
|
@ -673,7 +680,7 @@ defmodule Livebook.Config do
|
||||||
nil
|
nil
|
||||||
|
|
||||||
"standalone" ->
|
"standalone" ->
|
||||||
Livebook.Runtime.ElixirStandalone.new()
|
Livebook.Runtime.Standalone.new()
|
||||||
|
|
||||||
"embedded" ->
|
"embedded" ->
|
||||||
Livebook.Runtime.Embedded.new()
|
Livebook.Runtime.Embedded.new()
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
defmodule Livebook.EPMD do
|
defmodule Livebook.EPMD do
|
||||||
# A custom EPMD module used to bypass the epmd OS daemon
|
# A custom EPMD module used to bypass the epmd OS daemon on Livebook.
|
||||||
# on both Livebook and the runtimes.
|
#
|
||||||
@after_compile __MODULE__
|
# We also use it for the Fly runtime, such that we connect to the
|
||||||
|
# remote node via a local proxy port.
|
||||||
|
|
||||||
# From Erlang/OTP 23+
|
# From Erlang/OTP 23+
|
||||||
@epmd_dist_version 6
|
@epmd_dist_version 6
|
||||||
@external_resource "priv/epmd/Elixir.Livebook.EPMD.beam"
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a random child node name.
|
Gets a random child node name.
|
||||||
"""
|
"""
|
||||||
def random_child_node do
|
def random_child_node() do
|
||||||
String.to_atom(Livebook.EPMD.NodePool.get_name())
|
String.to_atom(Livebook.EPMD.NodePool.get_name())
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -30,82 +30,51 @@ defmodule Livebook.EPMD do
|
||||||
|
|
||||||
# Custom EPMD callbacks
|
# Custom EPMD callbacks
|
||||||
|
|
||||||
# Custom callback that registers the parent information.
|
|
||||||
# We read this information when trying to connect to the parent.
|
|
||||||
def start_link() do
|
|
||||||
with {:ok, [[node, port]]} <- :init.get_argument(:livebook_parent) do
|
|
||||||
[name, host] = :string.split(node, ~c"@")
|
|
||||||
|
|
||||||
:persistent_term.put(
|
|
||||||
:livebook_parent,
|
|
||||||
{name, host, List.to_atom(node), List.to_integer(port)}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
:erl_epmd.start_link()
|
|
||||||
end
|
|
||||||
|
|
||||||
# Custom callback to register our current node port.
|
# Custom callback to register our current node port.
|
||||||
def register_node(name, port), do: register_node(name, port, :inet)
|
def register_node(name, port), do: register_node(name, port, :inet)
|
||||||
|
|
||||||
def register_node(name, port, family) do
|
def register_node(name, port, family) do
|
||||||
:persistent_term.put(:livebook_dist_port, port)
|
:persistent_term.put(:livebook_dist_port, port)
|
||||||
:erl_epmd.register_node(name, port, family)
|
|
||||||
|
case :erl_epmd.register_node(name, port, family) do
|
||||||
|
{:ok, creation} -> {:ok, creation}
|
||||||
|
{:error, :already_registered} -> {:error, :already_registered}
|
||||||
|
# If registration fails because EPMD is not running, we ignore
|
||||||
|
# that, because we do not rely on EPMD
|
||||||
|
_ -> {:ok, -1}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Custom callback that accesses the parent information.
|
# Custom callback that accesses the parent information.
|
||||||
def port_please(name, host), do: port_please(name, host, :infinity)
|
def port_please(name, host), do: port_please(name, host, :infinity)
|
||||||
|
|
||||||
|
def port_please(~c"remote_runtime_" ++ port, _host, _timeout) do
|
||||||
|
# The node name includes the local port proxied to a remote machine
|
||||||
|
port = List.to_integer(port)
|
||||||
|
{:port, port, @epmd_dist_version}
|
||||||
|
end
|
||||||
|
|
||||||
def port_please(name, host, timeout) do
|
def port_please(name, host, timeout) do
|
||||||
case livebook_port(name) do
|
:erl_epmd.port_please(name, host, timeout)
|
||||||
0 -> :erl_epmd.port_please(name, host, timeout)
|
end
|
||||||
port -> {:port, port, @epmd_dist_version}
|
|
||||||
|
# Custom callback for resolving remote runtime node domain, such as
|
||||||
|
# Fly .internal, to loopback, because we communicate via a local
|
||||||
|
# proxied port
|
||||||
|
def address_please(~c"remote_runtime_" ++ _, _host, address_family) do
|
||||||
|
case address_family do
|
||||||
|
:inet -> {:ok, {127, 0, 0, 1}}
|
||||||
|
:inet6 -> {:ok, {0, 0, 0, 0, 0, 0, 0, 1}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# If we are running inside a Livebook Runtime,
|
def address_please(name, host, address_family) do
|
||||||
# we should be able to reach the parent directly
|
:erl_epmd.address_please(name, host, address_family)
|
||||||
# or reach siblings through the parent.
|
|
||||||
defp livebook_port(name) do
|
|
||||||
case :persistent_term.get(:livebook_parent, nil) do
|
|
||||||
{parent_name, parent_host, parent_node, parent_port} ->
|
|
||||||
case match_name(name, parent_name) do
|
|
||||||
:parent -> parent_port
|
|
||||||
:sibling -> sibling_port(parent_node, name, parent_host)
|
|
||||||
:none -> 0
|
|
||||||
end
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp match_name([x | name], [x | parent_name]), do: match_name(name, parent_name)
|
|
||||||
defp match_name([?-, ?- | _name], _parent), do: :sibling
|
|
||||||
defp match_name([], []), do: :parent
|
|
||||||
defp match_name(_name, _parent), do: :none
|
|
||||||
|
|
||||||
defp sibling_port(parent_node, name, host) do
|
|
||||||
:gen_server.call(
|
|
||||||
{Livebook.EPMD.NodePool, parent_node},
|
|
||||||
{:get_port, :erlang.list_to_binary(name ++ [?@] ++ host)},
|
|
||||||
5000
|
|
||||||
)
|
|
||||||
catch
|
|
||||||
_, _ -> 0
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Default EPMD callbacks
|
# Default EPMD callbacks
|
||||||
|
|
||||||
|
defdelegate start_link(), to: :erl_epmd
|
||||||
defdelegate listen_port_please(name, host), to: :erl_epmd
|
defdelegate listen_port_please(name, host), to: :erl_epmd
|
||||||
defdelegate names(host_name), to: :erl_epmd
|
defdelegate names(host_name), to: :erl_epmd
|
||||||
defdelegate address_please(name, host, address_family), to: :erl_epmd
|
|
||||||
|
|
||||||
# Store .beam file in priv as well
|
|
||||||
|
|
||||||
def __after_compile__(_env, binary) do
|
|
||||||
File.mkdir_p!("priv/epmd")
|
|
||||||
File.write!("priv/epmd/Elixir.Livebook.EPMD.beam", binary)
|
|
||||||
Mix.Project.build_structure()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -58,7 +58,7 @@ defmodule Livebook.EPMD.NodePool do
|
||||||
|
|
||||||
# Server side code
|
# Server side code
|
||||||
|
|
||||||
@impl GenServer
|
@impl true
|
||||||
def init(opts) do
|
def init(opts) do
|
||||||
:net_kernel.monitor_nodes(true, node_type: :all)
|
:net_kernel.monitor_nodes(true, node_type: :all)
|
||||||
[name, host] = node() |> Atom.to_string() |> :binary.split("@")
|
[name, host] = node() |> Atom.to_string() |> :binary.split("@")
|
||||||
|
@ -74,23 +74,21 @@ defmodule Livebook.EPMD.NodePool do
|
||||||
{:ok, state}
|
{:ok, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl GenServer
|
@impl true
|
||||||
def handle_call(:get_name, _, state) do
|
def handle_call(:get_name, _, state) do
|
||||||
{name, state} = server_get_name(state)
|
{name, state} = server_get_name(state)
|
||||||
{:reply, name, put_in(state.active_names[name], 0)}
|
{:reply, name, put_in(state.active_names[name], 0)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl GenServer
|
|
||||||
def handle_call({:get_port, name}, _, state) do
|
def handle_call({:get_port, name}, _, state) do
|
||||||
{:reply, Map.get(state.active_names, name, 0), state}
|
{:reply, Map.get(state.active_names, name, 0), state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl GenServer
|
|
||||||
def handle_call({:update_name, name, port}, _, state) do
|
def handle_call({:update_name, name, port}, _, state) do
|
||||||
{:reply, :ok, server_update_name(name, port, state)}
|
{:reply, :ok, server_update_name(name, port, state)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl GenServer
|
@impl true
|
||||||
def handle_info({:nodedown, node, _info}, state) do
|
def handle_info({:nodedown, node, _info}, state) do
|
||||||
case state.buffer_time do
|
case state.buffer_time do
|
||||||
0 -> send(self(), {:release_node, node})
|
0 -> send(self(), {:release_node, node})
|
||||||
|
@ -100,12 +98,10 @@ defmodule Livebook.EPMD.NodePool do
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl GenServer
|
|
||||||
def handle_info({:nodeup, _node, _info}, state) do
|
def handle_info({:nodeup, _node, _info}, state) do
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl GenServer
|
|
||||||
def handle_info({:release_node, node}, state) do
|
def handle_info({:release_node, node}, state) do
|
||||||
{:noreply, server_release_name(Atom.to_string(node), state)}
|
{:noreply, server_release_name(Atom.to_string(node), state)}
|
||||||
end
|
end
|
||||||
|
|
274
lib/livebook/fly_api.ex
Normal file
274
lib/livebook/fly_api.ex
Normal file
|
@ -0,0 +1,274 @@
|
||||||
|
defmodule Livebook.FlyAPI do
|
||||||
|
# Calls to the Fly API.
|
||||||
|
#
|
||||||
|
# Note that Fly currently exposes both a REST Machines API [1] and
|
||||||
|
# a more elaborate GraphQL API [2]. The Machines API should be
|
||||||
|
# preferred whenever possible. The Go client [3] serves as a good
|
||||||
|
# reference for various operations.
|
||||||
|
#
|
||||||
|
# [1]: https://fly.io/docs/machines/api
|
||||||
|
# [2]: https://github.com/superfly/fly-go/blob/v0.1.18/schema.graphql
|
||||||
|
# [3]: https://github.com/superfly/fly-go
|
||||||
|
|
||||||
|
# See https://github.com/superfly/fly-go/blob/ea7601fc38ba5e9786155711471646dcb0bf63b8/flaps/flaps_volumes.go#L12
|
||||||
|
@destroyed_volume_states ~w(scheduling_destroy fork_cleanup waiting_for_detach pending_destroy destroying)
|
||||||
|
|
||||||
|
@api_url "https://api.fly.io/graphql"
|
||||||
|
@flaps_url "https://api.machines.dev"
|
||||||
|
|
||||||
|
@type error :: %{message: String.t(), status: pos_integer() | nil}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
The valid values for CPU kind.
|
||||||
|
"""
|
||||||
|
@spec cpu_kinds() :: list(String.t())
|
||||||
|
def cpu_kinds(), do: ~w(shared performance)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
The valid values for GPU kind.
|
||||||
|
"""
|
||||||
|
@spec gpu_kinds() :: list(String.t())
|
||||||
|
def gpu_kinds(), do: ~w(a10 a100-pcie-40gb a100-sxm4-80gb l40s)
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Fetches information about organizations visible to the given token
|
||||||
|
and also regions data.
|
||||||
|
"""
|
||||||
|
@spec get_orgs_and_regions(String.t()) :: {:ok, data} | {:error, error}
|
||||||
|
when data: %{
|
||||||
|
orgs: list(%{name: String.t(), slug: String.t()}),
|
||||||
|
regions: %{name: String.t(), code: String.t()},
|
||||||
|
closest_region: String.t()
|
||||||
|
}
|
||||||
|
def get_orgs_and_regions(token) do
|
||||||
|
query = """
|
||||||
|
query {
|
||||||
|
organizations {
|
||||||
|
nodes {
|
||||||
|
rawSlug
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
platform {
|
||||||
|
requestRegion
|
||||||
|
regions {
|
||||||
|
name
|
||||||
|
code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
with {:ok, data} <- api_request(token, query) do
|
||||||
|
{:ok,
|
||||||
|
%{
|
||||||
|
orgs: Enum.map(data["organizations"]["nodes"], &parse_org/1),
|
||||||
|
regions: Enum.map(data["platform"]["regions"], &parse_region/1),
|
||||||
|
closest_region: data["platform"]["requestRegion"]
|
||||||
|
}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_org(org) do
|
||||||
|
%{name: org["name"], slug: org["rawSlug"]}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_region(region) do
|
||||||
|
%{code: region["code"], name: region["name"]}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Fetches volumes in the given app.
|
||||||
|
|
||||||
|
Note that destroyed volumes are ignored.
|
||||||
|
"""
|
||||||
|
@spec get_app_volumes(String.t(), String.t()) :: {:ok, data} | {:error, error}
|
||||||
|
when data:
|
||||||
|
list(%{
|
||||||
|
id: String.t(),
|
||||||
|
name: String.t(),
|
||||||
|
region: String.t(),
|
||||||
|
size_gb: pos_integer()
|
||||||
|
})
|
||||||
|
def get_app_volumes(token, app_name) do
|
||||||
|
with {:ok, data} <- flaps_request(token, "/v1/apps/#{app_name}/volumes") do
|
||||||
|
volumes =
|
||||||
|
for volume <- data,
|
||||||
|
volume["state"] not in @destroyed_volume_states,
|
||||||
|
do: parse_volume(volume)
|
||||||
|
|
||||||
|
{:ok, volumes}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_volume(volume) do
|
||||||
|
%{
|
||||||
|
id: volume["id"],
|
||||||
|
name: volume["name"],
|
||||||
|
region: volume["region"],
|
||||||
|
size_gb: volume["size_gb"]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates an app under the given organization.
|
||||||
|
"""
|
||||||
|
@spec create_app(String.t(), String.t(), String.t()) :: :ok | {:error, error}
|
||||||
|
def create_app(token, app_name, org_slug) do
|
||||||
|
with {:ok, _data} <-
|
||||||
|
flaps_request(token, "/v1/apps",
|
||||||
|
method: :post,
|
||||||
|
json: %{app_name: app_name, org_slug: org_slug}
|
||||||
|
) do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a new volume in the given app.
|
||||||
|
|
||||||
|
The `compute` attributes hint the expected machine specs that this
|
||||||
|
volume will be attached to. This helps to ensure that the volume is
|
||||||
|
placed on the right hardware (e.g. GPU-enabled).
|
||||||
|
"""
|
||||||
|
@spec create_volume(String.t(), String.t(), String.t(), String.t(), pos_integer(), map()) ::
|
||||||
|
{:ok, data} | {:error, error}
|
||||||
|
when data: %{
|
||||||
|
id: String.t(),
|
||||||
|
name: String.t(),
|
||||||
|
region: String.t(),
|
||||||
|
size_gb: pos_integer()
|
||||||
|
}
|
||||||
|
def create_volume(token, app_name, name, region, size_gb, compute) do
|
||||||
|
with {:ok, data} <-
|
||||||
|
flaps_request(token, "/v1/apps/#{app_name}/volumes",
|
||||||
|
method: :post,
|
||||||
|
json: %{
|
||||||
|
name: name,
|
||||||
|
size_gb: size_gb,
|
||||||
|
region: region,
|
||||||
|
compute: compute
|
||||||
|
}
|
||||||
|
) do
|
||||||
|
{:ok, parse_volume(data)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Deletes the given volume.
|
||||||
|
"""
|
||||||
|
@spec delete_volume(String.t(), String.t(), String.t()) :: :ok | {:error, error}
|
||||||
|
def delete_volume(token, app_name, volume_id) do
|
||||||
|
with {:ok, _data} <-
|
||||||
|
flaps_request(token, "/v1/apps/#{app_name}/volumes/#{volume_id}", method: :delete) do
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Creates a new machine in the given app.
|
||||||
|
"""
|
||||||
|
@spec create_machine(String.t(), String.t(), String.t(), String.t(), map()) ::
|
||||||
|
{:ok, data} | {:error, error}
|
||||||
|
when data: %{id: String.t(), private_ip: String.t()}
|
||||||
|
def create_machine(token, app_name, name, region, config) do
|
||||||
|
boot_timeout = 30_000
|
||||||
|
|
||||||
|
with {:ok, data} <-
|
||||||
|
flaps_request(token, "/v1/apps/#{app_name}/machines",
|
||||||
|
method: :post,
|
||||||
|
json: %{name: name, region: region, config: config},
|
||||||
|
receive_timeout: boot_timeout
|
||||||
|
) do
|
||||||
|
{:ok, parse_machine(data)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_machine(machine) do
|
||||||
|
%{id: machine["id"], private_ip: machine["private_ip"]}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp flaps_request(token, path, opts \\ []) do
|
||||||
|
opts =
|
||||||
|
[base_url: @flaps_url, url: path, auth: {:bearer, token}]
|
||||||
|
|> Keyword.merge(opts)
|
||||||
|
|> Keyword.merge(test_options())
|
||||||
|
|
||||||
|
case Req.request(opts) do
|
||||||
|
{:ok, %{status: status, body: body}} when status in 200..299 ->
|
||||||
|
{:ok, body}
|
||||||
|
|
||||||
|
{:ok, %{status: status, body: body}} ->
|
||||||
|
message =
|
||||||
|
case body do
|
||||||
|
%{"error" => error} when is_binary(error) ->
|
||||||
|
Livebook.Utils.downcase_first(error)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
"HTTP status #{status}"
|
||||||
|
end
|
||||||
|
|
||||||
|
{:error, %{message: message, status: status}}
|
||||||
|
|
||||||
|
{:error, exception} ->
|
||||||
|
{:error, %{message: "reason: #{Exception.message(exception)}", status: nil}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp api_request(token, query) do
|
||||||
|
opts =
|
||||||
|
[
|
||||||
|
base_url: @api_url,
|
||||||
|
method: :post,
|
||||||
|
auth: {:bearer, token},
|
||||||
|
json: %{query: query}
|
||||||
|
]
|
||||||
|
|> Keyword.merge(test_options())
|
||||||
|
|
||||||
|
case Req.request(opts) do
|
||||||
|
{:ok, %{status: 200, body: body}} ->
|
||||||
|
case body do
|
||||||
|
%{"errors" => [%{"extensions" => %{"code" => "UNAUTHORIZED"}} | _]} ->
|
||||||
|
{:error, %{message: "could not authorize with the given token", status: 401}}
|
||||||
|
|
||||||
|
%{"errors" => [%{"extensions" => %{"message" => message}} | _]} ->
|
||||||
|
{:error, %{message: Livebook.Utils.downcase_first(message), status: nil}}
|
||||||
|
|
||||||
|
%{"data" => data} ->
|
||||||
|
{:ok, data}
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, %{status: status}} ->
|
||||||
|
{:error, %{message: "HTTP status #{status}", status: status}}
|
||||||
|
|
||||||
|
{:error, exception} ->
|
||||||
|
{:error, %{message: "reason: #{Exception.message(exception)}", status: nil}}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# TODO: do not rely on private APIs. Also, ideally we should still
|
||||||
|
# be able to use Req.Test.expect/2
|
||||||
|
if Mix.env() == :test do
|
||||||
|
defp test_options() do
|
||||||
|
case Req.Test.__fetch_plug__(__MODULE__) do
|
||||||
|
:passthrough ->
|
||||||
|
[]
|
||||||
|
|
||||||
|
_plug ->
|
||||||
|
[plug: {Req.Test, __MODULE__}]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def stub(plug) do
|
||||||
|
Req.Test.stub(__MODULE__, plug)
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def passthrough() do
|
||||||
|
Req.Test.stub(__MODULE__, :passthrough)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
defp test_options(), do: []
|
||||||
|
end
|
||||||
|
end
|
|
@ -785,19 +785,32 @@ defprotocol Livebook.Runtime do
|
||||||
def describe(runtime)
|
def describe(runtime)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Synchronously initializes the given runtime.
|
Asynchronously initializes the given runtime.
|
||||||
|
|
||||||
This function starts the necessary resources and processes.
|
The initialization should take care of starting any OS processes
|
||||||
|
necessary, setting up resources and communication.
|
||||||
|
|
||||||
|
Since the initialization may take time, it should always happen in
|
||||||
|
a separate process. This function should return the `pid` of that
|
||||||
|
process. Once the initialization is finished, the process should
|
||||||
|
send the following message to the caller:
|
||||||
|
|
||||||
|
* `{:runtime_connect_done, pid, {:ok, runtime} | {:error, message}}`
|
||||||
|
|
||||||
|
The `runtime` should be the struct updated with all information
|
||||||
|
necessary for further communication.
|
||||||
|
|
||||||
|
In case the initialization is a particularly involved process, the
|
||||||
|
process may send updates to the caller:
|
||||||
|
|
||||||
|
* `{:runtime_connect_info, pid, info}`
|
||||||
|
|
||||||
|
Where `info` is a few word text describing the current initialization
|
||||||
|
step.
|
||||||
"""
|
"""
|
||||||
@spec connect(t()) :: {:ok, t()} | {:error, String.t()}
|
@spec connect(t()) :: pid()
|
||||||
def connect(runtime)
|
def connect(runtime)
|
||||||
|
|
||||||
@doc """
|
|
||||||
Checks if the given runtime is in a connected state.
|
|
||||||
"""
|
|
||||||
@spec connected?(t()) :: boolean()
|
|
||||||
def connected?(runtime)
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Sets the caller as the runtime owner.
|
Sets the caller as the runtime owner.
|
||||||
|
|
||||||
|
@ -824,13 +837,15 @@ defprotocol Livebook.Runtime do
|
||||||
Synchronously disconnects the runtime and cleans up the underlying
|
Synchronously disconnects the runtime and cleans up the underlying
|
||||||
resources.
|
resources.
|
||||||
"""
|
"""
|
||||||
@spec disconnect(t()) :: {:ok, t()}
|
@spec disconnect(t()) :: :ok
|
||||||
def disconnect(runtime)
|
def disconnect(runtime)
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Returns a fresh runtime of the same type with the same configuration.
|
Returns a fresh runtime of the same type with the same configuration.
|
||||||
|
|
||||||
Note that the runtime is in a stopped state.
|
This function is expected to only modify the runtime struct, unsetting
|
||||||
|
any information added by `connect/1`. It should not have any side
|
||||||
|
effects.
|
||||||
"""
|
"""
|
||||||
@spec duplicate(Runtime.t()) :: Runtime.t()
|
@spec duplicate(Runtime.t()) :: Runtime.t()
|
||||||
def duplicate(runtime)
|
def duplicate(runtime)
|
||||||
|
@ -889,6 +904,9 @@ defprotocol Livebook.Runtime do
|
||||||
* `:smart_cell_ref` - a reference of the smart cell which code is
|
* `:smart_cell_ref` - a reference of the smart cell which code is
|
||||||
to be evaluated, if applicable
|
to be evaluated, if applicable
|
||||||
|
|
||||||
|
* `:disable_dependencies_cache` - disables dependencies cache, so
|
||||||
|
they are fetched and compiled from scratch
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec evaluate_code(t(), atom(), String.t(), locator(), parent_locators(), keyword()) :: :ok
|
@spec evaluate_code(t(), atom(), String.t(), locator(), parent_locators(), keyword()) :: :ok
|
||||||
def evaluate_code(runtime, language, code, locator, parent_locators, opts \\ [])
|
def evaluate_code(runtime, language, code, locator, parent_locators, opts \\ [])
|
||||||
|
@ -1076,13 +1094,6 @@ defprotocol Livebook.Runtime do
|
||||||
@spec search_packages(t(), pid(), String.t()) :: reference()
|
@spec search_packages(t(), pid(), String.t()) :: reference()
|
||||||
def search_packages(runtime, send_to, search)
|
def search_packages(runtime, send_to, search)
|
||||||
|
|
||||||
@doc """
|
|
||||||
Disables dependencies cache, so they are fetched and compiled from
|
|
||||||
scratch.
|
|
||||||
"""
|
|
||||||
@spec disable_dependencies_cache(t()) :: :ok
|
|
||||||
def disable_dependencies_cache(runtime)
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Sets the given environment variables.
|
Sets the given environment variables.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
defmodule Livebook.Runtime.Attached do
|
defmodule Livebook.Runtime.Attached do
|
||||||
# A runtime backed by an Elixir node managed externally.
|
# A runtime backed by an Elixir node managed externally.
|
||||||
#
|
#
|
||||||
# Such node must be already started and available, Livebook doesn't
|
# Such node must be already started and accessible. Livebook doesn't
|
||||||
# manage its lifetime in any way and only loads/unloads the
|
# manage the node's lifetime in any way and only loads/unloads the
|
||||||
# necessary elements. The node can be an ordinary Elixir runtime,
|
# necessary modules and processes. The node can be an ordinary Elixir
|
||||||
# a Mix project shell, a running release or anything else.
|
# runtime, a Mix project shell, a running release or anything else.
|
||||||
|
|
||||||
defstruct [:node, :cookie, :server_pid]
|
defstruct [:node, :cookie, :server_pid]
|
||||||
|
|
||||||
|
@ -22,17 +22,20 @@ defmodule Livebook.Runtime.Attached do
|
||||||
%__MODULE__{node: node, cookie: cookie}
|
%__MODULE__{node: node, cookie: cookie}
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
def __connect__(runtime) do
|
||||||
Checks if the given node is available for use and initializes
|
caller = self()
|
||||||
it with Livebook-specific modules and processes.
|
|
||||||
"""
|
|
||||||
@spec connect(t()) :: {:ok, t()} | {:error, String.t()}
|
|
||||||
def connect(runtime) do
|
|
||||||
%{node: node, cookie: cookie} = runtime
|
|
||||||
|
|
||||||
# We need to append the hostname on connect because
|
{:ok, pid} =
|
||||||
# net_kernel has not yet started during new/2.
|
DynamicSupervisor.start_child(
|
||||||
node = append_hostname(node)
|
Livebook.RuntimeSupervisor,
|
||||||
|
{Task, fn -> do_connect(runtime, caller) end}
|
||||||
|
)
|
||||||
|
|
||||||
|
pid
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_connect(runtime, caller) do
|
||||||
|
%{node: node, cookie: cookie} = runtime
|
||||||
|
|
||||||
# Set cookie for connecting to this specific node
|
# Set cookie for connecting to this specific node
|
||||||
Node.set_cookie(node, cookie)
|
Node.set_cookie(node, cookie)
|
||||||
|
@ -44,7 +47,11 @@ defmodule Livebook.Runtime.Attached do
|
||||||
node_manager_opts: [parent_node: node(), capture_orphan_logs: false]
|
node_manager_opts: [parent_node: node(), capture_orphan_logs: false]
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, %{runtime | node: node, server_pid: server_pid}}
|
runtime = %{runtime | node: node, server_pid: server_pid}
|
||||||
|
send(caller, {:runtime_connect_done, self(), {:ok, runtime}})
|
||||||
|
else
|
||||||
|
{:error, error} ->
|
||||||
|
send(caller, {:runtime_connect_done, self(), {:error, error}})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -57,26 +64,45 @@ defmodule Livebook.Runtime.Attached do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@elixir_version_requirement Keyword.fetch!(Mix.Project.config(), :elixir)
|
|
||||||
|
|
||||||
defp check_attached_node_version(node) do
|
defp check_attached_node_version(node) do
|
||||||
attached_node_version = :erpc.call(node, System, :version, [])
|
attached_node_version = :erpc.call(node, System, :version, [])
|
||||||
|
|
||||||
if Version.match?(attached_node_version, @elixir_version_requirement) do
|
requirement = elixir_version_requirement()
|
||||||
|
|
||||||
|
if Version.match?(attached_node_version, requirement) do
|
||||||
:ok
|
:ok
|
||||||
else
|
else
|
||||||
{:error,
|
{:error, "the node uses Elixir #{attached_node_version}, but #{requirement} is required"}
|
||||||
"the node uses Elixir #{attached_node_version}, but #{@elixir_version_requirement} is required"}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp append_hostname(node) do
|
@elixir_version_requirement Keyword.fetch!(Mix.Project.config(), :elixir)
|
||||||
with :nomatch <- :string.find(Atom.to_string(node), "@"),
|
|
||||||
<<suffix::binary>> <- :string.find(Atom.to_string(:net_kernel.nodename()), "@") do
|
@doc """
|
||||||
:"#{node}#{suffix}"
|
Returns requirement for the attached node Elixir version.
|
||||||
|
"""
|
||||||
|
@spec elixir_version_requirement() :: String.t()
|
||||||
|
def elixir_version_requirement() do
|
||||||
|
# We load compiled modules binary into the remote node. Erlang
|
||||||
|
# provides rather good compatibility of the binary format, and
|
||||||
|
# in case loading fails we show an appropriate message. However,
|
||||||
|
# it is more likely that the Elixir core functions used in the
|
||||||
|
# compiled module differ across versions. We assume that such
|
||||||
|
# changes are unlikely within the same minor version, so that's
|
||||||
|
# the requirement we enforce.
|
||||||
|
|
||||||
|
current = Version.parse!(System.version())
|
||||||
|
same_minor = "#{current.major}.#{current.minor}.0"
|
||||||
|
|
||||||
|
# Make sure Livebook does not enforce a higher patch version
|
||||||
|
min_version =
|
||||||
|
if Version.match?(same_minor, @elixir_version_requirement) do
|
||||||
|
same_minor
|
||||||
else
|
else
|
||||||
_ -> node
|
current
|
||||||
end
|
end
|
||||||
|
|
||||||
|
"~> " <> min_version
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -85,17 +111,13 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do
|
||||||
|
|
||||||
def describe(runtime) do
|
def describe(runtime) do
|
||||||
[
|
[
|
||||||
{"Type", "Attached"},
|
{"Type", "Attached node"},
|
||||||
{"Node name", Atom.to_string(runtime.node)}
|
{"Node name", Atom.to_string(runtime.node)}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
def connect(runtime) do
|
def connect(runtime) do
|
||||||
Livebook.Runtime.Attached.connect(runtime)
|
Livebook.Runtime.Attached.__connect__(runtime)
|
||||||
end
|
|
||||||
|
|
||||||
def connected?(runtime) do
|
|
||||||
runtime.server_pid != nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def take_ownership(runtime, opts \\ []) do
|
def take_ownership(runtime, opts \\ []) do
|
||||||
|
@ -105,7 +127,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do
|
||||||
|
|
||||||
def disconnect(runtime) do
|
def disconnect(runtime) do
|
||||||
RuntimeServer.stop(runtime.server_pid)
|
RuntimeServer.stop(runtime.server_pid)
|
||||||
{:ok, %{runtime | server_pid: nil}}
|
Node.disconnect(runtime.node)
|
||||||
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
def duplicate(runtime) do
|
def duplicate(runtime) do
|
||||||
|
@ -181,10 +204,6 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do
|
||||||
raise "not supported"
|
raise "not supported"
|
||||||
end
|
end
|
||||||
|
|
||||||
def disable_dependencies_cache(runtime) do
|
|
||||||
RuntimeServer.disable_dependencies_cache(runtime.server_pid)
|
|
||||||
end
|
|
||||||
|
|
||||||
def put_system_envs(runtime, envs) do
|
def put_system_envs(runtime, envs) do
|
||||||
RuntimeServer.put_system_envs(runtime.server_pid, envs)
|
RuntimeServer.put_system_envs(runtime.server_pid, envs)
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,7 +2,13 @@ defmodule Livebook.Runtime.Embedded do
|
||||||
# A runtime backed by the same node Livebook is running in.
|
# A runtime backed by the same node Livebook is running in.
|
||||||
#
|
#
|
||||||
# This runtime is reserved for specific use cases, where there is
|
# This runtime is reserved for specific use cases, where there is
|
||||||
# no option of starting a separate Elixir runtime.
|
# no option of starting a separate Elixir OS process.
|
||||||
|
#
|
||||||
|
# As we run in the Livebook node, all the necessary modules are in
|
||||||
|
# place, so we just ensure the node manager process is running and
|
||||||
|
# we start a new runtime server. We also disable modules cleanup
|
||||||
|
# on termination, since we don't want to unload any modules from
|
||||||
|
# the current node.
|
||||||
|
|
||||||
defstruct [:server_pid]
|
defstruct [:server_pid]
|
||||||
|
|
||||||
|
@ -18,30 +24,26 @@ defmodule Livebook.Runtime.Embedded do
|
||||||
%__MODULE__{}
|
%__MODULE__{}
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
def __connect__(runtime) do
|
||||||
Initializes new runtime by starting the necessary processes within
|
caller = self()
|
||||||
the current node.
|
|
||||||
"""
|
|
||||||
@spec connect(t()) :: {:ok, t()}
|
|
||||||
def connect(runtime) do
|
|
||||||
# As we run in the Livebook node, all the necessary modules
|
|
||||||
# are in place, so we just start the manager process.
|
|
||||||
# We make it anonymous, so that multiple embedded runtimes
|
|
||||||
# can be started (for different notebooks).
|
|
||||||
# We also disable cleanup, as we don't want to unload any
|
|
||||||
# modules or revert the configuration (because other runtimes
|
|
||||||
# may rely on it). If someone uses embedded runtimes,
|
|
||||||
# this cleanup is not particularly important anyway.
|
|
||||||
# We tell manager to not override :standard_error,
|
|
||||||
# as we already do it for the Livebook application globally
|
|
||||||
# (see Livebook.Application.start/2).
|
|
||||||
|
|
||||||
|
{:ok, pid} =
|
||||||
|
DynamicSupervisor.start_child(
|
||||||
|
Livebook.RuntimeSupervisor,
|
||||||
|
{Task, fn -> do_connect(runtime, caller) end}
|
||||||
|
)
|
||||||
|
|
||||||
|
pid
|
||||||
|
end
|
||||||
|
|
||||||
|
defp do_connect(runtime, caller) do
|
||||||
server_pid =
|
server_pid =
|
||||||
ErlDist.initialize(node(),
|
ErlDist.initialize(node(),
|
||||||
node_manager_opts: [unload_modules_on_termination: false]
|
node_manager_opts: [unload_modules_on_termination: false]
|
||||||
)
|
)
|
||||||
|
|
||||||
{:ok, %{runtime | server_pid: server_pid}}
|
runtime = %{runtime | server_pid: server_pid}
|
||||||
|
send(caller, {:runtime_connect_done, self(), {:ok, runtime}})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -53,11 +55,7 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do
|
||||||
end
|
end
|
||||||
|
|
||||||
def connect(runtime) do
|
def connect(runtime) do
|
||||||
Livebook.Runtime.Embedded.connect(runtime)
|
Livebook.Runtime.Embedded.__connect__(runtime)
|
||||||
end
|
|
||||||
|
|
||||||
def connected?(runtime) do
|
|
||||||
runtime.server_pid != nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def take_ownership(runtime, opts \\ []) do
|
def take_ownership(runtime, opts \\ []) do
|
||||||
|
@ -66,8 +64,7 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do
|
||||||
end
|
end
|
||||||
|
|
||||||
def disconnect(runtime) do
|
def disconnect(runtime) do
|
||||||
RuntimeServer.stop(runtime.server_pid)
|
:ok = RuntimeServer.stop(runtime.server_pid)
|
||||||
{:ok, %{runtime | server_pid: nil}}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def duplicate(_runtime) do
|
def duplicate(_runtime) do
|
||||||
|
@ -147,10 +144,6 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do
|
||||||
Livebook.Runtime.Dependencies.search_packages_in_list(packages, send_to, search)
|
Livebook.Runtime.Dependencies.search_packages_in_list(packages, send_to, search)
|
||||||
end
|
end
|
||||||
|
|
||||||
def disable_dependencies_cache(runtime) do
|
|
||||||
RuntimeServer.disable_dependencies_cache(runtime.server_pid)
|
|
||||||
end
|
|
||||||
|
|
||||||
def put_system_envs(runtime, envs) do
|
def put_system_envs(runtime, envs) do
|
||||||
RuntimeServer.put_system_envs(runtime.server_pid, envs)
|
RuntimeServer.put_system_envs(runtime.server_pid, envs)
|
||||||
end
|
end
|
||||||
|
|
87
lib/livebook/runtime/epmd.ex
Normal file
87
lib/livebook/runtime/epmd.ex
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
defmodule Livebook.Runtime.EPMD do
|
||||||
|
# A custom EPMD module used to bypass the epmd OS daemon in the
|
||||||
|
# standalone runtime.
|
||||||
|
#
|
||||||
|
# We used to start epmd on application boot, however sometimes it
|
||||||
|
# would fail. In particular, on Windows starting epmd may require
|
||||||
|
# accepting a firewall pop up, and the first boot could still fail.
|
||||||
|
# To avoid this, we use a custom port resolution that does not rely
|
||||||
|
# on the epmd OS daemon running.
|
||||||
|
|
||||||
|
# From Erlang/OTP 23+
|
||||||
|
@epmd_dist_version 6
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Persists parent information, used when connecting to the parent.
|
||||||
|
"""
|
||||||
|
def register_parent(parent_node, parent_port) do
|
||||||
|
[name, host] = parent_node |> Atom.to_charlist() |> :string.split(~c"@")
|
||||||
|
:persistent_term.put(:livebook_parent, {name, host, parent_node, parent_port})
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns the current distribution port.
|
||||||
|
"""
|
||||||
|
def dist_port do
|
||||||
|
:persistent_term.get(:livebook_dist_port)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Custom EPMD callbacks
|
||||||
|
|
||||||
|
# Custom callback to register our current node port.
|
||||||
|
def register_node(name, port), do: register_node(name, port, :inet)
|
||||||
|
|
||||||
|
def register_node(name, port, family) do
|
||||||
|
:persistent_term.put(:livebook_dist_port, port)
|
||||||
|
|
||||||
|
case :erl_epmd.register_node(name, port, family) do
|
||||||
|
{:ok, creation} -> {:ok, creation}
|
||||||
|
{:error, :already_registered} -> {:error, :already_registered}
|
||||||
|
# If registration fails because EPMD is not running, we ignore
|
||||||
|
# that, because we do not rely on EPMD
|
||||||
|
_ -> {:ok, -1}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Custom callback for resolving parent and sibling node ports.
|
||||||
|
def port_please(name, host), do: port_please(name, host, :infinity)
|
||||||
|
|
||||||
|
def port_please(name, host, timeout) do
|
||||||
|
case livebook_port(name) do
|
||||||
|
0 -> :erl_epmd.port_please(name, host, timeout)
|
||||||
|
port -> {:port, port, @epmd_dist_version}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp livebook_port(name) do
|
||||||
|
{parent_name, parent_host, parent_node, parent_port} = :persistent_term.get(:livebook_parent)
|
||||||
|
|
||||||
|
case match_name(name, parent_name) do
|
||||||
|
:parent -> parent_port
|
||||||
|
:sibling -> sibling_port(parent_node, name, parent_host)
|
||||||
|
:none -> 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp match_name([x | name], [x | parent_name]), do: match_name(name, parent_name)
|
||||||
|
defp match_name([?-, ?- | _name], _parent), do: :sibling
|
||||||
|
defp match_name([], []), do: :parent
|
||||||
|
defp match_name(_name, _parent), do: :none
|
||||||
|
|
||||||
|
defp sibling_port(parent_node, name, host) do
|
||||||
|
:gen_server.call(
|
||||||
|
{Livebook.EPMD.NodePool, parent_node},
|
||||||
|
{:get_port, :erlang.list_to_binary(name ++ [?@] ++ host)},
|
||||||
|
5000
|
||||||
|
)
|
||||||
|
catch
|
||||||
|
_, _ -> 0
|
||||||
|
end
|
||||||
|
|
||||||
|
# Default EPMD callbacks
|
||||||
|
|
||||||
|
defdelegate start_link(), to: :erl_epmd
|
||||||
|
defdelegate address_please(name, host, address_family), to: :erl_epmd
|
||||||
|
defdelegate listen_port_please(name, host), to: :erl_epmd
|
||||||
|
defdelegate names(host_name), to: :erl_epmd
|
||||||
|
end
|
|
@ -5,7 +5,7 @@ defmodule Livebook.Runtime.ErlDist do
|
||||||
# To ensure proper isolation between sessions, code evaluation may
|
# To ensure proper isolation between sessions, code evaluation may
|
||||||
# take place in a separate Elixir runtime, which also makes it easy
|
# take place in a separate Elixir runtime, which also makes it easy
|
||||||
# to terminate the whole evaluation environment without stopping
|
# to terminate the whole evaluation environment without stopping
|
||||||
# Livebook. Both `Runtime.ElixirStandalone` and `Runtime.Attached`
|
# Livebook. Both `Runtime.Standalone` and `Runtime.Attached`
|
||||||
# do that and this module contains the shared functionality.
|
# do that and this module contains the shared functionality.
|
||||||
#
|
#
|
||||||
# To work with a separate node, we have to inject the necessary
|
# To work with a separate node, we have to inject the necessary
|
||||||
|
@ -40,7 +40,6 @@ defmodule Livebook.Runtime.ErlDist do
|
||||||
Livebook.Runtime.ErlDist.EvaluatorSupervisor,
|
Livebook.Runtime.ErlDist.EvaluatorSupervisor,
|
||||||
Livebook.Runtime.ErlDist.IOForwardGL,
|
Livebook.Runtime.ErlDist.IOForwardGL,
|
||||||
Livebook.Runtime.ErlDist.LoggerGLHandler,
|
Livebook.Runtime.ErlDist.LoggerGLHandler,
|
||||||
Livebook.Runtime.ErlDist.Sink,
|
|
||||||
Livebook.Runtime.ErlDist.SmartCellGL,
|
Livebook.Runtime.ErlDist.SmartCellGL,
|
||||||
Livebook.Proxy.Adapter,
|
Livebook.Proxy.Adapter,
|
||||||
Livebook.Proxy.Handler
|
Livebook.Proxy.Handler
|
||||||
|
@ -62,15 +61,22 @@ defmodule Livebook.Runtime.ErlDist do
|
||||||
"""
|
"""
|
||||||
@spec initialize(node(), keyword()) :: pid()
|
@spec initialize(node(), keyword()) :: pid()
|
||||||
def initialize(node, opts \\ []) do
|
def initialize(node, opts \\ []) do
|
||||||
|
# First, we attempt to communicate with the node manager, in case
|
||||||
|
# there is one running. Otherwise, the node is not initialized,
|
||||||
|
# so we need to initialize it and try again
|
||||||
|
case start_runtime_server(node, opts[:runtime_server_opts] || []) do
|
||||||
|
{:ok, pid} ->
|
||||||
|
pid
|
||||||
|
|
||||||
|
{:error, :down} ->
|
||||||
unless modules_loaded?(node) do
|
unless modules_loaded?(node) do
|
||||||
load_required_modules(node)
|
load_required_modules(node)
|
||||||
end
|
end
|
||||||
|
|
||||||
unless node_manager_started?(node) do
|
{:ok, _} = start_node_manager(node, opts[:node_manager_opts] || [])
|
||||||
start_node_manager(node, opts[:node_manager_opts] || [])
|
{:ok, pid} = start_runtime_server(node, opts[:runtime_server_opts] || [])
|
||||||
|
pid
|
||||||
end
|
end
|
||||||
|
|
||||||
start_runtime_server(node, opts[:runtime_server_opts] || [])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_required_modules(node) do
|
defp load_required_modules(node) do
|
||||||
|
@ -109,13 +115,6 @@ defmodule Livebook.Runtime.ErlDist do
|
||||||
:rpc.call(node, Code, :ensure_loaded?, [Livebook.Runtime.ErlDist.NodeManager])
|
:rpc.call(node, Code, :ensure_loaded?, [Livebook.Runtime.ErlDist.NodeManager])
|
||||||
end
|
end
|
||||||
|
|
||||||
defp node_manager_started?(node) do
|
|
||||||
case :rpc.call(node, Process, :whereis, [Livebook.Runtime.ErlDist.NodeManager]) do
|
|
||||||
nil -> false
|
|
||||||
_pid -> true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Unloads the previously loaded Livebook modules from the caller node.
|
Unloads the previously loaded Livebook modules from the caller node.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule Livebook.Runtime.ErlDist.LoggerGLHandler do
|
||||||
def log(%{meta: meta} = event, %{formatter: {formatter_module, formatter_config}}) do
|
def log(%{meta: meta} = event, %{formatter: {formatter_module, formatter_config}}) do
|
||||||
message = apply(formatter_module, :format, [event, formatter_config])
|
message = apply(formatter_module, :format, [event, formatter_config])
|
||||||
|
|
||||||
if Livebook.Runtime.ErlDist.NodeManager.known_io_proxy?(meta.gl) do
|
if Livebook.Runtime.Evaluator.IOProxy.io_proxy?(meta.gl) do
|
||||||
async_io(meta.gl, message)
|
async_io(meta.gl, message)
|
||||||
else
|
else
|
||||||
send(Livebook.Runtime.ErlDist.NodeManager, {:orphan_log, message})
|
send(Livebook.Runtime.ErlDist.NodeManager, {:orphan_log, message})
|
||||||
|
@ -11,7 +11,7 @@ defmodule Livebook.Runtime.ErlDist.LoggerGLHandler do
|
||||||
end
|
end
|
||||||
|
|
||||||
def async_io(device, output) when is_pid(device) do
|
def async_io(device, output) when is_pid(device) do
|
||||||
reply_to = Livebook.Runtime.ErlDist.Sink.pid()
|
reply_to = Livebook.Runtime.ErlDist.NodeManager.sink_pid()
|
||||||
send(device, {:io_request, reply_to, make_ref(), {:put_chars, :unicode, output}})
|
send(device, {:io_request, reply_to, make_ref(), {:put_chars, :unicode, output}})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@ defmodule Livebook.Runtime.ErlDist.NodeManager do
|
||||||
alias Livebook.Runtime.ErlDist
|
alias Livebook.Runtime.ErlDist
|
||||||
|
|
||||||
@name __MODULE__
|
@name __MODULE__
|
||||||
@io_proxy_registry_name __MODULE__.IOProxyRegistry
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Starts the node manager.
|
Starts the node manager.
|
||||||
|
@ -52,21 +51,44 @@ defmodule Livebook.Runtime.ErlDist.NodeManager do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Starts a new `Livebook.Runtime.ErlDist.RuntimeServer` for evaluation.
|
Starts a new `Livebook.Runtime.ErlDist.RuntimeServer` for evaluation.
|
||||||
|
|
||||||
|
This function fails gracefully when the node manager is not running
|
||||||
|
or is about to terminate. This is why we do not use `GenServer.call/2`.
|
||||||
|
|
||||||
|
To start a runtime server we could check if the node manager is alive
|
||||||
|
and then try to call it, however it could terminate between these
|
||||||
|
operations (if the last runtime server terminated). This race condition
|
||||||
|
could happen when reconnecting to the same runtime node. To avoid
|
||||||
|
this, we combine the check and start into an atomic operation.
|
||||||
"""
|
"""
|
||||||
@spec start_runtime_server(node(), keyword()) :: pid()
|
@spec start_runtime_server(node(), keyword()) :: {:ok, pid()} | {:error, :down}
|
||||||
def start_runtime_server(node, opts \\ []) do
|
def start_runtime_server(node, opts \\ []) do
|
||||||
GenServer.call(server(node), {:start_runtime_server, opts})
|
if pid = :rpc.call(node, Process, :whereis, [@name]) do
|
||||||
end
|
ref = Process.monitor(pid)
|
||||||
|
send(pid, {:start_runtime_server, self(), ref, opts})
|
||||||
|
|
||||||
@doc false
|
receive do
|
||||||
def known_io_proxy?(pid) do
|
{:reply, ^ref, pid} ->
|
||||||
case Registry.keys(@io_proxy_registry_name, pid) do
|
Process.demonitor(ref, [:flush])
|
||||||
[_] -> true
|
{:ok, pid}
|
||||||
[] -> false
|
|
||||||
|
{:DOWN, ^ref, :process, _, _} ->
|
||||||
|
{:error, :down}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{:error, :down}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp server(node) when is_atom(node), do: {@name, node}
|
@sink_key {__MODULE__, :sink}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a process that ignores all incoming messages.
|
||||||
|
"""
|
||||||
|
@spec sink_pid() :: pid()
|
||||||
|
def sink_pid() do
|
||||||
|
:persistent_term.get(@sink_key)
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(opts) do
|
def init(opts) do
|
||||||
|
@ -77,13 +99,17 @@ defmodule Livebook.Runtime.ErlDist.NodeManager do
|
||||||
|
|
||||||
## Initialize the node
|
## Initialize the node
|
||||||
|
|
||||||
|
# Note that we intentionally do not name any processes other than
|
||||||
|
# the manager itself. This way, when the manager terminates, another
|
||||||
|
# one can be started immediately without the possibility of the
|
||||||
|
# linked processes to be still around and cause name conflicts.
|
||||||
|
# This scenario could be the case when reconnecting to the same
|
||||||
|
# runtime node.
|
||||||
|
|
||||||
Process.flag(:trap_exit, true)
|
Process.flag(:trap_exit, true)
|
||||||
|
|
||||||
{:ok, server_supervisor} = DynamicSupervisor.start_link(strategy: :one_for_one)
|
{:ok, server_supervisor} = DynamicSupervisor.start_link(strategy: :one_for_one)
|
||||||
|
|
||||||
{:ok, io_proxy_registry} =
|
|
||||||
Registry.start_link(name: @io_proxy_registry_name, keys: :duplicate)
|
|
||||||
|
|
||||||
# Register our own standard error IO device that proxies to
|
# Register our own standard error IO device that proxies to
|
||||||
# sender's group leader.
|
# sender's group leader.
|
||||||
original_standard_error = Process.whereis(:standard_error)
|
original_standard_error = Process.whereis(:standard_error)
|
||||||
|
@ -91,7 +117,7 @@ defmodule Livebook.Runtime.ErlDist.NodeManager do
|
||||||
Process.unregister(:standard_error)
|
Process.unregister(:standard_error)
|
||||||
Process.register(io_forward_gl_pid, :standard_error)
|
Process.register(io_forward_gl_pid, :standard_error)
|
||||||
|
|
||||||
{:ok, _pid} = Livebook.Runtime.ErlDist.Sink.start_link()
|
:persistent_term.put(@sink_key, spawn_link(&sink_loop/0))
|
||||||
|
|
||||||
:logger.add_handler(:livebook_gl_handler, Livebook.Runtime.ErlDist.LoggerGLHandler, %{
|
:logger.add_handler(:livebook_gl_handler, Livebook.Runtime.ErlDist.LoggerGLHandler, %{
|
||||||
formatter: Logger.Formatter.new(),
|
formatter: Logger.Formatter.new(),
|
||||||
|
@ -131,8 +157,7 @@ defmodule Livebook.Runtime.ErlDist.NodeManager do
|
||||||
original_standard_error: original_standard_error,
|
original_standard_error: original_standard_error,
|
||||||
parent_node: parent_node,
|
parent_node: parent_node,
|
||||||
capture_orphan_logs: capture_orphan_logs,
|
capture_orphan_logs: capture_orphan_logs,
|
||||||
tmp_dir: tmp_dir,
|
tmp_dir: tmp_dir
|
||||||
io_proxy_registry: io_proxy_registry
|
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -151,6 +176,8 @@ defmodule Livebook.Runtime.ErlDist.NodeManager do
|
||||||
|
|
||||||
:logger.remove_handler(:livebook_gl_handler)
|
:logger.remove_handler(:livebook_gl_handler)
|
||||||
|
|
||||||
|
:persistent_term.erase(@sink_key)
|
||||||
|
|
||||||
if state.unload_modules_on_termination do
|
if state.unload_modules_on_termination do
|
||||||
ErlDist.unload_required_modules()
|
ErlDist.unload_required_modules()
|
||||||
end
|
end
|
||||||
|
@ -193,25 +220,26 @@ defmodule Livebook.Runtime.ErlDist.NodeManager do
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(_message, state), do: {:noreply, state}
|
def handle_info({:start_runtime_server, pid, ref, opts}, state) do
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_call({:start_runtime_server, opts}, _from, state) do
|
|
||||||
opts =
|
opts =
|
||||||
opts
|
opts
|
||||||
|> Keyword.put_new(:ebin_path, ebin_path(state.tmp_dir))
|
|> Keyword.put_new(:ebin_path, ebin_path(state.tmp_dir))
|
||||||
|> Keyword.put_new(:tmp_dir, child_tmp_dir(state.tmp_dir))
|
|> Keyword.put_new(:tmp_dir, child_tmp_dir(state.tmp_dir))
|
||||||
|> Keyword.put_new(:base_path_env, System.get_env("PATH", ""))
|
|> Keyword.put_new(:base_path_env, System.get_env("PATH", ""))
|
||||||
|> Keyword.put_new(:io_proxy_registry, @io_proxy_registry_name)
|
|
||||||
|
|
||||||
{:ok, server_pid} =
|
{:ok, server_pid} =
|
||||||
DynamicSupervisor.start_child(state.server_supervisor, {ErlDist.RuntimeServer, opts})
|
DynamicSupervisor.start_child(state.server_supervisor, {ErlDist.RuntimeServer, opts})
|
||||||
|
|
||||||
Process.monitor(server_pid)
|
Process.monitor(server_pid)
|
||||||
state = update_in(state.runtime_servers, &[server_pid | &1])
|
state = update_in(state.runtime_servers, &[server_pid | &1])
|
||||||
{:reply, server_pid, state}
|
|
||||||
|
send(pid, {:reply, ref, server_pid})
|
||||||
|
|
||||||
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info(_message, state), do: {:noreply, state}
|
||||||
|
|
||||||
defp make_tmp_dir() do
|
defp make_tmp_dir() do
|
||||||
path = Path.join([System.tmp_dir!(), "livebook_runtime", random_long_id()])
|
path = Path.join([System.tmp_dir!(), "livebook_runtime", random_long_id()])
|
||||||
|
|
||||||
|
@ -229,4 +257,10 @@ defmodule Livebook.Runtime.ErlDist.NodeManager do
|
||||||
defp random_long_id() do
|
defp random_long_id() do
|
||||||
:crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower)
|
:crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp sink_loop() do
|
||||||
|
receive do
|
||||||
|
_ -> sink_loop()
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -49,9 +49,6 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
||||||
to merge new values into when setting environment variables.
|
to merge new values into when setting environment variables.
|
||||||
Defaults to `System.get_env("PATH", "")`
|
Defaults to `System.get_env("PATH", "")`
|
||||||
|
|
||||||
* `:io_proxy_registry` - the registry to register IO proxy
|
|
||||||
processes in
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def start_link(opts \\ []) do
|
def start_link(opts \\ []) do
|
||||||
GenServer.start_link(__MODULE__, opts)
|
GenServer.start_link(__MODULE__, opts)
|
||||||
|
@ -269,14 +266,6 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
||||||
GenServer.call(pid, {:has_dependencies?, dependencies})
|
GenServer.call(pid, {:has_dependencies?, dependencies})
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Disables dependencies cache globally.
|
|
||||||
"""
|
|
||||||
@spec disable_dependencies_cache(pid()) :: :ok
|
|
||||||
def disable_dependencies_cache(pid) do
|
|
||||||
GenServer.cast(pid, :disable_dependencies_cache)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Sets the given environment variables.
|
Sets the given environment variables.
|
||||||
"""
|
"""
|
||||||
|
@ -378,7 +367,6 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
||||||
base_env_path:
|
base_env_path:
|
||||||
Keyword.get_lazy(opts, :base_env_path, fn -> System.get_env("PATH", "") end),
|
Keyword.get_lazy(opts, :base_env_path, fn -> System.get_env("PATH", "") end),
|
||||||
ebin_path: Keyword.get(opts, :ebin_path),
|
ebin_path: Keyword.get(opts, :ebin_path),
|
||||||
io_proxy_registry: Keyword.get(opts, :io_proxy_registry),
|
|
||||||
tmp_dir: Keyword.get(opts, :tmp_dir),
|
tmp_dir: Keyword.get(opts, :tmp_dir),
|
||||||
mix_install_project_dir: nil
|
mix_install_project_dir: nil
|
||||||
}}
|
}}
|
||||||
|
@ -391,7 +379,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
||||||
if state.owner do
|
if state.owner do
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
else
|
else
|
||||||
{:stop, :no_owner, state}
|
{:stop, {:shutdown, :no_owner}, state}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -656,12 +644,6 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_cast(:disable_dependencies_cache, state) do
|
|
||||||
System.put_env("MIX_INSTALL_FORCE", "true")
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_cast({:put_system_envs, envs}, state) do
|
def handle_cast({:put_system_envs, envs}, state) do
|
||||||
envs
|
envs
|
||||||
|> Enum.map(fn
|
|> Enum.map(fn
|
||||||
|
@ -799,8 +781,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
|
||||||
object_tracker: state.object_tracker,
|
object_tracker: state.object_tracker,
|
||||||
client_tracker: state.client_tracker,
|
client_tracker: state.client_tracker,
|
||||||
ebin_path: state.ebin_path,
|
ebin_path: state.ebin_path,
|
||||||
tmp_dir: evaluator_tmp_dir(state),
|
tmp_dir: evaluator_tmp_dir(state)
|
||||||
io_proxy_registry: state.io_proxy_registry
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Process.monitor(evaluator.pid)
|
Process.monitor(evaluator.pid)
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
defmodule Livebook.Runtime.ErlDist.Sink do
|
|
||||||
# An idle process that ignores all incoming messages.
|
|
||||||
|
|
||||||
use GenServer
|
|
||||||
|
|
||||||
@name __MODULE__
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Starts the process.
|
|
||||||
"""
|
|
||||||
@spec start_link() :: GenServer.on_start()
|
|
||||||
def start_link() do
|
|
||||||
GenServer.start_link(__MODULE__, {}, name: @name)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Returns pid of the global sink process.
|
|
||||||
"""
|
|
||||||
@spec pid() :: pid()
|
|
||||||
def pid() do
|
|
||||||
Process.whereis(@name)
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def init({}) do
|
|
||||||
{:ok, {}}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info(_message, state) do
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
end
|
|
|
@ -88,9 +88,6 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
* `:tmp_dir` - a temporary directory for arbitrary use during
|
* `:tmp_dir` - a temporary directory for arbitrary use during
|
||||||
evaluation
|
evaluation
|
||||||
|
|
||||||
* `:io_proxy_registry` - the registry to register IO proxy
|
|
||||||
processes in
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
@spec start_link(keyword()) :: {:ok, pid(), t()} | {:error, term()}
|
@spec start_link(keyword()) :: {:ok, pid(), t()} | {:error, term()}
|
||||||
def start_link(opts \\ []) do
|
def start_link(opts \\ []) do
|
||||||
|
@ -273,7 +270,6 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
client_tracker = Keyword.fetch!(opts, :client_tracker)
|
client_tracker = Keyword.fetch!(opts, :client_tracker)
|
||||||
ebin_path = Keyword.get(opts, :ebin_path)
|
ebin_path = Keyword.get(opts, :ebin_path)
|
||||||
tmp_dir = Keyword.get(opts, :tmp_dir)
|
tmp_dir = Keyword.get(opts, :tmp_dir)
|
||||||
io_proxy_registry = Keyword.get(opts, :io_proxy_registry)
|
|
||||||
|
|
||||||
{:ok, io_proxy} =
|
{:ok, io_proxy} =
|
||||||
Evaluator.IOProxy.start(%{
|
Evaluator.IOProxy.start(%{
|
||||||
|
@ -283,8 +279,7 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
object_tracker: object_tracker,
|
object_tracker: object_tracker,
|
||||||
client_tracker: client_tracker,
|
client_tracker: client_tracker,
|
||||||
ebin_path: ebin_path,
|
ebin_path: ebin_path,
|
||||||
tmp_dir: tmp_dir,
|
tmp_dir: tmp_dir
|
||||||
registry: io_proxy_registry
|
|
||||||
})
|
})
|
||||||
|
|
||||||
io_proxy_monitor = Process.monitor(io_proxy)
|
io_proxy_monitor = Process.monitor(io_proxy)
|
||||||
|
@ -430,6 +425,10 @@ defmodule Livebook.Runtime.Evaluator do
|
||||||
|
|
||||||
set_pdict(context, state.ignored_pdict_keys)
|
set_pdict(context, state.ignored_pdict_keys)
|
||||||
|
|
||||||
|
if opts[:disable_dependencies_cache] do
|
||||||
|
System.put_env("MIX_INSTALL_FORCE", "true")
|
||||||
|
end
|
||||||
|
|
||||||
start_time = System.monotonic_time()
|
start_time = System.monotonic_time()
|
||||||
{eval_result, code_markers} = eval(language, code, context.binding, context.env)
|
{eval_result, code_markers} = eval(language, code, context.binding, context.env)
|
||||||
evaluation_time_ms = time_diff_ms(start_time)
|
evaluation_time_ms = time_diff_ms(start_time)
|
||||||
|
|
|
@ -31,8 +31,7 @@ defmodule Livebook.Runtime.Evaluator.IOProxy do
|
||||||
object_tracker: pid(),
|
object_tracker: pid(),
|
||||||
client_tracker: pid(),
|
client_tracker: pid(),
|
||||||
ebin_path: String.t() | nil,
|
ebin_path: String.t() | nil,
|
||||||
tmp_dir: String.t() | nil,
|
tmp_dir: String.t() | nil
|
||||||
registry: atom() | nil
|
|
||||||
}) :: GenServer.on_start()
|
}) :: GenServer.on_start()
|
||||||
def start(args) do
|
def start(args) do
|
||||||
GenServer.start(__MODULE__, args)
|
GenServer.start(__MODULE__, args)
|
||||||
|
@ -71,6 +70,28 @@ defmodule Livebook.Runtime.Evaluator.IOProxy do
|
||||||
GenServer.cast(pid, {:tracer_updates, updates})
|
GenServer.cast(pid, {:tracer_updates, updates})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Checks if the given process is a IO proxy.
|
||||||
|
|
||||||
|
The check happens against the process dictionary.
|
||||||
|
"""
|
||||||
|
def io_proxy?(pid) do
|
||||||
|
process_get_key(pid, :io_proxy) == true
|
||||||
|
end
|
||||||
|
|
||||||
|
defp process_get_key(pid, key) do
|
||||||
|
try do
|
||||||
|
case Process.info(pid, {:dictionary, key}) do
|
||||||
|
{{:dictionary, ^key}, :undefined} -> nil
|
||||||
|
{{:dictionary, ^key}, value} -> value
|
||||||
|
nil -> nil
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
# TODO: remove error handler once we require OTP 26.2
|
||||||
|
_ -> Process.info(pid, [:dictionary])[:dictionary][key]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(args) do
|
def init(args) do
|
||||||
%{
|
%{
|
||||||
|
@ -80,15 +101,12 @@ defmodule Livebook.Runtime.Evaluator.IOProxy do
|
||||||
object_tracker: object_tracker,
|
object_tracker: object_tracker,
|
||||||
client_tracker: client_tracker,
|
client_tracker: client_tracker,
|
||||||
ebin_path: ebin_path,
|
ebin_path: ebin_path,
|
||||||
tmp_dir: tmp_dir,
|
tmp_dir: tmp_dir
|
||||||
registry: registry
|
|
||||||
} = args
|
} = args
|
||||||
|
|
||||||
evaluator_monitor = Process.monitor(evaluator)
|
evaluator_monitor = Process.monitor(evaluator)
|
||||||
|
|
||||||
if registry do
|
Process.put(:io_proxy, true)
|
||||||
Registry.register(registry, nil, nil)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
|
|
467
lib/livebook/runtime/fly.ex
Normal file
467
lib/livebook/runtime/fly.ex
Normal file
|
@ -0,0 +1,467 @@
|
||||||
|
defmodule Livebook.Runtime.Fly do
|
||||||
|
# A runtime backed by a Fly.io machine managed by Livebook.
|
||||||
|
#
|
||||||
|
# This runtime uses a Livebook-managed Elixir node, similarly to
|
||||||
|
# the Standalone runtime, however it runs on a temporary Fly.io
|
||||||
|
# machine. The machine is configured to automatically shutdown
|
||||||
|
# as soon as the runtime is disconnected.
|
||||||
|
#
|
||||||
|
# Note: this runtime requires `flyctl` executable to be available
|
||||||
|
# in the system.
|
||||||
|
#
|
||||||
|
# ## Communication
|
||||||
|
#
|
||||||
|
# The machine runs the Livebook Docker image and we configure it to
|
||||||
|
# invoke the start_runtime.exs script, by setting LIVEBOOK_RUNTIME.
|
||||||
|
# This environment variable also includes encoded information passed
|
||||||
|
# from the parent. Once the Elixir node starts on the machine, it
|
||||||
|
# waits for the parent to connect and finish the initialization.
|
||||||
|
#
|
||||||
|
# Now, we want to establish a distribution connection from the local
|
||||||
|
# Livebook node to the node on the Fly.io machine. We could reach
|
||||||
|
# the node directly, by requiring the user to set up WireGuard.
|
||||||
|
# However, that would require the user to install WireGuard and go
|
||||||
|
# through a few configuration steps. Instead, we use flyctl proxy
|
||||||
|
# feature and only require flyctl to be installed.
|
||||||
|
#
|
||||||
|
# With flyctl proxy, we proxy a local port to the the distribution
|
||||||
|
# port of the Fly.io node. Then, in our EPMD module (`Livebook.EPMD`)
|
||||||
|
# we special case those nodes in two ways: (1) we infer the
|
||||||
|
# distribution port from the node name; (2) we resolve the node
|
||||||
|
# address to loopback, ignoring its hostname.
|
||||||
|
#
|
||||||
|
# ### Distribution protocol
|
||||||
|
#
|
||||||
|
# Usually, nodes need to be configured to use the same distribution
|
||||||
|
# protocol (`-proto_dist`). We configure the Fly.io node to use IPv6
|
||||||
|
# distribution (`-proto_dist inet6_tcp`). However, depending whether
|
||||||
|
# the local node runs IPv4 or IPv6 distribution, we configure the
|
||||||
|
# flyctl proxy to bind to a IPv4 or IPv6 loopback respectively. The
|
||||||
|
# proxy always communicates with the Fly.io machine over IPv6, as
|
||||||
|
# is the case with all internal networking. Consequently, regardless
|
||||||
|
# of the protocol used by the local node, the remote node perceives
|
||||||
|
# it as IPv6.
|
||||||
|
#
|
||||||
|
# Sidenote, a node using IPv6 distribution may accept connections
|
||||||
|
# from a node using IPv4, depending on the `:kernel` application
|
||||||
|
# configuration `inet_dist_listen_options` -> `ipv6_v6only`, which
|
||||||
|
# has OS-specific value. However, we don't rely on this here.
|
||||||
|
|
||||||
|
defstruct [:config, :node, :server_pid]
|
||||||
|
|
||||||
|
use GenServer, restart: :temporary
|
||||||
|
|
||||||
|
require Logger
|
||||||
|
|
||||||
|
@type t :: %__MODULE__{
|
||||||
|
config: config(),
|
||||||
|
node: node() | nil,
|
||||||
|
server_pid: pid() | nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@type config :: %{
|
||||||
|
token: String.t(),
|
||||||
|
app_name: String.t(),
|
||||||
|
region: String.t(),
|
||||||
|
cpu_kind: String.t(),
|
||||||
|
cpus: pos_integer(),
|
||||||
|
memory_gb: pos_integer(),
|
||||||
|
gpu_kind: String.t() | nil,
|
||||||
|
gpus: pos_integer() | nil,
|
||||||
|
volume_id: String.t() | nil,
|
||||||
|
docker_tag: String.t()
|
||||||
|
}
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Returns a new runtime instance.
|
||||||
|
"""
|
||||||
|
@spec new(config()) :: t()
|
||||||
|
def new(config) do
|
||||||
|
%__MODULE__{config: config}
|
||||||
|
end
|
||||||
|
|
||||||
|
def __connect__(runtime) do
|
||||||
|
{:ok, pid} =
|
||||||
|
DynamicSupervisor.start_child(Livebook.RuntimeSupervisor, {__MODULE__, {runtime, self()}})
|
||||||
|
|
||||||
|
pid
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def start_link({runtime, caller}) do
|
||||||
|
GenServer.start_link(__MODULE__, {runtime, caller})
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def init({runtime, caller}) do
|
||||||
|
state = %{primary_ref: nil, proxy_port: nil}
|
||||||
|
{:ok, state, {:continue, {:init, runtime, caller}}}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_continue({:init, runtime, caller}, state) do
|
||||||
|
config = runtime.config
|
||||||
|
local_port = get_free_port!()
|
||||||
|
remote_port = 44444
|
||||||
|
node_base = "remote_runtime_#{local_port}"
|
||||||
|
|
||||||
|
runtime_data =
|
||||||
|
%{
|
||||||
|
node_base: node_base,
|
||||||
|
cookie: Node.get_cookie(),
|
||||||
|
dist_port: remote_port
|
||||||
|
}
|
||||||
|
|> :erlang.term_to_binary()
|
||||||
|
|> Base.encode64()
|
||||||
|
|
||||||
|
with {:ok, machine_id, machine_ip} <-
|
||||||
|
with_log(caller, "create machine", fn ->
|
||||||
|
create_machine(config, runtime_data)
|
||||||
|
end),
|
||||||
|
child_node <- :"#{node_base}@#{machine_id}.vm.#{config.app_name}.internal",
|
||||||
|
{:ok, proxy_port} <-
|
||||||
|
with_log(caller, "start proxy", fn ->
|
||||||
|
start_fly_proxy(config.app_name, machine_ip, local_port, remote_port, config.token)
|
||||||
|
end),
|
||||||
|
:ok <-
|
||||||
|
with_log(caller, "connect to node", fn ->
|
||||||
|
connect_loop(child_node, 40, 250)
|
||||||
|
end),
|
||||||
|
{:ok, primary_pid} <- fetch_runtime_info(child_node) do
|
||||||
|
primary_ref = Process.monitor(primary_pid)
|
||||||
|
|
||||||
|
server_pid =
|
||||||
|
with_log(caller, "initialize node", fn ->
|
||||||
|
initialize_node(child_node)
|
||||||
|
end)
|
||||||
|
|
||||||
|
send(primary_pid, :node_initialized)
|
||||||
|
|
||||||
|
runtime = %{runtime | node: child_node, server_pid: server_pid}
|
||||||
|
send(caller, {:runtime_connect_done, self(), {:ok, runtime}})
|
||||||
|
|
||||||
|
{:noreply, %{state | primary_ref: primary_ref, proxy_port: proxy_port}}
|
||||||
|
else
|
||||||
|
{:error, error} ->
|
||||||
|
send(caller, {:runtime_connect_done, self(), {:error, error}})
|
||||||
|
|
||||||
|
{:stop, :shutdown, state}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_info({:DOWN, ref, :process, _pid, _reason}, state) when ref == state.primary_ref do
|
||||||
|
{:stop, :shutdown, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({port, _message}, state) when state.proxy_port == port do
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_machine(config, runtime_data) do
|
||||||
|
base_image = Enum.find(Livebook.Config.docker_images(), &(&1.tag == config.docker_tag))
|
||||||
|
image = "ghcr.io/livebook-dev/livebook:#{base_image.tag}"
|
||||||
|
|
||||||
|
env =
|
||||||
|
Map.merge(
|
||||||
|
Map.new(base_image.env),
|
||||||
|
%{
|
||||||
|
"LIVEBOOK_RUNTIME" => runtime_data,
|
||||||
|
"ERL_AFLAGS" => "-proto_dist inet6_tcp"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
name = "#{config.app_name}-livebook-runtime-#{Livebook.Utils.random_id()}"
|
||||||
|
|
||||||
|
machine_config = %{
|
||||||
|
image: image,
|
||||||
|
guest: %{
|
||||||
|
cpu_kind: config.cpu_kind,
|
||||||
|
cpus: config.cpus,
|
||||||
|
memory_mb: config.memory_gb * 1024,
|
||||||
|
gpu_kind: config.gpu_kind,
|
||||||
|
gpus: config.gpus
|
||||||
|
},
|
||||||
|
mounts: config.volume_id && [%{volume: config.volume_id, path: "/home/livebook"}],
|
||||||
|
auto_destroy: true,
|
||||||
|
restart: %{policy: "no"},
|
||||||
|
env: env
|
||||||
|
}
|
||||||
|
|
||||||
|
case Livebook.FlyAPI.create_machine(
|
||||||
|
config.token,
|
||||||
|
config.app_name,
|
||||||
|
name,
|
||||||
|
config.region,
|
||||||
|
machine_config
|
||||||
|
) do
|
||||||
|
{:ok, %{id: machine_id, private_ip: machine_ip}} ->
|
||||||
|
{:ok, machine_id, machine_ip}
|
||||||
|
|
||||||
|
{:error, %{message: message}} ->
|
||||||
|
{:error, "could not create machine, reason: #{message}"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp connect_loop(_node, 0, _interval) do
|
||||||
|
{:error, "could not establish connection with the node"}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp connect_loop(node, attempts, interval) do
|
||||||
|
if Node.connect(node) do
|
||||||
|
:ok
|
||||||
|
else
|
||||||
|
Process.sleep(interval)
|
||||||
|
connect_loop(node, attempts - 1, interval)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp get_free_port!() do
|
||||||
|
{:ok, socket} = :gen_tcp.listen(0, active: false, reuseaddr: true)
|
||||||
|
{:ok, port} = :inet.port(socket)
|
||||||
|
:gen_tcp.close(socket)
|
||||||
|
port
|
||||||
|
end
|
||||||
|
|
||||||
|
defp start_fly_proxy(app_name, host, local_port, remote_port, token) do
|
||||||
|
with {:ok, flyctl_path} <- find_fly_executable() do
|
||||||
|
ports = "#{local_port}:#{remote_port}"
|
||||||
|
|
||||||
|
# We want the proxy to accept the same protocol that we are
|
||||||
|
# going to use for distribution
|
||||||
|
bind_addr =
|
||||||
|
if Livebook.Utils.proto_dist() == :inet6_tcp do
|
||||||
|
"[::1]"
|
||||||
|
else
|
||||||
|
"127.0.0.1"
|
||||||
|
end
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"proxy",
|
||||||
|
ports,
|
||||||
|
host,
|
||||||
|
"--app",
|
||||||
|
app_name,
|
||||||
|
"--bind-addr",
|
||||||
|
bind_addr,
|
||||||
|
"--access-token",
|
||||||
|
token,
|
||||||
|
"--watch-stdin"
|
||||||
|
]
|
||||||
|
|
||||||
|
port =
|
||||||
|
Port.open(
|
||||||
|
{:spawn_executable, flyctl_path},
|
||||||
|
[:binary, :hide, :stderr_to_stdout, args: args]
|
||||||
|
)
|
||||||
|
|
||||||
|
port_ref = Port.monitor(port)
|
||||||
|
|
||||||
|
result =
|
||||||
|
receive do
|
||||||
|
{^port, {:data, "Proxying " <> _}} ->
|
||||||
|
{:ok, port}
|
||||||
|
|
||||||
|
{^port, {:data, "Error: unknown flag: --watch-stdin\n"}} ->
|
||||||
|
{:error,
|
||||||
|
"failed to open fly proxy, because the current version " <>
|
||||||
|
"is missing a required feature. Please update flyctl"}
|
||||||
|
|
||||||
|
{^port, {:data, "Error: " <> error}} ->
|
||||||
|
{:error, "failed to open fly proxy. Error: #{String.trim(error)}"}
|
||||||
|
|
||||||
|
{:DOWN, ^port_ref, :port, _object, reason} ->
|
||||||
|
{:error, "failed to open fly proxy. Process terminated, reason: #{inspect(reason)}"}
|
||||||
|
after
|
||||||
|
30_000 ->
|
||||||
|
{:error, "failed to open fly proxy. Timed out after 30s"}
|
||||||
|
end
|
||||||
|
|
||||||
|
Port.demonitor(port_ref, [:flush])
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp find_fly_executable() do
|
||||||
|
if path = System.find_executable("flyctl") do
|
||||||
|
{:ok, path}
|
||||||
|
else
|
||||||
|
{:error,
|
||||||
|
"no flyctl executable found in PATH. For installation instructions" <>
|
||||||
|
" refer to https://fly.io/docs/flyctl/install"}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fetch_runtime_info(child_node) do
|
||||||
|
# Note: it is Livebook that starts the runtime node, so we know
|
||||||
|
# that the node runs Livebook release of the exact same version
|
||||||
|
|
||||||
|
%{
|
||||||
|
pid: pid,
|
||||||
|
elixir_version: elixir_version
|
||||||
|
} = :erpc.call(child_node, :persistent_term, :get, [:livebook_runtime_info])
|
||||||
|
|
||||||
|
if elixir_version != System.version() do
|
||||||
|
{:error,
|
||||||
|
"the local Elixir version (#{inspect(System.version())}) does not" <>
|
||||||
|
" match the one used by the runtime (#{elixir_version})"}
|
||||||
|
else
|
||||||
|
{:ok, pid}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp initialize_node(child_node) do
|
||||||
|
init_opts = [
|
||||||
|
runtime_server_opts: [
|
||||||
|
extra_smart_cell_definitions: Livebook.Runtime.Definitions.smart_cell_definitions()
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
Livebook.Runtime.ErlDist.initialize(child_node, init_opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp with_log(caller, name, fun) do
|
||||||
|
send(caller, {:runtime_connect_info, self(), name})
|
||||||
|
|
||||||
|
{microseconds, result} = :timer.tc(fun)
|
||||||
|
milliseconds = div(microseconds, 1000)
|
||||||
|
|
||||||
|
case result do
|
||||||
|
{:error, error} ->
|
||||||
|
Logger.debug("[fly runtime] #{name} FAILED in #{milliseconds}ms, error: #{error}")
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
Logger.debug("[fly runtime] #{name} finished in #{milliseconds}ms")
|
||||||
|
end
|
||||||
|
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defimpl Livebook.Runtime, for: Livebook.Runtime.Fly do
|
||||||
|
alias Livebook.Runtime.ErlDist.RuntimeServer
|
||||||
|
|
||||||
|
def describe(runtime) do
|
||||||
|
[{"Type", "Fly.io machine"}] ++
|
||||||
|
if runtime.node do
|
||||||
|
[{"Node name", Atom.to_string(runtime.node)}]
|
||||||
|
else
|
||||||
|
[]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def connect(runtime) do
|
||||||
|
Livebook.Runtime.Fly.__connect__(runtime)
|
||||||
|
end
|
||||||
|
|
||||||
|
def take_ownership(runtime, opts \\ []) do
|
||||||
|
RuntimeServer.attach(runtime.server_pid, self(), opts)
|
||||||
|
Process.monitor(runtime.server_pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disconnect(runtime) do
|
||||||
|
:ok = RuntimeServer.stop(runtime.server_pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
def duplicate(runtime) do
|
||||||
|
Livebook.Runtime.Fly.new(runtime.config)
|
||||||
|
end
|
||||||
|
|
||||||
|
def evaluate_code(runtime, language, code, locator, parent_locators, opts \\ []) do
|
||||||
|
RuntimeServer.evaluate_code(
|
||||||
|
runtime.server_pid,
|
||||||
|
language,
|
||||||
|
code,
|
||||||
|
locator,
|
||||||
|
parent_locators,
|
||||||
|
opts
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def forget_evaluation(runtime, locator) do
|
||||||
|
RuntimeServer.forget_evaluation(runtime.server_pid, locator)
|
||||||
|
end
|
||||||
|
|
||||||
|
def drop_container(runtime, container_ref) do
|
||||||
|
RuntimeServer.drop_container(runtime.server_pid, container_ref)
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_intellisense(runtime, send_to, request, parent_locators, node) do
|
||||||
|
RuntimeServer.handle_intellisense(runtime.server_pid, send_to, request, parent_locators, node)
|
||||||
|
end
|
||||||
|
|
||||||
|
def read_file(runtime, path) do
|
||||||
|
RuntimeServer.read_file(runtime.server_pid, path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def transfer_file(runtime, path, file_id, callback) do
|
||||||
|
RuntimeServer.transfer_file(runtime.server_pid, path, file_id, callback)
|
||||||
|
end
|
||||||
|
|
||||||
|
def relabel_file(runtime, file_id, new_file_id) do
|
||||||
|
RuntimeServer.relabel_file(runtime.server_pid, file_id, new_file_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def revoke_file(runtime, file_id) do
|
||||||
|
RuntimeServer.revoke_file(runtime.server_pid, file_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
def start_smart_cell(runtime, kind, ref, attrs, parent_locators) do
|
||||||
|
RuntimeServer.start_smart_cell(runtime.server_pid, kind, ref, attrs, parent_locators)
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_smart_cell_parent_locators(runtime, ref, parent_locators) do
|
||||||
|
RuntimeServer.set_smart_cell_parent_locators(runtime.server_pid, ref, parent_locators)
|
||||||
|
end
|
||||||
|
|
||||||
|
def stop_smart_cell(runtime, ref) do
|
||||||
|
RuntimeServer.stop_smart_cell(runtime.server_pid, ref)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fixed_dependencies?(_runtime), do: false
|
||||||
|
|
||||||
|
def add_dependencies(_runtime, code, dependencies) do
|
||||||
|
Livebook.Runtime.Dependencies.add_dependencies(code, dependencies)
|
||||||
|
end
|
||||||
|
|
||||||
|
def has_dependencies?(runtime, dependencies) do
|
||||||
|
RuntimeServer.has_dependencies?(runtime.server_pid, dependencies)
|
||||||
|
end
|
||||||
|
|
||||||
|
def snippet_definitions(_runtime) do
|
||||||
|
Livebook.Runtime.Definitions.snippet_definitions()
|
||||||
|
end
|
||||||
|
|
||||||
|
def search_packages(_runtime, send_to, search) do
|
||||||
|
Livebook.Runtime.Dependencies.search_packages_on_hex(send_to, search)
|
||||||
|
end
|
||||||
|
|
||||||
|
def put_system_envs(runtime, envs) do
|
||||||
|
RuntimeServer.put_system_envs(runtime.server_pid, envs)
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_system_envs(runtime, names) do
|
||||||
|
RuntimeServer.delete_system_envs(runtime.server_pid, names)
|
||||||
|
end
|
||||||
|
|
||||||
|
def restore_transient_state(runtime, transient_state) do
|
||||||
|
RuntimeServer.restore_transient_state(runtime.server_pid, transient_state)
|
||||||
|
end
|
||||||
|
|
||||||
|
def register_clients(runtime, clients) do
|
||||||
|
RuntimeServer.register_clients(runtime.server_pid, clients)
|
||||||
|
end
|
||||||
|
|
||||||
|
def unregister_clients(runtime, client_ids) do
|
||||||
|
RuntimeServer.unregister_clients(runtime.server_pid, client_ids)
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_proxy_handler_spec(runtime) do
|
||||||
|
RuntimeServer.fetch_proxy_handler_spec(runtime.server_pid)
|
||||||
|
end
|
||||||
|
|
||||||
|
def disconnect_node(runtime, node) do
|
||||||
|
RuntimeServer.disconnect_node(runtime.server_pid, node)
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Livebook.Runtime.ElixirStandalone do
|
defmodule Livebook.Runtime.Standalone do
|
||||||
defstruct [:node, :server_pid]
|
defstruct [:node, :server_pid]
|
||||||
|
|
||||||
# A runtime backed by a standalone Elixir node managed by Livebook.
|
# A runtime backed by a standalone Elixir node managed by Livebook.
|
||||||
|
@ -7,6 +7,26 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
||||||
# Most importantly we have to make sure the started node doesn't
|
# Most importantly we have to make sure the started node doesn't
|
||||||
# stay in the system when the session or the entire Livebook
|
# stay in the system when the session or the entire Livebook
|
||||||
# terminates.
|
# terminates.
|
||||||
|
#
|
||||||
|
# Note: this runtime requires `elixir` executable to be available in
|
||||||
|
# the system.
|
||||||
|
#
|
||||||
|
# ## Connecting
|
||||||
|
#
|
||||||
|
# Connecting the runtime starts a new Elixir node (a system process).
|
||||||
|
# That child node connects back to the parent and notifies that it
|
||||||
|
# is ready by sending a `:node_started` message. Next, the parent
|
||||||
|
# initializes the child node by loading the necessary modules and
|
||||||
|
# starting processes, in particular the node manager and one runtime
|
||||||
|
# server. Once done, the parent sends a `:node_initialized` message
|
||||||
|
# to the child, and the child starts monitoring the node manager.
|
||||||
|
# Once the node manager terminates, the node shuts down.
|
||||||
|
#
|
||||||
|
# If no process calls `Livebook.Runtime.take_ownership/1` for a
|
||||||
|
# period of time, the node automatically terminates. Whoever takes
|
||||||
|
# the ownership, becomes the owner and as soon as it terminates,
|
||||||
|
# the node shuts down. The node may also be shut down by calling
|
||||||
|
# `Livebook.Runtime.disconnect/1`.
|
||||||
|
|
||||||
alias Livebook.Utils
|
alias Livebook.Utils
|
||||||
|
|
||||||
|
@ -23,20 +43,19 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
||||||
%__MODULE__{}
|
%__MODULE__{}
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
def __connect__(runtime) do
|
||||||
Starts a new Elixir node (a system process) and initializes it with
|
caller = self()
|
||||||
Livebook-specific modules and processes.
|
|
||||||
|
|
||||||
If no process calls `Runtime.take_ownership/1` for a period of time,
|
{:ok, pid} =
|
||||||
the node automatically terminates. Whoever takes the ownersihp,
|
DynamicSupervisor.start_child(
|
||||||
becomes the owner and as soon as it terminates, the node terminates
|
Livebook.RuntimeSupervisor,
|
||||||
as well. The node may also be terminated by calling `Runtime.disconnect/1`.
|
{Task, fn -> do_connect(runtime, caller) end}
|
||||||
|
)
|
||||||
|
|
||||||
Note: to start the node it is required that `elixir` is a recognised
|
pid
|
||||||
executable within the system.
|
end
|
||||||
"""
|
|
||||||
@spec connect(t()) :: {:ok, t()} | {:error, String.t()}
|
defp do_connect(runtime, caller) do
|
||||||
def connect(runtime) do
|
|
||||||
child_node = Livebook.EPMD.random_child_node()
|
child_node = Livebook.EPMD.random_child_node()
|
||||||
|
|
||||||
Utils.temporarily_register(self(), child_node, fn ->
|
Utils.temporarily_register(self(), child_node, fn ->
|
||||||
|
@ -50,9 +69,10 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
||||||
port = start_elixir_node(elixir_path, child_node),
|
port = start_elixir_node(elixir_path, child_node),
|
||||||
{:ok, server_pid} <- parent_init_sequence(child_node, port, init_opts) do
|
{:ok, server_pid} <- parent_init_sequence(child_node, port, init_opts) do
|
||||||
runtime = %{runtime | node: child_node, server_pid: server_pid}
|
runtime = %{runtime | node: child_node, server_pid: server_pid}
|
||||||
{:ok, runtime}
|
send(caller, {:runtime_connect_done, self(), {:ok, runtime}})
|
||||||
else
|
else
|
||||||
{:error, error} -> {:error, error}
|
{:error, error} ->
|
||||||
|
send(caller, {:runtime_connect_done, self(), {:error, error}})
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
@ -77,31 +97,6 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
||||||
])
|
])
|
||||||
end
|
end
|
||||||
|
|
||||||
# ---
|
|
||||||
#
|
|
||||||
# Once the new node is spawned we need to establish a connection,
|
|
||||||
# initialize it and make sure it correctly reacts to the parent node terminating.
|
|
||||||
#
|
|
||||||
# The procedure goes as follows:
|
|
||||||
#
|
|
||||||
# 1. The child sends {:node_initialized, ref} message to the parent
|
|
||||||
# to communicate it's ready for initialization.
|
|
||||||
#
|
|
||||||
# 2. The parent initializes the child node - loads necessary modules,
|
|
||||||
# starts the NodeManager process and a single RuntimeServer process.
|
|
||||||
#
|
|
||||||
# 3. The parent sends {:node_initialized, ref} message back to the child,
|
|
||||||
# to communicate successful initialization.
|
|
||||||
#
|
|
||||||
# 4. The child starts monitoring the NodeManager process and freezes
|
|
||||||
# until the NodeManager process terminates. The NodeManager process
|
|
||||||
# serves as the leading remote process and represents the node from now on.
|
|
||||||
#
|
|
||||||
# The nodes either successfully go through this flow or return an error,
|
|
||||||
# either if the other node dies or is not responding for too long.
|
|
||||||
#
|
|
||||||
# ---
|
|
||||||
|
|
||||||
defp parent_init_sequence(child_node, port, init_opts) do
|
defp parent_init_sequence(child_node, port, init_opts) do
|
||||||
port_ref = Port.monitor(port)
|
port_ref = Port.monitor(port)
|
||||||
|
|
||||||
|
@ -131,76 +126,108 @@ defmodule Livebook.Runtime.ElixirStandalone do
|
||||||
loop.(loop)
|
loop.(loop)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Note Windows does not handle escaped quotes and newlines the same way as Unix,
|
defp child_node_eval_string(node, parent_node, parent_port) do
|
||||||
# so the string cannot have constructs newlines nor strings. That's why we pass
|
# We pass the child node code as --eval argument. Windows handles
|
||||||
# the parent node name as ARGV and write the code avoiding newlines.
|
# escaped quotes and newlines differently from Unix, so to avoid
|
||||||
#
|
# those kind of issues, we encode the string in base 64 and pass
|
||||||
# This boot script must be kept in sync with Livebook.EPMD.
|
# as positional argument. Then, we use a simple --eval that decodes
|
||||||
#
|
# and evaluates the string.
|
||||||
# Also note that we explicitly halt, just in case `System.no_halt(true)` is
|
|
||||||
# called within the runtime.
|
|
||||||
@child_node_eval_string """
|
|
||||||
{:ok, [[node]]} = :init.get_argument(:livebook_current);\
|
|
||||||
{:ok, _} = :net_kernel.start(List.to_atom(node), %{name_domain: :longnames});\
|
|
||||||
{:ok, [[parent_node, _port]]} = :init.get_argument(:livebook_parent);\
|
|
||||||
dist_port = :persistent_term.get(:livebook_dist_port, 0);\
|
|
||||||
init_ref = make_ref();\
|
|
||||||
parent_process = {node(), List.to_atom(parent_node)};\
|
|
||||||
send(parent_process, {:node_started, init_ref, node(), dist_port, self()});\
|
|
||||||
receive do {:node_initialized, ^init_ref} ->\
|
|
||||||
manager_ref = Process.monitor(Livebook.Runtime.ErlDist.NodeManager);\
|
|
||||||
receive do {:DOWN, ^manager_ref, :process, _object, _reason} -> :ok end;\
|
|
||||||
after 10_000 ->\
|
|
||||||
:timeout;\
|
|
||||||
end;\
|
|
||||||
System.halt()\
|
|
||||||
"""
|
|
||||||
|
|
||||||
if @child_node_eval_string =~ "\n" do
|
quote do
|
||||||
raise "invalid @child_node_eval_string, newline found: #{inspect(@child_node_eval_string)}"
|
node = unquote(node)
|
||||||
|
parent_node = unquote(parent_node)
|
||||||
|
parent_port = unquote(parent_port)
|
||||||
|
|
||||||
|
# We start distribution here, rather than on node boot, so that
|
||||||
|
# -pa takes effect and Livebook.EPMD is available
|
||||||
|
{:ok, _} = :net_kernel.start(node, %{name_domain: :longnames})
|
||||||
|
Livebook.Runtime.EPMD.register_parent(parent_node, parent_port)
|
||||||
|
dist_port = Livebook.Runtime.EPMD.dist_port()
|
||||||
|
|
||||||
|
init_ref = make_ref()
|
||||||
|
parent_process = {node(), parent_node}
|
||||||
|
send(parent_process, {:node_started, init_ref, node(), dist_port, self()})
|
||||||
|
|
||||||
|
receive do
|
||||||
|
{:node_initialized, ^init_ref} ->
|
||||||
|
manager_ref = Process.monitor(Livebook.Runtime.ErlDist.NodeManager)
|
||||||
|
|
||||||
|
receive do
|
||||||
|
{:DOWN, ^manager_ref, :process, _object, _reason} -> :ok
|
||||||
|
end
|
||||||
|
after
|
||||||
|
10_000 -> :timeout
|
||||||
|
end
|
||||||
|
|
||||||
|
# We explicitly halt at the end, just in case `System.no_halt(true)`
|
||||||
|
# is called within the runtime
|
||||||
|
System.halt()
|
||||||
|
end
|
||||||
|
|> Macro.to_string()
|
||||||
|
|> Base.encode64()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp elixir_flags(node_name) do
|
defp elixir_flags(node_name) do
|
||||||
parent_name = node()
|
parent_name = node()
|
||||||
parent_port = Livebook.EPMD.dist_port()
|
parent_port = Livebook.EPMD.dist_port()
|
||||||
|
|
||||||
epmdless_flags =
|
|
||||||
if parent_port != 0 do
|
|
||||||
"-epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0 "
|
|
||||||
else
|
|
||||||
""
|
|
||||||
end
|
|
||||||
|
|
||||||
[
|
[
|
||||||
"--erl",
|
"--erl",
|
||||||
# Minimize schedulers busy wait threshold,
|
# Note: keep these flags in sync with the remote runtime.
|
||||||
# so that they go to sleep immediately after evaluation.
|
#
|
||||||
# Increase the default stack for dirty io threads (cuda requires it).
|
# * minimize schedulers busy wait threshold, so that they go
|
||||||
# Enable ANSI escape codes as we handle them with HTML.
|
# to sleep immediately after evaluation
|
||||||
# Disable stdin, so that the system process never tries to read terminal input.
|
#
|
||||||
|
# * increase the default stack for dirty IO threads, necessary
|
||||||
|
# for CUDA
|
||||||
|
#
|
||||||
|
# * enable ANSI escape codes as we handle them with HTML
|
||||||
|
#
|
||||||
|
# * disable stdin, so that the system process never tries to
|
||||||
|
# read terminal input
|
||||||
|
#
|
||||||
|
# * specify a custom EPMD module and disable automatic EPMD
|
||||||
|
# startup
|
||||||
|
#
|
||||||
"+sbwt none +sbwtdcpu none +sbwtdio none +sssdio 128 -elixir ansi_enabled true -noinput " <>
|
"+sbwt none +sbwtdcpu none +sbwtdio none +sssdio 128 -elixir ansi_enabled true -noinput " <>
|
||||||
epmdless_flags <>
|
"-epmd_module Elixir.Livebook.Runtime.EPMD",
|
||||||
"-livebook_parent #{parent_name} #{parent_port} -livebook_current #{node_name}",
|
# Add the location of Livebook.Runtime.EPMD
|
||||||
# Add the location of Livebook.EPMD
|
|
||||||
"-pa",
|
"-pa",
|
||||||
Application.app_dir(:livebook, "priv/epmd"),
|
epmd_module_path!(),
|
||||||
# Make the node hidden, so it doesn't automatically join the cluster
|
# Make the node hidden, so it doesn't automatically join the cluster
|
||||||
"--hidden",
|
"--hidden",
|
||||||
# Use the cookie in Livebook
|
# Use the cookie in Livebook
|
||||||
"--cookie",
|
"--cookie",
|
||||||
Atom.to_string(Node.get_cookie()),
|
Atom.to_string(Node.get_cookie()),
|
||||||
"--eval",
|
"--eval",
|
||||||
@child_node_eval_string
|
"System.argv() |> hd() |> Base.decode64!() |> Code.eval_string()",
|
||||||
|
child_node_eval_string(node_name, parent_name, parent_port)
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp epmd_module_path!() do
|
||||||
|
# We need to make the custom Livebook.Runtime.EPMD module available
|
||||||
|
# before the child node starts distrubtion. We persist the module
|
||||||
|
# into a temporary directory and add to the code paths. Note that
|
||||||
|
# we could persist it to priv/ at build time, however for Escript
|
||||||
|
# priv/ is packaged into the archive, so it is not accessible in
|
||||||
|
# the file system.
|
||||||
|
|
||||||
|
epmd_path = Path.join(Livebook.Config.tmp_path(), "epmd")
|
||||||
|
File.rm_rf!(epmd_path)
|
||||||
|
File.mkdir_p!(epmd_path)
|
||||||
|
{_module, binary, path} = :code.get_object_code(Livebook.Runtime.EPMD)
|
||||||
|
File.write!(Path.join(epmd_path, Path.basename(path)), binary)
|
||||||
|
epmd_path
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do
|
defimpl Livebook.Runtime, for: Livebook.Runtime.Standalone do
|
||||||
alias Livebook.Runtime.ErlDist.RuntimeServer
|
alias Livebook.Runtime.ErlDist.RuntimeServer
|
||||||
|
|
||||||
def describe(runtime) do
|
def describe(runtime) do
|
||||||
[{"Type", "Elixir standalone"}] ++
|
[{"Type", "Standalone"}] ++
|
||||||
if connected?(runtime) do
|
if runtime.node do
|
||||||
[{"Node name", Atom.to_string(runtime.node)}]
|
[{"Node name", Atom.to_string(runtime.node)}]
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
|
@ -208,11 +235,7 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do
|
||||||
end
|
end
|
||||||
|
|
||||||
def connect(runtime) do
|
def connect(runtime) do
|
||||||
Livebook.Runtime.ElixirStandalone.connect(runtime)
|
Livebook.Runtime.Standalone.__connect__(runtime)
|
||||||
end
|
|
||||||
|
|
||||||
def connected?(runtime) do
|
|
||||||
runtime.server_pid != nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def take_ownership(runtime, opts \\ []) do
|
def take_ownership(runtime, opts \\ []) do
|
||||||
|
@ -222,11 +245,10 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do
|
||||||
|
|
||||||
def disconnect(runtime) do
|
def disconnect(runtime) do
|
||||||
:ok = RuntimeServer.stop(runtime.server_pid)
|
:ok = RuntimeServer.stop(runtime.server_pid)
|
||||||
{:ok, %{runtime | node: nil, server_pid: nil}}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def duplicate(_runtime) do
|
def duplicate(_runtime) do
|
||||||
Livebook.Runtime.ElixirStandalone.new()
|
Livebook.Runtime.Standalone.new()
|
||||||
end
|
end
|
||||||
|
|
||||||
def evaluate_code(runtime, language, code, locator, parent_locators, opts \\ []) do
|
def evaluate_code(runtime, language, code, locator, parent_locators, opts \\ []) do
|
||||||
|
@ -298,10 +320,6 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do
|
||||||
Livebook.Runtime.Dependencies.search_packages_on_hex(send_to, search)
|
Livebook.Runtime.Dependencies.search_packages_on_hex(send_to, search)
|
||||||
end
|
end
|
||||||
|
|
||||||
def disable_dependencies_cache(runtime) do
|
|
||||||
RuntimeServer.disable_dependencies_cache(runtime.server_pid)
|
|
||||||
end
|
|
||||||
|
|
||||||
def put_system_envs(runtime, envs) do
|
def put_system_envs(runtime, envs) do
|
||||||
RuntimeServer.put_system_envs(runtime.server_pid, envs)
|
RuntimeServer.put_system_envs(runtime.server_pid, envs)
|
||||||
end
|
end
|
|
@ -111,6 +111,7 @@ defmodule Livebook.Session do
|
||||||
data: Data.t(),
|
data: Data.t(),
|
||||||
client_pids_with_id: %{pid() => Data.client_id()},
|
client_pids_with_id: %{pid() => Data.client_id()},
|
||||||
created_at: DateTime.t(),
|
created_at: DateTime.t(),
|
||||||
|
runtime_connect: %{ref: reference(), pid: pid()} | nil,
|
||||||
runtime_monitor_ref: reference() | nil,
|
runtime_monitor_ref: reference() | nil,
|
||||||
autosave_timer_ref: reference() | nil,
|
autosave_timer_ref: reference() | nil,
|
||||||
autosave_path: String.t() | nil,
|
autosave_path: String.t() | nil,
|
||||||
|
@ -452,20 +453,12 @@ defmodule Livebook.Session do
|
||||||
GenServer.cast(pid, {:add_dependencies, dependencies})
|
GenServer.cast(pid, {:add_dependencies, dependencies})
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Sends disable dependencies cache request to the server.
|
|
||||||
"""
|
|
||||||
@spec disable_dependencies_cache(pid()) :: :ok
|
|
||||||
def disable_dependencies_cache(pid) do
|
|
||||||
GenServer.cast(pid, :disable_dependencies_cache)
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Sends cell evaluation request to the server.
|
Sends cell evaluation request to the server.
|
||||||
"""
|
"""
|
||||||
@spec queue_cell_evaluation(pid(), Cell.id()) :: :ok
|
@spec queue_cell_evaluation(pid(), Cell.id(), keyword()) :: :ok
|
||||||
def queue_cell_evaluation(pid, cell_id) do
|
def queue_cell_evaluation(pid, cell_id, evaluation_opts \\ []) do
|
||||||
GenServer.cast(pid, {:queue_cell_evaluation, self(), cell_id})
|
GenServer.cast(pid, {:queue_cell_evaluation, self(), cell_id, evaluation_opts})
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -586,14 +579,22 @@ defmodule Livebook.Session do
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Sends runtime update to the server.
|
Sends runtime update to the server.
|
||||||
|
|
||||||
If the runtime is connected, the session takes the ownership.
|
|
||||||
"""
|
"""
|
||||||
@spec set_runtime(pid(), Runtime.t()) :: :ok
|
@spec set_runtime(pid(), Runtime.t()) :: :ok
|
||||||
def set_runtime(pid, runtime) do
|
def set_runtime(pid, runtime) do
|
||||||
GenServer.cast(pid, {:set_runtime, self(), runtime})
|
GenServer.cast(pid, {:set_runtime, self(), runtime})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Sends request to connect to the configured runtime.
|
||||||
|
|
||||||
|
Once the runtime is connected, the session takes the ownership.
|
||||||
|
"""
|
||||||
|
@spec connect_runtime(pid()) :: :ok
|
||||||
|
def connect_runtime(pid) do
|
||||||
|
GenServer.cast(pid, {:connect_runtime, self()})
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Sends file location update request to the server.
|
Sends file location update request to the server.
|
||||||
"""
|
"""
|
||||||
|
@ -890,6 +891,7 @@ defmodule Livebook.Session do
|
||||||
data: data,
|
data: data,
|
||||||
client_pids_with_id: %{},
|
client_pids_with_id: %{},
|
||||||
created_at: DateTime.utc_now(),
|
created_at: DateTime.utc_now(),
|
||||||
|
runtime_connect: nil,
|
||||||
runtime_monitor_ref: nil,
|
runtime_monitor_ref: nil,
|
||||||
autosave_timer_ref: nil,
|
autosave_timer_ref: nil,
|
||||||
autosave_path: opts[:autosave_path],
|
autosave_path: opts[:autosave_path],
|
||||||
|
@ -997,7 +999,7 @@ defmodule Livebook.Session do
|
||||||
@impl true
|
@impl true
|
||||||
def handle_continue(:app_init, state) do
|
def handle_continue(:app_init, state) do
|
||||||
cell_ids = Data.cell_ids_for_full_evaluation(state.data, [])
|
cell_ids = Data.cell_ids_for_full_evaluation(state.data, [])
|
||||||
operation = {:queue_cells_evaluation, @client_id, cell_ids}
|
operation = {:queue_cells_evaluation, @client_id, cell_ids, []}
|
||||||
{:noreply, handle_operation(state, operation)}
|
{:noreply, handle_operation(state, operation)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1031,18 +1033,16 @@ defmodule Livebook.Session do
|
||||||
Notebook.find_asset_info(state.data.notebook, hash) ||
|
Notebook.find_asset_info(state.data.notebook, hash) ||
|
||||||
Enum.find_value(state.client_id_with_assets, fn {_client_id, assets} -> assets[hash] end)
|
Enum.find_value(state.client_id_with_assets, fn {_client_id, assets} -> assets[hash] end)
|
||||||
|
|
||||||
runtime = state.data.runtime
|
|
||||||
|
|
||||||
reply =
|
reply =
|
||||||
cond do
|
cond do
|
||||||
assets_info == nil ->
|
assets_info == nil ->
|
||||||
{:error, "unknown hash"}
|
{:error, "unknown hash"}
|
||||||
|
|
||||||
not Runtime.connected?(runtime) ->
|
state.data.runtime_status != :connected ->
|
||||||
{:error, "runtime not started"}
|
{:error, "runtime not started"}
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
{:ok, runtime, assets_info.archive_path}
|
{:ok, state.data.runtime, assets_info.archive_path}
|
||||||
end
|
end
|
||||||
|
|
||||||
{:reply, reply, state}
|
{:reply, reply, state}
|
||||||
|
@ -1076,17 +1076,7 @@ defmodule Livebook.Session do
|
||||||
|
|
||||||
def handle_call({:disconnect_runtime, client_pid}, _from, state) do
|
def handle_call({:disconnect_runtime, client_pid}, _from, state) do
|
||||||
client_id = client_id(state, client_pid)
|
client_id = client_id(state, client_pid)
|
||||||
|
state = handle_operation(state, {:disconnect_runtime, client_id})
|
||||||
state =
|
|
||||||
if Runtime.connected?(state.data.runtime) do
|
|
||||||
{:ok, runtime} = Runtime.disconnect(state.data.runtime)
|
|
||||||
|
|
||||||
%{state | runtime_monitor_ref: nil}
|
|
||||||
|> handle_operation({:set_runtime, client_id, runtime})
|
|
||||||
else
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
{:reply, :ok, state}
|
{:reply, :ok, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1099,7 +1089,7 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_call(:fetch_proxy_handler_spec, _from, state) do
|
def handle_call(:fetch_proxy_handler_spec, _from, state) do
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
{:reply, Runtime.fetch_proxy_handler_spec(state.data.runtime), state}
|
{:reply, Runtime.fetch_proxy_handler_spec(state.data.runtime), state}
|
||||||
else
|
else
|
||||||
{:reply, {:error, :disconnected}, state}
|
{:reply, {:error, :disconnected}, state}
|
||||||
|
@ -1233,17 +1223,9 @@ defmodule Livebook.Session do
|
||||||
{:noreply, do_add_dependencies(state, dependencies)}
|
{:noreply, do_add_dependencies(state, dependencies)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_cast(:disable_dependencies_cache, state) do
|
def handle_cast({:queue_cell_evaluation, client_pid, cell_id, evaluation_opts}, state) do
|
||||||
if Runtime.connected?(state.data.runtime) do
|
|
||||||
Runtime.disable_dependencies_cache(state.data.runtime)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, state}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_cast({:queue_cell_evaluation, client_pid, cell_id}, state) do
|
|
||||||
client_id = client_id(state, client_pid)
|
client_id = client_id(state, client_pid)
|
||||||
operation = {:queue_cells_evaluation, client_id, [cell_id]}
|
operation = {:queue_cells_evaluation, client_id, [cell_id], evaluation_opts}
|
||||||
{:noreply, handle_operation(state, operation)}
|
{:noreply, handle_operation(state, operation)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1253,7 +1235,7 @@ defmodule Livebook.Session do
|
||||||
case Notebook.fetch_section(state.data.notebook, section_id) do
|
case Notebook.fetch_section(state.data.notebook, section_id) do
|
||||||
{:ok, section} ->
|
{:ok, section} ->
|
||||||
cell_ids = for cell <- section.cells, Cell.evaluable?(cell), do: cell.id
|
cell_ids = for cell <- section.cells, Cell.evaluable?(cell), do: cell.id
|
||||||
operation = {:queue_cells_evaluation, client_id, cell_ids}
|
operation = {:queue_cells_evaluation, client_id, cell_ids, []}
|
||||||
{:noreply, handle_operation(state, operation)}
|
{:noreply, handle_operation(state, operation)}
|
||||||
|
|
||||||
:error ->
|
:error ->
|
||||||
|
@ -1268,7 +1250,7 @@ defmodule Livebook.Session do
|
||||||
for {bound_cell, _} <- Data.bound_cells_with_section(state.data, input_id),
|
for {bound_cell, _} <- Data.bound_cells_with_section(state.data, input_id),
|
||||||
do: bound_cell.id
|
do: bound_cell.id
|
||||||
|
|
||||||
operation = {:queue_cells_evaluation, client_id, cell_ids}
|
operation = {:queue_cells_evaluation, client_id, cell_ids, []}
|
||||||
{:noreply, handle_operation(state, operation)}
|
{:noreply, handle_operation(state, operation)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1277,7 +1259,7 @@ defmodule Livebook.Session do
|
||||||
|
|
||||||
cell_ids = Data.cell_ids_for_full_evaluation(state.data, forced_cell_ids)
|
cell_ids = Data.cell_ids_for_full_evaluation(state.data, forced_cell_ids)
|
||||||
|
|
||||||
operation = {:queue_cells_evaluation, client_id, cell_ids}
|
operation = {:queue_cells_evaluation, client_id, cell_ids, []}
|
||||||
{:noreply, handle_operation(state, operation)}
|
{:noreply, handle_operation(state, operation)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1286,7 +1268,7 @@ defmodule Livebook.Session do
|
||||||
|
|
||||||
cell_ids = Data.cell_ids_for_reevaluation(state.data)
|
cell_ids = Data.cell_ids_for_reevaluation(state.data)
|
||||||
|
|
||||||
operation = {:queue_cells_evaluation, client_id, cell_ids}
|
operation = {:queue_cells_evaluation, client_id, cell_ids, []}
|
||||||
{:noreply, handle_operation(state, operation)}
|
{:noreply, handle_operation(state, operation)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1343,21 +1325,14 @@ defmodule Livebook.Session do
|
||||||
|
|
||||||
def handle_cast({:set_runtime, client_pid, runtime}, state) do
|
def handle_cast({:set_runtime, client_pid, runtime}, state) do
|
||||||
client_id = client_id(state, client_pid)
|
client_id = client_id(state, client_pid)
|
||||||
|
|
||||||
if Runtime.connected?(state.data.runtime) do
|
|
||||||
{:ok, _} = Runtime.disconnect(state.data.runtime)
|
|
||||||
end
|
|
||||||
|
|
||||||
state =
|
|
||||||
if Runtime.connected?(runtime) do
|
|
||||||
own_runtime(runtime, state)
|
|
||||||
else
|
|
||||||
state
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, handle_operation(state, {:set_runtime, client_id, runtime})}
|
{:noreply, handle_operation(state, {:set_runtime, client_id, runtime})}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_cast({:connect_runtime, client_pid}, state) do
|
||||||
|
client_id = client_id(state, client_pid)
|
||||||
|
{:noreply, handle_operation(state, {:connect_runtime, client_id})}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_cast({:set_file, client_pid, file}, state) do
|
def handle_cast({:set_file, client_pid, file}, state) do
|
||||||
client_id = client_id(state, client_pid)
|
client_id = client_id(state, client_pid)
|
||||||
|
|
||||||
|
@ -1489,18 +1464,28 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
def handle_info({:DOWN, ref, :process, _, reason}, state)
|
||||||
|
when ref == state.runtime_connect.ref do
|
||||||
|
broadcast_error(
|
||||||
|
state.session_id,
|
||||||
|
"connecting runtime failed unexpectedly - #{Exception.format_exit(reason)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
{:noreply,
|
||||||
|
%{state | runtime_connect: nil}
|
||||||
|
|> handle_operation({:runtime_down, @client_id})}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_info({:DOWN, ref, :process, _, reason}, state)
|
def handle_info({:DOWN, ref, :process, _, reason}, state)
|
||||||
when ref == state.runtime_monitor_ref do
|
when ref == state.runtime_monitor_ref do
|
||||||
broadcast_error(
|
broadcast_error(
|
||||||
state.session_id,
|
state.session_id,
|
||||||
"runtime node terminated unexpectedly - #{Exception.format_exit(reason)}"
|
"runtime terminated unexpectedly - #{Exception.format_exit(reason)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
%{state | runtime_monitor_ref: nil}
|
%{state | runtime_monitor_ref: nil}
|
||||||
|> handle_operation(
|
|> handle_operation({:runtime_down, @client_id})}
|
||||||
{:set_runtime, @client_id, Livebook.Runtime.duplicate(state.data.runtime)}
|
|
||||||
)}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:DOWN, ref, :process, _, _}, state) when ref == state.save_task_ref do
|
def handle_info({:DOWN, ref, :process, _, _}, state) when ref == state.save_task_ref do
|
||||||
|
@ -1540,6 +1525,30 @@ defmodule Livebook.Session do
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info({:runtime_connect_info, pid, info}, state)
|
||||||
|
when pid == state.runtime_connect.pid do
|
||||||
|
state = handle_operation(state, {:set_runtime_connect_info, @client_id, info})
|
||||||
|
{:noreply, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_info({:runtime_connect_done, pid, result}, state)
|
||||||
|
when pid == state.runtime_connect.pid do
|
||||||
|
Process.demonitor(state.runtime_connect.ref, [:flush])
|
||||||
|
|
||||||
|
state =
|
||||||
|
case result do
|
||||||
|
{:ok, runtime} ->
|
||||||
|
state = own_runtime(runtime, state)
|
||||||
|
handle_operation(state, {:runtime_connected, @client_id, runtime})
|
||||||
|
|
||||||
|
{:error, message} ->
|
||||||
|
broadcast_error(state.session_id, "connecting runtime failed - #{message}")
|
||||||
|
handle_operation(state, {:runtime_down, @client_id})
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, %{state | runtime_connect: nil}}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_info({:runtime_evaluation_output, cell_id, output}, state) do
|
def handle_info({:runtime_evaluation_output, cell_id, output}, state) do
|
||||||
output = normalize_runtime_output(output)
|
output = normalize_runtime_output(output)
|
||||||
operation = {:add_cell_evaluation_output, @client_id, cell_id, output}
|
operation = {:add_cell_evaluation_output, @client_id, cell_id, output}
|
||||||
|
@ -1840,7 +1849,8 @@ defmodule Livebook.Session do
|
||||||
state =
|
state =
|
||||||
with {:ok, cell, section} <- Notebook.fetch_cell_and_section(state.data.notebook, cell_id),
|
with {:ok, cell, section} <- Notebook.fetch_cell_and_section(state.data.notebook, cell_id),
|
||||||
:evaluating <- state.data.cell_infos[cell.id].eval.status do
|
:evaluating <- state.data.cell_infos[cell.id].eval.status do
|
||||||
start_evaluation(state, cell, section)
|
evaluation_opts = state.data.cell_infos[cell.id].eval.evaluation_opts
|
||||||
|
start_evaluation(state, cell, section, evaluation_opts)
|
||||||
else
|
else
|
||||||
_ -> state
|
_ -> state
|
||||||
end
|
end
|
||||||
|
@ -1854,7 +1864,7 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:env_var_set, env_var}, state) do
|
def handle_info({:env_var_set, env_var}, state) do
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
Runtime.put_system_envs(state.data.runtime, [{env_var.name, env_var.value}])
|
Runtime.put_system_envs(state.data.runtime, [{env_var.name, env_var.value}])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1862,7 +1872,7 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({:env_var_unset, env_var}, state) do
|
def handle_info({:env_var_unset, env_var}, state) do
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
Runtime.delete_system_envs(state.data.runtime, [env_var.name])
|
Runtime.delete_system_envs(state.data.runtime, [env_var.name])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1874,7 +1884,7 @@ defmodule Livebook.Session do
|
||||||
|
|
||||||
case File.rm_rf(path) do
|
case File.rm_rf(path) do
|
||||||
{:ok, _} ->
|
{:ok, _} ->
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
{:file, file_id} = file_ref
|
{:file, file_id} = file_ref
|
||||||
Runtime.revoke_file(state.data.runtime, file_id)
|
Runtime.revoke_file(state.data.runtime, file_id)
|
||||||
end
|
end
|
||||||
|
@ -2223,17 +2233,14 @@ defmodule Livebook.Session do
|
||||||
notify_update(state)
|
notify_update(state)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp after_operation(state, _prev_state, {:set_runtime, _client_id, runtime}) do
|
defp after_operation(state, _prev_state, {:runtime_connected, _client_id, _runtime}) do
|
||||||
if Runtime.connected?(runtime) do
|
|
||||||
set_runtime_secrets(state, state.data.secrets)
|
set_runtime_secrets(state, state.data.secrets)
|
||||||
set_runtime_env_vars(state)
|
set_runtime_env_vars(state)
|
||||||
|
|
||||||
state
|
state
|
||||||
else
|
|
||||||
state
|
|
||||||
|> put_memory_usage(nil)
|
|
||||||
|> notify_update()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp after_operation(state, _prev_state, {:runtime_down, _client_id}) do
|
||||||
|
after_runtime_disconnected(state)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp after_operation(state, prev_state, {:set_file, _client_id, _file}) do
|
defp after_operation(state, prev_state, {:set_file, _client_id, _file}) do
|
||||||
|
@ -2273,7 +2280,7 @@ defmodule Livebook.Session do
|
||||||
|
|
||||||
state = put_in(state.client_id_with_assets[client_id], %{})
|
state = put_in(state.client_id_with_assets[client_id], %{})
|
||||||
|
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
Runtime.register_clients(state.data.runtime, [client_id])
|
Runtime.register_clients(state.data.runtime, [client_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2292,7 +2299,7 @@ defmodule Livebook.Session do
|
||||||
state = delete_client_files(state, client_id)
|
state = delete_client_files(state, client_id)
|
||||||
{_, state} = pop_in(state.client_id_with_assets[client_id])
|
{_, state} = pop_in(state.client_id_with_assets[client_id])
|
||||||
|
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
Runtime.unregister_clients(state.data.runtime, [client_id])
|
Runtime.unregister_clients(state.data.runtime, [client_id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2357,12 +2364,18 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp after_operation(state, _prev_state, {:set_secret, _client_id, secret}) do
|
defp after_operation(state, _prev_state, {:set_secret, _client_id, secret}) do
|
||||||
if Runtime.connected?(state.data.runtime), do: set_runtime_secret(state, secret)
|
if state.data.runtime_status == :connected do
|
||||||
|
set_runtime_secret(state, secret)
|
||||||
|
end
|
||||||
|
|
||||||
state
|
state
|
||||||
end
|
end
|
||||||
|
|
||||||
defp after_operation(state, _prev_state, {:unset_secret, _client_id, secret_name}) do
|
defp after_operation(state, _prev_state, {:unset_secret, _client_id, secret_name}) do
|
||||||
if Runtime.connected?(state.data.runtime), do: delete_runtime_secrets(state, [secret_name])
|
if state.data.runtime_status == :connected do
|
||||||
|
delete_runtime_secrets(state, [secret_name])
|
||||||
|
end
|
||||||
|
|
||||||
state
|
state
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2405,18 +2418,18 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_action(state, :connect_runtime) do
|
defp handle_action(state, :connect_runtime) do
|
||||||
case Runtime.connect(state.data.runtime) do
|
pid = Runtime.connect(state.data.runtime)
|
||||||
{:ok, runtime} ->
|
ref = Process.monitor(pid)
|
||||||
state = own_runtime(runtime, state)
|
%{state | runtime_connect: %{pid: pid, ref: ref}}
|
||||||
handle_operation(state, {:set_runtime, @client_id, runtime})
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
broadcast_error(state.session_id, "failed to connect runtime - #{error}")
|
|
||||||
handle_operation(state, {:set_runtime, @client_id, state.data.runtime})
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_action(state, {:start_evaluation, cell, section}) do
|
defp handle_action(state, {:disconnect_runtime, runtime}) do
|
||||||
|
Runtime.disconnect(runtime)
|
||||||
|
state = %{state | runtime_monitor_ref: nil}
|
||||||
|
after_runtime_disconnected(state)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp handle_action(state, {:start_evaluation, cell, section, evaluation_opts}) do
|
||||||
info = state.data.cell_infos[cell.id]
|
info = state.data.cell_infos[cell.id]
|
||||||
|
|
||||||
if is_struct(cell, Cell.Smart) and info.status == :started do
|
if is_struct(cell, Cell.Smart) and info.status == :started do
|
||||||
|
@ -2429,12 +2442,12 @@ defmodule Livebook.Session do
|
||||||
|
|
||||||
state
|
state
|
||||||
else
|
else
|
||||||
start_evaluation(state, cell, section)
|
start_evaluation(state, cell, section, evaluation_opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_action(state, {:stop_evaluation, section}) do
|
defp handle_action(state, {:stop_evaluation, section}) do
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
Runtime.drop_container(state.data.runtime, container_ref_for_section(section))
|
Runtime.drop_container(state.data.runtime, container_ref_for_section(section))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2442,7 +2455,7 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_action(state, {:forget_evaluation, cell, section}) do
|
defp handle_action(state, {:forget_evaluation, cell, section}) do
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
Runtime.forget_evaluation(state.data.runtime, {container_ref_for_section(section), cell.id})
|
Runtime.forget_evaluation(state.data.runtime, {container_ref_for_section(section), cell.id})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2450,7 +2463,7 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_action(state, {:start_smart_cell, cell, _section}) do
|
defp handle_action(state, {:start_smart_cell, cell, _section}) do
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
parent_locators = parent_locators_for_cell(state.data, cell)
|
parent_locators = parent_locators_for_cell(state.data, cell)
|
||||||
|
|
||||||
Runtime.start_smart_cell(
|
Runtime.start_smart_cell(
|
||||||
|
@ -2466,7 +2479,7 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_action(state, {:set_smart_cell_parents, cell, _section, parents}) do
|
defp handle_action(state, {:set_smart_cell_parents, cell, _section, parents}) do
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
parent_locators = evaluation_parents_to_locators(parents)
|
parent_locators = evaluation_parents_to_locators(parents)
|
||||||
Runtime.set_smart_cell_parent_locators(state.data.runtime, cell.id, parent_locators)
|
Runtime.set_smart_cell_parent_locators(state.data.runtime, cell.id, parent_locators)
|
||||||
end
|
end
|
||||||
|
@ -2475,7 +2488,7 @@ defmodule Livebook.Session do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_action(state, {:stop_smart_cell, cell}) do
|
defp handle_action(state, {:stop_smart_cell, cell}) do
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
Runtime.stop_smart_cell(state.data.runtime, cell.id)
|
Runtime.stop_smart_cell(state.data.runtime, cell.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2497,20 +2510,6 @@ defmodule Livebook.Session do
|
||||||
state
|
state
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_action(state, :app_recover) do
|
|
||||||
if Runtime.connected?(state.data.runtime) do
|
|
||||||
{:ok, _} = Runtime.disconnect(state.data.runtime)
|
|
||||||
end
|
|
||||||
|
|
||||||
new_runtime = Livebook.Runtime.duplicate(state.data.runtime)
|
|
||||||
cell_ids = Data.cell_ids_for_full_evaluation(state.data, [])
|
|
||||||
|
|
||||||
state
|
|
||||||
|> handle_operation({:erase_outputs, @client_id})
|
|
||||||
|> handle_operation({:set_runtime, @client_id, new_runtime})
|
|
||||||
|> handle_operation({:queue_cells_evaluation, @client_id, cell_ids})
|
|
||||||
end
|
|
||||||
|
|
||||||
defp handle_action(state, :app_terminate) do
|
defp handle_action(state, :app_terminate) do
|
||||||
send(self(), :close)
|
send(self(), :close)
|
||||||
|
|
||||||
|
@ -2519,7 +2518,7 @@ defmodule Livebook.Session do
|
||||||
|
|
||||||
defp handle_action(state, _action), do: state
|
defp handle_action(state, _action), do: state
|
||||||
|
|
||||||
defp start_evaluation(state, cell, section) do
|
defp start_evaluation(state, cell, section, evaluation_opts) do
|
||||||
path =
|
path =
|
||||||
case state.data.file || default_notebook_file(state) do
|
case state.data.file || default_notebook_file(state) do
|
||||||
nil -> ""
|
nil -> ""
|
||||||
|
@ -2534,7 +2533,7 @@ defmodule Livebook.Session do
|
||||||
_ -> nil
|
_ -> nil
|
||||||
end
|
end
|
||||||
|
|
||||||
opts = [file: file, smart_cell_ref: smart_cell_ref]
|
opts = evaluation_opts ++ [file: file, smart_cell_ref: smart_cell_ref]
|
||||||
|
|
||||||
locator = {container_ref_for_section(section), cell.id}
|
locator = {container_ref_for_section(section), cell.id}
|
||||||
parent_locators = parent_locators_for_cell(state.data, cell)
|
parent_locators = parent_locators_for_cell(state.data, cell)
|
||||||
|
@ -2601,6 +2600,12 @@ defmodule Livebook.Session do
|
||||||
Runtime.put_system_envs(state.data.runtime, env_vars)
|
Runtime.put_system_envs(state.data.runtime, env_vars)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp after_runtime_disconnected(state) do
|
||||||
|
state
|
||||||
|
|> put_memory_usage(nil)
|
||||||
|
|> notify_update()
|
||||||
|
end
|
||||||
|
|
||||||
defp notify_update(state) do
|
defp notify_update(state) do
|
||||||
session = self_from_state(state)
|
session = self_from_state(state)
|
||||||
Livebook.Sessions.update_session(session)
|
Livebook.Sessions.update_session(session)
|
||||||
|
@ -2880,7 +2885,7 @@ defmodule Livebook.Session do
|
||||||
cache_file = file_entry_cache_file(state.session_id, name)
|
cache_file = file_entry_cache_file(state.session_id, name)
|
||||||
FileSystem.File.remove(cache_file)
|
FileSystem.File.remove(cache_file)
|
||||||
|
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
file_id = file_entry_file_id(name)
|
file_id = file_entry_file_id(name)
|
||||||
Runtime.revoke_file(state.data.runtime, file_id)
|
Runtime.revoke_file(state.data.runtime, file_id)
|
||||||
end
|
end
|
||||||
|
@ -2901,7 +2906,7 @@ defmodule Livebook.Session do
|
||||||
FileSystem.File.rename(file, new_file)
|
FileSystem.File.rename(file, new_file)
|
||||||
end
|
end
|
||||||
|
|
||||||
if Runtime.connected?(state.data.runtime) do
|
if state.data.runtime_status == :connected do
|
||||||
file_id = file_entry_file_id(name)
|
file_id = file_entry_file_id(name)
|
||||||
new_file_id = file_entry_file_id(new_name)
|
new_file_id = file_entry_file_id(new_name)
|
||||||
Runtime.relabel_file(state.data.runtime, file_id, new_file_id)
|
Runtime.relabel_file(state.data.runtime, file_id, new_file_id)
|
||||||
|
|
|
@ -27,6 +27,8 @@ defmodule Livebook.Session.Data do
|
||||||
:input_infos,
|
:input_infos,
|
||||||
:bin_entries,
|
:bin_entries,
|
||||||
:runtime,
|
:runtime,
|
||||||
|
:runtime_status,
|
||||||
|
:runtime_connect_info,
|
||||||
:runtime_transient_state,
|
:runtime_transient_state,
|
||||||
:runtime_connected_nodes,
|
:runtime_connected_nodes,
|
||||||
:smart_cell_definitions,
|
:smart_cell_definitions,
|
||||||
|
@ -55,6 +57,8 @@ defmodule Livebook.Session.Data do
|
||||||
input_infos: %{input_id() => input_info()},
|
input_infos: %{input_id() => input_info()},
|
||||||
bin_entries: list(cell_bin_entry()),
|
bin_entries: list(cell_bin_entry()),
|
||||||
runtime: Runtime.t(),
|
runtime: Runtime.t(),
|
||||||
|
runtime_status: runtime_status(),
|
||||||
|
runtime_connect_info: String.t() | nil,
|
||||||
runtime_transient_state: Runtime.transient_state(),
|
runtime_transient_state: Runtime.transient_state(),
|
||||||
runtime_connected_nodes: list(node()),
|
runtime_connected_nodes: list(node()),
|
||||||
smart_cell_definitions: list(Runtime.smart_cell_definition()),
|
smart_cell_definitions: list(Runtime.smart_cell_definition()),
|
||||||
|
@ -125,6 +129,8 @@ defmodule Livebook.Session.Data do
|
||||||
deleted_at: DateTime.t()
|
deleted_at: DateTime.t()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@type runtime_status :: :disconnected | :connecting | :connected
|
||||||
|
|
||||||
@type cell_revision :: non_neg_integer()
|
@type cell_revision :: non_neg_integer()
|
||||||
|
|
||||||
@type cell_evaluation_validity :: :fresh | :evaluated | :stale | :aborted
|
@type cell_evaluation_validity :: :fresh | :evaluated | :stale | :aborted
|
||||||
|
@ -188,7 +194,7 @@ defmodule Livebook.Session.Data do
|
||||||
| {:restore_cell, client_id(), Cell.id()}
|
| {:restore_cell, client_id(), Cell.id()}
|
||||||
| {:move_cell, client_id(), Cell.id(), offset :: integer()}
|
| {:move_cell, client_id(), Cell.id(), offset :: integer()}
|
||||||
| {:move_section, client_id(), Section.id(), offset :: integer()}
|
| {:move_section, client_id(), Section.id(), offset :: integer()}
|
||||||
| {:queue_cells_evaluation, client_id(), list(Cell.id())}
|
| {:queue_cells_evaluation, client_id(), list(Cell.id()), evaluation_opts :: keyword()}
|
||||||
| {:add_cell_doctest_report, client_id(), Cell.id(), Runtime.doctest_report()}
|
| {:add_cell_doctest_report, client_id(), Cell.id(), Runtime.doctest_report()}
|
||||||
| {:add_cell_evaluation_output, client_id(), Cell.id(), term()}
|
| {:add_cell_evaluation_output, client_id(), Cell.id(), term()}
|
||||||
| {:add_cell_evaluation_response, client_id(), Cell.id(), term(), metadata :: map()}
|
| {:add_cell_evaluation_response, client_id(), Cell.id(), term(), metadata :: map()}
|
||||||
|
@ -215,6 +221,11 @@ defmodule Livebook.Session.Data do
|
||||||
| {:set_cell_attributes, client_id(), Cell.id(), map()}
|
| {:set_cell_attributes, client_id(), Cell.id(), map()}
|
||||||
| {:set_input_value, client_id(), input_id(), value :: term()}
|
| {:set_input_value, client_id(), input_id(), value :: term()}
|
||||||
| {:set_runtime, client_id(), Runtime.t()}
|
| {:set_runtime, client_id(), Runtime.t()}
|
||||||
|
| {:connect_runtime, client_id()}
|
||||||
|
| {:set_runtime_connect_info, client_id(), String.t()}
|
||||||
|
| {:runtime_connected, client_id(), Runtime.t()}
|
||||||
|
| {:disconnect_runtime, client_id()}
|
||||||
|
| {:runtime_down, client_id()}
|
||||||
| {:set_runtime_transient_state, client_id(), Runtime.transient_state()}
|
| {:set_runtime_transient_state, client_id(), Runtime.transient_state()}
|
||||||
| {:set_runtime_connected_nodes, client_id(), list(node())}
|
| {:set_runtime_connected_nodes, client_id(), list(node())}
|
||||||
| {:set_smart_cell_definitions, client_id(), list(Runtime.smart_cell_definition())}
|
| {:set_smart_cell_definitions, client_id(), list(Runtime.smart_cell_definition())}
|
||||||
|
@ -237,6 +248,7 @@ defmodule Livebook.Session.Data do
|
||||||
|
|
||||||
@type action ::
|
@type action ::
|
||||||
:connect_runtime
|
:connect_runtime
|
||||||
|
| {:disconnect_runtime, Runtime.t()}
|
||||||
| {:start_evaluation, Cell.t(), Section.t()}
|
| {:start_evaluation, Cell.t(), Section.t()}
|
||||||
| {:stop_evaluation, Section.t()}
|
| {:stop_evaluation, Section.t()}
|
||||||
| {:forget_evaluation, Cell.t(), Section.t()}
|
| {:forget_evaluation, Cell.t(), Section.t()}
|
||||||
|
@ -246,7 +258,6 @@ defmodule Livebook.Session.Data do
|
||||||
| {:report_delta, client_id(), Cell.t(), cell_source_tag(), Text.Delta.t()}
|
| {:report_delta, client_id(), Cell.t(), cell_source_tag(), Text.Delta.t()}
|
||||||
| {:clean_up_input_values, %{input_id() => input_info()}}
|
| {:clean_up_input_values, %{input_id() => input_info()}}
|
||||||
| :app_report_status
|
| :app_report_status
|
||||||
| :app_recover
|
|
||||||
| :app_terminate
|
| :app_terminate
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
@ -305,6 +316,8 @@ defmodule Livebook.Session.Data do
|
||||||
input_infos: initial_input_infos(notebook),
|
input_infos: initial_input_infos(notebook),
|
||||||
bin_entries: [],
|
bin_entries: [],
|
||||||
runtime: default_runtime,
|
runtime: default_runtime,
|
||||||
|
runtime_status: :disconnected,
|
||||||
|
runtime_connect_info: nil,
|
||||||
runtime_transient_state: %{},
|
runtime_transient_state: %{},
|
||||||
runtime_connected_nodes: [],
|
runtime_connected_nodes: [],
|
||||||
smart_cell_definitions: [],
|
smart_cell_definitions: [],
|
||||||
|
@ -552,7 +565,7 @@ defmodule Livebook.Session.Data do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_operation(data, {:queue_cells_evaluation, _client_id, cell_ids}) do
|
def apply_operation(data, {:queue_cells_evaluation, _client_id, cell_ids, evaluation_opts}) do
|
||||||
cells_with_section =
|
cells_with_section =
|
||||||
data.notebook
|
data.notebook
|
||||||
|> Notebook.evaluable_cells_with_section()
|
|> Notebook.evaluable_cells_with_section()
|
||||||
|
@ -568,7 +581,7 @@ defmodule Livebook.Session.Data do
|
||||||
|> with_actions()
|
|> with_actions()
|
||||||
|> queue_prerequisite_cells_evaluation(cell_ids)
|
|> queue_prerequisite_cells_evaluation(cell_ids)
|
||||||
|> reduce(cells_with_section, fn data_actions, {cell, section} ->
|
|> reduce(cells_with_section, fn data_actions, {cell, section} ->
|
||||||
queue_cell_evaluation(data_actions, cell, section)
|
queue_cell_evaluation(data_actions, cell, section, evaluation_opts)
|
||||||
end)
|
end)
|
||||||
|> maybe_connect_runtime(data)
|
|> maybe_connect_runtime(data)
|
||||||
|> update_validity_and_evaluation()
|
|> update_validity_and_evaluation()
|
||||||
|
@ -862,10 +875,71 @@ defmodule Livebook.Session.Data do
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_operation(data, {:set_runtime, _client_id, runtime}) do
|
def apply_operation(data, {:set_runtime, _client_id, runtime}) do
|
||||||
|
with true <- data.runtime_status in [:connected, :disconnected] do
|
||||||
data
|
data
|
||||||
|> with_actions()
|
|> with_actions()
|
||||||
|> set_runtime(data, runtime)
|
|> set_runtime(runtime)
|
||||||
|> wrap_ok()
|
|> wrap_ok()
|
||||||
|
else
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_operation(data, {:connect_runtime, _client_id}) do
|
||||||
|
with :disconnected <- data.runtime_status do
|
||||||
|
data
|
||||||
|
|> with_actions()
|
||||||
|
|> connect_runtime()
|
||||||
|
|> wrap_ok()
|
||||||
|
else
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_operation(data, {:set_runtime_connect_info, _client_id, info}) do
|
||||||
|
with :connecting <- data.runtime_status do
|
||||||
|
data
|
||||||
|
|> with_actions()
|
||||||
|
|> set_runtime_connect_info(info)
|
||||||
|
|> wrap_ok()
|
||||||
|
else
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_operation(data, {:runtime_connected, _client_id, runtime}) do
|
||||||
|
with :connecting <- data.runtime_status do
|
||||||
|
data
|
||||||
|
|> with_actions()
|
||||||
|
|> runtime_connected(runtime)
|
||||||
|
|> wrap_ok()
|
||||||
|
else
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_operation(data, {:disconnect_runtime, _client_id}) do
|
||||||
|
with :connected <- data.runtime_status do
|
||||||
|
data
|
||||||
|
|> with_actions()
|
||||||
|
|> disconnect_runtime()
|
||||||
|
|> app_update_execution_status()
|
||||||
|
|> wrap_ok()
|
||||||
|
else
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def apply_operation(data, {:runtime_down, _client_id}) do
|
||||||
|
with true <- data.runtime_status in [:connecting, :connected] do
|
||||||
|
data
|
||||||
|
|> with_actions()
|
||||||
|
|> clear_runtime()
|
||||||
|
|> app_update_execution_status()
|
||||||
|
|> wrap_ok()
|
||||||
|
else
|
||||||
|
_ -> :error
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_operation(data, {:set_runtime_transient_state, _client_id, transient_state}) do
|
def apply_operation(data, {:set_runtime_transient_state, _client_id, transient_state}) do
|
||||||
|
@ -1261,16 +1335,17 @@ defmodule Livebook.Session.Data do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp queue_cell_evaluation(data_actions, cell, section) do
|
defp queue_cell_evaluation(data_actions, cell, section, evaluation_opts \\ []) do
|
||||||
data_actions
|
data_actions
|
||||||
|> update_section_info!(section.id, fn section ->
|
|> update_section_info!(section.id, fn section ->
|
||||||
update_in(section.evaluation_queue, &MapSet.put(&1, cell.id))
|
update_in(section.evaluation_queue, &MapSet.put(&1, cell.id))
|
||||||
end)
|
end)
|
||||||
|> update_cell_eval_info!(cell.id, fn eval_info ->
|
|> update_cell_eval_info!(cell.id, fn eval_info ->
|
||||||
update_in(eval_info.status, fn
|
if eval_info.status == :ready do
|
||||||
:ready -> :queued
|
%{eval_info | status: :queued, evaluation_opts: evaluation_opts}
|
||||||
other -> other
|
else
|
||||||
end)
|
eval_info
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1374,9 +1449,9 @@ defmodule Livebook.Session.Data do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_connect_runtime({data, _} = data_actions, prev_data) do
|
defp maybe_connect_runtime({data, _} = data_actions, prev_data) do
|
||||||
if not Runtime.connected?(data.runtime) and not any_cell_queued?(prev_data) and
|
if data.runtime_status == :disconnected and not any_cell_queued?(prev_data) and
|
||||||
any_cell_queued?(data) do
|
any_cell_queued?(data) do
|
||||||
add_action(data_actions, :connect_runtime)
|
connect_runtime(data_actions)
|
||||||
else
|
else
|
||||||
data_actions
|
data_actions
|
||||||
end
|
end
|
||||||
|
@ -1403,8 +1478,10 @@ defmodule Livebook.Session.Data do
|
||||||
queue_prerequisite_cells_evaluation(data_actions, trailing_queued_cell_ids)
|
queue_prerequisite_cells_evaluation(data_actions, trailing_queued_cell_ids)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_evaluate_queued({data, _} = data_actions) do
|
defp maybe_evaluate_queued(data_actions) do
|
||||||
if Runtime.connected?(data.runtime) do
|
{data, _} = data_actions = check_setup_cell_for_reevaluation(data_actions)
|
||||||
|
|
||||||
|
if data.runtime_status == :connected do
|
||||||
main_flow_evaluating? = main_flow_evaluating?(data)
|
main_flow_evaluating? = main_flow_evaluating?(data)
|
||||||
|
|
||||||
{awaiting_branch_sections, awaiting_regular_sections} =
|
{awaiting_branch_sections, awaiting_regular_sections} =
|
||||||
|
@ -1453,6 +1530,43 @@ defmodule Livebook.Session.Data do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp check_setup_cell_for_reevaluation({data, _} = data_actions) do
|
||||||
|
# When setup cell has been evaluated and is queued again, we need
|
||||||
|
# to reconnect the runtime to get a fresh evaluation environment
|
||||||
|
# for setup. We subsequently queue all cells that are currently
|
||||||
|
# queued
|
||||||
|
|
||||||
|
case data.cell_infos[Cell.setup_cell_id()].eval do
|
||||||
|
%{status: :queued, validity: :evaluated} when data.runtime_status == :connected ->
|
||||||
|
queued_cells_with_section =
|
||||||
|
data.notebook
|
||||||
|
|> Notebook.evaluable_cells_with_section()
|
||||||
|
|> Enum.filter(fn {cell, _} ->
|
||||||
|
data.cell_infos[cell.id].eval.status == :queued
|
||||||
|
end)
|
||||||
|
|> Enum.map(fn {cell, section} ->
|
||||||
|
{cell, section, data.cell_infos[cell.id].eval.evaluation_opts}
|
||||||
|
end)
|
||||||
|
|
||||||
|
cell_ids =
|
||||||
|
for {cell, _section, _evaluation_opts} <- queued_cells_with_section, do: cell.id
|
||||||
|
|
||||||
|
data_actions
|
||||||
|
|> disconnect_runtime()
|
||||||
|
|> connect_runtime()
|
||||||
|
|> queue_prerequisite_cells_evaluation(cell_ids)
|
||||||
|
|> reduce(
|
||||||
|
queued_cells_with_section,
|
||||||
|
fn data_actions, {cell, section, evaluation_opts} ->
|
||||||
|
queue_cell_evaluation(data_actions, cell, section, evaluation_opts)
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
data_actions
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp first_queued_cell(data, section) do
|
defp first_queued_cell(data, section) do
|
||||||
find_queued_cell(data, section.cells)
|
find_queued_cell(data, section.cells)
|
||||||
end
|
end
|
||||||
|
@ -1533,7 +1647,9 @@ defmodule Livebook.Session.Data do
|
||||||
evaluating_cell_id: cell.id,
|
evaluating_cell_id: cell.id,
|
||||||
evaluation_queue: MapSet.delete(section_info.evaluation_queue, cell.id)
|
evaluation_queue: MapSet.delete(section_info.evaluation_queue, cell.id)
|
||||||
)
|
)
|
||||||
|> add_action({:start_evaluation, cell, section})
|
|> add_action(
|
||||||
|
{:start_evaluation, cell, section, data.cell_infos[cell.id].eval.evaluation_opts}
|
||||||
|
)
|
||||||
else
|
else
|
||||||
data_actions
|
data_actions
|
||||||
end
|
end
|
||||||
|
@ -1596,7 +1712,7 @@ defmodule Livebook.Session.Data do
|
||||||
|> Notebook.parent_cells_with_section(cell_ids)
|
|> Notebook.parent_cells_with_section(cell_ids)
|
||||||
|> Enum.filter(fn {cell, _section} ->
|
|> Enum.filter(fn {cell, _section} ->
|
||||||
info = data.cell_infos[cell.id]
|
info = data.cell_infos[cell.id]
|
||||||
Cell.evaluable?(cell) and cell_outdated?(data, cell) and info.eval.status == :ready
|
Cell.evaluable?(cell) and cell_outdated?(data, cell.id) and info.eval.status == :ready
|
||||||
end)
|
end)
|
||||||
|> Enum.reverse()
|
|> Enum.reverse()
|
||||||
|
|
||||||
|
@ -1709,7 +1825,7 @@ defmodule Livebook.Session.Data do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp recover_smart_cell({data, _} = data_actions, cell, section) do
|
defp recover_smart_cell({data, _} = data_actions, cell, section) do
|
||||||
if Runtime.connected?(data.runtime) do
|
if data.runtime_status == :connected do
|
||||||
start_smart_cell(data_actions, cell, section)
|
start_smart_cell(data_actions, cell, section)
|
||||||
else
|
else
|
||||||
data_actions
|
data_actions
|
||||||
|
@ -1965,24 +2081,53 @@ defmodule Livebook.Session.Data do
|
||||||
|> set!(input_infos: Map.put(data.input_infos, input_id, input_info(value)))
|
|> set!(input_infos: Map.put(data.input_infos, input_id, input_info(value)))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp set_runtime(data_actions, prev_data, runtime) do
|
defp set_runtime({data, _} = data_actions, runtime) do
|
||||||
{data, _} =
|
|
||||||
data_actions =
|
data_actions =
|
||||||
set!(data_actions,
|
case data.runtime_status do
|
||||||
runtime: runtime,
|
:connected ->
|
||||||
|
disconnect_runtime(data_actions)
|
||||||
|
|
||||||
|
:disconnected ->
|
||||||
|
data_actions
|
||||||
|
end
|
||||||
|
|
||||||
|
set!(data_actions, runtime: runtime)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp connect_runtime(data_actions) do
|
||||||
|
data_actions
|
||||||
|
|> set!(runtime_status: :connecting)
|
||||||
|
|> add_action(:connect_runtime)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp set_runtime_connect_info(data_actions, info) do
|
||||||
|
data_actions
|
||||||
|
|> set!(runtime_connect_info: info)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp runtime_connected(data_actions, runtime) do
|
||||||
|
data_actions
|
||||||
|
|> set!(runtime: runtime, runtime_status: :connected, runtime_connect_info: nil)
|
||||||
|
|> maybe_evaluate_queued()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp disconnect_runtime({data, _} = data_actions) do
|
||||||
|
data_actions
|
||||||
|
|> add_action({:disconnect_runtime, data.runtime})
|
||||||
|
|> clear_runtime()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp clear_runtime({data, _} = data_actions) do
|
||||||
|
data_actions
|
||||||
|
|> set!(
|
||||||
|
runtime: Runtime.duplicate(data.runtime),
|
||||||
|
runtime_status: :disconnected,
|
||||||
|
runtime_connect_info: nil,
|
||||||
runtime_connected_nodes: [],
|
runtime_connected_nodes: [],
|
||||||
smart_cell_definitions: []
|
smart_cell_definitions: []
|
||||||
)
|
)
|
||||||
|
|
||||||
if not Runtime.connected?(prev_data.runtime) and Runtime.connected?(data.runtime) do
|
|
||||||
data_actions
|
|
||||||
|> maybe_evaluate_queued()
|
|
||||||
else
|
|
||||||
data_actions
|
|
||||||
|> clear_all_evaluation()
|
|> clear_all_evaluation()
|
||||||
|> clear_smart_cells()
|
|> clear_smart_cells()
|
||||||
|> app_update_execution_status()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp set_secret({data, _} = data_actions, secret) do
|
defp set_secret({data, _} = data_actions, secret) do
|
||||||
|
@ -2037,7 +2182,7 @@ defmodule Livebook.Session.Data do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_start_smart_cells({data, _} = data_actions) do
|
defp maybe_start_smart_cells({data, _} = data_actions) do
|
||||||
if Runtime.connected?(data.runtime) do
|
if data.runtime_status == :connected do
|
||||||
dead_cells = dead_smart_cells_with_section(data)
|
dead_cells = dead_smart_cells_with_section(data)
|
||||||
|
|
||||||
kinds =
|
kinds =
|
||||||
|
@ -2219,6 +2364,7 @@ defmodule Livebook.Session.Data do
|
||||||
status: :ready,
|
status: :ready,
|
||||||
errored: false,
|
errored: false,
|
||||||
interrupted: false,
|
interrupted: false,
|
||||||
|
evaluation_opts: [],
|
||||||
evaluation_digest: nil,
|
evaluation_digest: nil,
|
||||||
evaluation_time_ms: nil,
|
evaluation_time_ms: nil,
|
||||||
evaluation_start: nil,
|
evaluation_start: nil,
|
||||||
|
@ -2619,25 +2765,38 @@ defmodule Livebook.Session.Data do
|
||||||
|
|
||||||
# If everything was executed and an error happened, it means it
|
# If everything was executed and an error happened, it means it
|
||||||
# was a runtime crash and everything is aborted
|
# was a runtime crash and everything is aborted
|
||||||
data_actions =
|
{data_actions, execution_status} =
|
||||||
if data.app_data.status.execution == :executed and execution_status == :error do
|
if data.app_data.status.execution == :executed and execution_status == :error do
|
||||||
add_action(data_actions, :app_recover)
|
{app_recover(data_actions), :executing}
|
||||||
else
|
else
|
||||||
data_actions
|
{data_actions, execution_status}
|
||||||
end
|
end
|
||||||
|
|
||||||
update_app_data!(data_actions, &put_in(&1.status.execution, execution_status))
|
update_app_data!(data_actions, &put_in(&1.status.execution, execution_status))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp app_recover({data, _} = data_actions) do
|
||||||
|
evaluable_cells_with_section = Notebook.evaluable_cells_with_section(data.notebook)
|
||||||
|
|
||||||
|
data_actions
|
||||||
|
|> disconnect_runtime()
|
||||||
|
|> connect_runtime()
|
||||||
|
|> erase_outputs()
|
||||||
|
|> garbage_collect_input_infos()
|
||||||
|
|> reduce(evaluable_cells_with_section, fn data_actions, {cell, section} ->
|
||||||
|
queue_cell_evaluation(data_actions, cell, section)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Checks if the given cell is outdated.
|
Checks if the given cell is outdated.
|
||||||
|
|
||||||
A cell is considered outdated if its new/fresh or its content
|
A cell is considered outdated if its fresh/stale or its content has
|
||||||
has changed since the last evaluation.
|
changed since the last evaluation.
|
||||||
"""
|
"""
|
||||||
@spec cell_outdated?(t(), Cell.t()) :: boolean()
|
@spec cell_outdated?(t(), Cell.id()) :: boolean()
|
||||||
def cell_outdated?(data, cell) do
|
def cell_outdated?(data, cell_id) do
|
||||||
info = data.cell_infos[cell.id]
|
info = data.cell_infos[cell_id]
|
||||||
info.eval.validity != :evaluated or info.eval.evaluation_digest != info.sources.primary.digest
|
info.eval.validity != :evaluated or info.eval.evaluation_digest != info.sources.primary.digest
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2649,11 +2808,18 @@ defmodule Livebook.Session.Data do
|
||||||
"""
|
"""
|
||||||
@spec cell_ids_for_full_evaluation(t(), list(Cell.id())) :: list(Cell.id())
|
@spec cell_ids_for_full_evaluation(t(), list(Cell.id())) :: list(Cell.id())
|
||||||
def cell_ids_for_full_evaluation(data, forced_cell_ids) do
|
def cell_ids_for_full_evaluation(data, forced_cell_ids) do
|
||||||
|
requires_reconnect? =
|
||||||
|
data.cell_infos[Cell.setup_cell_id()].eval.validity == :evaluated and
|
||||||
|
cell_outdated?(data, Cell.setup_cell_id())
|
||||||
|
|
||||||
evaluable_cells_with_section = Notebook.evaluable_cells_with_section(data.notebook)
|
evaluable_cells_with_section = Notebook.evaluable_cells_with_section(data.notebook)
|
||||||
|
|
||||||
|
if requires_reconnect? do
|
||||||
|
for {cell, _} <- evaluable_cells_with_section, do: cell.id
|
||||||
|
else
|
||||||
evaluable_cell_ids =
|
evaluable_cell_ids =
|
||||||
for {cell, _} <- evaluable_cells_with_section,
|
for {cell, _} <- evaluable_cells_with_section,
|
||||||
cell_outdated?(data, cell) or cell.id in forced_cell_ids,
|
cell_outdated?(data, cell.id) or cell.id in forced_cell_ids,
|
||||||
do: cell.id,
|
do: cell.id,
|
||||||
into: MapSet.new()
|
into: MapSet.new()
|
||||||
|
|
||||||
|
@ -2672,6 +2838,7 @@ defmodule Livebook.Session.Data do
|
||||||
info.eval.status == :ready
|
info.eval.status == :ready
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Builds identifier parent list for every evaluable cell.
|
# Builds identifier parent list for every evaluable cell.
|
||||||
#
|
#
|
||||||
|
|
|
@ -487,8 +487,11 @@ defmodule LivebookWeb.FormComponents do
|
||||||
id={@id}
|
id={@id}
|
||||||
name={@name}
|
name={@name}
|
||||||
class={[
|
class={[
|
||||||
"w-full px-3 py-2 pr-7 appearance-none bg-gray-50 text-sm border rounded-lg placeholder-gray-400 text-gray-600 disabled:opacity-70 disabled:cursor-not-allowed",
|
"w-full px-3 py-2 pr-7 appearance-none text-sm border rounded-lg placeholder-gray-400 disabled:opacity-70 disabled:cursor-not-allowed",
|
||||||
if(@errors == [], do: "border-gray-200", else: "border-red-300"),
|
if(@errors == [],
|
||||||
|
do: "bg-gray-50 border-gray-200 text-gray-600",
|
||||||
|
else: "bg-red-50 border-red-600 text-red-600"
|
||||||
|
),
|
||||||
@class
|
@class
|
||||||
]}
|
]}
|
||||||
{@rest}
|
{@rest}
|
||||||
|
|
|
@ -263,31 +263,39 @@ defmodule LivebookWeb.SessionLive do
|
||||||
data = socket.private.data
|
data = socket.private.data
|
||||||
%{"section_id" => section_id, "cell_id" => cell_id} = params
|
%{"section_id" => section_id, "cell_id" => cell_id} = params
|
||||||
|
|
||||||
if Livebook.Runtime.connected?(socket.private.data.runtime) do
|
socket =
|
||||||
|
case socket.private.data.runtime_status do
|
||||||
|
:disconnected ->
|
||||||
|
reason = "To insert this block, you need a connected runtime."
|
||||||
|
confirm_setup_runtime(socket, reason)
|
||||||
|
|
||||||
|
:connecting ->
|
||||||
|
message = "To insert this block, wait for the runtime to finish connecting."
|
||||||
|
{:noreply, put_flash(socket, :info, message)}
|
||||||
|
|
||||||
|
:connected ->
|
||||||
case example_snippet_definition_by_name(data, params["definition_name"]) do
|
case example_snippet_definition_by_name(data, params["definition_name"]) do
|
||||||
{:ok, definition} ->
|
{:ok, definition} ->
|
||||||
variant = Enum.fetch!(definition.variants, params["variant_idx"])
|
variant = Enum.fetch!(definition.variants, params["variant_idx"])
|
||||||
|
|
||||||
socket =
|
fun = fn socket ->
|
||||||
ensure_packages_then(socket, variant.packages, definition.name, "block", fn socket ->
|
|
||||||
with {:ok, section, index} <-
|
with {:ok, section, index} <-
|
||||||
section_with_next_index(socket.private.data.notebook, section_id, cell_id) do
|
section_with_next_index(socket.private.data.notebook, section_id, cell_id) do
|
||||||
attrs = %{source: variant.source}
|
attrs = %{source: variant.source}
|
||||||
Session.insert_cell(socket.assigns.session.pid, section.id, index, :code, attrs)
|
Session.insert_cell(socket.assigns.session.pid, section.id, index, :code, attrs)
|
||||||
{:ok, socket}
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
end)
|
end
|
||||||
|
|
||||||
{:noreply, socket}
|
ensure_packages_then(socket, variant.packages, definition.name, "block", fun)
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
else
|
|
||||||
reason = "To insert this block, you need a connected runtime."
|
|
||||||
{:noreply, confirm_setup_default_runtime(socket, reason)}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("insert_smart_cell_below", params, socket) do
|
def handle_event("insert_smart_cell_below", params, socket) do
|
||||||
data = socket.private.data
|
data = socket.private.data
|
||||||
|
@ -486,24 +494,14 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("queue_cell_evaluation", %{"cell_id" => cell_id} = params, socket) do
|
def handle_event("queue_cell_evaluation", %{"cell_id" => cell_id} = params, socket) do
|
||||||
data = socket.private.data
|
opts =
|
||||||
|
|
||||||
{status, socket} =
|
|
||||||
with {:ok, cell, _section} <- Notebook.fetch_cell_and_section(data.notebook, cell_id),
|
|
||||||
true <- Cell.setup?(cell),
|
|
||||||
false <- data.cell_infos[cell.id].eval.validity == :fresh do
|
|
||||||
maybe_reconnect_runtime(socket)
|
|
||||||
else
|
|
||||||
_ -> {:ok, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
if params["disable_dependencies_cache"] do
|
if params["disable_dependencies_cache"] do
|
||||||
Session.disable_dependencies_cache(socket.assigns.session.pid)
|
[disable_dependencies_cache: true]
|
||||||
|
else
|
||||||
|
[]
|
||||||
end
|
end
|
||||||
|
|
||||||
if status == :ok do
|
Session.queue_cell_evaluation(socket.assigns.session.pid, cell_id, opts)
|
||||||
Session.queue_cell_evaluation(socket.assigns.session.pid, cell_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
@ -559,18 +557,15 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("reconnect_runtime", %{}, socket) do
|
|
||||||
{_, socket} = maybe_reconnect_runtime(socket)
|
|
||||||
{:noreply, socket}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_event("connect_runtime", %{}, socket) do
|
def handle_event("connect_runtime", %{}, socket) do
|
||||||
{_, socket} = connect_runtime(socket)
|
Session.connect_runtime(socket.assigns.session.pid)
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("setup_default_runtime", %{"reason" => reason}, socket) do
|
def handle_event("reconnect_runtime", %{}, socket) do
|
||||||
{:noreply, confirm_setup_default_runtime(socket, reason)}
|
Session.disconnect_runtime(socket.assigns.session.pid)
|
||||||
|
Session.connect_runtime(socket.assigns.session.pid)
|
||||||
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("disconnect_runtime", %{}, socket) do
|
def handle_event("disconnect_runtime", %{}, socket) do
|
||||||
|
@ -578,12 +573,15 @@ defmodule LivebookWeb.SessionLive do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_event("setup_runtime", %{"reason" => reason}, socket) do
|
||||||
|
{:noreply, confirm_setup_runtime(socket, reason)}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_event("runtime_disconnect_node", %{"node" => node}, socket) do
|
def handle_event("runtime_disconnect_node", %{"node" => node}, socket) do
|
||||||
node = Enum.find(socket.private.data.runtime_connected_nodes, &(Atom.to_string(&1) == node))
|
node = Enum.find(socket.private.data.runtime_connected_nodes, &(Atom.to_string(&1) == node))
|
||||||
runtime = socket.private.data.runtime
|
|
||||||
|
|
||||||
if node && Runtime.connected?(runtime) do
|
if node do
|
||||||
Runtime.disconnect_node(runtime, node)
|
Runtime.disconnect_node(socket.private.data.runtime, node)
|
||||||
end
|
end
|
||||||
|
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
@ -628,7 +626,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
data = socket.private.data
|
data = socket.private.data
|
||||||
|
|
||||||
with {:ok, cell, _section} <- Notebook.fetch_cell_and_section(data.notebook, cell_id) do
|
with {:ok, cell, _section} <- Notebook.fetch_cell_and_section(data.notebook, cell_id) do
|
||||||
if Runtime.connected?(data.runtime) do
|
if data.runtime_status == :connected do
|
||||||
parent_locators = Session.parent_locators_for_cell(data, cell)
|
parent_locators = Session.parent_locators_for_cell(data, cell)
|
||||||
node = intellisense_node(cell)
|
node = intellisense_node(cell)
|
||||||
|
|
||||||
|
@ -636,19 +634,20 @@ defmodule LivebookWeb.SessionLive do
|
||||||
|
|
||||||
{:reply, %{"ref" => inspect(ref)}, socket}
|
{:reply, %{"ref" => inspect(ref)}, socket}
|
||||||
else
|
else
|
||||||
info =
|
reason =
|
||||||
cond do
|
cond do
|
||||||
params["type"] == "completion" and not params["editor_auto_completion"] ->
|
params["type"] == "completion" and not params["editor_auto_completion"] ->
|
||||||
"You need to start a runtime (or evaluate a cell) for code completion"
|
"You need a connected runtime to enable code completion."
|
||||||
|
|
||||||
params["type"] == "format" ->
|
params["type"] == "format" ->
|
||||||
"You need to start a runtime (or evaluate a cell) to enable code formatting"
|
"You need a connected runtime to enable code formatting."
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
socket = if info, do: put_flash(socket, :info, info), else: socket
|
socket = if reason, do: confirm_setup_runtime(socket, reason), else: socket
|
||||||
|
|
||||||
{:reply, %{"ref" => nil}, socket}
|
{:reply, %{"ref" => nil}, socket}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
@ -782,7 +781,8 @@ defmodule LivebookWeb.SessionLive do
|
||||||
socket
|
socket
|
||||||
) do
|
) do
|
||||||
if file_entry = find_file_entry(socket, file_entry_name) do
|
if file_entry = find_file_entry(socket, file_entry_name) do
|
||||||
if Livebook.Runtime.connected?(socket.private.data.runtime) do
|
case socket.private.data.runtime_status do
|
||||||
|
:connected ->
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(
|
|> assign(
|
||||||
|
@ -794,9 +794,14 @@ defmodule LivebookWeb.SessionLive do
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|> push_patch(to: ~p"/sessions/#{socket.assigns.session.id}/insert-file")}
|
|> push_patch(to: ~p"/sessions/#{socket.assigns.session.id}/insert-file")}
|
||||||
else
|
|
||||||
|
:connecting ->
|
||||||
|
message = "To see the available options, wait for the runtime to finish connecting."
|
||||||
|
{:noreply, put_flash(socket, :info, message)}
|
||||||
|
|
||||||
|
:disconnected ->
|
||||||
reason = "To see the available options, you need a connected runtime."
|
reason = "To see the available options, you need a connected runtime."
|
||||||
{:noreply, confirm_setup_default_runtime(socket, reason)}
|
{:noreply, confirm_setup_runtime(socket, reason)}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
|
@ -843,15 +848,21 @@ defmodule LivebookWeb.SessionLive do
|
||||||
%{"section_id" => section_id, "cell_id" => cell_id},
|
%{"section_id" => section_id, "cell_id" => cell_id},
|
||||||
socket
|
socket
|
||||||
) do
|
) do
|
||||||
if Livebook.Runtime.connected?(socket.private.data.runtime) do
|
case socket.private.data.runtime_status do
|
||||||
|
:disconnected ->
|
||||||
|
reason = "To see the available options, you need a connected runtime."
|
||||||
|
{:noreply, confirm_setup_runtime(socket, reason)}
|
||||||
|
|
||||||
|
:connecting ->
|
||||||
|
message = "To see the available options, wait for the runtime to finish connecting."
|
||||||
|
{:noreply, put_flash(socket, :info, message)}
|
||||||
|
|
||||||
|
:connected ->
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(file_drop_metadata: %{section_id: section_id, cell_id: cell_id})
|
|> assign(file_drop_metadata: %{section_id: section_id, cell_id: cell_id})
|
||||||
|> push_patch(to: ~p"/sessions/#{socket.assigns.session.id}/add-file/upload")
|
|> push_patch(to: ~p"/sessions/#{socket.assigns.session.id}/add-file/upload")
|
||||||
|> push_event("finish_file_drop", %{})}
|
|> push_event("finish_file_drop", %{})}
|
||||||
else
|
|
||||||
reason = "To see the available options, you need a connected runtime."
|
|
||||||
{:noreply, confirm_setup_default_runtime(socket, reason)}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -883,6 +894,20 @@ defmodule LivebookWeb.SessionLive do
|
||||||
{:noreply, handle_operation(socket, operation)}
|
{:noreply, handle_operation(socket, operation)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_info({:error, error}, socket) when socket.assigns.live_action == :runtime_settings do
|
||||||
|
# When the runtime settings modal is open we assume the error is
|
||||||
|
# related to connecting the runtime and we show it dirrectly there
|
||||||
|
|
||||||
|
message = error |> to_string() |> upcase_first()
|
||||||
|
|
||||||
|
send_update(LivebookWeb.SessionLive.RuntimeComponent,
|
||||||
|
id: "runtime-settings",
|
||||||
|
event: {:error, message}
|
||||||
|
)
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_info({:error, error}, socket) do
|
def handle_info({:error, error}, socket) do
|
||||||
message = error |> to_string() |> upcase_first()
|
message = error |> to_string() |> upcase_first()
|
||||||
socket = put_flash(socket, :error, message)
|
socket = put_flash(socket, :error, message)
|
||||||
|
@ -1527,49 +1552,15 @@ defmodule LivebookWeb.SessionLive do
|
||||||
defp autofocus_cell_id(%Notebook{sections: [%{cells: [%{id: id, source: ""}]}]}), do: id
|
defp autofocus_cell_id(%Notebook{sections: [%{cells: [%{id: id, source: ""}]}]}), do: id
|
||||||
defp autofocus_cell_id(_notebook), do: nil
|
defp autofocus_cell_id(_notebook), do: nil
|
||||||
|
|
||||||
defp connect_runtime(socket) do
|
defp confirm_setup_runtime(socket, reason) do
|
||||||
case Runtime.connect(socket.private.data.runtime) do
|
|
||||||
{:ok, runtime} ->
|
|
||||||
Session.set_runtime(socket.assigns.session.pid, runtime)
|
|
||||||
{:ok, socket}
|
|
||||||
|
|
||||||
{:error, message} ->
|
|
||||||
{:error, put_flash(socket, :error, "Failed to connect runtime - #{message}")}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp maybe_reconnect_runtime(%{private: %{data: data}} = socket) do
|
|
||||||
if Runtime.connected?(data.runtime) do
|
|
||||||
data.runtime
|
|
||||||
|> Runtime.duplicate()
|
|
||||||
|> Runtime.connect()
|
|
||||||
|> case do
|
|
||||||
{:ok, new_runtime} ->
|
|
||||||
Session.set_runtime(socket.assigns.session.pid, new_runtime)
|
|
||||||
{:ok, clear_flash(socket, :error)}
|
|
||||||
|
|
||||||
{:error, message} ->
|
|
||||||
{:error, put_flash(socket, :error, "Failed to connect runtime - #{message}")}
|
|
||||||
end
|
|
||||||
else
|
|
||||||
{:ok, socket}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp confirm_setup_default_runtime(socket, reason) do
|
|
||||||
on_confirm = fn socket ->
|
on_confirm = fn socket ->
|
||||||
{status, socket} = connect_runtime(socket)
|
|
||||||
|
|
||||||
if status == :ok do
|
|
||||||
Session.queue_cell_evaluation(socket.assigns.session.pid, Cell.setup_cell_id())
|
Session.queue_cell_evaluation(socket.assigns.session.pid, Cell.setup_cell_id())
|
||||||
end
|
|
||||||
|
|
||||||
socket
|
socket
|
||||||
end
|
end
|
||||||
|
|
||||||
confirm(socket, on_confirm,
|
confirm(socket, on_confirm,
|
||||||
title: "Setup runtime",
|
title: "Setup runtime",
|
||||||
description: "#{reason} Do you want to connect and setup the default one?",
|
description: "#{reason} Do you want to connect and setup the current one?",
|
||||||
confirm_text: "Setup runtime",
|
confirm_text: "Setup runtime",
|
||||||
confirm_icon: "play-line",
|
confirm_icon: "play-line",
|
||||||
danger: false
|
danger: false
|
||||||
|
@ -1582,7 +1573,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
|
|
||||||
defp example_snippet_definition_by_name(data, name) do
|
defp example_snippet_definition_by_name(data, name) do
|
||||||
data.runtime
|
data.runtime
|
||||||
|> Livebook.Runtime.snippet_definitions()
|
|> Runtime.snippet_definitions()
|
||||||
|> Enum.find_value(:error, &(&1.type == :example && &1.name == name && {:ok, &1}))
|
|> Enum.find_value(:error, &(&1.type == :example && &1.name == name && {:ok, &1}))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1590,25 +1581,12 @@ defmodule LivebookWeb.SessionLive do
|
||||||
Enum.find_value(data.smart_cell_definitions, :error, &(&1.kind == kind && {:ok, &1}))
|
Enum.find_value(data.smart_cell_definitions, :error, &(&1.kind == kind && {:ok, &1}))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp add_dependencies_and_reevaluate(socket, dependencies) do
|
|
||||||
Session.add_dependencies(socket.assigns.session.pid, dependencies)
|
|
||||||
|
|
||||||
{status, socket} = maybe_reconnect_runtime(socket)
|
|
||||||
|
|
||||||
if status == :ok do
|
|
||||||
Session.queue_cell_evaluation(socket.assigns.session.pid, Cell.setup_cell_id())
|
|
||||||
Session.queue_cells_reevaluation(socket.assigns.session.pid)
|
|
||||||
end
|
|
||||||
|
|
||||||
socket
|
|
||||||
end
|
|
||||||
|
|
||||||
defp ensure_packages_then(socket, packages, target_name, target_type, fun) do
|
defp ensure_packages_then(socket, packages, target_name, target_type, fun) do
|
||||||
dependencies = Enum.map(packages, & &1.dependency)
|
dependencies = Enum.map(packages, & &1.dependency)
|
||||||
|
|
||||||
has_dependencies? =
|
has_dependencies? =
|
||||||
dependencies == [] or
|
dependencies == [] or
|
||||||
Livebook.Runtime.has_dependencies?(socket.private.data.runtime, dependencies)
|
Runtime.has_dependencies?(socket.private.data.runtime, dependencies)
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
has_dependencies? ->
|
has_dependencies? ->
|
||||||
|
@ -1617,7 +1595,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
:error -> socket
|
:error -> socket
|
||||||
end
|
end
|
||||||
|
|
||||||
Livebook.Runtime.fixed_dependencies?(socket.private.data.runtime) ->
|
Runtime.fixed_dependencies?(socket.private.data.runtime) ->
|
||||||
put_flash(socket, :error, "This runtime doesn't support adding dependencies")
|
put_flash(socket, :error, "This runtime doesn't support adding dependencies")
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
|
@ -1632,6 +1610,13 @@ defmodule LivebookWeb.SessionLive do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp add_dependencies_and_reevaluate(socket, dependencies) do
|
||||||
|
Session.add_dependencies(socket.assigns.session.pid, dependencies)
|
||||||
|
Session.queue_cell_evaluation(socket.assigns.session.pid, Cell.setup_cell_id())
|
||||||
|
Session.queue_cells_reevaluation(socket.assigns.session.pid)
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
defp confirm_add_packages(socket, on_confirm, packages, target_name, target_type) do
|
defp confirm_add_packages(socket, on_confirm, packages, target_name, target_type) do
|
||||||
assigns = %{packages: packages, target_name: target_name, target_type: target_type}
|
assigns = %{packages: packages, target_name: target_name, target_type: target_type}
|
||||||
|
|
||||||
|
@ -1728,7 +1713,7 @@ defmodule LivebookWeb.SessionLive do
|
||||||
|
|
||||||
defp handlers_for_file_entry(file_entry, runtime) do
|
defp handlers_for_file_entry(file_entry, runtime) do
|
||||||
handlers =
|
handlers =
|
||||||
for definition <- Livebook.Runtime.snippet_definitions(runtime),
|
for definition <- Runtime.snippet_definitions(runtime),
|
||||||
definition.type == :file_action,
|
definition.type == :file_action,
|
||||||
do: %{definition: definition, cell_type: :code}
|
do: %{definition: definition, cell_type: :code}
|
||||||
|
|
||||||
|
@ -1789,11 +1774,13 @@ defmodule LivebookWeb.SessionLive do
|
||||||
dirty: data.dirty,
|
dirty: data.dirty,
|
||||||
persistence_warnings: data.persistence_warnings,
|
persistence_warnings: data.persistence_warnings,
|
||||||
runtime: data.runtime,
|
runtime: data.runtime,
|
||||||
|
runtime_status: data.runtime_status,
|
||||||
|
runtime_connect_info: data.runtime_connect_info,
|
||||||
runtime_connected_nodes: Enum.sort(data.runtime_connected_nodes),
|
runtime_connected_nodes: Enum.sort(data.runtime_connected_nodes),
|
||||||
smart_cell_definitions: Enum.sort_by(data.smart_cell_definitions, & &1.name),
|
smart_cell_definitions: Enum.sort_by(data.smart_cell_definitions, & &1.name),
|
||||||
example_snippet_definitions:
|
example_snippet_definitions:
|
||||||
data.runtime
|
data.runtime
|
||||||
|> Livebook.Runtime.snippet_definitions()
|
|> Runtime.snippet_definitions()
|
||||||
|> Enum.filter(&(&1.type == :example))
|
|> Enum.filter(&(&1.type == :example))
|
||||||
|> Enum.sort_by(& &1.name),
|
|> Enum.sort_by(& &1.name),
|
||||||
global_status: global_status(data),
|
global_status: global_status(data),
|
||||||
|
|
|
@ -1,33 +1,39 @@
|
||||||
defmodule LivebookWeb.SessionLive.AttachedLive do
|
defmodule LivebookWeb.SessionLive.AttachedRuntimeComponent do
|
||||||
use LivebookWeb, :live_view
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
|
||||||
alias Livebook.{Session, Runtime}
|
alias Livebook.{Session, Runtime}
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(
|
def mount(socket) do
|
||||||
_params,
|
|
||||||
%{"session_pid" => session_pid, "current_runtime" => current_runtime},
|
|
||||||
socket
|
|
||||||
) do
|
|
||||||
session = Session.get_by_pid(session_pid)
|
|
||||||
|
|
||||||
unless Livebook.Config.runtime_enabled?(Livebook.Runtime.Attached) do
|
unless Livebook.Config.runtime_enabled?(Livebook.Runtime.Attached) do
|
||||||
raise "runtime module not allowed"
|
raise "runtime module not allowed"
|
||||||
end
|
end
|
||||||
|
|
||||||
if connected?(socket) do
|
{:ok, socket}
|
||||||
Session.subscribe(session.id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok,
|
@impl true
|
||||||
assign(socket,
|
def update(assigns, socket) do
|
||||||
session: session,
|
changeset =
|
||||||
current_runtime: current_runtime,
|
case socket.assigns[:changeset] do
|
||||||
error_message: nil,
|
nil ->
|
||||||
changeset: changeset(current_runtime)
|
changeset(assigns.runtime)
|
||||||
)}
|
|
||||||
|
changeset when socket.assigns.runtime == assigns.runtime ->
|
||||||
|
changeset
|
||||||
|
|
||||||
|
changeset ->
|
||||||
|
changeset(assigns.runtime, changeset.params)
|
||||||
|
end
|
||||||
|
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(assigns)
|
||||||
|
|> assign(:changeset, changeset)
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp changeset(runtime, attrs \\ %{}) do
|
defp changeset(runtime, attrs \\ %{}) do
|
||||||
|
@ -50,13 +56,10 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
||||||
def render(assigns) do
|
def render(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="flex-col space-y-5">
|
<div class="flex-col space-y-5">
|
||||||
<div :if={@error_message} class="error-box">
|
|
||||||
<%= @error_message %>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-700">
|
<p class="text-gray-700">
|
||||||
Connect the session to an already running node
|
Connect the session to an already running node
|
||||||
and evaluate code in the context of that node.
|
and evaluate code in the context of that node.
|
||||||
The node must run Erlang/OTP <%= :erlang.system_info(:otp_release) %> and Elixir <%= System.version() %> (or later).
|
The node must run Elixir <%= Livebook.Runtime.Attached.elixir_version_requirement() %>.
|
||||||
Make sure to give the node a name and a cookie, for example:
|
Make sure to give the node a name and a cookie, for example:
|
||||||
</p>
|
</p>
|
||||||
<div class="text-gray-700 markdown">
|
<div class="text-gray-700 markdown">
|
||||||
|
@ -71,6 +74,7 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
||||||
as={:data}
|
as={:data}
|
||||||
phx-submit="init"
|
phx-submit="init"
|
||||||
phx-change="validate"
|
phx-change="validate"
|
||||||
|
phx-target={@myself}
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
>
|
>
|
||||||
|
@ -78,62 +82,52 @@ defmodule LivebookWeb.SessionLive.AttachedLive do
|
||||||
<.text_field field={f[:name]} label="Name" placeholder={test_node()} />
|
<.text_field field={f[:name]} label="Name" placeholder={test_node()} />
|
||||||
<.text_field field={f[:cookie]} label="Cookie" placeholder="mycookie" />
|
<.text_field field={f[:cookie]} label="Cookie" placeholder="mycookie" />
|
||||||
</div>
|
</div>
|
||||||
<.button type="submit" disabled={not @changeset.valid?}>
|
<.button type="submit" disabled={@runtime_status == :connecting or not @changeset.valid?}>
|
||||||
<%= if(reconnecting?(@changeset), do: "Reconnect", else: "Connect") %>
|
<%= label(@changeset, @runtime_status) %>
|
||||||
</.button>
|
</.button>
|
||||||
</.form>
|
</.form>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp label(changeset, runtime_status) do
|
||||||
|
reconnecting? = changeset.valid? and changeset.data == apply_changes(changeset)
|
||||||
|
|
||||||
|
case {reconnecting?, runtime_status} do
|
||||||
|
{true, :connected} -> "Reconnect"
|
||||||
|
{true, :connecting} -> "Connecting..."
|
||||||
|
_ -> "Connect"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("validate", %{"data" => data}, socket) do
|
def handle_event("validate", %{"data" => data}, socket) do
|
||||||
changeset =
|
changeset =
|
||||||
socket.assigns.current_runtime |> changeset(data) |> Map.replace!(:action, :validate)
|
socket.assigns.runtime
|
||||||
|
|> changeset(data)
|
||||||
|
|> Map.replace!(:action, :validate)
|
||||||
|
|
||||||
{:noreply, assign(socket, changeset: changeset)}
|
{:noreply, assign(socket, changeset: changeset)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("init", %{"data" => data}, socket) do
|
def handle_event("init", %{"data" => data}, socket) do
|
||||||
socket.assigns.current_runtime
|
socket.assigns.runtime
|
||||||
|> changeset(data)
|
|> changeset(data)
|
||||||
|> apply_action(:insert)
|
|> apply_action(:insert)
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, data} ->
|
{:ok, data} ->
|
||||||
node = String.to_atom(data.name)
|
node = String.to_atom(data.name)
|
||||||
cookie = String.to_atom(data.cookie)
|
cookie = String.to_atom(data.cookie)
|
||||||
|
|
||||||
runtime = Runtime.Attached.new(node, cookie)
|
runtime = Runtime.Attached.new(node, cookie)
|
||||||
|
|
||||||
case Runtime.connect(runtime) do
|
|
||||||
{:ok, runtime} ->
|
|
||||||
Session.set_runtime(socket.assigns.session.pid, runtime)
|
Session.set_runtime(socket.assigns.session.pid, runtime)
|
||||||
{:noreply, assign(socket, changeset: changeset(runtime), error_message: nil)}
|
Session.connect_runtime(socket.assigns.session.pid)
|
||||||
|
{:noreply, socket}
|
||||||
{:error, message} ->
|
|
||||||
{:noreply,
|
|
||||||
assign(socket,
|
|
||||||
changeset: changeset(socket.assigns.current_runtime, data),
|
|
||||||
error_message: Livebook.Utils.upcase_first(message)
|
|
||||||
)}
|
|
||||||
end
|
|
||||||
|
|
||||||
{:error, changeset} ->
|
{:error, changeset} ->
|
||||||
{:noreply, assign(socket, changeset: changeset)}
|
{:noreply, assign(socket, changeset: changeset)}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:operation, {:set_runtime, _pid, runtime}}, socket) do
|
|
||||||
{:noreply, assign(socket, current_runtime: runtime)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_info(_message, socket), do: {:noreply, socket}
|
|
||||||
|
|
||||||
defp reconnecting?(changeset) do
|
|
||||||
changeset.valid? and changeset.data == apply_changes(changeset)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp test_node() do
|
defp test_node() do
|
||||||
"test@#{Livebook.Utils.node_host()}"
|
"test@#{Livebook.Utils.node_host()}"
|
||||||
end
|
end
|
|
@ -1,65 +0,0 @@
|
||||||
defmodule LivebookWeb.SessionLive.ElixirStandaloneLive do
|
|
||||||
use LivebookWeb, :live_view
|
|
||||||
|
|
||||||
alias Livebook.{Session, Runtime}
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def mount(
|
|
||||||
_params,
|
|
||||||
%{"session_pid" => session_pid, "current_runtime" => current_runtime},
|
|
||||||
socket
|
|
||||||
) do
|
|
||||||
session = Session.get_by_pid(session_pid)
|
|
||||||
|
|
||||||
unless Livebook.Config.runtime_enabled?(Livebook.Runtime.ElixirStandalone) do
|
|
||||||
raise "runtime module not allowed"
|
|
||||||
end
|
|
||||||
|
|
||||||
if connected?(socket) do
|
|
||||||
Session.subscribe(session.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, assign(socket, session: session, current_runtime: current_runtime, error_message: nil)}
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def render(assigns) do
|
|
||||||
~H"""
|
|
||||||
<div class="flex-col space-y-5">
|
|
||||||
<div :if={@error_message} class="error-box">
|
|
||||||
<%= @error_message %>
|
|
||||||
</div>
|
|
||||||
<p class="text-gray-700">
|
|
||||||
Start a new local node to evaluate code.
|
|
||||||
</p>
|
|
||||||
<.button phx-click="init">
|
|
||||||
<%= if(matching_runtime?(@current_runtime), do: "Reconnect", else: "Connect") %>
|
|
||||||
</.button>
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
end
|
|
||||||
|
|
||||||
defp matching_runtime?(%Runtime.ElixirStandalone{} = runtime), do: Runtime.connected?(runtime)
|
|
||||||
defp matching_runtime?(_runtime), do: false
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_event("init", _params, socket) do
|
|
||||||
Runtime.ElixirStandalone.new()
|
|
||||||
|> Runtime.connect()
|
|
||||||
|> case do
|
|
||||||
{:ok, runtime} ->
|
|
||||||
Session.set_runtime(socket.assigns.session.pid, runtime)
|
|
||||||
{:noreply, assign(socket, error_message: nil)}
|
|
||||||
|
|
||||||
{:error, message} ->
|
|
||||||
{:noreply, assign(socket, error_message: message)}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:operation, {:set_runtime, _pid, runtime}}, socket) do
|
|
||||||
{:noreply, assign(socket, current_runtime: runtime)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_info(_message, socket), do: {:noreply, socket}
|
|
||||||
end
|
|
|
@ -1,25 +1,15 @@
|
||||||
defmodule LivebookWeb.SessionLive.EmbeddedLive do
|
defmodule LivebookWeb.SessionLive.EmbeddedRuntimeComponent do
|
||||||
use LivebookWeb, :live_view
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
alias Livebook.{Session, Runtime}
|
alias Livebook.{Session, Runtime}
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(
|
def mount(socket) do
|
||||||
_params,
|
|
||||||
%{"session_pid" => session_pid, "current_runtime" => current_runtime},
|
|
||||||
socket
|
|
||||||
) do
|
|
||||||
session = Session.get_by_pid(session_pid)
|
|
||||||
|
|
||||||
unless Livebook.Config.runtime_enabled?(Livebook.Runtime.Embedded) do
|
unless Livebook.Config.runtime_enabled?(Livebook.Runtime.Embedded) do
|
||||||
raise "runtime module not allowed"
|
raise "runtime module not allowed"
|
||||||
end
|
end
|
||||||
|
|
||||||
if connected?(socket) do
|
{:ok, socket}
|
||||||
Session.subscribe(session.id)
|
|
||||||
end
|
|
||||||
|
|
||||||
{:ok, assign(socket, session: session, current_runtime: current_runtime)}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
@ -31,7 +21,7 @@ defmodule LivebookWeb.SessionLive.EmbeddedLive do
|
||||||
This is reserved for specific cases where there is no option
|
This is reserved for specific cases where there is no option
|
||||||
of starting a separate Elixir runtime (for example, on embedded
|
of starting a separate Elixir runtime (for example, on embedded
|
||||||
devices or cases where the amount of memory available is
|
devices or cases where the amount of memory available is
|
||||||
limited). Prefer the "Elixir standalone" runtime whenever possible.
|
limited). Prefer the "Standalone" runtime whenever possible.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-gray-700">
|
<p class="text-gray-700">
|
||||||
<span class="font-semibold">Warning:</span>
|
<span class="font-semibold">Warning:</span>
|
||||||
|
@ -39,27 +29,22 @@ defmodule LivebookWeb.SessionLive.EmbeddedLive do
|
||||||
you restart Livebook. Furthermore, code in one notebook
|
you restart Livebook. Furthermore, code in one notebook
|
||||||
may interfere with code from another notebook.
|
may interfere with code from another notebook.
|
||||||
</p>
|
</p>
|
||||||
<.button phx-click="init">
|
<.button phx-click="init" phx-target={@myself} disabled={@runtime_status == :connecting}>
|
||||||
<%= if(matching_runtime?(@current_runtime), do: "Reconnect", else: "Connect") %>
|
<%= label(@runtime, @runtime_status) %>
|
||||||
</.button>
|
</.button>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp matching_runtime?(%Runtime.Embedded{}), do: true
|
defp label(%Runtime.Embedded{}, :connecting), do: "Connecting..."
|
||||||
defp matching_runtime?(_runtime), do: false
|
defp label(%Runtime.Embedded{}, :connected), do: "Reconnect"
|
||||||
|
defp label(_runtime, _runtime_status), do: "Connect"
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("init", _params, socket) do
|
def handle_event("init", _params, socket) do
|
||||||
{:ok, runtime} = Runtime.Embedded.new() |> Runtime.connect()
|
runtime = Runtime.Embedded.new()
|
||||||
Session.set_runtime(socket.assigns.session.pid, runtime)
|
Session.set_runtime(socket.assigns.session.pid, runtime)
|
||||||
|
Session.connect_runtime(socket.assigns.session.pid)
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
|
||||||
def handle_info({:operation, {:set_runtime, _pid, runtime}}, socket) do
|
|
||||||
{:noreply, assign(socket, current_runtime: runtime)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def handle_info(_message, socket), do: {:noreply, socket}
|
|
||||||
end
|
end
|
748
lib/livebook_web/live/session_live/fly_runtime_component.ex
Normal file
748
lib/livebook_web/live/session_live/fly_runtime_component.ex
Normal file
|
@ -0,0 +1,748 @@
|
||||||
|
defmodule LivebookWeb.SessionLive.FlyRuntimeComponent do
|
||||||
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
|
import Ecto.Changeset
|
||||||
|
|
||||||
|
alias Livebook.{Session, Runtime}
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(socket) do
|
||||||
|
unless Livebook.Config.runtime_enabled?(Livebook.Runtime.Fly) do
|
||||||
|
raise "runtime module not allowed"
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok,
|
||||||
|
assign(socket,
|
||||||
|
token: nil,
|
||||||
|
token_check: %{status: :initial, error: nil},
|
||||||
|
org: nil,
|
||||||
|
regions: nil,
|
||||||
|
app_name: nil,
|
||||||
|
app_check: %{status: :initial, error: nil},
|
||||||
|
volumes: nil,
|
||||||
|
region: nil,
|
||||||
|
specs_changeset: specs_changeset(%{}),
|
||||||
|
volume_id: nil,
|
||||||
|
volume_action: nil
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def update(assigns, socket) do
|
||||||
|
socket =
|
||||||
|
case assigns.runtime do
|
||||||
|
%Runtime.Fly{config: config} when not is_map_key(socket.assigns, :runtime) ->
|
||||||
|
assign(socket,
|
||||||
|
token: config.token,
|
||||||
|
app_name: config.app_name,
|
||||||
|
specs_changeset: specs_changeset(config)
|
||||||
|
)
|
||||||
|
|> load_org_and_regions()
|
||||||
|
|> load_app()
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
socket
|
||||||
|
end
|
||||||
|
|
||||||
|
socket = assign(socket, assigns)
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div>
|
||||||
|
<p class="text-gray-700">
|
||||||
|
Start a temporary Fly.io machine with an Elixir node to evaluate code.
|
||||||
|
The machine is automatically destroyed, once you disconnect the runtime.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<form class="mt-4 flex flex-col gap-4" phx-change="set_token" phx-nosubmit phx-target={@myself}>
|
||||||
|
<.password_field name="token" value={@token} label="Token" />
|
||||||
|
<.message_box :if={@token == nil} kind={:info}>
|
||||||
|
Go to <a
|
||||||
|
class="text-blue-600 hover:text-blue-700"
|
||||||
|
href="https://fly.io/dashboard"
|
||||||
|
phx-no-format
|
||||||
|
>Fly dashboard</a>, click "Tokens" in the left sidebar and create a new
|
||||||
|
token for your organization of choice. This functionality is restricted
|
||||||
|
to organization admins. Alternatively, you can create an app in the
|
||||||
|
organization by running <code>fly app create</code>
|
||||||
|
and generate a deploy token in
|
||||||
|
the app dashboard.
|
||||||
|
</.message_box>
|
||||||
|
<.loader :if={@token_check.status == :inflight} />
|
||||||
|
<.message_box
|
||||||
|
:if={error = @token_check.error}
|
||||||
|
kind={:error}
|
||||||
|
message={"Error: " <> error.message}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<.app_config
|
||||||
|
:if={@token_check.status == :ok}
|
||||||
|
org_name={@org.name}
|
||||||
|
regions={@regions}
|
||||||
|
app_name={@app_name}
|
||||||
|
app_check={@app_check}
|
||||||
|
volumes={@volumes}
|
||||||
|
region={@region}
|
||||||
|
myself={@myself}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div :if={@token_check.status == :ok and @app_check.status == :ok}>
|
||||||
|
<.specs_config specs_changeset={@specs_changeset} myself={@myself} />
|
||||||
|
|
||||||
|
<.storage_config
|
||||||
|
volumes={@volumes}
|
||||||
|
volume_id={@volume_id}
|
||||||
|
region={@region}
|
||||||
|
volume_action={@volume_action}
|
||||||
|
myself={@myself}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-8">
|
||||||
|
<.button
|
||||||
|
phx-click="init"
|
||||||
|
phx-target={@myself}
|
||||||
|
disabled={
|
||||||
|
@runtime_status == :connecting or not @specs_changeset.valid? or
|
||||||
|
volume_errors(@volume_id, @volumes, @region) != []
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<%= label(@app_name, @runtime, @runtime_status) %>
|
||||||
|
</.button>
|
||||||
|
<div
|
||||||
|
:if={reconnecting?(@app_name, @runtime) && @runtime_connect_info}
|
||||||
|
class="mt-4 scroll-mb-8"
|
||||||
|
phx-mounted={JS.dispatch("lb:scroll_into_view", detail: %{behavior: "instant"})}
|
||||||
|
>
|
||||||
|
<.message_box kind={:info}>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<.spinner />
|
||||||
|
<span>Step: <%= @runtime_connect_info %></span>
|
||||||
|
</div>
|
||||||
|
</.message_box>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp loader(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="text-sm font-gray-700">Loading</span>
|
||||||
|
<.spinner />
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp app_config(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="mt-4 flex flex-col gap-4">
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<.text_field name="org" label="Organization" value={@org_name} readonly />
|
||||||
|
<form
|
||||||
|
phx-change="set_app_name"
|
||||||
|
phx-nosubmit
|
||||||
|
phx-target={@myself}
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
>
|
||||||
|
<.text_field name="app_name" label="App" value={@app_name} phx-debounce="500" />
|
||||||
|
</form>
|
||||||
|
<form phx-change="set_region" phx-nosubmit phx-target={@myself}>
|
||||||
|
<.select_field
|
||||||
|
name="region"
|
||||||
|
label="Region"
|
||||||
|
value={@region}
|
||||||
|
options={region_options(@regions)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<.message_box
|
||||||
|
:if={@app_name == nil}
|
||||||
|
kind={:info}
|
||||||
|
message="Specify the app where machines should be created."
|
||||||
|
/>
|
||||||
|
<.loader :if={@app_check.status == :inflight} />
|
||||||
|
<.app_check_error
|
||||||
|
:if={@app_check.error}
|
||||||
|
error={@app_check.error}
|
||||||
|
app_name={@app_name}
|
||||||
|
myself={@myself}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp app_check_error(%{error: %{status: 404}} = assigns) do
|
||||||
|
~H"""
|
||||||
|
<.message_box kind={:info}>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
App <span class="font-semibold"><%= @app_name %></span> does not exist yet.
|
||||||
|
</div>
|
||||||
|
<.button phx-click="create_app" phx-target={@myself}>
|
||||||
|
Create
|
||||||
|
</.button>
|
||||||
|
</div>
|
||||||
|
</.message_box>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp app_check_error(assigns) do
|
||||||
|
~H"""
|
||||||
|
<.message_box kind={:error} message={"Error: " <> @error.message} />
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp specs_config(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="mt-8">
|
||||||
|
<div class="text-lg text-gray-800 font-semibold">
|
||||||
|
Specs
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-gray-700">
|
||||||
|
For more details refer to
|
||||||
|
<a
|
||||||
|
class="text-blue-600 hover:text-blue-700"
|
||||||
|
href="https://fly.io/docs/machines/guides-examples/machine-sizing"
|
||||||
|
>
|
||||||
|
Machine sizing
|
||||||
|
</a>
|
||||||
|
and
|
||||||
|
<a class="text-blue-600 hover:text-blue-700" href="https://fly.io/docs/about/pricing">
|
||||||
|
Pricing
|
||||||
|
</a>
|
||||||
|
pages in the Fly.io documentation.
|
||||||
|
</div>
|
||||||
|
<.form
|
||||||
|
:let={f}
|
||||||
|
for={@specs_changeset}
|
||||||
|
as={:specs}
|
||||||
|
class="mt-4 flex flex-col gap-4"
|
||||||
|
phx-change="validate_specs"
|
||||||
|
phx-nosubmit
|
||||||
|
phx-target={@myself}
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-5 gap-2">
|
||||||
|
<.select_field field={f[:cpu_kind]} label="CPU kind" options={cpu_kind_options()} />
|
||||||
|
<.text_field field={f[:cpus]} label="CPUs" type="number" min="1" />
|
||||||
|
<.text_field field={f[:memory_gb]} label="Memory (GB)" type="number" step="1" min="1" />
|
||||||
|
<.select_field field={f[:gpu_kind]} label="GPU kind" options={gpu_kind_options()} />
|
||||||
|
<.text_field
|
||||||
|
field={f[:gpus]}
|
||||||
|
label="GPUs"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
disabled={get_field(@specs_changeset, :gpu_kind) == nil}
|
||||||
|
/>
|
||||||
|
<div class="col-span-5 text-sm text-gray-700">
|
||||||
|
GPUs are available only in certain regions, see
|
||||||
|
<a
|
||||||
|
class="text-blue-600 hover:text-blue-700"
|
||||||
|
href="https://fly.io/docs/gpus/getting-started-gpus/#specify-the-region"
|
||||||
|
>
|
||||||
|
Getting started with GPUs.
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<.radio_field
|
||||||
|
field={f[:docker_tag]}
|
||||||
|
label="Base Docker image"
|
||||||
|
options={LivebookWeb.AppComponents.docker_tag_options()}
|
||||||
|
/>
|
||||||
|
</.form>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp storage_config(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="mt-8">
|
||||||
|
<div class="text-lg text-gray-800 font-semibold">
|
||||||
|
Storage
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-gray-700">
|
||||||
|
Every time you connect to the runtime, a fresh machine is created.
|
||||||
|
In order to persist data and caches, you can optionally mount a
|
||||||
|
volume at <code>/home/livebook</code>.
|
||||||
|
Keep in mind that volumes are billed even when not in use, so you
|
||||||
|
may want to remove those no longer needed.
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex flex-col gap-4">
|
||||||
|
<div class="flex items-start gap-1">
|
||||||
|
<div class="grow">
|
||||||
|
<form phx-change="set_volume_id" phx-nosubmit phx-target={@myself}>
|
||||||
|
<.select_field
|
||||||
|
name="volume_id"
|
||||||
|
label="Volume"
|
||||||
|
value={@volume_id}
|
||||||
|
options={[{"None", ""} | volume_options(@volumes)]}
|
||||||
|
errors={volume_errors(@volume_id, @volumes, @region)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="mt-7 flex items-center gap-1">
|
||||||
|
<span class="tooltip left" data-tooltip="Delete selected volume">
|
||||||
|
<.icon_button
|
||||||
|
phx-click="delete_volume"
|
||||||
|
phx-target={@myself}
|
||||||
|
disabled={@volume_id == nil or (@volume_action != nil and @volume_action.inflight)}
|
||||||
|
>
|
||||||
|
<.remix_icon icon="delete-bin-6-line" />
|
||||||
|
</.icon_button>
|
||||||
|
</span>
|
||||||
|
<span class="tooltip left" data-tooltip="Create new volume">
|
||||||
|
<.icon_button phx-click="new_volume" phx-target={@myself}>
|
||||||
|
<.remix_icon icon="add-line" />
|
||||||
|
</.icon_button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:if={@volume_action[:type] == :delete}
|
||||||
|
class="px-4 py-3 flex space-x-4 items-center border border-gray-200 rounded-lg"
|
||||||
|
>
|
||||||
|
<p class="grow text-gray-700 text-sm">
|
||||||
|
Are you sure you want to irreversibly delete <span class="font-semibold"><%= @volume_id %></span>?
|
||||||
|
</p>
|
||||||
|
<div class="flex space-x-4">
|
||||||
|
<button
|
||||||
|
class="text-red-600 font-medium text-sm whitespace-nowrap"
|
||||||
|
phx-click="confirm_delete_volume"
|
||||||
|
phx-target={@myself}
|
||||||
|
disabled={@volume_action.inflight}
|
||||||
|
>
|
||||||
|
<.remix_icon icon="delete-bin-6-line" class="align-middle mr-1" /> Delete
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="text-gray-600 font-medium text-sm"
|
||||||
|
phx-click="cancel_delete_volume"
|
||||||
|
phx-target={@myself}
|
||||||
|
disabled={@volume_action.inflight}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<.form
|
||||||
|
:let={f}
|
||||||
|
:if={@volume_action[:type] == :new}
|
||||||
|
for={@volume_action.changeset}
|
||||||
|
as={:volume}
|
||||||
|
phx-submit="create_volume"
|
||||||
|
phx-change="validate_volume"
|
||||||
|
phx-target={@myself}
|
||||||
|
class="flex gap-2 items-center"
|
||||||
|
autocomplete="off"
|
||||||
|
spellcheck="false"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<.remix_icon icon="corner-down-right-line" class="text-gray-400 text-lg" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-2 grow">
|
||||||
|
<.text_field field={f[:name]} placeholder="Name" />
|
||||||
|
<.text_field field={f[:size_gb]} placeholder="Size (GB)" type="number" min="1" />
|
||||||
|
</div>
|
||||||
|
<.button
|
||||||
|
type="button"
|
||||||
|
color="gray"
|
||||||
|
outlined
|
||||||
|
phx-click="cancel_new_volume"
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</.button>
|
||||||
|
<.button
|
||||||
|
type="submit"
|
||||||
|
disabled={not @volume_action.changeset.valid? or @volume_action.inflight}
|
||||||
|
>
|
||||||
|
<%= if(@volume_action.inflight, do: "Creating...", else: "Create") %>
|
||||||
|
</.button>
|
||||||
|
</.form>
|
||||||
|
<div :if={error = @volume_action[:error]}>
|
||||||
|
<.message_box kind={:error} message={error} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("set_token", %{"token" => token}, socket) do
|
||||||
|
{:noreply, socket |> assign(token: nullify(token)) |> load_org_and_regions()}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("set_app_name", %{"app_name" => app_name}, socket) do
|
||||||
|
{:noreply, socket |> assign(app_name: nullify(app_name)) |> load_app()}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("set_region", %{"region" => region}, socket) do
|
||||||
|
{:noreply, assign(socket, region: region)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("create_app", %{}, socket) do
|
||||||
|
{:noreply, create_app(socket)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("set_volume_id", %{"volume_id" => volume_id}, socket) do
|
||||||
|
{:noreply, assign(socket, volume_id: nullify(volume_id), volume_action: nil)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("delete_volume", %{}, socket) do
|
||||||
|
volume_action = %{type: :delete, inflight: false, error: nil}
|
||||||
|
{:noreply, assign(socket, volume_action: volume_action)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("cancel_delete_volume", %{}, socket) do
|
||||||
|
{:noreply, assign(socket, volume_action: nil)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("confirm_delete_volume", %{}, socket) do
|
||||||
|
{:noreply, delete_volume(socket)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("new_volume", %{}, socket) do
|
||||||
|
volume_action = %{type: :new, changeset: volume_changeset(), inflight: false, error: false}
|
||||||
|
{:noreply, assign(socket, volume_action: volume_action)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("cancel_new_volume", %{}, socket) do
|
||||||
|
{:noreply, assign(socket, volume_action: nil)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("validate_volume", %{"volume" => volume}, socket) do
|
||||||
|
changeset =
|
||||||
|
volume
|
||||||
|
|> volume_changeset()
|
||||||
|
|> Map.replace!(:action, :validate)
|
||||||
|
|
||||||
|
{:noreply, assign_nested(socket, :volume_action, changeset: changeset)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("create_volume", %{"volume" => volume}, socket) do
|
||||||
|
volume
|
||||||
|
|> volume_changeset()
|
||||||
|
|> apply_action(:insert)
|
||||||
|
|> case do
|
||||||
|
{:ok, %{name: name, size_gb: size_gb}} ->
|
||||||
|
{:noreply, create_volume(socket, name, size_gb)}
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
{:noreply, assign_nested(socket, :volume_action, changeset: changeset)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("validate_specs", %{"specs" => specs}, socket) do
|
||||||
|
changeset =
|
||||||
|
socket.assigns.specs_changeset.data
|
||||||
|
|> specs_changeset(specs)
|
||||||
|
|> Map.replace!(:action, :validate)
|
||||||
|
|
||||||
|
{:noreply, assign(socket, specs_changeset: changeset)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("init", %{}, socket) do
|
||||||
|
socket.assigns.specs_changeset
|
||||||
|
|> apply_action(:insert)
|
||||||
|
|> case do
|
||||||
|
{:ok, specs} ->
|
||||||
|
config = %{
|
||||||
|
token: socket.assigns.token,
|
||||||
|
app_name: socket.assigns.app_name,
|
||||||
|
region: socket.assigns.region,
|
||||||
|
cpu_kind: specs.cpu_kind,
|
||||||
|
cpus: specs.cpus,
|
||||||
|
memory_gb: specs.memory_gb,
|
||||||
|
gpu_kind: specs.gpu_kind,
|
||||||
|
gpus: specs.gpus,
|
||||||
|
volume_id: socket.assigns.volume_id,
|
||||||
|
docker_tag: specs.docker_tag
|
||||||
|
}
|
||||||
|
|
||||||
|
runtime = Runtime.Fly.new(config)
|
||||||
|
Session.set_runtime(socket.assigns.session.pid, runtime)
|
||||||
|
Session.connect_runtime(socket.assigns.session.pid)
|
||||||
|
{:noreply, socket}
|
||||||
|
|
||||||
|
{:error, changeset} ->
|
||||||
|
{:noreply, assign(socket, specs_changeset: changeset)}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_async(:load_org_and_regions, {:ok, result}, socket) do
|
||||||
|
socket =
|
||||||
|
case result do
|
||||||
|
{:ok, %{orgs: [org]} = data} ->
|
||||||
|
region =
|
||||||
|
case socket.assigns.runtime do
|
||||||
|
%Runtime.Fly{config: config} -> config.region
|
||||||
|
_ -> data.closest_region
|
||||||
|
end
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(org: org, regions: data.regions, region: region)
|
||||||
|
|> assign(:token_check, %{status: :ok, error: nil})
|
||||||
|
|
||||||
|
{:ok, %{orgs: orgs}} ->
|
||||||
|
error =
|
||||||
|
"expected organization-specific auth token, but the given one gives access to #{length(orgs)} organizations"
|
||||||
|
|
||||||
|
assign(socket, :token_check, %{status: :error, error: error})
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
assign(socket, :token_check, %{status: :error, error: error})
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_async(:load_app, {:ok, result}, socket) do
|
||||||
|
socket =
|
||||||
|
case result do
|
||||||
|
{:ok, volumes} ->
|
||||||
|
volume_id =
|
||||||
|
case socket.assigns.runtime do
|
||||||
|
%Runtime.Fly{config: %{volume_id: volume_id}} ->
|
||||||
|
# Ignore the volume if it no longer exists
|
||||||
|
if Enum.any?(volumes, &(&1.id == volume_id)), do: volume_id
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> assign(volumes: volumes, volume_id: volume_id)
|
||||||
|
|> assign(:app_check, %{status: :ok, error: nil})
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
assign(socket, :app_check, %{status: :error, error: error})
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_async(:create_app, {:ok, result}, socket) do
|
||||||
|
socket =
|
||||||
|
case result do
|
||||||
|
:ok ->
|
||||||
|
socket
|
||||||
|
|> assign(volumes: [], volume_id: nil)
|
||||||
|
|> assign(:app_check, %{status: :ok, error: nil})
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
assign(socket, :app_check, %{status: :error, error: error})
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_async(:create_volume, {:ok, result}, socket) do
|
||||||
|
socket =
|
||||||
|
case result do
|
||||||
|
{:ok, volume} ->
|
||||||
|
volumes = [volume | socket.assigns.volumes]
|
||||||
|
assign(socket, volumes: volumes, volume_id: volume.id, volume_action: nil)
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
assign_nested(socket, :volume_action, error: error, inflight: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_async(:delete_volume, {:ok, result}, socket) do
|
||||||
|
volume_id = socket.assigns.volume_id
|
||||||
|
|
||||||
|
socket =
|
||||||
|
case result do
|
||||||
|
:ok ->
|
||||||
|
volumes = Enum.reject(socket.assigns.volumes, &(&1.id == volume_id))
|
||||||
|
assign(socket, volumes: volumes, volume_id: nil, volume_action: nil)
|
||||||
|
|
||||||
|
{:error, error} ->
|
||||||
|
assign_nested(socket, :volume_action, error: error, inflight: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp label(app_name, runtime, runtime_status) do
|
||||||
|
reconnecting? = reconnecting?(app_name, runtime)
|
||||||
|
|
||||||
|
case {reconnecting?, runtime_status} do
|
||||||
|
{true, :connected} -> "Reconnect"
|
||||||
|
{true, :connecting} -> "Connecting..."
|
||||||
|
_ -> "Connect"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp reconnecting?(app_name, runtime) do
|
||||||
|
match?(%Runtime.Fly{config: %{app_name: ^app_name}}, runtime)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp cpu_kind_options() do
|
||||||
|
Enum.map(Livebook.FlyAPI.cpu_kinds(), &{&1, &1})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp gpu_kind_options() do
|
||||||
|
[{"None", ""}] ++ Enum.map(Livebook.FlyAPI.gpu_kinds(), &{&1, &1})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp region_options(regions) do
|
||||||
|
for region <- regions,
|
||||||
|
do: {"#{region.name} (#{region.code})", region.code}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp volume_options(volumes) do
|
||||||
|
for volume <- Enum.sort_by(volumes, &{&1.name, &1.id}),
|
||||||
|
do: {
|
||||||
|
"#{volume.id} (name: #{volume.name}, region: #{volume.region}, size: #{volume.size_gb} GB)",
|
||||||
|
volume.id
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp specs_changeset(config, attrs \\ %{}) do
|
||||||
|
defaults = %{
|
||||||
|
cpu_kind: "shared",
|
||||||
|
cpus: 1,
|
||||||
|
memory_gb: 1,
|
||||||
|
gpu_kind: nil,
|
||||||
|
gpus: nil,
|
||||||
|
docker_tag: Livebook.Config.docker_images() |> hd() |> Map.fetch!(:tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = for {key, default} <- defaults, into: %{}, do: {key, Map.get(config, key, default)}
|
||||||
|
|
||||||
|
types = %{
|
||||||
|
cpu_kind: :string,
|
||||||
|
cpus: :integer,
|
||||||
|
memory_gb: :integer,
|
||||||
|
gpu_kind: :string,
|
||||||
|
gpus: :integer,
|
||||||
|
docker_tag: :string
|
||||||
|
}
|
||||||
|
|
||||||
|
changeset =
|
||||||
|
cast({data, types}, attrs, Map.keys(types))
|
||||||
|
|> validate_required([:cpu_kind, :cpus, :memory_gb, :docker_tag])
|
||||||
|
|
||||||
|
if get_field(changeset, :gpu_kind) do
|
||||||
|
changeset
|
||||||
|
else
|
||||||
|
# We may be reverting back to the defult, so we force the change
|
||||||
|
# to take precedence over form params in Phoenix.HTML.FormData
|
||||||
|
force_change(changeset, :gpus, nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp volume_changeset(attrs \\ %{}) do
|
||||||
|
data = %{name: nil, size_gb: nil}
|
||||||
|
|
||||||
|
types = %{
|
||||||
|
name: :string,
|
||||||
|
size_gb: :integer
|
||||||
|
}
|
||||||
|
|
||||||
|
cast({data, types}, attrs, Map.keys(types))
|
||||||
|
|> validate_required([:name, :size_gb])
|
||||||
|
end
|
||||||
|
|
||||||
|
defp volume_errors(nil, _volumes, _region), do: []
|
||||||
|
|
||||||
|
defp volume_errors(volume_id, volumes, region) do
|
||||||
|
volume = Enum.find(volumes, &(&1.id == volume_id))
|
||||||
|
|
||||||
|
if volume.region == region do
|
||||||
|
[]
|
||||||
|
else
|
||||||
|
["must be in the same region as the machine (#{region})"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_org_and_regions(socket) when socket.assigns.token == nil do
|
||||||
|
assign(socket, :token_check, %{status: :initial, error: nil})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_org_and_regions(socket) do
|
||||||
|
token = socket.assigns.token
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> start_async(:load_org_and_regions, fn ->
|
||||||
|
Livebook.FlyAPI.get_orgs_and_regions(token)
|
||||||
|
end)
|
||||||
|
|> assign(:token_check, %{status: :inflight, error: nil})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_app(socket) when socket.assigns.app_name == nil do
|
||||||
|
assign(socket, :app_check, %{status: :initial, error: nil})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp load_app(socket) do
|
||||||
|
%{token: token, app_name: app_name} = socket.assigns
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> start_async(:load_app, fn ->
|
||||||
|
Livebook.FlyAPI.get_app_volumes(token, app_name)
|
||||||
|
end)
|
||||||
|
|> assign(:app_check, %{status: :inflight, error: nil})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_app(socket) do
|
||||||
|
%{token: token, app_name: app_name} = socket.assigns
|
||||||
|
org_slug = socket.assigns.org.slug
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> start_async(:create_app, fn ->
|
||||||
|
Livebook.FlyAPI.create_app(token, app_name, org_slug)
|
||||||
|
end)
|
||||||
|
|> assign(:app_check, %{status: :inflight, error: nil})
|
||||||
|
end
|
||||||
|
|
||||||
|
defp delete_volume(socket) do
|
||||||
|
%{token: token, app_name: app_name, volume_id: volume_id} = socket.assigns
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> start_async(:delete_volume, fn ->
|
||||||
|
Livebook.FlyAPI.delete_volume(token, app_name, volume_id)
|
||||||
|
end)
|
||||||
|
|> assign_nested(:volume_action, inflight: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_volume(socket, name, size_gb) do
|
||||||
|
%{token: token, app_name: app_name, region: region} = socket.assigns
|
||||||
|
|
||||||
|
specs = apply_changes(socket.assigns.specs_changeset)
|
||||||
|
|
||||||
|
compute = %{
|
||||||
|
cpu_kind: specs.cpu_kind,
|
||||||
|
cpus: specs.cpus,
|
||||||
|
memory_mb: specs.memory_gb * 1024,
|
||||||
|
gpu_kind: specs.gpu_kind,
|
||||||
|
gpus: specs.gpus
|
||||||
|
}
|
||||||
|
|
||||||
|
socket
|
||||||
|
|> start_async(:create_volume, fn ->
|
||||||
|
Livebook.FlyAPI.create_volume(token, app_name, name, region, size_gb, compute)
|
||||||
|
end)
|
||||||
|
|> assign_nested(:volume_action, inflight: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp assign_nested(socket, key, keyword) do
|
||||||
|
update(socket, key, fn map ->
|
||||||
|
Enum.reduce(keyword, map, fn {key, value}, map -> Map.replace!(map, key, value) end)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp nullify(""), do: nil
|
||||||
|
defp nullify(value), do: value
|
||||||
|
end
|
|
@ -148,9 +148,9 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
|
||||||
<% end %>
|
<% end %>
|
||||||
</.menu>
|
</.menu>
|
||||||
<%= cond do %>
|
<%= cond do %>
|
||||||
<% not Livebook.Runtime.connected?(@runtime) -> %>
|
<% @runtime_status == :disconnected -> %>
|
||||||
<.insert_button phx-click={
|
<.insert_button phx-click={
|
||||||
JS.push("setup_default_runtime",
|
JS.push("setup_runtime",
|
||||||
value: %{reason: "To see the available smart cells, you need a connected runtime."}
|
value: %{reason: "To see the available smart cells, you need a connected runtime."}
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
|
|
|
@ -33,7 +33,7 @@ defmodule LivebookWeb.SessionLive.Render do
|
||||||
dirty={@data_view.dirty}
|
dirty={@data_view.dirty}
|
||||||
persistence_warnings={@data_view.persistence_warnings}
|
persistence_warnings={@data_view.persistence_warnings}
|
||||||
autosave_interval_s={@data_view.autosave_interval_s}
|
autosave_interval_s={@data_view.autosave_interval_s}
|
||||||
runtime={@data_view.runtime}
|
runtime_status={@data_view.runtime_status}
|
||||||
global_status={@data_view.global_status}
|
global_status={@data_view.global_status}
|
||||||
/>
|
/>
|
||||||
<.notebook_content
|
<.notebook_content
|
||||||
|
@ -61,6 +61,8 @@ defmodule LivebookWeb.SessionLive.Render do
|
||||||
id="runtime-settings"
|
id="runtime-settings"
|
||||||
session={@session}
|
session={@session}
|
||||||
runtime={@data_view.runtime}
|
runtime={@data_view.runtime}
|
||||||
|
runtime_status={@data_view.runtime_status}
|
||||||
|
runtime_connect_info={@data_view.runtime_connect_info}
|
||||||
/>
|
/>
|
||||||
</.modal>
|
</.modal>
|
||||||
|
|
||||||
|
@ -652,23 +654,24 @@ defmodule LivebookWeb.SessionLive.Render do
|
||||||
</.labeled_text>
|
</.labeled_text>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-4 grid grid-cols-2 gap-2">
|
<div class="mt-4 grid grid-cols-2 gap-2">
|
||||||
<%= if Runtime.connected?(@data_view.runtime) do %>
|
<.button :if={@data_view.runtime_status == :disconnected} phx-click="connect_runtime">
|
||||||
<.button phx-click="reconnect_runtime">
|
|
||||||
<.remix_icon icon="wireless-charging-line" />
|
|
||||||
<span>Reconnect</span>
|
|
||||||
</.button>
|
|
||||||
<% else %>
|
|
||||||
<.button phx-click="connect_runtime">
|
|
||||||
<.remix_icon icon="wireless-charging-line" />
|
<.remix_icon icon="wireless-charging-line" />
|
||||||
<span>Connect</span>
|
<span>Connect</span>
|
||||||
</.button>
|
</.button>
|
||||||
<% end %>
|
<.button :if={@data_view.runtime_status == :connecting} disabled>
|
||||||
|
<.remix_icon icon="wireless-charging-line" />
|
||||||
|
<span>Connecting...</span>
|
||||||
|
</.button>
|
||||||
|
<.button :if={@data_view.runtime_status == :connected} phx-click="reconnect_runtime">
|
||||||
|
<.remix_icon icon="wireless-charging-line" />
|
||||||
|
<span>Reconnect</span>
|
||||||
|
</.button>
|
||||||
<.button color="gray" outlined patch={~p"/sessions/#{@session.id}/settings/runtime"}>
|
<.button color="gray" outlined patch={~p"/sessions/#{@session.id}/settings/runtime"}>
|
||||||
Configure
|
Configure
|
||||||
</.button>
|
</.button>
|
||||||
|
|
||||||
<.button
|
<.button
|
||||||
:if={Runtime.connected?(@data_view.runtime)}
|
:if={@data_view.runtime_status == :connected}
|
||||||
color="red"
|
color="red"
|
||||||
outlined
|
outlined
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -679,6 +682,15 @@ defmodule LivebookWeb.SessionLive.Render do
|
||||||
</.button>
|
</.button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div :if={@data_view.runtime_connect_info} class="mt-4">
|
||||||
|
<.message_box kind={:info}>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<.spinner />
|
||||||
|
<span>Step: <%= @data_view.runtime_connect_info %></span>
|
||||||
|
</div>
|
||||||
|
</.message_box>
|
||||||
|
</div>
|
||||||
|
|
||||||
<.memory_usage_info memory_usage={@session.memory_usage} />
|
<.memory_usage_info memory_usage={@session.memory_usage} />
|
||||||
|
|
||||||
<.runtime_connected_nodes_info runtime_connected_nodes={@data_view.runtime_connected_nodes} />
|
<.runtime_connected_nodes_info runtime_connected_nodes={@data_view.runtime_connected_nodes} />
|
||||||
|
@ -690,13 +702,8 @@ defmodule LivebookWeb.SessionLive.Render do
|
||||||
defp memory_usage_info(assigns) do
|
defp memory_usage_info(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<div class="mt-8 flex flex-col gap-2">
|
<div class="mt-8 flex flex-col gap-2">
|
||||||
<div class="text-sm text-gray-800 flex flex-row justify-between">
|
<div class="text-sm text-gray-500 font-semibold uppercase">
|
||||||
<span class="text-gray-500 font-semibold uppercase">
|
|
||||||
Memory
|
Memory
|
||||||
</span>
|
|
||||||
<span :if={uses_memory?(@memory_usage)}>
|
|
||||||
<%= format_bytes(@memory_usage.system.free) %> available
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<%= if uses_memory?(@memory_usage) do %>
|
<%= if uses_memory?(@memory_usage) do %>
|
||||||
<.runtime_memory_info memory_usage={@memory_usage} />
|
<.runtime_memory_info memory_usage={@memory_usage} />
|
||||||
|
@ -1003,7 +1010,7 @@ defmodule LivebookWeb.SessionLive.Render do
|
||||||
session_id={@session_id}
|
session_id={@session_id}
|
||||||
/>
|
/>
|
||||||
<.runtime_indicator
|
<.runtime_indicator
|
||||||
runtime={@runtime}
|
runtime_status={@runtime_status}
|
||||||
global_status={@global_status}
|
global_status={@global_status}
|
||||||
session_id={@session_id}
|
session_id={@session_id}
|
||||||
/>
|
/>
|
||||||
|
@ -1133,9 +1140,7 @@ defmodule LivebookWeb.SessionLive.Render do
|
||||||
|
|
||||||
defp runtime_indicator(assigns) do
|
defp runtime_indicator(assigns) do
|
||||||
~H"""
|
~H"""
|
||||||
<%= if Livebook.Runtime.connected?(@runtime) do %>
|
<%= if @runtime_status == :disconnected do %>
|
||||||
<.global_status status={elem(@global_status, 0)} cell_id={elem(@global_status, 1)} />
|
|
||||||
<% else %>
|
|
||||||
<span class="tooltip left" data-tooltip="Choose a runtime to run the notebook in">
|
<span class="tooltip left" data-tooltip="Choose a runtime to run the notebook in">
|
||||||
<.link
|
<.link
|
||||||
patch={~p"/sessions/#{@session_id}/settings/runtime"}
|
patch={~p"/sessions/#{@session_id}/settings/runtime"}
|
||||||
|
@ -1145,6 +1150,8 @@ defmodule LivebookWeb.SessionLive.Render do
|
||||||
<.remix_icon icon="loader-3-line" />
|
<.remix_icon icon="loader-3-line" />
|
||||||
</.link>
|
</.link>
|
||||||
</span>
|
</span>
|
||||||
|
<% else %>
|
||||||
|
<.global_status status={elem(@global_status, 0)} cell_id={elem(@global_status, 1)} />
|
||||||
<% end %>
|
<% end %>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
@ -1344,7 +1351,7 @@ defmodule LivebookWeb.SessionLive.Render do
|
||||||
session_id={@session.id}
|
session_id={@session.id}
|
||||||
session_pid={@session.pid}
|
session_pid={@session.pid}
|
||||||
client_id={@client_id}
|
client_id={@client_id}
|
||||||
runtime={@data_view.runtime}
|
runtime_status={@data_view.runtime_status}
|
||||||
smart_cell_definitions={@data_view.smart_cell_definitions}
|
smart_cell_definitions={@data_view.smart_cell_definitions}
|
||||||
example_snippet_definitions={@data_view.example_snippet_definitions}
|
example_snippet_definitions={@data_view.example_snippet_definitions}
|
||||||
installing?={@data_view.installing?}
|
installing?={@data_view.installing?}
|
||||||
|
|
|
@ -5,20 +5,21 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(socket) do
|
def mount(socket) do
|
||||||
{:ok, assign(socket, type: nil)}
|
{:ok, assign(socket, error_message: nil)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def update(assigns, socket) do
|
def update(%{event: {:error, message}}, socket) do
|
||||||
assigns =
|
{:ok, assign(socket, error_message: message)}
|
||||||
if socket.assigns.type == nil do
|
|
||||||
type = runtime_type(assigns.runtime)
|
|
||||||
Map.put(assigns, :type, type)
|
|
||||||
else
|
|
||||||
assigns
|
|
||||||
end
|
end
|
||||||
|
|
||||||
{:ok, assign(socket, assigns)}
|
def update(assigns, socket) do
|
||||||
|
socket =
|
||||||
|
socket
|
||||||
|
|> assign(assigns)
|
||||||
|
|> assign_new(:type, fn -> runtime_type(assigns.runtime) end)
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
@ -31,13 +32,13 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
|
||||||
<div class="w-full flex-col space-y-5">
|
<div class="w-full flex-col space-y-5">
|
||||||
<div class="flex space-x-4">
|
<div class="flex space-x-4">
|
||||||
<.choice_button
|
<.choice_button
|
||||||
:if={Livebook.Config.runtime_enabled?(Livebook.Runtime.ElixirStandalone)}
|
:if={Livebook.Config.runtime_enabled?(Livebook.Runtime.Standalone)}
|
||||||
active={@type == "elixir_standalone"}
|
active={@type == "standalone"}
|
||||||
phx-click="set_runtime_type"
|
phx-click="set_runtime_type"
|
||||||
phx-value-type="elixir_standalone"
|
phx-value-type="standalone"
|
||||||
phx-target={@myself}
|
phx-target={@myself}
|
||||||
>
|
>
|
||||||
Elixir standalone
|
Standalone
|
||||||
</.choice_button>
|
</.choice_button>
|
||||||
<.choice_button
|
<.choice_button
|
||||||
:if={Livebook.Config.runtime_enabled?(Livebook.Runtime.Attached)}
|
:if={Livebook.Config.runtime_enabled?(Livebook.Runtime.Attached)}
|
||||||
|
@ -57,25 +58,46 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
|
||||||
>
|
>
|
||||||
Embedded
|
Embedded
|
||||||
</.choice_button>
|
</.choice_button>
|
||||||
|
<.choice_button
|
||||||
|
:if={Livebook.Config.runtime_enabled?(Livebook.Runtime.Fly)}
|
||||||
|
active={@type == "fly"}
|
||||||
|
phx-click="set_runtime_type"
|
||||||
|
phx-value-type="fly"
|
||||||
|
phx-target={@myself}
|
||||||
|
>
|
||||||
|
Fly.io machine
|
||||||
|
</.choice_button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
:if={@error_message && @type == runtime_type(@runtime) && @runtime_status == :disconnected}
|
||||||
|
class="error-box"
|
||||||
|
>
|
||||||
|
<%= @error_message %>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<%= live_render(@socket, live_view_for_type(@type),
|
<.live_component
|
||||||
id: "runtime-config-#{@type}",
|
id={"runtime-config-#{@type}"}
|
||||||
session: %{"session_pid" => @session.pid, "current_runtime" => @runtime}
|
module={component_for_type(@type)}
|
||||||
) %>
|
session={@session}
|
||||||
|
runtime={@runtime}
|
||||||
|
runtime_status={@runtime_status}
|
||||||
|
runtime_connect_info={@runtime_connect_info}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
end
|
end
|
||||||
|
|
||||||
defp runtime_type(%Runtime.ElixirStandalone{}), do: "elixir_standalone"
|
defp runtime_type(%Runtime.Standalone{}), do: "standalone"
|
||||||
defp runtime_type(%Runtime.Attached{}), do: "attached"
|
defp runtime_type(%Runtime.Attached{}), do: "attached"
|
||||||
defp runtime_type(%Runtime.Embedded{}), do: "embedded"
|
defp runtime_type(%Runtime.Embedded{}), do: "embedded"
|
||||||
|
defp runtime_type(%Runtime.Fly{}), do: "fly"
|
||||||
|
|
||||||
defp live_view_for_type("elixir_standalone"), do: LivebookWeb.SessionLive.ElixirStandaloneLive
|
defp component_for_type("standalone"), do: LivebookWeb.SessionLive.StandaloneRuntimeComponent
|
||||||
defp live_view_for_type("attached"), do: LivebookWeb.SessionLive.AttachedLive
|
defp component_for_type("attached"), do: LivebookWeb.SessionLive.AttachedRuntimeComponent
|
||||||
defp live_view_for_type("embedded"), do: LivebookWeb.SessionLive.EmbeddedLive
|
defp component_for_type("embedded"), do: LivebookWeb.SessionLive.EmbeddedRuntimeComponent
|
||||||
|
defp component_for_type("fly"), do: LivebookWeb.SessionLive.FlyRuntimeComponent
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("set_runtime_type", %{"type" => type}, socket) do
|
def handle_event("set_runtime_type", %{"type" => type}, socket) do
|
||||||
|
|
|
@ -147,7 +147,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
||||||
persistent={@section_view.cell_views == []}
|
persistent={@section_view.cell_views == []}
|
||||||
smart_cell_definitions={@smart_cell_definitions}
|
smart_cell_definitions={@smart_cell_definitions}
|
||||||
example_snippet_definitions={@example_snippet_definitions}
|
example_snippet_definitions={@example_snippet_definitions}
|
||||||
runtime={@runtime}
|
runtime_status={@runtime_status}
|
||||||
section_id={@section_view.id}
|
section_id={@section_view.id}
|
||||||
cell_id={nil}
|
cell_id={nil}
|
||||||
session_id={@session_id}
|
session_id={@session_id}
|
||||||
|
@ -160,7 +160,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
||||||
session_id={@session_id}
|
session_id={@session_id}
|
||||||
session_pid={@session_pid}
|
session_pid={@session_pid}
|
||||||
client_id={@client_id}
|
client_id={@client_id}
|
||||||
runtime={@runtime}
|
runtime_status={@runtime_status}
|
||||||
installing?={@installing?}
|
installing?={@installing?}
|
||||||
allowed_uri_schemes={@allowed_uri_schemes}
|
allowed_uri_schemes={@allowed_uri_schemes}
|
||||||
cell_view={cell_view}
|
cell_view={cell_view}
|
||||||
|
@ -171,7 +171,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
|
||||||
persistent={false}
|
persistent={false}
|
||||||
smart_cell_definitions={@smart_cell_definitions}
|
smart_cell_definitions={@smart_cell_definitions}
|
||||||
example_snippet_definitions={@example_snippet_definitions}
|
example_snippet_definitions={@example_snippet_definitions}
|
||||||
runtime={@runtime}
|
runtime_status={@runtime_status}
|
||||||
section_id={@section_view.id}
|
section_id={@section_view.id}
|
||||||
cell_id={cell_view.id}
|
cell_id={cell_view.id}
|
||||||
session_id={@session_id}
|
session_id={@session_id}
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
defmodule LivebookWeb.SessionLive.StandaloneRuntimeComponent do
|
||||||
|
use LivebookWeb, :live_component
|
||||||
|
|
||||||
|
alias Livebook.{Session, Runtime}
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def mount(socket) do
|
||||||
|
unless Livebook.Config.runtime_enabled?(Livebook.Runtime.Standalone) do
|
||||||
|
raise "runtime module not allowed"
|
||||||
|
end
|
||||||
|
|
||||||
|
{:ok, socket}
|
||||||
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def render(assigns) do
|
||||||
|
~H"""
|
||||||
|
<div class="flex-col space-y-5">
|
||||||
|
<p class="text-gray-700">
|
||||||
|
Start a new local Elixir node to evaluate code. Whenever you reconnect this runtime,
|
||||||
|
a fresh node is started.
|
||||||
|
</p>
|
||||||
|
<.button phx-click="init" phx-target={@myself} disabled={@runtime_status == :connecting}>
|
||||||
|
<%= label(@runtime, @runtime_status) %>
|
||||||
|
</.button>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
end
|
||||||
|
|
||||||
|
defp label(%Runtime.Standalone{}, :connecting), do: "Connecting..."
|
||||||
|
defp label(%Runtime.Standalone{}, :connected), do: "Reconnect"
|
||||||
|
defp label(_runtime, _runtime_status), do: "Connect"
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_event("init", _params, socket) do
|
||||||
|
runtime = Runtime.Standalone.new()
|
||||||
|
Session.set_runtime(socket.assigns.session.pid, runtime)
|
||||||
|
Session.connect_runtime(socket.assigns.session.pid)
|
||||||
|
{:noreply, socket}
|
||||||
|
end
|
||||||
|
end
|
30
mix.exs
30
mix.exs
|
@ -75,7 +75,8 @@ defmodule Livebook.MixProject do
|
||||||
defp escript do
|
defp escript do
|
||||||
[
|
[
|
||||||
main_module: LivebookCLI,
|
main_module: LivebookCLI,
|
||||||
app: nil
|
app: nil,
|
||||||
|
emu_args: "-epmd_module Elixir.Livebook.EPMD"
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -121,7 +122,7 @@ defmodule Livebook.MixProject do
|
||||||
{:bypass, "~> 2.1", only: :test},
|
{:bypass, "~> 2.1", only: :test},
|
||||||
# ZTA deps
|
# ZTA deps
|
||||||
{:jose, "~> 1.11.5"},
|
{:jose, "~> 1.11.5"},
|
||||||
{:req, "~> 0.4.4"},
|
{:req, "~> 0.5.2"},
|
||||||
# Docs
|
# Docs
|
||||||
{:ex_doc, "~> 0.30", only: :dev, runtime: false}
|
{:ex_doc, "~> 0.30", only: :dev, runtime: false}
|
||||||
]
|
]
|
||||||
|
@ -163,7 +164,7 @@ defmodule Livebook.MixProject do
|
||||||
include_executables_for: [:unix, :windows],
|
include_executables_for: [:unix, :windows],
|
||||||
include_erts: false,
|
include_erts: false,
|
||||||
rel_templates_path: "rel/server",
|
rel_templates_path: "rel/server",
|
||||||
steps: [:assemble, &remove_cookie/1]
|
steps: [:assemble, &remove_cookie/1, &write_runtime_modules/1]
|
||||||
],
|
],
|
||||||
app: [
|
app: [
|
||||||
applications: @release_apps,
|
applications: @release_apps,
|
||||||
|
@ -179,10 +180,33 @@ defmodule Livebook.MixProject do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp remove_cookie(release) do
|
defp remove_cookie(release) do
|
||||||
|
# We remove the COOKIE file when assembling the release, because we
|
||||||
|
# don't want to share the same cookie across users.
|
||||||
|
|
||||||
File.rm!(Path.join(release.path, "releases/COOKIE"))
|
File.rm!(Path.join(release.path, "releases/COOKIE"))
|
||||||
release
|
release
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp write_runtime_modules(release) do
|
||||||
|
# We copy the subset of Livebook modules that are injected into
|
||||||
|
# the runtime node. See overlays/bin/server for more details
|
||||||
|
|
||||||
|
app = release.applications[:livebook]
|
||||||
|
|
||||||
|
source = Path.join([release.path, "lib", "livebook-#{app[:vsn]}", "ebin"])
|
||||||
|
destination = Path.join([release.path, "lib", "livebook_runtime_ebin"])
|
||||||
|
|
||||||
|
File.mkdir_p!(destination)
|
||||||
|
|
||||||
|
for module <- Livebook.Runtime.ErlDist.required_modules() do
|
||||||
|
from = Path.join(source, "#{module}.beam")
|
||||||
|
to = Path.join(destination, "#{module}.beam")
|
||||||
|
File.cp!(from, to)
|
||||||
|
end
|
||||||
|
|
||||||
|
release
|
||||||
|
end
|
||||||
|
|
||||||
@compile {:no_warn_undefined, Standalone}
|
@compile {:no_warn_undefined, Standalone}
|
||||||
|
|
||||||
defp standalone_erlang_elixir(release) do
|
defp standalone_erlang_elixir(release) do
|
||||||
|
|
4
mix.lock
4
mix.lock
|
@ -24,7 +24,7 @@
|
||||||
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
|
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
|
||||||
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
|
"makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"},
|
||||||
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
|
"makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"},
|
||||||
"mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"},
|
"mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"},
|
||||||
"mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"},
|
"mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"},
|
||||||
"mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"},
|
"mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"},
|
||||||
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
|
"nimble_options": {:hex, :nimble_options, "1.1.0", "3b31a57ede9cb1502071fade751ab0c7b8dbe75a9a4c2b5bbb0943a690b63172", [:mix], [], "hexpm", "8bbbb3941af3ca9acc7835f5655ea062111c9c27bcac53e004460dfd19008a99"},
|
||||||
|
@ -44,7 +44,7 @@
|
||||||
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
|
||||||
"protobuf": {:hex, :protobuf, "0.12.0", "58c0dfea5f929b96b5aa54ec02b7130688f09d2de5ddc521d696eec2a015b223", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "75fa6cbf262062073dd51be44dd0ab940500e18386a6c4e87d5819a58964dc45"},
|
"protobuf": {:hex, :protobuf, "0.12.0", "58c0dfea5f929b96b5aa54ec02b7130688f09d2de5ddc521d696eec2a015b223", [:mix], [{:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "75fa6cbf262062073dd51be44dd0ab940500e18386a6c4e87d5819a58964dc45"},
|
||||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||||
"req": {:hex, :req, "0.4.14", "103de133a076a31044e5458e0f850d5681eef23dfabf3ea34af63212e3b902e2", [:mix], [{:aws_signature, "~> 0.3.2", [hex: :aws_signature, repo: "hexpm", optional: true]}, {:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:nimble_ownership, "~> 0.2.0 or ~> 0.3.0", [hex: :nimble_ownership, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "2ddd3d33f9ab714ced8d3c15fd03db40c14dbf129003c4a3eb80fac2cc0b1b08"},
|
"req": {:hex, :req, "0.5.2", "70b4976e5fbefe84e5a57fd3eea49d4e9aa0ac015301275490eafeaec380f97f", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0c63539ab4c2d6ced6114d2684276cef18ac185ee00674ee9af4b1febba1f986"},
|
||||||
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
|
||||||
"telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
|
"telemetry_metrics": {:hex, :telemetry_metrics, "1.0.0", "29f5f84991ca98b8eb02fc208b2e6de7c95f8bb2294ef244a176675adc7775df", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f23713b3847286a534e005126d4c959ebcca68ae9582118ce436b521d1d47d5d"},
|
||||||
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
|
"telemetry_poller": {:hex, :telemetry_poller, "1.1.0", "58fa7c216257291caaf8d05678c8d01bd45f4bdbc1286838a28c4bb62ef32999", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9eb9d9cbfd81cbd7cdd24682f8711b6e2b691289a0de6826e58452f28c103c8f"},
|
||||||
|
|
|
@ -2,14 +2,6 @@ if exist "!USERPROFILE!\.livebookdesktop.bat" (
|
||||||
call "!USERPROFILE!\.livebookdesktop.bat"
|
call "!USERPROFILE!\.livebookdesktop.bat"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not defined LIVEBOOK_EPMDLESS set LIVEBOOK_EPMDLESS=true
|
|
||||||
if "!LIVEBOOK_EPMDLESS!"=="1" goto epmdless
|
|
||||||
if "!LIVEBOOK_EPMDLESS!"=="true" goto epmdless
|
|
||||||
goto continue
|
|
||||||
:epmdless
|
|
||||||
set ELIXIR_ERL_OPTIONS=!ELIXIR_ERL_OPTIONS! -epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0
|
|
||||||
:continue
|
|
||||||
|
|
||||||
set RELEASE_MODE=interactive
|
set RELEASE_MODE=interactive
|
||||||
set RELEASE_DISTRIBUTION=none
|
set RELEASE_DISTRIBUTION=none
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,6 @@ if [ -f "$HOME/.livebookdesktop.sh" ]; then
|
||||||
. "$HOME/.livebookdesktop.sh"
|
. "$HOME/.livebookdesktop.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export LIVEBOOK_EPMDLESS=${LIVEBOOK_EPMDLESS:-true}
|
|
||||||
if [ "$LIVEBOOK_EPMDLESS" = "true" ] || [ "$LIVEBOOK_EPMDLESS" = "1" ]; then
|
|
||||||
export ELIXIR_ERL_OPTIONS="${ELIXIR_ERL_OPTIONS} -epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0"
|
|
||||||
fi
|
|
||||||
|
|
||||||
export RELEASE_MODE="interactive"
|
export RELEASE_MODE="interactive"
|
||||||
export RELEASE_DISTRIBUTION="none"
|
export RELEASE_DISTRIBUTION="none"
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
# Disable busy waiting so that we don't waste resources
|
# Disable busy waiting so that we don't waste resources
|
||||||
# Limit the maximal number of ports for the same reason
|
# Limit the maximal number of ports for the same reason
|
||||||
+sbwt none +sbwtdcpu none +sbwtdio none +Q 65536
|
# Set the custom EPMD module
|
||||||
|
+sbwt none +sbwtdcpu none +sbwtdio none +Q 65536 -epmd_module Elixir.Livebook.EPMD
|
||||||
|
|
|
@ -2,13 +2,6 @@ if exist "!RELEASE_ROOT!\user\env.bat" (
|
||||||
call "!RELEASE_ROOT!\user\env.bat"
|
call "!RELEASE_ROOT!\user\env.bat"
|
||||||
)
|
)
|
||||||
|
|
||||||
if "!LIVEBOOK_EPMDLESS!"=="1" goto epmdless
|
|
||||||
if "!LIVEBOOK_EPMDLESS!"=="true" goto epmdless
|
|
||||||
goto continue
|
|
||||||
:epmdless
|
|
||||||
set ELIXIR_ERL_OPTIONS=!ELIXIR_ERL_OPTIONS! -epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0
|
|
||||||
:continue
|
|
||||||
|
|
||||||
set RELEASE_MODE=interactive
|
set RELEASE_MODE=interactive
|
||||||
set RELEASE_DISTRIBUTION=none
|
set RELEASE_DISTRIBUTION=none
|
||||||
|
|
||||||
|
@ -19,3 +12,5 @@ if not defined RELEASE_COOKIE (
|
||||||
for /f "skip=1" %%X in ('wmic os get localdatetime') do if not defined TIMESTAMP set TIMESTAMP=%%X
|
for /f "skip=1" %%X in ('wmic os get localdatetime') do if not defined TIMESTAMP set TIMESTAMP=%%X
|
||||||
set RELEASE_COOKIE=cookie-!TIMESTAMP:~0,11!-!RANDOM!
|
set RELEASE_COOKIE=cookie-!TIMESTAMP:~0,11!-!RANDOM!
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cd !HOMEDRIVE!!HOMEPATH!
|
||||||
|
|
|
@ -18,10 +18,6 @@ if [ -f "${RELEASE_ROOT}/user/env.sh" ]; then
|
||||||
. "${RELEASE_ROOT}/user/env.sh"
|
. "${RELEASE_ROOT}/user/env.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$LIVEBOOK_EPMDLESS" = "true" ] || [ "$LIVEBOOK_EPMDLESS" = "1" ]; then
|
|
||||||
export ELIXIR_ERL_OPTIONS="${ELIXIR_ERL_OPTIONS} -epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0"
|
|
||||||
fi
|
|
||||||
|
|
||||||
export RELEASE_MODE="interactive"
|
export RELEASE_MODE="interactive"
|
||||||
export RELEASE_DISTRIBUTION="none"
|
export RELEASE_DISTRIBUTION="none"
|
||||||
|
|
||||||
|
@ -39,3 +35,5 @@ if [ ! -z "${LIVEBOOK_COOKIE}" ]; then export RELEASE_COOKIE=${LIVEBOOK_COOKIE};
|
||||||
# a fixed value. Note that this value is overriden on boot, so other
|
# a fixed value. Note that this value is overriden on boot, so other
|
||||||
# than being the initial node cookie, we don't really use it.
|
# than being the initial node cookie, we don't really use it.
|
||||||
export RELEASE_COOKIE="${RELEASE_COOKIE:-$(cat /dev/urandom | env LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)}"
|
export RELEASE_COOKIE="${RELEASE_COOKIE:-$(cat /dev/urandom | env LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)}"
|
||||||
|
|
||||||
|
cd $HOME
|
||||||
|
|
|
@ -3,9 +3,23 @@ set -e
|
||||||
|
|
||||||
cd -P -- "$(dirname -- "$0")"
|
cd -P -- "$(dirname -- "$0")"
|
||||||
|
|
||||||
if [ -n "${FLAME_PARENT}" ]; then
|
# Livebook does not start EPMD automatically, but we want to start it
|
||||||
|
# here, becasue we need it for clustering
|
||||||
epmd -daemon
|
epmd -daemon
|
||||||
elixir ./start_flame.exs
|
|
||||||
|
if [ -n "${FLAME_PARENT}" ]; then
|
||||||
|
exec elixir ./start_flame.exs
|
||||||
|
elif [ -n "${LIVEBOOK_RUNTIME}" ]; then
|
||||||
|
# Note: keep the flags in sync with the standalone runtime
|
||||||
|
erl_flags="+sbwt none +sbwtdcpu none +sbwtdio none +sssdio 128 -elixir ansi_enabled true -noinput"
|
||||||
|
|
||||||
|
# We add Livebook modules to the path, so that they are loaded from
|
||||||
|
# from disk, rather than having module binaries sent from the parent
|
||||||
|
# node. This cuts down the initialization time.
|
||||||
|
livebook_beams="$(dirname -- "$(pwd)")/lib/livebook_runtime_ebin"
|
||||||
|
erl_flags="$erl_flags -pa $livebook_beams"
|
||||||
|
|
||||||
|
exec elixir --erl "$erl_flags" ./start_runtime.exs
|
||||||
else
|
else
|
||||||
exec ./livebook start
|
exec ./livebook start
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
File.cd!(System.fetch_env!("HOME"))
|
||||||
|
|
||||||
flame_parent = System.fetch_env!("FLAME_PARENT") |> Base.decode64!() |> :erlang.binary_to_term()
|
flame_parent = System.fetch_env!("FLAME_PARENT") |> Base.decode64!() |> :erlang.binary_to_term()
|
||||||
|
|
||||||
%{
|
%{
|
||||||
|
|
42
rel/server/overlays/bin/start_runtime.exs
Normal file
42
rel/server/overlays/bin/start_runtime.exs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
File.cd!(System.fetch_env!("HOME"))
|
||||||
|
|
||||||
|
%{
|
||||||
|
node_base: node_base,
|
||||||
|
cookie: cookie,
|
||||||
|
dist_port: dist_port
|
||||||
|
} = System.fetch_env!("LIVEBOOK_RUNTIME") |> Base.decode64!() |> :erlang.binary_to_term()
|
||||||
|
|
||||||
|
# This is the only Fly-specific part of starting Livebook as runtime
|
||||||
|
app = System.fetch_env!("FLY_APP_NAME")
|
||||||
|
machine_id = System.fetch_env!("FLY_MACHINE_ID")
|
||||||
|
node = :"#{node_base}@#{machine_id}.vm.#{app}.internal"
|
||||||
|
|
||||||
|
# We persist the information before the node is reachable
|
||||||
|
:persistent_term.put(:livebook_runtime_info, %{
|
||||||
|
pid: self(),
|
||||||
|
elixir_version: System.version()
|
||||||
|
})
|
||||||
|
|
||||||
|
Application.put_env(:kernel, :inet_dist_listen_min, dist_port)
|
||||||
|
Application.put_env(:kernel, :inet_dist_listen_max, dist_port)
|
||||||
|
|
||||||
|
{:ok, _} = :net_kernel.start(node, %{name_domain: :longnames, hidden: true})
|
||||||
|
Node.set_cookie(cookie)
|
||||||
|
|
||||||
|
IO.puts("Runtime node started, waiting for the parent finish initialization")
|
||||||
|
|
||||||
|
receive do
|
||||||
|
:node_initialized ->
|
||||||
|
manager_ref = Process.monitor(Livebook.Runtime.ErlDist.NodeManager)
|
||||||
|
|
||||||
|
receive do
|
||||||
|
{:DOWN, ^manager_ref, :process, _object, _reason} -> :ok
|
||||||
|
end
|
||||||
|
|
||||||
|
IO.puts("The owner disconnected from the runtime, shutting down")
|
||||||
|
after
|
||||||
|
20_000 ->
|
||||||
|
IO.puts(:stderr, "No node initialization within 20s, shutting down")
|
||||||
|
end
|
||||||
|
|
||||||
|
System.halt()
|
|
@ -1,3 +1,4 @@
|
||||||
# Disable busy waiting so that we don't waste resources
|
# Disable busy waiting so that we don't waste resources
|
||||||
# Limit the maximal number of ports for the same reason
|
# Limit the maximal number of ports for the same reason
|
||||||
+sbwt none +sbwtdcpu none +sbwtdio none +Q 65536
|
# Set the custom EPMD module
|
||||||
|
+sbwt none +sbwtdcpu none +sbwtdio none +Q 65536 -epmd_module Elixir.Livebook.EPMD
|
||||||
|
|
|
@ -1,18 +1,7 @@
|
||||||
defmodule Livebook.EPMDTest do
|
defmodule Livebook.EPMDTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
describe "with epmd" do
|
|
||||||
@describetag :with_epmd
|
|
||||||
test "has a random dist port" do
|
|
||||||
assert Livebook.EPMD.dist_port() == 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "without epmd" do
|
|
||||||
@describetag :without_epmd
|
|
||||||
|
|
||||||
test "has a custom dist port" do
|
test "has a custom dist port" do
|
||||||
assert Livebook.EPMD.dist_port() != 0
|
assert Livebook.EPMD.dist_port() != 0
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
|
@ -7,9 +7,9 @@ defmodule Livebook.Hubs.DockerfileTest do
|
||||||
alias Livebook.Hubs
|
alias Livebook.Hubs
|
||||||
alias Livebook.Secrets.Secret
|
alias Livebook.Secrets.Secret
|
||||||
|
|
||||||
@docker_tag if Livebook.Config.app_version() =~ "-dev",
|
@versions if Livebook.Config.app_version() =~ "-dev",
|
||||||
do: "latest",
|
do: %{base: "edge", cuda: "latest"},
|
||||||
else: Livebook.Config.app_version()
|
else: %{base: Livebook.Config.app_version(), cuda: Livebook.Config.app_version()}
|
||||||
|
|
||||||
describe "airgapped_dockerfile/7" do
|
describe "airgapped_dockerfile/7" do
|
||||||
test "deploying a single notebook in personal hub" do
|
test "deploying a single notebook in personal hub" do
|
||||||
|
@ -20,7 +20,7 @@ defmodule Livebook.Hubs.DockerfileTest do
|
||||||
dockerfile = Dockerfile.airgapped_dockerfile(config, hub, [], [], file, [], %{})
|
dockerfile = Dockerfile.airgapped_dockerfile(config, hub, [], [], file, [], %{})
|
||||||
|
|
||||||
assert dockerfile == """
|
assert dockerfile == """
|
||||||
FROM ghcr.io/livebook-dev/livebook:#{@docker_tag}
|
FROM ghcr.io/livebook-dev/livebook:#{@versions.base}
|
||||||
|
|
||||||
# Apps configuration
|
# Apps configuration
|
||||||
ENV LIVEBOOK_APPS_PATH "/apps"
|
ENV LIVEBOOK_APPS_PATH "/apps"
|
||||||
|
@ -97,7 +97,7 @@ defmodule Livebook.Hubs.DockerfileTest do
|
||||||
dockerfile = Dockerfile.airgapped_dockerfile(config, hub, [], [], file, [], %{})
|
dockerfile = Dockerfile.airgapped_dockerfile(config, hub, [], [], file, [], %{})
|
||||||
|
|
||||||
assert dockerfile == """
|
assert dockerfile == """
|
||||||
FROM ghcr.io/livebook-dev/livebook:#{@docker_tag}
|
FROM ghcr.io/livebook-dev/livebook:#{@versions.base}
|
||||||
|
|
||||||
ARG TEAMS_KEY="lb_tk_fn0pL3YLWzPoPFWuHeV3kd0o7_SFuIOoU4C_k6OWDYg"
|
ARG TEAMS_KEY="lb_tk_fn0pL3YLWzPoPFWuHeV3kd0o7_SFuIOoU4C_k6OWDYg"
|
||||||
|
|
||||||
|
@ -166,14 +166,14 @@ defmodule Livebook.Hubs.DockerfileTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deploying with different base image" do
|
test "deploying with different base image" do
|
||||||
config = dockerfile_config(%{docker_tag: "#{@docker_tag}-cuda11.8"})
|
config = dockerfile_config(%{docker_tag: "#{@versions.cuda}-cuda11.8"})
|
||||||
hub = personal_hub()
|
hub = personal_hub()
|
||||||
file = Livebook.FileSystem.File.local(p("/notebook.livemd"))
|
file = Livebook.FileSystem.File.local(p("/notebook.livemd"))
|
||||||
|
|
||||||
dockerfile = Dockerfile.airgapped_dockerfile(config, hub, [], [], file, [], %{})
|
dockerfile = Dockerfile.airgapped_dockerfile(config, hub, [], [], file, [], %{})
|
||||||
|
|
||||||
assert dockerfile =~ """
|
assert dockerfile =~ """
|
||||||
FROM ghcr.io/livebook-dev/livebook:#{@docker_tag}-cuda11.8
|
FROM ghcr.io/livebook-dev/livebook:#{@versions.cuda}-cuda11.8
|
||||||
|
|
||||||
ENV XLA_TARGET "cuda118"
|
ENV XLA_TARGET "cuda118"
|
||||||
"""
|
"""
|
||||||
|
@ -247,13 +247,13 @@ defmodule Livebook.Hubs.DockerfileTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "deploying with different base image" do
|
test "deploying with different base image" do
|
||||||
config = dockerfile_config(%{docker_tag: "#{@docker_tag}-cuda11.8"})
|
config = dockerfile_config(%{docker_tag: "#{@versions.cuda}-cuda11.8"})
|
||||||
hub = team_hub()
|
hub = team_hub()
|
||||||
agent_key = Livebook.Factory.build(:agent_key)
|
agent_key = Livebook.Factory.build(:agent_key)
|
||||||
|
|
||||||
%{image: image, env: env} = Dockerfile.online_docker_info(config, hub, agent_key)
|
%{image: image, env: env} = Dockerfile.online_docker_info(config, hub, agent_key)
|
||||||
|
|
||||||
assert image == "ghcr.io/livebook-dev/livebook:#{@docker_tag}-cuda11.8"
|
assert image == "ghcr.io/livebook-dev/livebook:#{@versions.cuda}-cuda11.8"
|
||||||
assert {"XLA_TARGET", "cuda118"} in env
|
assert {"XLA_TARGET", "cuda118"} in env
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1863,7 +1863,8 @@ defmodule Livebook.IntellisenseTest do
|
||||||
# in the past we used :peer.start, but it was often failing on CI
|
# in the past we used :peer.start, but it was often failing on CI
|
||||||
# (the start was timing out)
|
# (the start was timing out)
|
||||||
|
|
||||||
{:ok, runtime} = Livebook.Runtime.ElixirStandalone.new() |> Livebook.Runtime.connect()
|
pid = Livebook.Runtime.Standalone.new() |> Livebook.Runtime.connect()
|
||||||
|
assert_receive {:runtime_connect_done, ^pid, {:ok, runtime}}
|
||||||
|
|
||||||
parent = self()
|
parent = self()
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,10 @@ defmodule Livebook.Runtime.AttachedTest do
|
||||||
describe "Runtime.connect/1" do
|
describe "Runtime.connect/1" do
|
||||||
test "given an invalid node returns an error" do
|
test "given an invalid node returns an error" do
|
||||||
runtime = Runtime.Attached.new(:nonexistent@node)
|
runtime = Runtime.Attached.new(:nonexistent@node)
|
||||||
assert {:error, "node :nonexistent@node is unreachable"} = Runtime.connect(runtime)
|
pid = Runtime.connect(runtime)
|
||||||
|
|
||||||
|
assert_receive {:runtime_connect_done, ^pid,
|
||||||
|
{:error, "node :nonexistent@node is unreachable"}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -7,15 +7,16 @@ defmodule Livebook.Runtime.ErlDist.NodeManagerTest do
|
||||||
test "terminates when the last runtime server terminates" do
|
test "terminates when the last runtime server terminates" do
|
||||||
# We use a standalone runtime, so that we have an isolated node
|
# We use a standalone runtime, so that we have an isolated node
|
||||||
# with its own node manager
|
# with its own node manager
|
||||||
assert {:ok, %{node: node, server_pid: server1} = runtime} =
|
pid = Runtime.Standalone.new() |> Runtime.connect()
|
||||||
Runtime.ElixirStandalone.new() |> Runtime.connect()
|
assert_receive {:runtime_connect_done, ^pid, {:ok, runtime}}
|
||||||
|
%{node: node, server_pid: server1} = runtime
|
||||||
|
|
||||||
Runtime.take_ownership(runtime)
|
Runtime.take_ownership(runtime)
|
||||||
|
|
||||||
manager_pid = :erpc.call(node, Process, :whereis, [Livebook.Runtime.ErlDist.NodeManager])
|
manager_pid = :erpc.call(node, Process, :whereis, [Livebook.Runtime.ErlDist.NodeManager])
|
||||||
ref = Process.monitor(manager_pid)
|
ref = Process.monitor(manager_pid)
|
||||||
|
|
||||||
server2 = NodeManager.start_runtime_server(node)
|
{:ok, server2} = NodeManager.start_runtime_server(node)
|
||||||
|
|
||||||
RuntimeServer.stop(server1)
|
RuntimeServer.stop(server1)
|
||||||
RuntimeServer.stop(server2)
|
RuntimeServer.stop(server2)
|
||||||
|
|
|
@ -4,7 +4,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
||||||
alias Livebook.Runtime.ErlDist.{NodeManager, RuntimeServer}
|
alias Livebook.Runtime.ErlDist.{NodeManager, RuntimeServer}
|
||||||
|
|
||||||
setup ctx do
|
setup ctx do
|
||||||
runtime_server_pid = NodeManager.start_runtime_server(node(), ctx[:opts] || [])
|
{:ok, runtime_server_pid} = NodeManager.start_runtime_server(node(), ctx[:opts] || [])
|
||||||
RuntimeServer.attach(runtime_server_pid, self())
|
RuntimeServer.attach(runtime_server_pid, self())
|
||||||
{:ok, %{pid: runtime_server_pid}}
|
{:ok, %{pid: runtime_server_pid}}
|
||||||
end
|
end
|
||||||
|
@ -24,7 +24,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServerTest do
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
|
||||||
pid = NodeManager.start_runtime_server(node())
|
{:ok, pid} = NodeManager.start_runtime_server(node())
|
||||||
RuntimeServer.attach(pid, owner)
|
RuntimeServer.attach(pid, owner)
|
||||||
|
|
||||||
# Make sure the node is running.
|
# Make sure the node is running.
|
||||||
|
|
93
test/livebook/runtime/fly_test.exs
Normal file
93
test/livebook/runtime/fly_test.exs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
defmodule Livebook.Runtime.FlyTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
# To run these tests create a Fly app, generate deployment token,
|
||||||
|
# then set TEST_FLY_APP_NAME and TEST_FLY_API_TOKEN
|
||||||
|
@moduletag :fly
|
||||||
|
|
||||||
|
alias Livebook.Runtime
|
||||||
|
|
||||||
|
@assert_receive_timeout 10_000
|
||||||
|
|
||||||
|
setup do
|
||||||
|
Livebook.FlyAPI.passthrough()
|
||||||
|
:ok
|
||||||
|
end
|
||||||
|
|
||||||
|
test "connecting flow" do
|
||||||
|
fly = fly!()
|
||||||
|
config = config(%{token: fly.token, app_name: fly.app_name})
|
||||||
|
|
||||||
|
assert [] = fly_run(fly, ~w(machine list))
|
||||||
|
|
||||||
|
pid = Runtime.Fly.new(config) |> Runtime.connect()
|
||||||
|
|
||||||
|
Req.Test.allow(Livebook.FlyAPI, self(), pid)
|
||||||
|
|
||||||
|
assert_receive {:runtime_connect_info, ^pid, "create machine"}, @assert_receive_timeout
|
||||||
|
assert_receive {:runtime_connect_info, ^pid, "start proxy"}, @assert_receive_timeout
|
||||||
|
assert_receive {:runtime_connect_info, ^pid, "connect to node"}, @assert_receive_timeout
|
||||||
|
assert_receive {:runtime_connect_info, ^pid, "initialize node"}, @assert_receive_timeout
|
||||||
|
assert_receive {:runtime_connect_done, ^pid, {:ok, runtime}}, @assert_receive_timeout
|
||||||
|
|
||||||
|
Runtime.take_ownership(runtime)
|
||||||
|
|
||||||
|
assert [_] = fly_run(fly, ~w(machine list))
|
||||||
|
|
||||||
|
# Verify that we can actually evaluate code on the Fly machine
|
||||||
|
Runtime.evaluate_code(runtime, :elixir, ~s/System.fetch_env!("FLY_APP_NAME")/, {:c1, :e1}, [])
|
||||||
|
assert_receive {:runtime_evaluation_response, :e1, %{type: :terminal_text, text: text}, _meta}
|
||||||
|
assert text =~ fly.app_name
|
||||||
|
|
||||||
|
Runtime.disconnect(runtime)
|
||||||
|
|
||||||
|
# The machine should be automatically destroyed. Blocking in tests
|
||||||
|
# is bad, but this test suit is inherently time-consuming and it
|
||||||
|
# is opt-in anyway, so it is fine in this case.
|
||||||
|
Process.sleep(2000)
|
||||||
|
|
||||||
|
assert [] = fly_run(fly, ~w(machine list))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "connecting fails with invalid token" do
|
||||||
|
fly = fly!()
|
||||||
|
config = config(%{token: "invalid", app_name: fly.app_name})
|
||||||
|
|
||||||
|
pid = Runtime.Fly.new(config) |> Runtime.connect()
|
||||||
|
|
||||||
|
Req.Test.allow(Livebook.FlyAPI, self(), pid)
|
||||||
|
|
||||||
|
assert_receive {:runtime_connect_done, ^pid, {:error, error}}, @assert_receive_timeout
|
||||||
|
assert error == "could not create machine, reason: authenticate: token validation error"
|
||||||
|
end
|
||||||
|
|
||||||
|
defp config(attrs) do
|
||||||
|
defaults = %{
|
||||||
|
token: nil,
|
||||||
|
app_name: nil,
|
||||||
|
region: "fra",
|
||||||
|
cpu_kind: "shared",
|
||||||
|
cpus: 1,
|
||||||
|
memory_gb: 1,
|
||||||
|
gpu_kind: nil,
|
||||||
|
gpus: nil,
|
||||||
|
volume_id: nil,
|
||||||
|
docker_tag: "edge"
|
||||||
|
}
|
||||||
|
|
||||||
|
Map.merge(defaults, attrs)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fly_run(fly, args) do
|
||||||
|
{output, 0} =
|
||||||
|
System.cmd("fly", args ++ ["--app", fly.app_name, "--access-token", fly.token, "--json"])
|
||||||
|
|
||||||
|
Jason.decode!(output)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp fly!() do
|
||||||
|
token = System.fetch_env!("TEST_FLY_API_TOKEN")
|
||||||
|
app_name = System.fetch_env!("TEST_FLY_APP_NAME")
|
||||||
|
%{token: token, app_name: app_name}
|
||||||
|
end
|
||||||
|
end
|
|
@ -1,11 +1,13 @@
|
||||||
defmodule Livebook.Runtime.ElixirStandaloneTest do
|
defmodule Livebook.Runtime.StandaloneTest do
|
||||||
use ExUnit.Case, async: true
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
alias Livebook.Runtime
|
alias Livebook.Runtime
|
||||||
|
|
||||||
describe "Runtime.connect/1" do
|
describe "Runtime.connect/1" do
|
||||||
test "starts a new Elixir runtime in distribution mode and ties its lifetime to the NodeManager process" do
|
test "starts a new Elixir runtime in distribution mode and ties its lifetime to the NodeManager process" do
|
||||||
assert {:ok, %{node: node} = runtime} = Runtime.ElixirStandalone.new() |> Runtime.connect()
|
pid = Runtime.Standalone.new() |> Runtime.connect()
|
||||||
|
assert_receive {:runtime_connect_done, ^pid, {:ok, runtime}}
|
||||||
|
%{node: node} = runtime
|
||||||
Runtime.take_ownership(runtime)
|
Runtime.take_ownership(runtime)
|
||||||
|
|
||||||
# Make sure the node is running.
|
# Make sure the node is running.
|
||||||
|
@ -21,7 +23,9 @@ defmodule Livebook.Runtime.ElixirStandaloneTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "loads necessary modules and starts manager process" do
|
test "loads necessary modules and starts manager process" do
|
||||||
assert {:ok, %{node: node} = runtime} = Runtime.ElixirStandalone.new() |> Runtime.connect()
|
pid = Runtime.Standalone.new() |> Runtime.connect()
|
||||||
|
assert_receive {:runtime_connect_done, ^pid, {:ok, runtime}}
|
||||||
|
%{node: node} = runtime
|
||||||
Runtime.take_ownership(runtime)
|
Runtime.take_ownership(runtime)
|
||||||
|
|
||||||
assert evaluator_module_loaded?(node)
|
assert evaluator_module_loaded?(node)
|
||||||
|
@ -30,7 +34,9 @@ defmodule Livebook.Runtime.ElixirStandaloneTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "Runtime.disconnect/1 makes the node terminate" do
|
test "Runtime.disconnect/1 makes the node terminate" do
|
||||||
assert {:ok, %{node: node} = runtime} = Runtime.ElixirStandalone.new() |> Runtime.connect()
|
pid = Runtime.Standalone.new() |> Runtime.connect()
|
||||||
|
assert_receive {:runtime_connect_done, ^pid, {:ok, runtime}}
|
||||||
|
%{node: node} = runtime
|
||||||
Runtime.take_ownership(runtime)
|
Runtime.take_ownership(runtime)
|
||||||
|
|
||||||
# Make sure the node is running.
|
# Make sure the node is running.
|
File diff suppressed because it is too large
Load diff
|
@ -3,6 +3,7 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
import Livebook.HubHelpers
|
import Livebook.HubHelpers
|
||||||
import Livebook.AppHelpers
|
import Livebook.AppHelpers
|
||||||
|
import Livebook.SessionHelpers
|
||||||
import Livebook.TestHelpers
|
import Livebook.TestHelpers
|
||||||
|
|
||||||
alias Livebook.{Session, Text, Runtime, Utils, Notebook, FileSystem, Apps, App}
|
alias Livebook.{Session, Text, Runtime, Utils, Notebook, FileSystem, Apps, App}
|
||||||
|
@ -217,9 +218,6 @@ defmodule Livebook.SessionTest do
|
||||||
test "applies source change to the setup cell to include the given dependencies" do
|
test "applies source change to the setup cell to include the given dependencies" do
|
||||||
session = start_session()
|
session = start_session()
|
||||||
|
|
||||||
runtime = connected_noop_runtime()
|
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
|
|
||||||
Session.subscribe(session.id)
|
Session.subscribe(session.id)
|
||||||
|
|
||||||
Session.add_dependencies(session.pid, [%{dep: {:jason, "~> 1.3.0"}, config: []}])
|
Session.add_dependencies(session.pid, [%{dep: {:jason, "~> 1.3.0"}, config: []}])
|
||||||
|
@ -248,9 +246,6 @@ defmodule Livebook.SessionTest do
|
||||||
notebook = Notebook.new() |> Notebook.update_cell("setup", &%{&1 | source: "[,]"})
|
notebook = Notebook.new() |> Notebook.update_cell("setup", &%{&1 | source: "[,]"})
|
||||||
session = start_session(notebook: notebook)
|
session = start_session(notebook: notebook)
|
||||||
|
|
||||||
runtime = connected_noop_runtime()
|
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
|
|
||||||
Session.subscribe(session.id)
|
Session.subscribe(session.id)
|
||||||
|
|
||||||
Session.add_dependencies(session.pid, [%{dep: {:jason, "~> 1.3.0"}, config: []}])
|
Session.add_dependencies(session.pid, [%{dep: {:jason, "~> 1.3.0"}, config: []}])
|
||||||
|
@ -269,7 +264,7 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||||
|
|
||||||
assert_receive {:operation, {:queue_cells_evaluation, _client_id, [^cell_id]}}
|
assert_receive {:operation, {:queue_cells_evaluation, _client_id, [^cell_id], []}}
|
||||||
|
|
||||||
assert_receive {:operation,
|
assert_receive {:operation,
|
||||||
{:add_cell_evaluation_response, _, ^cell_id, _,
|
{:add_cell_evaluation_response, _, ^cell_id, _,
|
||||||
|
@ -392,10 +387,14 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
Session.subscribe(session.id)
|
Session.subscribe(session.id)
|
||||||
|
|
||||||
runtime = connected_noop_runtime()
|
runtime = Livebook.Runtime.NoopRuntime.new()
|
||||||
Session.set_runtime(session.pid, runtime)
|
Session.set_runtime(session.pid, runtime)
|
||||||
|
|
||||||
|
Session.connect_runtime(session.pid)
|
||||||
|
|
||||||
assert_receive {:operation, {:set_runtime, _client_id, ^runtime}}
|
assert_receive {:operation, {:set_runtime, _client_id, ^runtime}}
|
||||||
|
assert_receive {:operation, {:connect_runtime, _client_id}}
|
||||||
|
assert_receive {:operation, {:runtime_connected, _client_id, _runtime}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -405,16 +404,13 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
Session.subscribe(session.id)
|
Session.subscribe(session.id)
|
||||||
|
|
||||||
runtime = connected_noop_runtime()
|
set_noop_runtime(session.pid)
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
assert_receive {:operation, {:set_runtime, _client_id, _}}
|
|
||||||
|
|
||||||
# Calling twice can happen in a race, make sure it doesn't crash
|
# Calling twice can happen in a race, make sure it doesn't crash
|
||||||
Session.disconnect_runtime(session.pid)
|
Session.disconnect_runtime(session.pid)
|
||||||
Session.disconnect_runtime([session.pid])
|
Session.disconnect_runtime([session.pid])
|
||||||
|
|
||||||
assert_receive {:operation, {:set_runtime, _client_id, runtime}}
|
assert_receive {:operation, {:disconnect_runtime, _client_id}}
|
||||||
refute Runtime.connected?(runtime)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -570,8 +566,9 @@ defmodule Livebook.SessionTest do
|
||||||
File.write!(source_path, "content")
|
File.write!(source_path, "content")
|
||||||
{:ok, old_file_ref} = Session.register_file(session.pid, source_path, "key")
|
{:ok, old_file_ref} = Session.register_file(session.pid, source_path, "key")
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
set_noop_runtime(session.pid, self())
|
||||||
|
connect_and_await_runtime(session.pid)
|
||||||
send(session.pid, {:runtime_file_path_request, self(), old_file_ref})
|
send(session.pid, {:runtime_file_path_request, self(), old_file_ref})
|
||||||
assert_receive {:runtime_file_path_reply, {:ok, old_path}}
|
assert_receive {:runtime_file_path_reply, {:ok, old_path}}
|
||||||
|
|
||||||
|
@ -604,8 +601,9 @@ defmodule Livebook.SessionTest do
|
||||||
{:ok, file_ref} =
|
{:ok, file_ref} =
|
||||||
Session.register_file(session.pid, source_path, "key", linked_client_id: client_id)
|
Session.register_file(session.pid, source_path, "key", linked_client_id: client_id)
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
set_noop_runtime(session.pid, self())
|
||||||
|
connect_and_await_runtime(session.pid)
|
||||||
send(session.pid, {:runtime_file_path_request, self(), file_ref})
|
send(session.pid, {:runtime_file_path_request, self(), file_ref})
|
||||||
assert_receive {:runtime_file_path_reply, {:ok, path}}
|
assert_receive {:runtime_file_path_reply, {:ok, path}}
|
||||||
|
|
||||||
|
@ -643,8 +641,9 @@ defmodule Livebook.SessionTest do
|
||||||
client_name: "data.txt"
|
client_name: "data.txt"
|
||||||
})
|
})
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
set_noop_runtime(session.pid, self())
|
||||||
|
connect_and_await_runtime(session.pid)
|
||||||
send(session.pid, {:runtime_file_path_request, self(), file_ref})
|
send(session.pid, {:runtime_file_path_request, self(), file_ref})
|
||||||
assert_receive {:runtime_file_path_reply, {:ok, path}}
|
assert_receive {:runtime_file_path_reply, {:ok, path}}
|
||||||
|
|
||||||
|
@ -800,7 +799,7 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
# For most tests we use the lightweight embedded runtime,
|
# For most tests we use the lightweight embedded runtime,
|
||||||
# so that they are cheap to run. Here go several integration
|
# so that they are cheap to run. Here go several integration
|
||||||
# tests that actually start a Elixir standalone runtime (default in production)
|
# tests that actually start a Standalone runtime (default in production)
|
||||||
# to verify session integrates well with it properly.
|
# to verify session integrates well with it properly.
|
||||||
|
|
||||||
test "starts a standalone runtime upon first evaluation if there was none set explicitly" do
|
test "starts a standalone runtime upon first evaluation if there was none set explicitly" do
|
||||||
|
@ -819,20 +818,19 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
test "if the runtime node goes down, notifies the subscribers" do
|
test "if the runtime node goes down, notifies the subscribers" do
|
||||||
session = start_session()
|
session = start_session()
|
||||||
{:ok, runtime} = Runtime.ElixirStandalone.new() |> Runtime.connect()
|
|
||||||
|
|
||||||
Session.subscribe(session.id)
|
Session.subscribe(session.id)
|
||||||
|
|
||||||
# Wait for the runtime to be set
|
# Wait for the runtime to be set
|
||||||
Session.set_runtime(session.pid, runtime)
|
Session.set_runtime(session.pid, Runtime.Standalone.new())
|
||||||
assert_receive {:operation, {:set_runtime, _, ^runtime}}
|
Session.connect_runtime(session.pid)
|
||||||
|
assert_receive {:operation, {:runtime_connected, _, runtime}}
|
||||||
|
|
||||||
# Terminate the other node, the session should detect that
|
# Terminate the other node, the session should detect that
|
||||||
Node.spawn(runtime.node, System, :halt, [])
|
Node.spawn(runtime.node, System, :halt, [])
|
||||||
|
|
||||||
assert_receive {:operation, {:set_runtime, _, runtime}}
|
assert_receive {:operation, {:runtime_down, _}}
|
||||||
refute Runtime.connected?(runtime)
|
assert_receive {:error, "runtime terminated unexpectedly - no connection"}
|
||||||
assert_receive {:error, "runtime node terminated unexpectedly - no connection"}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "on user change sends an update operation subscribers" do
|
test "on user change sends an update operation subscribers" do
|
||||||
|
@ -934,8 +932,7 @@ defmodule Livebook.SessionTest do
|
||||||
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
|
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
|
||||||
session = start_session(notebook: notebook)
|
session = start_session(notebook: notebook)
|
||||||
|
|
||||||
runtime = connected_noop_runtime()
|
set_noop_runtime(session.pid)
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
|
|
||||||
send(
|
send(
|
||||||
session.pid,
|
session.pid,
|
||||||
|
@ -962,8 +959,9 @@ defmodule Livebook.SessionTest do
|
||||||
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
|
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
|
||||||
session = start_session(notebook: notebook)
|
session = start_session(notebook: notebook)
|
||||||
|
|
||||||
runtime = connected_noop_runtime()
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
set_noop_runtime(session.pid)
|
||||||
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
send(
|
send(
|
||||||
session.pid,
|
session.pid,
|
||||||
|
@ -1000,8 +998,9 @@ defmodule Livebook.SessionTest do
|
||||||
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
|
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
|
||||||
session = start_session(notebook: notebook)
|
session = start_session(notebook: notebook)
|
||||||
|
|
||||||
runtime = connected_noop_runtime()
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
set_noop_runtime(session.pid)
|
||||||
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
send(
|
send(
|
||||||
session.pid,
|
session.pid,
|
||||||
|
@ -1039,8 +1038,9 @@ defmodule Livebook.SessionTest do
|
||||||
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
|
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
|
||||||
session = start_session(notebook: notebook)
|
session = start_session(notebook: notebook)
|
||||||
|
|
||||||
runtime = connected_noop_runtime()
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
set_noop_runtime(session.pid)
|
||||||
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
send(
|
send(
|
||||||
session.pid,
|
session.pid,
|
||||||
|
@ -1048,8 +1048,6 @@ defmodule Livebook.SessionTest do
|
||||||
[%{kind: "text", name: "Text", requirement_presets: []}]}
|
[%{kind: "text", name: "Text", requirement_presets: []}]}
|
||||||
)
|
)
|
||||||
|
|
||||||
Session.subscribe(session.id)
|
|
||||||
|
|
||||||
editor = %{language: nil, placement: :bottom, source: "", intellisense_node: nil}
|
editor = %{language: nil, placement: :bottom, source: "", intellisense_node: nil}
|
||||||
|
|
||||||
send(
|
send(
|
||||||
|
@ -1087,8 +1085,9 @@ defmodule Livebook.SessionTest do
|
||||||
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
|
notebook = %{Notebook.new() | sections: [%{Notebook.Section.new() | cells: [smart_cell]}]}
|
||||||
session = start_session(notebook: notebook)
|
session = start_session(notebook: notebook)
|
||||||
|
|
||||||
runtime = connected_noop_runtime()
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
set_noop_runtime(session.pid)
|
||||||
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
send(
|
send(
|
||||||
session.pid,
|
session.pid,
|
||||||
|
@ -1096,8 +1095,6 @@ defmodule Livebook.SessionTest do
|
||||||
[%{kind: "text", name: "Text", requirement_presets: []}]}
|
[%{kind: "text", name: "Text", requirement_presets: []}]}
|
||||||
)
|
)
|
||||||
|
|
||||||
Session.subscribe(session.id)
|
|
||||||
|
|
||||||
send(
|
send(
|
||||||
session.pid,
|
session.pid,
|
||||||
{:runtime_smart_cell_started, smart_cell.id,
|
{:runtime_smart_cell_started, smart_cell.id,
|
||||||
|
@ -1145,8 +1142,10 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
data =
|
data =
|
||||||
data_after_operations!(data, [
|
data_after_operations!(data, [
|
||||||
{:set_runtime, self(), connected_noop_runtime()},
|
{:set_runtime, self(), Livebook.Runtime.NoopRuntime.new()},
|
||||||
{:queue_cells_evaluation, self(), ["c1"]},
|
{:connect_runtime, self()},
|
||||||
|
{:runtime_connected, self(), Livebook.Runtime.NoopRuntime.new()},
|
||||||
|
{:queue_cells_evaluation, self(), ["c1"], []},
|
||||||
{:add_cell_evaluation_response, self(), "setup", {:ok, nil}, @eval_meta},
|
{:add_cell_evaluation_response, self(), "setup", {:ok, nil}, @eval_meta},
|
||||||
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}, @eval_meta}
|
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}, @eval_meta}
|
||||||
])
|
])
|
||||||
|
@ -1174,8 +1173,10 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
data =
|
data =
|
||||||
data_after_operations!(data, [
|
data_after_operations!(data, [
|
||||||
{:set_runtime, self(), connected_noop_runtime()},
|
{:set_runtime, self(), Livebook.Runtime.NoopRuntime.new()},
|
||||||
{:queue_cells_evaluation, self(), ["c1"]},
|
{:connect_runtime, self()},
|
||||||
|
{:runtime_connected, self(), Livebook.Runtime.NoopRuntime.new()},
|
||||||
|
{:queue_cells_evaluation, self(), ["c1"], []},
|
||||||
{:add_cell_evaluation_response, self(), "setup", {:ok, nil}, @eval_meta},
|
{:add_cell_evaluation_response, self(), "setup", {:ok, nil}, @eval_meta},
|
||||||
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}, @eval_meta}
|
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}, @eval_meta}
|
||||||
])
|
])
|
||||||
|
@ -1205,8 +1206,10 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
data =
|
data =
|
||||||
data_after_operations!(data, [
|
data_after_operations!(data, [
|
||||||
{:set_runtime, self(), connected_noop_runtime()},
|
{:set_runtime, self(), Livebook.Runtime.NoopRuntime.new()},
|
||||||
{:queue_cells_evaluation, self(), ["c1"]},
|
{:connect_runtime, self()},
|
||||||
|
{:runtime_connected, self(), Livebook.Runtime.NoopRuntime.new()},
|
||||||
|
{:queue_cells_evaluation, self(), ["c1"], []},
|
||||||
{:add_cell_evaluation_response, self(), "setup", {:ok, nil}, @eval_meta},
|
{:add_cell_evaluation_response, self(), "setup", {:ok, nil}, @eval_meta},
|
||||||
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}, @eval_meta}
|
{:add_cell_evaluation_response, self(), "c1", {:ok, nil}, @eval_meta}
|
||||||
])
|
])
|
||||||
|
@ -1261,8 +1264,8 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
||||||
|
|
||||||
runtime = connected_noop_runtime()
|
set_noop_runtime(session.pid)
|
||||||
Session.set_runtime(session.pid, runtime)
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
archive_path = Path.expand("../support/assets.tar.gz", __DIR__)
|
archive_path = Path.expand("../support/assets.tar.gz", __DIR__)
|
||||||
hash = "test-" <> Utils.random_id()
|
hash = "test-" <> Utils.random_id()
|
||||||
|
@ -1285,13 +1288,15 @@ defmodule Livebook.SessionTest do
|
||||||
test "restores transient state when restarting runtimes" do
|
test "restores transient state when restarting runtimes" do
|
||||||
session = start_session()
|
session = start_session()
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
set_noop_runtime(session.pid, self())
|
||||||
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
transient_state = %{state: "anything"}
|
transient_state = %{state: "anything"}
|
||||||
send(session.pid, {:runtime_transient_state, transient_state})
|
send(session.pid, {:runtime_transient_state, transient_state})
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
assert_receive {:runtime_trace, :restore_transient_state, [^transient_state]}
|
assert_receive {:runtime_trace, :restore_transient_state, [^transient_state]}
|
||||||
end
|
end
|
||||||
|
@ -1419,9 +1424,6 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
Process.exit(Process.whereis(test), :shutdown)
|
Process.exit(Process.whereis(test), :shutdown)
|
||||||
|
|
||||||
assert_receive {:app_updated,
|
|
||||||
%{pid: ^app_pid, sessions: [%{app_status: %{execution: :error}}]}}
|
|
||||||
|
|
||||||
assert_receive {:app_updated,
|
assert_receive {:app_updated,
|
||||||
%{pid: ^app_pid, sessions: [%{app_status: %{execution: :executing}}]}}
|
%{pid: ^app_pid, sessions: [%{app_status: %{execution: :executing}}]}}
|
||||||
|
|
||||||
|
@ -1596,8 +1598,7 @@ defmodule Livebook.SessionTest do
|
||||||
test "replies with error when file entry does not exist" do
|
test "replies with error when file entry does not exist" do
|
||||||
session = start_session()
|
session = start_session()
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
||||||
|
|
||||||
assert_receive {:runtime_file_entry_path_reply,
|
assert_receive {:runtime_file_entry_path_reply,
|
||||||
|
@ -1623,8 +1624,7 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
session = start_session(notebook: notebook)
|
session = start_session(notebook: notebook)
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_file_entry_path_request, self(), "document.pdf"})
|
send(session.pid, {:runtime_file_entry_path_request, self(), "document.pdf"})
|
||||||
|
|
||||||
assert_receive {:runtime_file_entry_path_reply, {:error, :forbidden}}
|
assert_receive {:runtime_file_entry_path_reply, {:error, :forbidden}}
|
||||||
|
@ -1640,8 +1640,7 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
Session.add_file_entries(session.pid, [%{type: :attachment, name: "image.jpg"}])
|
Session.add_file_entries(session.pid, [%{type: :attachment, name: "image.jpg"}])
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
||||||
|
|
||||||
assert_receive {:runtime_file_entry_path_reply, {:error, "no file exists at path " <> _}}
|
assert_receive {:runtime_file_entry_path_reply, {:error, "no file exists at path " <> _}}
|
||||||
|
@ -1659,8 +1658,7 @@ defmodule Livebook.SessionTest do
|
||||||
:ok = FileSystem.File.write(image_file, "")
|
:ok = FileSystem.File.write(image_file, "")
|
||||||
Session.add_file_entries(session.pid, [%{type: :attachment, name: "image.jpg"}])
|
Session.add_file_entries(session.pid, [%{type: :attachment, name: "image.jpg"}])
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
||||||
|
|
||||||
path = image_file.path
|
path = image_file.path
|
||||||
|
@ -1680,8 +1678,7 @@ defmodule Livebook.SessionTest do
|
||||||
:ok = FileSystem.File.write(image_file, "content")
|
:ok = FileSystem.File.write(image_file, "content")
|
||||||
Session.add_file_entries(session.pid, [%{type: :file, name: "image.jpg", file: image_file}])
|
Session.add_file_entries(session.pid, [%{type: :file, name: "image.jpg", file: image_file}])
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
||||||
|
|
||||||
path = image_file.path
|
path = image_file.path
|
||||||
|
@ -1707,8 +1704,7 @@ defmodule Livebook.SessionTest do
|
||||||
image_file = FileSystem.File.new(s3_fs, "/image.jpg")
|
image_file = FileSystem.File.new(s3_fs, "/image.jpg")
|
||||||
Session.add_file_entries(session.pid, [%{type: :file, name: "image.jpg", file: image_file}])
|
Session.add_file_entries(session.pid, [%{type: :file, name: "image.jpg", file: image_file}])
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
||||||
|
|
||||||
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
||||||
|
@ -1745,8 +1741,7 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
Session.add_file_entries(session.pid, [%{type: :url, name: "image.jpg", url: url}])
|
Session.add_file_entries(session.pid, [%{type: :url, name: "image.jpg", url: url}])
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
||||||
|
|
||||||
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
||||||
|
@ -1773,8 +1768,7 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
Session.add_file_entries(session.pid, [%{type: :url, name: "image.jpg", url: url}])
|
Session.add_file_entries(session.pid, [%{type: :url, name: "image.jpg", url: url}])
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
||||||
|
|
||||||
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
||||||
|
@ -1800,8 +1794,7 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
Session.add_file_entries(session.pid, [%{type: :url, name: "image.jpg", url: url}])
|
Session.add_file_entries(session.pid, [%{type: :url, name: "image.jpg", url: url}])
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
||||||
|
|
||||||
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
||||||
|
@ -1824,8 +1817,7 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
Session.add_file_entries(session.pid, [%{type: :url, name: "image.jpg", url: url}])
|
Session.add_file_entries(session.pid, [%{type: :url, name: "image.jpg", url: url}])
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
||||||
|
|
||||||
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
||||||
|
@ -1853,8 +1845,7 @@ defmodule Livebook.SessionTest do
|
||||||
:ok = FileSystem.File.write(image_file, "")
|
:ok = FileSystem.File.write(image_file, "")
|
||||||
Session.add_file_entries(session.pid, [%{type: :attachment, name: "image2.jpg"}])
|
Session.add_file_entries(session.pid, [%{type: :attachment, name: "image2.jpg"}])
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
||||||
|
|
||||||
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
||||||
|
@ -1877,8 +1868,7 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
Session.add_file_entries(session.pid, [%{type: :url, name: "image.jpg", url: url}])
|
Session.add_file_entries(session.pid, [%{type: :url, name: "image.jpg", url: url}])
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
send(session.pid, {:runtime_file_entry_path_request, self(), "image.jpg"})
|
||||||
|
|
||||||
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
assert_receive {:runtime_file_entry_path_reply, {:ok, path}}
|
||||||
|
@ -1905,8 +1895,7 @@ defmodule Livebook.SessionTest do
|
||||||
test "replies with error when the session does not use teams hub" do
|
test "replies with error when the session does not use teams hub" do
|
||||||
session = start_session()
|
session = start_session()
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_user_info_request, self(), "c1"})
|
send(session.pid, {:runtime_user_info_request, self(), "c1"})
|
||||||
|
|
||||||
assert_receive {:runtime_user_info_reply, {:error, :not_available}}
|
assert_receive {:runtime_user_info_reply, {:error, :not_available}}
|
||||||
|
@ -1916,8 +1905,7 @@ defmodule Livebook.SessionTest do
|
||||||
notebook = %{Notebook.new() | teams_enabled: true}
|
notebook = %{Notebook.new() | teams_enabled: true}
|
||||||
session = start_session(notebook: notebook)
|
session = start_session(notebook: notebook)
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_user_info_request, self(), "c1"})
|
send(session.pid, {:runtime_user_info_request, self(), "c1"})
|
||||||
|
|
||||||
assert_receive {:runtime_user_info_reply, {:error, :not_found}}
|
assert_receive {:runtime_user_info_reply, {:error, :not_found}}
|
||||||
|
@ -1936,8 +1924,7 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
{_, client_id} = Session.register_client(session.pid, self(), user)
|
{_, client_id} = Session.register_client(session.pid, self(), user)
|
||||||
|
|
||||||
runtime = connected_noop_runtime(self())
|
set_noop_runtime(session.pid, self())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
send(session.pid, {:runtime_user_info_request, self(), client_id})
|
send(session.pid, {:runtime_user_info_request, self(), client_id})
|
||||||
|
|
||||||
assert_receive {:runtime_user_info_reply, {:ok, user_info}}
|
assert_receive {:runtime_user_info_reply, {:ok, user_info}}
|
||||||
|
@ -1959,8 +1946,7 @@ defmodule Livebook.SessionTest do
|
||||||
|
|
||||||
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
{_section_id, cell_id} = insert_section_and_cell(session.pid)
|
||||||
|
|
||||||
runtime = connected_noop_runtime()
|
set_noop_runtime(session.pid)
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
|
|
||||||
user = Livebook.Users.User.new()
|
user = Livebook.Users.User.new()
|
||||||
Session.register_client(session.pid, self(), user)
|
Session.register_client(session.pid, self(), user)
|
||||||
|
@ -2056,15 +2042,8 @@ defmodule Livebook.SessionTest do
|
||||||
{section_id, cell_id}
|
{section_id, cell_id}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp connected_noop_runtime(trace_to \\ nil) do
|
defp set_noop_runtime(session_pid, trace_to \\ nil) do
|
||||||
{:ok, runtime} = Livebook.Runtime.NoopRuntime.new(trace_to) |> Livebook.Runtime.connect()
|
runtime = Livebook.Runtime.NoopRuntime.new(trace_to)
|
||||||
runtime
|
Session.set_runtime(session_pid, runtime)
|
||||||
end
|
|
||||||
|
|
||||||
defp wait_for_session_update(session_pid) do
|
|
||||||
# This call is synchronous, so it gives the session time
|
|
||||||
# for handling the previously sent change messages.
|
|
||||||
Session.get_data(session_pid)
|
|
||||||
:ok
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -438,9 +438,11 @@ defmodule LivebookWeb.SessionControllerTest do
|
||||||
|
|
||||||
defp start_session_and_request_asset(conn, notebook, hash) do
|
defp start_session_and_request_asset(conn, notebook, hash) do
|
||||||
{:ok, session} = Sessions.create_session(notebook: notebook)
|
{:ok, session} = Sessions.create_session(notebook: notebook)
|
||||||
|
|
||||||
# We need runtime in place to actually copy the archive
|
# We need runtime in place to actually copy the archive
|
||||||
{:ok, runtime} = Livebook.Runtime.Embedded.new() |> Livebook.Runtime.connect()
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
Session.connect_runtime(session.pid)
|
||||||
|
assert_receive {:operation, {:runtime_connected, _, _}}
|
||||||
|
|
||||||
conn = get(conn, ~p"/public/sessions/#{session.id}/assets/#{hash}/main.js")
|
conn = get(conn, ~p"/public/sessions/#{session.id}/assets/#{hash}/main.js")
|
||||||
|
|
||||||
|
|
|
@ -134,26 +134,12 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
continue_fun.()
|
continue_fun.()
|
||||||
end
|
end
|
||||||
|
|
||||||
test "reevaluting the setup cell", %{conn: conn, session: session} do
|
|
||||||
Session.subscribe(session.id)
|
|
||||||
evaluate_setup(session.pid)
|
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
|
||||||
|
|
||||||
view
|
|
||||||
|> element(~s{[data-el-session]})
|
|
||||||
|> render_hook("queue_cell_evaluation", %{"cell_id" => "setup"})
|
|
||||||
|
|
||||||
assert_receive {:operation, {:set_runtime, _pid, %{} = _runtime}}
|
|
||||||
end
|
|
||||||
|
|
||||||
test "reevaluting the setup cell with dependencies cache disabled",
|
test "reevaluting the setup cell with dependencies cache disabled",
|
||||||
%{conn: conn, session: session} do
|
%{conn: conn, session: session} do
|
||||||
Session.subscribe(session.id)
|
Session.subscribe(session.id)
|
||||||
|
|
||||||
# Start the standalone runtime, to encapsulate env var changes
|
# Use the standalone runtime, to encapsulate env var changes
|
||||||
{:ok, runtime} = Runtime.ElixirStandalone.new() |> Runtime.connect()
|
Session.set_runtime(session.pid, Runtime.Standalone.new())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
|
|
||||||
evaluate_setup(session.pid)
|
evaluate_setup(session.pid)
|
||||||
|
|
||||||
|
@ -294,8 +280,9 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
:ok = FileSystem.File.write(image_file, "content")
|
:ok = FileSystem.File.write(image_file, "content")
|
||||||
Session.add_file_entries(session.pid, [%{type: :attachment, name: "file.bin"}])
|
Session.add_file_entries(session.pid, [%{type: :attachment, name: "file.bin"}])
|
||||||
|
|
||||||
{:ok, runtime} = Livebook.Runtime.NoopRuntime.new() |> Livebook.Runtime.connect()
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
Session.set_runtime(session.pid, Livebook.Runtime.NoopRuntime.new(self()))
|
||||||
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||||
|
|
||||||
|
@ -340,8 +327,9 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
:ok = FileSystem.File.write(image_file, "content")
|
:ok = FileSystem.File.write(image_file, "content")
|
||||||
Session.add_file_entries(session.pid, [%{type: :attachment, name: "image.jpg"}])
|
Session.add_file_entries(session.pid, [%{type: :attachment, name: "image.jpg"}])
|
||||||
|
|
||||||
{:ok, runtime} = Livebook.Runtime.NoopRuntime.new() |> Livebook.Runtime.connect()
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
Session.set_runtime(session.pid, Livebook.Runtime.NoopRuntime.new(self()))
|
||||||
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||||
|
|
||||||
|
@ -370,8 +358,9 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
section_id = insert_section(session.pid)
|
section_id = insert_section(session.pid)
|
||||||
cell_id = insert_text_cell(session.pid, section_id, :code)
|
cell_id = insert_text_cell(session.pid, section_id, :code)
|
||||||
|
|
||||||
{:ok, runtime} = Livebook.Runtime.NoopRuntime.new() |> Livebook.Runtime.connect()
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
Session.set_runtime(session.pid, Livebook.Runtime.NoopRuntime.new(self()))
|
||||||
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||||
|
|
||||||
|
@ -887,8 +876,8 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
%{conn: conn, session: session} do
|
%{conn: conn, session: session} do
|
||||||
insert_section(session.pid)
|
insert_section(session.pid)
|
||||||
|
|
||||||
{:ok, runtime} = Runtime.Embedded.new() |> Runtime.connect()
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||||
|
|
||||||
|
@ -907,23 +896,22 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "runtime settings" do
|
describe "runtime settings" do
|
||||||
test "connecting to elixir standalone updates connect button to reconnect",
|
test "connecting to standalone updates connect button to reconnect",
|
||||||
%{conn: conn, session: session} do
|
%{conn: conn, session: session} do
|
||||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/settings/runtime")
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/settings/runtime")
|
||||||
|
|
||||||
Session.subscribe(session.id)
|
Session.subscribe(session.id)
|
||||||
|
|
||||||
view
|
view
|
||||||
|> element("button", "Elixir standalone")
|
|> element("#runtime-settings-modal button", "Standalone")
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
[elixir_standalone_view] = live_children(view)
|
view
|
||||||
|
|> element("#runtime-settings-modal button", "Connect")
|
||||||
elixir_standalone_view
|
|
||||||
|> element("button", "Connect")
|
|
||||||
|> render_click()
|
|> render_click()
|
||||||
|
|
||||||
assert_receive {:operation, {:set_runtime, _pid, %Runtime.ElixirStandalone{} = runtime}}
|
assert_receive {:operation, {:set_runtime, _pid, %Runtime.Standalone{}}}
|
||||||
|
assert_receive {:operation, {:runtime_connected, _pid, %Runtime.Standalone{} = runtime}}
|
||||||
|
|
||||||
page = render(view)
|
page = render(view)
|
||||||
assert page =~ Atom.to_string(runtime.node)
|
assert page =~ Atom.to_string(runtime.node)
|
||||||
|
@ -932,13 +920,12 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
test "disconnecting a connected node", %{conn: conn, session: session} do
|
test "disconnecting a connected node", %{conn: conn, session: session} do
|
||||||
{:ok, runtime} = Livebook.Runtime.NoopRuntime.new(self()) |> Livebook.Runtime.connect()
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
Session.set_runtime(session.pid, Livebook.Runtime.NoopRuntime.new(self()))
|
||||||
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||||
|
|
||||||
Session.subscribe(session.id)
|
|
||||||
|
|
||||||
assert render(view) =~ "No connected nodes"
|
assert render(view) =~ "No connected nodes"
|
||||||
|
|
||||||
# Mimic the runtime reporting a connected node
|
# Mimic the runtime reporting a connected node
|
||||||
|
@ -956,6 +943,229 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
|
|
||||||
assert_receive {:runtime_trace, :disconnect_node, [^node]}
|
assert_receive {:runtime_trace, :disconnect_node, [^node]}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "configuring fly runtime", %{conn: conn, session: session} do
|
||||||
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/settings/runtime")
|
||||||
|
|
||||||
|
Session.subscribe(session.id)
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element("#runtime-settings-modal button", "Fly.io machine")
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
Livebook.FlyAPI.stub(fn conn when conn.method == "POST" ->
|
||||||
|
Req.Test.json(conn, %{
|
||||||
|
"data" => nil,
|
||||||
|
"errors" => [
|
||||||
|
%{
|
||||||
|
"extensions" => %{"code" => "UNAUTHORIZED"},
|
||||||
|
"locations" => [%{"column" => 3, "line" => 2}],
|
||||||
|
"message" => "You must be authenticated to view this.",
|
||||||
|
"path" => ["organizations"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element(~s{form[phx-change="set_token"]})
|
||||||
|
|> render_change(%{token: "invalid"})
|
||||||
|
|
||||||
|
assert render_async(view) =~ "Error: could not authorize with the given token"
|
||||||
|
|
||||||
|
Livebook.FlyAPI.stub(fn conn when conn.method == "POST" ->
|
||||||
|
Req.Test.json(conn, %{
|
||||||
|
"data" => %{
|
||||||
|
"organizations" => %{
|
||||||
|
"nodes" => [
|
||||||
|
%{
|
||||||
|
"id" => "1",
|
||||||
|
"name" => "Grumpy Cat",
|
||||||
|
"rawSlug" => "grumpy-cat",
|
||||||
|
"slug" => "personal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"platform" => %{
|
||||||
|
"regions" => [
|
||||||
|
%{"code" => "ams", "name" => "Amsterdam, Netherlands"},
|
||||||
|
%{"code" => "fra", "name" => "Frankfurt, Germany"}
|
||||||
|
],
|
||||||
|
"requestRegion" => "fra"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element(~s{form[phx-change="set_token"]})
|
||||||
|
|> render_change(%{token: "valid"})
|
||||||
|
|
||||||
|
assert render_async(view) =~ "Grumpy Cat"
|
||||||
|
|
||||||
|
# Selects the closest region by default
|
||||||
|
assert view
|
||||||
|
|> element(~s/select[name="region"] option[value="fra"][selected]/)
|
||||||
|
|> has_element?()
|
||||||
|
|
||||||
|
Livebook.FlyAPI.stub(fn conn
|
||||||
|
when conn.method == "GET" and
|
||||||
|
conn.path_info == ["v1", "apps", "new-app", "volumes"] ->
|
||||||
|
conn
|
||||||
|
|> Plug.Conn.put_status(404)
|
||||||
|
|> Req.Test.json(%{"error" => "App not found"})
|
||||||
|
end)
|
||||||
|
|
||||||
|
# Create a new app
|
||||||
|
view
|
||||||
|
|> element(~s{form[phx-change="set_app_name"]})
|
||||||
|
|> render_change(%{app_name: "new-app"})
|
||||||
|
|
||||||
|
assert render_async(view) =~ ~r/App .*new-app.* does not exist yet/
|
||||||
|
|
||||||
|
Livebook.FlyAPI.stub(fn conn
|
||||||
|
when conn.method == "POST" and conn.path_info == ["v1", "apps"] ->
|
||||||
|
Plug.Conn.send_resp(conn, 201, "")
|
||||||
|
end)
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element(~s/button[phx-click="create_app"]/)
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
assert render_async(view) =~ "CPU kind"
|
||||||
|
|
||||||
|
# Create a new volume
|
||||||
|
|
||||||
|
Livebook.FlyAPI.stub(fn conn
|
||||||
|
when conn.method == "POST" and
|
||||||
|
conn.path_info == ["v1", "apps", "new-app", "volumes"] ->
|
||||||
|
Req.Test.json(conn, %{
|
||||||
|
"id" => "vol_1",
|
||||||
|
"name" => "new_volume",
|
||||||
|
"region" => "ams",
|
||||||
|
"size_gb" => 1,
|
||||||
|
"state" => "created"
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element(~s/button[phx-click="new_volume"]/)
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element(~s/form[phx-submit="create_volume"]/)
|
||||||
|
|> render_submit(%{volume: %{name: "new_volume", size_gb: "1"}})
|
||||||
|
|
||||||
|
assert render_async(view) =~ "name: new_volume"
|
||||||
|
|
||||||
|
# The volume is automatically selected
|
||||||
|
assert view
|
||||||
|
|> element(~s/select[name="volume_id"] option[value="vol_1"][selected]/)
|
||||||
|
|> has_element?()
|
||||||
|
|
||||||
|
# Delete the volume
|
||||||
|
|
||||||
|
Livebook.FlyAPI.stub(fn conn
|
||||||
|
when conn.method == "DELETE" and
|
||||||
|
conn.path_info == [
|
||||||
|
"v1",
|
||||||
|
"apps",
|
||||||
|
"new-app",
|
||||||
|
"volumes",
|
||||||
|
"vol_1"
|
||||||
|
] ->
|
||||||
|
Req.Test.json(conn, %{})
|
||||||
|
end)
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element(~s/button[phx-click="delete_volume"]/)
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
view
|
||||||
|
|> element(~s/button[phx-click="confirm_delete_volume"]/)
|
||||||
|
|> render_click()
|
||||||
|
|
||||||
|
refute render_async(view) =~ "name: new_volume"
|
||||||
|
|
||||||
|
assert view
|
||||||
|
|> element(~s/select[name="volume_id"] option[value=""][selected]/)
|
||||||
|
|> has_element?()
|
||||||
|
|
||||||
|
# We do not actually connect the runtime. We test connecting
|
||||||
|
# againast the real API separately
|
||||||
|
end
|
||||||
|
|
||||||
|
test "populates fly runtime config form existing runtime", %{conn: conn, session: session} do
|
||||||
|
runtime =
|
||||||
|
Runtime.Fly.new(%{
|
||||||
|
token: "my-token",
|
||||||
|
app_name: "my-app",
|
||||||
|
region: "ams",
|
||||||
|
cpu_kind: "performance",
|
||||||
|
cpus: 1,
|
||||||
|
memory_gb: 1,
|
||||||
|
gpu_kind: nil,
|
||||||
|
gpus: nil,
|
||||||
|
volume_id: "vol_1",
|
||||||
|
docker_tag: "edge"
|
||||||
|
})
|
||||||
|
|
||||||
|
Session.set_runtime(session.pid, runtime)
|
||||||
|
|
||||||
|
Livebook.FlyAPI.stub(fn
|
||||||
|
conn when conn.method == "POST" ->
|
||||||
|
Req.Test.json(conn, %{
|
||||||
|
"data" => %{
|
||||||
|
"organizations" => %{
|
||||||
|
"nodes" => [
|
||||||
|
%{
|
||||||
|
"id" => "1",
|
||||||
|
"name" => "Grumpy Cat",
|
||||||
|
"rawSlug" => "grumpy-cat",
|
||||||
|
"slug" => "personal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"platform" => %{
|
||||||
|
"regions" => [
|
||||||
|
%{"code" => "ams", "name" => "Amsterdam, Netherlands"},
|
||||||
|
%{"code" => "fra", "name" => "Frankfurt, Germany"}
|
||||||
|
],
|
||||||
|
"requestRegion" => "fra"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
conn
|
||||||
|
when conn.method == "GET" and
|
||||||
|
conn.path_info == ["v1", "apps", "my-app", "volumes"] ->
|
||||||
|
Req.Test.json(conn, [
|
||||||
|
%{
|
||||||
|
"id" => "vol_1",
|
||||||
|
"name" => "new_volume",
|
||||||
|
"region" => "ams",
|
||||||
|
"size_gb" => 1,
|
||||||
|
"state" => "created"
|
||||||
|
}
|
||||||
|
])
|
||||||
|
end)
|
||||||
|
|
||||||
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}/settings/runtime")
|
||||||
|
|
||||||
|
assert render_async(view) =~ "Grumpy Cat"
|
||||||
|
|
||||||
|
assert view
|
||||||
|
|> element(~s/select[name="region"] option[value="ams"][selected]/)
|
||||||
|
|> has_element?()
|
||||||
|
|
||||||
|
assert view
|
||||||
|
|> element(~s/select[name="volume_id"] option[value="vol_1"][selected]/)
|
||||||
|
|> has_element?()
|
||||||
|
|
||||||
|
assert view
|
||||||
|
|> element(~s/select[name="specs[cpu_kind]"] option[value="performance"][selected]/)
|
||||||
|
|> has_element?()
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "persistence settings" do
|
describe "persistence settings" do
|
||||||
|
@ -1057,8 +1267,8 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
section_id = insert_section(session.pid)
|
section_id = insert_section(session.pid)
|
||||||
cell_id = insert_text_cell(session.pid, section_id, :code, "Process.sleep(10)")
|
cell_id = insert_text_cell(session.pid, section_id, :code, "Process.sleep(10)")
|
||||||
|
|
||||||
{:ok, runtime} = Runtime.Embedded.new() |> Runtime.connect()
|
Session.subscribe(session.id)
|
||||||
Session.set_runtime(session.pid, runtime)
|
connect_and_await_runtime(session.pid)
|
||||||
|
|
||||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||||
|
|
||||||
|
@ -1750,7 +1960,7 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "environment variables" do
|
describe "environment variables" do
|
||||||
test "outputs persisted env var from ets", %{conn: conn, session: session} do
|
test "outputs persisted env var from settings", %{conn: conn, session: session} do
|
||||||
Session.subscribe(session.id)
|
Session.subscribe(session.id)
|
||||||
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
|
||||||
|
|
||||||
|
@ -1802,9 +2012,8 @@ defmodule LivebookWeb.SessionLiveTest do
|
||||||
@tag :tmp_dir
|
@tag :tmp_dir
|
||||||
test "outputs persisted PATH delimited with os PATH env var",
|
test "outputs persisted PATH delimited with os PATH env var",
|
||||||
%{conn: conn, session: session, tmp_dir: tmp_dir} do
|
%{conn: conn, session: session, tmp_dir: tmp_dir} do
|
||||||
# Start the standalone runtime, to encapsulate env var changes
|
# Use the standalone runtime, to encapsulate env var changes
|
||||||
{:ok, runtime} = Runtime.ElixirStandalone.new() |> Runtime.connect()
|
Session.set_runtime(session.pid, Runtime.Standalone.new())
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
|
|
||||||
separator =
|
separator =
|
||||||
case :os.type() do
|
case :os.type() do
|
||||||
|
|
|
@ -6,7 +6,7 @@ defmodule LivebookWeb.ProxyPlugTest do
|
||||||
require Phoenix.LiveViewTest
|
require Phoenix.LiveViewTest
|
||||||
import Livebook.AppHelpers
|
import Livebook.AppHelpers
|
||||||
|
|
||||||
alias Livebook.{Notebook, Runtime, Session, Sessions}
|
alias Livebook.{Notebook, Session, Sessions}
|
||||||
|
|
||||||
describe "session" do
|
describe "session" do
|
||||||
test "returns error when session doesn't exist", %{conn: conn} do
|
test "returns error when session doesn't exist", %{conn: conn} do
|
||||||
|
@ -28,9 +28,7 @@ defmodule LivebookWeb.ProxyPlugTest do
|
||||||
test "returns the proxied response defined in notebook", %{conn: conn} do
|
test "returns the proxied response defined in notebook", %{conn: conn} do
|
||||||
%{sections: [%{cells: [%{id: cell_id}]}]} = notebook = proxy_notebook()
|
%{sections: [%{cells: [%{id: cell_id}]}]} = notebook = proxy_notebook()
|
||||||
{:ok, session} = Sessions.create_session(notebook: notebook)
|
{:ok, session} = Sessions.create_session(notebook: notebook)
|
||||||
{:ok, runtime} = Runtime.Embedded.new() |> Runtime.connect()
|
|
||||||
|
|
||||||
Session.set_runtime(session.pid, runtime)
|
|
||||||
Session.subscribe(session.id)
|
Session.subscribe(session.id)
|
||||||
Session.queue_cell_evaluation(session.pid, cell_id)
|
Session.queue_cell_evaluation(session.pid, cell_id)
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,10 @@ defmodule Livebook.Runtime.NoopRuntime do
|
||||||
# A runtime that doesn't do any actual evaluation,
|
# A runtime that doesn't do any actual evaluation,
|
||||||
# thus not requiring any underlying resources.
|
# thus not requiring any underlying resources.
|
||||||
|
|
||||||
defstruct [:started, :trace_to]
|
defstruct [:trace_to]
|
||||||
|
|
||||||
def new(trace_to \\ nil) do
|
def new(trace_to \\ nil) do
|
||||||
%__MODULE__{started: false, trace_to: trace_to}
|
%__MODULE__{trace_to: trace_to}
|
||||||
end
|
end
|
||||||
|
|
||||||
defimpl Livebook.Runtime do
|
defimpl Livebook.Runtime do
|
||||||
|
@ -13,11 +13,17 @@ defmodule Livebook.Runtime.NoopRuntime do
|
||||||
[{"Type", "Noop"}]
|
[{"Type", "Noop"}]
|
||||||
end
|
end
|
||||||
|
|
||||||
def connect(runtime), do: {:ok, %{runtime | started: true}}
|
def connect(runtime) do
|
||||||
def connected?(runtime), do: runtime.started
|
caller = self()
|
||||||
|
|
||||||
|
spawn(fn ->
|
||||||
|
send(caller, {:runtime_connect_done, self(), {:ok, runtime}})
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
def take_ownership(_, _), do: make_ref()
|
def take_ownership(_, _), do: make_ref()
|
||||||
def disconnect(runtime), do: {:ok, %{runtime | started: false}}
|
def disconnect(_), do: :ok
|
||||||
def duplicate(_), do: Livebook.Runtime.NoopRuntime.new()
|
def duplicate(runtime), do: Livebook.Runtime.NoopRuntime.new(runtime.trace_to)
|
||||||
|
|
||||||
def evaluate_code(_, _, _, _, _, _ \\ []), do: :ok
|
def evaluate_code(_, _, _, _, _, _ \\ []), do: :ok
|
||||||
def forget_evaluation(_, _), do: :ok
|
def forget_evaluation(_, _), do: :ok
|
||||||
|
@ -61,8 +67,6 @@ defmodule Livebook.Runtime.NoopRuntime do
|
||||||
|
|
||||||
def search_packages(_, _, _), do: make_ref()
|
def search_packages(_, _, _), do: make_ref()
|
||||||
|
|
||||||
def disable_dependencies_cache(_), do: :ok
|
|
||||||
|
|
||||||
def put_system_envs(_, _), do: :ok
|
def put_system_envs(_, _), do: :ok
|
||||||
def delete_system_envs(_, _), do: :ok
|
def delete_system_envs(_, _), do: :ok
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,11 @@ defmodule Livebook.SessionHelpers do
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def connect_and_await_runtime(session_pid) do
|
||||||
|
Session.connect_runtime(session_pid)
|
||||||
|
assert_receive {:operation, {:runtime_connected, _, _}}
|
||||||
|
end
|
||||||
|
|
||||||
def evaluate_setup(session_pid) do
|
def evaluate_setup(session_pid) do
|
||||||
Session.queue_cell_evaluation(session_pid, "setup")
|
Session.queue_cell_evaluation(session_pid, "setup")
|
||||||
assert_receive {:operation, {:add_cell_evaluation_response, _, "setup", _, _}}
|
assert_receive {:operation, {:add_cell_evaluation_response, _, "setup", _, _}}
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
# Start manager on the current node and configure it not to
|
# Start manager on the current node and configure it not to terminate
|
||||||
# terminate automatically, so there is no race condition
|
# automatically, so that we can use it to start runtime servers
|
||||||
# when starting/stopping Embedded runtimes in parallel
|
# explicitly
|
||||||
Livebook.Runtime.ErlDist.NodeManager.start(
|
Livebook.Runtime.ErlDist.NodeManager.start(
|
||||||
auto_termination: false,
|
auto_termination: false,
|
||||||
unload_modules_on_termination: false
|
unload_modules_on_termination: false
|
||||||
)
|
)
|
||||||
|
|
||||||
# Use the embedded runtime in tests by default, so they are
|
# Use the embedded runtime in tests by default, so they are cheaper
|
||||||
# cheaper to run. Other runtimes can be tested by starting
|
# to run. Other runtimes can be tested by setting them explicitly
|
||||||
# and setting them explicitly
|
|
||||||
Application.put_env(:livebook, :default_runtime, Livebook.Runtime.Embedded.new())
|
Application.put_env(:livebook, :default_runtime, Livebook.Runtime.Embedded.new())
|
||||||
Application.put_env(:livebook, :default_app_runtime, Livebook.Runtime.Embedded.new())
|
Application.put_env(:livebook, :default_app_runtime, Livebook.Runtime.Embedded.new())
|
||||||
|
|
||||||
Application.put_env(:livebook, :runtime_modules, [
|
Application.put_env(:livebook, :runtime_modules, [
|
||||||
Livebook.Runtime.ElixirStandalone,
|
Livebook.Runtime.Standalone,
|
||||||
Livebook.Runtime.Attached,
|
Livebook.Runtime.Attached,
|
||||||
Livebook.Runtime.Embedded
|
Livebook.Runtime.Embedded,
|
||||||
|
Livebook.Runtime.Fly
|
||||||
])
|
])
|
||||||
|
|
||||||
defmodule Livebook.Runtime.Embedded.Packages do
|
defmodule Livebook.Runtime.Embedded.Packages do
|
||||||
|
@ -71,15 +71,9 @@ teams_exclude =
|
||||||
[:teams_integration]
|
[:teams_integration]
|
||||||
end
|
end
|
||||||
|
|
||||||
# ELIXIR_ERL_OPTIONS="-epmd_module Elixir.Livebook.EPMD -start_epmd false -erl_epmd_port 0" LIVEBOOK_EPMDLESS=true mix test
|
fly_exclude = if System.get_env("TEST_FLY_API_TOKEN"), do: [], else: [:fly]
|
||||||
epmd_exclude =
|
|
||||||
if Livebook.Config.epmdless?() do
|
|
||||||
[:with_epmd, :teams_integration]
|
|
||||||
else
|
|
||||||
[:without_epmd]
|
|
||||||
end
|
|
||||||
|
|
||||||
ExUnit.start(
|
ExUnit.start(
|
||||||
assert_receive_timeout: if(windows?, do: 2_500, else: 1_500),
|
assert_receive_timeout: if(windows?, do: 2_500, else: 1_500),
|
||||||
exclude: erl_docs_exclude ++ windows_exclude ++ teams_exclude ++ epmd_exclude
|
exclude: erl_docs_exclude ++ windows_exclude ++ teams_exclude ++ fly_exclude
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue