Packages
electric
1.0.1
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.4.16
1.4.16-beta-1
1.4.15
1.4.14
1.4.13
1.4.12
1.4.11
1.4.10
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.4
1.3.3
1.3.2
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
retired
1.1.4
retired
1.1.3
retired
1.1.2
1.1.1
1.1.0
1.0.24
1.0.23
1.0.22
1.0.21
1.0.20
1.0.19
1.0.18
1.0.17
1.0.15
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-beta.23
1.0.0-beta.22
1.0.0-beta.20
1.0.0-beta.19
1.0.0-beta.18
1.0.0-beta.17
1.0.0-beta.16
1.0.0-beta.15
1.0.0-beta.14
1.0.0-beta.13
1.0.0-beta.12
1.0.0-beta.11
1.0.0-beta.10
1.0.0-beta.9
1.0.0-beta.8
1.0.0-beta.7
1.0.0-beta.6
1.0.0-beta.5
1.0.0-beta.4
1.0.0-beta.3
1.0.0-beta.2
1.0.0-beta.1
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.3
0.6.2
0.6.1
0.5.2
0.4.4
Postgres sync engine. Sync little subsets of your Postgres data into local apps and services.
Current section
Files
Jump to
Current section
Files
lib/electric/shape_cache/in_memory_storage.ex
defmodule Electric.ShapeCache.InMemoryStorage do
use Agent
alias Electric.ConcurrentStream
alias Electric.Replication.LogOffset
import Electric.Replication.LogOffset, only: :macros
alias Electric.Telemetry.OpenTelemetry
alias __MODULE__, as: MS
@behaviour Electric.ShapeCache.Storage
@snapshot_start_index 0
@snapshot_end_index :end
@pg_snapshot_key :pg_snapshot
defstruct [
:table_base_name,
:snapshot_table,
:log_table,
:chunk_checkpoint_table,
:shape_handle,
:stack_id
]
@impl Electric.ShapeCache.Storage
def shared_opts(opts) do
stack_id = Access.fetch!(opts, :stack_id)
table_base_name = Access.get(opts, :table_base_name, __MODULE__)
%{
table_base_name: table_base_name,
stack_id: stack_id
}
end
def name(stack_id, shape_handle) when is_binary(shape_handle) do
Electric.ProcessRegistry.name(stack_id, __MODULE__, shape_handle)
end
@impl Electric.ShapeCache.Storage
def for_shape(shape_handle, %{shape_handle: shape_handle} = opts) do
opts
end
def for_shape(shape_handle, %{
table_base_name: table_base_name,
stack_id: stack_id
}) do
snapshot_table_name = :"#{table_base_name}.Snapshot_#{shape_handle}"
log_table_name = :"#{table_base_name}.Log_#{shape_handle}"
chunk_checkpoint_table_name =
:"#{table_base_name}.ChunkCheckpoint_#{shape_handle}"
%__MODULE__{
table_base_name: table_base_name,
shape_handle: shape_handle,
snapshot_table: snapshot_table_name,
log_table: log_table_name,
chunk_checkpoint_table: chunk_checkpoint_table_name,
stack_id: stack_id
}
end
@impl Electric.ShapeCache.Storage
def start_link(%MS{} = opts) do
if is_nil(opts.shape_handle), do: raise("cannot start an un-attached storage instance")
if is_nil(opts.stack_id), do: raise("stack_id cannot be nil")
Agent.start_link(
fn ->
%{
snapshot_table: storage_table(opts.snapshot_table),
log_table: storage_table(opts.log_table),
chunk_checkpoint_table: storage_table(opts.chunk_checkpoint_table)
}
end,
name: name(opts.stack_id, opts.shape_handle)
)
end
defp storage_table(name) do
:ets.new(name, [:public, :named_table, :ordered_set])
end
@impl Electric.ShapeCache.Storage
def get_current_position(%MS{} = opts) do
{:ok, current_offset(opts), pg_snapshot(opts)}
end
defp pg_snapshot(opts) do
case :ets.lookup(opts.snapshot_table, @pg_snapshot_key) do
[{@pg_snapshot_key, pg_snapshot}] -> pg_snapshot
[] -> nil
end
end
defp current_offset(_opts) do
LogOffset.last_before_real_offsets()
end
@impl Electric.ShapeCache.Storage
def set_pg_snapshot(pg_snapshot, %MS{} = opts) do
:ets.insert(opts.snapshot_table, {@pg_snapshot_key, pg_snapshot})
:ok
end
@impl Electric.ShapeCache.Storage
def initialise(%MS{} = _opts), do: :ok
@impl Electric.ShapeCache.Storage
def set_shape_definition(_shape, %MS{} = _opts) do
# no-op - only used to restore shapes between sessions
:ok
end
@impl Electric.ShapeCache.Storage
def get_all_stored_shapes(_opts) do
# shapes not stored, empty map returned
{:ok, %{}}
end
@impl Electric.ShapeCache.Storage
def get_total_disk_usage(_opts) do
0
end
@impl Electric.ShapeCache.Storage
def snapshot_started?(%MS{} = opts) do
try do
:ets.member(opts.snapshot_table, snapshot_start())
rescue
ArgumentError ->
false
end
end
defp snapshot_key(chunk_key, index) do
{chunk_key, index}
end
defp snapshot_chunk_start(chunk_key), do: snapshot_key(chunk_key, @snapshot_start_index)
defp snapshot_chunk_end(chunk_key), do: snapshot_key(chunk_key, @snapshot_end_index)
defp snapshot_start(), do: snapshot_chunk_start(storage_offset(LogOffset.before_all()))
defp snapshot_end(),
do: snapshot_chunk_end(storage_offset(LogOffset.last_before_real_offsets()))
defp get_offset_indexed_stream(offset, max_offset, offset_indexed_table) do
offset = storage_offset(offset)
max_offset = storage_offset(max_offset)
Stream.unfold(offset, fn offset ->
case :ets.next_lookup(offset_indexed_table, {:offset, offset}) do
:"$end_of_table" ->
nil
{{:offset, position}, _} when position > max_offset ->
nil
{{:offset, position}, [{_, item}]} ->
{item, position}
end
end)
end
@snapshot_boundary_offset LogOffset.last_before_real_offsets()
@impl Electric.ShapeCache.Storage
def get_log_stream(offset, max_offset, %MS{} = opts)
when is_log_offset_lt(offset, @snapshot_boundary_offset) do
case :ets.lookup_element(opts.snapshot_table, snapshot_end(), 2, nil) do
nil -> stream_from_snapshot(offset, max_offset, opts)
max when is_log_offset_lt(offset, max) -> stream_from_snapshot(offset, max_offset, opts)
_ -> get_offset_indexed_stream(offset, max_offset, opts.log_table)
end
end
def get_log_stream(offset, max_offset, %MS{} = opts) do
get_offset_indexed_stream(offset, max_offset, opts.log_table)
end
defp stream_from_snapshot(offset, max_offset, %MS{} = opts) do
ConcurrentStream.stream_to_end(
excluded_start_key: snapshot_chunk_end(storage_offset(offset)),
end_marker_key: snapshot_chunk_end(storage_offset(max_offset)),
poll_time_in_ms: 10,
stream_fun: fn excluded_start_key, included_end_key ->
if !snapshot_started?(opts), do: raise("Snapshot no longer available")
:ets.select(
opts.snapshot_table,
[
{{:"$1", :"$2"},
[
{:andalso, {:>, :"$1", {:const, excluded_start_key}},
{:"=<", :"$1", {:const, included_end_key}}}
], [{{:"$1", :"$2"}}]}
]
)
end
)
|> Stream.map(fn {_, item} -> item end)
|> Stream.reject(&is_nil/1)
end
@impl Electric.ShapeCache.Storage
def get_chunk_end_log_offset(offset, _) when is_min_offset(offset),
do: LogOffset.first()
def get_chunk_end_log_offset(offset, %MS{} = opts) do
case :ets.next_lookup(opts.chunk_checkpoint_table, storage_offset(offset)) do
:"$end_of_table" ->
nil
{chunk_offset, _} ->
LogOffset.new(chunk_offset)
end
end
@impl Electric.ShapeCache.Storage
def make_new_snapshot!(data_stream, %MS{stack_id: stack_id} = opts) do
OpenTelemetry.with_span(
"storage.make_new_snapshot",
[storage_impl: "in_memory", "shape.handle": opts.shape_handle],
stack_id,
fn ->
table = opts.snapshot_table
chunk_checkpoint_table = opts.chunk_checkpoint_table
data_stream
|> Stream.with_index(1)
|> Stream.transform(
fn -> 0 end,
fn
{:chunk_boundary, _}, chunk_num ->
chunk_offset = storage_offset(LogOffset.new(0, chunk_num))
{[
{chunk_offset, :snapshot_checkpoint},
{snapshot_chunk_end(chunk_offset), nil}
], chunk_num + 1}
{line, index}, chunk_num ->
chunk_offset = storage_offset(LogOffset.new(0, chunk_num))
{[{snapshot_key(chunk_offset, index), line}], chunk_num}
end,
fn chunk_num ->
chunk_offset = storage_offset(LogOffset.new(0, chunk_num))
{[{chunk_offset, :snapshot_checkpoint}, {snapshot_chunk_end(chunk_offset), nil}],
chunk_num}
end,
fn _ -> nil end
)
|> Stream.chunk_every(500)
|> Stream.flat_map(fn chunk ->
{checkpoints, data} = Enum.split_with(chunk, &match?({_, :snapshot_checkpoint}, &1))
:ets.insert(chunk_checkpoint_table, checkpoints)
:ets.insert(table, data)
Enum.map(checkpoints, &elem(&1, 0))
end)
|> Enum.max()
|> then(fn max_chunk ->
:ets.insert(table, {snapshot_end(), LogOffset.new(max_chunk)})
end)
:ok
end
)
end
@impl Electric.ShapeCache.Storage
def mark_snapshot_as_started(%MS{} = opts) do
:ets.insert(opts.snapshot_table, {snapshot_start(), 0})
:ok
end
@impl Electric.ShapeCache.Storage
def append_to_log!(log_items, %MS{} = opts) do
log_table = opts.log_table
chunk_checkpoint_table = opts.chunk_checkpoint_table
log_items
|> Enum.map(fn
{:chunk_boundary, offset} ->
{storage_offset(offset), :checkpoint}
{offset, _key, _op_type, json_log_item} ->
{{:offset, storage_offset(offset)}, json_log_item}
end)
|> Enum.split_with(fn item -> match?({_, :checkpoint}, item) end)
|> then(fn {checkpoints, log_items} ->
:ets.insert(chunk_checkpoint_table, checkpoints)
:ets.insert(log_table, log_items)
log_items
end)
:ok
end
@impl Electric.ShapeCache.Storage
def cleanup!(%MS{} = opts) do
for table <- tables(opts), do: :ets.delete_all_objects(table)
:ok
rescue
_ ->
:ok
end
@impl Electric.ShapeCache.Storage
def unsafe_cleanup!(%MS{} = opts) do
for table <- tables(opts),
do: ignoring_exceptions(fn -> :ets.delete(table) end, ArgumentError)
:ok
end
defp ignoring_exceptions(fun, exception) do
fun.()
rescue
error ->
if error.__struct__ == exception do
:ok
else
reraise(error, __STACKTRACE__)
end
end
defp tables(%MS{} = opts) do
[
opts.snapshot_table,
opts.log_table,
opts.chunk_checkpoint_table
]
end
# Turns a LogOffset into a tuple representation
# for storing in the ETS table
defp storage_offset(offset) do
LogOffset.to_tuple(offset)
end
end