Current section

Files

Jump to
electric lib electric postgres snapshot_query.ex
Raw

lib/electric/postgres/snapshot_query.ex

defmodule Electric.Postgres.SnapshotQuery do
alias Electric.Postgres.Lsn
alias Electric.SnapshotError
alias Electric.Shapes.{Querying, Shape}
alias Electric.Telemetry.OpenTelemetry
@type pg_snapshot() ::
{xmin :: pos_integer(), xmax :: pos_integer(), xip_list :: [pos_integer()]}
@doc """
Execute a snapshot query for a shape in a isolated readonly transaction.
This function operates on two callbacks: `snapshot_info_fn` and `query_fn`.
`snapshot_info_fn` is called with the shape handle, the pg_snapshot, and the lsn as soon
as the snapshot information for the started transaction is available.
`query_fn` is called with the connection, the pg_snapshot, and the lsn and
is expected to do all the work querying and dealing with the results.
The query function is executed within a transaction, so it shouldn't return
a stream (as it will fail to be read after the transaction is committed), but
rather should execute all desired side-effects or materialize the results.
Query will be executed within a REPEATABLE READ READ ONLY transaction, with
correct display settings set.
Options:
- `:query_fn` - the function to execute the query.
- `:snapshot_info_fn` - the function to call with the snapshot information.
- `:stack_id` - the stack id for this shape.
"""
@spec execute_for_shape(Postgrex.conn(), Shape.handle(), Shape.t(), [option]) ::
{:ok, result} | {:error, any()}
when result: term(),
option:
{:snapshot_info_fn, (Shape.handle(), pg_snapshot, pos_integer() -> any())}
| {:query_fn, (Postgrex.conn(), pg_snapshot, pos_integer() -> result)}
| {:stack_id, Electric.stack_id()},
pg_snapshot:
{xmin :: pos_integer(), xmax :: pos_integer(), xip_list :: [pos_integer()]}
def execute_for_shape(pool, shape_handle, shape, opts) do
query_fn = Access.fetch!(opts, :query_fn)
snapshot_info_fn = Access.fetch!(opts, :snapshot_info_fn)
query_reason = Access.get(opts, :query_reason, "initial_snapshot")
span_attrs = Shape.otel_attrs(shape_handle, shape, query_reason: query_reason)
stack_id = Access.fetch!(opts, :stack_id)
OpenTelemetry.with_child_span(
"shape_snapshot.execute_for_shape",
span_attrs,
stack_id,
fn ->
OpenTelemetry.start_interval(:"shape_snapshot.checkout_wait.duration_µs")
Postgrex.transaction(
pool,
fn conn ->
OpenTelemetry.start_interval(:"shape_snapshot.setup.duration_µs")
ctx = %{
conn: conn,
stack_id: stack_id,
span_attrs: span_attrs
}
query!(ctx, "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY",
span_name: "shape_snapshot.start_readonly_txn"
)
[%{rows: [[pg_snapshot, lsn]]}] =
query!(ctx, "SELECT pg_current_snapshot(), pg_current_wal_lsn()",
span_name: "shape_snapshot.get_pg_snapshot"
)
snapshot_info_fn.(shape_handle, pg_snapshot, lsn)
query!(ctx, Electric.Postgres.display_settings(),
span_name: "shape_snapshot.set_display_settings"
)
OpenTelemetry.start_interval(:"shape_snapshot.query.duration_µs")
result =
OpenTelemetry.with_child_span(
"shape_snapshot.query_fn",
span_attrs,
stack_id,
fn -> query_fn.(conn, pg_snapshot, lsn) end
)
OpenTelemetry.stop_and_save_intervals(
total_attribute: :"shape_snapshot.total.duration_µs"
)
result
end,
timeout: :infinity
)
end
)
catch
:exit, {_, {DBConnection.Holder, :checkout, _}} ->
raise SnapshotError.connection_not_available()
end
@spec query!(map(), String.t() | [String.t()], Keyword.t()) :: [Postgrex.Result.t(), ...]
defp query!(
%{conn: conn, stack_id: stack_id, span_attrs: span_attrs},
query_or_queries,
opts
) do
OpenTelemetry.with_child_span(
Keyword.fetch!(opts, :span_name),
span_attrs,
stack_id,
fn ->
query_or_queries
|> List.wrap()
|> Enum.map(fn query -> Postgrex.query!(conn, query, Keyword.get(opts, :params, [])) end)
end
)
end
def execute_for_subset(shape_handle, %Shape{} = shape, subset, opts) when is_map(opts) do
stack_id = Map.fetch!(opts, :stack_id)
pool = Electric.Connection.Manager.pool_name(stack_id, :snapshot)
mark = Enum.random(0..(2 ** 31 - 1))
headers = %{snapshot_mark: mark}
execute_for_shape(pool, shape_handle, shape,
snapshot_info_fn: fn _, pg_snapshot, lsn ->
send(self(), {:pg_snapshot_info, pg_snapshot, lsn})
end,
query_fn: fn conn, _, _ ->
conn
|> Querying.query_subset(stack_id, shape_handle, shape, subset, headers)
|> record_subset_metrics(stack_id, shape_handle, shape)
|> Enum.to_list()
end,
stack_id: stack_id,
query_reason: "subset_query"
)
|> case do
{:ok, result} ->
metadata =
receive do
{:pg_snapshot_info, pg_snapshot, lsn} -> make_subset_metadata(pg_snapshot, lsn, mark)
after
0 ->
raise "failed to execute snapshot query for shape #{shape_handle}: missing pg_snapshot_info"
end
{:ok, {metadata, result}}
{:error, error} ->
{:error, error}
end
rescue
e in Querying.QueryError ->
{:error, {:where, e.message}}
end
defp make_subset_metadata({xmin, xmax, xip_list}, lsn, mark) do
%{
xmin: xmin,
xmax: xmax,
xip_list: xip_list,
database_lsn: to_string(Lsn.to_integer(lsn)),
snapshot_mark: mark
}
end
defp record_subset_metrics(stream, stack_id, shape_handle, shape) do
Stream.transform(
stream,
fn -> {System.monotonic_time(:microsecond), 0, 0} end,
fn row, {start_time, bytes, rows} ->
{[row], {start_time, bytes + IO.iodata_length(row), rows + 1}}
end,
fn {start_time, bytes, rows} ->
OpenTelemetry.execute(
[:electric, :subqueries, :subset_result],
%{
duration: System.monotonic_time(:microsecond) - start_time,
bytes: bytes,
count: 1,
rows: rows
},
%{
stack_id: stack_id,
"shape.handle": shape_handle,
"shape.root_table": shape.root_table
}
)
OpenTelemetry.add_span_attributes(%{
"subset.rows" => rows,
"subset.result_bytes" => bytes
})
end
)
end
end