Skip to content

Commit b021491

Browse files
committed
feat: add middleware support and related benchmarks; refactor middleware execution and routing
1 parent 6d08d24 commit b021491

11 files changed

Lines changed: 304 additions & 6 deletions

File tree

benchmarks/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ backend.
77
Routes:
88

99
- `GET /plaintext` returns `Hello, World!`
10+
- `GET /api/plaintext` returns `Hello, World!` for middleware-focused Mocket runs
1011
- `GET /json` returns `{ "message": "Hello, World!" }`
1112
- `GET /echo/:name` returns the path parameter
1213
- `POST /echo` echoes the request body
@@ -29,6 +30,12 @@ Run a subset:
2930
python3 benchmarks/run.py --prepare mocket nodejs bun deno gin axum
3031
```
3132

33+
Run the middleware-index benchmark:
34+
35+
```sh
36+
python3 benchmarks/run.py --prepare mocket-middleware --routes plaintext api_plaintext --duration 10 --connections 100
37+
```
38+
3239
The runner uses `oha` when it is installed. If `oha` is not available, it falls
3340
back to `autocannon` or `npx -y autocannon`. Raw JSON results are written under
3441
`benchmarks/results/`.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
///|
2+
async fn main {
3+
let port = benchmark_port()
4+
let app = @mocket_lib.new()
5+
6+
app.use_middleware(noop_middleware())
7+
for i = 0; i < 1000; i = i + 1 {
8+
app.use_middleware(noop_middleware(), base_path="/x\{i}")
9+
}
10+
app.use_middleware(noop_middleware(), base_path="/api")
11+
for i = 0; i < 1000; i = i + 1 {
12+
app.use_middleware(noop_middleware(), base_path="/api/x\{i}")
13+
}
14+
15+
app
16+
..get("/plaintext", _ => "Hello, World!")
17+
..get("/api/plaintext", _ => "Hello, World!")
18+
..get("/json", _ => ({ "message": "Hello, World!" } : Json))
19+
..get("/echo/:name", event => event.params.get("name").unwrap_or("World"))
20+
.post("/echo", event => event.req)
21+
22+
app.listen("0.0.0.0:\{port}")
23+
}
24+
25+
///|
26+
fn noop_middleware() -> @mocket_lib.Middleware {
27+
(_event, next) => next()
28+
}
29+
30+
///|
31+
fn benchmark_port() -> Int {
32+
match @env.get_env_var("PORT") {
33+
Some(port_text) => {
34+
let port : Int = @strconv.from_str(port_text) catch { _ => 3000 }
35+
port
36+
}
37+
None => 3000
38+
}
39+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import {
2+
"moonbitlang/async",
3+
"moonbitlang/core/env",
4+
"moonbitlang/core/json",
5+
"moonbitlang/core/strconv",
6+
"oboard/mocket" @mocket_lib,
7+
}
8+
9+
supported_targets = "+native"
10+
11+
options(
12+
"is-main": true,
13+
)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Generated using `moon info`, DON'T EDIT IT
2+
package "oboard/mocket/benchmarks/mocket_middleware"
3+
4+
// Values
5+
6+
// Errors
7+
8+
// Types and methods
9+
10+
// Type aliases
11+
12+
// Traits
13+

benchmarks/run.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
ROUTES = {
1818
"plaintext": "/plaintext",
19+
"api_plaintext": "/api/plaintext",
1920
"json": "/json",
2021
"echo": "/echo/moonbit",
2122
}
@@ -36,6 +37,21 @@ class Target:
3637
["moon", "run", "--release", "--target", "native", "benchmarks/mocket"],
3738
(("moon", "build", "--release", "--target", "native", "benchmarks/mocket"),),
3839
),
40+
"mocket-middleware": Target(
41+
"mocket-middleware",
42+
ROOT,
43+
["moon", "run", "--release", "--target", "native", "benchmarks/mocket_middleware"],
44+
(
45+
(
46+
"moon",
47+
"build",
48+
"--release",
49+
"--target",
50+
"native",
51+
"benchmarks/mocket_middleware",
52+
),
53+
),
54+
),
3955
"nodejs": Target("nodejs", BENCH_ROOT / "nodejs", ["node", "server.mjs"]),
4056
"bun": Target("bun", BENCH_ROOT / "bun", ["bun", "server.js"]),
4157
"deno": Target(
@@ -90,7 +106,12 @@ def main() -> int:
90106
parser.add_argument("--warmup", type=int, default=2)
91107
parser.add_argument("--connections", type=int, default=100)
92108
parser.add_argument("--port", type=int, default=3000)
93-
parser.add_argument("--routes", nargs="+", choices=sorted(ROUTES), default=list(ROUTES))
109+
parser.add_argument(
110+
"--routes",
111+
nargs="+",
112+
choices=sorted(ROUTES),
113+
default=["plaintext", "json", "echo"],
114+
)
94115
parser.add_argument("--results-dir", type=Path, default=BENCH_ROOT / "results")
95116
args = parser.parse_args()
96117

@@ -139,7 +160,7 @@ def main() -> int:
139160
"target": target.name,
140161
"route": route_name,
141162
"url": url,
142-
"file": str(result_file.relative_to(ROOT)),
163+
"file": display_path(result_file),
143164
})
144165
print(f"{target.name} {route_name}: {result_file}")
145166
except Exception as exc:
@@ -249,5 +270,12 @@ def write_result(results_dir: Path, target: str, route: str, tool: str, output:
249270
return path
250271

251272

273+
def display_path(path: Path) -> str:
274+
try:
275+
return str(path.relative_to(ROOT))
276+
except ValueError:
277+
return str(path)
278+
279+
252280
if __name__ == "__main__":
253281
raise SystemExit(main())

dispatch.mbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub async fn dispatch_http(
1515
res: HttpResponse::new(OK),
1616
params,
1717
}
18-
let responder = execute_middlewares(mocket.middlewares, event, handler)
18+
let responder = mocket.execute_middlewares(event, handler)
1919
responder.options(event.res)
2020
let buf = @buffer.new()
2121
responder.output(buf)

index.mbt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ pub(all) struct Mocket {
1111
base_path : String
1212
mappings : Map[(String, String), HttpHandler]
1313
middlewares : Array[(String, Middleware)]
14+
priv middleware_trie : MiddlewareTrieNode
1415
// 添加静态路由缓存(精确匹配的路由)
1516
static_routes : Map[String, Map[String, HttpHandler]]
1617
// 添加动态路由缓存(包含参数的路由)
@@ -29,6 +30,7 @@ pub fn new(base_path? : String = "") -> Mocket {
2930
base_path,
3031
mappings: {},
3132
middlewares: [],
33+
middleware_trie: new_middleware_trie_node(),
3234
static_routes: {},
3335
dynamic_routes: {},
3436
ws_static_routes: {},
@@ -521,7 +523,10 @@ pub fn Mocket::group(
521523
}
522524
})
523525
// 合并中间件
524-
self.middlewares.append(group.middlewares)
526+
group.middlewares.each(middleware => {
527+
let (base_path, middleware) = middleware
528+
self.use_middleware(middleware, base_path~)
529+
})
525530
}
526531

527532
///|

middleware.mbt

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@ pub type MiddlewareNext = async () -> &Responder noraise
55
// 中间件类型:接受 HttpEvent 和 next 函数,返回 HttpBody
66
pub type Middleware = async (MocketEvent, MiddlewareNext) -> &Responder noraise
77

8+
///|
9+
priv struct MiddlewareTrieEntry {
10+
order : Int
11+
middleware : Middleware
12+
}
13+
14+
///|
15+
priv struct MiddlewareTrieNode {
16+
middlewares : Array[MiddlewareTrieEntry]
17+
children : Map[String, MiddlewareTrieNode]
18+
}
19+
20+
///|
21+
fn new_middleware_trie_node() -> MiddlewareTrieNode {
22+
{ middlewares: [], children: {} }
23+
}
24+
825
///|
926
// 注册中间件,支持路径匹配
1027
pub fn Mocket::use_middleware(
@@ -13,8 +30,105 @@ pub fn Mocket::use_middleware(
1330
base_path? : String,
1431
) -> Unit {
1532
let base_path = base_path.unwrap_or(self.base_path)
16-
// 将中间件和路径信息一起存储
33+
let order = self.middlewares.length()
1734
self.middlewares.push((base_path, middleware))
35+
self.middleware_trie.insert_middleware(base_path, order, middleware)
36+
}
37+
38+
///|
39+
fn MiddlewareTrieNode::insert_middleware(
40+
self : MiddlewareTrieNode,
41+
base_path : String,
42+
order : Int,
43+
middleware : Middleware,
44+
) -> Unit {
45+
let mut node = self
46+
for segment in middleware_path_segments(base_path) {
47+
node = match node.children.get(segment) {
48+
Some(child) => child
49+
None => {
50+
let child = new_middleware_trie_node()
51+
node.children.set(segment, child)
52+
child
53+
}
54+
}
55+
}
56+
node.middlewares.push({ order, middleware })
57+
}
58+
59+
///|
60+
fn middleware_path_segments(path : String) -> Array[String] {
61+
let path = strip_url_suffix(path)
62+
let segments = []
63+
if path == "" || path == "/" {
64+
return segments
65+
}
66+
for segment in path.split("/") {
67+
let segment = segment.to_owned()
68+
if segment != "" {
69+
segments.push(segment)
70+
}
71+
}
72+
segments
73+
}
74+
75+
///|
76+
fn strip_url_suffix(path : String) -> String {
77+
let query_index = path.find("?").unwrap_or(path.length())
78+
let hash_index = path.find("#").unwrap_or(path.length())
79+
let end_index = if query_index < hash_index {
80+
query_index
81+
} else {
82+
hash_index
83+
}
84+
path[0:end_index].to_owned()
85+
}
86+
87+
///|
88+
fn insert_middleware_entry(
89+
entries : Array[MiddlewareTrieEntry],
90+
entry : MiddlewareTrieEntry,
91+
) -> Unit {
92+
let mut index = 0
93+
while index < entries.length() && entries[index].order < entry.order {
94+
index = index + 1
95+
}
96+
entries.insert(index, entry)
97+
}
98+
99+
///|
100+
fn append_sorted_middleware_entries(
101+
entries : Array[MiddlewareTrieEntry],
102+
source : Array[MiddlewareTrieEntry],
103+
) -> Unit {
104+
source.each(entry => insert_middleware_entry(entries, entry))
105+
}
106+
107+
///|
108+
fn Mocket::match_middlewares(self : Mocket, url : String) -> Array[Middleware] {
109+
let entries = []
110+
let mut node = self.middleware_trie
111+
append_sorted_middleware_entries(entries, node.middlewares)
112+
for segment in middleware_path_segments(url) {
113+
match node.children.get(segment) {
114+
Some(child) => {
115+
node = child
116+
append_sorted_middleware_entries(entries, node.middlewares)
117+
}
118+
None => break
119+
}
120+
}
121+
entries.map(entry => entry.middleware)
122+
}
123+
124+
///|
125+
async fn Mocket::execute_middlewares(
126+
self : Mocket,
127+
event : MocketEvent,
128+
final_handler : HttpHandler,
129+
) -> &Responder noraise {
130+
let matched_middlewares = self.match_middlewares(event.req.url)
131+
execute_middleware_chain(matched_middlewares, 0, event, final_handler)
18132
}
19133

20134
///|

0 commit comments

Comments
 (0)