Packages
electric
1.6.7
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/pure_file_storage/key_index.ex
defmodule Electric.ShapeCache.PureFileStorage.KeyIndex do
alias Electric.ShapeCache.Storage
alias Electric.Utils
alias Electric.ShapeCache.PureFileStorage.LogFile
alias Electric.Replication.LogOffset
@key_index_entry_size 1 + 16 + 1 + 8 + 8
@key_index_full_size @key_index_entry_size + 4
@spec make_entry(LogFile.log_item_with_sizes(), non_neg_integer(), non_neg_integer()) ::
{iodata(), iodata_size :: non_neg_integer()}
def make_entry(
{offset, key_size, key, op_type, _, json_size, _},
log_file_entry_start_pos,
label \\ 0
) do
{[
<<key_size + @key_index_entry_size::32, label::8, LogOffset.to_int128(offset)::binary,
op_type::8, log_file_entry_start_pos::64, json_size::64>>,
key
], key_size + @key_index_full_size}
end
def expected_position(pos, {_, key_size, _, _, _, _, _}),
do: @key_index_full_size + key_size + pos
def read_key_file(path) do
File.open!(path, [:read, :raw, :read_ahead], fn file ->
Stream.unfold({file, 0}, fn {file, pos} ->
with <<total_size::32, _label::8, tx_offset::64, op_offset::64, op_type::8,
log_file_entry_start_pos::64, json_size::64>> <-
IO.binread(file, @key_index_full_size),
<<key::binary-size(^total_size - @key_index_entry_size)>> <-
IO.binread(file, total_size - @key_index_entry_size) do
{{key, LogOffset.new(tx_offset, op_offset), op_type, log_file_entry_start_pos,
json_size}, {file, pos + total_size + 4}}
else
:eof ->
nil
_ ->
raise Storage.Error,
message:
"Incomplete keyfile entry at #{pos} (started reading from #{pos}, file #{path})"
end
end)
|> Enum.to_list()
end)
end
def trim(path, log_file_path, search_start_pos)
when is_binary(path) and is_binary(log_file_path) and is_integer(search_start_pos) do
# We are syncing the main log more often than the keyfile, so keyfile might be behind the log.
# We need to find the last full entry.
log_file_entry_end =
File.open!(path, [:read, :raw, :write, :read_ahead], fn file ->
:file.position(file, search_start_pos)
find_end_position(file, search_start_pos)
end)
LogFile.stream_entries(log_file_path, log_file_entry_end)
|> Stream.map(fn {entry, pos} -> elem(make_entry(entry, pos), 0) end)
|> Stream.into(File.stream!(path, [:delayed_write]))
end
defp find_end_position(file, search_start_pos) do
Stream.unfold(search_start_pos, fn position ->
with <<total_size::32, _::8, _::64, _::64, _::8, line_start_pos::64, json_size::64>> <-
IO.binread(file, @key_index_full_size),
<<_::binary-size(^total_size - @key_index_full_size)>> <-
IO.binread(file, total_size - @key_index_full_size) do
{
LogFile.expected_position(
line_start_pos,
total_size - @key_index_entry_size,
json_size
),
position + total_size + 4
}
else
_ ->
# Either EOF, or incomplete write - truncate works for both
:file.position(file, position)
:file.truncate(file)
nil
end
end)
|> Enum.reduce(0, fn elem, _acc -> elem end)
end
def stream_for_actions(path, starting_pos \\ 0) do
Stream.resource(
fn ->
file = File.open!(path, [:read, :raw, :read_ahead])
{:ok, ^starting_pos} = :file.position(file, starting_pos)
{file, starting_pos}
end,
fn {file, pos} ->
with <<total_size::32, label::8, tx_offset::64, op_offset::64, op_type::8,
log_file_entry_start_pos::64, json_size::64>> <-
IO.binread(file, @key_index_full_size),
<<key::binary-size(^total_size - @key_index_entry_size)>> <-
IO.binread(file, total_size - @key_index_entry_size) do
{[
{{total_size - @key_index_entry_size, key}, label,
LogOffset.new(tx_offset, op_offset), op_type, log_file_entry_start_pos, json_size}
], {file, pos + total_size + 4}}
else
:eof ->
{:halt, {file, pos}}
_ ->
raise Storage.Error,
message:
"Incomplete keyfile entry at #{pos} (started reading from #{starting_pos}, file #{path})"
end
end,
fn {file, _} -> File.close(file) end
)
end
@doc """
This is a "side-write" function when log file is being written start-to-end.
"Live" keyfile appends shouldn't be done using this function because we're not controlling
flush points.
"""
# label 0 is reserved for main log, which shouldn't be written through this function.
def write_from_stream(stream, path, label) when label != 0 do
Utils.stream_add_side_effect(
stream,
# We're using delayed writes to avoid interfering with writing the log. Write size here is 64KB or 1s delay
# It's used here because we're writing a line per log line, so this reduces disk contention
fn -> {File.open!(path, [:write, :raw, :delayed_write]), 0} end,
fn line, {file, write_position} ->
{data, _} = make_entry(line, write_position, label)
IO.binwrite(file, data)
{file, LogFile.expected_position(write_position, line)}
end,
&File.close(elem(&1, 0))
)
end
def create_from_log(log_file_path, key_index_path, end_pos \\ :eof) do
LogFile.stream_entries(log_file_path, 0, end_pos)
|> Stream.map(fn {entry, pos} -> elem(make_entry(entry, pos), 0) end)
|> Stream.into(File.stream!(key_index_path, [:delayed_write]))
|> Stream.run()
end
def sort(inputs, output) do
:ok =
:file_sorter.sort(inputs |> Enum.map(&to_charlist/1), to_charlist(output),
format: fn <<_::binary-size(@key_index_entry_size), key::binary>> -> key end
)
end
def copy_adjusting_positions(source, target, starting_pos, adjustment) when adjustment <= 0 do
source
|> stream_for_actions(starting_pos)
|> Stream.map(fn {{key_size, key}, label, offset, op_type, start_pos, json_size} ->
make_entry(
{offset, key_size, key, op_type, 0, json_size, ""},
start_pos + adjustment,
label
)
|> elem(0)
end)
|> Stream.into(File.stream!(target, [:delayed_write]))
|> Stream.run()
end
end