Files
go-omni/MODULES.md

10 KiB
Raw Blame History

业务基础模块

第二批的共享 Redis 中间件、可靠队列、多平台存储及其他模块见 扩展模块。下文的单进程约束指本页原有默认实现。

这批能力从 v0.7.0-preview 提供。统一示例见 examples/modules。业务入口继续使用 omni.New,中间件依赖 httpx,不依赖具体服务端网络实现。各模块按需创建、注入 Service,再注册到应用关闭流程。

缓存:cache 与 cache/redis

store, err := cache.NewMemory(cache.MemoryConfig{MaxEntries: 4096, MaxBytes: 32 << 20})
if err != nil { return err }
application.OnShutdown(store.Close)
loader, err := cache.NewLoader(store, cache.LoaderConfig{MaxLoads: 128, Timeout: 5 * time.Second})
if err != nil { return err }
value, err := loader.GetOrLoad(ctx, "tenant:42:product:7", time.Minute,
    func(ctx context.Context) ([]byte, error) { return repository.LoadProduct(ctx, "42", "7") })
  • Store 统一 Get/Set/Delete,返回字节切片;业务可用 JSON 编解码。
  • 内存缓存使用 LRU,同时限制条目数和 key/value 总字节数;字节上限不包含 Go 对象开销。 读写复制切片,过期时懒清理,没有每个条目一个 goroutine。
  • TTL 为零表示不过期,负值拒绝;缺失返回 cache.ErrMiss。删除不存在的 key 成功。
  • Loader 合并同进程、同 key的并发回源,最多同时运行 MaxLoads 个不同 key 的加载。 单个调用者取消只停止等待;共享加载有独立超时,加载函数必须响应 context。 超限返回 ErrBusy,缓存故障直接返回错误,不自动当成 miss 冲击数据库。
  • 同 key 的调用必须具有相同数据含义、权限范围和 TTL;加载采用首个调用者的 context 值。 多租户 key 必须含租户标识。进程间合并、分布式锁、缓存更新与数据库事务一致性由业务另外设计。

Redis 适配器借用调用方创建的 go-redis 客户端和连接池:

client := goredis.NewClient(&goredis.Options{
    Addr: "127.0.0.1:6379", PoolSize: 16, MaxRetries: -1,
    DialTimeout: time.Second, ReadTimeout: time.Second,
    WriteTimeout: time.Second, PoolTimeout: time.Second,
    ContextTimeoutEnabled: true,
})
// 启动时先使用有截止时间的 context 执行 client.Ping(ctx)。
store, err := rediscache.New(client, "billing:") // cache/redis 的导入别名
if err != nil { _ = client.Close(); return err }
application.OnShutdown(client.Close)
// store 可直接传入 cache.NewLoader。

连接失败与 Redis 错误保持原样;只有 Redis nil 映射为 ErrMiss。Redis TTL 精度为毫秒, 正的亚毫秒 TTL 提升到 1ms。适配器不负责关闭借用的客户端,Redis 服务端容量策略由部署配置。 连接池和超时配置参考 go-redis 官方生产使用说明。

出站 HTTP:httpc

client, err := httpc.New(httpc.Config{
    Timeout: 3*time.Second, MaxRetries: 2, MaxConnsPerHost: 32,
    MaxResponseBytes: 1 << 20,
})
if err != nil { return err }
application.OnShutdown(client.Close)
err = client.JSON(ctx, "GET", serviceURL, nil, &result)

客户端应在服务启动时创建并复用。默认使用独立的 net/http.Transport 连接池,每个目标主机 有连接上限;该上限不是整个进程的全局连接数上限。也可以注入自有 http.RoundTripper, 其连接限制和生命周期由调用方负责。这样出站实现同样可以替换。

总超时覆盖建连、重试、等待和读取响应体。Do 返回后调用方必须读取/关闭 response.Body; JSON 自动关闭,并限制解码前读取的字节数。Do 允许流式读取,本身不限制响应体大小。 Close 拒绝新请求、关闭自有池的空闲连接;在途请求应先排空。

MaxRetries 默认 0,最多 5。封装只对可重放的 GET/HEAD/OPTIONS 重试网络错误及 429/502/503/504;POST/PUT/PATCH/DELETE 不做应用层自动重试。标准库 Transport 和借用的 RoundTripper 自身的重放规则仍生效,不能据此承诺“只执行一次”。退避带抖动,尊重 Retry-After;服务器要求等待超过 MaxBackoff 时直接返回该响应,不提前重试。 重定向交给业务处理,3xx 不会自动跟随。

默认从 context 传递 X-Request-ID,日志只包含方法、主机、状态、尝试次数和收到响应头的耗时, 不记录完整 URL、查询参数、凭证和响应体。非 2xx 的 JSON 调用返回只带状态码的 StatusError。

请求保护:middleware

limiter, err := middleware.NewRateLimiter(middleware.RateLimitConfig{Rate: 20, Burst: 40})
if err != nil { return err }
idempotency, err := middleware.NewIdempotency(middleware.IdempotencyConfig{
    Scope: func(c *httpx.Context) (string, error) {
        principal, _ := auth.FromContext(c)
        return principal.UserID, nil // 多租户应用应组合租户和用户 ID
    },
})
if err != nil { return err }
api := application.Group("/api", middleware.Timeout(3*time.Second),
    auth.RequireAuth(authenticator), limiter.Middleware())
