Packages
electric
1.2.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/shape_cleaner.ex
defmodule Electric.ShapeCache.ShapeCleaner do
@moduledoc """
Removes a shape (consumer, status entry, on-disk data and publication entry) on demand.
This process ensures removing of shapes does not block critical path of shape creation.
"""
use GenServer
alias Electric.Shapes.ConsumerSupervisor
alias Electric.ShapeCache.ShapeStatus
require Logger
@type shape_handle() :: Electric.ShapeCacheBehaviour.shape_handle()
@schema NimbleOptions.new!(stack_id: [type: :string, required: true])
# Public API
@spec remove_shape(shape_handle(), Keyword.t()) :: :ok | {:error, term()}
def remove_shape(shape_handle, opts) do
stack_id = Keyword.fetch!(opts, :stack_id)
timeout = Keyword.get(opts, :timeout, 15_000)
GenServer.call(name(stack_id), {:remove_shape, shape_handle}, timeout)
end
@spec remove_shape(shape_handle(), Keyword.t()) :: :ok
def remove_shape_async(shape_handle, opts) do
stack_id = Keyword.fetch!(opts, :stack_id)
GenServer.cast(name(stack_id), {:remove_shape, shape_handle})
end
@spec remove_shapes_for_relations(list(Electric.oid_relation()), Keyword.t()) :: :ok
def remove_shapes_for_relations([], _opts) do
:ok
end
def remove_shapes_for_relations(relations, opts) do
stack_id = Keyword.fetch!(opts, :stack_id)
# We don't want for this call to be blocking because it will be called in `PublicationManager`
# if it notices a discrepancy in the schema
GenServer.cast(name(stack_id), {:clean_all_shapes_for_relations, relations})
end
def name(stack_id), do: Electric.ProcessRegistry.name(stack_id, __MODULE__)
def start_link(opts) do
with {:ok, opts} <- NimbleOptions.validate(opts, @schema) do
opts = Keyword.put_new(opts, :on_cleanup, fn _ -> :ok end)
stack_id = Keyword.fetch!(opts, :stack_id)
GenServer.start_link(__MODULE__, opts, name: name(stack_id))
end
end
# GenServer callbacks
@impl true
def init(opts) do
Process.set_label({:shape_remover, opts[:stack_id]})
Logger.metadata(stack_id: opts[:stack_id])
Electric.Telemetry.Sentry.set_tags_context(stack_id: opts[:stack_id])
{:ok,
%{
stack_id: opts[:stack_id],
queued_removals: []
}}
end
@impl true
def handle_call({:remove_shape, shape_handle}, _from, state) do
:ok = stop_and_clean_shape(shape_handle, state)
{:reply, :ok, state}
end
@impl true
def handle_cast({:remove_shape, shape_handle}, state) do
:ok = stop_and_clean_shape(shape_handle, state)
{:noreply, state}
end
def handle_cast({:clean_all_shapes_for_relations, relations}, state) do
affected_shapes =
ShapeStatus.list_shape_handles_for_relations(state.stack_id, relations)
Logger.info(fn ->
"Cleaning up all shapes for relations #{inspect(relations)}: #{length(affected_shapes)} shapes total"
end)
# schedule these shape removals one by one to avoid blocking the GenServer
# for too long to allow interleaved sync removals
new_queue = state.queued_removals ++ affected_shapes
# kick off processing if we just enqueued new shapes and weren't already processing
if affected_shapes != [] and state.queued_removals == [] do
GenServer.cast(self(), :remove_queued_shapes)
end
{:noreply, %{state | queued_removals: new_queue}}
end
def handle_cast(:remove_queued_shapes, %{queued_removals: []} = state) do
{:noreply, state}
end
def handle_cast(:remove_queued_shapes, %{queued_removals: [next_shape | rest]} = state) do
:ok = stop_and_clean_shape(next_shape, state)
# schedule the next removal immediately via another cast to keep mailbox ordering
if rest != [] do
GenServer.cast(self(), :remove_queued_shapes)
end
{:noreply, %{state | queued_removals: rest}}
end
@impl true
def handle_info({:remove_shape, shape_handle}, state) do
Logger.debug("Removing shape #{inspect(shape_handle)}")
:ok = stop_and_clean_shape(shape_handle, state)
{:noreply, state}
end
defp stop_and_clean_shape(shape_handle, state) do
Logger.debug("Removing shape #{inspect(shape_handle)}")
case ConsumerSupervisor.stop_and_clean(state.stack_id, shape_handle) do
:noproc ->
# if the consumer isn't running then we can just delete things gratuitously,
# starting with an immediate shape status removal
ShapeStatus.remove_shape(state.stack_id, shape_handle)
:ok = purge_shape(state.stack_id, shape_handle)
:ok ->
# if it is running then the stop_and_clean process will cleanup properly
:ok
end
end
defdelegate purge_shape(stack_id, shape_handle), to: Electric.Shapes.Monitor
end