Skip to content

Commit 10f7d71

Browse files
Periodically purge check-ins ETS table (#764)
Co-authored-by: Andrea Leopardi <[email protected]>
1 parent dda0256 commit 10f7d71

File tree

4 files changed

+93
-14
lines changed

4 files changed

+93
-14
lines changed

lib/sentry/application.ex

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,17 @@ defmodule Sentry.Application do
1919
[]
2020
end
2121

22+
integrations_config = Keyword.fetch!(config, :integrations)
23+
2224
children =
2325
[
2426
{Registry, keys: :unique, name: Sentry.Transport.SenderRegistry},
2527
Sentry.Dedupe,
26-
Sentry.Integrations.CheckInIDMappings
28+
{Sentry.Integrations.CheckInIDMappings,
29+
[
30+
max_expected_check_in_time:
31+
Keyword.fetch!(integrations_config, :max_expected_check_in_time)
32+
]}
2733
] ++
2834
maybe_http_client_spec ++
2935
[Sentry.Transport.SenderPool]
@@ -33,7 +39,7 @@ defmodule Sentry.Application do
3339

3440
with {:ok, pid} <-
3541
Supervisor.start_link(children, strategy: :one_for_one, name: Sentry.Supervisor) do
36-
start_integrations(Keyword.fetch!(config, :integrations))
42+
start_integrations(integrations_config)
3743
{:ok, pid}
3844
end
3945
end

lib/sentry/config.ex

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@ defmodule Sentry.Config do
22
@moduledoc false
33

44
integrations_schema = [
5+
max_expected_check_in_time: [
6+
type: :integer,
7+
default: 600_000,
8+
doc: """
9+
The time in milliseconds that a check-in ID will live after it has been created.
10+
11+
The SDK reports the start and end of each check-in. A check-in is used to track the
12+
progress of a specific check-in event associated with cron job telemetry events that are a part
13+
of the same job. However, to optimize performance and prevent potential memory issues,
14+
if a check-in end event is reported after the specified `max_expected_check_in_time`,
15+
the SDK will not report it. This behavior helps manage resource usage effectively while still
16+
providing necessary tracking for your jobs.
17+
*Available since 10.6.3*.
18+
"""
19+
],
520
oban: [
621
type: :keyword_list,
722
doc: """

lib/sentry/integrations/check_in_id_mappings.ex

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,58 @@ defmodule Sentry.Integrations.CheckInIDMappings do
55
alias Sentry.UUID
66

77
@table :sentry_cron_mappings
8+
@sweep_interval_millisec 30_000
89

910
@spec start_link(keyword()) :: GenServer.on_start()
10-
def start_link([] = _opts) do
11-
GenServer.start_link(__MODULE__, nil, name: __MODULE__)
11+
def start_link(opts \\ []) do
12+
name = Keyword.get(opts, :name, __MODULE__)
13+
ttl_millisec = Keyword.get(opts, :max_expected_check_in_time)
14+
GenServer.start_link(__MODULE__, ttl_millisec, name: name)
1215
end
1316

1417
@spec lookup_or_insert_new(String.t()) :: UUID.t()
15-
def lookup_or_insert_new(key) do
16-
case :ets.lookup(@table, key) do
17-
[{^key, value}] ->
18-
value
18+
def lookup_or_insert_new(cron_key) do
19+
inserted_at = System.system_time(:millisecond)
20+
21+
case :ets.lookup(@table, cron_key) do
22+
[{^cron_key, uuid, _inserted_at}] ->
23+
uuid
1924

2025
[] ->
21-
value = UUID.uuid4_hex()
22-
:ets.insert(@table, {key, value})
23-
value
26+
uuid = UUID.uuid4_hex()
27+
:ets.insert(@table, {cron_key, uuid, inserted_at})
28+
uuid
2429
end
2530
end
2631

2732
## Callbacks
2833

2934
@impl true
30-
def init(nil) do
31-
_table = :ets.new(@table, [:named_table, :public, :set])
32-
{:ok, :no_state}
35+
def init(ttl_millisec) do
36+
_table =
37+
if :ets.whereis(@table) == :undefined do
38+
:ets.new(@table, [:named_table, :public, :set])
39+
end
40+
41+
schedule_sweep()
42+
{:ok, ttl_millisec}
43+
end
44+
45+
@impl true
46+
def handle_info(:sweep, ttl_millisec) do
47+
now = System.system_time(:millisecond)
48+
# All rows (which are {cron_key, uuid, inserted_at}) with an inserted_at older than
49+
# now - ttl_millisec.
50+
match_spec = [{{:"$1", :"$2", :"$3"}, [], [{:<, :"$3", now - ttl_millisec}]}]
51+
_ = :ets.select_delete(@table, match_spec)
52+
53+
schedule_sweep()
54+
{:noreply, ttl_millisec}
55+
end
56+
57+
## Helpers
58+
59+
defp schedule_sweep() do
60+
Process.send_after(self(), :sweep, @sweep_interval_millisec)
3361
end
3462
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
defmodule Sentry.CheckInIDMappingsTest do
2+
# This is not async because it tests a singleton (the CheckInIDMappings GenServer).
3+
use Sentry.Case, async: false
4+
5+
alias Sentry.Integrations.CheckInIDMappings
6+
@table :sentry_cron_mappings
7+
8+
describe "lookup_or_insert_new/1" do
9+
test "works correctly" do
10+
cron_key = "quantum_123"
11+
12+
child_spec = %{
13+
id: TestMappings,
14+
start:
15+
{CheckInIDMappings, :start_link, [[max_expected_check_in_time: 0, name: TestMappings]]}
16+
}
17+
18+
pid = start_supervised!(child_spec)
19+
20+
CheckInIDMappings.lookup_or_insert_new(cron_key)
21+
assert :ets.lookup(@table, cron_key) != []
22+
23+
Process.sleep(5)
24+
send(pid, :sweep)
25+
_ = :sys.get_state(pid)
26+
27+
assert :ets.lookup(@table, cron_key) == []
28+
end
29+
end
30+
end

0 commit comments

Comments
 (0)