Package-level declarations

Types

Link copied to clipboard
sealed interface AfterFunction<S : Any>

An after hook: runs after the decide function of every command it applies to, sees the same, not yet evolved state, and returns events appended after the command's own, in the same event stream. It does not run when the command is rejected. A hook the model builder makes runs after a system command (a lifecycle or wow.schedule one) only when it names it (AggregateModelBuilder.after). Like a decide function it keeps its form; a Suspending.blocking one runs on a virtual thread.

Link copied to clipboard
class AggregateModel<S : Any> @InternalWowApi constructor(val type: AggregateType, val stateType: Class<S>, initial: (AggregateRef) -> S, deciders: Map<Class<*>, DecideFunction<S>>, evolvers: Map<Class<*>, (S, Any) -> S>, eventTypes: Collection<EventType>, commandTypes: Collection<CommandType>, afterFunctions: List<AfterFunction<S>> = emptyList(), lifecycle: Set<Class<*>> = Lifecycle.COMMANDS.values.toSet(), mountedCommandTypes: Collection<CommandType> = emptyList(), val ownership: Ownership = Ownership.OWNER, val stateRevision: Int = 1, retiredEvents: Collection<String> = emptyList(), val idSource: AggregateIdSource = AggregateIdSource.ASSIGNED, aggregateIds: Map<Class<*>, (Any) -> String?> = emptyMap(), commitEvolvers: Map<Class<*>, (S, Any, EventStream) -> S> = emptyMap(), access: Map<AccessAttribute<*, *>, (S) -> Any?> = emptyMap())

An aggregate type compiled into tables: what each command type runs and how it is named, how each event type evolves the state, and how each event type is named in the log. Built once, read on every command; the hot path only looks things up.

Link copied to clipboard
class AggregateModelBuilder<S : Any>(val type: AggregateType, val stateType: Class<S>)

The Kotlin DSL of an aggregate model. Command and event types are named context.aggregate.Name after their class's @Name, else its simple name, unless a name is given: the name the compiler gives them, so a model written by hand stores what the Manifest says. A name given that disagrees with the class's @Name, or an aggregate named otherwise than its @AggregateRoot state class, fails here: the log would hold names the Manifest does not.

Link copied to clipboard
class AmbiguousDependencyException(val type: Type, val candidates: List<String>, val building: String? = null, val parameter: String? = null) : IllegalStateException

Several dependencies are of type, the candidates by their names, and none is the one to take: the parameter of building names one with a qualifier (@Name), or the host marks one primary.

Link copied to clipboard
sealed interface BatchOutcome

What Kernel.executeBatch did with one command of a batch.

Link copied to clipboard

The dispatcher every @Blocking call runs on: a virtual thread per call (JDK 21). The kernel runs blocking decide functions and after hooks on it, the runtime its blocking subscription handlers and receipts, so a node has one of it.

Link copied to clipboard
class CachedState(val state: Any, val version: Int, val scope: Scope = Scope.NONE, val deleted: Boolean = false, val schedules: Map<String, ScheduleRule> = emptyMap())

An aggregate's state after its event stream version, with the metadata the kernel keeps beside it: its scope, the owner and space it has after that stream, which the next stream carries forward; whether it is deleted. Only the lifecycle commands change them. Its schedules are the rules of the schedules it keeps, by key: those its streams set and did not cancel.

Link copied to clipboard

What a command records of the caller that sends it, as admission derives it: the scope it acts in, the operator that sends it and the request facts of the caller, its application's among them. The command gateway, the edge and the test DSL derive them alike (of); the tenant is me.ahoo.wow.spi.tenant.TenantPolicy's.

Link copied to clipboard

A saga's handler let through the rejection of a synchronous call it made, which no @OnRejected handler of the saga takes: a business outcome, so the aggregate parks at once, without a retry, as for a derived command's rejection no handler takes, and a wait at the saga's hop ends REJECTED with the target's error.

Link copied to clipboard

Sends the synchronous calls of a saga's execution to other contexts: the runtime's HTTP client on a node, an answer of the test DSL in a domain module's test. call returns the target's result as it is, SUCCEEDED, REJECTED or FAILED, and throws a WowProblemException (failure) when it reaches none; ExecutionSagaContext.call turns the results that did not succeed into the handler's typed failures.

