从其他服务调用 Wow
不是 Wow 服务的应用,例如为前端服务的后端、较早的服务、用别的语言写的任务或合作方的系统,有四种方式访问 Wow 服务。每种方式访问服务的一个端口,并以一种身份调用。本页说明哪种方式适合哪种调用方、每个请求带什么,以及会返回什么。Wow 服务调用另一个 Wow 服务时,改用它自己的 CommandClient:见调用其他服务。
选择入口
| 调用方 | 访问 | 身份 |
|---|---|---|
| 平台之外:浏览器、移动应用、合作方 | 网关,再到服务的公开路由 | 终端用户,由网关认证;服务把网关断言的身份映射成调用方 |
| 平台内的 JVM 应用 | WowClient:user 模式访问公开路由,service 模式访问内部端口 | user 模式:终端用户,凭它转发的凭据。service 模式:它声明的服务,代它发送时的调用方行事 |
| 用其他语言写的应用 | 由服务的 OpenAPI 文档生成的客户端,经网关 | 终端用户,或网关认证的服务账号 |
| 对发生的事做出反应的应用 | 上下文在 Kafka 上发布的事件流 | 无身份:它以自己的消费组读取,不发送命令 |
公开端口只有网关能访问,内部端口只有平台自己的服务能访问(保护部署)。内部端口对调用方声明的服务名照单全收,因此只有平台的应用(像平台的服务一样部署和审查)才以 service 模式调用。其他所有应用都经网关,以它代为行事的终端用户的身份调用。
在 JVM 中
WowClient 发送命令对象、读取状态,与 Wow 服务自己的客户端一样。它从对端的 api jar 里取对端的契约,因此应用依赖对端的 *-api 模块。构建时给出每个上下文的服务在哪里,以及它的模式:
import me.ahoo.wow.client.EndpointResolver
import me.ahoo.wow.client.UserCredential
import me.ahoo.wow.client.WowClient
import java.net.URI
// In user mode, through the gateway, as the end user whose credential it forwards.
val asUser = WowClient.builder()
.endpoints(EndpointResolver.of(mapOf("eshop-catalog" to URI.create("https://gateway.internal"))))
.user(credential)
.build()
// In service mode, at the internal port, as the service it names.
val asService = WowClient.builder()
.endpoints(EndpointResolver.of(emptyMap(), mapOf("eshop-catalog" to URI.create("http://catalog.internal:9092"))))
.service("storefront-app")
.build()client.commands 与 Wow 服务里的 CommandClient 是同一个,client.queries 与 QueryApi 是同一个。service 模式下,名字放在每个请求的 Wow-Service 请求头里,被调用的服务可以列出这个名字能调用什么(wow.callers.<service>)。
在 Spring Cloud 注册中心上,客户端可以把服务写作 lb://<服务>,经 Spring Cloud LoadBalancer 找到它的实例(解析 lb://)。自己编写的 EndpointResolver 如果去查找实例(在注册中心、DNS 或 sidecar 里),还要重写 reaches,说明它服务哪些上下文:这样查找其中某个上下文时一个实例都没找到,调用会以 wow.peer.unavailable 失败,客户端会重试;而不是被当作没有 URL 的上下文,那是代码的错误,不会重试。Java 代码把客户端包进 JavaCommandClient(在 Java 中)。
在其他语言中
GET /wow/openapi 提供服务公开路由的 OpenAPI 3.1 文档,其中不含内部命令(OpenAPI 与 springdoc)。用所用语言的 OpenAPI 工具由它生成客户端,并指向网关。TypeScript 应用则用 wow-generator 由 Manifest 生成客户端,保留 OpenAPI 表达不了的等待计划与查询字段(生成的客户端)。
带上请求头
命令是把它的 JSON 体 POST 到它的路由 /<context>/<aggregate>/{id}/<command>。让重试安全、说明等待多久的请求头:
| 请求头 | 何时带 | 为什么 |
|---|---|---|
Wow-Request-Id | 每条命令都带,每条命令一个新的 UUID;再次发送同一条命令时用同一个 | 同一个 ID 的重试被当作已完成(duplicate)答复,绝不会决策两次 |
Wow-Wait | 默认的等待 snapshot 不合适时 | 命令走到哪一步再答复:accepted、committed、snapshot,或它等待的订阅(handled(<processor>.<function>)) |
Wow-Wait-Timeout | 默认的 10 秒不合适时 | 等待多久,至多 wow.wait.max-timeout |
If-Match | 命令必须作用于你读到的版本时 | 作用于其他版本的命令以 412 拒绝 |
Authorization | 网关认证所需的凭据 | 网关把它变成服务读取的身份 |
curl -X POST https://gateway.example.com/bank/account/alice/deposit \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-H 'Wow-Request-Id: 01937c2e-7b1a-7c4e-9f0e-2d1b8a6c5e3f' \
-H 'Wow-Wait: committed' \
-H 'Wow-Wait-Timeout: 5s' \
-d '{"amount": 100}'丢失答复的客户端用同一个 Wow-Request-Id 再次发送命令。换一个新 ID 就成了另一条命令,会再执行一次。全部请求头见 HTTP API;WowClient 会自己发送这些请求头。
读取答复
成功的命令答复 200 与它的结果(等待止于 accepted 时为 202)。其他答复都是 application/problem+json 的 problem,其中 code 说明发生了什么,retry 说明重试能做什么:
retry | 怎么做 |
|---|---|
none | 不要原样再发:修正请求,或告诉用户 |
same-request | 在 Retry-After 之后,用同一个 Wow-Request-Id 再发 |
new-request | 在 Retry-After 之后,作为新请求再发 |
调用方最常遇到的错误码:
| 错误码 | 状态 | 含义 |
|---|---|---|
wow.command.invalid | 400 | 命令未通过校验;violations 列出每个字段 |
wow.auth.unauthenticated、wow.auth.forbidden | 401、403 | 没有有效身份,或身份不允许发送这条命令 |
wow.aggregate.not-found | 404 | 没有该 ID 的聚合 |
领域错误码,例如 bank.account.insufficient-balance | 默认 422 | 聚合拒绝了命令;错误码是领域的契约 |
wow.aggregate.version-mismatch | 409,带 If-Match 时 412 | 聚合不在命令期望的版本上 |
wow.command.busy | 429 | 等待的命令太多;稍后用同一个请求 ID 重试 |
wow.node.closing、wow.storage.unavailable | 503 | 节点正在关闭、存储不可达;用同一个请求 ID 重试 |
wow.wait.timeout | 504 | 等待超时;命令可能已提交,因此用同一个请求 ID 重试 |
框架的全部错误码见错误码,领域如何声明自己的错误码见拒绝与错误码。
消费发布的事件流
对上下文中发生的事做出反应的应用,读取该上下文发布的事件流:在 Kafka 上,wow.<context>.<aggregate>.state 每次提交一条记录,带着提交的事件与提交后的状态(外部消费者)。事件流是上下文契约的一部分,Manifest 的兼容性检查像守护命令一样守护它的字段。
要改变上下文中的东西,调用它的 HTTP API,而不是写它的命令主题 wow.<context>.commands:
- 命令主题上的记录不经准入就执行:服务不认证它,所以谁能写它,谁就能冒充任何操作者、租户与范围。这个主题只对平台自己的服务开放(限制命令主题)。
- 记录得不到答复:没有结果、没有拒绝的错误码,也没有等待。
- 命令主题是平台自己的服务访问上下文的途径,不是给其他任何人的契约。