Skip to content

Commit bf857ce

Browse files
committed
fix: return the max_conns permit on every path, and track the toolchain
max_conns released its permit with a trailing statement after calling the inner app, so a request that raised — or was cancelled — never gave it back. With a limit of N, N such requests wedged the limiter shut and everything after them got 503 forever. It is a `defer` now. discov/maxconns_release_test.mbt drives the middleware with an app that always raises and asserts the permits come back; with the old code the second request is refused instead, so the test fails. Two more places had the same shape and are rewritten the way the compiler's new fragile_catch_all lint asks: consul_socket closes its socket with `defer` (there is no fd finalizer, so a cancelled request leaked it), and breaker records its failure with `errdefer`. Alongside that, the toolchain: StringBuilder::new() is deprecated across 23 sites, moon fmt now writes trailing commas in struct literals, and a raising `fn` needs to be an arrow function. The README's claim of a clean --deny-warn build was false on the current compiler; fixing the code makes it true again rather than softening it. The library's own pkg.generated.mbti is now tracked — it is what consumers read and what makes an interface change visible in review — while the examples' stay out. Verified on moon 0.1.20260827 / moonc v0.10.11: check clean on all four backends, 147 tests at the root and 157 native. Signed-off-by: Leo Cheng (heke1228) <chengkelfan@qq.com>
1 parent 3313632 commit bf857ce

67 files changed

Lines changed: 981 additions & 297 deletions