Link copied to clipboard

How the kernel finished a command; the runtime reads it, an application never does.

Link copied to clipboard
class CommandType @InternalWowApi constructor(val name: String, val type: Class<*>, val aliases: Set<String> = emptySet())

How a command type is named: context.aggregate.CommandName, the class's simple name unless the model gives another. The log records it with each event stream, and a request id reused by a command of another name is rejected rather than taken for a retry; renaming or moving the class does not change it.

Link copied to clipboard

Names the commands a decide function schedules for an aggregate of another type than its own: the full type name (context.aggregate.Command) of the command class sent to an aggregate of target, or null when none is known. The runtime names them from the context's models and the Manifests of the service and its peers.

Link copied to clipboard

One @OnCommitted handler of a saga, bound to its processor instance, the counterpart of RejectionBinding: what the compiler generates for each @OnCommitted function of a @Saga. It belongs to the saga's ProcessorBindings (ProcessorBinding.commits), since it takes the success receipts of the commands the saga's own handlers derive; like them, it knows no runtime.

Link copied to clipboard
class CommitTrace(val stream: EventStreamRecord, val events: List<Any>, val state: JsonNode, val deleted: Boolean, val access: Map<String, Any?> = emptyMap())

One commit of an aggregate's history (Kernel.trace): its stream, the stream's events (but the retired ones) read as the model's current schema (upgraded), and the aggregate after it: its state as the event codec writes it (a tree, so a mutable state evolving further does not change it), its deleted mark and its declared access attributes in their stored form (EventCodec.accessOf).

Link copied to clipboard

The queries a subscription handler reads with (me.ahoo.wow.api.subscribe.SubscriptionContext.queries), by the tenant of the commit it handles: the node's query API on a node, a test's fake in a domain module's test.

Link copied to clipboard
sealed interface DecideFunction<S : Any>

A decide function, in the form its signature has. The kernel calls a Sync one directly, with no continuation and no dispatch; only a Suspending one runs as a coroutine call, and a Suspending.blocking one (@Blocking) on the kernel's blocking dispatcher, a virtual thread.

Link copied to clipboard
fun interface Dependencies

