From 34a09c104f75f3a51829fcce4aab5969263665cb Mon Sep 17 00:00:00 2001 From: ruslandoga Date: Fri, 21 Aug 2026 16:35:53 +0300 Subject: [PATCH 1/5] fix: stop the requested backend child --- lib/logflare/backends/source_sup.ex | 10 ++- test/logflare/backends_test.exs | 103 ++++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 6 deletions(-) diff --git a/lib/logflare/backends/source_sup.ex b/lib/logflare/backends/source_sup.ex index 893b9e1cce..3ab2e859d1 100644 --- a/lib/logflare/backends/source_sup.ex +++ b/lib/logflare/backends/source_sup.ex @@ -156,12 +156,10 @@ defmodule Logflare.Backends.SourceSup do found_id = Supervisor.which_children(via) - |> Enum.find_value( - fn {{_mod, _source_id, bid}, _pid, _type, _sup} -> - bid == backend_id - end, - &elem(&1, 0) - ) + |> Enum.find_value(fn + {{_mod, _source_id, ^backend_id} = child_id, _pid, _type, _sup} -> child_id + _child -> nil + end) if found_id do Supervisor.terminate_child(via, found_id) diff --git a/test/logflare/backends_test.exs b/test/logflare/backends_test.exs index 12d859ef0d..d20245d9ba 100644 --- a/test/logflare/backends_test.exs +++ b/test/logflare/backends_test.exs @@ -824,6 +824,109 @@ defmodule Logflare.BackendsTest do assert :ok = Backends.restart_source_sup(source) end + test "stop_backend_child/2 stops only the requested backend child", %{ + source: source, + user: user + } do + [backend1, backend2] = + insert_pair(:backend, + user: user, + sources: [source], + type: :webhook, + config: %{url: "https://example.com"} + ) + + Backends.clear_list_backends_cache(source.id) + start_supervised!({SourceSup, source}) + + via = Backends.via_source(source, SourceSup) + children = Supervisor.which_children(via) + backend_ids = [backend1.id, backend2.id] + + assert Enum.count(children, fn + {{_mod, _source_id, backend_id}, _pid, _type, _modules} -> + backend_id in backend_ids + + _other -> + false + end) == 2 + + [_first_child | remaining_children] = children + + {{_mod, _source_id, requested_backend_id} = requested_child_id, requested_child_pid, _type, + _modules} = + Enum.find(remaining_children, fn + {{_mod, _source_id, backend_id}, _pid, _type, _modules} -> + backend_id in backend_ids + + _other -> + false + end) + + unrelated_children = + children + |> Map.new(fn {child_id, pid, _type, _modules} -> {child_id, pid} end) + |> Map.delete(requested_child_id) + + assert :ok = SourceSup.stop_backend_child(source, requested_backend_id) + + remaining_child_pids = + via + |> Supervisor.which_children() + |> Map.new(fn {child_id, pid, _type, _modules} -> {child_id, pid} end) + + refute Process.alive?(requested_child_pid) + refute Map.has_key?(remaining_child_pids, requested_child_id) + + for {child_id, pid} <- unrelated_children do + assert remaining_child_pids[child_id] == pid + assert Process.alive?(pid) + end + end + + test "stop_backend_child/2 leaves children running when the backend is missing", %{ + source: source, + user: user + } do + insert_pair(:backend, + user: user, + sources: [source], + type: :webhook, + config: %{url: "https://example.com"} + ) + + Backends.clear_list_backends_cache(source.id) + start_supervised!({SourceSup, source}) + + via = Backends.via_source(source, SourceSup) + + children = Supervisor.which_children(via) + existing_child_pids = Map.new(children, fn {id, pid, _type, _modules} -> {id, pid} end) + + missing_backend_id = + children + |> Enum.flat_map(fn + {{_mod, _source_id, backend_id}, _pid, _type, _modules} + when is_integer(backend_id) -> + [backend_id] + + _other -> + [] + end) + |> Enum.max() + |> Kernel.+(1) + + assert {:error, :not_found} = SourceSup.stop_backend_child(source, missing_backend_id) + + remaining_child_pids = + via + |> Supervisor.which_children() + |> Map.new(fn {id, pid, _type, _modules} -> {id, pid} end) + + assert remaining_child_pids == existing_child_pids + assert Enum.all?(existing_child_pids, fn {_id, pid} -> Process.alive?(pid) end) + end + test "rules_child_started? when SourceSup already started", %{source: source} do rule_backend = insert(:backend) rule = insert(:rule, source: source, backend: rule_backend) From d60524dd8156f7c6618c017c56baf196ce67ee9d Mon Sep 17 00:00:00 2001 From: ruslandoga Date: Fri, 21 Aug 2026 16:59:32 +0300 Subject: [PATCH 2/5] test: streamline backend child regression --- test/logflare/backends_test.exs | 122 ++++++++++++-------------------- 1 file changed, 45 insertions(+), 77 deletions(-) diff --git a/test/logflare/backends_test.exs b/test/logflare/backends_test.exs index d20245d9ba..931b4041e3 100644 --- a/test/logflare/backends_test.exs +++ b/test/logflare/backends_test.exs @@ -824,107 +824,75 @@ defmodule Logflare.BackendsTest do assert :ok = Backends.restart_source_sup(source) end - test "stop_backend_child/2 stops only the requested backend child", %{ + test "stop_backend_child/2 stops only requested backend children", %{ source: source, user: user } do - [backend1, backend2] = - insert_pair(:backend, - user: user, - sources: [source], - type: :webhook, - config: %{url: "https://example.com"} - ) + # Start two distinct backend children so each lifecycle can be tracked independently. + backend_ids = + for backend_name <- ["first", "second"] do + insert(:backend, + name: "#{backend_name} webhook", + user: user, + sources: [source], + type: :webhook, + config: %{url: "https://#{backend_name}.example.com"} + ).id + end Backends.clear_list_backends_cache(source.id) start_supervised!({SourceSup, source}) via = Backends.via_source(source, SourceSup) children = Supervisor.which_children(via) - backend_ids = [backend1.id, backend2.id] - assert Enum.count(children, fn - {{_mod, _source_id, backend_id}, _pid, _type, _modules} -> - backend_id in backend_ids + backend_child? = fn + {{_mod, _source_id, backend_id}, _pid, _type, _modules} -> backend_id in backend_ids + _child -> false + end - _other -> - false - end) == 2 + child_pids = fn children -> + Map.new(children, fn {child_id, pid, _type, _modules} -> {child_id, pid} end) + end - [_first_child | remaining_children] = children + # Select the second backend child, which cannot be the supervisor's first child. + [ + {{_, _, first_backend_id} = first_child_id, first_child_pid, _, _}, + {{_, _, requested_backend_id} = requested_child_id, requested_child_pid, _, _} + ] = Enum.filter(children, backend_child?) - {{_mod, _source_id, requested_backend_id} = requested_child_id, requested_child_pid, _type, - _modules} = - Enum.find(remaining_children, fn - {{_mod, _source_id, backend_id}, _pid, _type, _modules} -> - backend_id in backend_ids + refute requested_child_id == elem(hd(children), 0) - _other -> - false - end) + existing_child_pids = child_pids.(children) + missing_backend_id = Enum.max(backend_ids) + 1 - unrelated_children = - children - |> Map.new(fn {child_id, pid, _type, _modules} -> {child_id, pid} end) - |> Map.delete(requested_child_id) + # A missing backend ID must leave every existing child running with the same PID. + assert {:error, :not_found} = SourceSup.stop_backend_child(source, missing_backend_id) + assert child_pids.(Supervisor.which_children(via)) == existing_child_pids + assert Enum.all?(Map.values(existing_child_pids), &Process.alive?/1) + # Stopping the second backend must not disturb the first or any unrelated child. assert :ok = SourceSup.stop_backend_child(source, requested_backend_id) - remaining_child_pids = - via - |> Supervisor.which_children() - |> Map.new(fn {child_id, pid, _type, _modules} -> {child_id, pid} end) + remaining_child_pids = child_pids.(Supervisor.which_children(via)) refute Process.alive?(requested_child_pid) - refute Map.has_key?(remaining_child_pids, requested_child_id) + assert remaining_child_pids == Map.delete(existing_child_pids, requested_child_id) + assert remaining_child_pids[first_child_id] == first_child_pid + assert Process.alive?(first_child_pid) + assert Enum.all?(Map.values(remaining_child_pids), &Process.alive?/1) - for {child_id, pid} <- unrelated_children do - assert remaining_child_pids[child_id] == pid - assert Process.alive?(pid) - end - end + # The first backend remains independently stoppable after the second is removed. + assert :ok = SourceSup.stop_backend_child(source, first_backend_id) - test "stop_backend_child/2 leaves children running when the backend is missing", %{ - source: source, - user: user - } do - insert_pair(:backend, - user: user, - sources: [source], - type: :webhook, - config: %{url: "https://example.com"} - ) - - Backends.clear_list_backends_cache(source.id) - start_supervised!({SourceSup, source}) - - via = Backends.via_source(source, SourceSup) - - children = Supervisor.which_children(via) - existing_child_pids = Map.new(children, fn {id, pid, _type, _modules} -> {id, pid} end) - - missing_backend_id = - children - |> Enum.flat_map(fn - {{_mod, _source_id, backend_id}, _pid, _type, _modules} - when is_integer(backend_id) -> - [backend_id] - - _other -> - [] - end) - |> Enum.max() - |> Kernel.+(1) - - assert {:error, :not_found} = SourceSup.stop_backend_child(source, missing_backend_id) + remaining_child_pids = child_pids.(Supervisor.which_children(via)) - remaining_child_pids = - via - |> Supervisor.which_children() - |> Map.new(fn {id, pid, _type, _modules} -> {id, pid} end) + expected_child_pids = + existing_child_pids |> Map.delete(requested_child_id) |> Map.delete(first_child_id) - assert remaining_child_pids == existing_child_pids - assert Enum.all?(existing_child_pids, fn {_id, pid} -> Process.alive?(pid) end) + refute Process.alive?(first_child_pid) + assert remaining_child_pids == expected_child_pids + assert Enum.all?(Map.values(remaining_child_pids), &Process.alive?/1) end test "rules_child_started? when SourceSup already started", %{source: source} do From 63b3ad2439f09e0c044c19cb09a6704544ff4b29 Mon Sep 17 00:00:00 2001 From: ruslandoga Date: Fri, 21 Aug 2026 17:04:43 +0300 Subject: [PATCH 3/5] test: simplify child PID snapshots --- test/logflare/backends_test.exs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/logflare/backends_test.exs b/test/logflare/backends_test.exs index 931b4041e3..6b63bb8658 100644 --- a/test/logflare/backends_test.exs +++ b/test/logflare/backends_test.exs @@ -851,8 +851,10 @@ defmodule Logflare.BackendsTest do _child -> false end - child_pids = fn children -> - Map.new(children, fn {child_id, pid, _type, _modules} -> {child_id, pid} end) + child_pids = fn -> + via + |> Supervisor.which_children() + |> Map.new(fn {child_id, pid, _type, _modules} -> {child_id, pid} end) end # Select the second backend child, which cannot be the supervisor's first child. @@ -863,18 +865,18 @@ defmodule Logflare.BackendsTest do refute requested_child_id == elem(hd(children), 0) - existing_child_pids = child_pids.(children) + existing_child_pids = child_pids.() missing_backend_id = Enum.max(backend_ids) + 1 # A missing backend ID must leave every existing child running with the same PID. assert {:error, :not_found} = SourceSup.stop_backend_child(source, missing_backend_id) - assert child_pids.(Supervisor.which_children(via)) == existing_child_pids + assert child_pids.() == existing_child_pids assert Enum.all?(Map.values(existing_child_pids), &Process.alive?/1) # Stopping the second backend must not disturb the first or any unrelated child. assert :ok = SourceSup.stop_backend_child(source, requested_backend_id) - remaining_child_pids = child_pids.(Supervisor.which_children(via)) + remaining_child_pids = child_pids.() refute Process.alive?(requested_child_pid) assert remaining_child_pids == Map.delete(existing_child_pids, requested_child_id) @@ -885,7 +887,7 @@ defmodule Logflare.BackendsTest do # The first backend remains independently stoppable after the second is removed. assert :ok = SourceSup.stop_backend_child(source, first_backend_id) - remaining_child_pids = child_pids.(Supervisor.which_children(via)) + remaining_child_pids = child_pids.() expected_child_pids = existing_child_pids |> Map.delete(requested_child_id) |> Map.delete(first_child_id) From 17fbf801b4e1e1789c8b1aca97c06b80c2bfbdb7 Mon Sep 17 00:00:00 2001 From: ruslandoga Date: Fri, 21 Aug 2026 17:15:16 +0300 Subject: [PATCH 4/5] test: derive backend PID snapshots from source --- test/logflare/backends_test.exs | 49 +++++++++++++++++---------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/test/logflare/backends_test.exs b/test/logflare/backends_test.exs index 6b63bb8658..ecacfdbd3c 100644 --- a/test/logflare/backends_test.exs +++ b/test/logflare/backends_test.exs @@ -843,58 +843,61 @@ defmodule Logflare.BackendsTest do Backends.clear_list_backends_cache(source.id) start_supervised!({SourceSup, source}) - via = Backends.via_source(source, SourceSup) - children = Supervisor.which_children(via) - - backend_child? = fn - {{_mod, _source_id, backend_id}, _pid, _type, _modules} -> backend_id in backend_ids - _child -> false + children = fn -> + source + |> Backends.via_source(SourceSup) + |> Supervisor.which_children() end child_pids = fn -> - via - |> Supervisor.which_children() + children.() |> Map.new(fn {child_id, pid, _type, _modules} -> {child_id, pid} end) end + backend_pids = fn -> + for {{_mod, _source_id, backend_id} = child_id, pid, _type, _modules} <- children.(), + backend_id in backend_ids, + do: {child_id, pid} + end + + existing_child_pids = child_pids.() + existing_backend_pids = backend_pids.() + # Select the second backend child, which cannot be the supervisor's first child. [ - {{_, _, first_backend_id} = first_child_id, first_child_pid, _, _}, - {{_, _, requested_backend_id} = requested_child_id, requested_child_pid, _, _} - ] = Enum.filter(children, backend_child?) + {{_, _, first_backend_id} = first_child_id, first_child_pid}, + {{_, _, requested_backend_id} = requested_child_id, requested_child_pid} + ] = existing_backend_pids - refute requested_child_id == elem(hd(children), 0) + refute requested_child_id == elem(hd(children.()), 0) - existing_child_pids = child_pids.() missing_backend_id = Enum.max(backend_ids) + 1 # A missing backend ID must leave every existing child running with the same PID. assert {:error, :not_found} = SourceSup.stop_backend_child(source, missing_backend_id) assert child_pids.() == existing_child_pids + assert backend_pids.() == existing_backend_pids assert Enum.all?(Map.values(existing_child_pids), &Process.alive?/1) - # Stopping the second backend must not disturb the first or any unrelated child. + # Stopping the requested backend must not disturb the other or any unrelated child. assert :ok = SourceSup.stop_backend_child(source, requested_backend_id) - remaining_child_pids = child_pids.() - refute Process.alive?(requested_child_pid) - assert remaining_child_pids == Map.delete(existing_child_pids, requested_child_id) - assert remaining_child_pids[first_child_id] == first_child_pid + assert child_pids.() == Map.delete(existing_child_pids, requested_child_id) + assert backend_pids.() == [{first_child_id, first_child_pid}] assert Process.alive?(first_child_pid) - assert Enum.all?(Map.values(remaining_child_pids), &Process.alive?/1) + assert Enum.all?(Map.values(child_pids.()), &Process.alive?/1) # The first backend remains independently stoppable after the second is removed. assert :ok = SourceSup.stop_backend_child(source, first_backend_id) - remaining_child_pids = child_pids.() - expected_child_pids = existing_child_pids |> Map.delete(requested_child_id) |> Map.delete(first_child_id) refute Process.alive?(first_child_pid) - assert remaining_child_pids == expected_child_pids - assert Enum.all?(Map.values(remaining_child_pids), &Process.alive?/1) + assert child_pids.() == expected_child_pids + assert backend_pids.() == [] + assert Enum.all?(Map.values(child_pids.()), &Process.alive?/1) end test "rules_child_started? when SourceSup already started", %{source: source} do From 4bcfa6c981181ba6e5c3a85db70f2568fb082f4a Mon Sep 17 00:00:00 2001 From: ruslandoga Date: Fri, 21 Aug 2026 17:21:13 +0300 Subject: [PATCH 5/5] test: pattern match the first backend child --- test/logflare/backends_test.exs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/logflare/backends_test.exs b/test/logflare/backends_test.exs index ecacfdbd3c..6aff4a85b1 100644 --- a/test/logflare/backends_test.exs +++ b/test/logflare/backends_test.exs @@ -869,7 +869,8 @@ defmodule Logflare.BackendsTest do {{_, _, requested_backend_id} = requested_child_id, requested_child_pid} ] = existing_backend_pids - refute requested_child_id == elem(hd(children.()), 0) + [{first_supervisor_child_id, _, _, _} | _] = children.() + refute requested_child_id == first_supervisor_child_id missing_backend_id = Enum.max(backend_ids) + 1