Some content is hidden

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

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@
33
!.moonignore
44
_build/
55
target/
6+
7+
# example packages regenerate these; only the library interface is tracked
8+
examples/*/pkg.generated.mbti

AGENTS.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
`moonzero` assembles microservices for MoonBit — config, resilience middleware, service discovery, tracing, and a Prometheus endpoint — in the shape of go-zero. The portable library lives at the root; `discov/` holds the native drivers that need real I/O.
2+
3+
# Working here
4+
5+
- `moon fmt` before anything else. CI runs `moon fmt && git diff --exit-code`, so an unformatted file fails the build on its own.
6+
- `moon check --target all --deny-warn` is the gate. Warnings are errors, and all four backends (wasm, wasm-gc, js, native) must pass. Run it again after the first round of fixes: a package whose sources do not compile hides the diagnostics in its own test files, so errors surface in waves.
7+
- `moon test --target all` at the root; `moon test --target native` inside `discov/`.
8+
- `moon info` regenerates `pkg.generated.mbti`. If that file does not change, your edit is not visible to anyone depending on this package, which usually means the refactor was safe. If it does change, read the diff before committing — that is the public interface moving. The examples regenerate their own, which is why those are gitignored.
9+
- CI installs the latest moon on every run, so a toolchain that is behind will disagree with it. Upgrade locally rather than pinning.
10+
11+
# Layout
12+
13+
Root is portable and has no async dependency: `config.mbt`, `yaml.mbt`, the resilience middleware (`breaker.mbt`, `maxconns.mbt`, `limits.mbt`, `retry.mbt`), `metrics.mbt`, `tracing.mbt`, `jwt.mbt`, `discovery.mbt`, and the zrpc client. `discov/` is `supported_targets = "native"` and carries the drivers that speak to real etcd, consul and redis over sockets, plus the file-backed registry. Tests sit beside their subject as `*_wbtest.mbt` at the root and `*_test.mbt` under `discov/`; `examples/NN-topic/` are runnable one-file demos.
14+
15+
# Things worth knowing
16+
17+
- Middleware is async — `Middleware` is `(async App) -> async App` — but the root package deliberately does not depend on `moonbitlang/async`, so it cannot host an `async test`. End-to-end middleware tests belong in `discov/`, which already imports async and runs native-only. `maxconns_release_test.mbt` is the pattern to copy.
18+
- Anything holding a resource across a call into user code must release it with `defer`, not with a trailing statement or a `catch` that re-raises. Both skip cancellation, and the compiler's `fragile_catch_all` lint now says so. `max_conns` leaked a permit exactly this way and wedged the limiter shut at 503.
19+
- The tests against real etcd, consul and redis are gated on `MOON_ETCD_TEST`, `MOON_CONSUL_TEST` and `MOON_REDIS_TEST`; without them the suite silently skips those. CI sets them and starts the containers.
20+
- `ConsulHttp` and `RedisConn` are `pub trait`, which in MoonBit is sealed — only this package can implement them. Consumers therefore cannot build a `ConsulClient` or `RedisClient` of their own; the transports in `discov/` are the only implementations.

breaker.mbt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ pub fn breaker(b : Breaker, clock : Clock) -> Middleware {
163163
match scope {
164164
Http(_) =>
165165
if b.allow(clock.now()) {
166-
let status : Ref[Int] = { val: 200 }
166+
let status : Ref[Int] = { val: 200, }
167167
let observed : @moonasgi.Send = event => {
168168
match event {
169169
HttpResponseStart(status=s, headers~, trailers~) => {
@@ -179,12 +179,8 @@ pub fn breaker(b : Breaker, clock : Clock) -> Middleware {
179179
other => send(other)
180180
}
181181
}
182-
inner(scope, receive, observed) catch {
183-
err => {
184-
b.record_failure(clock.now())
185-
raise err
186-
}
187-
}
182+
errdefer b.record_failure(clock.now())
183+
inner(scope, receive, observed)
188184
if status.val >= 500 {
189185
b.record_failure(clock.now())
190186
} else {

clock.mbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub struct ManualClock {
3333
///|
3434
/// A manual clock starting at `start` milliseconds (default `0`).
3535
pub fn ManualClock::new(start? : Int64 = 0) -> ManualClock {
36-
{ ms: start }
36+
{ ms: start, }
3737
}
3838

3939
///|

config.mbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ fn service_conf_of_object(
7575
Some(_) => raise ConfigError("log_level must be a JSON string")
7676
None => def.log_level
7777
}
78-
{ name, host, port, timeout_ms, log_level }
78+
{ name, host, port, timeout_ms, log_level, }
7979
}
8080

8181
///|

consul.mbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ pub struct ConsulResponse {
2222
///|
2323
/// A consul response.
2424
pub fn ConsulResponse::new(status : Int, body : Bytes) -> ConsulResponse {
25-
{ status, body }
25+
{ status, body, }
2626
}
2727

2828
///|

consul_discovery_wbtest.mbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct FakeConsul {
2222

2323
///|
2424
fn FakeConsul::new() -> FakeConsul {
25-
{ services: Map([]) }
25+
{ services: Map([]), }
2626
}
2727

2828
///|

crypto_wbtest.mbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/// test vectors.
44
fn hex(b : Bytes) -> String {
55
let digits = "0123456789abcdef"
6-
let sb = StringBuilder::new()
6+
let sb = StringBuilder()
77
for i = 0; i < b.length(); i = i + 1 {
88
let v = b[i].to_int()
99
sb.write_char(digits[v >> 4].unsafe_to_char())

discov/consul_socket.mbt

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub struct ConsulSocket {
1818
///|
1919
/// A transport to the consul agent at `host:port` (default port 8500).
2020
pub fn ConsulSocket::new(host : String, port? : Int = 8500) -> ConsulSocket {
21-
{ host, port }
21+
{ host, port, }
2222
}
2323

2424
///|
@@ -40,17 +40,11 @@ pub async fn ConsulSocket::request(
4040
body,
4141
content_type~,
4242
)
43-
let response = try {
44-
(tcp : &@io.Writer).write(request)
45-
let raw = (tcp : &@io.Reader).read_all()
46-
let parsed = @moonzero.http1_parse_response(raw.binary())
47-
@moonzero.ConsulResponse::new(parsed.status(), parsed.body())
48-
} catch {
49-
e => {
50-
tcp.close()
51-
raise e
52-
}
53-
}
54-
tcp.close()
55-
response
43+
// The socket has to close on every path, cancellation included — there is no fd
44+
// finalizer to reclaim it otherwise.
45+
defer tcp.close()
46+
(tcp : &@io.Writer).write(request)
47+
let raw = (tcp : &@io.Reader).read_all()
48+
let parsed = @moonzero.http1_parse_response(raw.binary())
49+
@moonzero.ConsulResponse::new(parsed.status(), parsed.body())
5650
}

discov/discov.mbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub struct FileRegistry {
3838
/// is treated as an empty registry, so a reader can start before the publisher has
3939
/// written anything and pick the state up on the first `reload`/`watch`.
4040
pub async fn FileRegistry::load(path : String) -> FileRegistry {
41-
{ path, reg: read_snapshot(path) }
41+
{ path, reg: read_snapshot(path), }
4242
}
4343

4444
///|

0 commit comments

Comments
 (0)