@tishlang/tish 1.6.0 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.toml +2 -0
- package/README.md +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/error.rs +2 -8
- package/crates/js_to_tish/src/transform/expr.rs +128 -137
- package/crates/js_to_tish/src/transform/stmt.rs +62 -32
- package/crates/tish/Cargo.toml +15 -5
- package/crates/tish/src/cargo_native_registry.rs +29 -0
- package/crates/tish/src/cli_help.rs +92 -39
- package/crates/tish/src/main.rs +172 -86
- package/crates/tish/src/repl_completion.rs +3 -3
- package/crates/tish/tests/cargo_example_compile.rs +4 -2
- package/crates/tish/tests/integration_test.rs +216 -54
- package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
- package/crates/tish/tests/shortcircuit.rs +20 -5
- package/crates/tish_ast/src/ast.rs +92 -23
- package/crates/tish_build_utils/Cargo.toml +4 -0
- package/crates/tish_build_utils/src/lib.rs +136 -8
- package/crates/tish_builtins/Cargo.toml +5 -1
- package/crates/tish_builtins/src/array.rs +65 -33
- package/crates/tish_builtins/src/construct.rs +34 -39
- package/crates/tish_builtins/src/globals.rs +42 -26
- package/crates/tish_builtins/src/helpers.rs +2 -1
- package/crates/tish_builtins/src/lib.rs +5 -5
- package/crates/tish_builtins/src/math.rs +5 -3
- package/crates/tish_builtins/src/object.rs +3 -2
- package/crates/tish_builtins/src/string.rs +144 -22
- package/crates/tish_bytecode/src/chunk.rs +0 -1
- package/crates/tish_bytecode/src/compiler.rs +173 -71
- package/crates/tish_bytecode/src/opcode.rs +24 -6
- package/crates/tish_bytecode/src/peephole.rs +2 -2
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +1621 -453
- package/crates/tish_compile/src/infer.rs +75 -19
- package/crates/tish_compile/src/lib.rs +19 -8
- package/crates/tish_compile/src/resolve.rs +278 -137
- package/crates/tish_compile/src/types.rs +184 -24
- package/crates/tish_compile_js/Cargo.toml +1 -0
- package/crates/tish_compile_js/src/codegen.rs +181 -37
- package/crates/tish_compile_js/src/lib.rs +3 -1
- package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
- package/crates/tish_compiler_wasm/src/lib.rs +16 -13
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +69 -59
- package/crates/tish_core/Cargo.toml +8 -0
- package/crates/tish_core/src/json.rs +107 -56
- package/crates/tish_core/src/lib.rs +4 -2
- package/crates/tish_core/src/macros.rs +5 -5
- package/crates/tish_core/src/uri.rs +9 -6
- package/crates/tish_core/src/value.rs +145 -43
- package/crates/tish_core/src/vmref.rs +178 -0
- package/crates/tish_cranelift/src/link.rs +6 -9
- package/crates/tish_cranelift/src/lower.rs +14 -8
- package/crates/tish_eval/Cargo.toml +17 -2
- package/crates/tish_eval/src/eval.rs +474 -165
- package/crates/tish_eval/src/http.rs +61 -0
- package/crates/tish_eval/src/lib.rs +12 -8
- package/crates/tish_eval/src/natives.rs +136 -38
- package/crates/tish_eval/src/promise.rs +14 -8
- package/crates/tish_eval/src/timers.rs +28 -19
- package/crates/tish_eval/src/value.rs +17 -6
- package/crates/tish_eval/src/value_convert.rs +13 -5
- package/crates/tish_fmt/src/lib.rs +149 -43
- package/crates/tish_lexer/src/lib.rs +232 -63
- package/crates/tish_lexer/src/token.rs +10 -6
- package/crates/tish_llvm/src/lib.rs +17 -8
- package/crates/tish_lsp/Cargo.toml +4 -1
- package/crates/tish_lsp/README.md +1 -1
- package/crates/tish_lsp/src/builtin_goto.rs +261 -0
- package/crates/tish_lsp/src/import_goto.rs +549 -0
- package/crates/tish_lsp/src/main.rs +504 -106
- package/crates/tish_native/src/build.rs +4 -8
- package/crates/tish_native/src/lib.rs +54 -21
- package/crates/tish_opt/src/lib.rs +84 -52
- package/crates/tish_parser/src/lib.rs +45 -13
- package/crates/tish_parser/src/parser.rs +505 -130
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3436 -0
- package/crates/tish_resolve/src/pos.rs +133 -0
- package/crates/tish_runtime/Cargo.toml +68 -3
- package/crates/tish_runtime/src/http.rs +1136 -145
- package/crates/tish_runtime/src/http_fetch.rs +38 -27
- package/crates/tish_runtime/src/http_hyper.rs +418 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +375 -189
- package/crates/tish_runtime/src/promise.rs +199 -40
- package/crates/tish_runtime/src/promise_io.rs +2 -1
- package/crates/tish_runtime/src/timers.rs +37 -1
- package/crates/tish_runtime/src/ws.rs +65 -42
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
- package/crates/tish_ui/src/jsx.rs +317 -27
- package/crates/tish_ui/src/lib.rs +5 -2
- package/crates/tish_ui/src/runtime/hooks.rs +406 -45
- package/crates/tish_ui/src/runtime/mod.rs +36 -9
- package/crates/tish_vm/Cargo.toml +15 -5
- package/crates/tish_vm/src/vm.rs +725 -281
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +11 -4
- package/crates/tish_wasm/src/lib.rs +55 -42
- package/crates/tish_wasm_runtime/Cargo.toml +2 -1
- package/crates/tish_wasm_runtime/src/lib.rs +1 -1
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +120 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +350 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
- package/justfile +8 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//! Web Fetch–aligned Response, ReadableStream, reader.read(), text()/json().
|
|
2
2
|
|
|
3
3
|
use std::cell::RefCell;
|
|
4
|
+
use tishlang_core::VmRef;
|
|
4
5
|
use std::pin::Pin;
|
|
5
6
|
use std::rc::Rc;
|
|
6
7
|
use std::sync::{Arc, Mutex};
|
|
@@ -35,7 +36,11 @@ impl TishPromise for FetchResponsePromise {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
struct FetchAllResponsesPromise {
|
|
38
|
-
rx: Mutex<
|
|
39
|
+
rx: Mutex<
|
|
40
|
+
Option<
|
|
41
|
+
tokio::sync::oneshot::Receiver<Result<Vec<Result<reqwest::Response, String>>, String>>,
|
|
42
|
+
>,
|
|
43
|
+
>,
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
impl TishPromise for FetchAllResponsesPromise {
|
|
@@ -47,9 +52,12 @@ impl TishPromise for FetchAllResponsesPromise {
|
|
|
47
52
|
Ok(Ok(vec)) => {
|
|
48
53
|
let out: Vec<Value> = vec
|
|
49
54
|
.into_iter()
|
|
50
|
-
.map(|x|
|
|
55
|
+
.map(|x| {
|
|
56
|
+
x.map(response_value_from_reqwest)
|
|
57
|
+
.unwrap_or_else(|e| build_error_response(&e))
|
|
58
|
+
})
|
|
51
59
|
.collect();
|
|
52
|
-
Ok(Value::Array(
|
|
60
|
+
Ok(Value::Array(VmRef::new(out)))
|
|
53
61
|
}
|
|
54
62
|
Ok(Err(e)) => Ok(build_error_response(&e)),
|
|
55
63
|
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
@@ -79,22 +87,19 @@ impl TishPromise for ReadChunkPromise {
|
|
|
79
87
|
let mut o = ObjectMap::default();
|
|
80
88
|
o.insert(Arc::from("done"), Value::Bool(true));
|
|
81
89
|
o.insert(Arc::from("value"), Value::Null);
|
|
82
|
-
Ok(Value::Object(
|
|
90
|
+
Ok(Value::Object(VmRef::new(o)))
|
|
83
91
|
}
|
|
84
92
|
Ok(Ok(ReadChunk::Bytes(b))) => {
|
|
85
93
|
let arr: Vec<Value> = b.iter().map(|u| Value::Number(*u as f64)).collect();
|
|
86
94
|
let mut o = ObjectMap::default();
|
|
87
95
|
o.insert(Arc::from("done"), Value::Bool(false));
|
|
88
|
-
o.insert(
|
|
89
|
-
|
|
90
|
-
Value::Array(Rc::new(RefCell::new(arr))),
|
|
91
|
-
);
|
|
92
|
-
Ok(Value::Object(Rc::new(RefCell::new(o))))
|
|
96
|
+
o.insert(Arc::from("value"), Value::Array(VmRef::new(arr)));
|
|
97
|
+
Ok(Value::Object(VmRef::new(o)))
|
|
93
98
|
}
|
|
94
99
|
Ok(Err(e)) => Err({
|
|
95
100
|
let mut obj = ObjectMap::default();
|
|
96
101
|
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
97
|
-
Value::Object(
|
|
102
|
+
Value::Object(VmRef::new(obj))
|
|
98
103
|
}),
|
|
99
104
|
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
100
105
|
}
|
|
@@ -119,13 +124,13 @@ impl TishPromise for JsonTextPromise {
|
|
|
119
124
|
Err(e) => Err({
|
|
120
125
|
let mut obj = ObjectMap::default();
|
|
121
126
|
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
122
|
-
Value::Object(
|
|
127
|
+
Value::Object(VmRef::new(obj))
|
|
123
128
|
}),
|
|
124
129
|
},
|
|
125
130
|
Ok(Err(e)) => Err({
|
|
126
131
|
let mut obj = ObjectMap::default();
|
|
127
132
|
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
128
|
-
Value::Object(
|
|
133
|
+
Value::Object(VmRef::new(obj))
|
|
129
134
|
}),
|
|
130
135
|
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
131
136
|
}
|
|
@@ -160,18 +165,23 @@ impl HttpBody {
|
|
|
160
165
|
let mut g = self.state.lock().unwrap();
|
|
161
166
|
match &mut *g {
|
|
162
167
|
BodyState::Fresh(r) => {
|
|
163
|
-
let resp = r
|
|
168
|
+
let resp = r
|
|
169
|
+
.take()
|
|
170
|
+
.ok_or_else(|| "Response body already consumed".to_string())?;
|
|
164
171
|
*g = BodyState::ReadInProgress;
|
|
165
172
|
Ok(Box::pin(resp.bytes_stream()))
|
|
166
173
|
}
|
|
167
|
-
BodyState::ReadInProgress =>
|
|
174
|
+
BodyState::ReadInProgress => {
|
|
175
|
+
Err("ReadableStream is locked; getReader() already called".into())
|
|
176
|
+
}
|
|
168
177
|
BodyState::Gone => Err("Response body already consumed".into()),
|
|
169
178
|
}
|
|
170
179
|
}
|
|
171
180
|
|
|
172
181
|
pub fn take_text_async(
|
|
173
182
|
&self,
|
|
174
|
-
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String, String>> + Send + '_>>
|
|
183
|
+
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String, String>> + Send + '_>>
|
|
184
|
+
{
|
|
175
185
|
let resp = {
|
|
176
186
|
let mut g = self.state.lock().unwrap();
|
|
177
187
|
match &mut *g {
|
|
@@ -182,9 +192,9 @@ impl HttpBody {
|
|
|
182
192
|
}
|
|
183
193
|
None => Err("Response body already consumed".into()),
|
|
184
194
|
},
|
|
185
|
-
BodyState::ReadInProgress =>
|
|
186
|
-
"Cannot call text(): body is locked by ReadableStreamDefaultReader".into()
|
|
187
|
-
|
|
195
|
+
BodyState::ReadInProgress => {
|
|
196
|
+
Err("Cannot call text(): body is locked by ReadableStreamDefaultReader".into())
|
|
197
|
+
}
|
|
188
198
|
BodyState::Gone => Err("Response body already consumed".into()),
|
|
189
199
|
}
|
|
190
200
|
};
|
|
@@ -220,7 +230,7 @@ impl TishOpaque for HttpReadableStream {
|
|
|
220
230
|
return None;
|
|
221
231
|
}
|
|
222
232
|
let body = Arc::clone(&self.body);
|
|
223
|
-
Some(
|
|
233
|
+
Some(Arc::new(move |_args: &[Value]| match body.take_stream() {
|
|
224
234
|
Ok(stream) => {
|
|
225
235
|
let inner = Arc::new(tokio::sync::Mutex::new(StreamSlot { stream }));
|
|
226
236
|
Value::Opaque(Arc::new(HttpStreamReader {
|
|
@@ -231,7 +241,7 @@ impl TishOpaque for HttpReadableStream {
|
|
|
231
241
|
Err(e) => {
|
|
232
242
|
let mut m = ObjectMap::default();
|
|
233
243
|
m.insert(Arc::from("error"), Value::String(e.into()));
|
|
234
|
-
Value::Object(
|
|
244
|
+
Value::Object(VmRef::new(m))
|
|
235
245
|
}
|
|
236
246
|
}))
|
|
237
247
|
}
|
|
@@ -261,7 +271,7 @@ impl TishOpaque for HttpStreamReader {
|
|
|
261
271
|
}
|
|
262
272
|
let inner = Arc::clone(&self.inner);
|
|
263
273
|
let body = Arc::clone(&self.body);
|
|
264
|
-
Some(
|
|
274
|
+
Some(Arc::new(move |_args: &[Value]| {
|
|
265
275
|
let inner = Arc::clone(&inner);
|
|
266
276
|
let body = Arc::clone(&body);
|
|
267
277
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
@@ -296,7 +306,7 @@ fn headers_to_value(headers: &reqwest::header::HeaderMap) -> Value {
|
|
|
296
306
|
headers_obj.insert(Arc::from(key.as_str()), Value::String(v.into()));
|
|
297
307
|
}
|
|
298
308
|
}
|
|
299
|
-
Value::Object(
|
|
309
|
+
Value::Object(VmRef::new(headers_obj))
|
|
300
310
|
}
|
|
301
311
|
|
|
302
312
|
pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
|
|
@@ -310,7 +320,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
|
|
|
310
320
|
let body_stream_val = Value::Opaque(stream);
|
|
311
321
|
let bh_text = Arc::clone(&body_holder);
|
|
312
322
|
let bh_json = Arc::clone(&body_holder);
|
|
313
|
-
let text_fn: NativeFn =
|
|
323
|
+
let text_fn: NativeFn = Arc::new(move |_args: &[Value]| {
|
|
314
324
|
let bh = Arc::clone(&bh_text);
|
|
315
325
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
316
326
|
crate::http::RUNTIME.with(|rt| {
|
|
@@ -321,7 +331,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
|
|
|
321
331
|
});
|
|
322
332
|
crate::promise_io::string_result_promise(rx)
|
|
323
333
|
});
|
|
324
|
-
let json_fn: NativeFn =
|
|
334
|
+
let json_fn: NativeFn = Arc::new(move |_args: &[Value]| {
|
|
325
335
|
let bh = Arc::clone(&bh_json);
|
|
326
336
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
327
337
|
crate::http::RUNTIME.with(|rt| {
|
|
@@ -341,7 +351,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
|
|
|
341
351
|
obj.insert(Arc::from("body"), body_stream_val);
|
|
342
352
|
obj.insert(Arc::from("text"), Value::Function(text_fn));
|
|
343
353
|
obj.insert(Arc::from("json"), Value::Function(json_fn));
|
|
344
|
-
Value::Object(
|
|
354
|
+
Value::Object(VmRef::new(obj))
|
|
345
355
|
}
|
|
346
356
|
|
|
347
357
|
async fn send_request_parts(
|
|
@@ -420,7 +430,8 @@ pub fn fetch_all_promise_from_args(args: Vec<Value>) -> Value {
|
|
|
420
430
|
Some(u) => (u, Some(req.clone())),
|
|
421
431
|
None => {
|
|
422
432
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
423
|
-
let _ =
|
|
433
|
+
let _ =
|
|
434
|
+
tx.send(Err("Each request object must have a 'url' property".into()));
|
|
424
435
|
return Value::Promise(Arc::new(FetchAllResponsesPromise {
|
|
425
436
|
rx: Mutex::new(Some(rx)),
|
|
426
437
|
}));
|
|
@@ -430,7 +441,7 @@ pub fn fetch_all_promise_from_args(args: Vec<Value>) -> Value {
|
|
|
430
441
|
_ => {
|
|
431
442
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
432
443
|
let _ = tx.send(Err(
|
|
433
|
-
"Each request must be a string URL or request object".into()
|
|
444
|
+
"Each request must be a string URL or request object".into()
|
|
434
445
|
));
|
|
435
446
|
return Value::Promise(Arc::new(FetchAllResponsesPromise {
|
|
436
447
|
rx: Mutex::new(Some(rx)),
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
//! `hyper`-based HTTP backend for `tish:http`.
|
|
2
|
+
//!
|
|
3
|
+
//! Selectable at runtime via `TISH_HTTP_BACKEND=hyper` when the
|
|
4
|
+
//! `http-hyper` feature is compiled in. When unset, `serve` falls back to
|
|
5
|
+
//! the `tiny_http` path in [`crate::http`].
|
|
6
|
+
//!
|
|
7
|
+
//! ## Architecture
|
|
8
|
+
//!
|
|
9
|
+
//! ```text
|
|
10
|
+
//! N OS threads (one per CPU, pinned via core_affinity)
|
|
11
|
+
//! ├─ single-threaded tokio runtime per thread
|
|
12
|
+
//! ├─ SO_REUSEPORT-bound TcpListener per thread
|
|
13
|
+
//! └─ hyper HTTP/1.1 + HTTP/2 server
|
|
14
|
+
//! │
|
|
15
|
+
//! ├─ async per-connection state machine (no OS thread)
|
|
16
|
+
//! ├─ static-route fast path (lock-free ArcSwap<HashMap> from
|
|
17
|
+
//! │ [`crate::http::register_static_route`])
|
|
18
|
+
//! └─ VM-dispatch slow path: crosses mpsc to VM thread, awaits
|
|
19
|
+
//! oneshot reply, writes response
|
|
20
|
+
//! ```
|
|
21
|
+
//!
|
|
22
|
+
//! ## Why this is broadly useful (beyond the bench)
|
|
23
|
+
//!
|
|
24
|
+
//! * Removes tiny_http's thread-per-connection model (the bottleneck on
|
|
25
|
+
//! every macOS/Linux Tish HTTP server at >50k concurrent connections).
|
|
26
|
+
//! * Gives HTTP/2 + TLS-ready surface for free (hyper handles the state
|
|
27
|
+
//! machine; we only have to convert our `RequestPrimitive` to/from
|
|
28
|
+
//! `http::Request` / `http::Response`).
|
|
29
|
+
//! * Shared async context with `reqwest`, `tokio-postgres`, `tokio` in
|
|
30
|
+
//! general — the tokio runtime is reused for client fetch, db, and
|
|
31
|
+
//! server accept.
|
|
32
|
+
//!
|
|
33
|
+
//! ## Integration with the Tish VM
|
|
34
|
+
//!
|
|
35
|
+
//! The Tish handler returns a synchronous `Value`. We call it from inside
|
|
36
|
+
//! a tokio task via `tokio::task::spawn_blocking`, which:
|
|
37
|
+
//! * detaches onto tokio's blocking thread pool,
|
|
38
|
+
//! * lets `tish-pg`'s `block_on` detect no ambient runtime and block
|
|
39
|
+
//! directly (no extra thread spawn per query),
|
|
40
|
+
//! * unblocks hyper's reactor to serve other connections while the VM
|
|
41
|
+
//! runs.
|
|
42
|
+
//!
|
|
43
|
+
//! Multi-VM (one Tish VM per worker thread) is the **next** step layered
|
|
44
|
+
//! on top of this file — see [`WorkerHandler`] below and the `onWorker`
|
|
45
|
+
//! semantics in [`crate::http::serve`]. For now the VM stays single-threaded
|
|
46
|
+
//! and we share it via the mpsc-dispatch pattern; adding per-core VMs is
|
|
47
|
+
//! a drop-in replacement of `dispatch_to_vm` with a per-worker handler
|
|
48
|
+
//! closure.
|
|
49
|
+
|
|
50
|
+
use std::convert::Infallible;
|
|
51
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
52
|
+
use std::sync::{mpsc as std_mpsc, Arc};
|
|
53
|
+
use std::thread;
|
|
54
|
+
|
|
55
|
+
use http_body_util::Full;
|
|
56
|
+
use hyper::body::Bytes;
|
|
57
|
+
use hyper::server::conn::http1;
|
|
58
|
+
use hyper::service::service_fn;
|
|
59
|
+
use hyper::{Request, Response, StatusCode};
|
|
60
|
+
use hyper_util::rt::TokioIo;
|
|
61
|
+
|
|
62
|
+
use crate::http::{
|
|
63
|
+
bind_listeners_reuseport, build_error_response_pub, cached_date_header_arc,
|
|
64
|
+
lookup_static_route_pub, num_workers_pub as num_workers, RequestPrimitive, ResponseBody,
|
|
65
|
+
ResponsePrimitive, StaticRouteSnapshot,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/// Bridge from hyper-thread → VM thread and back. Same `Job` shape as the
|
|
69
|
+
/// tiny_http path uses so the VM dispatcher code is identical.
|
|
70
|
+
pub(crate) type Job = (
|
|
71
|
+
RequestPrimitive,
|
|
72
|
+
std_mpsc::SyncSender<ResponsePrimitive>,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
/// Drop-in replacement for [`crate::http::serve`]. Same arg layout, same
|
|
76
|
+
/// return value. Selected at runtime when `TISH_HTTP_BACKEND=hyper`.
|
|
77
|
+
pub fn serve<F>(args: &[tishlang_core::Value], handler: F) -> tishlang_core::Value
|
|
78
|
+
where
|
|
79
|
+
F: Fn(&[tishlang_core::Value]) -> tishlang_core::Value,
|
|
80
|
+
{
|
|
81
|
+
use tishlang_core::Value;
|
|
82
|
+
|
|
83
|
+
let port = match args.first() {
|
|
84
|
+
Some(Value::Number(n)) => *n as u16,
|
|
85
|
+
_ => return build_error_response_pub("serve requires a port number"),
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
let max_requests: Option<usize> = args.get(2).and_then(|v| match v {
|
|
89
|
+
Value::Number(n) if *n >= 1.0 => Some(*n as usize),
|
|
90
|
+
_ => None,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Kick the Date background thread so the first response has a value.
|
|
94
|
+
let _ = cached_date_header_arc();
|
|
95
|
+
|
|
96
|
+
// Prefork: same semantics as tiny_http path — spawn N-1 subprocesses,
|
|
97
|
+
// each runs its own single-threaded tokio runtime + Tish VM.
|
|
98
|
+
let role = crate::http_prefork::role_from_env();
|
|
99
|
+
let prefork_n = crate::http::num_prefork_workers_pub();
|
|
100
|
+
let mut in_prefork_group = false;
|
|
101
|
+
match role {
|
|
102
|
+
crate::http_prefork::PreforkRole::Parent if prefork_n > 1 => {
|
|
103
|
+
match crate::http_prefork::spawn_children(prefork_n) {
|
|
104
|
+
Ok(c) => {
|
|
105
|
+
let _ = crate::http_prefork::install_parent_signal_handler(c);
|
|
106
|
+
in_prefork_group = true;
|
|
107
|
+
}
|
|
108
|
+
Err(e) => {
|
|
109
|
+
eprintln!(
|
|
110
|
+
"[tish http/hyper] prefork spawn failed, running single process: {}",
|
|
111
|
+
e
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
_ => {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let workers = if in_prefork_group { 1 } else { num_workers() };
|
|
120
|
+
let listeners = match bind_listeners_reuseport(port, workers) {
|
|
121
|
+
Ok(l) => l,
|
|
122
|
+
Err(e) => {
|
|
123
|
+
eprintln!("[tish http/hyper] failed to bind: {}", e);
|
|
124
|
+
return build_error_response_pub(&e);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
let worker_id = match role {
|
|
129
|
+
crate::http_prefork::PreforkRole::Child(id) => id,
|
|
130
|
+
_ => 0,
|
|
131
|
+
};
|
|
132
|
+
if matches!(role, crate::http_prefork::PreforkRole::Parent) && in_prefork_group {
|
|
133
|
+
println!(
|
|
134
|
+
"tish http/hyper: listening on http://0.0.0.0:{} ({} process{} x {} accept thread{})",
|
|
135
|
+
port,
|
|
136
|
+
prefork_n,
|
|
137
|
+
if prefork_n == 1 { "" } else { "es" },
|
|
138
|
+
workers,
|
|
139
|
+
if workers == 1 { "" } else { "s" }
|
|
140
|
+
);
|
|
141
|
+
} else if worker_id == 0 {
|
|
142
|
+
println!(
|
|
143
|
+
"tish http/hyper: listening on http://0.0.0.0:{} ({} worker{})",
|
|
144
|
+
port,
|
|
145
|
+
workers,
|
|
146
|
+
if workers == 1 { "" } else { "s" }
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Shared job queue (same shape as tiny_http path).
|
|
151
|
+
let (tx, rx) = std_mpsc::sync_channel::<Job>(workers * 512);
|
|
152
|
+
let stop = Arc::new(AtomicBool::new(false));
|
|
153
|
+
|
|
154
|
+
// Spawn the HTTP worker threads. Each owns a single-threaded tokio
|
|
155
|
+
// runtime and a SO_REUSEPORT-bound listener. Optionally pins to a
|
|
156
|
+
// physical core via core_affinity (opt-in via env).
|
|
157
|
+
let core_ids: Vec<Option<core_affinity::CoreId>> = {
|
|
158
|
+
let pin = std::env::var("TISH_HTTP_PIN_CORES")
|
|
159
|
+
.map(|v| v != "0" && v != "false")
|
|
160
|
+
.unwrap_or(false);
|
|
161
|
+
if pin {
|
|
162
|
+
core_affinity::get_core_ids()
|
|
163
|
+
.unwrap_or_default()
|
|
164
|
+
.into_iter()
|
|
165
|
+
.map(Some)
|
|
166
|
+
.collect()
|
|
167
|
+
} else {
|
|
168
|
+
(0..workers).map(|_| None).collect()
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
let mut worker_handles = Vec::with_capacity(workers);
|
|
173
|
+
for (idx, listener) in listeners.into_iter().enumerate() {
|
|
174
|
+
let tx = tx.clone();
|
|
175
|
+
let stop = Arc::clone(&stop);
|
|
176
|
+
let core = core_ids.get(idx).copied().flatten();
|
|
177
|
+
let handle = thread::Builder::new()
|
|
178
|
+
.name(format!("tish-http-h{}", idx))
|
|
179
|
+
.spawn(move || worker_thread(listener, tx, stop, core))
|
|
180
|
+
.expect("spawn tish-http-hyper worker");
|
|
181
|
+
worker_handles.push(handle);
|
|
182
|
+
}
|
|
183
|
+
drop(tx);
|
|
184
|
+
|
|
185
|
+
// VM-thread dispatcher loop: identical to the tiny_http path.
|
|
186
|
+
let mut count = 0usize;
|
|
187
|
+
while let Ok((req_prim, resp_tx)) = rx.recv() {
|
|
188
|
+
let req_value = req_prim.into_value_pub();
|
|
189
|
+
let response_value = handler(&[req_value]);
|
|
190
|
+
let resp_prim = ResponsePrimitive::from_value_pub(&response_value);
|
|
191
|
+
let _ = resp_tx.send(resp_prim);
|
|
192
|
+
|
|
193
|
+
count += 1;
|
|
194
|
+
if max_requests.map(|m| count >= m).unwrap_or(false) {
|
|
195
|
+
stop.store(true, Ordering::Relaxed);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
stop.store(true, Ordering::Relaxed);
|
|
201
|
+
for h in worker_handles {
|
|
202
|
+
let _ = h.join();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
Value::Null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// Per-OS-thread entry: build a single-threaded tokio runtime, adopt the
|
|
209
|
+
/// pre-bound SO_REUSEPORT listener, serve connections with hyper.
|
|
210
|
+
fn worker_thread(
|
|
211
|
+
listener: std::net::TcpListener,
|
|
212
|
+
dispatch: std_mpsc::SyncSender<Job>,
|
|
213
|
+
stop: Arc<AtomicBool>,
|
|
214
|
+
core: Option<core_affinity::CoreId>,
|
|
215
|
+
) {
|
|
216
|
+
if let Some(id) = core {
|
|
217
|
+
let _ = core_affinity::set_for_current(id);
|
|
218
|
+
}
|
|
219
|
+
let rt = tokio::runtime::Builder::new_current_thread()
|
|
220
|
+
.enable_all()
|
|
221
|
+
.build()
|
|
222
|
+
.expect("tish-http-hyper: build current-thread runtime");
|
|
223
|
+
|
|
224
|
+
rt.block_on(async move {
|
|
225
|
+
listener
|
|
226
|
+
.set_nonblocking(true)
|
|
227
|
+
.expect("tish-http-hyper: set_nonblocking");
|
|
228
|
+
let tokio_listener = tokio::net::TcpListener::from_std(listener)
|
|
229
|
+
.expect("tish-http-hyper: adopt tokio listener");
|
|
230
|
+
|
|
231
|
+
loop {
|
|
232
|
+
if stop.load(Ordering::Relaxed) {
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
// Accept with a short timeout so we notice the stop flag.
|
|
236
|
+
let accept_fut = tokio_listener.accept();
|
|
237
|
+
let (stream, _peer) = match tokio::time::timeout(
|
|
238
|
+
std::time::Duration::from_millis(250),
|
|
239
|
+
accept_fut,
|
|
240
|
+
)
|
|
241
|
+
.await
|
|
242
|
+
{
|
|
243
|
+
Ok(Ok(s)) => s,
|
|
244
|
+
Ok(Err(e)) => {
|
|
245
|
+
eprintln!("[tish http/hyper] accept error: {}", e);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
Err(_) => continue, // timeout; re-check stop flag
|
|
249
|
+
};
|
|
250
|
+
// TCP_NODELAY once, at the socket level.
|
|
251
|
+
let _ = stream.set_nodelay(true);
|
|
252
|
+
|
|
253
|
+
let dispatch = dispatch.clone();
|
|
254
|
+
tokio::task::spawn(async move {
|
|
255
|
+
let io = TokioIo::new(stream);
|
|
256
|
+
let svc = service_fn(move |req: Request<hyper::body::Incoming>| {
|
|
257
|
+
let dispatch = dispatch.clone();
|
|
258
|
+
async move { handle_request(req, dispatch).await }
|
|
259
|
+
});
|
|
260
|
+
if let Err(e) = http1::Builder::new()
|
|
261
|
+
.keep_alive(true)
|
|
262
|
+
.serve_connection(io, svc)
|
|
263
|
+
.await
|
|
264
|
+
{
|
|
265
|
+
// Chatty connection errors (resets, client timeouts) are
|
|
266
|
+
// normal; silently drop unless debug logging is on.
|
|
267
|
+
if std::env::var_os("TISH_HTTP_DEBUG").is_some() {
|
|
268
|
+
eprintln!("[tish http/hyper] conn closed: {}", e);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/// Per-request hyper service. Static-route fast path first; otherwise
|
|
277
|
+
/// dispatch via mpsc to the VM thread.
|
|
278
|
+
async fn handle_request(
|
|
279
|
+
req: Request<hyper::body::Incoming>,
|
|
280
|
+
dispatch: std_mpsc::SyncSender<Job>,
|
|
281
|
+
) -> Result<Response<Full<Bytes>>, Infallible> {
|
|
282
|
+
// Fast path: static routes (pre-baked bodies).
|
|
283
|
+
let uri_path_str = req.uri().path();
|
|
284
|
+
if let Some(route) = lookup_static_route_pub(uri_path_str) {
|
|
285
|
+
return Ok(static_route_response(route));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Slow path: cross the mpsc to the VM thread.
|
|
289
|
+
let (method, url, path, query, headers, body_bytes) = extract_request(req).await;
|
|
290
|
+
let body = String::from_utf8(body_bytes).unwrap_or_default();
|
|
291
|
+
let prim = RequestPrimitive::new_pub(method, url, path, query, headers, body);
|
|
292
|
+
|
|
293
|
+
let (resp_tx, resp_rx) = std_mpsc::sync_channel::<ResponsePrimitive>(1);
|
|
294
|
+
if dispatch.send((prim, resp_tx)).is_err() {
|
|
295
|
+
return Ok(simple_error_response(
|
|
296
|
+
StatusCode::SERVICE_UNAVAILABLE,
|
|
297
|
+
"tish http: dispatch channel closed",
|
|
298
|
+
));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Block on the VM response from a tokio blocking worker so we don't
|
|
302
|
+
// starve hyper's reactor while the Tish handler runs.
|
|
303
|
+
let resp_prim = tokio::task::spawn_blocking(move || {
|
|
304
|
+
resp_rx
|
|
305
|
+
.recv_timeout(std::time::Duration::from_secs(30))
|
|
306
|
+
.ok()
|
|
307
|
+
})
|
|
308
|
+
.await
|
|
309
|
+
.ok()
|
|
310
|
+
.flatten();
|
|
311
|
+
|
|
312
|
+
match resp_prim {
|
|
313
|
+
Some(r) => Ok(primitive_to_hyper(r)),
|
|
314
|
+
None => Ok(simple_error_response(
|
|
315
|
+
StatusCode::GATEWAY_TIMEOUT,
|
|
316
|
+
"tish handler timed out",
|
|
317
|
+
)),
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async fn extract_request(
|
|
322
|
+
req: Request<hyper::body::Incoming>,
|
|
323
|
+
) -> (
|
|
324
|
+
String,
|
|
325
|
+
String,
|
|
326
|
+
String,
|
|
327
|
+
String,
|
|
328
|
+
Vec<(String, String)>,
|
|
329
|
+
Vec<u8>,
|
|
330
|
+
) {
|
|
331
|
+
let method = req.method().to_string();
|
|
332
|
+
let uri = req.uri().clone();
|
|
333
|
+
let url = uri.to_string();
|
|
334
|
+
let path = uri.path().to_string();
|
|
335
|
+
let query = uri.query().unwrap_or("").to_string();
|
|
336
|
+
let headers: Vec<(String, String)> = req
|
|
337
|
+
.headers()
|
|
338
|
+
.iter()
|
|
339
|
+
.map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
|
|
340
|
+
.collect();
|
|
341
|
+
let body_bytes = match http_body_util::BodyExt::collect(req.into_body()).await {
|
|
342
|
+
Ok(c) => c.to_bytes().to_vec(),
|
|
343
|
+
Err(_) => Vec::new(),
|
|
344
|
+
};
|
|
345
|
+
(method, url, path, query, headers, body_bytes)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fn primitive_to_hyper(resp: ResponsePrimitive) -> Response<Full<Bytes>> {
|
|
349
|
+
let (body_bytes, default_ct): (Bytes, Option<&str>) = match resp.body {
|
|
350
|
+
ResponseBody::Text(s) => (Bytes::copy_from_slice(s.as_bytes()), Some("text/plain")),
|
|
351
|
+
ResponseBody::Bytes(b) => (Bytes::from(b), Some("application/octet-stream")),
|
|
352
|
+
ResponseBody::File(p) => match std::fs::read(&p) {
|
|
353
|
+
Ok(b) => (Bytes::from(b), Some("application/octet-stream")),
|
|
354
|
+
Err(_) => (Bytes::from_static(b"file not found"), Some("text/plain")),
|
|
355
|
+
},
|
|
356
|
+
};
|
|
357
|
+
let mut builder = Response::builder().status(resp.status);
|
|
358
|
+
let mut has_ct = false;
|
|
359
|
+
let mut has_server = false;
|
|
360
|
+
let mut has_date = false;
|
|
361
|
+
for (k, v) in &resp.headers {
|
|
362
|
+
if k.eq_ignore_ascii_case("content-type") {
|
|
363
|
+
has_ct = true;
|
|
364
|
+
}
|
|
365
|
+
if k.eq_ignore_ascii_case("server") {
|
|
366
|
+
has_server = true;
|
|
367
|
+
}
|
|
368
|
+
if k.eq_ignore_ascii_case("date") {
|
|
369
|
+
has_date = true;
|
|
370
|
+
}
|
|
371
|
+
builder = builder.header(k, v);
|
|
372
|
+
}
|
|
373
|
+
if !has_ct {
|
|
374
|
+
if let Some(ct) = default_ct {
|
|
375
|
+
builder = builder.header("content-type", ct);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if !has_server {
|
|
379
|
+
builder = builder.header("server", "Tish");
|
|
380
|
+
}
|
|
381
|
+
if !has_date {
|
|
382
|
+
builder = builder.header("date", cached_date_header_arc().as_str());
|
|
383
|
+
}
|
|
384
|
+
builder.body(Full::new(body_bytes)).unwrap_or_else(|_| {
|
|
385
|
+
Response::builder()
|
|
386
|
+
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
|
387
|
+
.body(Full::new(Bytes::new()))
|
|
388
|
+
.unwrap()
|
|
389
|
+
})
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
fn static_route_response(route: StaticRouteSnapshot) -> Response<Full<Bytes>> {
|
|
393
|
+
let body = Bytes::copy_from_slice(&route.body);
|
|
394
|
+
Response::builder()
|
|
395
|
+
.status(200)
|
|
396
|
+
.header("content-type", route.content_type.as_ref())
|
|
397
|
+
.header("server", "Tish")
|
|
398
|
+
.header("date", cached_date_header_arc().as_str())
|
|
399
|
+
.body(Full::new(body))
|
|
400
|
+
.unwrap()
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
fn simple_error_response(status: StatusCode, msg: &str) -> Response<Full<Bytes>> {
|
|
404
|
+
Response::builder()
|
|
405
|
+
.status(status)
|
|
406
|
+
.header("content-type", "text/plain")
|
|
407
|
+
.header("server", "Tish")
|
|
408
|
+
.body(Full::new(Bytes::copy_from_slice(msg.as_bytes())))
|
|
409
|
+
.unwrap()
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/// Marker used by external callers (like the bench) to gate the backend
|
|
413
|
+
/// choice. Returns true when `TISH_HTTP_BACKEND=hyper` is in the env.
|
|
414
|
+
pub fn is_enabled_via_env() -> bool {
|
|
415
|
+
std::env::var("TISH_HTTP_BACKEND")
|
|
416
|
+
.map(|v| v.eq_ignore_ascii_case("hyper"))
|
|
417
|
+
.unwrap_or(false)
|
|
418
|
+
}
|