api.POST("/orders", createOrder, idempotency.Middleware())

限流为进程内令牌桶,Rate 为每秒额度,Burst 为突发容量。默认按已解析的可信客户端 IP, 可用 Key 按已认证身份分桶。额度不足返回 429 + Retry-After;key 数超过 MaxKeys 返回 503。满容量时不会踢掉活跃桶让攻击者绕过限流;空闲桶经过完整补充周期后才允许回收。

Timeout 设置协作式 deadline,不额外启动 Handler goroutine。业务及下游调用必须响应 context; 超时后尚未提交响应时返回 504,已经提交的响应保持原状态。它不能强制中断任意 Go 代码, 也不能替代网络层读写超时。不要在业务代码中异步持有 httpx.Context。

幂等保护适用于小型、非流式写接口:

  • 必须先认证/鉴权,再执行幂等中间件;Scope 必须取自服务端验证过的身份。 GET/HEAD/OPTIONS 直接通过,写请求要求 Idempotency-Key,长度 1..128 个非空格 ASCII 字符。
  • 同 Scope + key + 方法/URL/Content-Type/body 重放首次响应;内容改变返回 409。 正在执行或结果不可重放的相同请求也返回 409,不并发执行第二次。
  • 默认最多 32 个处理中请求、256 条记录、16 MiB 保存的响应头/body,单次请求体 1 MiB、 响应 body 64 KiB。请求超限 413,容量满 503,响应超限 500。
  • panic、5xx、超时、响应超限、Set-Cookie、Cache-Control:no-store 或保存容量不足时保留失败记录, 阻止在 TTL 内自动重执行业务。业务可能已经提交,调用方应查询实际结果后处理。
  • 完成记录默认保留 10 分钟;在途记录不会因 TTL 到期而允许重复执行。 重放保留原响应 body、原 requestId 与原 X-Request-ID,增加 Idempotency-Replayed:true; 本次访问日志仍使用本次请求 ID。
  • 记录仅在当前进程有效,重启、TTL 到期和多副本切换后均无法保证去重。支付、库存等关键写入 应在数据库事务内配合唯一业务键。该实现不提供跨进程 exactly-once,也不支持 SSE/流式/升级连接。

后台执行:task

jobs, err := task.New(task.Config{Workers: 4, QueueSize: 128, TaskTimeout: 30*time.Second})
if err != nil { return err }
application.OnShutdown(jobs.Close) // 在注册数据库/缓存关闭之后注册,先排空任务
future, err := jobs.Submit(serviceContext, "refreshCatalog", refreshCatalog)
if err != nil { return err } // 满队列立即返回 ErrQueueFull
err = future.Wait(ctx)       // 可选;不等待时,任务失败仍通过 OnError/logx 报告
err = jobs.Every("refreshCatalog", time.Minute, refreshCatalog)

固定 worker 和有界队列控制资源。任务保留提交 context;需要独立于 HTTP 请求执行时使用服务 context,或明确使用 context.WithoutCancel 保留日志关联值。任务仍受 Runner 的超时/退出控制。 任务函数与 OnError 回调应及时返回,尤其要响应取消;panic 被转为失败,worker 继续工作。

Every 使用固定间隔,第一次在一个间隔后执行;同一计划上轮尚未完成、排队或队列满时跳过。 默认最多 128 个计划,名称不可重复。Stats 提供 active/queued/completed/failed/skipped。 它是进程内任务执行器,没有任务持久化、失败自动重试、cron 表达式、跨副本选主或崩溃恢复。

Shutdown(ctx) 停止接收和调度,排空已接受任务;截止时间到后取消任务并及时返回。 忽略取消的任务不能被 Go 强杀,可能继续运行。Close 默认给排空 10 秒。

增加业务模块:go-omni add

go build -o /tmp/go-omni ./cmd/go-omni
/tmp/go-omni add orderItems --dir /path/to/billing

生成 internal/orderItems/{module.go,model,repository,service,handler,README.md},包含 name/createdAt 示例模型、内存 CRUD、输入校验、错误映射及可注入 Repository。模块名使用 lowerCamelCase。

在现有启动代码中导入模块后调用 orderItems.Register(application, repository, authMiddleware)。 脚手架不自动修改现有路由、main、go.mod 或数据库,已有目录不会覆盖。 内存 Repository 仅为可运行示例:重启丢数据,列表未分页;正式业务应实现自己的持久化 Repository。

验证入口

GO_OMNI_SCAFFOLD_INTEGRATION=1 go test -race ./...
go vet ./...
go build ./...
python3 scripts/verifyModules.py --goproxy https://goproxy.cn,direct

Linux runner 顺序运行两个容器:全量 race/vet/build/独立生成项目,随后真实 Redis 适配验证。 每次最多一个容器,限制 2 CPU、2 GiB(不额外使用 swap)、256 pids;Redis 测试容器禁外网, 只用内部 loopback,不发布端口。Redis 集成二进制为静态非 race,适配器单测在全量 race 中覆盖。 结束清理本轮容器,并记录提交、源文件摘要、镜像 ID、日志摘要和原有容器是否仍运行。

本轮通过结果和原始证据见 业务模块验收。