Where the parameters of the generated model factories come from: the domain services a command function takes besides its command. A ModelProvider resolves each when the host builds the models, once, at startup, so a missing one fails the start rather than a command. The port knows no DI framework: a host adapts its own container (Spring's bean factory, for one), and of serves tests and hosts without one.

Link copied to clipboard

The execution of a handler that derives commands, as its context: it turns the requests the handler returned into the commands the subscription sends, as me.ahoo.wow.api.subscribe.SagaContext describes, followed by those the handler recorded through its context (a saga's schedules), in the order it recorded them.

Link copied to clipboard
class EventCodec(mapper: JsonMapper = DEFAULT_MAPPER)

Turns events into what the log stores, and back. Events are named by the model, never by class name, so renaming or moving a class does not orphan its history. States go into and come out of snapshots the same way.

Link copied to clipboard
class EventType @InternalWowApi constructor(val name: String, val revision: Int, val type: Class<*>, val upgrades: Map<Int, EventUpgrade> = emptyMap(), val aliases: Set<String> = emptySet())

How an event type is named in the log and which schema revision the model reads it as.

Link copied to clipboard
fun interface EventUpgrade

Turns an event's payload of one revision into the next revision's, as an @Upgrade function does: the tree of a record as stored, numbers exact, in and out.

Link copied to clipboard
class ExecutionContext @InternalWowApi constructor(val executionId: ExecutionId, contextQueries: ContextQueries) : SubscriptionContext

The context of a projection's or a reactor's handler for one execution; its queries are contextQueries' of the commit's tenant.

Link copied to clipboard

Whether the command the kernel executes in this coroutine runs on the owner of its log partition (C2 and C4), as the router that chose this node says:

Link copied to clipboard
class ExecutionReceiptContext(val executionId: ExecutionId, receiptOf: String, causation: Causation, operator: Operator?, scope: Scope, val ids: Ids = IdGenerators.DEFAULT, headers: Map<String, String> = emptyMap(), commandTypes: CommandTypes = CommandTypes.NONE, contextQueries: ContextQueries = ContextQueries.NONE, codec: EventCodec = EventCodec(), calls: CommandCalls = CommandCalls.NONE, sends: SagaSends? = null) : SagaContext, Derivation

The context of a saga's handler of a receipt: of the rejection of the command receiptOf, by an @OnRejected handler, or of its commit, by an @OnCommitted one. Handling the same receipt again, after a retry or a second delivery, derives the same requests, which the kernel does not decide twice: each command's id and request id are compensationId of receiptOf, this execution, and what it asks (its target, its command type and its business key or occurrence), as a saga's derived commands are keyed (ExecutionSagaContext.derivedId). Each command's causation is the command receiptOf, in the chain of its root (causation), and its operator and scope are that command's, and so are its headers, the request facts it inherited (me.ahoo.wow.api.decide.RequestFacts.PROPAGATED). It schedules, cancels and calls as every saga context does (ExecutionSagaContext): its schedules written by codec, its calls through calls.

Link copied to clipboard
class ExecutionSagaContext @InternalWowApi constructor(val executionId: ExecutionId, event: StateEvent<*>, val ids: Ids, commandTypes: CommandTypes, codec: EventCodec = EventCodec(), calls: CommandCalls = CommandCalls.NONE, contextQueries: ContextQueries = ContextQueries.NONE, sends: SagaSends? = null) : SagaContext

The context of a saga's handler for one execution of the commit event. Each execution, a retry included, gets a new one, and each command it derives is keyed by what it asks (derivedId): its target, its command type and its business key or its occurrence among the commands of this execution with both. Handling the commit again derives the same request for each command it derives again, in whatever order, which the kernel does not decide twice (it deduplicates by request id). A domain module's tests build one to call a saga directly.

Link copied to clipboard

How a handler's failures are classified, by the runtime's subscriptions and by the test DSL alike: a handler's own @Retry(recoverable, unrecoverable) (HandlerRetry.classify) first, of the failure itself or, for an UnreadableEventException, of what the kernel could not read; then, for the failure and each of its causes in turn, each of classifiers in order, the first class one gives deciding; FailureClass.RECOVERABLE when none does. of orders them: the application's, then the modules' found on the classpath (MODULES), then the caller's own, then the built-in rules for Wow's SPI failures (BuiltInFailures). A HandlingRejectedException is no failure to classify: a business outcome, it parks the aggregate at once.

Link copied to clipboard
data class FirstCommit(val eventTime: Long, val operator: Operator?)

What an aggregate's first stream recorded: its commit time (eventTime) and its operator, the creator.

Link copied to clipboard
class HandlerRetry @InternalWowApi constructor(val maxAttempts: Int? = null, val initialBackoff: Duration? = null, val maxBackoff: Duration? = null, val multiplier: Double? = null, val timeout: Duration? = null, val recoverable: List<Class<out Throwable>>? = null, val unrecoverable: List<Class<out Throwable>>? = null, val systemic: List<Class<out Throwable>>? = null)

A subscription's own in-process retry budget: the settings it gives, each null when it gives none, which then come from the budget below it (over). A handler's @Retry gives one (of), the host's wow.subscriptions.[<context>.<name>].retry.* another, over it; both fall back to wow.subscription.retry.

Link copied to clipboard
class HandlerTimeoutException(val subscription: String, val ref: AggregateRef, val version: Int, val timeout: Duration) : IllegalStateException

An attempt of subscription's handler at ref's version that ran longer than its budget's timeout (HandlerRetry.timeout, @Retry(timeout)): it was cancelled and counts as a failed attempt, retried like any other failure. The runtime's subscriptions and the test DSL throw it alike.

Link copied to clipboard
open class HandlingRejectedException(val error: ErrorInfo, message: String, cause: Throwable? = null) : IllegalStateException

A handling that failed for a business reason, which no retry changes: a subscription does not retry it but parks the aggregate at once, and a wait at the hop ends REJECTED with error. A saga's call or derived command whose rejection no @OnRejected handler takes ends a handling so.

Link copied to clipboard

A saga's handler derived what is no derived request: an element that is no CommandRequest (a Command, another object), or a request with a wait or a timeout, which the subscription that sends it waits for none of. A defect in the saga, which no retry changes: the handling fails unrecoverable, so the aggregate parks at once.

Link copied to clipboard
class Kernel(log: EventLog, codec: EventCodec = EventCodec(), clock: () -> Long = System::currentTimeMillis, reservations: ReservationStore? = null, states: StateCache = StateCache.None, snapshots: SnapshotStore? = null, commandTypes: CommandTypes = CommandTypes.NONE, ids: Ids = IdGenerators.DEFAULT, observer: Observer = Observer.NONE, blocking: CoroutineContext = BlockingDispatcher.dispatcher, options: KernelOptions = KernelOptions(), snapshotTailReaders: (AggregateType) -> SnapshotTailReader? = { null }, policies: WritePolicies = WritePolicies.NONE)

The write kernel: turns one admitted command into at most one committed event stream (documentation/designs/02-write-path.md §1).

Link copied to clipboard
class KernelOptions @InternalWowApi constructor(val maxAttempts: Int = DEFAULT_MAX_ATTEMPTS, val verifyDecidePurity: Boolean = false, val reservationTtl: Duration = DEFAULT_RESERVATION_TTL, val maxEarlyWait: Duration = DEFAULT_MAX_EARLY_WAIT, val recentRequests: Int = Kernel.DEFAULT_RECENT_REQUESTS, val maxReplay: Int = DEFAULT_MAX_REPLAY)

The tuning of a Kernel, set in code, not by configuration keys:

Link copied to clipboard
class MissingDependencyException(val type: Class<*>, val qualifier: String?, val building: String? = null, val fullType: Type = type) : IllegalStateException

No dependency of type, named qualifier when it is not null, to build building with: a processor (OrderSummaryProjection) or a model (the Order model), the generated code's models and processors when it is null.

Link copied to clipboard
interface ModelProvider

The binding index of one module: the aggregate models of its bounded context context, the handlers of its subscription processors (processors) and of its service routes (routes), and where its Manifest fragment is (fragment, META-INF/wow/manifest/<module>.json). The compiler generates one per module and registers it in META-INF/services/me.ahoo.wow.kernel.ModelProvider, so the host finds every module of the service with load, without scanning or reflecting.

Link copied to clipboard

One handler of an annotated subscription processor, bound to its processor instance: what the compiler generates for each @OnEvent function of a @Projection, @Reactor or @Saga, exposed by its module's ModelProvider.processors. It knows no runtime: the runtime compiles each into one subscription named name, processor.function.

Link copied to clipboard
class QueuedCommand(val command: Command<*>, val wait: WaitContext? = null)

A command an aggregate's lane hands to Kernel.executeBatch, with the wait context the runtime built for it.

Link copied to clipboard

One @OnRejected handler of a saga, bound to its processor instance: what the compiler generates for each @OnRejected function of a @Saga. It belongs to the saga's ProcessorBindings (ProcessorBinding.rejections), since it takes the rejections of the commands the saga's own handlers derive, not entries of the log; like them, it knows no runtime.

Link copied to clipboard

Rejection codes as the Manifest, results, receipts and HTTP name them: a decide function may reject with the local name of a code of its aggregate (invalid-item, a code without a dot, declared within its @AggregateRoot class), which the kernel qualifies with the deciding command's aggregate type (eshop-basket.basket.invalid-item) before anything else sees it; a code with a dot is full, and stays as it is.

Link copied to clipboard

The one shape of a command's rejection in the log, under the logger CATEGORY: a readable message, and the fields as SLF4J key-values named as the spans' attributes name them (CONTEXT, AGGREGATE, COMMAND, ERROR_TYPE, REQUEST_ID, and WITH_EVENTS for a decision's). A decision's rejection is a business outcome, at INFO (rejected); an admission's refusal is the gateway's ordinary work, at DEBUG (refused).

Link copied to clipboard

A read-only state read of ref (Kernel.stateAt, Kernel.trace) would replay more than limit streams of its log: it is refused before the stream past limit is folded, so a read of a long history costs at most limit streams and never holds the history in memory. The state the read asked for exists; it is only too far from the snapshot, or from version 1, to be replayed for a point read.

Link copied to clipboard
class SagaSends(val derives: List<Class<*>> = emptyList(), val schedules: List<Class<*>> = emptyList(), val calls: List<Class<*>> = emptyList())

What a saga's handler declares it sends (me.ahoo.wow.api.annotation.Sends), by the commands' classes: those it derives (returns), those it schedules with SagaContext.schedule and those it calls with SagaContext.call. The compiler writes them into the handler's binding: what @Sends declares, else, for derives, what the handler's return type names. A command of a class none of the list's classes is, or is a subclass of, is undeclared: sending it fails the handling with an UndeclaredSendException.

Link copied to clipboard
class ScheduleNotDueException(val ref: AggregateRef, val requestId: String, val due: Long, val now: Long) : RuntimeException

A command a schedule sent (me.ahoo.wow.api.decide.Command.schedule) reached the kernel of ref longer before its slot's due time, by this kernel's clock (now), than the kernel waits for one: the firing node's clock runs that far ahead of this one. The command is not decided and the aggregate's queue is not held for it; the firing runner sends it again later, under the same request id, as after any technical failure.

Link copied to clipboard
enum class StartFrom : Enum<StartFrom>

Where a subscription without a checkpoint in a partition starts.

Link copied to clipboard

Where the kernel keeps aggregates' states between commands, so a command does not replay the log.

Link copied to clipboard

The measured size of the states of each class, for a state cache bounded by bytes: the mean length of their JSON as the EventCodec that measures them writes it (the shared states and snapshots) and reads it (the snapshots loaded), over about its last WINDOW states.

Link copied to clipboard
object Types

The full types of the generated factories' dependencies, built without reflection: the compiler writes a parameterized parameter type as parameterized of its class and its type arguments, so Dependencies can tell StateViewCache<OrderStateView> from StateViewCache<PaymentStateView>. The types equal the JDK's of the same declaration, as ParameterizedType and WildcardType specify, so a host's container matches them.

Link copied to clipboard
class UndeclaredEventException(val type: AggregateType, val command: Class<*>, val event: Class<*>, message: String) : RuntimeException

A command's handling produced an event its function does not declare: a decide function or an after hook of the model of type produced event for command, outside the events its return type yields or its produces lists, or a lifecycle command's decide function another lifecycle event than its own. A bug in the model, not an outcome of the command: the command fails (wow.command.undeclared-event) and commits nothing.

Link copied to clipboard
class UndeclaredSendException(val subscription: String, val command: Class<*>, val member: String) : IllegalStateException

A saga's handler subscription sent command, a command of a class its @Sends([member]) does not declare: a bug in the saga, not an outcome of the command, which no retry changes. The handling fails unrecoverable, so the aggregate parks at once, and the command is not sent.

Link copied to clipboard
class UnreadableEventException(val ref: AggregateRef, val version: Int, val name: String, val revision: Int, val cause: RuntimeException) : IllegalStateException

An event of ref's stream at version that its model cannot read: its record name at revision fails to decode or to upgrade, as cause says. The kernel throws it from rebuilding a state, so the failure names the aggregate. It classifies as its cause does: a handler's @Retry lists and the failure classifiers see through it (FailureClassification).

Link copied to clipboard

The access policy (AccessPolicy) that decides who changes each aggregate type: the service's policy of it, the one its reads are decided by too, or none, and the default rule (ScopeAccess) decides. The host resolves the service's policies once, as the query side does; none by default.

Properties

Link copied to clipboard

Whether this command was derived by the server's own code rather than sent by a caller: a saga's, an @OnRejected compensation, a timer's, or one another context sent through its command topic. Such a command belongs to a chain, so it carries a Command.causation; admission gives a caller's none (it is its own root).

Functions

Link copied to clipboard
inline fun <reified S : Any> aggregate(context: String, name: String, noinline block: AggregateModelBuilder<S>.() -> Unit): AggregateModel<S>

Builds an AggregateModel of state S for the aggregate name of bounded context context.

fun <S : Any> aggregate(stateType: Class<S>, context: String, name: String, block: AggregateModelBuilder<S>.() -> Unit): AggregateModel<S>

Builds an AggregateModel of state stateType for the aggregate name of bounded context context.

Link copied to clipboard
fun committedContextOf(handler: CommittedBinding<*>, committed: CommandReceipt.Committed<*>, ids: Ids = IdGenerators.DEFAULT): SagaContext

The context an @OnCommitted handler gets for committed: its execution is (handler, aggregate, version) of the commit whose saga execution derived the command, and the commands it derives are ExecutionReceiptContext's, deterministic per committed command; it takes its ids from ids.

fun committedContextOf(handler: CommittedBinding<*>, committed: CommandReceipt.Committed<*>, ids: Ids, commandTypes: CommandTypes, queries: ContextQueries = ContextQueries.NONE, calls: CommandCalls = CommandCalls.NONE): SagaContext

The context an @OnCommitted handler gets for committed, as committedContextOf gives it, whose commands without a ref go to the aggregate type commandTypes assigns their class, and whose calls go through calls.

Link copied to clipboard
fun contextOf(kind: FunctionKind, subscription: String, event: StateEvent<*>, ids: Ids = IdGenerators.DEFAULT, commandTypes: CommandTypes = CommandTypes.SIMPLE_NAMES, calls: CommandCalls = CommandCalls.NONE, queries: ContextQueries = ContextQueries.NONE, sends: SagaSends? = null): SubscriptionContext

The context a handler of kind gets for the execution of event by the subscription subscription: its ExecutionId is (subscription, aggregate, version); a saga's takes its ids from ids, names the commands it schedules by commandTypes, makes its synchronous calls through calls and sends what sends declares; every handler reads through queries.

Link copied to clipboard
fun decisionOf(result: Any?): Decision

What an annotated command function returned, as a Decision: the conversion code that wow-compiler generates calls, so annotated functions keep returning what they return in 9.x.

Link copied to clipboard
suspend fun <S, E : Any, C : SubscriptionContext, R> eachEvent(stream: StateEvent<S>, type: Class<E>, context: C, handle: suspend (StateTypedEvent<S, E>, C) -> R): List<R>

Calls handle once for each event of stream that is a type, in their order, with the StateTypedEvent of that event and context seen as that call (invocationOf): its invocationKey names the event, while everything else, the ids a saga's commands take included, is the stream's context, so the n-th command the calls derive is the same request every time the stream is handled again. What each call returns, in order. The generated bindings call it, as they call requestsOf.

Link copied to clipboard
fun eventsOf(result: Any?): List<Any>

What an annotated after hook returned, as events: like decisionOf, but a hook cannot reject or decide.

Link copied to clipboard
fun rejectionContextOf(handler: RejectionBinding<*>, rejected: CommandReceipt.Rejected<*>, ids: Ids = IdGenerators.DEFAULT): SagaContext

The context an @OnRejected handler gets for rejected: its execution is (handler, aggregate, version) of the commit whose saga execution derived the command, and the commands it derives are ExecutionReceiptContext's, deterministic per rejected command; it takes its ids from ids.

fun rejectionContextOf(handler: RejectionBinding<*>, rejected: CommandReceipt.Rejected<*>, ids: Ids, commandTypes: CommandTypes, queries: ContextQueries = ContextQueries.NONE, calls: CommandCalls = CommandCalls.NONE): SagaContext

The context an @OnRejected handler gets for rejected, as rejectionContextOf gives it, whose commands without a ref go to the aggregate type commandTypes assigns their class, and whose calls go through calls.

Link copied to clipboard
suspend fun requestsOf(result: Any?): List<CommandRequest<*>>

The command requests a saga's handler returned: nothing (null, Unit, an empty Mono), one CommandRequest, or several, as an Iterable (such as the list a Flux was collected into), a Sequence or a Flow, collected in order. Anything else is a handler that does not return command requests: an InvalidDerivedRequestException.