Packages
livebook
0.6.2
0.19.8
0.19.7
0.19.6
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.14.0-rc.1
0.14.0-rc.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.8.2
0.8.1
0.8.0
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.2
0.1.1
0.1.0
Automate code & data workflows with interactive notebooks
Current section
Files
Jump to
Current section
Files
lib/livebook/runtime/evaluator/object_tracker.ex
defmodule Livebook.Runtime.Evaluator.ObjectTracker do
@moduledoc false
# This module is an abstraction for tracking objects,
# references to them and garbage collection.
#
# Every object is identified by an arbitrary unique term.
# Processes can reference those objects by adding a pair
# of `{pid, scope}`, scope is an optional additinal term
# distinguishing the reference.
#
# Each reference can be released either manually by calling
# `remove_reference/2` or automatically when the pointing
# process terminates.
#
# When all references for the given object are removed,
# all messages scheduled with `monitor/3` are sent.
use GenServer
@type state :: %{
objects: %{
object() => %{
references: list(object_reference()),
monitors: list(monitor())
}
}
}
@typedoc """
Arbitrary term identifying an object.
"""
@type object :: term()
@typedoc """
Reference to an object with an optional scope.
"""
@type object_reference :: {process :: pid(), scope :: term()}
@typedoc """
Scheduled message to be sent when an object is released.
"""
@type monitor :: {Process.dest(), payload :: term()}
@doc """
Starts a new object tracker.
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts)
end
@doc """
Adds a reference to the given object.
"""
@spec add_reference(pid(), object(), object_reference()) :: :ok
def add_reference(object_tracker, object, reference) do
GenServer.cast(object_tracker, {:add_reference, object, reference})
end
@doc """
Removes the given reference from all objects it is attached to.
"""
@spec remove_reference(pid(), object_reference()) :: :ok
def remove_reference(object_tracker, reference) do
GenServer.cast(object_tracker, {:remove_reference, reference})
end
@doc """
Schedules `payload` to be send to `destination` when the object
is released.
"""
@spec monitor(pid(), object(), Process.dest(), term()) :: :ok | {:error, :bad_object}
def monitor(object_tracker, object, destination, payload) do
GenServer.call(object_tracker, {:monitor, object, destination, payload})
end
@impl true
def init(_opts) do
{:ok, %{objects: %{}}}
end
@impl true
def handle_cast({:add_reference, object, reference}, state) do
{parent, _scope} = reference
Process.monitor(parent)
state =
if state.objects[object] do
update_in(state.objects[object].references, fn references ->
if reference in references, do: references, else: [reference | references]
end)
else
put_in(state.objects[object], %{references: [reference], monitors: []})
end
{:noreply, state}
end
def handle_cast({:remove_reference, reference}, state) do
state = update_references(state, fn references -> List.delete(references, reference) end)
{:noreply, garbage_collect(state)}
end
@impl true
def handle_call({:monitor, object, destination, payload}, _from, state) do
monitor = {destination, payload}
if state.objects[object] do
state =
update_in(state.objects[object].monitors, fn monitors ->
if monitor in monitors, do: monitors, else: [monitor | monitors]
end)
{:reply, :ok, garbage_collect(state)}
else
{:reply, {:error, :bad_object}, state}
end
end
@impl true
def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do
state =
update_references(state, fn references ->
Enum.reject(references, &match?({^pid, _}, &1))
end)
{:noreply, garbage_collect(state)}
end
# Updates references for every object with the given function
defp update_references(state, fun) do
update_in(state.objects, fn objects ->
for {object, %{references: references} = info} <- objects, into: %{} do
{object, %{info | references: fun.(references)}}
end
end)
end
defp garbage_collect(state) do
{to_release, objects} = Enum.split_with(state.objects, &match?({_, %{references: []}}, &1))
for {_, %{monitors: monitors}} <- to_release, {dest, payload} <- monitors do
send(dest, payload)
end
%{state | objects: Map.new(objects)}
end
end