Packages
electric
1.6.10
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/shapes/dynamic_consumer_supervisor.ex
defmodule Electric.Shapes.DynamicConsumerSupervisor do
@moduledoc """
Responsible for managing shape consumer processes.
Uses a set of `DynamicSupervisor`s supervised by a parent `DynamicSupervisor`
to take advantage of the fact that `DynamicSupervisor` terminates its
children in parallel rather than one at a time.
This improves shutdown time because all consumer processes are effectively
terminated simultaneously.
"""
use DynamicSupervisor
require Logger
import Electric, only: [is_stack_id: 1]
defmodule PartitionDynamicSupervisor do
@moduledoc false
use DynamicSupervisor
def name(stack_id, partition) when is_binary(stack_id) do
Electric.ProcessRegistry.name(stack_id, __MODULE__, partition)
end
def start_link({stack_id, partition}) do
DynamicSupervisor.start_link(__MODULE__, [stack_id: stack_id],
name: name(stack_id, partition)
)
end
@impl true
def init(stack_id: stack_id) do
Process.set_label({:consumer_supervisor_partition, stack_id})
Logger.metadata(stack_id: stack_id)
Electric.Telemetry.Sentry.set_tags_context(stack_id: stack_id)
DynamicSupervisor.init(strategy: :one_for_one)
end
end
# The max number of processes to start per-partition. Found empirically to
# give a reasonable tradeoff between memory usage and shutdown speed.
@target_per_partition 4_000
@partition_count_key :partition_count
def name(name) when is_atom(name) do
name
end
def name({:via, _, _} = name) do
name
end
def name(stack_id) when is_binary(stack_id) do
Electric.ProcessRegistry.name(stack_id, __MODULE__)
end
def start_link(opts) do
stack_id = Keyword.fetch!(opts, :stack_id)
{name, opts} = Keyword.pop(opts, :name, name(stack_id))
max_processes = Keyword.get(opts, :max_shapes) || 0
# use a fixed value for the partition count if its configured, if not then
# calculate based on the max_shapes setting (using @target_per_partition)
# or fallback to the number of schedulers
partitions = Keyword.get(opts, :partitions) || partition_count(max_processes)
Logger.info("Starting DynamicConsumerSupervisor with #{partitions} partitions")
with {:ok, supervisor_pid} <-
DynamicSupervisor.start_link(__MODULE__, %{stack_id: stack_id, partitions: partitions},
name: name
) do
case start_partition_supervisors(supervisor_pid, stack_id, partitions) do
{:ok, _pids} ->
{:ok, supervisor_pid}
{:error, _} = error ->
DynamicSupervisor.stop(supervisor_pid, :shutdown)
error
end
end
end
defp start_partition_supervisors(supervisor_pid, stack_id, partitions) do
Electric.Utils.reduce_while_ok(0..(partitions - 1), [], fn partition, pids ->
with {:ok, pid} <-
DynamicSupervisor.start_child(
supervisor_pid,
Supervisor.child_spec(
{PartitionDynamicSupervisor, {stack_id, partition}},
id: {:partition, partition}
)
) do
{:ok, [pid | pids]}
end
end)
end
def start_shape_consumer(stack_id, config) when is_stack_id(stack_id) do
start_child(stack_id, {Electric.Shapes.Consumer, config})
end
def start_snapshotter(stack_id, config) when is_stack_id(stack_id) do
start_child(stack_id, {Electric.Shapes.Consumer.Snapshotter, config})
end
def start_materializer(stack_id, config) when is_stack_id(stack_id) do
start_child(stack_id, {Electric.Shapes.Consumer.Materializer, config})
end
defp start_child(stack_id, {child_module, child_opts} = child_spec) do
%{shape_handle: shape_handle} = child_opts
Logger.debug(fn -> "Starting #{inspect(child_module)} for #{shape_handle}" end)
DynamicSupervisor.start_child(partition_for(stack_id, shape_handle), child_spec)
end
@impl true
def init(%{stack_id: stack_id, partitions: partitions}) do
Process.set_label({:dynamic_consumer_supervisor, stack_id})
Logger.metadata(stack_id: stack_id)
Electric.Telemetry.Sentry.set_tags_context(stack_id: stack_id)
table = :ets.new(table(stack_id), [:named_table, :public, read_concurrency: true])
true = :ets.insert(table, [{@partition_count_key, partitions}])
DynamicSupervisor.init(strategy: :one_for_one)
end
defp table(stack_id), do: :"Electric.Shapes.DynamicConsumerSupervisor:#{stack_id}"
defp partition_for(stack_id, shape_handle) do
partitions = :ets.lookup_element(table(stack_id), @partition_count_key, 2)
partition = :erlang.phash2(shape_handle, partitions)
PartitionDynamicSupervisor.name(stack_id, partition)
end
# we don't always have a value for `max_processes`, in which case just
# default to the number of schedulers
defp partition_count(0) do
System.schedulers_online()
end
defp partition_count(max_processes) when max_processes > 0 do
max(
System.schedulers_online(),
div(max_processes + @target_per_partition - 1, @target_per_partition)
)
end
end