Skip to content

Commit

Permalink
Periodically purge check-ins ETS table (#764)
Browse files Browse the repository at this point in the history
Co-authored-by: Andrea Leopardi <an.leopardi@gmail.com>
  • Loading branch information
savhappy and whatyouhide authored Aug 13, 2024
1 parent dda0256 commit 10f7d71
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 14 deletions.
10 changes: 8 additions & 2 deletions lib/sentry/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,17 @@ defmodule Sentry.Application do
[]
end

integrations_config = Keyword.fetch!(config, :integrations)

children =
[
{Registry, keys: :unique, name: Sentry.Transport.SenderRegistry},
Sentry.Dedupe,
Sentry.Integrations.CheckInIDMappings
{Sentry.Integrations.CheckInIDMappings,
[
max_expected_check_in_time:
Keyword.fetch!(integrations_config, :max_expected_check_in_time)
]}
] ++
maybe_http_client_spec ++
[Sentry.Transport.SenderPool]
Expand All @@ -33,7 +39,7 @@ defmodule Sentry.Application do

with {:ok, pid} <-
Supervisor.start_link(children, strategy: :one_for_one, name: Sentry.Supervisor) do
start_integrations(Keyword.fetch!(config, :integrations))
start_integrations(integrations_config)
{:ok, pid}
end
end
Expand Down
15 changes: 15 additions & 0 deletions lib/sentry/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@ defmodule Sentry.Config do
@moduledoc false

integrations_schema = [
max_expected_check_in_time: [
type: :integer,
default: 600_000,
doc: """
The time in milliseconds that a check-in ID will live after it has been created.
The SDK reports the start and end of each check-in. A check-in is used to track the
progress of a specific check-in event associated with cron job telemetry events that are a part
of the same job. However, to optimize performance and prevent potential memory issues,
if a check-in end event is reported after the specified `max_expected_check_in_time`,
the SDK will not report it. This behavior helps manage resource usage effectively while still
providing necessary tracking for your jobs.
*Available since 10.6.3*.
"""
],
oban: [
type: :keyword_list,
doc: """
Expand Down
52 changes: 40 additions & 12 deletions lib/sentry/integrations/check_in_id_mappings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,58 @@ defmodule Sentry.Integrations.CheckInIDMappings do
alias Sentry.UUID

@table :sentry_cron_mappings
@sweep_interval_millisec 30_000

@spec start_link(keyword()) :: GenServer.on_start()
def start_link([] = _opts) do
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
def start_link(opts \\ []) do
name = Keyword.get(opts, :name, __MODULE__)
ttl_millisec = Keyword.get(opts, :max_expected_check_in_time)
GenServer.start_link(__MODULE__, ttl_millisec, name: name)
end

@spec lookup_or_insert_new(String.t()) :: UUID.t()
def lookup_or_insert_new(key) do
case :ets.lookup(@table, key) do
[{^key, value}] ->
value
def lookup_or_insert_new(cron_key) do
inserted_at = System.system_time(:millisecond)

case :ets.lookup(@table, cron_key) do
[{^cron_key, uuid, _inserted_at}] ->
uuid

[] ->
value = UUID.uuid4_hex()
:ets.insert(@table, {key, value})
value
uuid = UUID.uuid4_hex()
:ets.insert(@table, {cron_key, uuid, inserted_at})
uuid
end
end

## Callbacks

@impl true
def init(nil) do
_table = :ets.new(@table, [:named_table, :public, :set])
{:ok, :no_state}
def init(ttl_millisec) do
_table =
if :ets.whereis(@table) == :undefined do
:ets.new(@table, [:named_table, :public, :set])
end

schedule_sweep()
{:ok, ttl_millisec}
end

@impl true
def handle_info(:sweep, ttl_millisec) do
now = System.system_time(:millisecond)
# All rows (which are {cron_key, uuid, inserted_at}) with an inserted_at older than
# now - ttl_millisec.
match_spec = [{{:"$1", :"$2", :"$3"}, [], [{:<, :"$3", now - ttl_millisec}]}]
_ = :ets.select_delete(@table, match_spec)

schedule_sweep()
{:noreply, ttl_millisec}
end

## Helpers

defp schedule_sweep() do
Process.send_after(self(), :sweep, @sweep_interval_millisec)
end
end
30 changes: 30 additions & 0 deletions test/sentry/check_in_id_mappings_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule Sentry.CheckInIDMappingsTest do
# This is not async because it tests a singleton (the CheckInIDMappings GenServer).
use Sentry.Case, async: false

alias Sentry.Integrations.CheckInIDMappings
@table :sentry_cron_mappings

describe "lookup_or_insert_new/1" do
test "works correctly" do
cron_key = "quantum_123"

child_spec = %{
id: TestMappings,
start:
{CheckInIDMappings, :start_link, [[max_expected_check_in_time: 0, name: TestMappings]]}
}

pid = start_supervised!(child_spec)

CheckInIDMappings.lookup_or_insert_new(cron_key)
assert :ets.lookup(@table, cron_key) != []

Process.sleep(5)
send(pid, :sweep)
_ = :sys.get_state(pid)

assert :ets.lookup(@table, cron_key) == []
end
end
end

0 comments on commit 10f7d71

Please sign in to comment.