Recompilation triggered after request in dev even without code changes (iex -S mix phx.server)

Hi all,

I’m seeing some unexpected recompilation behavior in dev, and I’m not sure what’s causing it.

Here’s the flow:

  • rm -rf _build deps .elixir_ls
  • mix deps.get
  • mix clean and mix compile
  • I start my Phoenix app with MIX_DEBUG=1 iex -S mix phx.server.
  • Everything boots up cleanly (already compiled).
  • I haven’t modified any .ex files.
  • Then, as soon as I make a request in the browser, a recompilation is triggered:

Shortly after that, a background process (in my case, an Oban plugin) crashes with an UndefinedFunctionError, because the module it depends on (like MyApp.Repo) seems to have been purged and recompiled after app startup.

Here is the output from iex just after the browser request:

    iex()>
    -> Running mix compile.elixir --purge-consolidation-path-if-stale /home/xxxxxxxx/workspace/my_app/_build/dev/lib/my_app/consolidated --no-all-warnings (inside MyApp.MixProject)
    ...
    [error] GenServer {Oban.Registry, {Oban, Oban.Sonar}} terminating
** (UndefinedFunctionError) function MyApp.Repo.query/3 is undefined (module MyApp.Repo is not available
    # ... (more similar errors)
    [notice] Application oban_met exited: shutdown
    # .... (errors)
    Compiling 432 files (.ex)
    # .... (still errors)
    <- Ran mix compile.elixir in 18412ms
    -> Running mix compile.app --purge-consolidation-path-if-stale /home/xxxxxxxx/workspace/my_app/_build/dev/lib/my_app/consolidated --no-all-warnings (inside MyApp.MixProject)
    Generated my_app app
    <- Ran mix compile.app in 6ms
    -> Running mix compile.protocols (inside MyApp.MixProject)
    <- Ran mix compile.protocols in 32ms
    [info] GET /patient/patient_records/new
    # ...
    [notice] Application my_app exited: shutdown
    iex()>

Things I’ve already tried:

  • Disabled all watchers in config/dev.exs (same behaviour)
  • Verified that I’m not touching any files manually.
  • Confirmed that it only recompiles after an HTTP request, not on app boot.
  • If I disable CodeReloader, I don’t have the recompilation…
config :my_app, MyAppWeb.Endpoint,
  code_reloader: false

So it seems like Phoenix.CodeReloader is the one triggering the recompilation but why would Phoenix.CodeReloader trigger a recompilation if no source files changed?

I have also the standard config for live_reload Endpoint

# Watch static and templates for browser reloading.
config :my_app, MyAppWeb.Endpoint,
  live_reload: [
    patterns: [
      ~r"priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$",
      ~r"priv/gettext/.*(po)$",
      ~r"lib/my_app_web/(controllers|live|components)/.*(ex|heex)$"
    ]
  ]

Also note that this happened after an update to erlang 28.0.2, elixir 1.18.4 from elixir 1.17 / erlang 27.

Any insights appreciated!

Thanks :folded_hands:

Have you tried the usual suspects i.e. rm -rf _build deps .elixir_ls && mix do deps.get, compile?

Yes I already did that first, I forgot to mention it

I rolled back to erlang 27.0.1 and elixir 1.17.2 (without touching anything else), I get the following result (same flow, i.e. after cleaning _build deps folder and and clean recompile)

MIX_DEBUG=1 iex -S mix phx.server

The app starts as normal.
I start a browser request.

iex>
-> Running mix compile.elixir --purge-consolidation-path-if-stale /home/xxx/workspace/my_app/_build/dev/lib/my_app/consolidated --no-all-warnings (inside MyApp.MixProject)
<- Ran mix compile.elixir in 32ms
-> Running mix compile.app --purge-consolidation-path-if-stale /home/xxx/workspace/my_app/_build/dev/lib/my_app/consolidated --no-all-warnings (inside MyApp.MixProject)
<- Ran mix compile.app in 2ms
[info] GET /fr

# debug logs for the request

[info] Sent 200 in 41ms
-> Running mix compile.elixir --purge-consolidation-path-if-stale /home/xxx/workspace/my_app/_build/dev/lib/my_app/consolidated --no-all-warnings (inside MyApp.MixProject)
<- Ran mix compile.elixir in 32ms
-> Running mix compile.app --purge-consolidation-path-if-stale /home/xxx/workspace/my_app/_build/dev/lib/my_app/consolidated --no-all-warnings (inside MyApp.MixProject)
<- Ran mix compile.app in 2ms

I also get have a first mix compile.elixir --purge-consolidation-path-if-stale ... and a Running mix compile.app --purge-consolidation-path-if-stale but it doesn’t seem to do anything (I guess as it should since nothing changed…)

So it seems to work as expected. At least the request finishes successfully.

Strangely (?), after the request is returned, I see again those 2 calls in the logs… not sure it’s expected.

Note: I’ve tested the behaviour on a another app I have (elixir 1.18), and it seems normal to have the 2 mix compile.elixir --purge-consolidation-path-if-stale and mix compile.app --purge-consolidation-path-if-stale before the request and after…

But there is definitely something in my app that triggers the full recompilation in mix compile.elixir --purge-consolidation-path-if-stale

Are you able to reproduce this in a sample project?

Yes I managed to have a test app with the error: GitHub - novaccess-care/test_app

Basically I created a new app on the same elixir / erlang version (erlang 28.0.2, elixir 1.18.4), copied my app mix & mix.lock files, and my config.exs / dev.exs (removing a few things). I added a fake Oban job also to view the errors in the dev logs.

The tricky things is that it’s not consistent, the error may not appear sometimes… but it’s still pretty frequent, I would say one out of three times.

Basically I do this in loop to reproduce

MIX_DEBUG=1 iex -S mix phx.server

→ Wait for app to start
→ Go to http://localhost:4000/

If no error : Ctrl C + Ctrl C and do it again :

MIX_DEBUG=1 iex -S mix phx.server

→ Wait for app to start
→ Go to http://localhost:4000/

…etc

Youhou I found the culprit!

I removed one by one each of my dependency, and it turns out that it was related to Kino. It was probably not a good idea to include it in my dev environment, but at the time I thought it was convenient in order to have Kino in attached livebooks…

So removing this line from my mix.exs file solved the issue:

 {:kino, "~> 0.16", only: :dev},

I don’t understand why exactly it breaks my env like this though…

Ah, I think it’s related to this _mix_recompile__? definition within Kino. What is that assets_path pointing to in your environment?

1 Like