Warn if session is missing after reboot (#2128)

Closes #2075.
This commit is contained in:
José Valim 2023-08-01 19:12:38 +02:00 committed by GitHub
parent c404e817fb
commit 7c40ab22e3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 68 additions and 23 deletions

View file

@ -86,6 +86,8 @@ defmodule Livebook do
def config_runtime do def config_runtime do
import Config import Config
config :livebook, :random_boot_id, :crypto.strong_rand_bytes(3)
config :livebook, LivebookWeb.Endpoint, config :livebook, LivebookWeb.Endpoint,
secret_key_base: secret_key_base:
Livebook.Config.secret!("LIVEBOOK_SECRET_KEY_BASE") || Livebook.Config.secret!("LIVEBOOK_SECRET_KEY_BASE") ||

View file

@ -305,6 +305,13 @@ defmodule Livebook.Config do
Application.fetch_env!(:livebook, :allowed_uri_schemes) Application.fetch_env!(:livebook, :allowed_uri_schemes)
end end
@doc """
Returns a random id set on boot.
"""
def random_boot_id() do
Application.fetch_env!(:livebook, :random_boot_id)
end
## Parsing ## Parsing
@doc """ @doc """

View file

@ -50,21 +50,32 @@ defmodule Livebook.Sessions do
@doc """ @doc """
Returns tracked session with the given id. Returns tracked session with the given id.
""" """
@spec fetch_session(Session.id()) :: {:ok, Session.t()} | :error @spec fetch_session(Session.id()) ::
{:ok, Session.t()} | {:error, :not_found | :different_boot_id}
def fetch_session(id) do def fetch_session(id) do
case Livebook.Tracker.fetch_session(id) do case Livebook.Tracker.fetch_session(id) do
{:ok, session} -> {:ok, session} ->
{:ok, session} {:ok, session}
:error -> :error ->
boot_id = Livebook.Config.random_boot_id()
case Utils.node_from_node_aware_id(id) do
# The local tracker server doesn't know about this session, # The local tracker server doesn't know about this session,
# but it may not have propagated yet, so we extract the session # but it may not have propagated yet, so we extract the session
# node from id and ask the corresponding tracker directly # node from id and ask the corresponding tracker directly
with {:ok, other_node} when other_node != node() <- Utils.node_from_node_aware_id(id), {:ok, other_node, _other_boot_id} when other_node != node() ->
{:ok, session} <- :rpc.call(other_node, Livebook.Tracker, :fetch_session, [id]) do case :rpc.call(other_node, Livebook.Tracker, :fetch_session, [id]) do
{:ok, session} {:ok, session} -> {:ok, session}
else _ -> {:error, :not_found}
_ -> :error end
{:ok, other_node, other_boot_id}
when other_node == node() and other_boot_id != boot_id ->
{:error, :different_boot_id}
_ ->
{:error, :not_found}
end end
end end
end end

View file

@ -36,6 +36,7 @@ defmodule Livebook.Utils do
The id is formed from the following binary parts: The id is formed from the following binary parts:
* 3B - random boot id
* 16B - hashed node name * 16B - hashed node name
* 9B - random bytes * 9B - random bytes
@ -43,10 +44,11 @@ defmodule Livebook.Utils do
""" """
@spec random_node_aware_id() :: id() @spec random_node_aware_id() :: id()
def random_node_aware_id() do def random_node_aware_id() do
boot_id = Livebook.Config.random_boot_id()
node_part = node_hash(node()) node_part = node_hash(node())
random_part = :crypto.strong_rand_bytes(9) random_part = :crypto.strong_rand_bytes(11)
binary = <<node_part::binary, random_part::binary>> binary = <<boot_id::binary, node_part::binary, random_part::binary>>
# 16B + 9B = 25B is suitable for base32 encoding without padding # 3B + 16B + 11B = 30B is suitable for base32 encoding without padding
Base.encode32(binary, case: :lower) Base.encode32(binary, case: :lower)
end end
@ -61,16 +63,20 @@ defmodule Livebook.Utils do
The node in question must be connected, otherwise it won't be found. The node in question must be connected, otherwise it won't be found.
""" """
@spec node_from_node_aware_id(id()) :: {:ok, node()} | :error @spec node_from_node_aware_id(id()) :: {:ok, node(), boot_id :: binary()} | :error
def node_from_node_aware_id(id) do def node_from_node_aware_id(id) do
binary = Base.decode32!(id, case: :lower) case Base.decode32(id, case: :lower) do
<<node_part::binary-size(16), _random_part::binary-size(9)>> = binary {:ok,
<<boot_id::binary-size(3), node_part::binary-size(16), _random_part::binary-size(11)>>} ->
known_nodes = [node() | Node.list()] known_nodes = [node() | Node.list()]
Enum.find_value(known_nodes, :error, fn node -> Enum.find_value(known_nodes, :error, fn node ->
node_hash(node) == node_part && {:ok, node} node_hash(node) == node_part && {:ok, node, boot_id}
end) end)
_ ->
:error
end
end end
@doc """ @doc """

View file

@ -34,7 +34,7 @@ defmodule LivebookWeb.SessionController do
file = FileSystem.File.resolve(images_dir, name) file = FileSystem.File.resolve(images_dir, name)
serve_static(conn, file) serve_static(conn, file)
:error -> {:error, _} ->
send_resp(conn, 404, "Not found") send_resp(conn, 404, "Not found")
end end
end end
@ -57,7 +57,7 @@ defmodule LivebookWeb.SessionController do
send_notebook_source(conn, notebook, file_name, format) send_notebook_source(conn, notebook, file_name, format)
:error -> {:error, _} ->
send_resp(conn, 404, "Not found") send_resp(conn, 404, "Not found")
end end
end end

View file

@ -94,8 +94,18 @@ defmodule LivebookWeb.SessionLive do
|> prune_outputs() |> prune_outputs()
|> prune_cell_sources()} |> prune_cell_sources()}
:error -> {:error, :not_found} ->
{:ok, redirect(socket, to: ~p"/")} {:ok, redirect(socket, to: ~p"/")}
{:error, :different_boot_id} ->
{:ok,
socket
|> put_flash(
:error,
"Could not find notebook session because Livebook has rebooted. " <>
"This may happen if Livebook runs out of memory while installing dependencies or executing code."
)
|> redirect(to: ~p"/")}
end end
end end

View file

@ -32,7 +32,7 @@ defmodule Livebook.SessionsTest do
describe "fetch_session/1" do describe "fetch_session/1" do
test "returns an error if no session with the given id exists" do test "returns an error if no session with the given id exists" do
id = Livebook.Utils.random_node_aware_id() id = Livebook.Utils.random_node_aware_id()
assert :error = Sessions.fetch_session(id) assert Sessions.fetch_session(id) == {:error, :not_found}
end end
test "returns session matching the given id" do test "returns session matching the given id" do
@ -41,6 +41,15 @@ defmodule Livebook.SessionsTest do
Session.close(session.pid) Session.close(session.pid)
end end
test "returns an error if session comes from a different boot" do
Application.put_env(:livebook, :random_boot_id, "aaa")
id = Livebook.Utils.random_node_aware_id()
assert Sessions.fetch_session(id) == {:error, :not_found}
Application.put_env(:livebook, :random_boot_id, "bbb")
assert Sessions.fetch_session(id) == {:error, :different_boot_id}
end
end end
describe "update_session/1" do describe "update_session/1" do