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.
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.
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", ...}}})
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.
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.