@tishlang/tish 1.7.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 +1 -0
- package/README.md +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +28 -8
- package/crates/js_to_tish/src/transform/stmt.rs +49 -22
- package/crates/tish/Cargo.toml +15 -5
- package/crates/tish/src/cargo_native_registry.rs +29 -0
- package/crates/tish/src/cli_help.rs +16 -10
- package/crates/tish/src/main.rs +87 -32
- package/crates/tish/src/repl_completion.rs +3 -3
- package/crates/tish/tests/cargo_example_compile.rs +1 -1
- package/crates/tish/tests/integration_test.rs +19 -7
- package/crates/tish/tests/shortcircuit.rs +1 -1
- package/crates/tish_ast/src/ast.rs +80 -9
- package/crates/tish_build_utils/Cargo.toml +4 -0
- package/crates/tish_build_utils/src/lib.rs +105 -2
- package/crates/tish_builtins/Cargo.toml +5 -1
- package/crates/tish_builtins/src/array.rs +13 -12
- package/crates/tish_builtins/src/construct.rs +34 -33
- package/crates/tish_builtins/src/globals.rs +12 -11
- package/crates/tish_builtins/src/helpers.rs +2 -1
- package/crates/tish_builtins/src/object.rs +3 -2
- package/crates/tish_builtins/src/string.rs +73 -3
- package/crates/tish_bytecode/src/compiler.rs +12 -14
- package/crates/tish_bytecode/src/opcode.rs +12 -3
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +745 -199
- package/crates/tish_compile/src/infer.rs +6 -0
- package/crates/tish_compile/src/lib.rs +4 -3
- package/crates/tish_compile/src/resolve.rs +180 -82
- package/crates/tish_compile/src/types.rs +175 -11
- package/crates/tish_compile_js/Cargo.toml +1 -0
- package/crates/tish_compile_js/src/codegen.rs +152 -29
- package/crates/tish_compile_js/src/lib.rs +3 -1
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +31 -12
- package/crates/tish_core/Cargo.toml +8 -0
- package/crates/tish_core/src/json.rs +102 -53
- package/crates/tish_core/src/lib.rs +3 -1
- package/crates/tish_core/src/macros.rs +5 -5
- package/crates/tish_core/src/value.rs +53 -15
- package/crates/tish_core/src/vmref.rs +178 -0
- package/crates/tish_eval/Cargo.toml +17 -2
- package/crates/tish_eval/src/eval.rs +90 -28
- package/crates/tish_eval/src/http.rs +61 -0
- package/crates/tish_eval/src/lib.rs +3 -3
- package/crates/tish_eval/src/natives.rs +41 -0
- package/crates/tish_eval/src/value.rs +7 -3
- package/crates/tish_eval/src/value_convert.rs +13 -5
- package/crates/tish_fmt/src/lib.rs +120 -30
- package/crates/tish_lexer/src/lib.rs +20 -5
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_llvm/src/lib.rs +3 -1
- 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 +502 -102
- package/crates/tish_native/src/build.rs +3 -2
- package/crates/tish_native/src/lib.rs +6 -2
- package/crates/tish_opt/src/lib.rs +17 -2
- package/crates/tish_parser/src/lib.rs +10 -3
- package/crates/tish_parser/src/parser.rs +346 -56
- 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 +1123 -141
- package/crates/tish_runtime/src/http_fetch.rs +15 -14
- 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 +159 -29
- package/crates/tish_runtime/src/promise.rs +199 -36
- 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 +26 -28
- package/crates/tish_ui/src/jsx.rs +279 -8
- 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 +506 -259
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
- package/crates/tish_wasm/src/lib.rs +17 -14
- 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 +1 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
- 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};
|
|
@@ -56,7 +57,7 @@ impl TishPromise for FetchAllResponsesPromise {
|
|
|
56
57
|
.unwrap_or_else(|e| build_error_response(&e))
|
|
57
58
|
})
|
|
58
59
|
.collect();
|
|
59
|
-
Ok(Value::Array(
|
|
60
|
+
Ok(Value::Array(VmRef::new(out)))
|
|
60
61
|
}
|
|
61
62
|
Ok(Err(e)) => Ok(build_error_response(&e)),
|
|
62
63
|
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
@@ -86,19 +87,19 @@ impl TishPromise for ReadChunkPromise {
|
|
|
86
87
|
let mut o = ObjectMap::default();
|
|
87
88
|
o.insert(Arc::from("done"), Value::Bool(true));
|
|
88
89
|
o.insert(Arc::from("value"), Value::Null);
|
|
89
|
-
Ok(Value::Object(
|
|
90
|
+
Ok(Value::Object(VmRef::new(o)))
|
|
90
91
|
}
|
|
91
92
|
Ok(Ok(ReadChunk::Bytes(b))) => {
|
|
92
93
|
let arr: Vec<Value> = b.iter().map(|u| Value::Number(*u as f64)).collect();
|
|
93
94
|
let mut o = ObjectMap::default();
|
|
94
95
|
o.insert(Arc::from("done"), Value::Bool(false));
|
|
95
|
-
o.insert(Arc::from("value"), Value::Array(
|
|
96
|
-
Ok(Value::Object(
|
|
96
|
+
o.insert(Arc::from("value"), Value::Array(VmRef::new(arr)));
|
|
97
|
+
Ok(Value::Object(VmRef::new(o)))
|
|
97
98
|
}
|
|
98
99
|
Ok(Err(e)) => Err({
|
|
99
100
|
let mut obj = ObjectMap::default();
|
|
100
101
|
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
101
|
-
Value::Object(
|
|
102
|
+
Value::Object(VmRef::new(obj))
|
|
102
103
|
}),
|
|
103
104
|
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
104
105
|
}
|
|
@@ -123,13 +124,13 @@ impl TishPromise for JsonTextPromise {
|
|
|
123
124
|
Err(e) => Err({
|
|
124
125
|
let mut obj = ObjectMap::default();
|
|
125
126
|
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
126
|
-
Value::Object(
|
|
127
|
+
Value::Object(VmRef::new(obj))
|
|
127
128
|
}),
|
|
128
129
|
},
|
|
129
130
|
Ok(Err(e)) => Err({
|
|
130
131
|
let mut obj = ObjectMap::default();
|
|
131
132
|
obj.insert(Arc::from("error"), Value::String(e.into()));
|
|
132
|
-
Value::Object(
|
|
133
|
+
Value::Object(VmRef::new(obj))
|
|
133
134
|
}),
|
|
134
135
|
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
135
136
|
}
|
|
@@ -229,7 +230,7 @@ impl TishOpaque for HttpReadableStream {
|
|
|
229
230
|
return None;
|
|
230
231
|
}
|
|
231
232
|
let body = Arc::clone(&self.body);
|
|
232
|
-
Some(
|
|
233
|
+
Some(Arc::new(move |_args: &[Value]| match body.take_stream() {
|
|
233
234
|
Ok(stream) => {
|
|
234
235
|
let inner = Arc::new(tokio::sync::Mutex::new(StreamSlot { stream }));
|
|
235
236
|
Value::Opaque(Arc::new(HttpStreamReader {
|
|
@@ -240,7 +241,7 @@ impl TishOpaque for HttpReadableStream {
|
|
|
240
241
|
Err(e) => {
|
|
241
242
|
let mut m = ObjectMap::default();
|
|
242
243
|
m.insert(Arc::from("error"), Value::String(e.into()));
|
|
243
|
-
Value::Object(
|
|
244
|
+
Value::Object(VmRef::new(m))
|
|
244
245
|
}
|
|
245
246
|
}))
|
|
246
247
|
}
|
|
@@ -270,7 +271,7 @@ impl TishOpaque for HttpStreamReader {
|
|
|
270
271
|
}
|
|
271
272
|
let inner = Arc::clone(&self.inner);
|
|
272
273
|
let body = Arc::clone(&self.body);
|
|
273
|
-
Some(
|
|
274
|
+
Some(Arc::new(move |_args: &[Value]| {
|
|
274
275
|
let inner = Arc::clone(&inner);
|
|
275
276
|
let body = Arc::clone(&body);
|
|
276
277
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
@@ -305,7 +306,7 @@ fn headers_to_value(headers: &reqwest::header::HeaderMap) -> Value {
|
|
|
305
306
|
headers_obj.insert(Arc::from(key.as_str()), Value::String(v.into()));
|
|
306
307
|
}
|
|
307
308
|
}
|
|
308
|
-
Value::Object(
|
|
309
|
+
Value::Object(VmRef::new(headers_obj))
|
|
309
310
|
}
|
|
310
311
|
|
|
311
312
|
pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
|
|
@@ -319,7 +320,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
|
|
|
319
320
|
let body_stream_val = Value::Opaque(stream);
|
|
320
321
|
let bh_text = Arc::clone(&body_holder);
|
|
321
322
|
let bh_json = Arc::clone(&body_holder);
|
|
322
|
-
let text_fn: NativeFn =
|
|
323
|
+
let text_fn: NativeFn = Arc::new(move |_args: &[Value]| {
|
|
323
324
|
let bh = Arc::clone(&bh_text);
|
|
324
325
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
325
326
|
crate::http::RUNTIME.with(|rt| {
|
|
@@ -330,7 +331,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
|
|
|
330
331
|
});
|
|
331
332
|
crate::promise_io::string_result_promise(rx)
|
|
332
333
|
});
|
|
333
|
-
let json_fn: NativeFn =
|
|
334
|
+
let json_fn: NativeFn = Arc::new(move |_args: &[Value]| {
|
|
334
335
|
let bh = Arc::clone(&bh_json);
|
|
335
336
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
336
337
|
crate::http::RUNTIME.with(|rt| {
|
|
@@ -350,7 +351,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
|
|
|
350
351
|
obj.insert(Arc::from("body"), body_stream_val);
|
|
351
352
|
obj.insert(Arc::from("text"), Value::Function(text_fn));
|
|
352
353
|
obj.insert(Arc::from("json"), Value::Function(json_fn));
|
|
353
|
-
Value::Object(
|
|
354
|
+
Value::Object(VmRef::new(obj))
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
async fn send_request_parts(
|
|
@@ -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
|
+
}
|