Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85711239
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
7 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Sep 19, 4:29 AM (1 d, 7 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1768855
Default Alt Text
(7 KB)
Attached To
Mode
rPUBE pleroma-upstream
Attached
Detach File
Event Timeline
Log In to Comment