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/storage/ets.ex
defmodule Livebook.Storage.Ets do
@moduledoc """
Ets implementation of `Livebook.Storage` behaviour.
The module is supposed to be started just once as it
is responsible for managing a named ets table.
`insert` and `delete` operations are supposed to be called using a GenServer
while all the lookups can be performed by directly accessing the named table.
"""
@behaviour Livebook.Storage
require Logger
use GenServer
@impl Livebook.Storage
def all(namespace) do
table_name()
|> :ets.match({{namespace, :"$1"}, :"$2", :"$3", :_})
|> Enum.group_by(
fn [entity_id, _attr, _val] -> entity_id end,
fn [_id, attr, val] -> {attr, val} end
)
|> Enum.map(fn {entity_id, attributes} ->
attributes
|> Map.new()
|> Map.put(:id, entity_id)
end)
end
@impl Livebook.Storage
def fetch(namespace, entity_id) do
table_name()
|> :ets.lookup({namespace, entity_id})
|> case do
[] ->
:error
entries ->
entries
|> Enum.map(fn {_key, attr, val, _timestamp} -> {attr, val} end)
|> Map.new()
|> Map.put(:id, entity_id)
|> then(&{:ok, &1})
end
end
@impl Livebook.Storage
def fetch_key(namespace, entity_id, key) do
table_name()
|> :ets.match({{namespace, entity_id}, key, :"$1", :_})
|> case do
[[value]] -> {:ok, value}
[] -> :error
end
end
@spec config_file_path() :: Path.t()
def config_file_path() do
Path.join([Livebook.Config.data_path(), "livebook_config.ets"])
end
@spec sync() :: :ok
def sync() do
GenServer.call(__MODULE__, :sync)
end
@impl Livebook.Storage
def insert(namespace, entity_id, attributes) do
GenServer.call(__MODULE__, {:insert, namespace, entity_id, attributes})
end
@impl Livebook.Storage
def delete(namespace, entity_id) do
GenServer.call(__MODULE__, {:delete, namespace, entity_id})
end
@impl Livebook.Storage
def delete_key(namespace, entity_id, key) do
GenServer.call(__MODULE__, {:delete_key, namespace, entity_id, key})
end
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl GenServer
def init(_opts) do
# Make sure that this process does not terminate abruptly
# in case it is persisting to disk. terminate/2 is still a no-op.
Process.flag(:trap_exit, true)
table = load_or_create_table()
:persistent_term.put(__MODULE__, table)
{:ok, %{table: table}}
end
@impl GenServer
def handle_call(:sync, _from, state) do
{:reply, :ok, state}
end
@impl GenServer
def handle_call({:insert, namespace, entity_id, attributes}, _from, %{table: table} = state) do
keys_to_delete = Enum.map(attributes, fn {key, _val} -> key end)
delete_keys(table, namespace, entity_id, keys_to_delete)
timestamp = System.os_time(:millisecond)
attributes =
Enum.map(attributes, fn {attr, val} ->
{{namespace, entity_id}, attr, val, timestamp}
end)
:ets.insert(table, attributes)
{:reply, :ok, state, {:continue, :save_to_file}}
end
@impl GenServer
def handle_call({:delete, namespace, entity_id}, _from, %{table: table} = state) do
:ets.delete(table, {namespace, entity_id})
{:reply, :ok, state, {:continue, :save_to_file}}
end
@impl GenServer
def handle_call({:delete_key, namespace, entity_id, key}, _from, %{table: table} = state) do
delete_keys(table, namespace, entity_id, [key])
{:reply, :ok, state, {:continue, :save_to_file}}
end
@impl GenServer
def handle_continue(:save_to_file, %{table: table} = state) do
file_path = String.to_charlist(config_file_path())
:ok = :ets.tab2file(table, file_path)
{:noreply, state}
end
defp table_name(), do: :persistent_term.get(__MODULE__)
defp load_or_create_table() do
config_file_path()
|> String.to_charlist()
|> :ets.file2tab()
|> case do
{:ok, tab} ->
tab
{:error, reason} ->
case reason do
{:read_error, {:file_error, _, :enoent}} -> :ok
_ -> Logger.warning("Could not open up #{config_file_path()}: #{inspect(reason)}")
end
:ets.new(__MODULE__, [:protected, :duplicate_bag])
end
end
defp delete_keys(table, namespace, entity_id, keys) do
match_head = {{namespace, entity_id}, :"$1", :_, :_}
guards = Enum.map(keys, &{:==, :"$1", &1})
:ets.select_delete(table, [{match_head, guards, [true]}])
end
end