diff --git a/.sampo/changesets/jovial-seer-ukko.md b/.sampo/changesets/jovial-seer-ukko.md new file mode 100644 index 0000000..e794cd9 --- /dev/null +++ b/.sampo/changesets/jovial-seer-ukko.md @@ -0,0 +1,5 @@ +--- +hex/posthog: patch +--- + +Stop leaking access tracker processes during OpenFeature flag evaluations. diff --git a/lib/posthog/open_feature.ex b/lib/posthog/open_feature.ex index 26c2a8a..89d2719 100644 --- a/lib/posthog/open_feature.ex +++ b/lib/posthog/open_feature.ex @@ -166,8 +166,15 @@ if Code.ensure_loaded?(OpenFeature.Provider) do body = flags_body(distinct_id, key, context) case FeatureFlags.evaluate_flags(provider.supervisor_name, body) do - {:ok, snapshot} -> fetch_result(provider, snapshot, key) - {:error, reason} -> {:error, :unexpected_error, to_exception(reason)} + {:ok, snapshot} -> + try do + fetch_result(provider, snapshot, key) + after + Agent.stop(snapshot.accessed_pid) + end + + {:error, reason} -> + {:error, :unexpected_error, to_exception(reason)} end :error -> diff --git a/test/posthog/open_feature_test.exs b/test/posthog/open_feature_test.exs index 0e79809..b7376af 100644 --- a/test/posthog/open_feature_test.exs +++ b/test/posthog/open_feature_test.exs @@ -38,6 +38,48 @@ defmodule PostHog.OpenFeature.ProviderTest do end end + describe "snapshot cleanup" do + for events <- [true, false], present <- [true, false] do + @events events + @present present + test "releases access trackers with events=#{events} and flag present=#{present}" do + flags = if @present, do: %{"flag" => flag(%{"key" => "flag"})}, else: %{} + + expect(API.Mock, :request, 10, fn _client, :post, "/flags", _opts -> + {:ok, %{status: 200, body: %{"flags" => flags}}} + end) + + provider = provider(send_feature_flag_events: @events) + {:links, before} = Process.info(self(), :links) + + for _ <- 1..10 do + result = Provider.resolve_boolean_value(provider, "flag", false, @context) + + if @present do + assert {:ok, %ResolutionDetails{value: true}} = result + else + assert {:error, :flag_not_found} = result + end + end + + {:links, after_calls} = Process.info(self(), :links) + retained = after_calls -- before + + retained + |> Enum.filter(fn pid -> + is_pid(pid) and + match?( + {PostHog.FeatureFlags.Evaluations, _, _}, + :proc_lib.translate_initial_call(pid) + ) + end) + |> Enum.each(&Agent.stop/1) + + assert retained == [] + end + end + end + describe "resolve_boolean_value/4" do test "resolves an enabled flag" do expect_flag("flag", %{"enabled" => true})