Page MenuHomePhorge

No OneTemporary

Size
7 KB
Referenced Files
None
Subscribers
None
diff --git a/test/pleroma/release_vm_args_test.exs b/test/pleroma/release_vm_args_test.exs
index 1df050c72..20370df7e 100644
--- a/test/pleroma/release_vm_args_test.exs
+++ b/test/pleroma/release_vm_args_test.exs
@@ -1,52 +1,265 @@
# Pleroma: A lightweight social networking server
# Copyright © 2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ReleaseVMArgsTest do
use ExUnit.Case, async: true
@vm_args_path Path.expand("rel/vm.args.eex", Path.join([__DIR__, "..", ".."]))
+ @erl Path.join(:code.root_dir(), "bin/erl")
- test "release VM args set a finite kernel shutdown_timeout" do
- assert File.exists?(@vm_args_path)
+ @sources %{
+ "check_env" => ~S"""
+ -module(check_env).
+ -export([main/0]).
+
+ main() ->
+ io:format("~p", [application:get_env(kernel, shutdown_timeout)]),
+ halt(0).
+ """,
+ "main_app" => ~S"""
+ -module(main_app).
+ -behaviour(application).
+ -export([start/2, stop/1]).
+
+ start(_Type, _Args) ->
+ {ok, spawn(fun exiting_top/0)}.
+
+ exiting_top() ->
+ %% Mirrors the :pleroma application exiting with shutdown: this node
+ %% terminates while the application master of the other application
+ %% stays stuck.
+ receive
+ after 50 -> exit(shutdown)
+ end.
+
+ stop(_State) ->
+ ok.
+ """,
+ "stuck_app" => ~S"""
+ -module(stuck_app).
+ -behaviour(application).
+ -export([start/2, stop/1]).
+
+ start(_Type, _Args) ->
+ {ok, spawn(fun stuck/0)}.
+
+ stuck() ->
+ %% Traps exits and never terminates, wedging its application master on
+ %% stop just like a stuck supervision tree in the reported crash.
+ process_flag(trap_exit, true),
+ stuck_loop().
+
+ stuck_loop() ->
+ receive
+ never -> stuck_loop()
+ end.
+
+ stop(_State) ->
+ ok.
+ """,
+ "test_node" => ~S"""
+ -module(test_node).
+ -export([boot/0]).
+
+ boot() ->
+ %% Safety net so a hung node cannot leak past this test suite.
+ spawn(fun() ->
+ receive
+ after 20000 -> halt(124)
+ end
+ end),
+ application:start(stuck_app),
+ application:start(main_app, permanent),
+ io:put_chars("STUCK_READY\n").
+ """
+ }
+
+ setup do
+ tmp_dir =
+ Path.join(System.tmp_dir!(), "release_vm_args_test_#{System.unique_integer([:positive])}")
+
+ ebin = Path.join(tmp_dir, "ebin")
+ File.mkdir_p!(ebin)
+ write_app_file(ebin, "stuck_app")
+ write_app_file(ebin, "main_app")
+ compile_sources(ebin)
+ on_exit(fn -> File.rm_rf!(tmp_dir) end)
+
+ %{ebin: ebin}
+ end
+
+ test "release VM args set a finite kernel shutdown_timeout" do
assert timeout = shutdown_timeout(File.read!(@vm_args_path))
assert is_integer(timeout)
assert timeout > 0
end
+ test "kernel shutdown_timeout from rel/vm.args.eex reaches the VM as kernel config", %{
+ ebin: ebin
+ } do
+ assert timeout = shutdown_timeout(File.read!(@vm_args_path))
+ timeout_arg = Integer.to_string(timeout)
+
+ {port, ref} =
+ start_erl([
+ "-pa",
+ ebin,
+ "-kernel",
+ "shutdown_timeout",
+ timeout_arg,
+ "-env",
+ "ERL_CRASH_DUMP",
+ "/dev/null",
+ "-s",
+ "check_env",
+ "main"
+ ])
+
+ assert {:exited, 0, output} = collect(port, ref, nil, 15_000)
+ kill(port)
+ assert String.trim(output) == "{ok,#{timeout}}"
+ end
+
+ test "a stuck application master cannot keep the node alive past shutdown_timeout", %{
+ ebin: ebin
+ } do
+ # rel/vm.args.eex ships 30 seconds; a real wait for the shipped value would
+ # make this test far too slow, so the mechanism is proven with a short one.
+ {port, ref} =
+ start_erl([
+ "-pa",
+ ebin,
+ "-kernel",
+ "shutdown_timeout",
+ "500",
+ "-env",
+ "ERL_CRASH_DUMP",
+ "/dev/null",
+ "-s",
+ "test_node",
+ "boot"
+ ])
+
+ assert {:exited, status, output} = collect(port, ref, nil, 15_000)
+ kill(port)
+ refute status == 0
+ assert output =~ "Kernel pid terminated"
+ end
+
+ test "without shutdown_timeout a stuck application master keeps the node alive", %{
+ ebin: ebin
+ } do
+ {port, ref} =
+ start_erl(["-pa", ebin, "-env", "ERL_CRASH_DUMP", "/dev/null", "-s", "test_node", "boot"])
+
+ assert {:running, _output} = collect(port, ref, "STUCK_READY", 15_000)
+ assert {:running, _output} = collect(port, ref, nil, 3_000)
+ kill(port)
+ end
+
defp shutdown_timeout(content) do
content
|> String.split(["\r\n", "\n"])
|> Enum.map(&strip_comment/1)
|> Enum.find_value(fn line ->
case Regex.run(~r/^-\s*kernel\s+shutdown_timeout\s+(\S+)\s*$/, String.trim(line)) do
[_, value] -> value
nil -> nil
end
end)
|> case do
nil ->
flunk("rel/vm.args.eex does not set -kernel shutdown_timeout")
"infinity" ->
flunk("rel/vm.args.eex sets -kernel shutdown_timeout to infinity")
value ->
case Integer.parse(value) do
{timeout, ""} when timeout > 0 ->
timeout
_ ->
flunk("rel/vm.args.eex sets -kernel shutdown_timeout to an invalid value")
end
end
end
defp strip_comment(line) do
case String.split(line, "#", parts: 2) do
[code, _comment] -> code
[code] -> code
end
end
+
+ defp write_app_file(ebin, name) do
+ File.write!(Path.join(ebin, "#{name}.app"), ~s"""
+ {application, #{name}, [
+ {description, "#{name} test fixture"},
+ {vsn, "1.0.0"},
+ {registered, []},
+ {modules, [#{name}]},
+ {applications, [kernel, stdlib]},
+ {mod, {#{name}, []}}
+ ]}.
+ """)
+ end
+
+ defp compile_sources(ebin) do
+ Enum.each(@sources, fn {name, source} ->
+ source_path = Path.join(ebin, "#{name}.erl")
+ File.write!(source_path, source)
+ assert {:ok, _module} = :compile.file(source_path, [:report_errors, {:outdir, ebin}])
+ end)
+ end
+
+ defp start_erl(args) do
+ port =
+ Port.open({:spawn_executable, @erl}, [
+ :binary,
+ :exit_status,
+ :stderr_to_stdout,
+ {:args, ["-noshell" | args]},
+ :hide
+ ])
+
+ {port, Port.monitor(port)}
+ end
+
+ defp collect(port, ref, marker, deadline, output \\ "") do
+ receive do
+ {^port, {:data, data}} ->
+ output = output <> data
+
+ if is_binary(marker) and String.contains?(output, marker) do
+ {:running, output}
+ else
+ collect(port, ref, marker, deadline, output)
+ end
+
+ {^port, {:exit_status, status}} ->
+ receive do
+ {:DOWN, ^ref, :port, ^port, _reason} -> {:exited, status, output}
+ after
+ 5_000 -> {:exited, status, output}
+ end
+
+ {:DOWN, ^ref, :port, ^port, _reason} ->
+ {:exited, nil, output}
+ after
+ deadline ->
+ {:running, output}
+ end
+ end
+
+ defp kill(port) do
+ case :erlang.port_info(port, :os_pid) do
+ {:os_pid, os_pid} -> System.cmd("kill", ["-9", Integer.to_string(os_pid)])
+ _ -> :ok
+ end
+
+ Port.close(port)
+ end
end

File Metadata

Mime Type
text/x-diff
Expires
Sat, Sep 19, 4:22 AM (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768855
Default Alt Text
(7 KB)

Event Timeline