Artifact Registration
Chronicle’s kernel has to know what your application is made of before it can do anything useful with it. Event types need schemas registered so events can be validated and migrated. Reducers and projections need to be declared so the kernel knows what to feed them. Constraints need to exist before the append they are meant to stop.
The client does all of that for you. Every class carrying @EventType,
@ReadModel, @Reactor, @Reducer, @Constraint, @FromEvent, or
implementing IProjectionFor, ICanSeedEvents or IWebhookDefiner is found on
the classpath and registered the moment the client connects — in the order the
kernel needs them.
The short version
Section titled “The short version”Write your artifacts. Connect. That is the whole setup:
val client = ChronicleClient(ChronicleOptions.development())val store = client.getEventStore("MyApp")store.awaitRegistration()
store.eventLog.append("employee-1", EmployeeHired("Ada", "Lovelace", "Engineer"))From Java:
var client = BlockingChronicleClient.connect(ChronicleOptions.development());var store = client.getEventStore("MyApp");store.awaitRegistration();
store.getEventLog().append("employee-1", new EmployeeHired("Ada", "Lovelace", "Engineer"));awaitRegistration() returns as soon as the first registration pass has run.
It is not required: the first append waits for the same pass. Calling it at
startup moves that wait out of your first request. It has no time limit of its
own, so bound it; see
Connection lifecycle.
In a Spring Boot application the starter already waits for it at startup, for
up to cratis.chronicle.registration-timeout. See Spring Boot.
What gets discovered
Section titled “What gets discovered”| Artifact | Recognized by |
|---|---|
| Event type | @EventType |
| Event type migration | Implements IEventTypeMigration |
| Read model | @ReadModel |
| Projection | Implements IProjectionFor |
| Model-bound projection | Read model carrying @FromEvent |
| Reactor | @Reactor |
| Reducer | @Reducer |
| Constraint | Implements IConstraint |
| Event seeder | Implements ICanSeedEvents |
| Webhook | Implements IWebhookDefiner |
| Capture | Implements ICapture |
| Reactor middleware | Implements IReactorMiddleware |
| Reactor argument resolver | Implements IReactorMethodArgumentResolver |
Only concrete classes qualify — interfaces and abstract classes are skipped, so your own base types never get registered by accident.
The two reactor entries are client-side: they are never declared to the kernel, they take part in how a reactor handler is invoked. They are discovered here because they answer the same question — what is this application made of? See Reactor middlewares below.
External services and event store subscriptions are configuration rather than
artifacts: they describe systems outside your application, so they are still set
up explicitly through store.externalServices and store.eventStoreSubscriptions.
The order things are registered in
Section titled “The order things are registered in”Order matters, and the client gets it right so you do not have to think about it:
- Event types and their migrations, together in one call, because everything else is expressed in terms of event type ids and because the kernel merges an event type’s generations from a single registration.
- Read models that no observer produces. A read model built by a reducer or a projection is registered by that observer instead, which is the only place the observer’s type and identity are known.
- Constraints, projections and webhooks.
- Reactors and reducers, which start observing.
- Captures, which start appending the moment they run, so like seeding they go behind every observer that should see what they bring in.
- Seeders, last of all — the kernel appends seeded events immediately, so every observer that should see them has to be watching first.
Registration runs again on every reconnect, because a kernel that restarted has forgotten what it was told. Reactors and reducers are started only once: each one re-establishes its own observation when the connection comes back.
When registration fails
Section titled “When registration fails”Reactors and reducers are started one at a time. When one cannot be started, the client reports it and carries on with the rest, including captures and seeders:
[ArtifactRegistrations] Reactor 'com.acme.Notifications' could not be started: <message>The failed one is tried again on the next registration pass, after a reconnect
or when you call registerAll(); one that started is never started twice.
The other steps (event types, read models, constraints, projections and webhooks) run as a whole. When one of them throws, the pass stops there, everything later in the order is not registered in that pass, and the client writes the failure to standard error:
[EventStore] Automatic registration of artifacts failed: <message>The pass runs again on the next reconnect.
The waiting calls are released anyway: awaitRegistration() returns normally,
and the first append goes ahead, failing if its event type was never
registered. So a clean return from awaitRegistration() does
not prove that registration succeeded. Check standard error after startup.
Common causes:
- A reactor or reducer with no handler method throws
ObserverHasNoHandlers. A handler is a public method whose first parameter is an@EventTypeclass. - An artifact the default activator cannot create throws
ArtifactActivationFailed; see Artifacts with dependencies. - A reactor handler asks for a parameter nothing can supply.
An IConstraint needs @Constraint as well. Without the annotation it is
skipped without a message.
Narrowing the scan
Section titled “Narrowing the scan”Scanning the whole classpath is fine for most applications and takes a fraction of a second. In a large one, or where third-party libraries ship Chronicle artifacts of their own, point discovery at the packages you own:
val options = ChronicleOptions.development() .withArtifactsFrom("com.acme.ordering", "com.acme.shipping")Sub-packages are included, so one entry per top-level package is usually enough.
Listing artifacts explicitly
Section titled “Listing artifacts explicitly”KnownClientArtifacts takes the list instead of finding it — useful where
classpath scanning is unwanted, such as a locked-down runtime or a spec that must
not see the rest of the classpath. Each class is sorted into every kind it
qualifies for, so a @ReadModel carrying @FromEvent only has to be listed once:
import io.cratis.chronicle.artifacts.KnownClientArtifacts
val options = ChronicleOptions.development().copy( artifacts = KnownClientArtifacts( EmployeeHired::class, EmployeeState::class, EmployeeStateReducer::class, UniqueEmployeeEmail::class ))Turning it off
Section titled “Turning it off”Manual registration is still fully supported. Turn discovery off and register whatever you like, whenever you like:
val client = ChronicleClient(ChronicleOptions.development().withoutAutoRegistration())val store = client.getEventStore("MyApp")
store.eventTypes.register(EmployeeHired::class, EmployeePromoted::class)store.reducers.register(EmployeeStateReducer())store.constraints.register(UniqueEmployeeEmail())From Java:
var options = ChronicleOptions.development().withoutAutoRegistration();var client = BlockingChronicleClient.connect(options);var store = client.getEventStore("MyApp");
// Event types are not wrapped by the blocking store; use the static bridge.EventTypesServiceJavaBridge.register(store.unwrap().getEventTypes(), EmployeeHired.class, EmployeePromoted.class);store.getReducers().register(new EmployeeStateReducer());With auto-registration off, awaitRegistration() returns immediately — there is
nothing to wait for. store.registerAll() still works, and registers everything
discovery found in one call, so you can keep discovery and merely control when
it happens.
The console samples in Samples/Kotlin/Console and Samples/Java/Console
deliberately opt out, so they double as a tour of the registration API.
Artifacts with dependencies
Section titled “Artifacts with dependencies”By default an artifact is created by calling a constructor that takes no arguments — either a genuine no-arg constructor, or one where every parameter has a default. That covers reactors, reducers, constraints and seeders as they are normally written.
An artifact that needs collaborators should be created by a container. Supply an
IArtifactActivator and the client will ask it instead:
import io.cratis.chronicle.artifacts.IArtifactActivator
val options = ChronicleOptions.development().copy( artifactActivator = IArtifactActivator { type -> myContainer.resolve(type) })Spring Boot applications get this for free — see Spring Boot.
If an artifact cannot be created, the client throws ArtifactActivationFailed
naming the class and explaining the three ways out: give it a constructor that
takes nothing, give its parameters defaults, or activate it through a container.
Reactor middlewares
Section titled “Reactor middlewares”Tracing, logging, metrics and correlation scoping want to happen around every
reactor handler and belong to none of them. Put them in an IReactorMiddleware
and every reactor stays a description of what happens when a fact arrives:
import io.cratis.chronicle.events.EventContextimport io.cratis.chronicle.observation.IReactorMiddleware
class HandlerLogging : IReactorMiddleware { override suspend fun beforeInvoke(context: EventContext, event: Any) { val name = event::class.simpleName println("Handling $name #${context.sequenceNumber} for ${context.eventSourceId}") }
override suspend fun afterInvoke(context: EventContext, event: Any) { println("Handled ${event::class.simpleName} #${context.sequenceNumber}") }}Writing the class is all there is to it — discovery finds it and applies it to
every reactor. beforeInvoke runs outermost-first and afterInvoke in reverse,
so middlewares nest the way you would write them by hand, and afterInvoke runs
whether the handler returned or threw.
One middleware instance serves every reactor, and a suspending handler can
resume on a different thread, so afterInvoke may run on another thread than
beforeInvoke. Keep per-invocation state out of fields and ThreadLocals.
Java cannot implement a suspending method, so a Java middleware implements
BlockingReactorMiddleware — the same two methods without suspend — and the
client adapts it onto the same chain.
Handler parameters beyond the event
Section titled “Handler parameters beyond the event”A reactor handler takes the event, and optionally its EventContext. Anything
past that is resolved per invocation, which is how a handler asks for a read
model without reaching for the event log:
@Reactorclass OverdraftAlerts(private val mail: Mailer) { suspend fun moneyWithdrawn( event: MoneyWithdrawn, account: AccountBalance? ) { if ((account?.balance ?: 0.0) < 0.0) mail.overdrawn(event.accountId) }}The instance is fetched for the event source the event arrived under, and is
null when nothing has been projected for that key yet, so declare the
parameter nullable. It is read from the read model store, which is updated
asynchronously: it may not include the event being handled, or events just
before it. Do not base a decision that must be exact on it; enforce such rules
with a constraint when the event is appended.
Implement IReactorMethodArgumentResolver to supply anything else, a
container-backed service for instance. Discovered resolvers are consulted before
the built-in read model one, so an application can take over a parameter the
client would otherwise claim. A parameter nothing can supply is rejected when the
reactor registers, rather than failing on every event.
Reference
Section titled “Reference”| Option | Default | Description |
|---|---|---|
autoDiscoverAndRegister | true | Register artifacts on connect |
artifacts | ClientArtifacts.default | What the application is made of |
artifactActivator | ArtifactActivator | How artifacts are created |
Method on IEventStore | Description |
|---|---|
registerAll() | Register everything now. Safe to call repeatedly |
awaitRegistration() | Wait for it. Returns at once when it is off |