`Indexer.Memory.Monitor` checks if the BEAM memory usage exceeds a set limit (defaults to 1 GiB) and if it does, it asks the process with the most memory that is registered as shrinkable to shrink.pull/971/head
parent
879a8382f2
commit
cae25195e5
@ -0,0 +1,148 @@ |
|||||||
|
defmodule Indexer.Memory.Monitor do |
||||||
|
@moduledoc """ |
||||||
|
Monitors memory usage of Erlang VM. |
||||||
|
|
||||||
|
If memory usage (as reported by `:erlang.memory(:total)` exceeds the configured limit, then the `Process` with the |
||||||
|
worst memory usage (as reported by `Process.info(pid, :memory)`) in `shrinkable_set` is asked to |
||||||
|
`c:Indexer.Memory.Shrinkable.shrink/0`. |
||||||
|
""" |
||||||
|
|
||||||
|
require Bitwise |
||||||
|
require Logger |
||||||
|
|
||||||
|
import Bitwise |
||||||
|
|
||||||
|
alias Indexer.Memory.Shrinkable |
||||||
|
|
||||||
|
defstruct limit: 1 <<< 30, |
||||||
|
timer_interval: :timer.minutes(1), |
||||||
|
timer_reference: nil, |
||||||
|
shrinkable_set: MapSet.new() |
||||||
|
|
||||||
|
use GenServer |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Registers caller as `Indexer.Memory.Shrinkable`. |
||||||
|
""" |
||||||
|
def shrinkable(server \\ __MODULE__) do |
||||||
|
GenServer.call(server, :shrinkable) |
||||||
|
end |
||||||
|
|
||||||
|
def child_spec([]) do |
||||||
|
child_spec([%{}, []]) |
||||||
|
end |
||||||
|
|
||||||
|
def child_spec([init_options, gen_server_options] = start_link_arguments) |
||||||
|
when is_map(init_options) and is_list(gen_server_options) do |
||||||
|
Supervisor.child_spec(%{id: __MODULE__, start: {__MODULE__, :start_link, start_link_arguments}}, []) |
||||||
|
end |
||||||
|
|
||||||
|
def start_link(init_options, gen_server_options \\ []) when is_map(init_options) and is_list(gen_server_options) do |
||||||
|
GenServer.start_link(__MODULE__, init_options, Keyword.put_new(gen_server_options, :name, __MODULE__)) |
||||||
|
end |
||||||
|
|
||||||
|
@impl GenServer |
||||||
|
def init(options) when is_map(options) do |
||||||
|
state = struct!(__MODULE__, options) |
||||||
|
{:ok, timer_reference} = :timer.send_interval(state.timer_interval, :check) |
||||||
|
|
||||||
|
{:ok, %__MODULE__{state | timer_reference: timer_reference}} |
||||||
|
end |
||||||
|
|
||||||
|
@impl GenServer |
||||||
|
def handle_call(:shinkable, {pid, _}, %__MODULE__{shrinkable_set: shrinkable_set} = state) do |
||||||
|
Process.monitor(pid) |
||||||
|
|
||||||
|
{:reply, :ok, %__MODULE__{state | shrinkable_set: MapSet.put(shrinkable_set, pid)}} |
||||||
|
end |
||||||
|
|
||||||
|
@impl GenServer |
||||||
|
def handle_info({:DOWN, _, :process, pid, _}, %__MODULE__{shrinkable_set: shrinkable_set}) do |
||||||
|
{:noreply, %__MODULE__{shrinkable_set: MapSet.delete(shrinkable_set, pid)}} |
||||||
|
end |
||||||
|
|
||||||
|
@impl GenServer |
||||||
|
def handle_info(:check, %__MODULE__{limit: limit} = state) do |
||||||
|
total = :erlang.memory(:total) |
||||||
|
|
||||||
|
if limit < total do |
||||||
|
case shrinkable_with_most_memory(state) do |
||||||
|
{:error, :not_found} -> |
||||||
|
Logger.error(fn -> |
||||||
|
[ |
||||||
|
prefix(%{total: total, limit: limit}), |
||||||
|
" No processes are registered as shrinkable. Limit will remain surpassed." |
||||||
|
] |
||||||
|
end) |
||||||
|
|
||||||
|
{:ok, {pid, memory}} -> |
||||||
|
Logger.warn(fn -> |
||||||
|
prefix = [ |
||||||
|
prefix(%{total: total, limit: limit}), |
||||||
|
" Worst memory usage (", |
||||||
|
to_string(memory), |
||||||
|
" bytes) among shrinkable processes is ", |
||||||
|
inspect(pid) |
||||||
|
] |
||||||
|
|
||||||
|
{:registered_name, registered_name} = Process.info(pid, :registered_name) |
||||||
|
|
||||||
|
prefix = |
||||||
|
case registered_name do |
||||||
|
[] -> [prefix, "."] |
||||||
|
_ -> [prefix, " (", inspect(registered_name), ")."] |
||||||
|
end |
||||||
|
|
||||||
|
[prefix, " Asking ", inspect(pid), " to shrinkable to drop below limit."] |
||||||
|
end) |
||||||
|
|
||||||
|
:ok = Shrinkable.shrink(pid) |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
flush(:check) |
||||||
|
|
||||||
|
{:noreply, state} |
||||||
|
end |
||||||
|
|
||||||
|
defp flush(message) do |
||||||
|
receive do |
||||||
|
^message -> flush(message) |
||||||
|
after |
||||||
|
0 -> |
||||||
|
:ok |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp memory(pid) when is_pid(pid) do |
||||||
|
case Process.info(pid, :memory) do |
||||||
|
{:memory, memory} -> memory |
||||||
|
# process died |
||||||
|
nil -> 0 |
||||||
|
end |
||||||
|
end |
||||||
|
|
||||||
|
defp prefix(%{total: total, limit: limit}) do |
||||||
|
[ |
||||||
|
to_string(total), |
||||||
|
" / ", |
||||||
|
to_string(limit), |
||||||
|
" bytes (", |
||||||
|
to_string(div(100 * total, limit)), |
||||||
|
"%) of memory limit used." |
||||||
|
] |
||||||
|
end |
||||||
|
|
||||||
|
defp shrinkable_with_most_memory(%__MODULE__{shrinkable_set: shrinkable_set}) do |
||||||
|
if Enum.empty?(shrinkable_set) do |
||||||
|
{:error, :not_found} |
||||||
|
else |
||||||
|
pid_memory = |
||||||
|
shrinkable_set |
||||||
|
|> Enum.map(fn pid -> {pid, memory(pid)} end) |
||||||
|
|> Enum.max_by(&elem(&1, 1)) |
||||||
|
|
||||||
|
{:ok, pid_memory} |
||||||
|
end |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,15 @@ |
|||||||
|
defmodule Indexer.Memory.Shrinkable do |
||||||
|
@moduledoc """ |
||||||
|
A process that can shrink its memory usage when asked by `Indexer.Memory.Monitor`. |
||||||
|
|
||||||
|
Processes need to `handle_call(:shrink, from, state)`. |
||||||
|
""" |
||||||
|
|
||||||
|
@doc """ |
||||||
|
Asks `pid` to shrink its memory usage. |
||||||
|
""" |
||||||
|
@spec shrink(pid()) :: :ok |
||||||
|
def shrink(pid) when is_pid(pid) do |
||||||
|
GenServer.call(pid, :shrink) |
||||||
|
end |
||||||
|
end |
@ -0,0 +1,70 @@ |
|||||||
|
defmodule Indexer.Shrinkable.Supervisor do |
||||||
|
@moduledoc """ |
||||||
|
Supervisor of all supervision trees that depend on `Indexer.Alarm.Supervisor`. |
||||||
|
""" |
||||||
|
|
||||||
|
use Supervisor |
||||||
|
|
||||||
|
alias Indexer.{ |
||||||
|
Block, |
||||||
|
CoinBalance, |
||||||
|
InternalTransaction, |
||||||
|
PendingTransaction, |
||||||
|
Token, |
||||||
|
TokenBalance, |
||||||
|
TokenTransfer |
||||||
|
} |
||||||
|
|
||||||
|
def child_spec([]) do |
||||||
|
child_spec([[]]) |
||||||
|
end |
||||||
|
|
||||||
|
def child_spec([init_arguments]) do |
||||||
|
child_spec([init_arguments, []]) |
||||||
|
end |
||||||
|
|
||||||
|
def child_spec([_init_arguments, _gen_server_options] = start_link_arguments) do |
||||||
|
default = %{ |
||||||
|
id: __MODULE__, |
||||||
|
start: {__MODULE__, :start_link, start_link_arguments}, |
||||||
|
type: :supervisor |
||||||
|
} |
||||||
|
|
||||||
|
Supervisor.child_spec(default, []) |
||||||
|
end |
||||||
|
|
||||||
|
def start_link(arguments, gen_server_options \\ []) do |
||||||
|
Supervisor.start_link(__MODULE__, arguments, Keyword.put_new(gen_server_options, :name, __MODULE__)) |
||||||
|
end |
||||||
|
|
||||||
|
@impl Supervisor |
||||||
|
def init([]) do |
||||||
|
json_rpc_named_arguments = Application.fetch_env!(:indexer, :json_rpc_named_arguments) |
||||||
|
|
||||||
|
block_fetcher_supervisor_named_arguments = |
||||||
|
:indexer |
||||||
|
|> Application.get_all_env() |
||||||
|
|> Keyword.take( |
||||||
|
~w(blocks_batch_size blocks_concurrency block_interval json_rpc_named_arguments receipts_batch_size |
||||||
|
receipts_concurrency subscribe_named_arguments)a |
||||||
|
) |
||||||
|
|> Enum.into(%{}) |
||||||
|
|
||||||
|
Supervisor.init( |
||||||
|
[ |
||||||
|
{CoinBalance.Supervisor, |
||||||
|
[[json_rpc_named_arguments: json_rpc_named_arguments], [name: CoinBalance.Supervisor]]}, |
||||||
|
{PendingTransaction.Supervisor, |
||||||
|
[[json_rpc_named_arguments: json_rpc_named_arguments], [name: PendingTransactionFetcher]]}, |
||||||
|
{InternalTransaction.Supervisor, |
||||||
|
[[json_rpc_named_arguments: json_rpc_named_arguments], [name: InternalTransaction.Supervisor]]}, |
||||||
|
{Token.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments], [name: Token.Supervisor]]}, |
||||||
|
{TokenBalance.Supervisor, |
||||||
|
[[json_rpc_named_arguments: json_rpc_named_arguments], [name: TokenBalance.Supervisor]]}, |
||||||
|
{Block.Supervisor, [block_fetcher_supervisor_named_arguments, [name: Block.Supervisor]]}, |
||||||
|
{TokenTransfer.Uncataloged.Supervisor, [[], [name: TokenTransfer.Uncataloged.Supervisor]]} |
||||||
|
], |
||||||
|
strategy: :one_for_one |
||||||
|
) |
||||||
|
end |
||||||
|
end |
Loading…
Reference in new issue