Skip to content

Tags: qor5/x

Tags

v3.3.1

Toggle v3.3.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
feat(gormx): add TemplatePool — one container, one migration, a priva…

…te database per test (#633)

OpenContainer has no reuse: a package with N suites that each call it pays N
container starts and N migration runs. That is linear, and it is dominated
entirely by container startup — measured in theplant/iam, ~3600ms to start a
container against ~360ms for a full 33-migration run, so the migrations were
never the part worth optimising. 38 suites there meant 38 Postgres containers
and ~152s of pure setup, and because the containers were never terminated
mid-run, 40 of them ended up resident and Docker started refusing to hand out
ports.

TemplatePool starts one container, migrates one template database inside it, and
hands each test a clone via CREATE DATABASE ... TEMPLATE. Cloning is a
file-level copy of the finished schema, so it costs tens of milliseconds
(0.02-0.06s per fork in the tests here) and is independent of how many
migrations produced that schema.

    var pool = gormx.NewTemplatePool(nil, migrate)

    func TestSomething(t *testing.T) {
        db := pool.Fork(t)
    }

Isolation is stronger than the shared-database-plus-truncate pattern several
repos use: each test gets its own database, so there is no per-package list of
tables to keep in sync and no way to silently forget one.

**Nothing runs until the first Fork.** The alternative — an eager
StartTemplatePool called from TestMain — pushed the same lazy-init dance into
every consumer, which is what the first draft of this did to theplant/iam:

    var (poolOnce sync.Once; pool *gormx.TemplatePool; poolErr error)
    poolOnce.Do(func() { pool, poolErr = gormx.StartTemplatePool(...) })
    require.NoError(t, poolErr)

That belongs here, once, not in every repo. Owning it also means the constructor
cannot fail, so there is no Must- variant to offer and no error to thread; a
startup failure is reported by Fork to *every* test that asks for a database,
not only to whichever one happened to be first. A package whose tests are all
filtered out now starts no container at all.

Five decisions worth stating, all of them lessons from doing this by hand in
theplant/iam first (theplant/iam#167) rather than guesses:

- **Fork drops the database it created.** This is the one that is easy to miss
  and expensive to miss. Leaving the clones behind makes each *later* test
  slower, because the autovacuum launcher round-robins over every database that
  exists: in iam the last third of the suites ran ~1.8x slower
  (TestSAMLConfig 27.56s -> 49.71s), and the whole optimisation was worth 38s
  instead of 190s until the drop was added. It is in Fork rather than left to
  the caller precisely so no consumer can reproduce that. WITH (FORCE) because a
  test may leave a session behind — a lifecycle that was never stopped, a
  background worker — and the database exists only for that test anyway. A test
  asserts that finished forks are gone.

- **The migrator does not own its connection.** TemplateMigrator receives both a
  live *gorm.DB and the DSN — an external migrator (Atlas, golang-migrate) takes
  the DSN, AutoMigrate and seed code take the DB — and the pool closes that
  connection as soon as the migrator returns. Cloning refuses to run while any
  session is connected to the source, so making the caller responsible for
  closing would hand every consumer the same footgun. Closing is unconditional:
  a failed migration must not leave the template pinned open either.

- **max_connections is raised to 300.** The stock 100 is not a sensible default
  for a type whose purpose is many simultaneous databases each with its own
  pool: iam died with `FATAL: sorry, too many clients already` at -parallel 16.
  It is prepended ahead of the caller's Args so a caller who names the setting
  still wins (postgres takes the last occurrence) — covered by a test. Forks
  also override DefaultDatabaseConfig's maxIdleConns of 20 down to 2: that
  default suits a long-lived service holding one database, not N concurrent
  single-test databases.

- **Fork does not call t.Parallel().** Per-test databases are what makes
  parallelism safe, and iam gained ~13x wall-clock from it, but whether a
  package's tests may actually run concurrently depends on state the pool cannot
  see — t.Setenv panics in a parallel test, package-level fixtures may be
  shared. The capability is documented on the type; the decision stays with the
  caller.

- **Close is optional.** It stops the container promptly and is a no-op for a
  pool that never started; skipping it is not a leak either, since
  testcontainers' reaper removes the container when the binary exits.

Forks are opened through this package's own Open, so they carry the same
tracing and plugins as any other gormx-managed connection rather than a bare
gorm.Open. That is not only for consistency: it is what makes tests run against
the stack production actually gets, and it immediately found a latent bug in
iam's own models that a bare gorm.Open had been hiding.

TestSuite is untouched. Its ResetDB is AutoMigrate-based, which is the wrong
tool for any repo with real migrations — AutoMigrate misses partial indexes,
custom DEFAULT clauses and check constraints — but existing consumers, including
this package's own tests, depend on it.

v3.3.0

Toggle v3.3.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
fix(httpx,gormx): unencrypted-HTTP2 via Protocols, resource caps, and…

… three ltefield tags that reject valid configs (#625)

* fix(httpx): replace deprecated h2c.NewHandler, add body size and connection caps

三处改动,核心是第一处。

1. h2c.NewHandler 已废弃,改用标准库的 http.Server.Protocols

   x/net(本仓库依赖的 v0.55.0 起)里的声明:

       // Deprecated: Set the [http.Server] Protocols field to use
       // unencrypted HTTP/2 instead.
       func NewHandler(h http.Handler, s *http2.Server) http.Handler

   同一个文件还有一条我们一直没照做的警告:h2c.NewHandler 为支持 HTTP/1.1
   Upgrade 模式,会把 h2c 连接的**首个请求整体读入内存**,文档要求用
   http.MaxBytesHandler 包裹——此前并没有。

   标准库实现(net/http/server.go 的 maybeServeUnencryptedHTTP2)只 Peek 24
   字节比对 PRI 前导,仅支持 prior-knowledge 模式,没有这个内存放大面。

   行为差异:依赖 `Upgrade: h2c` 头升级的客户端将静默退回 HTTP/1.1(不报错)。
   Envoy(配 appProtocol=kubernetes.io/h2c 时)和 gRPC 客户端用的都是
   prior-knowledge,不受影响。

   顺带把 HTTP/2 的启用从「由 tls.enabled=false 反推」改成显式声明三个协议位。
   原来的 if/else 结构让「TLS 关闭」隐式蕴含「启用 h2c」,这两件事语义无关。

2. MaxRequestBodySize(新增,0 = 不限)

   经 http.MaxBytesHandler 包在最外层,先于路由与业务 handler 生效。

3. MaxConnections(新增,0 = 不限)

   经 netutil.LimitListener 加在 SetupListener 上。**只防 fd 耗尽,不是并发
   闸门**:HTTP/2 一条连接可承载多个 stream,全局并发 = 连接数 × 每连接 stream
   数。真正的并发上限应由上游(网关 circuit breaker)或 in-flight middleware
   控制。注释和 usage 里都写明了这一点,避免被误当成限流开关。
   另注:超出限制时 Accept 阻塞(连接停在内核 accept queue),不是拒绝。

未加 maxConcurrentStreams:它是 per-connection 的,调小只会让客户端多开连接
绕过去,管不住总并发;在网关后面收紧它更是有害无益(把请求挤成网关侧排队或
更多连接)。Go 默认 250 保持不动。

新增 httpx/server_test.go —— NewServer 此前没有测试文件。覆盖迁移后最需要
守住的行为:h2c(prior-knowledge)仍可用、HTTP/1.1 仍可用、body 上限的三种
情形。go test ./httpx/... ./healthz/... ./netx/... 全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(httpx): spell out that MaxConnections counts connections, not requests

usage 文案原来只写「maximum number of concurrent connections」,读的人很容易
把它当成并发请求上限。HTTP/1.1 下两者数值接近,HTTP/2 下完全脱钩——一条连接
可以多路复用许多并发请求,所以它管不住并发,只防 fd 耗尽。

这个混淆在 review 中被真实地问到了,说明文案不够。现在 usage 里直接写明
「connections, NOT requests」,doc comment 里补上两种协议下的差异,以及应该
用什么来限并发(网关 circuit breaker 或 in-flight middleware)。

* feat(httpx): make maxConcurrentStreams configurable

既然两种模式下 HTTP/2 都启用了,就把每连接的 stream 上限也暴露出来。

单独看它约束不了任何东西——它是 per-connection 的,客户端多开几条连接就绕过去
了。但和 maxConnections 相乘就得到一个**算术上可知**的在途请求硬上限:

    maxConnections × maxConcurrentStreams = 在途请求上限

这是加它的真正理由:不是为了限流(网关的 circuit breaker 才是并发闸门),而是
为了让容量上界从「无法计算」变成「一眼可算」。默认 0 = Go 默认 250,不改变
现有行为。

实现上用 http.Server.HTTP2(Go 1.24 引入的 http.HTTP2Config),不用 x/net 的
http2.Server —— 后者需要配合已废弃的 h2c.NewHandler 或 ConfigureServer 才能生效,
而前者对 TLS 与 h2c 两条路径统一生效。

有一个坑值得记下:Go 1.25 的 http.Server.HTTP2 字段注释仍写着

    // This field does not yet have any effect.
    // See https://go.dev/issue/67813.

**这句已经过时**。读 h2_bundle.go 会发现调用链是通的:configFromServer →
fillNetHTTPServerConfig → fillNetHTTPConfig(conf, srv.HTTP2)。Go 1.26 已删掉
那句注释。为了不让后人重新怀疑这一点,新增的测试直接读服务端 SETTINGS 帧里
通告的 MAX_CONCURRENT_STREAMS 来断言:

    configured value is advertised   设 42 → 通告 42
    zero falls back to Go default    不设   → 通告 250

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(httpx): English comments; pin the compat guarantees that matter in prod

三件事,都是冲着「已有生产项目在引用这个库」来的。

1. 注释改英文

   本仓库其余部分都是英文注释,之前几处中文是不该混进来的。

2. 把版本结论改准确,并给出可复现的证据

   之前写的是「Go 1.25 起 Server.HTTP2 生效」。更准确的说法是:**它从字段落地
   (1.24)那天起就一直生效,是文档注释错了**。

   读服务端 SETTINGS 帧实测,MaxConcurrentStreams: 42 在以下版本全部被如实通告
   (不设时为 250):

     go1.24.1  go1.24.11  go1.25.1  go1.25.6  go1.25.12  go1.26.3

   源码侧对得上:1.24 的 h2_bundle.go 里 configFromServer 就经
   fillNetHTTPServerConfig 消费 h1.HTTP2;1.26 只是把中间那层去掉、直接调
   fillNetHTTPConfig,并顺手删掉了那句过时注释。go.dev/issue/67813。

3. 把两条兼容性保证钉成测试

   迁移 h2c 实现动的是所有 tls.enabled=false 的消费方(也就是绝大多数),所以
   行为差异必须是「已验证」而不是「我认为」:

   TestNewServer_H2CUpgradeFallsBackToHTTP1
     基于 Upgrade 头的 h2c 不再升级——但请求照常被服务。实测旧写法回
     101 Switching Protocols,新写法回 200 OK。是协议降级,不是失败。
     浏览器不用这个模式,Envoy(appProtocol=h2c)和 gRPC 用的都是
     prior-knowledge,不受影响。

   TestNewServer_IdleTimeoutAppliesToH2C
     旧代码显式转发 &http2.Server{IdleTimeout: srv.IdleTimeout},新写法没有这
     一步。实测标准库路径会从 http.Server 继承,两者在同一时刻关掉空闲连接。
     这条不钉住的话,h2c 连接可能会静默地永不超时。

三个新配置项的默认值一律为 0(= 保持既有行为),不给非零默认:合理的上限取决
于服务本身(上传端点可能确实需要几百 MiB)和部署形态(fd ulimit),库无从替
调用方决定。要防「忘了设」应该靠 provisioning 侧强制显式配置,而不是在库里塞
一个会静默掐断生产流量的默认值。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(httpx): cover MaxConnections, reject negative limits — per review

两条 review 意见,都采纳。

1. 负值静默变成「不限」

   三个新配置项都用 `> 0` 判断是否启用,于是 -1 会被当成「不设」而不是报错
   —— 一个明显的配置笔误就这么被吞了。加 validate:"gte=0",与本文件既有的
   validate 用法一致(required / ltefield / required_if)。

2. MaxConnections 没有测试

   补上 TestNewServer_MaxConnections。它必须走真实装配路径:这个上限在
   SetupListener(netutil.LimitListener)里生效,而不是 NewServer,所以测试
   经 lifecycle 拿 listener,而不是像其余用例那样裸 net.Listen。

   断言的是 LimitListener 的实际语义 —— 超限时不 Accept(连接停在内核
   backlog),而不是拒绝:

     第一条连接        正常拿到 200,随后用 keep-alive 占住唯一的槽位
     第二条连接        TCP 握手完成,但拿不到任何响应(读超时)
     关掉第一条之后    排队中的那条立刻被 Accept 并拿到 200

   第三步特意复用已排队的连接而不是新拨一条:槽位释放后先被 Accept 的正是
   backlog 里那条,新拨的会继续排在后面。

go test ./httpx/... ./healthz/... ./netx/... 全绿。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(gormx): default maxOpenConns to 0 (unlimited), and move the idle/open pairing out of validation

## 默认值 200 → 0

200 从来没有真正生效过:单个 pod 不可能有 200 个并发查询,所以它不是上限,
只是一个看起来像经过考量、实际从不 binding 的数字 —— 而消费方会照着它做容量
规划(「200 × 5 副本 = 1000 条,超了 RDS 的 475」),基于一个从未发生的前提。

而池上限本来就不是约束资源占用的正确手段,请求超时才是:超时的请求释放它占着
的连接,前提是 timeout context 一路传到 DB 这一层。设上限只是把队列挪进
database/sql,而那一层是看不见的 —— 没人给 DBStats.WaitCount 打点,症状是延迟
毛刺而不是错误。它还会掩盖真实负载:请求堵在池口而不是堵在数据库上,于是数据库
看着空闲、CPU 低到 HPA 阈值够不到,既不扩容也查不出原因。

不限制时,过载的数据库变慢但不会崩,浪头过去自己恢复;容量真不够就升实例规格
—— 而池上限会让每个消费方都多出一个必须跟着重调的值。

maxIdleConns 保持 20:它不是上限,是保持多少条空闲连接不关,避免稳定流量反复
付重连成本。

## 连带必须改的:MaxIdleConns 的 ltefield

MaxIdleConns 原本带 `validate:"ltefield=MaxOpenConns"`。0 表示 unlimited 而不是
零,所以拿它当上界比较是错的 —— 实测 `idle=20, open=0` 直接校验失败。也就是说
只改默认值会让所有消费方启动即崩。

改成在 Open() 入口检查,且只在真的配了上限时才检查(MaxOpenConns > 0)。放在
拨号之前,配置错误应当以自己的面目出现,而不是藏在连接错误后面。

ConnMaxIdleTime 的 ltefield 保留 —— 那里 0 没有特殊含义。

新增 TestMaxIdleConnsAgainstCap 钉住三种组合;TestConfig 补一条 20/0 的用例,
它正是本次改动的前提。gormx 全部测试通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(gormx,httpx): move the three "0 means unlimited" pairings out of struct tags

三个字段用 `ltefield` 表达「不得超过另一个字段」,而那三个「另一个字段」取 0 时
的含义都是**不限制**,不是零。于是完全合理的配置会校验失败、服务起不来:

  gormx  MaxIdleConns      ltefield=MaxOpenConns     (20, 0)     → 失败
  gormx  ConnMaxIdleTime   ltefield=ConnMaxLifetime  (10m, 0)    → 失败
  httpx  ReadHeaderTimeout ltefield=ReadTimeout      (10s, 0)    → 失败

三条都实测复现过。httpx 那条最容易踩:它没有默认值文件,而「只设一个 header
超时」是很自然的最小加固。

## 为什么不是换个 tag 写法

go-playground/validator 没有「目标字段为零就跳过」的内置 tag(`omitzero` 跳的是
**当前**字段),只能自定义。但自定义 tag 要注册进校验器实例,而实例由应用侧
构造,库注册不进去。

更根本的是位置不对:「0 = 不限制」这个语义是由下面那几行 `if conf.X > 0` 定义
的,tag 层看不见。把配对检查放到构造函数里,和定义语义的代码在一起,才是它该
待的地方。三处统一放在入口、拨号/监听之前 —— 配置错误应当以自己的面目出现,
而不是藏在连接错误后面。

## 顺带

httpx.MaxConcurrentStreams 的注释去掉了「网关已经用 maxParallelRequests 限了
并发」这个前提 —— 那个做法已不再推荐。

新增 TestConnMaxIdleTimeAgainstLifetime 与 TestReadHeaderTimeoutAgainstReadTimeout,
各钉三种组合(有上限且合规 / 有上限且越界 / 无上限)。httpx + gormx 全部通过。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs(httpx): MaxConnections 默认 0 才是常态,说清它两头都保护不到

原注释把它说成「防 fd 耗尽」,并让人去用网关的 circuitBreaker 兜并发。两句都要改:

  · 它触顶时 netutil.LimitListener 停止 Accept,连接堆在内核 backlog 里,
    客户端等到自己超时 —— 不记日志、不拒绝,是一条看不见的队列。
  · 这个数两头都站不住:低到能约束单连接内存时,离进程的 fd 上限还差几个
    数量级,所以既没防住 fd 也没防住内存。
  · 网关侧的并发闸门已不再推荐(见 theplant/mad-provisioning#123),
    不该再把它当成配套方案写在这里。

真正约束资源占用的是上面那几个超时。改成「默认 0 通常就是对的,除非你确实
需要一个硬性连接上限、且拿得出依据」。

* docs(gormx,httpx): state the mechanics, not a recommendation

之前在这两个包的注释里写了「0 才是推荐值」「池上限不是约束资源的手段,请求超时
才是」之类的话。那是消费方的取舍,不该由共享库替所有人拍板 —— 同一个库的不同
使用者完全可能有不同结论。

只留可验证的事实:

  gormx.MaxOpenConns   0 = unlimited,与 database/sql 自身默认一致;超过上限时
                       调用方阻塞在 sql.DB 内部,且只能通过 DBStats.WaitCount
                       看到
  gormx.MaxIdleConns   pool 里保留的空闲连接数,超出的在归还时关闭;它不限制
                       能开多少连接
  httpx.MaxConnections 触顶后 netutil.LimitListener 停止 Accept,后续连接在内核
                       backlog 里等到客户端放弃:不记日志、不拒绝

默认值本身(maxOpenConns 200 → 0)不变,那是上一个 commit 的事,理由在那条
commit message 里 —— 200 从来没有真正生效过。

* fix(gormx,httpx): use confx's stop_if instead of hand-checking in the constructors

三处跨字段比较的右手边取 0 表示「不限制」,`ltefield` 拿它当上界是错的。上一轮
把配对检查挪进了构造函数当权宜之计,现在 qor5/confx#21 提供了 stop_if,改回
tag 写法:

    MaxIdleConns      validate:"stop_if=MaxOpenConns 0,ltefield=MaxOpenConns"
    ConnMaxIdleTime   validate:"stop_if=ConnMaxLifetime 0,ltefield=ConnMaxLifetime"
    ReadHeaderTimeout validate:"stop_if=ReadTimeout 0,ltefield=ReadTimeout"

stop_if 命中时让该字段后续的 tag 短路,它自己的错误由 confx 按 tag 名滤掉。

比手写检查好在三点:回到配置校验阶段(confx 的 ValidationSuite 抓得到,而不是
等到 Open()/NewServer() 才炸)、错误是结构化的(path + tag)、三处写法与其余
校验一致。gormx.Open 与 httpx.NewServer 里那两段手写检查随之删除。

测试同步改回 confx.ValidationSuite。已反证:把 stop_if 从 tag 里去掉,
「无上限 + 热池 (20, 0)」与「只设 header 超时 (10s, 0)」两个合法配置立刻被拒
——正是这个改动要解决的。

⚠️ go.mod 暂时把 confx 指向 qor5/confx#21 的分支 commit。该 PR 合并发版后
需要 bump 成正式版本。

* chore: bump confx to the released stop_if / stop_unless

qor5/confx#21 已合入 main(8d9c78b),go.mod 从分支 commit 换成正式的
pseudo-version v0.0.0-20260810031108-8d9c78bbd3fb。

gormx + httpx 全部测试通过。反证依旧成立:把三处 tag 里的 stop_if 去掉,
「无上限 + 热池 (20, 0)」与「只设 header 超时 (10s, 0)」两个合法配置立刻被拒。

* docs(httpx): translate the test comments added by this PR to English

本 PR 在 server_test.go 里加的 9 行注释是中文,与仓库其余部分不一致,翻掉。
只动本 PR 自己加的部分。

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

v3.2.0

Toggle v3.2.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Refactor error handling for NoticeError using errors.As (#528)

Updated error type assertions in builder.go and flash.go to use errors.As for NoticeError, improving error handling robustness. Added import for github.com/pkg/errors in flash.go. Updated go.mod to move dependencies for go-sqlite3 and rs/xid from indirect to direct.

v3.1.2

Toggle v3.1.2's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #510 from qor5/more-x

Refactor database dialector initialization to use postgresx

v3.1.1

Toggle v3.1.1's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #509 from qor5/more-x

Update go.mod to remove direct dependency on github.com/jjeffery/errors

v3.1.0

Toggle v3.1.0's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #507 from qor5/more-x

Add gobusx package for bus setup and migration

v3.0.13

Toggle v3.0.13's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #345 from qor5/newtag

upgrade web to v3.0.11

v3.0.12

Toggle v3.0.12's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #330 from qor5/feat-ui-reborn

feat: support https://theplanttokyo.atlassian.net/browse/QOR5-389

v3.0.11

Toggle v3.0.11's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #314 from qor5/new-tag

upgrade web to v3.0.10

v3.0.10

Toggle v3.0.10's commit message

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
Merge pull request #305 from qor5/newtag

upgrade web to v3.0.9