Stuart Sierra Components and config

So, at work we use Stuart Sierras Component system. It’s been serving us well, but IMO, it’s been overused, so we’re cleaning up a bit.

A couple of our services which you could consider stateful, are in fact not, since these days much communication with the outer world is done via http, so you don’t really need a connection, but you do need an address.

It is nice if such addresses are configurable, so naturally, we stick it in a config, which then gets injected into the components.

So you end up with things that don’t need to be components, but which are, because they need access to the config.

So the question then becomes. How do you deal with configurable stuff which is basically static/immutable for the runtime of the app?

Stick it in an atom and deref it?
alter-var-root or some such?
Pass it as a parameter?
Do as we do and have a bunch of non-stateful components?
Use some lib that basically does one of the above?

3 Likes

Let’s take your concrete example, you could just close over the config and build your own request function which closes over the URL.
As long as you use it in the edges of the system, it’s not too much of a hassle.

We have a single component for loading the config (from external EDN files mostly) and everything else gets what it needs from that component. We implement IFn so you can “call” the config component with one or more keys and it does get-in under the hood to navigate into the config map.

1 Like

A good question. Normally we want to avoid global shared state but config is immutable and thus it does not really make functions using it impure since they will always return the same result based on the explicit arguments anyway, that is until you restart the app and thus reload the config. And it makes the code much simpler than having to pass the config around, IMHO.

I like to use Aero (which supports .edn config with tagged literals for reading env variables, including files etc.) or, in simpler cases, Fulcro’s config, which only supports hardcoded values + :env/SOME_ENV_VAR for reading env variables into the .edn config.

1 Like

the system component pattern is exactly dataflow programming / FRP. The system dependencies form a DAG and if something upstream changes (like a config file) the things that depend on it (downstream in the DAG) should get the update event. So I would code this with missionary.

Pseudocode:

; component
(defn example-system [config-options]
  (let [{:keys [host port]} config-options]
    (component/system-map
      :db (new-database host port)
      :scheduler (new-scheduler)
      :app (component/using
             (example-component config-options)
             {:database  :db
              :scheduler :scheduler}))))

; dataflow pseudocode (not missionary, this is hyperfiddle.photon, 
; our unreleased reactive Clojure dialect that macroexpands to missionary signals)
(r/defn system [{:keys [host port] :as config}]
  (let [database (Database host port)
        scheduler (Scheduler config)]
    (Example-component database scheduler)))

Well, in this case the config does not change at runtime, so it seems to be an overkill?

you’re right, sorry. if the config is fixed at app startup you don’t need an atom or any ref type at all, just fns right? (def config (load-file …))

We developed and use Schematic (GitHub - walmartlabs/schematic: Combine configuration with building a Component system) to organize components, dependencies, and configuration as a single EDN file.

I use a delay or sometimes a promise.

(def config
  (delay
    (get-config
      (System/getProperty "stage")
      (System/getProperty "geography"))))

or

(def config
  (promise))

(defn init-config
  [config-prom stage geography]
  (deliver config-prom (get-config stage geography)))

(defn -main
  []
  (init-config
    config
    (System/getProperty "stage")
    (System/getProperty "geography"))

   ...)

But I won’t simply have everything directly depend on the config, afterwards I’ll only use it from top-level functions:

(defn some-api
  [input]
  (let [something
        (foo (select-keys @config [:endpoint :user])]
  ;; Or I might pass in the config map, but not the delay:
    (bar @config)))

So I don’t couple anything that’s not at the top with the global config or the delay it’s wrapped in. That way I can still unit test everything. And for the top level functions I tend to integ test them mostly.

3 Likes

The issue with this is that you’ve now made loading side-effectful, which unless it’s by design, I would consider a bad practice.

If you don’t have a good reason for things to run at load-time, I wouldn’t have them run at load-time. That’s where you can simply wrap things in a delay to delay their evaluation to run-time, or use a promise for a similar effect.

You can also use functions if you prefer, but then you probably want to add some memoization to it so you don’t always reload the config:

(def config
  (memoize
    (fn []
      (load-config ...))))

This can actually be useful sometimes when you do want to be able to refresh your config as the app is running, I’ve used this approach with core.memoize sometimes:

(def config
  (->
    (fn [] (load-config ...))
    (clojure.core.memoize/ttl :ttl/threshold 600000)))

So this will refresh every 10 minutes, and you need to get the config with (config) instead of @config.

2 Likes

I want to second this. Aero plays really well with systems defined as maps. With component, as components are open to “foreign” keys, you can just stick the config in them, and on start pull the keys out of this

“Configuration doesn’t change during runtime” – famous last words! As soon as you rip out the configuration component, you will receive a telegram informing you that you must dynamically switch http service addresses upon receipt of an event. Also, isn’t testing a variety of circumstances easier if configuration isn’t global? In short, a system with a complete but tedious system map is better than a system that is not completely captured by the system map.

2 Likes

When we first wrote our original configuration component, the requirements were “config is fixed at startup” but we wrote a nice, flexible system that read EDN files and supported a form of “inheritance” so common config could be reused across processes. Over-engineering perhaps.

After we’d been live for a while, the requirement came in to be able to trigger a reload of the config while the app was running. Part of me is like “Well, of course there was!” as @Phill says… and because we had it in a component, it was fairly easy to implement a Reloadable lifecycle on top of the start/stop lifecycle and make it possible to change the config while the app was running (we’d been smart enough to use a derefable in the component!).

And fast forward a while later and that was no longer a requirement(!) and we eventually deleted the Reloadable stuff and no client code changed :slight_smile:

In my opinion, this is why implementing IFn on your component (to get at actual values in it) is a good idea, as it introduces a level of indirection that makes the actual structure of your component opaque (and of course with protocol extension via metadata, your component can actually be a function).

4 Likes

I believe that telegram got stuck in somewhere around 2013. At that time, with deploys being a nightmare every three months, and our runtimes running for days on end, being able to dynamically switch config was a must/given.

Even if we don’t deploy that often, only a couple of times pr day, so the need for a dynamic config is not there and, I’m willing to bet, will not arise.

As an interesting artefact we have this piece of code:

(defn foo-service
  [config]
  (map->FooService {:key ((:key config))}))

Notice how the config-value for :key is being evaluated :slight_smile:

1 Like