From 77765763b25866893a566edb0940e0ca65355725 Mon Sep 17 00:00:00 2001 From: "palantir-valiot[bot]" <279094399+palantir-valiot[bot]@users.noreply.github.com> Date: Sat, 13 Jun 2026 20:22:48 +0000 Subject: [PATCH] fix(rpc): catch :exit/:throw from erpc.call; handle {:exception, :terminating} (OPS-4536) --- CHANGELOG.md | 4 ++++ lib/relix/rpc/relix_rpc.ex | 16 +++++++++++++++- mix.exs | 4 ++-- mix.lock | 4 ++-- test/relix_test.exs | 33 +++++++++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0ff149..5a98c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 0.1.5 [2026-06-13] + +- [Bug Fix] Handle `:exit` (e.g. `{:exception, :terminating}`) and `:throw` from `erpc.call` in `Relix.RPC.execute/1` (OPS-4536). Previously these propagated as unhandled exits, crashing callers such as `TokenHelper.authorize_token_via_rpc`. + ## 0.1.4 [2026-04-02] - [Bug Fix] Improve RPC error logging to include module, function, arity, and exception message. diff --git a/lib/relix/rpc/relix_rpc.ex b/lib/relix/rpc/relix_rpc.ex index c43244d..41b89ca 100644 --- a/lib/relix/rpc/relix_rpc.ex +++ b/lib/relix/rpc/relix_rpc.ex @@ -69,7 +69,21 @@ defmodule Relix.RPC do "RPC call #{rpc.module}.#{rpc.function}/#{length(rpc.args)} failed on #{match_node}: #{Exception.message(e)}" ) - reraise e, __STACKTRACE__ + {:error, {:rpc_exit, e}} + catch + :exit, reason -> + Logger.error( + "RPC call #{rpc.module}.#{rpc.function}/#{length(rpc.args)} exited on #{match_node}: #{inspect(reason)}" + ) + + {:error, {:rpc_exit, reason}} + + :throw, value -> + Logger.error( + "RPC call #{rpc.module}.#{rpc.function}/#{length(rpc.args)} threw on #{match_node}: #{inspect(value)}" + ) + + {:error, {:rpc_exit, {:throw, value}}} end) do %{rpc | resp_body: response} |> put_status(:executed) diff --git a/mix.exs b/mix.exs index be97513..6b2a47d 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Relix.MixProject do def project do [ app: :relix, - version: "0.1.4", + version: "0.1.5", elixir: "~> 1.15", start_permanent: Mix.env() == :prod, deps: deps(), @@ -34,7 +34,7 @@ defmodule Relix.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:ex_doc, ">= 0.0.0", only: :dev, runtime: false} + {:ex_doc, "~> 0.40", only: :dev, runtime: false} ] end end diff --git a/mix.lock b/mix.lock index 0c19687..c5fdd6e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,8 +1,8 @@ %{ "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, - "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, + "ex_doc": {:hex, :ex_doc, "0.40.3", "4a972ffe64bc07dc605af487e98fc19b72a4185f55ca031b94c0552d6071c1d9", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "2756e357742fecd9749b489b85d67c9ce99c465f2e75728d9e6dc8d704b973de"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, - "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, + "makeup_erlang": {:hex, :makeup_erlang, "1.1.0", "835f7e60792e08824cda445639555d7bf1bbbddb1b60b306e33cb6f6db24dc74", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "1cd6780fb1dd1a03979abaed0fe82712b0625118fd5257d3ebbf73f960c73c3c"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, } diff --git a/test/relix_test.exs b/test/relix_test.exs index cbf7f0c..9e6aa0f 100644 --- a/test/relix_test.exs +++ b/test/relix_test.exs @@ -65,4 +65,37 @@ defmodule RelixTest do |> RPC.execute() |> RPC.response(select: [1]) end + + # Reproduces OPS-4536: erpc.call surfaces exit(R) from the remote MFA as {:exception, R} + # (a catchable :exit, not a rescue-able exception). Before the fix this crashed the caller. + # Plain `exit(:terminating)` in the MFA yields exactly `{:exception, :terminating}` at the call site. + def cause_erpc_terminating_exit, do: exit(:terminating) + + test "handles {:exception, :terminating} exit from erpc.call and returns error response" do + rpc = %Relix.RPC{ + node: :self, + module: __MODULE__, + function: :cause_erpc_terminating_exit, + args: [] + } + + executed = RPC.execute(rpc) + assert executed.status == :executed + assert {:error, {:rpc_exit, {:exception, :terminating}}} = RPC.response(executed) + end + + def cause_erpc_throw, do: throw(:boom) + + test "handles throw from erpc.call and returns error response" do + rpc = %Relix.RPC{ + node: :self, + module: __MODULE__, + function: :cause_erpc_throw, + args: [] + } + + executed = RPC.execute(rpc) + assert executed.status == :executed + assert {:error, {:rpc_exit, {:throw, :boom}}} = RPC.response(executed) + end end