Engine

So far, we’ve only used claro.engine/run!! to resolve our values. But claro’s engine offers a lot more.

First off, both run!! and run! use the default engine but you can build a custom one using claro.engine/engine. Engines implement IFn, so they can be called like functions.

(defonce run-engine
  (engine/engine ...))

@(run-engine (->Person 1))

The next sections describe the moving/customizable parts.

Deferred Values

Instead of blocking during resolution, run! produces a deferred value:

(engine/run! (->Person 1))
;; => << … >>

The default implementation uses Manifold, so you can set execution timeouts or register error handlers:

(-> (->Person 1)
    (engine/run!)
    (d/catch
      (fn [_]
        ::error))
    (d/timeout! 1000 ::timeout))

To use a different implementation (e.g. the one provided in claro.runtime.impl.core-async), use the two-parameter engine constructor:

(defonce run-engine
  (engine/engine
    claro.runtime.impl.core-async/impl
    {}))

This will work for any Resolvable returning a core.async channel.

Note: The implementation used by an engine can be accessed using the impl function.

Environment

Meaningful data access without configuration pointing at a datasource is rare, so it is necessary for Resolvable values to be aware of said configuration. There are multiple possibilities:

  • store it in global vars,
  • store it in dynamic vars and use binding around the resolution call,
  • store it in the Resolvable record.

These are viable options for claro, too, but the preferred way would be to bind an engine to your environment, using the :env key:

(defonce run-engine
  (engine/engine
    {:env {:db {:subprotocol "postgresql", ...}}}))

The environment will be passed as the second parameter to both resolve! and resolve-batch! and can contain things like datastore connections, a user to scope resolution too, etc …

Parts of the environment can be added or replaced when calling the engine:

(run-engine
  (->Person 1)
  {:env {:db {:subprotocol "mysql", ...}}})

Selector

During each iteration, the resolution engine selects a set of resolvables to process. By default, it attempts to resolve all availble values, but this behaviour can be adjusted by supplying a different Selector when creating the engine:

(defonce run-engine
  (engine/engine
    {:selector (claro.data.selector/parallel-selector 2)}))

Just like :env this can be overridden on a per-call basis, so you are able to use specialized selection for cases where you need it.

See the claro.engine.selector namespace for more variants.

Middlewares

The resolver function can be wrapped with custom middlewares using claro.engine/wrap. It takes two parameters: the environment and the batch of resolvables – all of the same class – and produces a deferred value.

For example, we can write a middleware that attaches a timeout to each single Resolvable batch:

(defn wrap-timeout
  [engine timeout-ms]
  (->> (fn [resolver]
         (fn [env batch]
           (-> (resolver env batch)
               (d/timeout! timeout-ms))))
       (engine/wrap engine)))

More possibilities include caching, tracing, monitoring, circuit-breaking, etc… I recommend checking out the existing middlewares (in claro.middleware.*) for more examples.