component

stephenmhopper 2021-06-15T12:50:26.004600Z

Hi, everyone! I’m moving an application over to use component. For the most part, my components are immutable and don’t change at all once the system starts. However, I do have a couple of places where this is not the case. My application makes calls to an external API which requires a new token every 60 minutes. In the current version of my application, I just always reference the token from an atom and I have a background job set up with chime that just refreshes the token every 55 minutes. When I move this to component, I plan to use an atom as property on a component, but I’m not entirely sure if that’s the best approach. Also, should I still stick with the chime loops inside of my component for keeping that token up-to-date or is there a better alternative. How would you handle this situation?

seancorfield 2021-06-15T16:06:51.004700Z

That’s probably what I would do — “encapsulate” all the updating machinery inside the component and set it up in the start function (and tear it down in the stop function if possible). I would probably have the component implement IFn for no args and return the contents of the atom rather than make user code reach into the component for a field and then deref it — that’s a pattern we’ve adopted for a lot of components that “represent” some “value”, so that client code does not need to know how to access the value within the component.

stephenmhopper 2021-06-15T16:13:07.004900Z

Can you explain the piece about having the component implement IFn in more detail? That sounds interesting. The main downside to passing the atom around everywhere is that any component could theoretically perform swap! or reset! on it, so I’d like to avoid that if possible while still allowing folks to effectively deref it

seancorfield 2021-06-15T16:20:42.005100Z

next.jdbc’s Component implementation for connection-pooled datasources is a function that returns the datasource https://github.com/seancorfield/next-jdbc/blob/develop/src/next/jdbc/connection.clj#L322 but it uses metadata to implement start/`stop` so it’s a little harder to see what I’m talking about.

seancorfield 2021-06-15T16:22:28.005400Z

This is an example from work of a “configuration” component, where we implement multiple arities so that (cfg :a :b :c) can act like (get-in cfg [:configuration :a :b :c]):

