CommandGateway

class CommandGateway @InternalWowApi constructor(registry: ModelRegistry, coordinators: Map<String, WaitCoordinator>, authorizer: CommandAuthorizer? = null, transformers: List<CommandTransformer> = emptyList(), validators: List<CommandValidator> = emptyList(), val ids: IdGenerators = IdGenerators.DEFAULT, val tenants: TenantPolicy = TenantPolicy.CALLERS, mapper: JsonMapper = ClusterCodec.mapper, clock: Clock = Clock.systemUTC(), detachedContext: CoroutineContext = Dispatchers.Default, committed: CommittedRequests = CommittedRequests.NONE, observer: Observer = Observer.NONE, peers: PeerCommands? = null)

Admits commands: turns a ServerCommandRequest into an immutable Command and hands it to the WaitCoordinator of its context, which checks its wait plan, routes it and waits for it.

Admission runs in this order, and the first step that refuses answers the request:

  1. Resolve the command type from registry, by its model name or the part of it after the aggregate type (AddItem for shop.cart.AddItem), or by the class of a command object (CommandBody.Value), among the commands the aggregate decides and those mounted under it: a command this node does not serve is FrameworkCodes.COMMAND_UNHANDLED. An aggregate it does not serve is a peer context's when peers knows its contract: admitted as PeerAdmission.admitPeer says and sent to that context's command topic; any other is FrameworkCodes.COMMAND_UNHANDLED.

  2. Decide the identity, each fact by one rule (–); a fact given twice, or missing, is FrameworkCodes.COMMAND_MALFORMED:

    • the tenant and the scope are the caller's; a ServerCommandRequest.target replaces them only for a caller with Caller.crossScope, and is FrameworkCodes.AUTH_FORBIDDEN for any other, never ignored;

    • an aggregate type whose tenant tenants fixes is in that tenant whoever the caller is; a target that names another tenant for it is FrameworkCodes.COMMAND_MALFORMED;

    • the aggregate id is the request's; a command that may create the aggregate may name none: it takes the id of the aggregate its request id already created in committed, within the caller's scope, else a new one from ids' generator of the aggregate type, so a retry keeps it. A request of a type one of transformers takes, a mounted command among them, may name none: its transformer finds the aggregate. A command whose body names its aggregate (AggregateId) has its body decoded here, as step 4 says, and its aggregate id is the body's: a request that names it too names the same one, and a blank one is FrameworkCodes.COMMAND_MALFORMED; a creation whose body names none is given one as above;

    • the command id is always a new global one from ids; the request id is the request's, or the command id;

    • the operator is the caller's (me.ahoo.wow.api.caller.Caller.toOperator): its subject in its own tenant, marked as acting elsewhere when a target was named; none for an anonymous caller;

    • a synchronous call's ServerCommandRequest.call is FrameworkCodes.AUTH_FORBIDDEN unless the caller is a service identity, and FrameworkCodes.COMMAND_MALFORMED beside a target; from a service, the command's id, tenant, scope, operator, causation and propagating request facts (CallContext.facts) are the call's: a derived command, executed with the call's wait context as one from another context's command topic, and answered at COMMITTED, apart from the caller and within the default wait timeout (WaitCoordinator.call);

    • a request header in the framework's namespace (Wow-…, wow.…) is refused: identity, ids and the wait have fields of their own.

  3. Authorize the action with authorizer, before the body is decoded unless it names the aggregate; without one, every action is allowed here (authorizes), and the public port refuses the built-in aggregates' system commands itself. An id longer than 256 characters (a request, aggregate, tenant or space id) is FrameworkCodes.COMMAND_MALFORMED (AdmissionLimits).

  4. Decode the body as the command type with mapper; one that does not decode, or that writes \u0000 in a string or a key, which not every store keeps (AdmissionLimits), is FrameworkCodes.COMMAND_MALFORMED. A command object is not decoded: it must be of the command type, and is checked as the JSON mapper writes it as. A request whose ref names another tenant than the one decided (ServerCommandRequest.tenantId) is FrameworkCodes.COMMAND_MALFORMED too.

  5. Transform the command with the one of transformers that takes its type, if any (CommandTransformer). The command it becomes must be decided by an aggregate this node serves (FrameworkCodes.COMMAND_UNHANDLED otherwise); its tenant is decided again for that aggregate type, and its scope, request id, expected version and wait plan are the request's. When the type or the aggregate changed, authorizer authorizes the result again. A result that names no aggregate gets an id as a creation without one does when it creates its aggregate, and is FrameworkCodes.AGGREGATE_NOT_FOUND otherwise. The command's headers then name the input (CommandTransformer.TRANSFORMED_FROM), and so does every result (CommandResult.transformedFrom). A transformer that throws a RejectedException refuses the command with its rejection, as the kernel answers a decide function's: its code qualified with the input's aggregate type, with its message and extensions.

  6. Validate the body with every one of validators: their violations, together, are FrameworkCodes.COMMAND_INVALID.

  7. Build the Command, its wait plan the request's, or WaitPlan.DEFAULT within the coordinator's default timeout (defaultWaitTimeout) when it names none; its timeout capped by the coordinator's maximum.

