List connected distribution nodes in the runtime panel (#2636)

This commit is contained in:
Jonatan Kłosko 2024-06-06 18:08:59 +02:00 committed by GitHub
parent 87daabaf60
commit 68fa363d2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 256 additions and 42 deletions

View file

@ -244,6 +244,11 @@ solely client-side operations.
@apply hidden;
}
[data-el-session][data-js-side-panel-content="runtime-info"]
[data-el-runtime-indicator] {
@apply border-gray-700;
}
[data-el-session]:not([data-js-side-panel-content="app-info"])
[data-el-app-info] {
@apply hidden;

View file

@ -765,6 +765,17 @@ defprotocol Livebook.Runtime do
@type proxy_handler_spec :: {module :: module(), function :: atom(), args :: list()}
@typedoc """
An information about Elixir nodes that the runtime is connected to.
Whenever the node list change, the runtime should send an updated
list as:
* `{:runtime_connected_nodes, connected_nodes()}`
"""
@type connected_nodes :: list(node())
@doc """
Returns relevant information about the runtime.
@ -1116,4 +1127,13 @@ defprotocol Livebook.Runtime do
"""
@spec fetch_proxy_handler_spec(t()) :: {:ok, proxy_handler_spec()} | {:error, :not_found}
def fetch_proxy_handler_spec(runtime)
@doc """
Asks the runtime to disconnect from the given connected node.
The node should be one of `connected_nodes()` reported by the runtime
earlier.
"""
@spec disconnect_node(t(), node()) :: :ok
def disconnect_node(runtime, node)
end

View file

@ -208,4 +208,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Attached do
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

View file

@ -327,4 +327,8 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.ElixirStandalone do
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

View file

@ -175,6 +175,10 @@ defimpl Livebook.Runtime, for: Livebook.Runtime.Embedded do
RuntimeServer.fetch_proxy_handler_spec(runtime.server_pid)
end
def disconnect_node(runtime, node) do
RuntimeServer.disconnect_node(runtime.server_pid, node)
end
defp config() do
Application.get_env(:livebook, Livebook.Runtime.Embedded, [])
end

View file

@ -328,6 +328,10 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
end
end
def disconnect_node(pid, node) do
GenServer.cast(pid, {:disconnect_node, node})
end
@doc """
Stops the runtime server.
@ -362,10 +366,11 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
smart_cell_supervisor: nil,
smart_cell_gl: nil,
smart_cells: %{},
smart_cell_definitions: nil,
smart_cell_definitions: [],
smart_cell_definitions_module:
Keyword.get(opts, :smart_cell_definitions_module, Kino.SmartCell),
extra_smart_cell_definitions: Keyword.get(opts, :extra_smart_cell_definitions, []),
connected_nodes: [],
memory_timer_ref: nil,
last_evaluator: nil,
base_env_path:
@ -408,7 +413,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
def handle_info({:evaluation_finished, locator}, state) do
{:noreply,
state
|> report_smart_cell_definitions()
|> report_environment()
|> report_transient_state()
|> scan_binding_after_evaluation(locator)}
end
@ -480,7 +485,7 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
Process.monitor(owner)
state = %{state | owner: owner, runtime_broadcast_to: opts[:runtime_broadcast_to]}
state = report_smart_cell_definitions(state)
state = report_environment(state)
report_memory_usage(state)
{:ok, smart_cell_supervisor} = DynamicSupervisor.start_link(strategy: :one_for_one)
@ -710,6 +715,11 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
{:noreply, state}
end
def handle_cast({:disconnect_node, node}, state) do
Node.disconnect(node)
{:noreply, report_connected_nodes(state)}
end
@impl true
def handle_call({:read_file, path}, {from_pid, _}, state) do
# Delegate reading to a separate task and let the caller
@ -817,6 +827,12 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
send(state.owner, {:runtime_memory_usage, Evaluator.memory()})
end
defp report_environment(state) do
state
|> report_smart_cell_definitions()
|> report_connected_nodes()
end
defp report_smart_cell_definitions(state) do
smart_cell_definitions = get_smart_cell_definitions(state.smart_cell_definitions_module)
@ -837,6 +853,19 @@ defmodule Livebook.Runtime.ErlDist.RuntimeServer do
end
end
defp report_connected_nodes(state) do
owner_node = node(state.owner)
nodes = Node.list(:connected) |> List.delete(owner_node) |> Enum.sort()
if nodes == state.connected_nodes do
state
else
send(state.owner, {:runtime_connected_nodes, nodes})
%{state | connected_nodes: nodes}
end
end
defp get_smart_cell_definitions(module) do
if Code.ensure_loaded?(module) and function_exported?(module, :definitions, 0) do
module.definitions()

View file

@ -1735,6 +1735,11 @@ defmodule Livebook.Session do
{:noreply, state |> put_memory_usage(runtime_memory) |> notify_update()}
end
def handle_info({:runtime_connected_nodes, nodes}, state) do
operation = {:set_runtime_connected_nodes, @client_id, nodes}
{:noreply, handle_operation(state, operation)}
end
def handle_info({:runtime_smart_cell_definitions, definitions}, state) do
operation = {:set_smart_cell_definitions, @client_id, definitions}
{:noreply, handle_operation(state, operation)}

View file

@ -28,6 +28,7 @@ defmodule Livebook.Session.Data do
:bin_entries,
:runtime,
:runtime_transient_state,
:runtime_connected_nodes,
:smart_cell_definitions,
:clients_map,
:users_map,
@ -55,6 +56,7 @@ defmodule Livebook.Session.Data do
bin_entries: list(cell_bin_entry()),
runtime: Runtime.t(),
runtime_transient_state: Runtime.transient_state(),
runtime_connected_nodes: list(node()),
smart_cell_definitions: list(Runtime.smart_cell_definition()),
clients_map: %{client_id() => User.id()},
users_map: %{User.id() => User.t()},
@ -214,6 +216,7 @@ defmodule Livebook.Session.Data do
| {:set_input_value, client_id(), input_id(), value :: term()}
| {:set_runtime, client_id(), Runtime.t()}
| {:set_runtime_transient_state, client_id(), Runtime.transient_state()}
| {:set_runtime_connected_nodes, client_id(), list(node())}
| {:set_smart_cell_definitions, client_id(), list(Runtime.smart_cell_definition())}
| {:set_file, client_id(), FileSystem.File.t() | nil}
| {:set_autosave_interval, client_id(), non_neg_integer() | nil}
@ -303,6 +306,7 @@ defmodule Livebook.Session.Data do
bin_entries: [],
runtime: default_runtime,
runtime_transient_state: %{},
runtime_connected_nodes: [],
smart_cell_definitions: [],
clients_map: %{},
users_map: %{},
@ -871,6 +875,13 @@ defmodule Livebook.Session.Data do
|> wrap_ok()
end
def apply_operation(data, {:set_runtime_connected_nodes, _client_id, nodes}) do
data
|> with_actions()
|> set_runtime_connected_nodes(nodes)
|> wrap_ok()
end
def apply_operation(data, {:set_smart_cell_definitions, _client_id, definitions}) do
data
|> with_actions()
@ -1955,7 +1966,13 @@ defmodule Livebook.Session.Data do
end
defp set_runtime(data_actions, prev_data, runtime) do
{data, _} = data_actions = set!(data_actions, runtime: runtime, smart_cell_definitions: [])
{data, _} =
data_actions =
set!(data_actions,
runtime: runtime,
runtime_connected_nodes: [],
smart_cell_definitions: []
)
if not Runtime.connected?(prev_data.runtime) and Runtime.connected?(data.runtime) do
data_actions
@ -2006,6 +2023,12 @@ defmodule Livebook.Session.Data do
end
end
defp set_runtime_connected_nodes(data_actions, nodes) do
data_actions
|> set!(runtime_connected_nodes: nodes)
|> maybe_start_smart_cells()
end
defp set_smart_cell_definitions(data_actions, smart_cell_definitions) do
data_actions
|> set!(smart_cell_definitions: smart_cell_definitions)

View file

@ -539,9 +539,11 @@ defmodule LivebookWeb.CoreComponents do
<span class="text-sm text-gray-500">
<%= @label %>
</span>
<span class={
"text-gray-800 text-sm font-semibold #{if @one_line, do: "whitespace-nowrap overflow-auto tiny-scrollbar"}"
}>
<span class={[
"text-gray-800 text-sm font-semibold",
@one_line &&
"whitespace-nowrap overflow-hidden text-ellipsis hover:text-clip hover:overflow-auto hover:tiny-scrollbar"
]}>
<%= render_slot(@inner_block) %>
</span>
</div>

View file

@ -578,6 +578,17 @@ defmodule LivebookWeb.SessionLive do
{:noreply, socket}
end
def handle_event("runtime_disconnect_node", %{"node" => node}, socket) do
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
Runtime.disconnect_node(runtime, node)
end
{:noreply, socket}
end
def handle_event("deploy_app", _, socket) do
data = socket.private.data
app_settings = data.notebook.app_settings
@ -1778,6 +1789,7 @@ defmodule LivebookWeb.SessionLive do
dirty: data.dirty,
persistence_warnings: data.persistence_warnings,
runtime: data.runtime,
runtime_connected_nodes: Enum.sort(data.runtime_connected_nodes),
smart_cell_definitions: Enum.sort_by(data.smart_cell_definitions, & &1.name),
example_snippet_definitions:
data.runtime

View file

@ -18,7 +18,12 @@ defmodule LivebookWeb.SessionLive.Render do
data-p-global-status={hook_prop(elem(@data_view.global_status, 0))}
data-p-autofocus-cell-id={hook_prop(@autofocus_cell_id)}
>
<.sidebar app={@app} session={@session} live_action={@live_action} current_user={@current_user} />
<.sidebar
session={@session}
live_action={@live_action}
current_user={@current_user}
runtime_connected_nodes={@data_view.runtime_connected_nodes}
/>
<.side_panel app={@app} session={@session} data_view={@data_view} client_id={@client_id} />
<div class="grow overflow-y-auto relative" data-el-notebook>
<div data-el-js-view-iframes phx-update="ignore" id="js-view-iframes"></div>
@ -333,11 +338,21 @@ defmodule LivebookWeb.SessionLive.Render do
button_attrs={["data-el-clients-list-toggle": true]}
/>
<.button_item
icon="cpu-line"
label="Runtime settings (sr)"
button_attrs={["data-el-runtime-info-toggle": true]}
/>
<div class="relative">
<.button_item
icon="cpu-line"
label="Runtime settings (sr)"
button_attrs={["data-el-runtime-info-toggle": true]}
/>
<div
:if={@runtime_connected_nodes != []}
data-el-runtime-indicator
class={[
"absolute w-[12px] h-[12px] border-gray-900 border-2 rounded-full right-1.5 top-1.5 pointer-events-none",
"bg-blue-500"
]}
/>
</div>
<%!-- Hub functionality --%>
@ -626,7 +641,7 @@ defmodule LivebookWeb.SessionLive.Render do
</.icon_button>
</span>
</div>
<div class="flex flex-col mt-2 space-y-4">
<div class="flex flex-col mt-2">
<div class="flex flex-col space-y-3">
<.labeled_text
:for={{label, value} <- Runtime.describe(@data_view.runtime)}
@ -636,7 +651,7 @@ defmodule LivebookWeb.SessionLive.Render do
<%= value %>
</.labeled_text>
</div>
<div class="grid grid-cols-2 gap-2">
<div class="mt-4 grid grid-cols-2 gap-2">
<%= if Runtime.connected?(@data_view.runtime) do %>
<.button phx-click="reconnect_runtime">
<.remix_icon icon="wireless-charging-line" />
@ -651,21 +666,6 @@ defmodule LivebookWeb.SessionLive.Render do
<.button color="gray" outlined patch={~p"/sessions/#{@session.id}/settings/runtime"}>
Configure
</.button>
</div>
<div class="flex flex-col pt-6 space-y-2">
<%= if uses_memory?(@session.memory_usage) do %>
<.memory_info memory_usage={@session.memory_usage} />
<% else %>
<div class="text-sm text-gray-800 flex flex-col">
<span class="w-full uppercase font-semibold text-gray-500">Memory</span>
<p class="py-1">
<%= format_bytes(@session.memory_usage.system.free) %> available out of <%= format_bytes(
@session.memory_usage.system.total
) %>
</p>
</div>
<% end %>
<.button
:if={Runtime.connected?(@data_view.runtime)}
@ -673,27 +673,50 @@ defmodule LivebookWeb.SessionLive.Render do
outlined
type="button"
phx-click="disconnect_runtime"
class="col-span-2"
>
Disconnect
</.button>
</div>
<.memory_usage_info memory_usage={@session.memory_usage} />
<.runtime_connected_nodes_info runtime_connected_nodes={@data_view.runtime_connected_nodes} />
</div>
</div>
"""
end
defp memory_info(assigns) do
assigns = assign(assigns, :runtime_memory, runtime_memory(assigns.memory_usage))
defp memory_usage_info(assigns) do
~H"""
<div class="flex flex-col justify-center">
<div class="mb-1 text-sm text-gray-800 flex flex-row justify-between">
<span class="text-gray-500 font-semibold uppercase">Memory</span>
<span class="text-right">
<div class="mt-8 flex flex-col gap-2">
<div class="text-sm text-gray-800 flex flex-row justify-between">
<span class="text-gray-500 font-semibold uppercase">
Memory
</span>
<span :if={uses_memory?(@memory_usage)}>
<%= format_bytes(@memory_usage.system.free) %> available
</span>
</div>
<div class="w-full h-8 flex flex-row py-1 gap-0.5">
<%= if uses_memory?(@memory_usage) do %>
<.runtime_memory_info memory_usage={@memory_usage} />
<% else %>
<p class="text-sm text-gray-800">
<%= format_bytes(@memory_usage.system.free) %> available out of <%= format_bytes(
@memory_usage.system.total
) %>
</p>
<% end %>
</div>
"""
end
defp runtime_memory_info(assigns) do
assigns = assign(assigns, :runtime_memory, runtime_memory(assigns.memory_usage))
~H"""
<div class="flex flex-col gap-2">
<div class="w-full flex flex-row gap-0.5">
<div
:for={{type, memory} <- @runtime_memory}
class={["h-6", memory_color(type)]}
@ -701,15 +724,15 @@ defmodule LivebookWeb.SessionLive.Render do
>
</div>
</div>
<div class="flex flex-col py-1">
<div class="flex flex-col">
<div :for={{type, memory} <- @runtime_memory} class="flex flex-row items-center">
<span class={["w-4 h-4 mr-2 rounded", memory_color(type)]}></span>
<span class="capitalize text-gray-700"><%= type %></span>
<span class="text-gray-500 ml-auto"><%= memory.unit %></span>
</div>
<div class="flex rounded justify-center my-2 py-0.5 text-sm text-gray-800 bg-gray-200">
Total: <%= format_bytes(@memory_usage.runtime.total) %>
</div>
</div>
<div class="flex rounded justify-center py-0.5 text-sm text-gray-800 bg-gray-200">
Total: <%= format_bytes(@memory_usage.runtime.total) %>
</div>
</div>
"""
@ -735,6 +758,41 @@ defmodule LivebookWeb.SessionLive.Render do
end)
end
defp runtime_connected_nodes_info(assigns) do
~H"""
<div class="mt-8 flex flex-col gap-2">
<span class="text-sm text-gray-500 font-semibold uppercase">
Connected nodes
</span>
<%= if @runtime_connected_nodes == [] do %>
<div class="text-sm text-gray-800 flex flex-col">
No connected nodes
</div>
<% else %>
<div class="flex flex-col">
<div
:for={node <- @runtime_connected_nodes}
class="flex flex-nowrap items-baseline py-1 pl-2 -ml-2 pr-1 hover:bg-gray-100 group rounded-lg"
>
<.remix_icon icon="circle-fill" class="mr-2 text-xs text-blue-500" />
<div class={[
"flex-grow text-sm text-gray-700 text-medium whitespace-nowrap text-ellipsis overflow-hidden",
"group-hover:overflow-visible group-hover:whitespace-normal group-hover:break-all"
]}>
<%= node %>
</div>
<span class="tooltip left" data-tooltip="Disconnect">
<.icon_button phx-click="runtime_disconnect_node" phx-value-node={node} small>
<.remix_icon icon="close-line" />
</.icon_button>
</span>
</div>
</div>
<% end %>
</div>
"""
end
defp section_status(%{status: :evaluating} = assigns) do
~H"""
<button data-el-focus-cell-button data-target={@cell_id}>

View file

@ -3836,6 +3836,23 @@ defmodule Livebook.Session.DataTest do
assert new_data.section_infos["setup-section"].evaluation_queue == MapSet.new([])
end
test "clears runtime-related state" do
data =
data_after_operations!([
{:set_smart_cell_definitions, @cid, @smart_cell_definitions},
{:set_runtime_connected_nodes, @cid, [:node@host]}
])
runtime = connected_noop_runtime()
operation = {:set_runtime, @cid, runtime}
assert {:ok,
%{
smart_cell_definitions: [],
runtime_connected_nodes: []
}, []} = Data.apply_operation(data, operation)
end
end
describe "apply_operation/2 given :set_runtime_transient_state" do

View file

@ -930,6 +930,32 @@ defmodule LivebookWeb.SessionLiveTest do
assert page =~ "Reconnect"
assert page =~ "Disconnect"
end
test "disconnecting a connected node", %{conn: conn, session: session} do
{:ok, runtime} = Livebook.Runtime.NoopRuntime.new(self()) |> Livebook.Runtime.connect()
Session.set_runtime(session.pid, runtime)
{:ok, view, _} = live(conn, ~p"/sessions/#{session.id}")
Session.subscribe(session.id)
assert render(view) =~ "No connected nodes"
# Mimic the runtime reporting a connected node
node = :node@host
send(session.pid, {:runtime_connected_nodes, [node]})
assert_receive {:operation, {:set_runtime_connected_nodes, _pid, _nodes}}
refute render(view) =~ "No connected nodes"
assert render(view) =~ "#{node}"
view
|> element(~s{button[phx-click="runtime_disconnect_node"]})
|> render_click()
assert_receive {:runtime_trace, :disconnect_node, [^node]}
end
end
describe "persistence settings" do

View file

@ -75,6 +75,11 @@ defmodule Livebook.Runtime.NoopRuntime do
def unregister_clients(_, _), do: :ok
def fetch_proxy_handler_spec(_), do: {:error, :not_found}
def disconnect_node(runtime, node) do
trace(runtime, :disconnect_node, [node])
:ok
end
defp trace(runtime, fun, args) do
if runtime.trace_to do
send(runtime.trace_to, {:runtime_trace, fun, args})