Files
go-omni/MODULES.md

11 KiB
Raw Permalink Blame History

业务基础模块

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

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

请求追踪与基础指标

omni.New() 的默认链包含 RequestID、访问日志(可配置开关)和 Recovery;无需重复挂载。业务通过 c.Context() 传递 requestId,logx.FromContext(ctx) 取得该 App 的 Logger。手动组装 httpx 链时,也可使用 middleware.RequestID("X-Request-ID")、middleware.AccessLog() 和 middleware.Recovery()。

轻量进程指标直接使用 metrics,无需 OTel collector:

// import omni "git.yuntuo.hk/k2safe/go-omni"
// import "git.yuntuo.hk/k2safe/go-omni/metrics"
app := omni.New()
measurements := metrics.New()
app.Use(measurements.Middleware())
app.GET("/metrics", measurements.Handler())
// 也可以直接读取 measurements.Snapshot()。

记录请求数、失败数、处理中数量、累计耗时与 HTTP 状态分布;返回的是 JSON 快照,并非 Prometheus 文本格式。指标接口是否暴露以及访问权限由项目配置。需要 HTTP/SQL/Redis spans 和标准 OTel exporter 时,使用 telemetry 示例。CORS 由项目中间件或代理配置,当前没有内置 CORS 模块。

缓存: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{
    Proxy: httpc.ProxyConfig{Mode: httpc.ProxyDirect},
    Timeout: 3*time.Second, DialTimeout: time.Second,
    TLSHandshakeTimeout: time.Second, ResponseHeaderTimeout: time.Second,
    MaxConnsPerHost: 32, MaxIdleConnsPerHost: 16, MaxIdleConns: 64,
    IdleConnTimeout: 60*time.Second, MaxResponseBytes: 1<<20,
})
if err != nil { return err }
application.OnShutdown(client.Close)
err = client.JSON(ctx, "GET", serviceURL, nil, &result,
    httpc.WithHeader("Authorization", "Bearer "+token),
)

启动时创建并复用 Client。代理可选环境、直连、固定地址;不配置时保留标准库原代理策略。JSON 支持单次请求头和响应限额,不保存 Client 全局 Token。新阶段超时与空闲连接参数可选,0 沿用默认。

成功响应读取有界;output=nil 流式丢弃,不整体缓存。小错误体只在大小/时间预算内尝试读完以复用连接;未知或大错误体及时关闭。Do 保留标准流式 Request/Response,由调用者限量读取并关闭 body。Close 拒绝新调用并关闭自有池空闲连接,项目先排空在途工作。

自定义 Transport 借用、不修改、不关闭;不能同时指定池/代理/阶段选项。总超时持续覆盖 HTTP 等待、重试和读取 body,但不抢占 JSON 编解码或不遵守 context 的自定义代码。MaxRetries 默认 0,只对可重放 GET/HEAD/OPTIONS 重试;3xx 不跟随。标准库和借用 Transport 自身仍可能重放,不能据此承诺业务只执行一次。

完整参数、NO_PROXY、TLS、安全日志、重试、签名兼容说明见 httpc 指南,可运行 HTTP Client 示例。

请求保护: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、日志摘要和原有容器是否仍运行。

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