The coordinator then checks the plan, routes the command and waits for it; deduplication by request id and the check that an existing aggregate belongs to the command's scope are the kernel's.

The transformers are checked when the gateway is built (Transformations).

A refusal is a result at ACCEPTED, REJECTED with its code, and the only result the flow emits. Until the command is built, it names the facts decided so far; its ref names the aggregate id UNADDRESSED when none was decided.

A command whose plan has only an ACCEPTED hop is answered once admitted, and runs detached from the request in a supervised scope this gateway owns; it is not durable: close waits for the ones running, and a node that stops before running one loses it.

Constructors

Link copied to clipboard
constructor(registry: ModelRegistry, coordinators: Map<String, WaitCoordinator>, authorizer: CommandAuthorizer? = null, transformers: List<CommandTransformer> = emptyList(), validators: List<CommandValidator> = emptyList(), ids: IdGenerators = IdGenerators.DEFAULT, tenants: TenantPolicy = TenantPolicy.CALLERS, mapper: JsonMapper = ClusterCodec.mapper, clock: Clock = Clock.systemUTC(), detachedContext: CoroutineContext = Dispatchers.Default, committed: CommittedRequests = CommittedRequests.NONE, observer: Observer = Observer.NONE, peers: PeerCommands? = null)

Types

Link copied to clipboard

Properties

Link copied to clipboard

Whether the node has an authorizer (CommandGateway's authorizer): without one, every action is allowed at step 3, and the public port refuses the system commands of Wow's built-in aggregates, the jobs' and the snapshot rebuild's, never allowing them by default. Their senders in the process and on the internal port are not refused.

Link copied to clipboard

The node's id generators: command ids from the global one, new aggregate ids per type.

Link copied to clipboard

Whether the node is on standby (me.ahoo.wow.runtime.node.ServiceNode.start): every request is refused with FrameworkCodes.NODE_STANDBY, before anything of it is read, until the node is activated.

Link copied to clipboard

The tenant of each aggregate type: the caller's, unless fixed for the type.

Functions

Link copied to clipboard

The aggregate types that take command, a command class, for a request that leaves its type out: the ones this node decides or mounts it on, and the ones a peer context's contract lists it for.

Link copied to clipboard
suspend fun close()

Refuses new requests from now on (FrameworkCodes.NODE_CLOSING), waits for the detached commands accepted before, those whose admission was still under way included, and closes their scope.

Link copied to clipboard

The timeout of a command of context whose request names none: its coordinator's default, capped by its maximum; WaitPlan.DEFAULT_TIMEOUT for a context this gateway does not admit. An edge whose request names a plan but no timeout gives the plan this timeout.

Link copied to clipboard

Admits request and returns the result its wait ends with: the last one send emits.

Link copied to clipboard

Admits request and emits its results: an admission refusal alone, or what its wait emits for each hop as it is reached (see WaitCoordinator.stream); for a plan with only an ACCEPTED hop, that result alone. The command is admitted when the flow is collected.