Skip to content

Commit bf3294f

Browse files
authored
Release 3.2.0 (#84)
See CHANGELOG.md [3.2.0].
1 parent 22624f6 commit bf3294f

65 files changed

Lines changed: 4194 additions & 846 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.env.example‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,23 @@ DB_MAX_CONNECTIONS=10
2626
DB_PASSWORD=__SECRET_HEX_16__
2727
# dev: DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@localhost:5432/${DB_NAME}
2828
# prod: DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@postgres:5432/${DB_NAME}?sslmode=disable
29+
# Postgres directly, or a pooler in session mode: never one in transaction
30+
# mode (PgBouncer pool_mode = transaction). Start-up schema setup holds a
31+
# session-level advisory lock (deploy/helm/think-watch/README.md).
2932

3033
# --- Redis ---
3134
REDIS_PASSWORD=__SECRET_HEX_16__
3235
# dev: REDIS_URL=redis://:${REDIS_PASSWORD}@localhost:6379
3336
# prod: REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379
37+
# Redis Cluster: REDIS_URL=redis-cluster://:${REDIS_PASSWORD}@redis-0:6379?node=redis-1:6379
38+
# (see deploy/helm/think-watch/README.md, "Redis Cluster")
39+
# TLS (managed Redis services usually require it): the rediss:// scheme,
40+
# rediss-cluster:// for a cluster. The certificate is checked against the
41+
# public CAs and the host name in the URL.
42+
# REDIS_URL=rediss://:${REDIS_PASSWORD}@my-cache.example.com:6379
43+
# A Redis whose certificate a private CA signed: a PEM file with that CA,
44+
# then trusted alone (see deploy/helm/think-watch/README.md, "Redis over TLS")
45+
# REDIS_CA_CERT=/etc/thinkwatch/redis-ca.crt
3446

3547
# --- Application ---
3648
JWT_SECRET=__SECRET_HEX_32__

‎CHANGELOG.md‎

Lines changed: 153 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,157 @@ target.
1111

1212
## [Unreleased]
1313

14+
## [3.2.0] — 2026-10-05
15+
16+
Redis Cluster and Redis over TLS now work, so managed Redis services can be
17+
used. Usage limits work as documented: token limits refuse, an API key's
18+
limits count on that key, and a refused request counts against nothing.
19+
Server instances can start together against one database, a restart no
20+
longer clears captured bodies older than 30 days, and the Helm network
21+
policy allows the ports the database URLs name. The thinkwatch-core crates
22+
stay at v0.62.0.
23+
24+
### Read before upgrading
25+
26+
- **Rate-limit windows start empty.** Rate-limit counters move to new Redis
27+
keys (one hash per counter, tagged so that Redis Cluster can run them), and
28+
the counts from before the upgrade are not carried over: every window starts
29+
empty and fills from the first request after the upgrade. The old keys
30+
expire by themselves within two window lengths. Users' budget counters are
31+
kept; API keys' are not (see below).
32+
- **Route health starts fresh.** A route's samples, circuit breaker and
33+
lifetime request count move to new keys for the same reason, so every route
34+
starts closed with nothing counted. The old lifetime counters never expire;
35+
`redis-cli --scan --pattern 'route_health:[0-9a-f]*' | xargs redis-cli del`
36+
removes them (the new keys start `route_health:{`).
37+
- **An API key's own budgets start again.** 3.1.0 counted a key's budget on
38+
its owner's counter (`budget:user:<user id>:…`); 3.2.0 counts it on the
39+
key's own (`budget:api_key_lineage:<lineage id>:…`), which starts at zero.
40+
A key with a monthly budget of its own can spend all of it again in the
41+
rest of the month.
42+
- **Limits are looser while the rollout runs.** Pods of 3.1.0 and 3.2.0
43+
count rate limits and route health on different keys, so each sees only
44+
its own version's requests: limits let more through, and circuit breakers
45+
can disagree, until the last 3.1.0 pod is gone.
46+
- **Token limits start refusing.** A `tokens` rate limit never refused a
47+
request before. It now does once its window is full, so a deployment with
48+
token limits will see `429`s where it saw none.
49+
- **A key's limits no longer replace its owner's.** A rate limit or budget set
50+
on an API key used to take the place of the owner's limit for the same
51+
window or period. Both now apply, each on its own counter: a key's limits can
52+
narrow what its owner may do through that key, never widen it. A key given a
53+
higher limit than its owner to give it more room needs the owner's limit
54+
raised instead.
55+
- **`rediss://` connects over TLS.** 3.1.0 connected to a `rediss://` URL
56+
over plain TCP. A `rediss://` URL pointing at a port without TLS now fails
57+
at start: point it at the TLS port, or write `redis://`.
58+
- The certificate must name the host in a subjectAltName. One that names
59+
it only in its CN, which `redis-cli` accepts, is refused.
60+
- Cluster nodes that announce IP addresses their certificates don't name
61+
can't be reached.
62+
- **`budget_unavailable` in dashboards.** With Redis down and
63+
`security.rate_limit_fail_closed` on, a request that has a budget is
64+
refused as `budget_unavailable`, not `rate_limiter_unavailable`: budgets
65+
are checked first.
66+
- **Upgrade with an ordinary rollout.** Earlier versions don't take the lock
67+
that now makes instances set up the schema one at a time, so don't restart
68+
instances of the old version while the first one of this version starts.
69+
- **No transaction-mode pooler in front of Postgres.** Schema setup now holds a
70+
Postgres session-level advisory lock, which a pooler in transaction mode
71+
(PgBouncer `pool_mode = transaction`) can leave held, and every later start
72+
then waits for it: point `DATABASE_URL` at Postgres itself or at a pooler in
73+
session mode (the Helm chart's README has the details).
74+
75+
### Fixed
76+
77+
- **Token limits refuse requests.** A `tokens` rate limit never refused
78+
anything, and stopped counting once a request would have taken it past its
79+
limit. A request is now refused once the window's recorded usage reaches the
80+
limit, and every request's tokens are recorded after it, even past the limit
81+
— a window can overshoot by what was in flight when it filled.
82+
- **Several request limits at once.** With two or more `requests` limits on a
83+
user (per minute and per hour, say), every request that passed was counted
84+
twice, and only one of the limits could refuse; with
85+
`security.rate_limit_fail_closed` on, every request was refused as
86+
`rate_limiter_unavailable`.
87+
- **An API key's limits count on that key.** They were counted on its owner's
88+
counter, which every key of the owner shared, and the usage the console
89+
reads for a key (`/api/admin/limits/api_key/{id}/usage`) was always 0. Each
90+
key now has counters of its own, rate limits and budgets alike, for the
91+
gateway and the MCP gateway, and its usage shows what it used.
92+
- **A refused request counts against nothing.** A request refused by a spent
93+
budget, or by one rate limit after another had passed, was still counted
94+
against the request limits. Budgets are now checked first and every rate
95+
limit in one step, so a refused request leaves every counter as it was.
96+
- **`Retry-After` says when to retry.** A `429` from the gateway's own limits
97+
said `Retry-After: 30` whatever the limit. It now gives the seconds until the
98+
window has room for another request, or until a spent budget's period ends
99+
(the next midnight, Monday or 1st of the month, UTC). A spent budget also
100+
sends `x-should-retry: false`, so the OpenAI and Anthropic SDKs don't retry it
101+
by themselves. The body stays in the caller's API format.
102+
- **Redis Cluster.** The rate-limit, route-health and quota scripts touched
103+
keys of several hash slots, which a Redis Cluster refuses: on a cluster, rate
104+
limits silently stopped applying (or refused every request with
105+
`security.rate_limit_fail_closed`), circuit breakers never tripped, and cache
106+
invalidation reached one node only. Every key a script touches now shares a
107+
hash tag, pattern deletes scan every node, and the Helm chart's README
108+
describes a `redis-cluster://` URL.
109+
- **Redis over TLS.** The server was built without TLS for Redis: a
110+
`rediss://` URL was used as plain TCP, so against a Redis that requires
111+
TLS — ElastiCache with in-transit encryption, Upstash, Azure Cache for
112+
Redis and Redis Cloud among them — the server did not start
113+
(`Failed to connect to Redis: Protocol Error: Expected string.`).
114+
`rediss://` and `rediss-cluster://` URLs now connect over TLS and check
115+
the certificate against the system's CAs, as upstream HTTPS does. For a
116+
Redis whose certificate a private CA signed, `REDIS_CA_CERT` names a PEM
117+
file with that CA, which is then trusted alone; the Helm chart sets it
118+
from a Secret given in `redis.caSecret`. The chart's README describes
119+
both.
120+
- **Several instances starting at once.** Server instances starting together
121+
against one database — a Helm `replicaCount` above 1, a rolling upgrade, an
122+
autoscaler adding pods — applied the schema side by side, and all but one
123+
could exit with `Database migration failed: apply db/schema.sql: … deadlock
124+
detected` (on an empty database: `duplicate key value violates unique
125+
constraint "pg_extension_name_index"`). With ClickHouse, the rollups that an
126+
instance fills from the logs when it finds them empty (`cost_rollup_hourly`,
127+
`provider_health_5m`, `mcp_server_call_counts`) could be filled by each of
128+
them, counting every request once per instance on the cost pages, the
129+
dashboard and the MCP server list. Instances now set up Postgres and
130+
ClickHouse one at a time, under Postgres advisory locks: the others wait,
131+
logging `Another instance is setting up the database schema; waiting for it
132+
to finish`, then find it done. An instance that dies holding a lock releases
133+
it with its connection. The Helm chart's startup probe allows a pod 125 s
134+
to start instead of 35 s, set in `startupProbe` (the chart's README says
135+
when to raise it), and an attempt to reach a ClickHouse that doesn't answer
136+
gives up after 5 s instead of the system's TCP timeout.
137+
- **Captured bodies kept as long as configured.** With ClickHouse and
138+
`audit.body_retention_days` above 30, every server start could clear the
139+
captured request and response bodies older than 30 days
140+
(`gateway_logs.request_body` / `response_body`, `mcp_logs.tool_arguments`
141+
/ `tool_result`; the rows themselves stayed). The start-up table setup set
142+
those columns' TTL to 30 days each time, and ClickHouse applies a TTL to
143+
the data already stored as soon as it is set, before the server put the
144+
configured TTL back a moment later. The setup now gives these columns a
145+
TTL only when it creates them, so a restart leaves the configured one in
146+
place. Bodies already cleared cannot be recovered. The log tables' own
147+
TTLs (`data.retention_days_*`) were not affected.
148+
- **Helm network policy and databases on other ports.** With
149+
`networkPolicy.enabled`, the server could reach PostgreSQL only on `5432`,
150+
Redis on `6379` and ClickHouse on `8123`, whatever their `externalUrl` said,
151+
so a database on another port was blocked — Azure Cache for Redis over TLS
152+
(`6380`), a managed Postgres or a ClickHouse on a port of its own: the
153+
server could not start, or started without writing to ClickHouse. The
154+
allowed ports now follow `postgres.externalUrl`, `redis.externalUrl` and
155+
`clickhouse.externalUrl`: every port a URL names, and the client's default
156+
for its scheme where it names none. `networkPolicy.extraEgress` adds egress
157+
rules as written, for ports no URL names (Redis Cluster nodes announcing
158+
other ports, an upstream or MCP server on a port other than `443`). The
159+
chart's README describes both. Port `9000`, ClickHouse's native protocol,
160+
is no longer allowed: the server reaches ClickHouse over plain HTTP only
161+
(an `http://` URL; HTTPS is not supported). An S3 endpoint on `9000`
162+
(RustFS, MinIO) configured outside the chart needs a rule in
163+
`networkPolicy.extraEgress`.
164+
14165
## [3.1.0] — 2026-10-05
15166

16167
The thinkwatch-core crates move from v0.59.0 to v0.62.0. Two changes reach
@@ -1180,7 +1331,8 @@ unreleased builds should: stop the gateway, run `db/schema.sql`
11801331
against PostgreSQL, restart against this tag. The schema is
11811332
idempotent end-to-end, so the apply is safe to repeat.
11821333

1183-
[Unreleased]: https://github.com/ThinkWatchProject/ThinkWatch/compare/v3.1.0...HEAD
1334+
[Unreleased]: https://github.com/ThinkWatchProject/ThinkWatch/compare/v3.2.0...HEAD
1335+
[3.2.0]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.2.0
11841336
[3.1.0]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.1.0
11851337
[3.0.0]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v3.0.0
11861338
[2.2.0]: https://github.com/ThinkWatchProject/ThinkWatch/releases/tag/v2.2.0

‎Cargo.lock‎

Lines changed: 14 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ members = [
1010
]
1111

1212
[workspace.package]
13-
version = "3.1.0"
13+
version = "3.2.0"
1414
edition = "2024"
1515
# Pin the MSRV to the first stable rustc that ships edition 2024 (1.85,
1616
# released 2025-02-20). Without this, contributors on older toolchains
@@ -90,7 +90,14 @@ rust_decimal = { version = "1", features = ["serde", "serde-with-str"] }
9090
clickhouse = { version = "0.13", features = ["time", "chrono"] }
9191

9292
# Redis
93-
fred = { version = "10", features = ["subscriber-client", "i-scripts"] }
93+
# `enable-rustls` gives fred its rustls connector, for `rediss://` URLs.
94+
# The connector itself is built in `think_watch_common::redis_config`
95+
# with the same TLS stack the HTTP client uses (below): rustls on
96+
# aws-lc-rs, the platform's roots through rustls-platform-verifier. No
97+
# OpenSSL anywhere, so the static musl image needs nothing new.
98+
fred = { version = "10", features = ["subscriber-client", "i-scripts", "enable-rustls"] }
99+
rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs", "std", "tls12"] }
100+
rustls-platform-verifier = "0.6"
94101

95102
# Auth
96103
# Crypto-critical crates are pinned with `=` so a silent minor-release

‎README.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ The gateway (port `3000`) is the only part that clients need to reach. The conso
9292
- A model's maximum output tokens, set on the Models page, caps `max_tokens` on every request to that model; it replaces the old output length guardrail.
9393

9494
**Limits and budgets**
95-
- Request-count limits are checked before the request; token limits and budgets are counted after the response, so one request can cross a budget before the next is refused.
95+
- Every limit and budget is checked before the request, against what earlier requests used; tokens are counted after the response, so one request can cross a token limit or a budget before the next is refused. A refused request counts against nothing, and its `429` says in `Retry-After` when the limit frees.
96+
- A limit set on an API key applies on top of its owner's, on a counter of its own.
97+
- Redis can be a single node or a Redis Cluster.
9698
- If Redis is unavailable, limits fail open by default. Setting `security.rate_limit_fail_closed` refuses requests instead.
9799
- Budget alerts fire once per period at 50%, 80%, 95% and 100%.
98100

‎README.zh-CN.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,9 @@ cd web && pnpm install && pnpm dev
9292
- 模型的「最大输出 token」在模型页设置,限制发往该模型的每个请求的 `max_tokens`,取代原来的输出长度护栏。
9393

9494
**限流与预算**
95-
- 请求数限制在请求发出前检查;Token 限制与预算在响应返回后计入,因此单个请求可能越过预算,此后的请求才会被拒绝。
95+
- 所有限流与预算都在请求发出前按此前的用量检查;Token 在响应返回后计入,因此单个请求可能越过 Token 限制或预算,此后的请求才会被拒绝。被拒绝的请求不计入任何限制,其 `429` 响应以 `Retry-After` 说明限制何时解除。
96+
- 设置在 API Key 上的限制叠加在其所属用户的限制之上,单独计数。
97+
- Redis 可以是单节点,也可以是 Redis Cluster。
9698
- Redis 不可用时,限流默认放行;设置 `security.rate_limit_fail_closed` 后改为拒绝请求。
9799
- 预算提醒在每个周期内于 50%、80%、95% 和 100% 各触发一次。
98100

0 commit comments

Comments
 (0)