duct

2019-11-05T14:43:02.063Z

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

2019-11-05T14:43:59.064100Z

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?

2019-11-05T14:44:34.065Z

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

2019-11-05T14:45:04.066Z

So you can explicitly say ā€œI want profile A to be applied before profile Bā€

2019-11-05T14:45:24.066300Z

how do you state that?

2019-11-05T14:46:54.068600Z

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

2019-11-05T14:47:27.069500Z

So a reference will make one profile dependent on another.

2019-11-05T14:47:57.070400Z

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

2019-11-05T14:48:09.070600Z

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

2019-11-05T14:48:55.071400Z

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

2019-11-05T14:49:25.072100Z

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

2019-11-05T14:50:25.073700Z

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)))

2019-11-05T14:53:28.076Z

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?

2019-11-05T14:55:35.077900Z

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.

2019-11-05T14:56:06.078700Z

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.

2019-11-05T14:56:08.078900Z

ok makes senseā€¦ I was expecting profiles were design 2.

2019-11-05T14:56:48.079900Z

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

2019-11-05T14:57:05.080300Z

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.

2019-11-05T14:57:28.080900Z

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

2019-11-05T14:58:39.082400Z

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

2019-11-05T14:59:07.083300Z

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

2019-11-05T14:59:10.083400Z

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

2019-11-05T15:00:23.084100Z

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

2019-11-05T15:00:59.084900Z

It does allow for some pretty sophisticated dependency resolution though.

2019-11-05T15:02:11.085900Z

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

2019-11-05T15:09:42.089900Z

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.

2019-11-05T15:10:02.090200Z

:man-bowing:

2019-11-05T15:10:06.090400Z

Criticism is good šŸ™‚

2019-11-05T15:12:24.092900Z

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.

2019-11-05T15:13:20.094500Z

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.

2019-11-05T15:14:15.096100Z

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

2019-11-05T15:15:35.097200Z

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.

2019-11-05T15:16:03.097800Z

Maybe thereā€™s a bit of that, too.

2019-11-05T15:20:02.101Z

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.

2019-11-05T15:22:22.102500Z

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.

2019-11-05T15:24:30.103Z

Yes - certainly the modules are something Iā€™ve wondered about.

2019-11-05T15:24:59.103700Z

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

2019-11-05T15:25:22.104500Z

Yeah those are tough decisions

2019-11-05T15:25:43.105300Z

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

2019-11-05T15:26:09.105900Z

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.

2019-11-05T15:28:05.106400Z

yeah theyā€™re the macros of duct ednā€¦ but with global scope

2019-11-05T15:46:36.110900Z

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.

2019-11-05T15:48:59.112800Z

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.

2019-11-05T15:49:28.113200Z

That said, thatā€™s easier to do with modules than profiles.

2019-11-05T15:57:34.117800Z

> 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.

2019-11-05T15:59:30.118500Z

Hm. I can see the reasoning.

2019-11-05T15:59:48.119200Z

Though Iā€™m not sure of the solution šŸ™‚

2019-11-05T16:02:27.120500Z

(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.

2019-11-05T16:02:57.121100Z

then exec that with duct

2019-11-05T16:03:04.121500Z

which is admittedly a hack.

2019-11-05T16:04:28.122600Z

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ā€¦

2019-11-05T16:04:59.123400Z

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

2019-11-05T16:05:18.123700Z

oh that might work

2019-11-05T16:05:25.124100Z

and be backwards compatibleā€¦ maybe. :thinking_face:

2019-11-05T16:05:58.125Z

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 šŸ˜‰

2019-11-05T16:08:13.126400Z

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

2019-11-05T16:10:06.127800Z

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

2019-11-05T16:10:39.128600Z

But Iā€™d still like to think about it a little before committing to anything.

2019-11-05T16:10:41.128800Z

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

2019-11-05T16:11:08.129300Z

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

2019-11-05T16:13:23.130400Z

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.

2019-11-05T16:14:30.131Z

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

2019-11-05T16:15:11.131600Z

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).

2019-11-05T16:16:34.132500Z

Yes, thatā€™s what I meant about the profiles. šŸ™‚

2019-11-05T16:16:55.133300Z

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.

2019-11-05T16:17:16.133700Z

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

2019-11-05T16:27:52.136800Z

> 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.

2019-11-05T16:29:17.138900Z

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

2019-11-05T16:29:48.139200Z

when you have a hammer šŸ”Ø

2019-11-05T16:30:05.139900Z

Right šŸ™‚

2019-11-05T16:31:14.141Z

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.

2019-11-05T16:32:44.142200Z

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.

2019-11-05T16:34:37.143100Z

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

2019-11-05T16:34:50.143200Z

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.

2019-11-05T16:37:05.145300Z

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.

2019-11-05T16:39:37.146200Z

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

2019-11-05T16:40:02.146900Z

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

2019-11-05T16:44:56.150400Z

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.

2019-11-05T16:45:50.151200Z

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

2019-11-05T16:46:11.152Z

Also logging from 3rd party deps is usually plaintext.

2019-11-05T16:46:22.152500Z

And ideally I want to be logging data.

2019-11-05T16:47:36.153300Z

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.

2019-11-05T16:47:56.153500Z

Understandable.

2019-11-05T16:48:03.153800Z

Thanks for the feedback, btw.

2019-11-05T16:48:25.154200Z

Pleasure. Thanks for listening šŸ™‚

2019-11-05T16:52:00.156900Z

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.

2019-11-05T16:52:22.157300Z

Thatā€™s good to hear.

2019-11-05T16:57:28.161800Z

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.

2019-11-05T16:58:49.162300Z

(different customers can have different subsets of routing configured)

2019-11-05T17:50:23.164500Z

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?

2019-11-05T17:50:35.164800Z

Iā€™m guessing the answer is donā€™t do that šŸ™‚

2019-11-05T17:51:51.165800Z

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

šŸ‘ 1
ccann 2019-11-05T23:18:40.169900Z

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]))