diff --git a/lib/livebook/application.ex b/lib/livebook/application.ex index fa617de5f..96504772a 100644 --- a/lib/livebook/application.ex +++ b/lib/livebook/application.ex @@ -15,8 +15,10 @@ defmodule Livebook.Application do LivebookWeb.Telemetry, # Start the PubSub system {Phoenix.PubSub, name: Livebook.PubSub}, + # Start the tracker server on this node + {Livebook.Tracker, pubsub_server: Livebook.PubSub}, # Start the supervisor dynamically managing sessions - Livebook.SessionSupervisor, + {DynamicSupervisor, name: Livebook.SessionSupervisor, strategy: :one_for_one}, # Start the server responsible for associating files with sessions Livebook.Session.FileGuard, # Start the Node Pool for managing node names diff --git a/lib/livebook/session.ex b/lib/livebook/session.ex index 475f7404a..d5dd76eb3 100644 --- a/lib/livebook/session.ex +++ b/lib/livebook/session.ex @@ -43,6 +43,11 @@ defmodule Livebook.Session do # for a single specific evaluation context we make sure to copy # as little memory as necessary. + # The struct holds the basic session information that we track + # and pass around. The notebook and evaluation state is kept + # within the process state. + defstruct [:id, :pid, :origin, :notebook_name, :file, :images_dir] + use GenServer, restart: :temporary alias Livebook.Session.{Data, FileGuard} @@ -50,6 +55,15 @@ defmodule Livebook.Session do alias Livebook.Users.User alias Livebook.Notebook.{Cell, Section} + @type t :: %__MODULE__{ + id: id(), + pid: pid(), + origin: {:file, FileSystem.File.t()} | {:url, String.t()} | nil, + notebook_name: String.t(), + file: FileSystem.File.t() | nil, + images_dir: FileSystem.File.t() + } + @type state :: %{ session_id: id(), data: Data.t(), @@ -58,15 +72,6 @@ defmodule Livebook.Session do save_task_pid: pid() | nil } - @type summary :: %{ - session_id: id(), - pid: pid(), - notebook_name: String.t(), - file: FileSystem.File.t() | nil, - images_dir: FileSystem.File.t(), - origin: {:file, FileSystem.File.t()} | {:url, String.t()} | nil - } - @typedoc """ An id assigned to every running session process. """ @@ -75,12 +80,11 @@ defmodule Livebook.Session do ## API @doc """ - Starts the server process and registers it globally using the `:global` module, - so that it's identifiable by the given id. + Starts a session server process. ## Options - * `:id` (**required**) - a unique identifier to register the session under + * `:id` (**required**) - a unique session identifier * `:notebook` - the initial `Notebook` structure (e.g. imported from a file) @@ -94,22 +98,17 @@ defmodule Livebook.Session do * `:images` - a map from image name to its binary content, an alternative to `:copy_images_from` when the images are in memory """ - @spec start_link(keyword()) :: GenServer.on_start() + @spec start_link(keyword()) :: {:ok, pid} | {:error, any()} def start_link(opts) do - id = Keyword.fetch!(opts, :id) - GenServer.start_link(__MODULE__, opts, name: name(id)) - end - - defp name(session_id) do - {:global, {:session, session_id}} + GenServer.start_link(__MODULE__, opts) end @doc """ - Returns session pid given its id. + Fetches session information from the session server. """ - @spec get_pid(id()) :: pid() | nil - def get_pid(session_id) do - GenServer.whereis(name(session_id)) + @spec get_by_pid(pid()) :: Session.t() + def get_by_pid(pid) do + GenServer.call(pid, :describe_self) end @doc """ @@ -121,162 +120,153 @@ defmodule Livebook.Session do keep in sync with the server by subscribing to the `sessions:id` topic and receiving operations to apply. """ - @spec register_client(id(), pid(), User.t()) :: Data.t() - def register_client(session_id, client_pid, user) do - GenServer.call(name(session_id), {:register_client, client_pid, user}) + @spec register_client(pid(), pid(), User.t()) :: Data.t() + def register_client(pid, client_pid, user) do + GenServer.call(pid, {:register_client, client_pid, user}) end @doc """ Returns data of the given session. """ - @spec get_data(id()) :: Data.t() - def get_data(session_id) do - GenServer.call(name(session_id), :get_data) - end - - @doc """ - Returns basic information about the given session. - """ - @spec get_summary(id()) :: summary() - def get_summary(session_id) do - GenServer.call(name(session_id), :get_summary) + @spec get_data(pid()) :: Data.t() + def get_data(pid) do + GenServer.call(pid, :get_data) end @doc """ Returns the current notebook structure. """ - @spec get_notebook(id()) :: Notebook.t() - def get_notebook(session_id) do - GenServer.call(name(session_id), :get_notebook) + @spec get_notebook(pid()) :: Notebook.t() + def get_notebook(pid) do + GenServer.call(pid, :get_notebook) end @doc """ Asynchronously sends notebook attributes update to the server. """ - @spec set_notebook_attributes(id(), map()) :: :ok - def set_notebook_attributes(session_id, attrs) do - GenServer.cast(name(session_id), {:set_notebook_attributes, self(), attrs}) + @spec set_notebook_attributes(pid(), map()) :: :ok + def set_notebook_attributes(pid, attrs) do + GenServer.cast(pid, {:set_notebook_attributes, self(), attrs}) end @doc """ Asynchronously sends section insertion request to the server. """ - @spec insert_section(id(), non_neg_integer()) :: :ok - def insert_section(session_id, index) do - GenServer.cast(name(session_id), {:insert_section, self(), index}) + @spec insert_section(pid(), non_neg_integer()) :: :ok + def insert_section(pid, index) do + GenServer.cast(pid, {:insert_section, self(), index}) end @doc """ Asynchronously sends section insertion request to the server. """ - @spec insert_section_into(id(), Section.id(), non_neg_integer()) :: :ok - def insert_section_into(session_id, section_id, index) do - GenServer.cast(name(session_id), {:insert_section_into, self(), section_id, index}) + @spec insert_section_into(pid(), Section.id(), non_neg_integer()) :: :ok + def insert_section_into(pid, section_id, index) do + GenServer.cast(pid, {:insert_section_into, self(), section_id, index}) end @doc """ Asynchronously sends parent update request to the server. """ - @spec set_section_parent(id(), Section.id(), Section.id()) :: :ok - def set_section_parent(session_id, section_id, parent_id) do - GenServer.cast(name(session_id), {:set_section_parent, self(), section_id, parent_id}) + @spec set_section_parent(pid(), Section.id(), Section.id()) :: :ok + def set_section_parent(pid, section_id, parent_id) do + GenServer.cast(pid, {:set_section_parent, self(), section_id, parent_id}) end @doc """ Asynchronously sends parent update request to the server. """ - @spec unset_section_parent(id(), Section.id()) :: :ok - def unset_section_parent(session_id, section_id) do - GenServer.cast(name(session_id), {:unset_section_parent, self(), section_id}) + @spec unset_section_parent(pid(), Section.id()) :: :ok + def unset_section_parent(pid, section_id) do + GenServer.cast(pid, {:unset_section_parent, self(), section_id}) end @doc """ Asynchronously sends cell insertion request to the server. """ - @spec insert_cell(id(), Section.id(), non_neg_integer(), Cell.type()) :: - :ok - def insert_cell(session_id, section_id, index, type) do - GenServer.cast(name(session_id), {:insert_cell, self(), section_id, index, type}) + @spec insert_cell(pid(), Section.id(), non_neg_integer(), Cell.type()) :: :ok + def insert_cell(pid, section_id, index, type) do + GenServer.cast(pid, {:insert_cell, self(), section_id, index, type}) end @doc """ Asynchronously sends section deletion request to the server. """ - @spec delete_section(id(), Section.id(), boolean()) :: :ok - def delete_section(session_id, section_id, delete_cells) do - GenServer.cast(name(session_id), {:delete_section, self(), section_id, delete_cells}) + @spec delete_section(pid(), Section.id(), boolean()) :: :ok + def delete_section(pid, section_id, delete_cells) do + GenServer.cast(pid, {:delete_section, self(), section_id, delete_cells}) end @doc """ Asynchronously sends cell deletion request to the server. """ - @spec delete_cell(id(), Cell.id()) :: :ok - def delete_cell(session_id, cell_id) do - GenServer.cast(name(session_id), {:delete_cell, self(), cell_id}) + @spec delete_cell(pid(), Cell.id()) :: :ok + def delete_cell(pid, cell_id) do + GenServer.cast(pid, {:delete_cell, self(), cell_id}) end @doc """ Asynchronously sends cell restoration request to the server. """ - @spec restore_cell(id(), Cell.id()) :: :ok - def restore_cell(session_id, cell_id) do - GenServer.cast(name(session_id), {:restore_cell, self(), cell_id}) + @spec restore_cell(pid(), Cell.id()) :: :ok + def restore_cell(pid, cell_id) do + GenServer.cast(pid, {:restore_cell, self(), cell_id}) end @doc """ Asynchronously sends cell move request to the server. """ - @spec move_cell(id(), Cell.id(), integer()) :: :ok - def move_cell(session_id, cell_id, offset) do - GenServer.cast(name(session_id), {:move_cell, self(), cell_id, offset}) + @spec move_cell(pid(), Cell.id(), integer()) :: :ok + def move_cell(pid, cell_id, offset) do + GenServer.cast(pid, {:move_cell, self(), cell_id, offset}) end @doc """ Asynchronously sends section move request to the server. """ - @spec move_section(id(), Section.id(), integer()) :: :ok - def move_section(session_id, section_id, offset) do - GenServer.cast(name(session_id), {:move_section, self(), section_id, offset}) + @spec move_section(pid(), Section.id(), integer()) :: :ok + def move_section(pid, section_id, offset) do + GenServer.cast(pid, {:move_section, self(), section_id, offset}) end @doc """ Asynchronously sends cell evaluation request to the server. """ - @spec queue_cell_evaluation(id(), Cell.id()) :: :ok - def queue_cell_evaluation(session_id, cell_id) do - GenServer.cast(name(session_id), {:queue_cell_evaluation, self(), cell_id}) + @spec queue_cell_evaluation(pid(), Cell.id()) :: :ok + def queue_cell_evaluation(pid, cell_id) do + GenServer.cast(pid, {:queue_cell_evaluation, self(), cell_id}) end @doc """ Asynchronously sends cell evaluation cancellation request to the server. """ - @spec cancel_cell_evaluation(id(), Cell.id()) :: :ok - def cancel_cell_evaluation(session_id, cell_id) do - GenServer.cast(name(session_id), {:cancel_cell_evaluation, self(), cell_id}) + @spec cancel_cell_evaluation(pid(), Cell.id()) :: :ok + def cancel_cell_evaluation(pid, cell_id) do + GenServer.cast(pid, {:cancel_cell_evaluation, self(), cell_id}) end @doc """ Asynchronously sends notebook name update request to the server. """ - @spec set_notebook_name(id(), String.t()) :: :ok - def set_notebook_name(session_id, name) do - GenServer.cast(name(session_id), {:set_notebook_name, self(), name}) + @spec set_notebook_name(pid(), String.t()) :: :ok + def set_notebook_name(pid, name) do + GenServer.cast(pid, {:set_notebook_name, self(), name}) end @doc """ Asynchronously sends section name update request to the server. """ - @spec set_section_name(id(), Section.id(), String.t()) :: :ok - def set_section_name(session_id, section_id, name) do - GenServer.cast(name(session_id), {:set_section_name, self(), section_id, name}) + @spec set_section_name(pid(), Section.id(), String.t()) :: :ok + def set_section_name(pid, section_id, name) do + GenServer.cast(pid, {:set_section_name, self(), section_id, name}) end @doc """ Asynchronously sends a cell delta to apply to the server. """ - @spec apply_cell_delta(id(), Cell.id(), Delta.t(), Data.cell_revision()) :: :ok - def apply_cell_delta(session_id, cell_id, delta, revision) do - GenServer.cast(name(session_id), {:apply_cell_delta, self(), cell_id, delta, revision}) + @spec apply_cell_delta(pid(), Cell.id(), Delta.t(), Data.cell_revision()) :: :ok + def apply_cell_delta(pid, cell_id, delta, revision) do + GenServer.cast(pid, {:apply_cell_delta, self(), cell_id, delta, revision}) end @doc """ @@ -284,17 +274,17 @@ defmodule Livebook.Session do This helps to remove old deltas that are no longer necessary. """ - @spec report_cell_revision(id(), Cell.id(), Data.cell_revision()) :: :ok - def report_cell_revision(session_id, cell_id, revision) do - GenServer.cast(name(session_id), {:report_cell_revision, self(), cell_id, revision}) + @spec report_cell_revision(pid(), Cell.id(), Data.cell_revision()) :: :ok + def report_cell_revision(pid, cell_id, revision) do + GenServer.cast(pid, {:report_cell_revision, self(), cell_id, revision}) end @doc """ Asynchronously sends a cell attributes update to the server. """ - @spec set_cell_attributes(id(), Cell.id(), map()) :: :ok - def set_cell_attributes(session_id, cell_id, attrs) do - GenServer.cast(name(session_id), {:set_cell_attributes, self(), cell_id, attrs}) + @spec set_cell_attributes(pid(), Cell.id(), map()) :: :ok + def set_cell_attributes(pid, cell_id, attrs) do + GenServer.cast(pid, {:set_cell_attributes, self(), cell_id, attrs}) end @doc """ @@ -303,9 +293,9 @@ defmodule Livebook.Session do Note that this results in initializing the corresponding remote node with modules and processes required for evaluation. """ - @spec connect_runtime(id(), Runtime.t()) :: :ok - def connect_runtime(session_id, runtime) do - GenServer.cast(name(session_id), {:connect_runtime, self(), runtime}) + @spec connect_runtime(pid(), Runtime.t()) :: :ok + def connect_runtime(pid, runtime) do + GenServer.cast(pid, {:connect_runtime, self(), runtime}) end @doc """ @@ -313,17 +303,17 @@ defmodule Livebook.Session do Note that this results in clearing the evaluation state. """ - @spec disconnect_runtime(id()) :: :ok - def disconnect_runtime(session_id) do - GenServer.cast(name(session_id), {:disconnect_runtime, self()}) + @spec disconnect_runtime(pid()) :: :ok + def disconnect_runtime(pid) do + GenServer.cast(pid, {:disconnect_runtime, self()}) end @doc """ Asynchronously sends file location update request to the server. """ - @spec set_file(id(), FileSystem.File.t() | nil) :: :ok - def set_file(session_id, file) do - GenServer.cast(name(session_id), {:set_file, self(), file}) + @spec set_file(pid(), FileSystem.File.t() | nil) :: :ok + def set_file(pid, file) do + GenServer.cast(pid, {:set_file, self(), file}) end @doc """ @@ -334,17 +324,17 @@ defmodule Livebook.Session do Note that notebooks are automatically persisted every @autosave_interval milliseconds. """ - @spec save(id()) :: :ok - def save(session_id) do - GenServer.cast(name(session_id), :save) + @spec save(pid()) :: :ok + def save(pid) do + GenServer.cast(pid, :save) end @doc """ Synchronous version of `save/1`. """ - @spec save_sync(id()) :: :ok - def save_sync(session_id) do - GenServer.call(name(session_id), :save_sync) + @spec save_sync(pid()) :: :ok + def save_sync(pid) do + GenServer.call(pid, :save_sync) end @doc """ @@ -353,9 +343,9 @@ defmodule Livebook.Session do This results in saving the file and broadcasting a :closed message to the session topic. """ - @spec close(id()) :: :ok - def close(session_id) do - GenServer.cast(name(session_id), :close) + @spec close(pid()) :: :ok + def close(pid) do + GenServer.cast(pid, :close) end ## Callbacks @@ -432,6 +422,10 @@ defmodule Livebook.Session do end @impl true + def handle_call(:describe_self, _from, state) do + {:reply, self_from_state(state), state} + end + def handle_call({:register_client, client_pid, user}, _from, state) do Process.monitor(client_pid) @@ -444,10 +438,6 @@ defmodule Livebook.Session do {:reply, state.data, state} end - def handle_call(:get_summary, _from, state) do - {:reply, summary_from_state(state), state} - end - def handle_call(:get_notebook, _from, state) do {:reply, state.data.notebook, state} end @@ -694,14 +684,14 @@ defmodule Livebook.Session do # --- - defp summary_from_state(state) do - %{ - session_id: state.session_id, + defp self_from_state(state) do + %__MODULE__{ + id: state.session_id, pid: self(), + origin: state.data.origin, notebook_name: state.data.notebook.name, file: state.data.file, - images_dir: images_dir_from_state(state), - origin: state.data.origin + images_dir: images_dir_from_state(state) } end @@ -807,6 +797,11 @@ defmodule Livebook.Session do end end + defp after_operation(state, _prev_state, {:set_notebook_name, _pid, _name}) do + notify_update(state) + state + end + defp after_operation(state, prev_state, {:set_file, _pid, _file}) do prev_images_dir = images_dir_from_state(prev_state) @@ -823,6 +818,8 @@ defmodule Livebook.Session do broadcast_error(state.session_id, "failed to copy images - #{message}") end + notify_update(state) + state end @@ -948,6 +945,12 @@ defmodule Livebook.Session do Phoenix.PubSub.broadcast(Livebook.PubSub, "sessions:#{session_id}", message) end + defp notify_update(state) do + session = self_from_state(state) + Livebook.Sessions.update_session(session) + broadcast_message(state.session_id, {:session_updated, session}) + end + defp maybe_save_notebook_async(state) do if should_save_notebook?(state) do pid = self() diff --git a/lib/livebook/session_supervisor.ex b/lib/livebook/session_supervisor.ex deleted file mode 100644 index 6eddb8376..000000000 --- a/lib/livebook/session_supervisor.ex +++ /dev/null @@ -1,106 +0,0 @@ -defmodule Livebook.SessionSupervisor do - @moduledoc false - - # Supervisor responsible for managing running notebook sessions. - # - # Allows for creating new session processes on demand - # and managing them using random ids. - - use DynamicSupervisor - - alias Livebook.{Session, Utils} - - @name __MODULE__ - - def start_link(opts \\ []) do - DynamicSupervisor.start_link(__MODULE__, opts, name: @name) - end - - @impl true - def init(_opts) do - DynamicSupervisor.init(strategy: :one_for_one) - end - - @doc """ - Spawns a new `Session` process with the given options. - - Broadcasts `{:session_created, id}` message under the `"sessions"` topic. - """ - @spec create_session(keyword()) :: {:ok, Session.id()} | {:error, any()} - def create_session(opts \\ []) do - id = Utils.random_id() - - opts = Keyword.put(opts, :id, id) - - case DynamicSupervisor.start_child(@name, {Session, opts}) do - {:ok, _} -> - broadcast_sessions_message({:session_created, id}) - {:ok, id} - - {:ok, _, _} -> - broadcast_sessions_message({:session_created, id}) - {:ok, id} - - :ignore -> - {:error, :ignore} - - {:error, reason} -> - {:error, reason} - end - end - - @doc """ - Asynchronously stops a session process identified by the given id. - - Broadcasts `{:session_closed, id}` message under the `"sessions"` topic. - """ - @spec close_session(Session.id()) :: :ok - def close_session(id) do - Session.close(id) - broadcast_sessions_message({:session_closed, id}) - :ok - end - - defp broadcast_sessions_message(message) do - Phoenix.PubSub.broadcast(Livebook.PubSub, "sessions", message) - end - - @doc """ - Returns ids of all the running session processes. - """ - @spec get_session_ids() :: list(Session.id()) - def get_session_ids() do - :global.registered_names() - |> Enum.flat_map(fn - {:session, id} -> [id] - _ -> [] - end) - end - - @doc """ - Returns summaries of all the running session processes. - """ - @spec get_session_summaries() :: list(Session.summary()) - def get_session_summaries() do - Enum.map(get_session_ids(), &Session.get_summary/1) - end - - @doc """ - Checks if a session process with the given id exists. - """ - @spec session_exists?(Session.id()) :: boolean() - def session_exists?(id) do - :global.whereis_name({:session, id}) != :undefined - end - - @doc """ - Retrieves pid of a session process identified by the given id. - """ - @spec get_session_pid(Session.id()) :: {:ok, pid()} | {:error, :nonexistent} - def get_session_pid(id) do - case :global.whereis_name({:session, id}) do - :undefined -> {:error, :nonexistent} - pid -> {:ok, pid} - end - end -end diff --git a/lib/livebook/sessions.ex b/lib/livebook/sessions.ex new file mode 100644 index 000000000..52d5b38b9 --- /dev/null +++ b/lib/livebook/sessions.ex @@ -0,0 +1,83 @@ +defmodule Livebook.Sessions do + @moduledoc false + + # This module is responsible for starting and discovering sessions. + # + # Every session has a server process and is described by a `%Session{}` + # info struct. Information about all sessions in the cluster is + # propagated using `Livebook.Tracker`, which serves as an ephemeral + # distributed database for the `%Session{}` structs. + + alias Livebook.{Session, Utils} + + @doc """ + Spawns a new `Session` process with the given options. + + Makes the session globally visible within the session tracker. + + `{:session_created, session}` gets broadcasted under the "tracker_sessions" topic. + """ + @spec create_session(keyword()) :: {:ok, Session.t()} | {:error, any()} + def create_session(opts \\ []) do + id = Utils.random_node_aware_id() + + opts = Keyword.put(opts, :id, id) + + case DynamicSupervisor.start_child(Livebook.SessionSupervisor, {Session, opts}) do + {:ok, pid} -> + session = Session.get_by_pid(pid) + + case Livebook.Tracker.track_session(session) do + :ok -> + {:ok, session} + + {:error, reason} -> + Session.close(pid) + {:error, reason} + end + + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Returns all the running sessions. + """ + @spec list_sessions() :: list(Session.t()) + def list_sessions() do + Livebook.Tracker.list_sessions() + end + + @doc """ + Returns tracked session with the given id. + """ + @spec fetch_session(Session.id()) :: {:ok, Session.t()} | :error + def fetch_session(id) do + case Livebook.Tracker.fetch_session(id) do + {:ok, session} -> + {:ok, session} + + :error -> + # The local tracker server doesn't know about this session, + # but it may not have propagated yet, so we extract the session + # 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, session} <- :rpc.call(other_node, Livebook.Tracker, :fetch_session, [id]) do + {:ok, session} + else + _ -> :error + end + end + end + + @doc """ + Updates the given session info across the cluster. + + `{:session_updated, session}` gets broadcasted under the "tracker_sessions" topic. + """ + @spec update_session(Session.t()) :: :ok | {:error, any()} + def update_session(session) do + Livebook.Tracker.update_session(session) + end +end diff --git a/lib/livebook/tracker.ex b/lib/livebook/tracker.ex new file mode 100644 index 000000000..a65f9b5c6 --- /dev/null +++ b/lib/livebook/tracker.ex @@ -0,0 +1,100 @@ +defmodule Livebook.Tracker do + @moduledoc false + + use Phoenix.Tracker + + alias Livebook.Session + + @name __MODULE__ + + def start_link(opts \\ []) do + opts = Keyword.merge([name: @name], opts) + Phoenix.Tracker.start_link(__MODULE__, opts, opts) + end + + @sessions_topic "sessions" + + @doc """ + Starts tracking the given session, making it visible globally. + """ + @spec track_session(Session.t()) :: :ok | {:error, any()} + def track_session(session) do + case Phoenix.Tracker.track(@name, session.pid, @sessions_topic, session.id, %{ + session: session + }) do + {:ok, _ref} -> :ok + {:error, reason} -> {:error, reason} + end + end + + @doc """ + Updates the tracked session object matching the given id. + """ + @spec update_session(Session.t()) :: :ok | {:error, any()} + def update_session(session) do + case Phoenix.Tracker.update(@name, session.pid, @sessions_topic, session.id, %{ + session: session + }) do + {:ok, _ref} -> :ok + {:error, reason} -> {:error, reason} + end + end + + @doc """ + Returns all tracked sessions. + """ + @spec list_sessions() :: list(Session.t()) + def list_sessions() do + presences = Phoenix.Tracker.list(@name, @sessions_topic) + for {_id, %{session: session}} <- presences, do: session + end + + @doc """ + Returns tracked session with the given id. + """ + @spec fetch_session(Session.id()) :: {:ok, Session.t()} | :error + def fetch_session(id) do + case Phoenix.Tracker.get_by_key(@name, @sessions_topic, id) do + [{_id, %{session: session}}] -> {:ok, session} + _ -> :error + end + end + + @impl true + def init(opts) do + server = Keyword.fetch!(opts, :pubsub_server) + {:ok, %{pubsub_server: server, node_name: Phoenix.PubSub.node_name(server)}} + end + + @impl true + def handle_diff(diff, state) do + for {topic, topic_diff} <- diff do + handle_topic_diff(topic, topic_diff, state) + end + + {:ok, state} + end + + defp handle_topic_diff(@sessions_topic, {joins, leaves}, state) do + joins = Map.new(joins) + leaves = Map.new(leaves) + + messages = + for id <- Enum.uniq(Map.keys(joins) ++ Map.keys(leaves)) do + case {joins[id], leaves[id]} do + {%{session: session}, nil} -> {:session_created, session} + {nil, %{session: session}} -> {:session_closed, session} + {%{session: session}, %{}} -> {:session_updated, session} + end + end + + for message <- messages do + Phoenix.PubSub.direct_broadcast!( + state.node_name, + state.pubsub_server, + "tracker_sessions", + message + ) + end + end +end diff --git a/lib/livebook/utils.ex b/lib/livebook/utils.ex index c7780c08d..213d3a4fa 100644 --- a/lib/livebook/utils.ex +++ b/lib/livebook/utils.ex @@ -6,7 +6,7 @@ defmodule Livebook.Utils do @doc """ Generates a random binary id. """ - @spec random_id() :: binary() + @spec random_id() :: id() def random_id() do :crypto.strong_rand_bytes(20) |> Base.encode32(case: :lower) end @@ -14,7 +14,7 @@ defmodule Livebook.Utils do @doc """ Generates a random short binary id. """ - @spec random_short_id() :: binary() + @spec random_short_id() :: id() def random_short_id() do :crypto.strong_rand_bytes(5) |> Base.encode32(case: :lower) end @@ -27,6 +27,50 @@ defmodule Livebook.Utils do :crypto.strong_rand_bytes(42) |> Base.url_encode64() |> String.to_atom() end + @doc """ + Generates a random binary id that includes node information. + + ## Format + + The id is formed from the following binary parts: + + * 16B - hashed node name + * 9B - random bytes + + The binary is base32 encoded. + """ + @spec random_node_aware_id() :: id() + def random_node_aware_id() do + node_part = node_hash(node()) + random_part = :crypto.strong_rand_bytes(9) + binary = <> + # 16B + 9B = 25B is suitable for base32 encoding without padding + Base.encode32(binary, case: :lower) + end + + # Note: the result is always 16 bytes long + defp node_hash(node) do + content = Atom.to_string(node) + :erlang.md5(content) + end + + @doc """ + Extracts node name from the given node aware id. + + The node in question must be connected, otherwise it won't be found. + """ + @spec node_from_node_aware_id(id()) :: {:ok, node()} | :error + def node_from_node_aware_id(id) do + binary = Base.decode32!(id, case: :lower) + <> = binary + + known_nodes = [node() | Node.list()] + + Enum.find_value(known_nodes, :error, fn node -> + node_hash(node) == node_part && {:ok, node} + end) + end + @doc """ Converts the given name to node identifier. """ diff --git a/lib/livebook_web/controllers/session_controller.ex b/lib/livebook_web/controllers/session_controller.ex index 98ce2f402..e97f92c91 100644 --- a/lib/livebook_web/controllers/session_controller.ex +++ b/lib/livebook_web/controllers/session_controller.ex @@ -1,25 +1,28 @@ defmodule LivebookWeb.SessionController do use LivebookWeb, :controller - alias Livebook.{SessionSupervisor, Session, FileSystem} + alias Livebook.{Sessions, Session, FileSystem} def show_image(conn, %{"id" => id, "image" => image}) do - if SessionSupervisor.session_exists?(id) do - %{images_dir: images_dir} = Session.get_summary(id) - file = FileSystem.File.resolve(images_dir, image) - serve_static(conn, file) - else - send_resp(conn, 404, "Not found") + case Sessions.fetch_session(id) do + {:ok, session} -> + file = FileSystem.File.resolve(session.images_dir, image) + serve_static(conn, file) + + :error -> + send_resp(conn, 404, "Not found") end end def download_source(conn, %{"id" => id, "format" => format}) do - if SessionSupervisor.session_exists?(id) do - notebook = Session.get_notebook(id) + case Sessions.fetch_session(id) do + {:ok, session} -> + notebook = Session.get_notebook(session.pid) - send_notebook_source(conn, notebook, format) - else - send_resp(conn, 404, "Not found") + send_notebook_source(conn, notebook, format) + + :error -> + send_resp(conn, 404, "Not found") end end diff --git a/lib/livebook_web/live/home_live.ex b/lib/livebook_web/live/home_live.ex index 8775cf394..da6057ae0 100644 --- a/lib/livebook_web/live/home_live.ex +++ b/lib/livebook_web/live/home_live.ex @@ -5,17 +5,17 @@ defmodule LivebookWeb.HomeLive do import LivebookWeb.SessionHelpers alias LivebookWeb.{SidebarHelpers, ExploreHelpers} - alias Livebook.{SessionSupervisor, Session, LiveMarkdown, Notebook, FileSystem} + alias Livebook.{Sessions, Session, LiveMarkdown, Notebook, FileSystem} @impl true def mount(_params, %{"current_user_id" => current_user_id} = session, socket) do if connected?(socket) do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions") + Phoenix.PubSub.subscribe(Livebook.PubSub, "tracker_sessions") Phoenix.PubSub.subscribe(Livebook.PubSub, "users:#{current_user_id}") end current_user = build_current_user(session, socket) - session_summaries = sort_session_summaries(SessionSupervisor.get_session_summaries()) + sessions = sort_sessions(Sessions.list_sessions()) notebook_infos = Notebook.Explore.notebook_infos() |> Enum.take(3) {:ok, @@ -23,7 +23,7 @@ defmodule LivebookWeb.HomeLive do current_user: current_user, file: Livebook.Config.default_dir(), file_info: %{exists: true, access: :read_write}, - session_summaries: session_summaries, + sessions: sessions, notebook_infos: notebook_infos )} end @@ -63,7 +63,7 @@ defmodule LivebookWeb.HomeLive do id: "home-file-select", file: @file, extnames: [LiveMarkdown.extension()], - running_files: files(@session_summaries) do %> + running_files: files(@sessions) do %>
- <%= if file_running?(@file, @session_summaries) do %> + <%= if file_running?(@file, @sessions) do %> <%= live_redirect "Join session", - to: Routes.session_path(@socket, :page, session_id_by_file(@file, @session_summaries)), + to: Routes.session_path(@socket, :page, session_id_by_file(@file, @sessions)), class: "button button-blue" %> <% else %> @@ -112,7 +112,7 @@ defmodule LivebookWeb.HomeLive do

