Current section

Files

Jump to
electric lib electric shape_cache expiry_manager.ex
Raw

lib/electric/shape_cache/expiry_manager.ex

defmodule Electric.ShapeCache.ExpiryManager do
use GenServer
alias Electric.ShapeCache.ShapeStatus
alias Electric.StatusMonitor
alias Electric.Telemetry.OpenTelemetry
require Logger
@schema NimbleOptions.new!(
max_shapes: [type: {:or, [:non_neg_integer, nil]}, default: nil],
period: [type: :non_neg_integer, default: 60_000],
stack_id: [type: :string, required: true]
)
def name(stack_ref) do
Electric.ProcessRegistry.name(stack_ref, __MODULE__)
end
def start_link(opts) do
with {:ok, opts} <- NimbleOptions.validate(opts, @schema) do
GenServer.start_link(__MODULE__, opts, name: name(opts))
end
end
def init(opts) do
stack_id = Keyword.fetch!(opts, :stack_id)
Process.set_label({:shape_expiry_manager, stack_id})
Logger.metadata(stack_id: stack_id)
Electric.Telemetry.Sentry.set_tags_context(stack_id: stack_id)
state =
%{
stack_id: stack_id,
max_shapes: Keyword.fetch!(opts, :max_shapes),
period: Keyword.fetch!(opts, :period)
}
if not is_nil(state.max_shapes), do: schedule_next_check(state)
{:ok, state}
end
defp schedule_next_check(state) do
Process.send_after(self(), :maybe_expire_shapes, state.period)
end
def handle_info(:maybe_expire_shapes, state) do
maybe_expire_shapes(state)
schedule_next_check(state)
{:noreply, state}
end
defp maybe_expire_shapes(%{max_shapes: nil}), do: :ok
defp maybe_expire_shapes(%{max_shapes: max_shapes} = state) do
case StatusMonitor.status(state.stack_id) do
%{shape: :up} ->
shape_count = shape_count(state)
if shape_count > max_shapes do
expire_shapes(shape_count, state)
end
status ->
# We do not expire shapes if the stack is not active since this may mean that
# shapes have not fully restored yet and we don't want to expire while restoring
# as this may cause race conditions.
Logger.debug("Expiry check skipped due to inactive stack: #{inspect(status)}")
end
end
defp expire_shapes(shape_count, state) do
number_to_expire = shape_count - state.max_shapes
{handles_to_expire, min_age} = least_recently_used(state, number_to_expire)
Logger.info(
"Expiring shapes as the number of shapes has exceeded the limit",
number_to_expire: number_to_expire,
max_shapes: state.max_shapes
)
OpenTelemetry.with_span(
"expiry_manager.expire_shapes",
[
max_shapes: state.max_shapes,
shape_count: shape_count,
number_to_expire: number_to_expire,
elapsed_minutes_since_use: min_age
],
fn ->
Electric.ShapeCache.ShapeCleaner.remove_shapes(state.stack_id, handles_to_expire)
end
)
end
defp least_recently_used(%{stack_id: stack_id}, number_to_expire) do
ShapeStatus.least_recently_used(stack_id, number_to_expire)
end
defp shape_count(%{stack_id: stack_id}) do
ShapeStatus.count_shapes(stack_id)
end
end