Skip to content

fix: avoid unnecessary compile dependencies for runtime DSL modules - #2885

Merged
zachdaniel merged 2 commits into
ash-project:mainfrom
jechol:fix/runtime-dsl-module-dependencies-260820
Aug 25, 2026
Merged

fix: avoid unnecessary compile dependencies for runtime DSL modules#2885
zachdaniel merged 2 commits into
ash-project:mainfrom
jechol:fix/runtime-dsl-module-dependencies-260820

Conversation

@jechol

@jechol jechol commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Contributor checklist

Leave anything that you believe does not apply unchecked.

  • I accept the AI Policy, or AI was not used in the creation of this PR.
  • Bug fixes include regression tests
  • Chores
  • Documentation changes
  • Features include unit/acceptance tests
  • Refactoring
  • Update dependencies

Summary

Several Ash DSL fields contain module references that are stored and used at runtime, but were not included in no_depend_modules.

As a result, normal module aliases in those fields could create compile dependencies from the resource to runtime callback modules.

This adds the missing no_depend_modules declarations for:

  • generic action error_handler and touches_resources
  • create, update, and destroy error_handler and notifiers
  • read action modify_query
  • multitenancy parse_attribute and tenant_from_attribute callbacks
  • custom aggregate implementation
  • unrelated exists aggregate resources
  • PubSub publication dispatcher
  • Reactor default_domain

These fields either store module identities or invoke their callbacks at runtime. Their implementations are not used to produce compile-time resource state, so changes to those modules should not require recompiling the resource.

default_domain is explicitly ensured compiled before its existing Ash.Domain validation so clean parallel compilation does not depend on file ordering.

Fields that are consumed at compile time, including type constraints and aggregate target fields used to derive metadata, intentionally retain their compile dependencies.

Validation

  • Audited each affected field from its DSL declaration through its transformer, verifier, generated state, and runtime consumer
  • Verified the full Ash test suite

This pull request was developed with AI assistance and reviewed by the submitter.

@zachdaniel

Copy link
Copy Markdown
Contributor

I believe we will need to use Code.ensure_compiled on the run argument if it is a reactor in: VerifyGenericActionReactorInputs. And in action_transformer.ex for reactors we will need to do the same on the default_domain 🤔

@jechol
jechol force-pushed the fix/runtime-dsl-module-dependencies-260820 branch 2 times, most recently from db2f946 to 97a43dc Compare August 23, 2026 08:25
@jechol

jechol commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for pointing this out. I made two different changes based on how each module is consumed.

I removed run from no_depend_modules. The generic action verifier consumes module.reactor().inputs at compile time, so compilation ordering alone is not enough. If the Reactor inputs change, the resource needs to be recompiled so the argument validation runs again.

I kept default_domain in no_depend_modules and added Code.ensure_compiled/1 before validating it as an Ash.Domain. The transformer stores the domain module itself and does not derive generated state from the domain's resources or configuration. This preserves the initial compilation ordering without propagating compile dependencies through the domain's resources.

A possible follow-up would be to split generic action run and reactor into mutually exclusive DSL options. That would allow ordinary implementation modules to remain dependency-free while Reactor modules retain the compile dependency required for input validation. I kept that design change out of this PR.

@jechol
jechol force-pushed the fix/runtime-dsl-module-dependencies-260820 branch from 97a43dc to 013519c Compare August 23, 2026 08:42
@zachdaniel

Copy link
Copy Markdown
Contributor

For this:

I removed run from no_depend_modules. The generic action verifier consumes module.reactor().inputs at compile time, so compilation ordering alone is not enough. If the Reactor inputs change, the resource needs to be recompiled so the argument validation runs again.

I think that this can be handled by calling Code.ensure_compiled at compile time for a module. Like that it would conditionally incur a compile time dependency. I forget if that works in practice but it would be good to confirm because it would allow us not to depend on the run module unless that run module is a reactor?

@jechol

jechol commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

I tested this in a minimal fixture. Code.ensure_compiled/1 provides compile ordering, but it does not create a compile dependency.

Macro.compile_apply/4, available since Elixir 1.16, does create the conditional dependency you described. We can first ensure that the run module is compiled, determine whether it is a Reactor, and call Macro.compile_apply/4 with the caller's Macro.Env only in that case. Ordinary implementations would have no compile dependency, while Reactor modules would.

I created a minimal reproduction comparing both approaches and verified it with Elixir 1.16.3 / OTP 26 and Elixir 1.20.0 / OTP 29:

https://github.com/jechol/elixir-conditional-compile-dependency

The most direct solution seems to be raising Ash's minimum Elixir version to 1.16, adding run back to no_depend_modules, and using Macro.compile_apply/4 only when run is a Reactor. Requiring Elixir 1.16 or later does not seem unreasonable for users of an actively developed framework like Ash.

@zachdaniel

Copy link
Copy Markdown
Contributor

Okay, so, in retrospect, this has a critical issue (the reactor conditional compile time dependency). Specifically, that in order to know if a module is a reactor, you have to compile it. So this current iteration would actually break in the case that someone changes a module from an Implementation to a Reactor. So that means we do just have to depend on the run module unfortunately. So lets just take the rest of the benefits you're adding here and go from there.

@jechol

jechol commented Aug 25, 2026

Copy link
Copy Markdown
Contributor Author

Agreed. The current branch already keeps the compile dependency on every run module: run was removed from no_depend_modules in the follow-up commit. I did not implement the conditional Macro.compile_apply/4 approach discussed above, so changing an Implementation into a Reactor will still invalidate and recompile the resource.

The remaining changes only remove dependencies for the other runtime-only module fields and preserve compile ordering for default_domain. No further code changes should be needed for this issue.

@zachdaniel
zachdaniel merged commit e4d0305 into ash-project:main Aug 25, 2026
51 checks passed
@zachdaniel

Copy link
Copy Markdown
Contributor

🚀 Thank you for your contribution! 🚀

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.

2 participants