Running sessions

- <.sessions_list session_summaries={@session_summaries} socket={@socket} /> + <.sessions_list sessions={@sessions} socket={@socket} />
@@ -131,7 +131,7 @@ defmodule LivebookWeb.HomeLive do id: "close-session", modal_class: "w-full max-w-xl", return_to: Routes.home_path(@socket, :page), - session_summary: @session_summary %> + session: @session %> <% end %> <%= if @live_action == :import do %> @@ -152,7 +152,7 @@ defmodule LivebookWeb.HomeLive do end end - defp sessions_list(%{session_summaries: []} = assigns) do + defp sessions_list(%{sessions: []} = assigns) do ~H"""
@@ -170,35 +170,35 @@ defmodule LivebookWeb.HomeLive do defp sessions_list(assigns) do ~H"""
- <%= for summary <- @session_summaries do %> + <%= for session <- @sessions do %>
+ data-test-session-id={session.id}>
- <%= live_redirect summary.notebook_name, - to: Routes.session_path(@socket, :page, summary.session_id), + <%= live_redirect session.notebook_name, + to: Routes.session_path(@socket, :page, session.id), class: "font-semibold text-gray-800 hover:text-gray-900" %>
- <%= if summary.file, do: summary.file.path, else: "No file" %> + <%= if session.file, do: session.file.path, else: "No file" %>
-
+
""" diff --git a/lib/livebook_web/live/session_live/export_elixir_component.ex b/lib/livebook_web/live/session_live/export_elixir_component.ex index 59f337182..603409649 100644 --- a/lib/livebook_web/live/session_live/export_elixir_component.ex +++ b/lib/livebook_web/live/session_live/export_elixir_component.ex @@ -41,7 +41,7 @@ defmodule LivebookWeb.SessionLive.ExportElixirComponent do + href={Routes.session_path(@socket, :download_source, @session.id, "exs")}> <.remix_icon icon="download-2-line" class="text-lg" /> diff --git a/lib/livebook_web/live/session_live/export_live_markdown_component.ex b/lib/livebook_web/live/session_live/export_live_markdown_component.ex index d5be0824d..c2437d3f8 100644 --- a/lib/livebook_web/live/session_live/export_live_markdown_component.ex +++ b/lib/livebook_web/live/session_live/export_live_markdown_component.ex @@ -48,7 +48,7 @@ defmodule LivebookWeb.SessionLive.ExportLiveMarkdownComponent do + href={Routes.session_path(@socket, :download_source, @session.id, "livemd", include_outputs: @include_outputs)}> <.remix_icon icon="download-2-line" class="text-lg" /> diff --git a/lib/livebook_web/live/session_live/input_cell_settings_component.ex b/lib/livebook_web/live/session_live/input_cell_settings_component.ex index 8082145a2..939c989d5 100644 --- a/lib/livebook_web/live/session_live/input_cell_settings_component.ex +++ b/lib/livebook_web/live/session_live/input_cell_settings_component.ex @@ -136,7 +136,7 @@ defmodule LivebookWeb.SessionLive.InputCellSettingsComponent do attrs end - Session.set_cell_attributes(socket.assigns.session_id, socket.assigns.cell.id, attrs) + Session.set_cell_attributes(socket.assigns.session.pid, socket.assigns.cell.id, attrs) {:noreply, push_patch(socket, to: socket.assigns.return_to)} end diff --git a/lib/livebook_web/live/session_live/mix_standalone_live.ex b/lib/livebook_web/live/session_live/mix_standalone_live.ex index 1f8c79f12..b700f5732 100644 --- a/lib/livebook_web/live/session_live/mix_standalone_live.ex +++ b/lib/livebook_web/live/session_live/mix_standalone_live.ex @@ -6,14 +6,14 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do @type status :: :initial | :initializing | :finished @impl true - def mount(_params, %{"session_id" => session_id, "current_runtime" => current_runtime}, socket) do + def mount(_params, %{"session" => session, "current_runtime" => current_runtime}, socket) do if connected?(socket) do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") end {:ok, assign(socket, - session_id: session_id, + session: session, status: :initial, current_runtime: current_runtime, file: initial_file(current_runtime), @@ -84,7 +84,7 @@ defmodule LivebookWeb.SessionLive.MixStandaloneLive do {:noreply, add_output(socket, output)} {:ok, runtime} -> - Session.connect_runtime(socket.assigns.session_id, runtime) + Session.connect_runtime(socket.assigns.session.pid, runtime) {:noreply, socket |> assign(status: :finished) |> add_output("Connected successfully")} {:error, error} -> diff --git a/lib/livebook_web/live/session_live/persistence_live.ex b/lib/livebook_web/live/session_live/persistence_live.ex index 659680ef8..57243ffef 100644 --- a/lib/livebook_web/live/session_live/persistence_live.ex +++ b/lib/livebook_web/live/session_live/persistence_live.ex @@ -6,21 +6,21 @@ defmodule LivebookWeb.SessionLive.PersistenceLive do # the parent live view. use LivebookWeb, :live_view - alias Livebook.{Session, SessionSupervisor, LiveMarkdown, FileSystem} + alias Livebook.{Sessions, Session, LiveMarkdown, FileSystem} @impl true def mount( _params, %{ - "session_id" => session_id, + "session" => session, "file" => file, "persist_outputs" => persist_outputs, "autosave_interval_s" => autosave_interval_s }, socket ) do - session_summaries = SessionSupervisor.get_session_summaries() - running_files = Enum.map(session_summaries, & &1.file) + sessions = Sessions.list_sessions() + running_files = Enum.map(sessions, & &1.file) attrs = %{ file: file, @@ -30,7 +30,7 @@ defmodule LivebookWeb.SessionLive.PersistenceLive do {:ok, assign(socket, - session_id: session_id, + session: session, running_files: running_files, attrs: attrs, new_attrs: attrs @@ -152,20 +152,20 @@ defmodule LivebookWeb.SessionLive.PersistenceLive do autosave_interval_s = assigns.new_attrs.autosave_interval_s if file != assigns.attrs.file do - Session.set_file(assigns.session_id, file) + Session.set_file(assigns.session.pid, file) end if autosave_interval_s != assigns.attrs.autosave_interval_s do - Session.set_notebook_attributes(assigns.session_id, %{ + Session.set_notebook_attributes(assigns.session.pid, %{ autosave_interval_s: autosave_interval_s }) end - Session.set_notebook_attributes(assigns.session_id, %{ + Session.set_notebook_attributes(assigns.session.pid, %{ persist_outputs: assigns.new_attrs.persist_outputs }) - Session.save_sync(assigns.session_id) + Session.save_sync(assigns.session.pid) running_files = if file do diff --git a/lib/livebook_web/live/session_live/runtime_component.ex b/lib/livebook_web/live/session_live/runtime_component.ex index fe6e3c79f..9bce49ad7 100644 --- a/lib/livebook_web/live/session_live/runtime_component.ex +++ b/lib/livebook_web/live/session_live/runtime_component.ex @@ -88,7 +88,7 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do
<%= live_render @socket, live_view_for_type(@type), id: "runtime-config-#{@type}", - session: %{"session_id" => @session_id, "current_runtime" => @runtime} %> + session: %{"session" => @session, "current_runtime" => @runtime} %>
@@ -116,7 +116,7 @@ defmodule LivebookWeb.SessionLive.RuntimeComponent do end def handle_event("disconnect", _params, socket) do - Session.disconnect_runtime(socket.assigns.session_id) + Session.disconnect_runtime(socket.assigns.session.pid) {:noreply, socket} end diff --git a/test/livebook/session_supervisor_test.exs b/test/livebook/session_supervisor_test.exs deleted file mode 100644 index df90b5163..000000000 --- a/test/livebook/session_supervisor_test.exs +++ /dev/null @@ -1,80 +0,0 @@ -defmodule Livebook.SessionSupervisorTest do - use ExUnit.Case - - alias Livebook.SessionSupervisor - - describe "create_session/0" do - test "creates a new session process and returns its id" do - {:ok, id} = SessionSupervisor.create_session() - {:ok, pid} = SessionSupervisor.get_session_pid(id) - - assert has_child_with_pid?(SessionSupervisor, pid) - end - - test "broadcasts a message" do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions") - {:ok, id} = SessionSupervisor.create_session() - - assert_receive {:session_created, ^id} - end - end - - describe "close_session/1" do - test "stops the session process identified by the given id" do - {:ok, id} = SessionSupervisor.create_session() - {:ok, pid} = SessionSupervisor.get_session_pid(id) - ref = Process.monitor(pid) - - SessionSupervisor.close_session(id) - - assert_receive {:DOWN, ^ref, :process, _, _} - refute has_child_with_pid?(SessionSupervisor, pid) - end - - test "broadcasts a message" do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions") - {:ok, id} = SessionSupervisor.create_session() - - SessionSupervisor.close_session(id) - - assert_receive {:session_closed, ^id} - end - end - - describe "get_session_ids/0" do - test "lists ids identifying sessions running under the supervisor" do - {:ok, id} = SessionSupervisor.create_session() - - assert id in SessionSupervisor.get_session_ids() - end - end - - describe "session_exists?/1" do - test "returns true if a session process with the given id exists" do - {:ok, id} = SessionSupervisor.create_session() - - assert SessionSupervisor.session_exists?(id) - end - - test "returns false if a session process with the given id does not exist" do - refute SessionSupervisor.session_exists?("nonexistent") - end - end - - describe "get_session_pid/1" do - test "returns pid if a session process with the given id is running" do - {:ok, id} = SessionSupervisor.create_session() - - assert {:ok, pid} = SessionSupervisor.get_session_pid(id) - assert :global.whereis_name({:session, id}) == pid - end - - test "returns an error if a session process with the given id does not exist" do - assert {:error, :nonexistent} = SessionSupervisor.get_session_pid("nonexistent") - end - end - - defp has_child_with_pid?(supervisor, pid) do - List.keymember?(DynamicSupervisor.which_children(supervisor), pid, 1) - end -end diff --git a/test/livebook/session_test.exs b/test/livebook/session_test.exs index a9fc4f990..abbd96d21 100644 --- a/test/livebook/session_test.exs +++ b/test/livebook/session_test.exs @@ -10,101 +10,101 @@ defmodule Livebook.SessionTest do @evaluation_wait_timeout 3_000 setup do - session_id = start_session() - %{session_id: session_id} + session = start_session() + %{session: session} end describe "set_notebook_attributes/2" do - test "sends an attributes update to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends an attributes update to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() attrs = %{set_notebook_attributes: true} - Session.set_notebook_attributes(session_id, attrs) + Session.set_notebook_attributes(session.pid, attrs) assert_receive {:operation, {:set_notebook_attributes, ^pid, ^attrs}} end end describe "insert_section/2" do - test "sends an insert opreation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends an insert opreation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() - Session.insert_section(session_id, 0) + Session.insert_section(session.pid, 0) assert_receive {:operation, {:insert_section, ^pid, 0, _id}} end end describe "insert_cell/4" do - test "sends an insert opreation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends an insert opreation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() - Session.insert_section(session_id, 0) + Session.insert_section(session.pid, 0) assert_receive {:operation, {:insert_section, ^pid, 0, section_id}} - Session.insert_cell(session_id, section_id, 0, :elixir) + Session.insert_cell(session.pid, section_id, 0, :elixir) assert_receive {:operation, {:insert_cell, ^pid, ^section_id, 0, :elixir, _id}} end end describe "delete_section/3" do - test "sends a delete opreation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends a delete opreation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() - {section_id, _cell_id} = insert_section_and_cell(session_id) + {section_id, _cell_id} = insert_section_and_cell(session.pid) - Session.delete_section(session_id, section_id, false) + Session.delete_section(session.pid, section_id, false) assert_receive {:operation, {:delete_section, ^pid, ^section_id, false}} end end describe "delete_cell/2" do - test "sends a delete opreation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends a delete opreation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() - {_section_id, cell_id} = insert_section_and_cell(session_id) + {_section_id, cell_id} = insert_section_and_cell(session.pid) - Session.delete_cell(session_id, cell_id) + Session.delete_cell(session.pid, cell_id) assert_receive {:operation, {:delete_cell, ^pid, ^cell_id}} end end describe "restore_cell/2" do - test "sends a restore opreation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends a restore opreation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() - {_section_id, cell_id} = insert_section_and_cell(session_id) - Session.delete_cell(session_id, cell_id) + {_section_id, cell_id} = insert_section_and_cell(session.pid) + Session.delete_cell(session.pid, cell_id) - Session.restore_cell(session_id, cell_id) + Session.restore_cell(session.pid, cell_id) assert_receive {:operation, {:restore_cell, ^pid, ^cell_id}} end end describe "queue_cell_evaluation/2" do - test "sends a queue evaluation operation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends a queue evaluation operation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() - {_section_id, cell_id} = insert_section_and_cell(session_id) + {_section_id, cell_id} = insert_section_and_cell(session.pid) - Session.queue_cell_evaluation(session_id, cell_id) + Session.queue_cell_evaluation(session.pid, cell_id) assert_receive {:operation, {:queue_cell_evaluation, ^pid, ^cell_id}}, @evaluation_wait_timeout end test "triggers evaluation and sends update operation once it finishes", - %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") - {_section_id, cell_id} = insert_section_and_cell(session_id) + {_section_id, cell_id} = insert_section_and_cell(session.pid) - Session.queue_cell_evaluation(session_id, cell_id) + Session.queue_cell_evaluation(session.pid, cell_id) assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, @@ -114,14 +114,14 @@ defmodule Livebook.SessionTest do end describe "cancel_cell_evaluation/2" do - test "sends a cancel evaluation operation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends a cancel evaluation operation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() - {_section_id, cell_id} = insert_section_and_cell(session_id) - Session.queue_cell_evaluation(session_id, cell_id) + {_section_id, cell_id} = insert_section_and_cell(session.pid) + Session.queue_cell_evaluation(session.pid, cell_id) - Session.cancel_cell_evaluation(session_id, cell_id) + Session.cancel_cell_evaluation(session.pid, cell_id) assert_receive {:operation, {:cancel_cell_evaluation, ^pid, ^cell_id}}, @evaluation_wait_timeout @@ -129,89 +129,89 @@ defmodule Livebook.SessionTest do end describe "set_notebook_name/2" do - test "sends a notebook name update operation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends a notebook name update operation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() - Session.set_notebook_name(session_id, "Cat's guide to life") + Session.set_notebook_name(session.pid, "Cat's guide to life") assert_receive {:operation, {:set_notebook_name, ^pid, "Cat's guide to life"}} end end describe "set_section_name/3" do - test "sends a section name update operation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends a section name update operation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() - {section_id, _cell_id} = insert_section_and_cell(session_id) + {section_id, _cell_id} = insert_section_and_cell(session.pid) - Session.set_section_name(session_id, section_id, "Chapter 1") + Session.set_section_name(session.pid, section_id, "Chapter 1") assert_receive {:operation, {:set_section_name, ^pid, ^section_id, "Chapter 1"}} end end describe "apply_cell_delta/4" do - test "sends a cell delta operation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends a cell delta operation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() - {_section_id, cell_id} = insert_section_and_cell(session_id) + {_section_id, cell_id} = insert_section_and_cell(session.pid) delta = Delta.new() |> Delta.insert("cats") revision = 1 - Session.apply_cell_delta(session_id, cell_id, delta, revision) + Session.apply_cell_delta(session.pid, cell_id, delta, revision) assert_receive {:operation, {:apply_cell_delta, ^pid, ^cell_id, ^delta, ^revision}} end end describe "report_cell_revision/3" do - test "sends a revision report operation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends a revision report operation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() - {_section_id, cell_id} = insert_section_and_cell(session_id) + {_section_id, cell_id} = insert_section_and_cell(session.pid) revision = 1 - Session.report_cell_revision(session_id, cell_id, revision) + Session.report_cell_revision(session.pid, cell_id, revision) assert_receive {:operation, {:report_cell_revision, ^pid, ^cell_id, ^revision}} end end describe "set_cell_attributes/3" do - test "sends an attributes update operation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends an attributes update operation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() - {_section_id, cell_id} = insert_section_and_cell(session_id) + {_section_id, cell_id} = insert_section_and_cell(session.pid) attrs = %{disable_formatting: true} - Session.set_cell_attributes(session_id, cell_id, attrs) + Session.set_cell_attributes(session.pid, cell_id, attrs) assert_receive {:operation, {:set_cell_attributes, ^pid, ^cell_id, ^attrs}} end end describe "connect_runtime/2" do - test "sends a runtime update operation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends a runtime update operation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() {:ok, runtime} = Livebook.Runtime.Embedded.init() - Session.connect_runtime(session_id, runtime) + Session.connect_runtime(session.pid, runtime) assert_receive {:operation, {:set_runtime, ^pid, ^runtime}} end end describe "disconnect_runtime/1" do - test "sends a runtime update operation to subscribers", %{session_id: session_id} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + test "sends a runtime update operation to subscribers", %{session: session} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() {:ok, runtime} = Livebook.Runtime.Embedded.init() - Session.connect_runtime(session_id, runtime) + Session.connect_runtime(session.pid, runtime) - Session.disconnect_runtime(session_id) + Session.disconnect_runtime(session.pid) assert_receive {:operation, {:set_runtime, ^pid, nil}} end @@ -220,41 +220,41 @@ defmodule Livebook.SessionTest do describe "set_file/1" do @tag :tmp_dir test "sends a file update operation to subscribers", - %{session_id: session_id, tmp_dir: tmp_dir} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + %{session: session, tmp_dir: tmp_dir} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") pid = self() tmp_dir = FileSystem.File.local(tmp_dir <> "/") file = FileSystem.File.resolve(tmp_dir, "notebook.livemd") - Session.set_file(session_id, file) + Session.set_file(session.pid, file) assert_receive {:operation, {:set_file, ^pid, ^file}} end @tag :tmp_dir test "broadcasts an error if the path is already in use", - %{session_id: session_id, tmp_dir: tmp_dir} do + %{session: session, tmp_dir: tmp_dir} do tmp_dir = FileSystem.File.local(tmp_dir <> "/") file = FileSystem.File.resolve(tmp_dir, "notebook.livemd") start_session(file: file) - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") - Session.set_file(session_id, file) + Session.set_file(session.pid, file) assert_receive {:error, "failed to set new file because it is already in use"} end @tag :tmp_dir - test "moves images to the new directory", %{session_id: session_id, tmp_dir: tmp_dir} do + test "moves images to the new directory", %{session: session, tmp_dir: tmp_dir} do tmp_dir = FileSystem.File.local(tmp_dir <> "/") - %{images_dir: images_dir} = Session.get_summary(session_id) + %{images_dir: images_dir} = session image_file = FileSystem.File.resolve(images_dir, "test.jpg") :ok = FileSystem.File.write(image_file, "") file = FileSystem.File.resolve(tmp_dir, "notebook.livemd") - Session.set_file(session_id, file) + Session.set_file(session.pid, file) # Wait for the session to deal with the files Process.sleep(50) @@ -267,23 +267,23 @@ defmodule Livebook.SessionTest do @tag :tmp_dir test "does not remove images from the previous dir if not temporary", - %{session_id: session_id, tmp_dir: tmp_dir} do + %{session: session, tmp_dir: tmp_dir} do tmp_dir = FileSystem.File.local(tmp_dir <> "/") file = FileSystem.File.resolve(tmp_dir, "notebook.livemd") - Session.set_file(session_id, file) + Session.set_file(session.pid, file) - %{images_dir: images_dir} = Session.get_summary(session_id) + %{images_dir: images_dir} = session image_file = FileSystem.File.resolve(images_dir, "test.jpg") :ok = FileSystem.File.write(image_file, "") - Session.set_file(session_id, nil) + Session.set_file(session.pid, nil) # Wait for the session to deal with the files Process.sleep(50) assert {:ok, true} = FileSystem.File.exists?(image_file) - %{images_dir: new_images_dir} = Session.get_summary(session_id) + %{images_dir: new_images_dir} = session assert {:ok, true} = FileSystem.File.exists?(FileSystem.File.resolve(new_images_dir, "test.jpg")) @@ -293,36 +293,36 @@ defmodule Livebook.SessionTest do describe "save/1" do @tag :tmp_dir test "persists the notebook to the associated file and notifies subscribers", - %{session_id: session_id, tmp_dir: tmp_dir} do + %{session: session, tmp_dir: tmp_dir} do tmp_dir = FileSystem.File.local(tmp_dir <> "/") file = FileSystem.File.resolve(tmp_dir, "notebook.livemd") - Session.set_file(session_id, file) + Session.set_file(session.pid, file) # Perform a change, so the notebook is dirty - Session.set_notebook_name(session_id, "My notebook") + Session.set_notebook_name(session.pid, "My notebook") - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") assert {:ok, false} = FileSystem.File.exists?(file) - Session.save(session_id) + Session.save(session.pid) assert_receive {:operation, {:mark_as_not_dirty, _}} assert {:ok, "# My notebook\n" <> _rest} = FileSystem.File.read(file) end @tag :tmp_dir - test "creates nonexistent directories", %{session_id: session_id, tmp_dir: tmp_dir} do + test "creates nonexistent directories", %{session: session, tmp_dir: tmp_dir} do tmp_dir = FileSystem.File.local(tmp_dir <> "/") file = FileSystem.File.resolve(tmp_dir, "nonexistent/dir/notebook.livemd") - Session.set_file(session_id, file) + Session.set_file(session.pid, file) # Perform a change, so the notebook is dirty - Session.set_notebook_name(session_id, "My notebook") + Session.set_notebook_name(session.pid, "My notebook") - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") assert {:ok, false} = FileSystem.File.exists?(file) - Session.save(session_id) + Session.save(session.pid) assert_receive {:operation, {:mark_as_not_dirty, _}} assert {:ok, "# My notebook\n" <> _rest} = FileSystem.File.read(file) @@ -332,32 +332,32 @@ defmodule Livebook.SessionTest do describe "close/1" do @tag :tmp_dir test "saves the notebook and notifies subscribers once the session is closed", - %{session_id: session_id, tmp_dir: tmp_dir} do + %{session: session, tmp_dir: tmp_dir} do tmp_dir = FileSystem.File.local(tmp_dir <> "/") file = FileSystem.File.resolve(tmp_dir, "notebook.livemd") - Session.set_file(session_id, file) + Session.set_file(session.pid, file) # Perform a change, so the notebook is dirty - Session.set_notebook_name(session_id, "My notebook") + Session.set_notebook_name(session.pid, "My notebook") - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") assert {:ok, false} = FileSystem.File.exists?(file) Process.flag(:trap_exit, true) - Session.close(session_id) + Session.close(session.pid) assert_receive :session_closed assert {:ok, "# My notebook\n" <> _rest} = FileSystem.File.read(file) end - test "clears session temporary directory", %{session_id: session_id} do - %{images_dir: images_dir} = Session.get_summary(session_id) + test "clears session temporary directory", %{session: session} do + %{images_dir: images_dir} = session :ok = FileSystem.File.create_dir(images_dir) assert {:ok, true} = FileSystem.File.exists?(images_dir) Process.flag(:trap_exit, true) - Session.close(session_id) + Session.close(session.pid) # Wait for the session to deal with the files Process.sleep(50) @@ -384,9 +384,8 @@ defmodule Livebook.SessionTest do image_file = FileSystem.File.resolve(tmp_dir, "image.jpg") :ok = FileSystem.File.write(image_file, "") - session_id = start_session(copy_images_from: tmp_dir) - - %{images_dir: images_dir} = Session.get_summary(session_id) + session = start_session(copy_images_from: tmp_dir) + %{images_dir: images_dir} = session assert {:ok, true} = FileSystem.File.exists?(FileSystem.File.resolve(images_dir, "image.jpg")) @@ -395,9 +394,8 @@ defmodule Livebook.SessionTest do test "saves images when :images option is specified" do images = %{"image.jpg" => "binary content"} - session_id = start_session(images: images) - - %{images_dir: images_dir} = Session.get_summary(session_id) + session = start_session(images: images) + %{images_dir: images_dir} = session assert FileSystem.File.resolve(images_dir, "image.jpg") |> FileSystem.File.read() == {:ok, "binary content"} @@ -410,14 +408,13 @@ defmodule Livebook.SessionTest do # to verify session integrates well with it properly. test "starts a standalone runtime upon first evaluation if there was none set explicitly" do - session_id = Utils.random_id() - {:ok, _} = Session.start_link(id: session_id) + session = start_session() - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") - {_section_id, cell_id} = insert_section_and_cell(session_id) + {_section_id, cell_id} = insert_section_and_cell(session.pid) - Session.queue_cell_evaluation(session_id, cell_id) + Session.queue_cell_evaluation(session.pid, cell_id) # Give it a bit more time as this involves starting a system process. assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, _, @@ -426,14 +423,13 @@ defmodule Livebook.SessionTest do end test "if the runtime node goes down, notifies the subscribers" do - session_id = Utils.random_id() - {:ok, _} = Session.start_link(id: session_id) + session = start_session() {:ok, runtime} = Runtime.ElixirStandalone.init() - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") # Wait for the runtime to be set - Session.connect_runtime(session_id, runtime) + Session.connect_runtime(session.pid, runtime) assert_receive {:operation, {:set_runtime, _, ^runtime}} # Terminate the other node, the session should detect that @@ -443,11 +439,11 @@ defmodule Livebook.SessionTest do assert_receive {:info, "runtime node terminated unexpectedly"} end - test "on user change sends an update operation subscribers", %{session_id: session_id} do + test "on user change sends an update operation subscribers", %{session: session} do user = Livebook.Users.User.new() - Session.register_client(session_id, self(), user) + Session.register_client(session.pid, self(), user) - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") updated_user = %{user | name: "Jake Peralta"} Livebook.Users.broadcast_change(updated_user) @@ -475,12 +471,12 @@ defmodule Livebook.SessionTest do ] } - session_id = start_session(notebook: notebook) + session = start_session(notebook: notebook) cell_id = elixir_cell.id - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") - Session.queue_cell_evaluation(session_id, cell_id) + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") + Session.queue_cell_evaluation(session.pid, cell_id) assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, {:text, text_output}, @@ -505,12 +501,12 @@ defmodule Livebook.SessionTest do ] } - session_id = start_session(notebook: notebook) + session = start_session(notebook: notebook) cell_id = elixir_cell.id - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") - Session.queue_cell_evaluation(session_id, cell_id) + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") + Session.queue_cell_evaluation(session.pid, cell_id) assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, {:text, text_output}, @@ -537,12 +533,12 @@ defmodule Livebook.SessionTest do ] } - session_id = start_session(notebook: notebook) + session = start_session(notebook: notebook) cell_id = elixir_cell.id - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") - Session.queue_cell_evaluation(session_id, cell_id) + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") + Session.queue_cell_evaluation(session.pid, cell_id) assert_receive {:operation, {:add_cell_evaluation_response, _, ^cell_id, {:text, text_output}, @@ -555,14 +551,14 @@ defmodule Livebook.SessionTest do defp start_session(opts \\ []) do session_id = Utils.random_id() - {:ok, _} = Session.start_link(Keyword.merge(opts, id: session_id)) - session_id + {:ok, pid} = Session.start_link(Keyword.merge([id: session_id], opts)) + Session.get_by_pid(pid) end - defp insert_section_and_cell(session_id) do - Session.insert_section(session_id, 0) + defp insert_section_and_cell(session_pid) do + Session.insert_section(session_pid, 0) assert_receive {:operation, {:insert_section, _, 0, section_id}} - Session.insert_cell(session_id, section_id, 0, :elixir) + Session.insert_cell(session_pid, section_id, 0, :elixir) assert_receive {:operation, {:insert_cell, _, ^section_id, 0, :elixir, cell_id}} {section_id, cell_id} diff --git a/test/livebook/sessions_test.exs b/test/livebook/sessions_test.exs new file mode 100644 index 000000000..b58e9a594 --- /dev/null +++ b/test/livebook/sessions_test.exs @@ -0,0 +1,51 @@ +defmodule Livebook.SessionsTest do + use ExUnit.Case + + alias Livebook.Sessions + + describe "create_session/0" do + test "starts a new session process under the sessions supervisor" do + {:ok, session} = Sessions.create_session() + assert has_child_with_pid?(Livebook.SessionSupervisor, session.pid) + end + + test "broadcasts a message to subscribers" do + Phoenix.PubSub.subscribe(Livebook.PubSub, "tracker_sessions") + {:ok, %{id: id}} = Sessions.create_session() + assert_receive {:session_created, %{id: ^id}} + end + end + + describe "list_sessions/0" do + test "lists all sessions" do + {:ok, session} = Sessions.create_session() + assert session in Sessions.list_sessions() + end + end + + describe "fetch_session/1" do + test "returns an error if no session with the given id exists" do + id = Livebook.Utils.random_node_aware_id() + assert :error = Sessions.fetch_session(id) + end + + test "returns session matching the given id" do + {:ok, session} = Sessions.create_session() + assert {:ok, ^session} = Sessions.fetch_session(session.id) + end + end + + describe "update_session/1" do + test "broadcasts a message to subscribers" do + {:ok, %{id: id} = session} = Sessions.create_session() + Phoenix.PubSub.subscribe(Livebook.PubSub, "tracker_sessions") + updated_session = %{session | notebook_name: "New name"} + Livebook.Sessions.update_session(updated_session) + assert_receive {:session_updated, %{id: ^id, notebook_name: "New name"}} + end + end + + defp has_child_with_pid?(supervisor, pid) do + List.keymember?(DynamicSupervisor.which_children(supervisor), pid, 1) + end +end diff --git a/test/livebook_web/controllers/session_controller_test.exs b/test/livebook_web/controllers/session_controller_test.exs index cf449eee9..c5561ddef 100644 --- a/test/livebook_web/controllers/session_controller_test.exs +++ b/test/livebook_web/controllers/session_controller_test.exs @@ -1,62 +1,64 @@ defmodule LivebookWeb.SessionControllerTest do use LivebookWeb.ConnCase, async: true - alias Livebook.{SessionSupervisor, Session, Notebook, FileSystem} + alias Livebook.{Sessions, Session, Notebook, FileSystem} describe "show_image" do test "returns not found when the given session does not exist", %{conn: conn} do - conn = get(conn, Routes.session_path(conn, :show_image, "nonexistent", "image.jpg")) + id = Livebook.Utils.random_node_aware_id() + conn = get(conn, Routes.session_path(conn, :show_image, id, "image.jpg")) assert conn.status == 404 assert conn.resp_body == "Not found" end test "returns not found when the given image does not exist", %{conn: conn} do - {:ok, session_id} = SessionSupervisor.create_session() + {:ok, session} = Sessions.create_session() - conn = get(conn, Routes.session_path(conn, :show_image, session_id, "nonexistent.jpg")) + conn = get(conn, Routes.session_path(conn, :show_image, session.id, "nonexistent.jpg")) assert conn.status == 404 assert conn.resp_body == "No such file or directory" - SessionSupervisor.close_session(session_id) + Session.close(session.pid) end test "returns the image when it does exist", %{conn: conn} do - {:ok, session_id} = SessionSupervisor.create_session() - %{images_dir: images_dir} = Session.get_summary(session_id) + {:ok, session} = Sessions.create_session() + %{images_dir: images_dir} = session :ok = FileSystem.File.resolve(images_dir, "test.jpg") |> FileSystem.File.write("") - conn = get(conn, Routes.session_path(conn, :show_image, session_id, "test.jpg")) + conn = get(conn, Routes.session_path(conn, :show_image, session.id, "test.jpg")) assert conn.status == 200 assert get_resp_header(conn, "content-type") == ["image/jpeg"] - SessionSupervisor.close_session(session_id) + Session.close(session.pid) end end describe "download_source" do test "returns not found when the given session does not exist", %{conn: conn} do - conn = get(conn, Routes.session_path(conn, :download_source, "nonexistent", "livemd")) + id = Livebook.Utils.random_node_aware_id() + conn = get(conn, Routes.session_path(conn, :download_source, id, "livemd")) assert conn.status == 404 assert conn.resp_body == "Not found" end test "returns bad request when given an invalid format", %{conn: conn} do - {:ok, session_id} = SessionSupervisor.create_session() + {:ok, session} = Sessions.create_session() - conn = get(conn, Routes.session_path(conn, :download_source, session_id, "invalid")) + conn = get(conn, Routes.session_path(conn, :download_source, session.id, "invalid")) assert conn.status == 400 assert conn.resp_body == "Invalid format, supported formats: livemd, exs" end test "handles live markdown notebook source", %{conn: conn} do - {:ok, session_id} = SessionSupervisor.create_session() + {:ok, session} = Sessions.create_session() - conn = get(conn, Routes.session_path(conn, :download_source, session_id, "livemd")) + conn = get(conn, Routes.session_path(conn, :download_source, session.id, "livemd")) assert conn.status == 200 assert get_resp_header(conn, "content-type") == ["text/plain"] @@ -71,7 +73,7 @@ defmodule LivebookWeb.SessionControllerTest do ``` """ - SessionSupervisor.close_session(session_id) + Session.close(session.pid) end test "includes output in markdown when include_outputs parameter is set", %{conn: conn} do @@ -95,10 +97,10 @@ defmodule LivebookWeb.SessionControllerTest do ] } - {:ok, session_id} = SessionSupervisor.create_session(notebook: notebook) + {:ok, session} = Sessions.create_session(notebook: notebook) query = [include_outputs: "true"] - conn = get(conn, Routes.session_path(conn, :download_source, session_id, "livemd", query)) + conn = get(conn, Routes.session_path(conn, :download_source, session.id, "livemd", query)) assert conn.status == 200 assert get_resp_header(conn, "content-type") == ["text/plain"] @@ -117,13 +119,13 @@ defmodule LivebookWeb.SessionControllerTest do ``` """ - SessionSupervisor.close_session(session_id) + Session.close(session.pid) end test "handles elixir notebook source", %{conn: conn} do - {:ok, session_id} = SessionSupervisor.create_session() + {:ok, session} = Sessions.create_session() - conn = get(conn, Routes.session_path(conn, :download_source, session_id, "exs")) + conn = get(conn, Routes.session_path(conn, :download_source, session.id, "exs")) assert conn.status == 200 assert get_resp_header(conn, "content-type") == ["text/plain"] @@ -134,7 +136,7 @@ defmodule LivebookWeb.SessionControllerTest do # ── Section ── """ - SessionSupervisor.close_session(session_id) + Session.close(session.pid) end end end diff --git a/test/livebook_web/live/home_live_test.exs b/test/livebook_web/live/home_live_test.exs index 339d0bee9..20703a4ce 100644 --- a/test/livebook_web/live/home_live_test.exs +++ b/test/livebook_web/live/home_live_test.exs @@ -3,7 +3,7 @@ defmodule LivebookWeb.HomeLiveTest do import Phoenix.LiveViewTest - alias Livebook.{SessionSupervisor, Session} + alias Livebook.{Sessions, Session} test "disconnected and connected render", %{conn: conn} do {:ok, view, disconnected_html} = live(conn, "/") @@ -113,34 +113,38 @@ defmodule LivebookWeb.HomeLiveTest do describe "sessions list" do test "lists running sessions", %{conn: conn} do - {:ok, id1} = SessionSupervisor.create_session() - {:ok, id2} = SessionSupervisor.create_session() + {:ok, session1} = Sessions.create_session() + {:ok, session2} = Sessions.create_session() {:ok, view, _} = live(conn, "/") - assert render(view) =~ id1 - assert render(view) =~ id2 + assert render(view) =~ session1.id + assert render(view) =~ session2.id end test "updates UI whenever a session is added or deleted", %{conn: conn} do + Phoenix.PubSub.subscribe(Livebook.PubSub, "tracker_sessions") + {:ok, view, _} = live(conn, "/") - {:ok, id} = SessionSupervisor.create_session() + {:ok, %{id: id} = session} = Sessions.create_session() + assert_receive {:session_created, %{id: ^id}} assert render(view) =~ id - SessionSupervisor.close_session(id) + Session.close(session.pid) + assert_receive {:session_closed, %{id: ^id}} refute render(view) =~ id end test "allows forking existing session", %{conn: conn} do - {:ok, id} = SessionSupervisor.create_session() - Session.set_notebook_name(id, "My notebook") + {:ok, session} = Sessions.create_session() + Session.set_notebook_name(session.pid, "My notebook") {:ok, view, _} = live(conn, "/") assert {:error, {:live_redirect, %{to: to}}} = view - |> element(~s{[data-test-session-id="#{id}"] button}, "Fork") + |> element(~s{[data-test-session-id="#{session.id}"] button}, "Fork") |> render_click() assert to =~ "/sessions/" @@ -150,21 +154,21 @@ defmodule LivebookWeb.HomeLiveTest do end test "allows closing session after confirmation", %{conn: conn} do - {:ok, id} = SessionSupervisor.create_session() + {:ok, session} = Sessions.create_session() {:ok, view, _} = live(conn, "/") - assert render(view) =~ id + assert render(view) =~ session.id view - |> element(~s{[data-test-session-id="#{id}"] a}, "Close") + |> element(~s{[data-test-session-id="#{session.id}"] a}, "Close") |> render_click() view |> element(~s{button}, "Close session") |> render_click() - refute render(view) =~ id + refute render(view) =~ session.id end end @@ -185,7 +189,7 @@ defmodule LivebookWeb.HomeLiveTest do describe "notebook import" do test "allows importing notebook directly from content", %{conn: conn} do - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions") + Phoenix.PubSub.subscribe(Livebook.PubSub, "tracker_sessions") {:ok, view, _} = live(conn, "/home/import/content") @@ -197,7 +201,7 @@ defmodule LivebookWeb.HomeLiveTest do |> element("form", "Import") |> render_submit(%{data: %{content: notebook_content}}) - assert_receive {:session_created, id} + assert_receive {:session_created, %{id: id}} {:ok, view, _} = live(conn, "/sessions/#{id}") assert render(view) =~ "My notebook" diff --git a/test/livebook_web/live/session_live_test.exs b/test/livebook_web/live/session_live_test.exs index 01694f9c9..d99d3a718 100644 --- a/test/livebook_web/live/session_live_test.exs +++ b/test/livebook_web/live/session_live_test.exs @@ -3,116 +3,116 @@ defmodule LivebookWeb.SessionLiveTest do import Phoenix.LiveViewTest - alias Livebook.{SessionSupervisor, Session, Delta, Runtime, Users, FileSystem} + alias Livebook.{Sessions, Session, Delta, Runtime, Users, FileSystem} alias Livebook.Notebook.Cell alias Livebook.Users.User setup do - {:ok, session_id} = SessionSupervisor.create_session(notebook: Livebook.Notebook.new()) - %{session_id: session_id} + {:ok, session} = Sessions.create_session(notebook: Livebook.Notebook.new()) + %{session: session} end - test "disconnected and connected render", %{conn: conn, session_id: session_id} do - {:ok, view, disconnected_html} = live(conn, "/sessions/#{session_id}") + test "disconnected and connected render", %{conn: conn, session: session} do + {:ok, view, disconnected_html} = live(conn, "/sessions/#{session.id}") assert disconnected_html =~ "Untitled notebook" assert render(view) =~ "Untitled notebook" end describe "asynchronous updates" do - test "renders an updated notebook name", %{conn: conn, session_id: session_id} do - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + test "renders an updated notebook name", %{conn: conn, session: session} do + {:ok, view, _} = live(conn, "/sessions/#{session.id}") - Session.set_notebook_name(session_id, "My notebook") - wait_for_session_update(session_id) + Session.set_notebook_name(session.pid, "My notebook") + wait_for_session_update(session.pid) assert render(view) =~ "My notebook" end - test "renders a newly inserted section", %{conn: conn, session_id: session_id} do - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + test "renders a newly inserted section", %{conn: conn, session: session} do + {:ok, view, _} = live(conn, "/sessions/#{session.id}") - section_id = insert_section(session_id) + section_id = insert_section(session.pid) assert render(view) =~ section_id end - test "renders an updated section name", %{conn: conn, session_id: session_id} do - section_id = insert_section(session_id) + test "renders an updated section name", %{conn: conn, session: session} do + section_id = insert_section(session.pid) - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") - Session.set_section_name(session_id, section_id, "My section") - wait_for_session_update(session_id) + Session.set_section_name(session.pid, section_id, "My section") + wait_for_session_update(session.pid) assert render(view) =~ "My section" end - test "renders a newly inserted cell", %{conn: conn, session_id: session_id} do - section_id = insert_section(session_id) + test "renders a newly inserted cell", %{conn: conn, session: session} do + section_id = insert_section(session.pid) - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") - cell_id = insert_text_cell(session_id, section_id, :markdown) + cell_id = insert_text_cell(session.pid, section_id, :markdown) assert render(view) =~ "cell-" <> cell_id end - test "un-renders a deleted cell", %{conn: conn, session_id: session_id} do - section_id = insert_section(session_id) - cell_id = insert_text_cell(session_id, section_id, :markdown) + test "un-renders a deleted cell", %{conn: conn, session: session} do + section_id = insert_section(session.pid) + cell_id = insert_text_cell(session.pid, section_id, :markdown) - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") - Session.delete_cell(session_id, cell_id) - wait_for_session_update(session_id) + Session.delete_cell(session.pid, cell_id) + wait_for_session_update(session.pid) refute render(view) =~ "cell-" <> cell_id end end describe "UI events update the shared state" do - test "adding a new section", %{conn: conn, session_id: session_id} do - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + test "adding a new section", %{conn: conn, session: session} do + {:ok, view, _} = live(conn, "/sessions/#{session.id}") view |> element("button", "New section") |> render_click() - assert %{notebook: %{sections: [_section]}} = Session.get_data(session_id) + assert %{notebook: %{sections: [_section]}} = Session.get_data(session.pid) end - test "adding a new cell", %{conn: conn, session_id: session_id} do - Session.insert_section(session_id, 0) + test "adding a new cell", %{conn: conn, session: session} do + Session.insert_section(session.pid, 0) - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") view |> element("button", "+ Markdown") |> render_click() assert %{notebook: %{sections: [%{cells: [%Cell.Markdown{}]}]}} = - Session.get_data(session_id) + Session.get_data(session.pid) end - test "queueing cell evaluation", %{conn: conn, session_id: session_id} do - section_id = insert_section(session_id) - cell_id = insert_text_cell(session_id, section_id, :elixir, "Process.sleep(10)") + test "queueing cell evaluation", %{conn: conn, session: session} do + section_id = insert_section(session.pid) + cell_id = insert_text_cell(session.pid, section_id, :elixir, "Process.sleep(10)") - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") view |> element("#session") |> render_hook("queue_cell_evaluation", %{"cell_id" => cell_id}) assert %{cell_infos: %{^cell_id => %{evaluation_status: :evaluating}}} = - Session.get_data(session_id) + Session.get_data(session.pid) end - test "cancelling cell evaluation", %{conn: conn, session_id: session_id} do - section_id = insert_section(session_id) - cell_id = insert_text_cell(session_id, section_id, :elixir, "Process.sleep(2000)") + test "cancelling cell evaluation", %{conn: conn, session: session} do + section_id = insert_section(session.pid) + cell_id = insert_text_cell(session.pid, section_id, :elixir, "Process.sleep(2000)") - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") view |> element("#session") @@ -123,71 +123,71 @@ defmodule LivebookWeb.SessionLiveTest do |> render_hook("cancel_cell_evaluation", %{"cell_id" => cell_id}) assert %{cell_infos: %{^cell_id => %{evaluation_status: :ready}}} = - Session.get_data(session_id) + Session.get_data(session.pid) end - test "inserting a cell below the given cell", %{conn: conn, session_id: session_id} do - section_id = insert_section(session_id) - cell_id = insert_text_cell(session_id, section_id, :elixir) + test "inserting a cell below the given cell", %{conn: conn, session: session} do + section_id = insert_section(session.pid) + cell_id = insert_text_cell(session.pid, section_id, :elixir) - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") view |> element("#session") |> render_hook("insert_cell_below", %{"cell_id" => cell_id, "type" => "markdown"}) assert %{notebook: %{sections: [%{cells: [_first_cell, %Cell.Markdown{}]}]}} = - Session.get_data(session_id) + Session.get_data(session.pid) end - test "inserting a cell above the given cell", %{conn: conn, session_id: session_id} do - section_id = insert_section(session_id) - cell_id = insert_text_cell(session_id, section_id, :elixir) + test "inserting a cell above the given cell", %{conn: conn, session: session} do + section_id = insert_section(session.pid) + cell_id = insert_text_cell(session.pid, section_id, :elixir) - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") view |> element("#session") |> render_hook("insert_cell_above", %{"cell_id" => cell_id, "type" => "markdown"}) assert %{notebook: %{sections: [%{cells: [%Cell.Markdown{}, _first_cell]}]}} = - Session.get_data(session_id) + Session.get_data(session.pid) end - test "deleting the given cell", %{conn: conn, session_id: session_id} do - section_id = insert_section(session_id) - cell_id = insert_text_cell(session_id, section_id, :elixir) + test "deleting the given cell", %{conn: conn, session: session} do + section_id = insert_section(session.pid) + cell_id = insert_text_cell(session.pid, section_id, :elixir) - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") view |> element("#session") |> render_hook("delete_cell", %{"cell_id" => cell_id}) - assert %{notebook: %{sections: [%{cells: []}]}} = Session.get_data(session_id) + assert %{notebook: %{sections: [%{cells: []}]}} = Session.get_data(session.pid) end - test "newlines in input values are normalized", %{conn: conn, session_id: session_id} do - section_id = insert_section(session_id) - cell_id = insert_input_cell(session_id, section_id) + test "newlines in input values are normalized", %{conn: conn, session: session} do + section_id = insert_section(session.pid) + cell_id = insert_input_cell(session.pid, section_id) - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") view |> element(~s/form[phx-change="set_cell_value"]/) |> render_change(%{"value" => "line\r\nline"}) assert %{notebook: %{sections: [%{cells: [%{id: ^cell_id, value: "line\nline"}]}]}} = - Session.get_data(session_id) + Session.get_data(session.pid) end end describe "runtime settings" do test "connecting to elixir standalone updates connect button to reconnect", - %{conn: conn, session_id: session_id} do - {:ok, view, _} = live(conn, "/sessions/#{session_id}/settings/runtime") + %{conn: conn, session: session} do + {:ok, view, _} = live(conn, "/sessions/#{session.id}/settings/runtime") - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") [elixir_standalone_view] = live_children(view) @@ -207,8 +207,8 @@ defmodule LivebookWeb.SessionLiveTest do describe "persistence settings" do @tag :tmp_dir test "saving to file shows the newly created file", - %{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do - {:ok, view, _} = live(conn, "/sessions/#{session_id}/settings/file") + %{conn: conn, session: session, tmp_dir: tmp_dir} do + {:ok, view, _} = live(conn, "/sessions/#{session.id}/settings/file") assert view = find_live_child(view, "persistence") @@ -233,8 +233,8 @@ defmodule LivebookWeb.SessionLiveTest do @tag :tmp_dir test "changing output persistence updates data", - %{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do - {:ok, view, _} = live(conn, "/sessions/#{session_id}/settings/file") + %{conn: conn, session: session, tmp_dir: tmp_dir} do + {:ok, view, _} = live(conn, "/sessions/#{session.id}/settings/file") assert view = find_live_child(view, "persistence") @@ -256,17 +256,17 @@ defmodule LivebookWeb.SessionLiveTest do |> element(~s{button[phx-click="save"]}, "Save") |> render_click() - assert %{notebook: %{persist_outputs: true}} = Session.get_data(session_id) + assert %{notebook: %{persist_outputs: true}} = Session.get_data(session.pid) end end describe "completion" do test "replies with nil completion reference when no runtime is started", - %{conn: conn, session_id: session_id} do - section_id = insert_section(session_id) - cell_id = insert_text_cell(session_id, section_id, :elixir, "Process.sleep(10)") + %{conn: conn, session: session} do + section_id = insert_section(session.pid) + cell_id = insert_text_cell(session.pid, section_id, :elixir, "Process.sleep(10)") - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") view |> element("#session") @@ -280,14 +280,14 @@ defmodule LivebookWeb.SessionLiveTest do end test "replies with completion reference and then sends asynchronous response", - %{conn: conn, session_id: session_id} do - section_id = insert_section(session_id) - cell_id = insert_text_cell(session_id, section_id, :elixir, "Process.sleep(10)") + %{conn: conn, session: session} do + section_id = insert_section(session.pid) + cell_id = insert_text_cell(session.pid, section_id, :elixir, "Process.sleep(10)") {:ok, runtime} = Livebook.Runtime.Embedded.init() - Session.connect_runtime(session_id, runtime) + Session.connect_runtime(session.pid, runtime) - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") view |> element("#session") @@ -307,11 +307,11 @@ defmodule LivebookWeb.SessionLiveTest do end end - test "forking the session", %{conn: conn, session_id: session_id} do - Session.set_notebook_name(session_id, "My notebook") - wait_for_session_update(session_id) + test "forking the session", %{conn: conn, session: session} do + Session.set_notebook_name(session.pid, "My notebook") + wait_for_session_update(session.pid) - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") assert {:error, {:live_redirect, %{to: to}}} = view @@ -325,19 +325,19 @@ defmodule LivebookWeb.SessionLiveTest do end describe "connected users" do - test "lists connected users", %{conn: conn, session_id: session_id} do + test "lists connected users", %{conn: conn, session: session} do user1 = create_user_with_name("Jake Peralta") client_pid = spawn_link(fn -> - Session.register_client(session_id, self(), user1) + Session.register_client(session.pid, self(), user1) receive do :stop -> :ok end end) - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") assert render(view) =~ "Jake Peralta" @@ -345,16 +345,16 @@ defmodule LivebookWeb.SessionLiveTest do end test "updates users list whenever a user joins or leaves", - %{conn: conn, session_id: session_id} do - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + %{conn: conn, session: session} do + {:ok, view, _} = live(conn, "/sessions/#{session.id}") - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") user1 = create_user_with_name("Jake Peralta") client_pid = spawn_link(fn -> - Session.register_client(session_id, self(), user1) + Session.register_client(session.pid, self(), user1) receive do :stop -> :ok @@ -370,21 +370,21 @@ defmodule LivebookWeb.SessionLiveTest do end test "updates users list whenever a user changes his data", - %{conn: conn, session_id: session_id} do + %{conn: conn, session: session} do user1 = create_user_with_name("Jake Peralta") client_pid = spawn_link(fn -> - Session.register_client(session_id, self(), user1) + Session.register_client(session.pid, self(), user1) receive do :stop -> :ok end end) - {:ok, view, _} = live(conn, "/sessions/#{session_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}") - Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session_id}") + Phoenix.PubSub.subscribe(Livebook.PubSub, "sessions:#{session.id}") assert render(view) =~ "Jake Peralta" @@ -399,11 +399,11 @@ defmodule LivebookWeb.SessionLiveTest do end describe "input cell settings" do - test "setting input cell attributes updates data", %{conn: conn, session_id: session_id} do - section_id = insert_section(session_id) - cell_id = insert_input_cell(session_id, section_id) + test "setting input cell attributes updates data", %{conn: conn, session: session} do + section_id = insert_section(session.pid) + cell_id = insert_input_cell(session.pid, section_id) - {:ok, view, _} = live(conn, "/sessions/#{session_id}/cell-settings/#{cell_id}") + {:ok, view, _} = live(conn, "/sessions/#{session.id}/cell-settings/#{cell_id}") form_selector = ~s/[role="dialog"] form/ @@ -439,28 +439,28 @@ defmodule LivebookWeb.SessionLiveTest do } ] } - } = Session.get_data(session_id) + } = Session.get_data(session.pid) end end describe "relative paths" do test "renders an info message when the path doesn't have notebook extension", - %{conn: conn, session_id: session_id} do - session_path = "/sessions/#{session_id}" + %{conn: conn, session: session} do + session_path = "/sessions/#{session.id}" assert {:error, {:live_redirect, %{to: ^session_path}}} = - result = live(conn, "/sessions/#{session_id}/document.pdf") + result = live(conn, "/sessions/#{session.id}/document.pdf") {:ok, view, _} = follow_redirect(result, conn) assert render(view) =~ "Got unrecognised session path: document.pdf" end test "renders an info message when the session has neither original url nor path", - %{conn: conn, session_id: session_id} do - session_path = "/sessions/#{session_id}" + %{conn: conn, session: session} do + session_path = "/sessions/#{session.id}" assert {:error, {:live_redirect, %{to: ^session_path}}} = - result = live(conn, "/sessions/#{session_id}/notebook.livemd") + result = live(conn, "/sessions/#{session.id}/notebook.livemd") {:ok, view, _} = follow_redirect(result, conn) @@ -470,18 +470,18 @@ defmodule LivebookWeb.SessionLiveTest do @tag :tmp_dir test "renders an error message when the relative notebook does not exist", - %{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do + %{conn: conn, session: session, tmp_dir: tmp_dir} do tmp_dir = FileSystem.File.local(tmp_dir <> "/") index_file = FileSystem.File.resolve(tmp_dir, "index.livemd") notebook_file = FileSystem.File.resolve(tmp_dir, "notebook.livemd") - Session.set_file(session_id, index_file) - wait_for_session_update(session_id) + Session.set_file(session.pid, index_file) + wait_for_session_update(session.pid) - session_path = "/sessions/#{session_id}" + session_path = "/sessions/#{session.id}" assert {:error, {:live_redirect, %{to: ^session_path}}} = - result = live(conn, "/sessions/#{session_id}/notebook.livemd") + result = live(conn, "/sessions/#{session.id}/notebook.livemd") {:ok, view, _} = follow_redirect(result, conn) @@ -491,24 +491,25 @@ defmodule LivebookWeb.SessionLiveTest do @tag :tmp_dir test "opens a relative notebook if it exists", - %{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do + %{conn: conn, session: session, tmp_dir: tmp_dir} do tmp_dir = FileSystem.File.local(tmp_dir <> "/") index_file = FileSystem.File.resolve(tmp_dir, "index.livemd") notebook_file = FileSystem.File.resolve(tmp_dir, "notebook.livemd") - Session.set_file(session_id, index_file) - wait_for_session_update(session_id) + Session.set_file(session.pid, index_file) + wait_for_session_update(session.pid) :ok = FileSystem.File.write(notebook_file, "# Sibling notebook") assert {:error, {:live_redirect, %{to: new_session_path}}} = - result = live(conn, "/sessions/#{session_id}/notebook.livemd") + result = live(conn, "/sessions/#{session.id}/notebook.livemd") {:ok, view, _} = follow_redirect(result, conn) assert render(view) =~ "Sibling notebook" "/sessions/" <> session_id = new_session_path - data = Session.get_data(session_id) + {:ok, session} = Sessions.fetch_session(session_id) + data = Session.get_data(session.pid) assert data.file == notebook_file end @@ -519,76 +520,77 @@ defmodule LivebookWeb.SessionLiveTest do index_file = FileSystem.File.resolve(tmp_dir, "index.livemd") notebook_file = FileSystem.File.resolve(tmp_dir, "notebook.livemd") - {:ok, session_id} = SessionSupervisor.create_session(origin: {:file, index_file}) + {:ok, session} = Sessions.create_session(origin: {:file, index_file}) :ok = FileSystem.File.write(notebook_file, "# Sibling notebook") assert {:error, {:live_redirect, %{to: new_session_path}}} = - result = live(conn, "/sessions/#{session_id}/notebook.livemd") + result = live(conn, "/sessions/#{session.id}/notebook.livemd") {:ok, view, _} = follow_redirect(result, conn) assert render(view) =~ "Sibling notebook - fork" "/sessions/" <> session_id = new_session_path - data = Session.get_data(session_id) + {:ok, session} = Sessions.fetch_session(session_id) + data = Session.get_data(session.pid) assert data.file == nil assert data.origin == {:file, notebook_file} end @tag :tmp_dir test "if the notebook is already open, redirects to the session", - %{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do + %{conn: conn, session: session, tmp_dir: tmp_dir} do tmp_dir = FileSystem.File.local(tmp_dir <> "/") index_file = FileSystem.File.resolve(tmp_dir, "index.livemd") notebook_file = FileSystem.File.resolve(tmp_dir, "notebook.livemd") - Session.set_file(session_id, index_file) - wait_for_session_update(session_id) + Session.set_file(session.pid, index_file) + wait_for_session_update(session.pid) :ok = FileSystem.File.write(notebook_file, "# Sibling notebook") assert {:error, {:live_redirect, %{to: session_path}}} = - live(conn, "/sessions/#{session_id}/notebook.livemd") + live(conn, "/sessions/#{session.id}/notebook.livemd") assert {:error, {:live_redirect, %{to: ^session_path}}} = - live(conn, "/sessions/#{session_id}/notebook.livemd") + live(conn, "/sessions/#{session.id}/notebook.livemd") end @tag :tmp_dir - test "handles nested paths", %{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do + test "handles nested paths", %{conn: conn, session: session, tmp_dir: tmp_dir} do tmp_dir = FileSystem.File.local(tmp_dir <> "/") parent_file = FileSystem.File.resolve(tmp_dir, "parent.livemd") child_dir = FileSystem.File.resolve(tmp_dir, "dir/") child_file = FileSystem.File.resolve(child_dir, "child.livemd") - Session.set_file(session_id, parent_file) - wait_for_session_update(session_id) + Session.set_file(session.pid, parent_file) + wait_for_session_update(session.pid) :ok = FileSystem.File.write(child_file, "# Child notebook") {:ok, view, _} = conn - |> live("/sessions/#{session_id}/dir/child.livemd") + |> live("/sessions/#{session.id}/dir/child.livemd") |> follow_redirect(conn) assert render(view) =~ "Child notebook" end @tag :tmp_dir - test "handles parent paths", %{conn: conn, session_id: session_id, tmp_dir: tmp_dir} do + test "handles parent paths", %{conn: conn, session: session, tmp_dir: tmp_dir} do tmp_dir = FileSystem.File.local(tmp_dir <> "/") parent_file = FileSystem.File.resolve(tmp_dir, "parent.livemd") child_dir = FileSystem.File.resolve(tmp_dir, "dir/") child_file = FileSystem.File.resolve(child_dir, "child.livemd") - Session.set_file(session_id, child_file) - wait_for_session_update(session_id) + Session.set_file(session.pid, child_file) + wait_for_session_update(session.pid) :ok = FileSystem.File.write(parent_file, "# Parent notebook") {:ok, view, _} = conn - |> live("/sessions/#{session_id}/__parent__/parent.livemd") + |> live("/sessions/#{session.id}/__parent__/parent.livemd") |> follow_redirect(conn) assert render(view) =~ "Parent notebook" @@ -604,11 +606,11 @@ defmodule LivebookWeb.SessionLiveTest do end) index_url = url(bypass.port) <> "/index.livemd" - {:ok, session_id} = SessionSupervisor.create_session(origin: {:url, index_url}) + {:ok, session} = Sessions.create_session(origin: {:url, index_url}) {:ok, view, _} = conn - |> live("/sessions/#{session_id}/notebook.livemd") + |> live("/sessions/#{session.id}/notebook.livemd") |> follow_redirect(conn) assert render(view) =~ "My notebook" @@ -623,12 +625,12 @@ defmodule LivebookWeb.SessionLiveTest do index_url = url(bypass.port) <> "/index.livemd" - {:ok, session_id} = SessionSupervisor.create_session(origin: {:url, index_url}) + {:ok, session} = Sessions.create_session(origin: {:url, index_url}) - session_path = "/sessions/#{session_id}" + session_path = "/sessions/#{session.id}" assert {:error, {:live_redirect, %{to: ^session_path}}} = - result = live(conn, "/sessions/#{session_id}/notebook.livemd") + result = live(conn, "/sessions/#{session.id}/notebook.livemd") {:ok, view, _} = follow_redirect(result, conn) assert render(view) =~ "Cannot navigate, failed to download notebook from the given URL" @@ -639,13 +641,13 @@ defmodule LivebookWeb.SessionLiveTest do index_url = "http://example.com/#{test}/index.livemd" notebook_url = "http://example.com/#{test}/notebook.livemd" - {:ok, index_session_id} = SessionSupervisor.create_session(origin: {:url, index_url}) - {:ok, notebook_session_id} = SessionSupervisor.create_session(origin: {:url, notebook_url}) + {:ok, index_session} = Sessions.create_session(origin: {:url, index_url}) + {:ok, notebook_session} = Sessions.create_session(origin: {:url, notebook_url}) - notebook_session_path = "/sessions/#{notebook_session_id}" + notebook_session_path = "/sessions/#{notebook_session.id}" assert {:error, {:live_redirect, %{to: ^notebook_session_path}}} = - live(conn, "/sessions/#{index_session_id}/notebook.livemd") + live(conn, "/sessions/#{index_session.id}/notebook.livemd") end test "renders an error message if there are already multiple session imported from the relative URL", @@ -653,18 +655,14 @@ defmodule LivebookWeb.SessionLiveTest do index_url = "http://example.com/#{test}/index.livemd" notebook_url = "http://example.com/#{test}/notebook.livemd" - {:ok, index_session_id} = SessionSupervisor.create_session(origin: {:url, index_url}) + {:ok, index_session} = Sessions.create_session(origin: {:url, index_url}) + {:ok, _notebook_session1} = Sessions.create_session(origin: {:url, notebook_url}) + {:ok, _notebook_session2} = Sessions.create_session(origin: {:url, notebook_url}) - {:ok, _notebook_session_id1} = - SessionSupervisor.create_session(origin: {:url, notebook_url}) - - {:ok, _notebook_session_id2} = - SessionSupervisor.create_session(origin: {:url, notebook_url}) - - index_session_path = "/sessions/#{index_session_id}" + index_session_path = "/sessions/#{index_session.id}" assert {:error, {:live_redirect, %{to: ^index_session_path}}} = - result = live(conn, "/sessions/#{index_session_id}/notebook.livemd") + result = live(conn, "/sessions/#{index_session.id}/notebook.livemd") {:ok, view, _} = follow_redirect(result, conn) @@ -675,41 +673,41 @@ defmodule LivebookWeb.SessionLiveTest do # Helpers - defp wait_for_session_update(session_id) do + 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_id) + Session.get_data(session_pid) :ok end # Utils for sending session requests, waiting for the change to be applied # and retrieving new ids if applicable. - defp insert_section(session_id) do - Session.insert_section(session_id, 0) - %{notebook: %{sections: [section]}} = Session.get_data(session_id) + defp insert_section(session_pid) do + Session.insert_section(session_pid, 0) + %{notebook: %{sections: [section]}} = Session.get_data(session_pid) section.id end - defp insert_text_cell(session_id, section_id, type, content \\ "") do - Session.insert_cell(session_id, section_id, 0, type) - %{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_id) + defp insert_text_cell(session_pid, section_id, type, content \\ "") do + Session.insert_cell(session_pid, section_id, 0, type) + %{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_pid) # We need to register ourselves as a client to start submitting cell deltas user = Livebook.Users.User.new() - Session.register_client(session_id, self(), user) + Session.register_client(session_pid, self(), user) delta = Delta.new(insert: content) - Session.apply_cell_delta(session_id, cell.id, delta, 1) + Session.apply_cell_delta(session_pid, cell.id, delta, 1) - wait_for_session_update(session_id) + wait_for_session_update(session_pid) cell.id end - defp insert_input_cell(session_id, section_id) do - Session.insert_cell(session_id, section_id, 0, :input) - %{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_id) + defp insert_input_cell(session_pid, section_id) do + Session.insert_cell(session_pid, section_id, 0, :input) + %{notebook: %{sections: [%{cells: [cell]}]}} = Session.get_data(session_pid) cell.id end