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/shape_cleaner.ex
defmodule Electric.ShapeCache.ShapeCleaner do
@moduledoc """
Removes a shape (consumer, status entry, on-disk data and publication entry) on demand.
"""
alias Electric.Shapes.Consumer
alias Electric.ShapeCache.ShapeStatus
alias Electric.ShapeCache.Storage
alias Electric.ShapeCache.ShapeCleaner.CleanupTaskSupervisor
alias Electric.Telemetry.OpenTelemetry
require Logger
@type shape_handle() :: Electric.shape_handle()
@type stack_id() :: Electric.stack_id()
@shutdown_cleanup {:shutdown, :cleanup}
@shutdown_suspend {:shutdown, :suspend}
# Public API
def consumer_cleanup_reason, do: @shutdown_cleanup
def consumer_suspend_reason, do: @shutdown_suspend
@spec remove_shapes(stack_id(), [shape_handle()], term()) :: :ok | {:error, term()}
def remove_shapes(stack_id, shape_handles, reason \\ @shutdown_cleanup)
when is_list(shape_handles) do
OpenTelemetry.with_span(
"shape_cleaner.remove_shapes",
[],
stack_id,
fn ->
valid_handles = remove_shapes_immediate(stack_id, shape_handles, reason)
remove_shapes_deferred(stack_id, valid_handles)
OpenTelemetry.stop_and_save_intervals(total_attribute: "remove_shape.total_duration_µs")
:ok
end
)
end
@spec remove_shape(stack_id(), shape_handle(), term()) :: :ok | {:error, term()}
def remove_shape(stack_id, shape_handle, reason \\ @shutdown_cleanup) do
remove_shapes(stack_id, List.wrap(shape_handle), reason)
end
@spec remove_shapes_async(stack_id(), [shape_handle()]) :: :ok
def remove_shapes_async(stack_id, shape_handles) do
CleanupTaskSupervisor.perform_async(stack_id, fn ->
activate_mocked_functions_from_test_process()
remove_shapes(stack_id, shape_handles)
end)
end
@spec remove_shape_async(stack_id(), shape_handle()) :: :ok
def remove_shape_async(stack_id, shape_handle) do
remove_shapes_async(stack_id, List.wrap(shape_handle))
end
@spec remove_shapes_for_relations(list(Electric.oid_relation()), stack_id(), term()) :: :ok
def remove_shapes_for_relations(stack_id, relations, reason \\ @shutdown_cleanup)
def remove_shapes_for_relations(_stack_id, [], _reason) do
:ok
end
def remove_shapes_for_relations(stack_id, relations, reason) do
# 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
CleanupTaskSupervisor.perform_async(stack_id, fn ->
affected_shapes = ShapeStatus.list_shape_handles_for_relations(stack_id, relations)
Logger.notice(fn ->
"Cleaning up all shapes for relations #{inspect(relations)}: #{length(affected_shapes)} shapes total"
end)
remove_shapes(stack_id, affected_shapes, reason)
end)
end
@spec remove_shape_storage_async(stack_id(), [shape_handle()]) :: :ok
def remove_shape_storage_async(_stack_id, []) do
:ok
end
def remove_shape_storage_async(stack_id, shape_handles) do
CleanupTaskSupervisor.perform_async(stack_id, fn ->
activate_mocked_functions_from_test_process()
stack_storage = Storage.for_stack(stack_id)
Enum.each(shape_handles, fn shape_handle ->
:ok = Storage.cleanup!(stack_storage, shape_handle)
end)
end)
end
@type reason() :: {:shutdown, :cleanup} | {:shutdown, :suspend} | term()
@spec handle_writer_termination(stack_id(), shape_handle(), reason()) :: :removed | :ok
def handle_writer_termination(stack_id, shape_handle, @shutdown_cleanup) do
Logger.info("Removing shape #{inspect(shape_handle)}")
remove_shape_async(stack_id, shape_handle)
:removed
end
def handle_writer_termination(stack_id, shape_handle, @shutdown_suspend) do
# deregister the consumer without removing it from the rest of the system
# the next time a txn comes in matching this consumer it will be re-started
# by the consumer registry as per any other lazily loaded consumer
Electric.Shapes.ConsumerRegistry.remove_consumer(shape_handle, stack_id)
end
def handle_writer_termination(_stack_id, _shape_handle, reason)
when reason in [:normal, :killed, :shutdown] or
(is_tuple(reason) and elem(reason, 0) == :shutdown) do
:ok
end
def handle_writer_termination(stack_id, shape_handle, reason) do
reason_message =
case reason do
{error, stacktrace} when is_tuple(error) and is_list(stacktrace) ->
Exception.format(:error, error, stacktrace)
other ->
inspect(other)
end
Logger.notice(
"Removing shape #{inspect(shape_handle)} due to abnormal shutdown: #{reason_message}"
)
remove_shape_async(stack_id, shape_handle)
:removed
end
defp remove_shapes_immediate(stack_id, shape_handles, reason) when is_list(shape_handles) do
OpenTelemetry.with_child_span(
"shape_cleaner.remove_shapes.remove_shapes_immediate",
[count: length(shape_handles)],
stack_id,
fn ->
Enum.flat_map(shape_handles, fn shape_handle ->
case remove_shape_immediate(stack_id, shape_handle, reason) do
:ok -> [shape_handle]
{:error, :data_removed} -> []
end
end)
end
)
end
defp remove_shape_immediate(stack_id, shape_handle, reason) do
OpenTelemetry.start_interval(:"remove_shape.shape_status_remove.duration_µs")
case Electric.ShapeCache.ShapeStatus.remove_shape(stack_id, shape_handle) do
:ok ->
OpenTelemetry.start_interval(:"remove_shape.shape_consumer_stop.duration_µs")
stack_storage = Storage.for_stack(stack_id)
with :ok <- Consumer.stop(stack_id, shape_handle, reason),
OpenTelemetry.start_interval(:"remove_shape.storage_cleanup.duration_µs"),
:ok <- Storage.cleanup!(stack_storage, shape_handle),
OpenTelemetry.start_interval(:"remove_shape.shape_log_collector_remove.duration_µs"),
:ok <-
Electric.Replication.ShapeLogCollector.remove_shape(stack_id, shape_handle) do
:ok
end
{:error, _reason} ->
{:error, :data_removed}
end
end
defp remove_shapes_deferred(stack_id, shape_handles) when is_list(shape_handles) do
OpenTelemetry.start_interval(:"remove_shape.remove_shapes_deferred.duration_µs")
:ok = CleanupTaskSupervisor.cleanup_async(stack_id, shape_handles)
end
if Mix.env() == :test do
def activate_mocked_functions_from_test_process do
Support.TestUtils.activate_mocked_functions_for_module(__MODULE__)
end
else
def activate_mocked_functions_from_test_process, do: :noop
end
end