Elixir / Phoenix: Почему определенные файлы / строки не отображаются в моей трассировке стека исключений? - PullRequest
2 голосов
/ 06 мая 2019

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

Пример 1. Помощник видаметод опущен

Вот один пример использования помощника format_date, вызванного из моего шаблона.Код:

# The template, `...web/templates/home/index.html.eex`:
<h1>My Super Old Company</h1>
<p>We've been operating since <%= format_date(Timex.now()) %>!!</p>

# The `format_date` helper lives in the corresponding View module:
def format_date(date) do
  template = "%b %d, %Y"
  method = :strftime
  # Whoops, it looks like I passed a bad value to Timex here
  Timex.format!(nil, template, method)
end

Попытка визуализации этой страницы приводит к исключению:

Request: GET /
** (exit) an exception was raised:
    ** (ArgumentError) invalid_date
        (timex) lib/format/datetime/formatter.ex:60: Timex.Format.DateTime.Formatter.lformat!/4
        (rtl) lib/rtl_web/templates/home/index.html.eex:3: RTLWeb.HomeView."index.html"/1
        (rtl) lib/rtl_web/templates/layout/app.html.eex:27: RTLWeb.LayoutView."app.html"/1
        ...

Понятно, что Timex.format!/3 не нравится nil и он взрывается.Но подождите, почему мой помощник format_date/1 не упоминается в трассировке стека?В конце концов, этот помощник, где ошибка.И код определенно проходит через эту функцию;если я изменю nil, чтобы передать date var, ошибка исчезнет.

Я мог бы взглянуть на код шаблона, чтобы сделать вывод, что этот конкретный помощник был вызван, но я не могу сказать, какая строкаошибка была включена.Если функция длинная или включает несколько вызовов Timex, я могу легко запутаться в том, какой конкретный вызов произошел с ошибкой.

Пример 2: функция контекста опущена

Вот еще один пример.В этом случае мой контроллер вызывает контекстный помощник для извлечения записи Ecto.

# The controller action:
def index(conn, _params) do
  # Whoops, looks like I hard-coded the project id by accident
  project = MyContext.get_project!(42)
  render(conn, "index.html", project: project)
end

# The MyContext context defines `get_project!/1` to fetch that id from the repo:
def get_project!(id) do
  Repo.get!(Project, id)
end

Как и ожидалось, когда я запускаю тест моего контроллера, я получаю Ecto.NoResultsError, потому что нет проекта с таким идентификатором.Но опять же, я замечаю, что вызов MyContext.get_project!/1 не включен в трассировку стека:

** (Ecto.NoResultsError) expected at least one result but got none in query:

from p in RTL.Projects.Project,
  where: p.id == ^42

code: conn = get(conn, Routes.home_path(conn, :index))
stacktrace:
  (ecto) lib/ecto/repo/queryable.ex:80: Ecto.Repo.Queryable.one!/4
  (rtl) lib/rtl_web/controllers/home_controller.ex:5: RTLWeb.HomeController.index/2
  (rtl) lib/rtl_web/controllers/home_controller.ex:1: RTLWeb.HomeController.action/2
  (rtl) lib/rtl_web/controllers/home_controller.ex:1: RTLWeb.HomeController.phoenix_controller_pipeline/2
  ...

Еще более странно, я замечаю, что если я добавлю оператор в конце get_project/1, как это, трассировка стека меняется , чтобы включить эту строку.

# When I add this nil at the tail...
def get_project!(id) do
  Repo.get!(Project, id)
  nil
end

# The stacktrace includes the function call.
...
(ecto) lib/ecto/repo/queryable.ex:80: Ecto.Repo.Queryable.one!/4
(rtl) lib/rtl/projects/projects.ex:12: RTL.Projects.get_project!/1
(rtl) lib/rtl_web/controllers/home_controller.ex:5: RTLWeb.HomeController.index/2
...

Но если я переместу этот ноль в начало функции, строка снова исчезнет из трассировки стека.

# When I move the nil to above the Repo.get! call...
def get_project!(id) do
  nil
  Repo.get!(Project, id)
end

# The stacktrace excludes the function call.
...
(ecto) lib/ecto/repo/queryable.ex:80: Ecto.Repo.Queryable.one!/4
(rtl) lib/rtl_web/controllers/home_controller.ex:5: RTLWeb.HomeController.index/2
...

Пример 3: тестовые помощники утверждений опущены

Вот тест контроллера, который вызывает помощника подтверждения, который вызывает другого помощника, который вызывает другого помощника, который, в конце концов, проваливает утверждение.Но в моей трассировке стека ошибок я не вижу никаких признаков того, что эти помощники вызваны;Я получаю только строку самого высокого уровня в моем тестовом блоке.

# The test example and helpers:
test "#index renders the page", %{conn: conn} do
  conn = get(conn, Routes.home_path(conn, :index))
  assert_foo("foo")
  assert html_response(conn, 200) =~ "bring personal storytelling back"
end

defp assert_foo(object) do
  assert_bar(object)
end

defp assert_bar(object) do
  assert_baz(object)
end

defp assert_baz(object) do
  learn_secret_of_life(object)
end

defp learn_secret_of_life(object) do
  assert object == "elixir"
end
# The test failure message:
Assertion with == failed
code:  assert object == "elixir"
left:  "foo"
right: "elixir"
stacktrace:
  test/rtl_web/controllers/home_controller_test.exs:6: (test)

Если я вызову исключение в sub-sub-sub-helper, а не просто сделаю обреченное утверждение, трассировка стекаупоминает последний вспомогательный метод, но продолжает опускать промежуточные:

# Modified helper function:
defp learn_secret_of_life(object) do
  assert object == raise("OMG")
end

# Now the stacktrace includes this helper, but not any of the chain of helpers that lead to its invocation:
** (RuntimeError) OMG
code: assert_foo("foo")
stacktrace:
  test/rtl_web/controllers/home_controller_test.exs:23: RTLWeb.HomeControllerTest.learn_secret_of_life/1
  test/rtl_web/controllers/home_controller_test.exs:6: (test)

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

Мои вопросы:

  • Почему Elixir иногда пропускает определенные файлы / строки из трассировки стека?
  • есть ли способ сказать это, пожалуйста, не делайте этого?
  • Это меня смущает только я?

1 Ответ

4 голосов
/ 07 мая 2019

Это происходит из-за оптимизации хвостового вызова.

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

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

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


Чтобы избежать этого, измените код так, чтобы вызов внутренней функции больше не был последним, что делает внешняя функция.Как вы заметили, возврат nil вместо возвращаемого значения внутренней функции работает - это то, что я бы сделал для помощника assert.Для не тестового кода я бы с большей неохотой делал это, так как обычно вам нужна оптимизация хвостового вызова: она уменьшает использование памяти и время выполнения.Хотя, похоже, что это может стоить сделать это для функций, вызываемых из шаблонов, просто для повышения отладки.


Вы определенно не единственный, кого это смущает: это достаточно часто, чтобы быть упоминается в Erlang FAQ .Смотрите также обсуждение в https://github.com/elixir-lang/elixir/issues/6357.

...