(defrecord Configuration [application-name edn-path configuration]
  component/Lifecycle
  (start [this]
    (if configuration
      this
      (assoc this
             :configuration
             (load-configuration application-name edn-path))))

  (stop [this]
    (assoc this :configuration nil))

  clojure.lang.IFn
  (invoke [_] configuration)
  (invoke [_ k] (get configuration k))
  (invoke [_ k1 k2] (get-in configuration [k1 k2]))
  (invoke [_ k1 k2 k3] (get-in configuration [k1 k2 k3]))
  (invoke [_ k1 k2 k3 k4] (get-in configuration [k1 k2 k3 k4]))
  ;; shouldn't need any other arities
  (applyTo [_ ks] (get-in configuration ks))
...

stephenmhopper 2021-06-15T16:36:14.005700Z

Got it. That’s helpful. I think implementing IFn on a component that’s just wrapping an atom will fit my use case. I have a couple of follow-up questions: 1. Is there a reason for (assoc this :configuration nil) instead of using dissoc? 2. Is configuration just a map? In start , could you just return configuration instead of associng it onto this? I’ve only been using component for a couple of days now, so I’m not aware of all of the pros and cons of this approach, but it seems like all downstream components would have configuration injected as a map and would be able to use it, but you lose the ability for any kind of cleanup to happen (since component will be holding a reference to a map instead of an actual component). Is that accurate?

seancorfield 2021-06-15T16:37:20.005900Z

If you dissoc a declared field of a record, it becomes a plain hash map and is no longer a record.

👍 1
seancorfield 2021-06-15T16:38:06.006100Z

configuration is just a hash map, but start should return a component which can be stop’d.

seancorfield 2021-06-15T16:41:15.006400Z

Component start/`stop` should be idempotent: when you start a component, you should be able to call start on it again and it should be a no-op. In addition, when you call stop on a component, what you get back should be startable — so to call start on a stopped configuration here, we’d need to keep track of application-name and edn-path so that load-configuration could be called again.

stephenmhopper 2021-06-15T17:03:12.006600Z

Got it. So when I’m designing components, should the fields in my defrecord declaration be all of my component’s dependencies as well as all of the fields my component initializes in start?

seancorfield 2021-06-15T17:05:47.006800Z

Our approach has always been to have three types of field, all declared: 1. configuration for the component (either passed in via map->Component or defaulted inside the start function) 2. dependencies (wired up via using externally) 3. state (computed and maintained in the start/`stop` functions)

seancorfield 2021-06-15T17:05:55.007Z

Not every component has all three.

seancorfield 2021-06-15T17:07:05.007200Z

However, since Component added support for metadata-based protocol implementations, we are moving more to hash maps and/or functions — like next.jdbc for example — and then it’s not important to “declare” anything or worry about keeping fields around just to stay as a “special” data type.

stephenmhopper 2021-06-15T17:09:24.007400Z

Thanks for the clarification on the fields question. What’s the “metadata-based protocol implementation” stuff all about? I don’t recall seeing that in the component docs

seancorfield 2021-06-15T17:09:31.007600Z

For example, here’s our “host services” component that figures out the hostname and artifact version at startup:

(defn system
  "Build a 'system' component that contains the cached hostname
  and this process's full version string."
  []
  (with-meta {}
    {`component/start
     #(assoc %
             :hostname (or (:hostname %)
                           @system-starting-message
                           (lookup-hostname))
             :version  @process-version)
     `component/stop
     #(assoc % :hostname nil :version nil)}))
We do this because it turns out that repeatedly calling
(if-let [address (java.net.InetAddress/getLocalHost)]
      (or (.getHostName address) "")
      ""))
a) has a performance overhead and b) can sometimes return nil

seancorfield 2021-06-15T17:11:11.007800Z

“metadata-based protocol implementation” — it’s a feature added to Clojure in 1.10: https://github.com/clojure/clojure/blob/master/changes.md#22-protocol-extension-by-metadata

seancorfield 2021-06-15T17:12:15.008200Z

Component was updated to add :extend-via-metadata true to its defprotocol form — that’s all it took.

seancorfield 2021-06-15T17:13:24.008400Z

Most protocols I define these days have that flag, unless they can only be extended to things that cannot carry metadata.

stephenmhopper 2021-06-15T17:16:37.008600Z

Got it. That’s cool. I remember seeing that in the patch notes, but I rarely deal with defprotocol so I didn’t pay much attention to it. I see two main advantages to this approach: 1. I can call dissoc on my map and not have to worry about the whole “it’s no longer a component” thing, right? 2. I can use a “let-over-lambda” approach to wrap values in my component and hide them from being accessed by folks who can access the component. Is that accurate? Are there other advantages?

stephenmhopper 2021-06-15T17:27:55.008800Z

Also, do you have an example of a component defined via the “metadata-based protocol implementation” approach that has dependencies which are injected by component?

stephenmhopper 2021-06-15T17:35:10.009Z

I’m assuming that component still just places those values on the map that’s passed to start and stop?

seancorfield 2021-06-15T17:35:34.009200Z

I don’t have any public examples of metadata-based components with dependencies.

seancorfield 2021-06-15T17:36:53.009400Z

One thing to remember is that Component requires associative data structures for injecting dependencies (and you have to rely on them being metadata-preserving — which assoc is), but you can implement protocols via metadata using things that are not associative, such as functions.

seancorfield 2021-06-15T17:38:51.009600Z

But, yeah, if you’re dealing with a hash map with metadata, you’re safe to dissoc any fields and you still have a “Component” in the sense that it still carries metadata that implements the protocol:

dev=> (meta (dissoc (with-meta {:a 1} {:b 2}) :a))
{:b 2}
(although, again, be careful to avoid metadata-destroying functions 🙂 )

seancorfield 2021-06-15T17:39:43.009800Z

And, yes, you can use captured bindings to encapsulate things if you want.

seancorfield 2021-06-15T17:41:45.010Z

I talked with Stuart about extending dependencies into metadata so that Component would support dependency injection on non-associative objects but he felt that didn’t bring enough benefit to be worth the additional complexity (because associative things still need dependencies assoc'd in as well as having the dependency metadata updated).

seancorfield 2021-06-15T17:43:29.010200Z

It’s an experiment I may still move forward with myself at some point, where both lifecycle functions and dependencies are handled entirely via metadata and for associative things, dependencies would still also be injected. I believe I could make a library that was compatible with Component but I just don’t know if it’s really worth the effort 🙂

stephenmhopper 2021-06-15T17:45:54.010400Z

Okay, that all makes sense. And yeah, supporting DI on non-associative objects does sound tricky. I saw the example for how next.jdbc.connection/component is effectively turning a DataSource into a component with non-trivial start / stop methods. I’ll be doing something similar with core.async chans in my app. Thanks for all of your assistance on this!

stephenmhopper 2021-06-15T18:14:23.010600Z

Oh, nevermind on that. core async chan’s don’t appear to implement IObj, so I don’t think I can attach metadata to them I’d still have to pass them around as functions

2021-06-15T23:17:48.010900Z

there are protocols that define channels (ReadPort, WritePort, Channel) so you can create a defrecord that is both a channel and a component if you like