Skip to content

fix(deps): break :application_controller cycle with vfs at boot#5

Merged
ivarvong merged 1 commit into
mainfrom
fix/vfs-runtime-false-no-app-cycle
May 5, 2026
Merged

fix(deps): break :application_controller cycle with vfs at boot#5
ivarvong merged 1 commit into
mainfrom
fix/vfs-runtime-false-no-app-cycle

Conversation

@ivarvong
Copy link
Copy Markdown
Owner

@ivarvong ivarvong commented May 5, 2026

Summary

  • Add runtime: false to the :vfs dep so Mix does not auto-inject it into exgit's generated applications list.
  • Add plt_add_apps: [:vfs] to the dialyzer config to keep vfs beams in the PLT after runtime: false removes them from the default closure.
  • Add Exgit.BootWithVfsTest — a regression smoke test that boots :exgit and :vfs together in the same VM under a 30s per-test timeout.

The bug

Bundling both :exgit and :vfs into the same release (e.g. a downstream agent app) deadlocked :application_controller at boot.

Mechanism:

  1. :exgit had {:vfs, ..., optional: true} — Mix still adds optional deps as application edges.
  2. At boot, :application_controller walks applications for every loaded app. When it sees :vfs listed by :exgit, it tries to start :vfs.
  3. :vfs (in dev/test) depends on :exgit for its own integration tests against exgit as a vfs storage backend.
  4. Cycle in :application_controller → deadlock.

optional_applications only tells the controller "it's OK if this app is absent" — it does not break the dependency edge when the app is present. Once both libs are in the same build, you wedge.

The fix

runtime: false on the :vfs dep is the load-bearing flag. It tells Mix "compile against this, but don't list it in applications." That is exactly the relationship we want: exgit uses vfs's protocol module at compile time (for defimpl VFS.Mountable, for: Exgit.Workspace) but does not depend on a vfs supervision tree at runtime. Same shape as Phoenix's optional templating adapters.

Architecture invariant preserved (per CLAUDE.md):

Backend libraries take :vfs as an optional dep — never the reverse. Dependency direction is backend → vfs.

  • exgit standalone: works without vfs ever entering the picture.
  • exgit + vfs together: defimpl compiles in, protocol consolidation picks it up, Exgit.Workspace is mountable.
  • vfs → exgit: only in vfs's own dev/test env, for testing exgit as a vfs storage backend. Production consumers of vfs that want git-backed mounts add exgit themselves.

Verification

Locally on darwin, Elixir 1.19, OTP 28:

Check Result
mix format --check-formatted clean
mix compile --warnings-as-errors clean
MIX_ENV=dev mix credo --strict 0 issues / 179 files
MIX_ENV=dev mix dialyzer 0 errors
mix test --warnings-as-errors 699 tests, 0 failures (incl. new smoke test)
mix test test/exgit/workspace_vfs_test.exs --include vfs 34 tests + 3 properties (full vfs conformance against Exgit.Workspace)
Reproducer: mix run -e 'Application.ensure_all_started(:exgit); Application.ensure_all_started(:vfs); IO.puts("ok")' prints ok (previously deadlocked)

Inspected _build/dev/lib/exgit/ebin/exgit.app:

  • applications no longer contains vfs
  • optional_applications is []
  • defimpl modules (Elixir.VFS.Mountable.Exgit.Workspace, Elixir.Exgit.Workspace.VFS) remain in modules — exactly the intended compile-time-only integration shape.

Mark :vfs as runtime: false so Mix does not auto-inject it into
exgit's generated `applications` list. vfs depends on exgit in its
own dev/test env, and when both libs are bundled in a downstream
consumer the application edge from exgit -> vfs deadlocks
:application_controller at boot.

The integration is compile-time only — `defimpl VFS.Mountable,
for: Exgit.Workspace` references vfs's protocol module during
compilation; there is no vfs supervision tree to start. So
runtime: false is not a workaround, it is an accurate description
of the relationship, matching Phoenix's pattern with its optional
templating adapters.

Side effect: Dialyxir's default PLT closure follows `applications`,
so vfs beams need an explicit `plt_add_apps: [:vfs]` to keep
Workspace.VFS clean of spurious unknown_function warnings.

Adds Exgit.BootWithVfsTest as a regression smoke test: boots both
apps in the same VM with a 30s per-test timeout. If a future change
reintroduces the cycle the test deadlocks and ExUnit surfaces it
as a clear failure rather than a stuck CI run.
@ivarvong ivarvong merged commit 4a79314 into main May 5, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant