Fork me on GitHub
#duct
<
2019-11-05
>
weavejester14:11:02

@rickmoynihan The profiles are traversed in dependency order. This is deterministic, and unrelated to the order the profile keys are given.

rickmoynihan14:11:59

Hi 🙂 It’s deterministic, but when you add new profiles the ordering is highly non intuitive. Is there a reason they’re not in profile-key order?

weavejester14:11:34

Because modules (of which profiles are a type) have their own ordering.

weavejester14:11:04

So you can explicitly say “I want profile A to be applied before profile B”

rickmoynihan14:11:24

how do you state that?

weavejester14:11:54

Add a key like: :profile/a {:duct.core/requires #ig/ref :profile/b}

weavejester14:11:27

So a reference will make one profile dependent on another.

weavejester14:11:57

If you want a soft dependency - where you want A to depend on B, but only if B exists, then you can use a refset

weavejester14:11:09

:profile/a {:duct.core/requires #ig/refset :profile/b}

weavejester14:11:55

There’s nothing particularly special about :duct.core/requires, other than it’s cleaned out of the profile when it’s merged in.

weavejester14:11:25

I should probably make that key public, as right now it’s just an internal detail.

weavejester14:11:25

As an example, this ensures modules come after profiles:

(defmethod ig/prep-key :duct/module [_ profile]
  (assoc profile :duct.core/requires (ig/refset :duct/profile)))

rickmoynihan14:11:28

ahh ok makes some sense — I saw that code, but didn’t quite know what it was doing. I’ll give it a try. Though I still question why profiles are modules… They don’t typically depend on each other in the same way as modules… i.e. each module may define subsets (potentially overlapping but not necesssarily) of the keys in the final system. And they’re not actually modules in the hierarchy (isa? :duct/profile :duct/module) ;; => false. Wouldn’t it be better and more direct to merge profiles in profile key order?

weavejester14:11:35

So when designing Duct there were three options. 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.

weavejester14:11:06

The problem with placing modules and profiles at the same layer, if they’re not the same thing, is how to resolve references between the two.

rickmoynihan14:11:08

ok makes sense… I was expecting profiles were design 2.

rickmoynihan14:11:48

ok makes sense. I guess I don’t use modules very much, aside from the ring wrappers.

weavejester14:11:05

If another layer is added, then that makes things more verbose. And options 2 and 3 are also more complex, in that there are more things to consider.

weavejester14:11:28

However, I recognize that the first option is conceptually hard to get to grips with at first.

rickmoynihan14:11:39

Yeah, I think I just expected because profiles were a sequence for that to determine the meta merge order.

weavejester14:11:07

They should probably be documented as a set, to make things clear.

rickmoynihan14:11:10

it turns out we’ve been using duct for years with custom profiles, and getting the merge semantics we wanted by accident. Until now.

weavejester15:11:23

It isn’t the most intuitive thing, I agree. More documentation coupled with using a set instead of a vector would help.

weavejester15:11:59

It does allow for some pretty sophisticated dependency resolution though.

weavejester15:11:11

And I wanted to get away from explicit ordering in favour of using dependency order for keys.

rickmoynihan15:11:42

Ok, this makes a lot more sense to me now, as I hadn’t thought about modules being inside profiles. Thanks again for the answers; and super sorry if my questions sounded critical. We’re huge fans of the work you do, particularly now in integrant and duct.

weavejester15:11:06

Criticism is good 🙂

weavejester15:11:24

I’m still not entirely sure if I’ve made the right decision. The current design trades ease for simplicity, perhaps more than any other library I’ve designed. In terms of moving parts it has very little there, but conceptually it’s not as intuitive as I’d like.

weavejester15:11:20

While we generally talk about making things simple rather than easy, making something easy is still a good design goal to have. Sometimes things can be both simple and easy.

weavejester15:11:15

But Duct’s modules and profiles currently have a learning curve to them. I need to clear some time to improve the documentation.

rickmoynihan15:11:35

yeah we’ve spoken about this before… it’s a tricky balance. > The current design trades ease for simplicity, I thought it was more the other way around… in that duct/modules are trying to apply ease to the bare simplicity of integrant.

weavejester15:11:03

Maybe there’s a bit of that, too.

rickmoynihan15:11:02

I suspect this is the root cause of your uneasiness about the design of duct, and likely the source of my issues with its design (compared to integrant)… i.e. when something is simple it’s far easier to know it’s correct, that it is self consistent, and achieves it’s goals; you can objectively talk about it. When it’s trying to be easy, well you’ve moved into more subjective territory.

rickmoynihan15:11:22

Though I appreciate there is a simplicitly to duct… i.e. the meta-system is just an integrant system, profiles and modules are (almost) the same; etc… but I think perhaps the motivation for duct modules lies in trying to be easy rather than simple.

weavejester15:11:30

Yes - certainly the modules are something I’ve wondered about.

weavejester15:11:59

Like whether or not a set of defaults to be merged in would deal with most eventualities.

rickmoynihan15:11:22

Yeah those are tough decisions

rickmoynihan15:11:43

FWIW most of those defaults seem pretty sane; I don’t think we’ve tweaked that many

weavejester15:11:09

But they do allow for more involved defaults, and there’s a nice parallel we can draw between higher level functions, and higher level configurations, which is what modules are.

rickmoynihan15:11:05

yeah they’re the macros of duct edn… but with global scope

rickmoynihan15:11:36

One issue with the profiles relying on :duct.core/requires is that it’s optional; and that it seems easy to accidentally end up in implicit/accidental ordering territory simply by missing one. This is a little awkward if different environments the app is deployed in, have different profile chains, e.g. you have various levels of dev/prod override. You want to make sure you’ve specified a total ordering/precedence but there’s no guarantee you have, and you’re not accidentally just getting the right result.

weavejester15:11:59

You can always set the key via the prep-key step. Or enforce it though the pre-init-spec. Ideally you also shouldn’t be thinking in terms of total ordering, but what profiles make changes that are dependent on each other.

weavejester15:11:28

That said, that’s easier to do with modules than profiles.

rickmoynihan15:11:34

> Ideally you also shouldn’t be thinking in terms of total ordering, but what profiles make changes that are dependent on each other. I don’t know, I think ideally I want to say: - in dev merge in this order [:duct.profile/base :project.profile/customer :duct.profile/dev :project.profile/customer-dev :project.profile/customer-dev-local] - in test merge in this order [ ,,, ] - in prod mege in this order [:duct.profile/base :project.profile/customer :project.profile/customer-prod :project.profile/customer-prod-local] as a somewhat simplified example of what I actually want. I don’t really want to think about dependencies between profiles, because any key/config could go in any profile depending on circumstance. So the dependencies are only really there to force the ordering.

weavejester15:11:30

Hm. I can see the reasoning.

weavejester15:11:48

Though I’m not sure of the solution 🙂

rickmoynihan16:11:27

(defn apply-profiles [meta-sys profiles]
  (reduce (fn [system profile]
            (mm/meta-merge system (profile meta-sys))) {} profiles))

(defn build-system [uber-config project-key profiles]
  (let [common-modules (-> (duct/resource "modules.edn")
                                               (duct/read-config default-readers))]
               
    (-> (merge {:duct.profile/base (apply-profiles uber-config profiles)}
               common-modules)
       (project/assoc-project-injector project-key))))
Well this was my first attempt at a solution, before you mentioned the :duct.core/requires stuff.

rickmoynihan16:11:57

then exec that with duct

rickmoynihan16:11:04

which is admittedly a hack.

weavejester16:11:28

In order for profiles to be ordered, and the configuration still to make sense, it would need to be redesigned. Specifically, profiles would probably need to be separated from modules, which means either adding a new layer, or redesigning modules…

weavejester16:11:59

Or perhaps I could add something in where the profile ordering is used when the dependency order is ambiguous…

rickmoynihan16:11:18

oh that might work

rickmoynihan16:11:25

and be backwards compatible… maybe. :thinking_face:

weavejester16:11:58

I’m not sure if the latter is a smart backward compatible move, or a cheap workaround. Perhaps it depends on how good my marketing department is 😉

rickmoynihan16:11:13

On second thoughts it won’t be backwards compatbile, if someone is getting an arbitrary ordering right now that is accidentally the right one.

weavejester16:11:06

It might be a good idea to expose an accidental ordering.

weavejester16:11:39

But I’d still like to think about it a little before committing to anything.

rickmoynihan16:11:41

(Incidentally my hack above is essentially the extra layer of system you talk about.)

rickmoynihan16:11:08

for sure, it’d be a pretty fundamental and scary change if it wasn’t well thought out.

weavejester16:11:23

I think I’m solid on the Integrant part, and solid on the profiles. I like how they’re keys, and I like the simplicity of having #duct/include.

weavejester16:11:30

I’m less sold on the modules part. I like the concept, but I’m not sure how they work in practice.

rickmoynihan16:11:11

I’d have to say that matches my experience with duct/integrant. Though given that profiles are modules I’m not sure I’m sold on profiles… I’m sold on them with the semantics I expect 😉 (and thought we were getting).

weavejester16:11:34

Yes, that’s what I meant about the profiles. 🙂

weavejester16:11:55

I like having something that gives me good defaults. I recently started an application and being able to just dump in a :duct.module/sql felt very nice.

rickmoynihan16:11:16

Yeah profiles I think are essentially just override chains that control the order of meta-merge

rickmoynihan16:11:52

> I like having something that gives me good defaults. I definitely agree that it’s really nice to get good defaults… but I’ve often wondered if those defaults might not be better as just raw integrant config, in a jar… and then if you stray from the defaults, you either stomp the defaults via the meta-merge; or if it’s more fundamental just copy the config from the jar into your app and modify.

weavejester16:11:17

I admit I worry about over-using meta-merge.

rickmoynihan16:11:50

I’ve definitely overused it in the past; though I think meta-merge is great for config when combined with profiles. I also think I prefer meta-merge to using something more granular like aero; aero tends to snowball tagged readers, and become much harder to reason about than ig + meta-merge.

rickmoynihan16:11:48

when you have a hammer 🔨

rickmoynihan16:11:14

I think I’m less sold on the use of modules as data macros… though having said that we have a subset of elaborate/fiddly config which could really use something module like simplifying it for ease… so I totally get the desire for them.

rickmoynihan16:11:44

An alternative to modules might be tagged readers — no idea what a design based on those might look like though. I guess it would lexically scope the module’s field of influence somewhat.

weavejester16:11:37

The introduction of prep-key in Integrant has lessened the need a little, as they’re effectively local macros.

rickmoynihan16:11:05

Yeah prep-key is a nice addition; I’ve used it a little; though I am wary of it too as it can obscure dependencies… e.g. returning ig/refs in a prep-key can make things hard to debug; though it can also be super handy.

weavejester16:11:37

Yeah. I admit I’ve had a few times where I’ve decided to remove prep-key.

weavejester16:11:02

Like should I use it for adding the logger automatically, or do that explicitly. It’s a hard decision.

rickmoynihan16:11:56

agreed… though I also avoid the duct logging stuff; simply because the raw JVM logging stuff is simpler with fewer layers of configuration. Sure it’s not pure, and doesn’t work with clojure data; but I’d rather get logging out of my 3rd party deps consistently and use one logger/config everywhere.

weavejester16:11:50

I think Timbre can intercept logging… that said, I take your point. I prefer to be pure where I can be.

weavejester16:11:11

Also logging from 3rd party deps is usually plaintext.

weavejester16:11:22

And ideally I want to be logging data.

rickmoynihan16:11:36

Yeah don’t get me wrong, I’d love to do it ducts way; it’s more principled… but I found the practicalities were more important.

weavejester16:11:56

Understandable.

weavejester16:11:03

Thanks for the feedback, btw.

rickmoynihan16:11:25

Pleasure. Thanks for listening 🙂

rickmoynihan16:11:00

I think we have quite an interesting duct app; in that we’re multi-tenant (i.e. multiple client configurations) in one app; with corresponding dev/prod/test overrides etc, some localised to specific customers, others going across customers etc. So we’ve had to stray a little out of the duct mold/template a little to achieve it; but it’s holding together really well.

weavejester16:11:22

That’s good to hear.

rickmoynihan16:11:28

we have a few nice tweaks… such as (reset :customer-name) that let you switch customers live at the repl; customers have different classpaths configured through tools.deps that are all merged together in a :prj/all alias for the repls classpaths; so essentially in dev all assets are on the classpath (which lets the branding etc swap when you change customers). Then some tests iterate over all configured customers and check things like their systems all start, and have a home page that 200s. So being data driven enables quite a lot; we have some macros to start systems in tests and they put in scope both the prepped config and the system, so your test can in principle do things like check every configured route returns a 200 for every customers app.

rickmoynihan16:11:49

(different customers can have different subsets of routing configured)

rickmoynihan17:11:23

Another weirdness/dimension to profiles as they currently are is that they can exist in hierarchies… so does the line (exec-config config [:duct.profile/prod]) mean start all profiles which isa? :duct.profile/prod? If that’s the case and you were to derive profiles from that key; how would you guarantee a merge ordering across that refset?

rickmoynihan17:11:35

I’m guessing the answer is don’t do that 🙂

weavejester17:11:51

Yes, it will start all derived profiles. The ordering is always the same - dependency order followed by alphanumeric ordering of keys to tiebreak.

👍 4
ccann23:11:40

If I want configuration specified in the :duct.profile/local profile to merge over configuration specified in a :duct.profile/test profile, how do I tell Duct that? If local has configuration: {:foo {:a false}} and test: {:foo {:a true}} Then I want the resulting config to be: {:foo {:a false}} Which I think means that I need to have :local depend on :test, so I added: :duct.core/requires #ig/ref :duct.profile/test to my local.edn but the value is still true I am eventually calling:

(-> (duct/resource "config.edn")
    (duct/read-config)
    (duct/prep-config [:duct.profile/test :duct.profile/local]))