Skip to content

Commit 5e1cf67

Browse files
committed
feat(maxconns): concurrent in-flight request limiter middleware (go-zero MaxConns, 503 when saturated).
Signed-off-by: 林晨 (Leo Cheng) <chengkelfan@qq.com>
1 parent dc1dedb commit 5e1cf67

3 files changed

Lines changed: 89 additions & 0 deletions

File tree

maxconns.mbt

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Bounding the number of concurrent in-flight requests (← go-zero's `MaxConns` /
2+
// `handler.MaxConnsHandler`): a semaphore of `max` permits. Each admitted HTTP request holds a
3+
// permit for its whole duration and returns it when it finishes; a request that finds no permit
4+
// free is answered `503 Service Unavailable` without reaching the app. This caps the work in flight
5+
// so a burst of connections cannot exhaust the server, complementing the rate limiter (which bounds
6+
// the arrival rate) and the breaker (which sheds on downstream failure).
7+
8+
///|
9+
/// A permit pool of `max` concurrent slots (← go-zero's `syncx.Limit`).
10+
pub struct MaxConns {
11+
max : Int
12+
mut in_flight : Int
13+
}
14+
15+
///|
16+
/// A pool admitting at most `max` requests at once.
17+
pub fn MaxConns::new(max : Int) -> MaxConns {
18+
{ max, in_flight: 0 }
19+
}
20+
21+
///|
22+
/// Take a permit if one is free (`TryBorrow`), returning whether it was taken.
23+
pub fn MaxConns::try_acquire(self : MaxConns) -> Bool {
24+
if self.in_flight < self.max {
25+
self.in_flight = self.in_flight + 1
26+
true
27+
} else {
28+
false
29+
}
30+
}
31+
32+
///|
33+
/// Return a permit taken by `try_acquire` (`Return`).
34+
pub fn MaxConns::release(self : MaxConns) -> Unit {
35+
if self.in_flight > 0 {
36+
self.in_flight = self.in_flight - 1
37+
}
38+
}
39+
40+
///|
41+
/// The number of requests currently holding a permit.
42+
pub fn MaxConns::in_flight(self : MaxConns) -> Int {
43+
self.in_flight
44+
}
45+
46+
///|
47+
/// Max-connections middleware (← go-zero's `MaxConns`): hold a permit for the wrapped app's
48+
/// duration, or answer `503 Service Unavailable` when every permit is taken. Non-HTTP scopes
49+
/// (lifespan, websocket) pass through untouched.
50+
pub fn max_conns(limit : MaxConns) -> Middleware {
51+
inner => {
52+
(scope, receive, send) => {
53+
match scope {
54+
Http(_) =>
55+
if limit.try_acquire() {
56+
inner(scope, receive, send)
57+
limit.release()
58+
} else {
59+
for event in service_unavailable_events() {
60+
send(event)
61+
}
62+
}
63+
_ => inner(scope, receive, send)
64+
}
65+
}
66+
}
67+
}

maxconns_wbtest.mbt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// The concurrent-request limiter (← go-zero's MaxConns).
2+
3+
///|
4+
test "max conns: admits up to the limit, then rejects until a permit frees" {
5+
let m = MaxConns::new(2)
6+
assert_eq(m.try_acquire(), true)
7+
assert_eq(m.try_acquire(), true)
8+
assert_eq(m.in_flight(), 2)
9+
// At the limit, the next request is refused.
10+
assert_eq(m.try_acquire(), false)
11+
// Returning a permit admits one more.
12+
m.release()
13+
assert_eq(m.in_flight(), 1)
14+
assert_eq(m.try_acquire(), true)
15+
assert_eq(m.try_acquire(), false)
16+
// Releasing below zero is a no-op.
17+
m.release()
18+
m.release()
19+
m.release()
20+
assert_eq(m.in_flight(), 0)
21+
}

resilience_wbtest.mbt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ test "resilience middlewares assemble onto an AsgiApp" {
194194
let ck = mc.as_clock()
195195
let assembled = Server::new(ServiceConf::new(), app)
196196
.use_(maxbytes(1024))
197+
.use_(max_conns(MaxConns::new(100)))
197198
.use_(rate_limit(TokenBucket::new(10.0, now=0L), ck))
198199
.use_(breaker(Breaker::new(), ck))
199200
.use_(timeout(2000L, ck))

0 commit comments

Comments
 (0)