Создать рекурсивный вызов в макрофункции - PullRequest
0 голосов
/ 01 апреля 2020

Я пытаюсь создать канал >>> , который снова запускает каждую функцию в случае сбоя.

Рабочая версия без макросов:

defmodule Retry.Test do
  use ExUnit.Case
  import RetryPipe

  setup_all do

    %{a: 1, b: 2}

  end

  test "example", details do

    retry_function(fn -> func1(details) end, 2)
    |> (&(retry_function(fn -> func2(&1) end, 2))).()
    |> (&(retry_function(fn -> func3(&1) end, 2))).()
    |> (&(retry_function(fn -> func4(&1) end, 2))).()

  end

  def retry_function(fun_to_retrieve, 1) do
    fun_to_retrieve.()
  end
  def retry_function(fun_to_retrieve, loops) do
    try do
      fun_to_retrieve.()
    rescue
      msg -> IO.inspect(msg)
        retry_function(fun_to_retrieve, loops-1)
    end
  end

  def func1(details) do
    IO.inspect("func1")
    assert details.a == 1
    details
  end

  def func2(details) do
    IO.inspect("func2")
    assert details.b == 2
    details
  end

  def func3(details) do
    IO.inspect("func3")
    raise "failure"
  end

  def func4(details) do
    IO.inspect("func4")
  end
end

Я получил ожидаемый результат:


Но когда я пытаюсь реализовать его с помощью макроса следующим образом:

defmodule RetryPipe do
  defmacro left >>> right,
    do: retry(left, right, 2)

  def retry(param, func, 1),
    do: quote do: unquote(param) |> unquote(func)

  def retry_function(param, func, loops) do
    quote  do
      try do

        unquote(param) |> unquote(func)

      rescue

        msg -> IO.inspect(msg)
               retry_function(param, func, loops-1)

      end
    end
  end
end

Я пытаюсь вызвать из функции макроса рекурсивную функцию: retry_function , который должен запускаться снова в случае сбоя pf в функции (изменяя третий параметр, я могу контролировать, сколько раз он будет повторяться, прежде чем он действительно выйдет из строя)

И затем я использую его следующим образом:

defmodule Retry.Test do
  use ExUnit.Case
  import RetryPipe

  setup_all do

    %{a: 1, b: 2}

  end

  test "example", details do

    details
    >>> func1()
    >>> func2()
    >>> func3()
    >>> func4()

  end

  def func1(details) do
    IO.inspect("func1")
    assert details.a == 1
    details
  end

  def func2(details) do
    IO.inspect("func2")
    assert details.b == 2
    details
  end

  def func3(details) do
    IO.inspect("func3")
    raise "failure"
  end

  def func4(details) do
    IO.inspect("func4")
  end
end

Я ожидал получить в результате:

Automation.CICDTests.EndToEnd.Test
  * test example
"func1"
"func2"
"func3"
%RuntimeError{message: "failure"}
"func3"
  * test example
 (7.2ms)

  1) test example
 (Automation.CICDTests.EndToEnd.Test)
     test/cicd_tests/end_to_end_test.exs:17
     ** (RuntimeError) failure
     stacktrace:
       test/cicd_tests/end_to_end_test.exs:40: Automation.CICDTests.EndToEnd.Test.func3/1



Finished in 0.1 seconds
1 test, 1 failure

Randomized with seed 0

Но вместо этого я получил:

Automation.CICDTests.EndToEnd.Test
  * test example
"func1"
"func2"
"func3"
%RuntimeError{message: "failure"}
"func1"
"func2"
"func3"
%RuntimeError{message: "failure"}
"func1"
"func2"
"func3"
%RuntimeError{message: "failure"}
"func1"
"func2"
"func3"
  * test example
 (7.2ms)

  1) test example
 (Automation.CICDTests.EndToEnd.Test)
     test/cicd_tests/end_to_end_test.exs:17
     ** (RuntimeError) failure
     stacktrace:
       test/cicd_tests/end_to_end_test.exs:40: Automation.CICDTests.EndToEnd.Test.func3/1



Finished in 0.1 seconds
1 test, 1 failure

Randomized with seed 0

1 Ответ

0 голосов
/ 02 апреля 2020

Основная проблема заключается в том, что как только повторные попытки исчерпаны, следует повторить попытку, если и только если это было в пределах владельца вызова, а не вызванной ошибки по конвейеру.

Тем не менее, необходимо явно аннулировать __STACKTRACE__ и повторите попытку, если переданный func был на самом деле вызвавшей функцию. Приведенный ниже код демонстрирует возможный подход.

Кроме того, важно заключить в кавычки последующий вызов retry/3, мне любопытно, как версия, которую вы разместили, когда-либо компилировалась после того, как вы вернули содержимое retry, указанное дважды.

defmodule RetryPipe do
  defmacro left >>> right,
    do: retry(left, right, 2)

  def retry(param, func, 1),
    do: quote do: unquote(param) |> unquote(func)

  def retry(param, func, loops) do
    {m, f} =
      case func do
        {{:., _, [{:__aliases__, _, [mod]}, fun]}, _, _} ->
          {Module.concat([mod]), fun}
        _ ->
          {nil, nil}
      end

    quote do
      try do
        unquote(param) |> unquote(func)
      rescue
        ex ->
          case __STACKTRACE__ do
            [{unquote(m), unquote(f), _arity, _meta} | _] ->
              unquote(retry(param, func, loops - 1))
            _ -> reraise ex, __STACKTRACE__
          end
      end
    end 
  end
end

Тест будет выглядеть следующим образом.

defmodule Test do
  def f1(details),
    do: IO.inspect(details, label: "OK[1]")

  def f2(details) do
    IO.inspect(details, label: "BAD[2]")
    raise "failure"
  end

  def f3(details),
    do: IO.inspect(details, label: "OK[3]")
end

import RetryPipe
%{a: 1, b: 2} >>> Test.f1() >>> Test.f2() >>> Test.f3()

#⇒ OK[1]: %{a: 1, b: 2}
#⇒ BAD[2]: %{a: 1, b: 2}
#
# ** (RuntimeError) failure
#    iex:35: Test.f2/1

Обратите внимание, что этот код может потребовать дополнительной настройки при работе с локальными функциями, и, как правило, он чрезвычайно высок agile. В конце концов, __STACKTRACE__ не был изобретен для этого.


Если вы хотите повторить попытку несколько раз, просто сделайте это явно. Трубы могут выглядеть сексуально, но обычно ваши коллеги-разработчики не хотят видеть их в незнакомом коде.

...