Skip to content

:duct/profile merge order is unintuitive #31

@RickMoynihan

Description

@RickMoynihan

The merge order of :duct/profile's does not work as expected.

Firstly one might reasonably expect that the merge order of profiles is determined by the order of profiles listed in the profiles argument to functions such as duct.core/exec-config, e.g. a user might expect the call:

(duct.core/exec-config config [:my.profile/b-one :my.profile/a-two])

to have the profile :my-profile/a-two meta-merge over the profile :my-profile/b-two. However the above profile will actually always be merged in the order of [:my-profile/a-two :my-profile/b-one] i.e. alphabetically on key name, due to the fallback comparator inside integrants key-comparator.

As duct profiles are almost modules one might try to force an ordering across them by establishing a dependency chain e.g:

  (require '[duct.core :as duct])

  (derive :example.profile/b-one :duct/profile)
  (derive :example.profile/a-two :duct/profile)

  (derive :my.app/requires :duct/const)

  (duct/prep-config {:example.profile/b-one {:a {:replace-me :init}
                                             :b [:b-one]}

                     :example.profile/a-two {:a {:replace-me :replaced}
                                             :b [:b-two]
                                             :c [:c-two]
                                             :my.app/requires (ig/ref :example.profile/b-one)}}
                    #{:example.profile/b-one :example.profile/a-two})

However, this doesn't work because any key deriving from :duct/profile has any #ig/refs they contain deactivated and converted into InertRefs here. Which means the subsequent call to fold-modules won't apply the profiles in the topological dependency order.

I should state that even if it worked I find the later solution significantly more confusing to reason about than an explicit ordering based on the order of the given profile keys. In particular in the case where you have multiple profile chains (with some shared and potentially optional profiles at various points in the chain e.g.

  • in dev merge in this order [:duct.profile/base :project.profile/customer :duct.profile/dev :project.profile/customer-dev :project.profile/local]
  • in test merge in this order [:duct.profile/base :project.profile/customer :duct.profile/test]
  • in prod merge in this order [:duct.profile/base :project.profile/customer :project.profile/customer-prod :project.profile/local]

This is really easy to reason about if each of the profile chains are specified like so; however when they're declared through dependencies they become a graph, and the intent is somewhat lost. Also I tend to think of profiles as maps that get merged with a well defined precedence to form a complete system; and don't think profiles depend on each other; they're just merged into a complete artifact.

On slack @weavejester mentioned there were 3 designs for modules

The current design, where a profile is a type of module. A design where modules and profiles are separate things, but at the same layer, or a design where modules are inside profiles, and normal configuration is inside modules.

I think the implementation is currently actually different from the intended design here, and is much more like the second suggested design. i.e. profiles and modules exist as separate things but at the same layer, e.g. (isa? :duct/profile :duct/module) ;; => false.

I'd certainly vote for a move towards profiles containing modules and normal configuration; with them being merged together prior to the system initiation in a well defined explicit and provided order. The risk with this move is that it might break peoples applications if they have defined custom profiles; however those users will currently be getting profiles merged in an alphabetical order. So perhaps we could effectively deprecate the current duct.core/exec-config but leave that merging profiles in alphabetical order, and create a new function duct.core/execute-config that applies profiles in the specified order?

An additional complication is what the semantics of keyword inheritance for profiles should be? e.g. we could possibly specify things like this:

(derive :duct.profile/prod-db :duct.profile/prod)
(derive :duct.profile/prod-routes :duct.profile/prod)

However when merging the profiles/refset for :duct.profile/prod the above the order will have to fallback to alphabetical between those profiles. Personally I think this should be strongly discouraged; as accidental orderings can easily arise, and that profiles should only use explicit keys.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions