@tishlang/tish-format 1.0.12 → 1.0.13
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 +49 -0
- package/LICENSE +13 -0
- package/README.md +138 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/Cargo.toml +11 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +55 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +610 -0
- package/crates/js_to_tish/src/transform/stmt.rs +503 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +54 -0
- package/crates/tish/src/cargo_native_registry.rs +32 -0
- package/crates/tish/src/cli_help.rs +565 -0
- package/crates/tish/src/main.rs +781 -0
- package/crates/tish/src/repl_completion.rs +200 -0
- package/crates/tish/tests/cargo_example_compile.rs +67 -0
- package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
- package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
- package/crates/tish/tests/integration_test.rs +1095 -0
- package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
- package/crates/tish/tests/shortcircuit.rs +65 -0
- package/crates/tish_ast/Cargo.toml +9 -0
- package/crates/tish_ast/src/ast.rs +620 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +11 -0
- package/crates/tish_build_utils/src/lib.rs +577 -0
- package/crates/tish_builtins/Cargo.toml +20 -0
- package/crates/tish_builtins/src/array.rs +441 -0
- package/crates/tish_builtins/src/construct.rs +159 -0
- package/crates/tish_builtins/src/globals.rs +213 -0
- package/crates/tish_builtins/src/helpers.rs +35 -0
- package/crates/tish_builtins/src/lib.rs +16 -0
- package/crates/tish_builtins/src/math.rs +89 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +647 -0
- package/crates/tish_builtins/src/symbol.rs +83 -0
- package/crates/tish_bytecode/Cargo.toml +17 -0
- package/crates/tish_bytecode/src/chunk.rs +96 -0
- package/crates/tish_bytecode/src/compiler.rs +1760 -0
- package/crates/tish_bytecode/src/encoding.rs +100 -0
- package/crates/tish_bytecode/src/lib.rs +19 -0
- package/crates/tish_bytecode/src/opcode.rs +142 -0
- package/crates/tish_bytecode/src/peephole.rs +189 -0
- package/crates/tish_bytecode/src/serialize.rs +163 -0
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +26 -0
- package/crates/tish_compile/src/codegen.rs +5332 -0
- package/crates/tish_compile/src/infer.rs +292 -0
- package/crates/tish_compile/src/lib.rs +164 -0
- package/crates/tish_compile/src/resolve.rs +1388 -0
- package/crates/tish_compile/src/types.rs +501 -0
- package/crates/tish_compile_js/Cargo.toml +18 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +871 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/lib.rs +26 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +350 -0
- package/crates/tish_compiler_wasm/Cargo.toml +21 -0
- package/crates/tish_compiler_wasm/src/lib.rs +57 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
- package/crates/tish_core/Cargo.toml +26 -0
- package/crates/tish_core/src/console_style.rs +160 -0
- package/crates/tish_core/src/json.rs +387 -0
- package/crates/tish_core/src/lib.rs +17 -0
- package/crates/tish_core/src/macros.rs +36 -0
- package/crates/tish_core/src/uri.rs +118 -0
- package/crates/tish_core/src/value.rs +696 -0
- package/crates/tish_core/src/vmref.rs +178 -0
- package/crates/tish_cranelift/Cargo.toml +19 -0
- package/crates/tish_cranelift/src/lib.rs +43 -0
- package/crates/tish_cranelift/src/link.rs +117 -0
- package/crates/tish_cranelift/src/lower.rs +85 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +25 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
- package/crates/tish_eval/Cargo.toml +45 -0
- package/crates/tish_eval/src/eval.rs +3717 -0
- package/crates/tish_eval/src/http.rs +188 -0
- package/crates/tish_eval/src/lib.rs +99 -0
- package/crates/tish_eval/src/natives.rs +399 -0
- package/crates/tish_eval/src/promise.rs +179 -0
- package/crates/tish_eval/src/regex.rs +299 -0
- package/crates/tish_eval/src/timers.rs +120 -0
- package/crates/tish_eval/src/value.rs +318 -0
- package/crates/tish_eval/src/value_convert.rs +111 -0
- package/crates/tish_fmt/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +2101 -0
- package/crates/tish_jsx_web/Cargo.toml +9 -0
- package/crates/tish_jsx_web/README.md +5 -0
- package/crates/tish_jsx_web/src/lib.rs +2 -0
- package/crates/tish_lexer/Cargo.toml +9 -0
- package/crates/tish_lexer/src/lib.rs +716 -0
- package/crates/tish_lexer/src/token.rs +163 -0
- package/crates/tish_lint/Cargo.toml +18 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
- package/crates/tish_lint/src/lib.rs +289 -0
- package/crates/tish_llvm/Cargo.toml +13 -0
- package/crates/tish_llvm/src/lib.rs +115 -0
- package/crates/tish_lsp/Cargo.toml +25 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/builtin_goto.rs +362 -0
- package/crates/tish_lsp/src/import_goto.rs +562 -0
- package/crates/tish_lsp/src/main.rs +1046 -0
- package/crates/tish_native/Cargo.toml +16 -0
- package/crates/tish_native/src/build.rs +427 -0
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +416 -0
- package/crates/tish_opt/Cargo.toml +13 -0
- package/crates/tish_opt/src/lib.rs +943 -0
- package/crates/tish_parser/Cargo.toml +11 -0
- package/crates/tish_parser/src/lib.rs +332 -0
- package/crates/tish_parser/src/parser.rs +2304 -0
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +955 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3561 -0
- package/crates/tish_resolve/src/pos.rs +141 -0
- package/crates/tish_runtime/Cargo.toml +96 -0
- package/crates/tish_runtime/src/http.rs +1298 -0
- package/crates/tish_runtime/src/http_fetch.rs +471 -0
- 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 +1192 -0
- package/crates/tish_runtime/src/native_promise.rs +15 -0
- package/crates/tish_runtime/src/promise.rs +248 -0
- package/crates/tish_runtime/src/promise_io.rs +38 -0
- package/crates/tish_runtime/src/timers.rs +166 -0
- package/crates/tish_runtime/src/ws.rs +761 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +682 -0
- package/crates/tish_ui/src/lib.rs +20 -0
- package/crates/tish_ui/src/runtime/hooks.rs +569 -0
- package/crates/tish_ui/src/runtime/mod.rs +180 -0
- package/crates/tish_vm/Cargo.toml +47 -0
- package/crates/tish_vm/src/lib.rs +39 -0
- package/crates/tish_vm/src/vm.rs +2192 -0
- package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
- package/crates/tish_wasm/Cargo.toml +15 -0
- package/crates/tish_wasm/src/lib.rs +424 -0
- package/crates/tish_wasm_runtime/Cargo.toml +37 -0
- package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
- package/crates/tish_wasm_runtime/src/lib.rs +42 -0
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +263 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
- package/justfile +268 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
|
@@ -0,0 +1,1298 @@
|
|
|
1
|
+
//! HTTP server + shared request parsing. Client `fetch` lives in `http_fetch.rs`.
|
|
2
|
+
//!
|
|
3
|
+
//! ## Concurrency model
|
|
4
|
+
//!
|
|
5
|
+
//! `serve(port, handler)` spawns `num_workers` OS threads (default
|
|
6
|
+
//! `num_cpus`, tuned via env `TISH_HTTP_WORKERS`). Each worker binds its own
|
|
7
|
+
//! accept socket with `SO_REUSEPORT` on Linux so the kernel load-balances
|
|
8
|
+
//! `accept()` across cores; on macOS we still bind N sockets with
|
|
9
|
+
//! `SO_REUSEPORT` (which exists but has different semantics) so each worker
|
|
10
|
+
//! gets its own tiny_http connection pool but the accept queue is shared at
|
|
11
|
+
//! the kernel level.
|
|
12
|
+
//!
|
|
13
|
+
//! Without `send-values`, the handler captures `Value::Function` backed by
|
|
14
|
+
//! `Rc` and is `!Send`, so handler execution stays on a single VM thread:
|
|
15
|
+
//!
|
|
16
|
+
//! ```text
|
|
17
|
+
//! worker 0 ──┐
|
|
18
|
+
//! worker 1 ──┼─> mpsc::SyncSender<Job> ──> VM thread (handler) ──> oneshot
|
|
19
|
+
//! worker N ──┘ │
|
|
20
|
+
//! worker writes response bytes (parallel) <─────────────┘
|
|
21
|
+
//! ```
|
|
22
|
+
//!
|
|
23
|
+
//! Parallel accept + parse + response-write while preserving the single-
|
|
24
|
+
//! threaded VM guarantee. Cached `Date:` header + shared `Arc<str>` response
|
|
25
|
+
//! bodies round out the hot-path optimisations.
|
|
26
|
+
|
|
27
|
+
use std::fs::File;
|
|
28
|
+
use std::io::Write;
|
|
29
|
+
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
|
30
|
+
use std::sync::{Arc, OnceLock};
|
|
31
|
+
|
|
32
|
+
#[cfg(not(feature = "send-values"))]
|
|
33
|
+
use std::collections::VecDeque;
|
|
34
|
+
#[cfg(not(feature = "send-values"))]
|
|
35
|
+
use std::sync::mpsc;
|
|
36
|
+
use std::thread;
|
|
37
|
+
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
|
38
|
+
|
|
39
|
+
use tishlang_core::{ObjectMap, Value};
|
|
40
|
+
use tokio::runtime::Runtime;
|
|
41
|
+
|
|
42
|
+
thread_local! {
|
|
43
|
+
pub(crate) static RUNTIME: Runtime = tokio::runtime::Builder::new_multi_thread()
|
|
44
|
+
.worker_threads(4)
|
|
45
|
+
.enable_all()
|
|
46
|
+
.build()
|
|
47
|
+
.expect("Failed to create tokio runtime");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Block on a future on the HTTP runtime. Uses a dedicated thread to avoid deadlocks when
|
|
51
|
+
/// called from contexts that share or nest tokio runtimes (e.g. WS + HTTP both enabled).
|
|
52
|
+
pub(crate) fn block_on_http<F>(f: F) -> F::Output
|
|
53
|
+
where
|
|
54
|
+
F: std::future::Future + Send,
|
|
55
|
+
F::Output: Send,
|
|
56
|
+
{
|
|
57
|
+
std::thread::scope(|s| {
|
|
58
|
+
let (tx, rx) = std::sync::mpsc::channel();
|
|
59
|
+
s.spawn(move || {
|
|
60
|
+
let out = RUNTIME.with(|rt| rt.block_on(f));
|
|
61
|
+
let _ = tx.send(out);
|
|
62
|
+
});
|
|
63
|
+
rx.recv().expect("block_on_http thread panicked")
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
pub fn await_fetch(args: Vec<Value>) -> Value {
|
|
68
|
+
crate::promise::await_promise(crate::native_promise::fetch_promise(args))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pub fn await_fetch_all(args: Vec<Value>) -> Value {
|
|
72
|
+
crate::promise::await_promise(crate::native_promise::fetch_all_promise(args))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
pub(crate) fn extract_method(options: Option<&Value>) -> String {
|
|
76
|
+
options
|
|
77
|
+
.and_then(|v| match v {
|
|
78
|
+
Value::Object(obj) => obj.borrow().strings.get("method").cloned(),
|
|
79
|
+
_ => None,
|
|
80
|
+
})
|
|
81
|
+
.map(|v| v.to_display_string().to_uppercase())
|
|
82
|
+
.unwrap_or_else(|| "GET".to_string())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pub(crate) fn extract_headers(options: Option<&Value>) -> Vec<(String, String)> {
|
|
86
|
+
options
|
|
87
|
+
.and_then(|v| match v {
|
|
88
|
+
Value::Object(obj) => obj.borrow().strings.get("headers").cloned(),
|
|
89
|
+
_ => None,
|
|
90
|
+
})
|
|
91
|
+
.map(|v| match v {
|
|
92
|
+
Value::Object(obj) => obj
|
|
93
|
+
.borrow()
|
|
94
|
+
.strings
|
|
95
|
+
.iter()
|
|
96
|
+
.map(|(k, v)| (k.to_string(), v.to_display_string()))
|
|
97
|
+
.collect(),
|
|
98
|
+
_ => vec![],
|
|
99
|
+
})
|
|
100
|
+
.unwrap_or_default()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pub(crate) fn extract_body(options: Option<&Value>) -> Option<String> {
|
|
104
|
+
options.and_then(|v| match v {
|
|
105
|
+
Value::Object(obj) => obj
|
|
106
|
+
.borrow()
|
|
107
|
+
.strings
|
|
108
|
+
.get("body")
|
|
109
|
+
.map(|v| v.to_display_string()),
|
|
110
|
+
_ => None,
|
|
111
|
+
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
pub(crate) fn build_error_response(error: &str) -> Value {
|
|
115
|
+
let mut obj: ObjectMap = ObjectMap::with_capacity(2);
|
|
116
|
+
obj.insert(Arc::from("error"), Value::String(error.into()));
|
|
117
|
+
obj.insert(Arc::from("ok"), Value::Bool(false));
|
|
118
|
+
Value::object(obj)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// -------- cached Date header -----------------------------------------------
|
|
122
|
+
//
|
|
123
|
+
// Lock-free via `arc-swap`: readers do `load().clone()` which is a single
|
|
124
|
+
// atomic fetch + ref-count inc. Writers (the 1 Hz background thread) do
|
|
125
|
+
// `store(new Arc)`. No Mutex contention on the 100k+ RPS path.
|
|
126
|
+
|
|
127
|
+
static DATE_HEADER: OnceLock<arc_swap::ArcSwap<String>> = OnceLock::new();
|
|
128
|
+
|
|
129
|
+
fn format_http_date(now_secs: u64) -> String {
|
|
130
|
+
const DAYS: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
131
|
+
const MONTHS: [&str; 12] = [
|
|
132
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
133
|
+
];
|
|
134
|
+
let z = now_secs as i64 / 86_400;
|
|
135
|
+
let secs_of_day = (now_secs as i64).rem_euclid(86_400);
|
|
136
|
+
let h = secs_of_day / 3600;
|
|
137
|
+
let m = (secs_of_day % 3600) / 60;
|
|
138
|
+
let s = secs_of_day % 60;
|
|
139
|
+
let dow = ((z + 4).rem_euclid(7)) as usize;
|
|
140
|
+
let (y, mo, d) = civil_from_days(z);
|
|
141
|
+
format!(
|
|
142
|
+
"{}, {:02} {} {:04} {:02}:{:02}:{:02} GMT",
|
|
143
|
+
DAYS[dow],
|
|
144
|
+
d,
|
|
145
|
+
MONTHS[(mo - 1) as usize],
|
|
146
|
+
y,
|
|
147
|
+
h,
|
|
148
|
+
m,
|
|
149
|
+
s
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn civil_from_days(days: i64) -> (i32, u32, u32) {
|
|
154
|
+
let z = days + 719_468;
|
|
155
|
+
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
|
|
156
|
+
let doe = (z - era * 146_097) as u64;
|
|
157
|
+
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
|
|
158
|
+
let y = yoe as i64 + era * 400;
|
|
159
|
+
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
160
|
+
let mp = (5 * doy + 2) / 153;
|
|
161
|
+
let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
|
|
162
|
+
let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
|
|
163
|
+
let y = if m <= 2 { y + 1 } else { y };
|
|
164
|
+
(y as i32, m, d)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn ensure_date_thread() -> &'static arc_swap::ArcSwap<String> {
|
|
168
|
+
DATE_HEADER.get_or_init(|| {
|
|
169
|
+
let initial = Arc::new(format_http_date(
|
|
170
|
+
SystemTime::now()
|
|
171
|
+
.duration_since(UNIX_EPOCH)
|
|
172
|
+
.map(|d| d.as_secs())
|
|
173
|
+
.unwrap_or(0),
|
|
174
|
+
));
|
|
175
|
+
let slot = arc_swap::ArcSwap::new(initial);
|
|
176
|
+
thread::Builder::new()
|
|
177
|
+
.name("tish-http-date".into())
|
|
178
|
+
.spawn(move || loop {
|
|
179
|
+
thread::sleep(Duration::from_millis(1000));
|
|
180
|
+
let now = SystemTime::now()
|
|
181
|
+
.duration_since(UNIX_EPOCH)
|
|
182
|
+
.map(|d| d.as_secs())
|
|
183
|
+
.unwrap_or(0);
|
|
184
|
+
let next = Arc::new(format_http_date(now));
|
|
185
|
+
if let Some(cell) = DATE_HEADER.get() {
|
|
186
|
+
cell.store(next);
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
.ok();
|
|
190
|
+
slot
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// Lock-free snapshot of the cached Date header. Callers get an `Arc<String>`
|
|
195
|
+
/// clone (single atomic ref-count inc).
|
|
196
|
+
pub fn cached_date_header_arc() -> Arc<String> {
|
|
197
|
+
ensure_date_thread().load_full()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// -------- Send-safe request/response primitives ----------------------------
|
|
201
|
+
|
|
202
|
+
#[derive(Debug, Clone)]
|
|
203
|
+
pub struct RequestPrimitive {
|
|
204
|
+
pub method: String,
|
|
205
|
+
pub url: String,
|
|
206
|
+
pub path: String,
|
|
207
|
+
pub query: String,
|
|
208
|
+
pub headers: Vec<(String, String)>,
|
|
209
|
+
pub body: String,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
impl RequestPrimitive {
|
|
213
|
+
fn from_tiny_http(req: &mut tiny_http::Request) -> Self {
|
|
214
|
+
// Single-pass split of url into (path, query) so we avoid 2 extra
|
|
215
|
+
// String allocations per request (~300 ns at M-series load).
|
|
216
|
+
let url_str = req.url();
|
|
217
|
+
let (path, query) = match url_str.split_once('?') {
|
|
218
|
+
Some((p, q)) => (p.to_string(), q.to_string()),
|
|
219
|
+
None => (url_str.to_string(), String::new()),
|
|
220
|
+
};
|
|
221
|
+
let url = url_str.to_string();
|
|
222
|
+
let method = req.method().to_string();
|
|
223
|
+
// Fast path: GET/HEAD/OPTIONS almost never have a body. Skip the
|
|
224
|
+
// reader allocation + syscall unless the body is advertised.
|
|
225
|
+
let has_body = !matches!(method.as_str(), "GET" | "HEAD" | "OPTIONS")
|
|
226
|
+
&& req.body_length().map(|n| n > 0).unwrap_or(true);
|
|
227
|
+
let mut body = String::new();
|
|
228
|
+
if has_body {
|
|
229
|
+
let _ = req.as_reader().read_to_string(&mut body);
|
|
230
|
+
}
|
|
231
|
+
let headers = req
|
|
232
|
+
.headers()
|
|
233
|
+
.iter()
|
|
234
|
+
.map(|h| {
|
|
235
|
+
(
|
|
236
|
+
h.field.as_str().as_str().to_string(),
|
|
237
|
+
h.value.as_str().to_string(),
|
|
238
|
+
)
|
|
239
|
+
})
|
|
240
|
+
.collect();
|
|
241
|
+
Self {
|
|
242
|
+
method,
|
|
243
|
+
url,
|
|
244
|
+
path,
|
|
245
|
+
query,
|
|
246
|
+
headers,
|
|
247
|
+
body,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fn into_value(self) -> Value {
|
|
252
|
+
// Interned keys: reuse the same Arc<str> across every request object
|
|
253
|
+
// (saves 6 Arc::from allocations per request on the dispatcher hot
|
|
254
|
+
// path). Thread-local because Value / Arc<str> are local-scope.
|
|
255
|
+
thread_local! {
|
|
256
|
+
static KEYS: RequestKeys = RequestKeys::new();
|
|
257
|
+
}
|
|
258
|
+
KEYS.with(|keys| {
|
|
259
|
+
let mut obj: ObjectMap = ObjectMap::with_capacity(6);
|
|
260
|
+
obj.insert(Arc::clone(&keys.method), Value::String(self.method.into()));
|
|
261
|
+
obj.insert(Arc::clone(&keys.url), Value::String(self.url.into()));
|
|
262
|
+
obj.insert(Arc::clone(&keys.path), Value::String(self.path.into()));
|
|
263
|
+
obj.insert(Arc::clone(&keys.query), Value::String(self.query.into()));
|
|
264
|
+
let mut h: ObjectMap = ObjectMap::with_capacity(self.headers.len());
|
|
265
|
+
for (k, v) in self.headers {
|
|
266
|
+
h.insert(Arc::from(k), Value::String(v.into()));
|
|
267
|
+
}
|
|
268
|
+
obj.insert(
|
|
269
|
+
Arc::clone(&keys.headers),
|
|
270
|
+
Value::object(h),
|
|
271
|
+
);
|
|
272
|
+
obj.insert(Arc::clone(&keys.body), Value::String(self.body.into()));
|
|
273
|
+
Value::object(obj)
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
struct RequestKeys {
|
|
279
|
+
method: Arc<str>,
|
|
280
|
+
url: Arc<str>,
|
|
281
|
+
path: Arc<str>,
|
|
282
|
+
query: Arc<str>,
|
|
283
|
+
headers: Arc<str>,
|
|
284
|
+
body: Arc<str>,
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
impl RequestKeys {
|
|
288
|
+
fn new() -> Self {
|
|
289
|
+
Self {
|
|
290
|
+
method: Arc::from("method"),
|
|
291
|
+
url: Arc::from("url"),
|
|
292
|
+
path: Arc::from("path"),
|
|
293
|
+
query: Arc::from("query"),
|
|
294
|
+
headers: Arc::from("headers"),
|
|
295
|
+
body: Arc::from("body"),
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
#[derive(Debug, Clone)]
|
|
301
|
+
pub struct ResponsePrimitive {
|
|
302
|
+
pub status: u16,
|
|
303
|
+
pub headers: Vec<(String, String)>,
|
|
304
|
+
pub body: ResponseBody,
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
#[derive(Debug, Clone)]
|
|
308
|
+
pub enum ResponseBody {
|
|
309
|
+
/// Text body; `Arc<str>` shares the value with Tish's `Value::String`.
|
|
310
|
+
Text(Arc<str>),
|
|
311
|
+
Bytes(Vec<u8>),
|
|
312
|
+
File(String),
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
impl ResponsePrimitive {
|
|
316
|
+
fn from_value(value: &Value) -> Self {
|
|
317
|
+
if let Some((status, headers, file)) = extract_file_from_response(value) {
|
|
318
|
+
return Self {
|
|
319
|
+
status,
|
|
320
|
+
headers,
|
|
321
|
+
body: ResponseBody::File(file),
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
let default_status = 200u16;
|
|
326
|
+
match value {
|
|
327
|
+
Value::Object(obj) => {
|
|
328
|
+
let obj_ref = obj.borrow();
|
|
329
|
+
|
|
330
|
+
let status = obj_ref
|
|
331
|
+
.strings
|
|
332
|
+
.get("status")
|
|
333
|
+
.and_then(|v| match v {
|
|
334
|
+
Value::Number(n) => Some(*n as u16),
|
|
335
|
+
_ => None,
|
|
336
|
+
})
|
|
337
|
+
.unwrap_or(default_status);
|
|
338
|
+
|
|
339
|
+
let has_error = obj_ref.strings.contains_key("error");
|
|
340
|
+
|
|
341
|
+
let body: ResponseBody = if let Some(bb) = obj_ref.strings.get("bodyBytes") {
|
|
342
|
+
match bb {
|
|
343
|
+
Value::Array(a) => {
|
|
344
|
+
let v: Vec<u8> = a
|
|
345
|
+
.borrow()
|
|
346
|
+
.iter()
|
|
347
|
+
.filter_map(|x| match x {
|
|
348
|
+
Value::Number(n) => Some((*n as u32 & 0xff) as u8),
|
|
349
|
+
_ => None,
|
|
350
|
+
})
|
|
351
|
+
.collect();
|
|
352
|
+
ResponseBody::Bytes(v)
|
|
353
|
+
}
|
|
354
|
+
_ => ResponseBody::Text(Arc::from(bb.to_display_string())),
|
|
355
|
+
}
|
|
356
|
+
} else if let Some(b) = obj_ref.strings.get("body") {
|
|
357
|
+
match b {
|
|
358
|
+
Value::String(s) => ResponseBody::Text(Arc::clone(&s)),
|
|
359
|
+
Value::Array(a) => {
|
|
360
|
+
let borrow = a.borrow();
|
|
361
|
+
if !borrow.is_empty()
|
|
362
|
+
&& borrow.iter().all(|x| matches!(x, Value::Number(_)))
|
|
363
|
+
{
|
|
364
|
+
ResponseBody::Bytes(
|
|
365
|
+
borrow
|
|
366
|
+
.iter()
|
|
367
|
+
.filter_map(|x| match x {
|
|
368
|
+
Value::Number(n) => Some((*n as u32 & 0xff) as u8),
|
|
369
|
+
_ => None,
|
|
370
|
+
})
|
|
371
|
+
.collect(),
|
|
372
|
+
)
|
|
373
|
+
} else {
|
|
374
|
+
ResponseBody::Text(Arc::from(b.to_display_string()))
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
_ => ResponseBody::Text(Arc::from(b.to_display_string())),
|
|
378
|
+
}
|
|
379
|
+
} else if has_error {
|
|
380
|
+
ResponseBody::Text(Arc::from(
|
|
381
|
+
obj_ref
|
|
382
|
+
.strings
|
|
383
|
+
.get("error")
|
|
384
|
+
.map(|v| v.to_display_string())
|
|
385
|
+
.unwrap_or_default(),
|
|
386
|
+
))
|
|
387
|
+
} else {
|
|
388
|
+
ResponseBody::Text(Arc::from(""))
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
let status = if has_error && status == default_status {
|
|
392
|
+
500
|
|
393
|
+
} else {
|
|
394
|
+
status
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
let headers = obj_ref
|
|
398
|
+
.strings
|
|
399
|
+
.get("headers")
|
|
400
|
+
.and_then(|v| match v {
|
|
401
|
+
Value::Object(h) => Some(
|
|
402
|
+
h.borrow()
|
|
403
|
+
.strings
|
|
404
|
+
.iter()
|
|
405
|
+
.map(|(k, v)| (k.to_string(), v.to_display_string()))
|
|
406
|
+
.collect(),
|
|
407
|
+
),
|
|
408
|
+
_ => None,
|
|
409
|
+
})
|
|
410
|
+
.unwrap_or_default();
|
|
411
|
+
|
|
412
|
+
Self {
|
|
413
|
+
status,
|
|
414
|
+
headers,
|
|
415
|
+
body,
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
Value::String(s) => Self {
|
|
419
|
+
status: default_status,
|
|
420
|
+
headers: vec![],
|
|
421
|
+
body: ResponseBody::Text(Arc::clone(s)),
|
|
422
|
+
},
|
|
423
|
+
_ => Self {
|
|
424
|
+
status: default_status,
|
|
425
|
+
headers: vec![],
|
|
426
|
+
body: ResponseBody::Text(Arc::from("")),
|
|
427
|
+
},
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// -------- legacy shims -----------------------------------------------------
|
|
433
|
+
|
|
434
|
+
#[allow(dead_code)]
|
|
435
|
+
pub fn request_to_value(request: &mut tiny_http::Request) -> Value {
|
|
436
|
+
RequestPrimitive::from_tiny_http(request).into_value()
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
#[allow(dead_code)]
|
|
440
|
+
pub fn value_to_response(value: &Value) -> (u16, Vec<(String, String)>, String) {
|
|
441
|
+
let r = ResponsePrimitive::from_value(value);
|
|
442
|
+
let body = match r.body {
|
|
443
|
+
ResponseBody::Text(s) => s.to_string(),
|
|
444
|
+
ResponseBody::Bytes(b) => String::from_utf8(b).unwrap_or_default(),
|
|
445
|
+
ResponseBody::File(_) => String::new(),
|
|
446
|
+
};
|
|
447
|
+
(r.status, r.headers, body)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
fn extract_file_from_response(value: &Value) -> Option<(u16, Vec<(String, String)>, String)> {
|
|
451
|
+
let Value::Object(obj) = value else {
|
|
452
|
+
return None;
|
|
453
|
+
};
|
|
454
|
+
let obj_ref = obj.borrow();
|
|
455
|
+
let Value::String(file_path) = obj_ref.strings.get("file")? else {
|
|
456
|
+
return None;
|
|
457
|
+
};
|
|
458
|
+
let file_path = file_path.to_string();
|
|
459
|
+
let status = obj_ref
|
|
460
|
+
.strings
|
|
461
|
+
.get("status")
|
|
462
|
+
.and_then(|v| match v {
|
|
463
|
+
Value::Number(n) => Some(*n as u16),
|
|
464
|
+
_ => None,
|
|
465
|
+
})
|
|
466
|
+
.unwrap_or(200);
|
|
467
|
+
let headers = obj_ref
|
|
468
|
+
.strings
|
|
469
|
+
.get("headers")
|
|
470
|
+
.and_then(|v| match v {
|
|
471
|
+
Value::Object(h) => Some(
|
|
472
|
+
h.borrow()
|
|
473
|
+
.strings
|
|
474
|
+
.iter()
|
|
475
|
+
.map(|(k, v)| (k.to_string(), v.to_display_string()))
|
|
476
|
+
.collect(),
|
|
477
|
+
),
|
|
478
|
+
_ => None,
|
|
479
|
+
})
|
|
480
|
+
.unwrap_or_default();
|
|
481
|
+
Some((status, headers, file_path))
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// -------- response writers -------------------------------------------------
|
|
485
|
+
|
|
486
|
+
fn inject_default_headers(headers: &mut Vec<(String, String)>) {
|
|
487
|
+
let has_date = headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("Date"));
|
|
488
|
+
let has_server = headers
|
|
489
|
+
.iter()
|
|
490
|
+
.any(|(k, _)| k.eq_ignore_ascii_case("Server"));
|
|
491
|
+
if !has_date {
|
|
492
|
+
// One atomic load + ref-inc + one String allocation for the Vec slot.
|
|
493
|
+
headers.push(("Date".into(), cached_date_header_arc().as_str().to_string()));
|
|
494
|
+
}
|
|
495
|
+
if !has_server {
|
|
496
|
+
headers.push(("Server".into(), "Tish".into()));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
#[allow(dead_code)]
|
|
501
|
+
pub fn send_response(
|
|
502
|
+
request: tiny_http::Request,
|
|
503
|
+
status: u16,
|
|
504
|
+
mut headers: Vec<(String, String)>,
|
|
505
|
+
body: String,
|
|
506
|
+
) {
|
|
507
|
+
send_response_arc(request, status, headers.drain(..).collect(), Arc::from(body));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
pub fn send_response_arc(
|
|
511
|
+
request: tiny_http::Request,
|
|
512
|
+
status: u16,
|
|
513
|
+
mut headers: Vec<(String, String)>,
|
|
514
|
+
body: Arc<str>,
|
|
515
|
+
) {
|
|
516
|
+
inject_default_headers(&mut headers);
|
|
517
|
+
let status_code = tiny_http::StatusCode(status);
|
|
518
|
+
let len = body.len();
|
|
519
|
+
let bytes: Arc<[u8]> = Arc::from(body.as_bytes());
|
|
520
|
+
let mut response = tiny_http::Response::new(
|
|
521
|
+
status_code,
|
|
522
|
+
vec![],
|
|
523
|
+
ArcBytesReader::new(bytes),
|
|
524
|
+
Some(len),
|
|
525
|
+
None,
|
|
526
|
+
);
|
|
527
|
+
for (key, value) in headers {
|
|
528
|
+
if let Ok(header) = tiny_http::Header::from_bytes(key.as_bytes(), value.as_bytes()) {
|
|
529
|
+
response = response.with_header(header);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
let _ = request.respond(response);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
struct ArcBytesReader {
|
|
536
|
+
bytes: Arc<[u8]>,
|
|
537
|
+
pos: usize,
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
impl ArcBytesReader {
|
|
541
|
+
fn new(bytes: Arc<[u8]>) -> Self {
|
|
542
|
+
Self { bytes, pos: 0 }
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
impl std::io::Read for ArcBytesReader {
|
|
547
|
+
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
|
548
|
+
let remaining = self.bytes.len().saturating_sub(self.pos);
|
|
549
|
+
let n = remaining.min(buf.len());
|
|
550
|
+
if n == 0 {
|
|
551
|
+
return Ok(0);
|
|
552
|
+
}
|
|
553
|
+
buf[..n].copy_from_slice(&self.bytes[self.pos..self.pos + n]);
|
|
554
|
+
self.pos += n;
|
|
555
|
+
Ok(n)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
pub fn send_response_bytes(
|
|
560
|
+
request: tiny_http::Request,
|
|
561
|
+
status: u16,
|
|
562
|
+
mut headers: Vec<(String, String)>,
|
|
563
|
+
body: Vec<u8>,
|
|
564
|
+
) {
|
|
565
|
+
inject_default_headers(&mut headers);
|
|
566
|
+
let status_code = tiny_http::StatusCode(status);
|
|
567
|
+
let len = body.len();
|
|
568
|
+
let mut response = tiny_http::Response::new(
|
|
569
|
+
status_code,
|
|
570
|
+
vec![],
|
|
571
|
+
std::io::Cursor::new(body),
|
|
572
|
+
Some(len),
|
|
573
|
+
None,
|
|
574
|
+
);
|
|
575
|
+
for (key, value) in headers {
|
|
576
|
+
if let Ok(header) = tiny_http::Header::from_bytes(key.as_bytes(), value.as_bytes()) {
|
|
577
|
+
response = response.with_header(header);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
let _ = request.respond(response);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
fn send_file_response(
|
|
584
|
+
request: tiny_http::Request,
|
|
585
|
+
status: u16,
|
|
586
|
+
mut headers: Vec<(String, String)>,
|
|
587
|
+
file_path: String,
|
|
588
|
+
) {
|
|
589
|
+
inject_default_headers(&mut headers);
|
|
590
|
+
let file = match File::open(&file_path) {
|
|
591
|
+
Ok(f) => f,
|
|
592
|
+
Err(e) => {
|
|
593
|
+
eprintln!("Failed to open file {}: {}", file_path, e);
|
|
594
|
+
let fallback =
|
|
595
|
+
tiny_http::Response::from_string(format!("File not found: {}", file_path))
|
|
596
|
+
.with_status_code(tiny_http::StatusCode(500));
|
|
597
|
+
let _ = request.respond(fallback);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
let status_code = tiny_http::StatusCode(status);
|
|
602
|
+
let mut response = tiny_http::Response::from_file(file).with_status_code(status_code);
|
|
603
|
+
for (key, value) in headers {
|
|
604
|
+
if let Ok(header) = tiny_http::Header::from_bytes(key.as_bytes(), value.as_bytes()) {
|
|
605
|
+
response = response.with_header(header);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
let _ = request.respond(response);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
fn respond_from_primitive(request: tiny_http::Request, resp: ResponsePrimitive) {
|
|
612
|
+
match resp.body {
|
|
613
|
+
ResponseBody::Text(s) => send_response_arc(request, resp.status, resp.headers, s),
|
|
614
|
+
ResponseBody::Bytes(b) => send_response_bytes(request, resp.status, resp.headers, b),
|
|
615
|
+
ResponseBody::File(p) => send_file_response(request, resp.status, resp.headers, p),
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// -------- static-route fast path ------------------------------------------
|
|
620
|
+
//
|
|
621
|
+
// Endpoints whose body is constant (TFB `/plaintext` and `/json`) don't need
|
|
622
|
+
// to roundtrip through the Tish VM per request. `register_static_route`
|
|
623
|
+
// stores the pre-built response bytes in a process-wide table; workers
|
|
624
|
+
// consult the table before pushing to the dispatcher and serve the
|
|
625
|
+
// response directly, skipping Value construction and channel hops entirely.
|
|
626
|
+
//
|
|
627
|
+
// This is the main lever that lets N workers actually *scale* for plaintext:
|
|
628
|
+
// without it, every request serialises through the single VM thread.
|
|
629
|
+
|
|
630
|
+
#[derive(Clone)]
|
|
631
|
+
struct StaticRoute {
|
|
632
|
+
body: Arc<[u8]>,
|
|
633
|
+
content_type: Arc<str>,
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
type StaticRoutes = std::collections::HashMap<String, StaticRoute>;
|
|
637
|
+
|
|
638
|
+
/// Lock-free static-route map: readers use `load()` which is a single atomic
|
|
639
|
+
/// operation. Writers (registration) do `rcu` to publish a new Arc. This is
|
|
640
|
+
/// what lets per-worker /plaintext lookups actually scale — a Mutex here was
|
|
641
|
+
/// bouncing cache lines between every worker thread on every request.
|
|
642
|
+
static STATIC_ROUTES: OnceLock<arc_swap::ArcSwap<StaticRoutes>> = OnceLock::new();
|
|
643
|
+
|
|
644
|
+
fn static_routes() -> &'static arc_swap::ArcSwap<StaticRoutes> {
|
|
645
|
+
STATIC_ROUTES
|
|
646
|
+
.get_or_init(|| arc_swap::ArcSwap::new(Arc::new(StaticRoutes::new())))
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/// Register a static response for `path`. Subsequent requests to exactly
|
|
650
|
+
/// that path (ignoring query string) are served by HTTP workers directly,
|
|
651
|
+
/// bypassing the Tish VM dispatcher. Content-type and Server/Date are
|
|
652
|
+
/// appended automatically.
|
|
653
|
+
pub fn register_static_route(path: &str, body: &[u8], content_type: &str) {
|
|
654
|
+
let cell = static_routes();
|
|
655
|
+
cell.rcu(|cur| {
|
|
656
|
+
let mut next = (**cur).clone();
|
|
657
|
+
next.insert(
|
|
658
|
+
path.to_string(),
|
|
659
|
+
StaticRoute {
|
|
660
|
+
body: Arc::from(body),
|
|
661
|
+
content_type: Arc::from(content_type),
|
|
662
|
+
},
|
|
663
|
+
);
|
|
664
|
+
Arc::new(next)
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
fn lookup_static_route(path: &str) -> Option<StaticRoute> {
|
|
669
|
+
// Strip query string in-place (no allocation).
|
|
670
|
+
let pure = path.split('?').next().unwrap_or(path);
|
|
671
|
+
let guard = static_routes().load();
|
|
672
|
+
guard.get(pure).cloned()
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
fn serve_static_route(request: tiny_http::Request, route: StaticRoute) {
|
|
676
|
+
let status_code = tiny_http::StatusCode(200);
|
|
677
|
+
let body_len = route.body.len();
|
|
678
|
+
let mut response = tiny_http::Response::new(
|
|
679
|
+
status_code,
|
|
680
|
+
vec![],
|
|
681
|
+
ArcBytesReader::new(route.body),
|
|
682
|
+
Some(body_len),
|
|
683
|
+
None,
|
|
684
|
+
);
|
|
685
|
+
// Hand-built headers; Date comes from the cached slot.
|
|
686
|
+
if let Ok(h) =
|
|
687
|
+
tiny_http::Header::from_bytes(b"Content-Type".as_slice(), route.content_type.as_bytes())
|
|
688
|
+
{
|
|
689
|
+
response = response.with_header(h);
|
|
690
|
+
}
|
|
691
|
+
if let Ok(h) = tiny_http::Header::from_bytes(b"Server".as_slice(), b"Tish".as_slice()) {
|
|
692
|
+
response = response.with_header(h);
|
|
693
|
+
}
|
|
694
|
+
let date = cached_date_header_arc();
|
|
695
|
+
if let Ok(h) = tiny_http::Header::from_bytes(b"Date".as_slice(), date.as_bytes()) {
|
|
696
|
+
response = response.with_header(h);
|
|
697
|
+
}
|
|
698
|
+
let _ = request.respond(response);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// -------- SO_REUSEPORT listeners -------------------------------------------
|
|
702
|
+
|
|
703
|
+
#[cfg(all(unix, not(any(target_os = "solaris", target_os = "illumos"))))]
|
|
704
|
+
fn set_reuse_port(s: &socket2::Socket) {
|
|
705
|
+
let _ = s.set_reuse_port(true);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
fn bind_listeners(port: u16, n: usize) -> Result<Vec<std::net::TcpListener>, String> {
|
|
709
|
+
use socket2::{Domain, Protocol, SockAddr, Socket, Type};
|
|
710
|
+
|
|
711
|
+
let addr: std::net::SocketAddr = format!("0.0.0.0:{}", port)
|
|
712
|
+
.parse()
|
|
713
|
+
.map_err(|e: std::net::AddrParseError| e.to_string())?;
|
|
714
|
+
let sa: SockAddr = addr.into();
|
|
715
|
+
|
|
716
|
+
let mut out = Vec::with_capacity(n);
|
|
717
|
+
for _ in 0..n {
|
|
718
|
+
let s = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP))
|
|
719
|
+
.map_err(|e| format!("socket: {}", e))?;
|
|
720
|
+
s.set_reuse_address(true)
|
|
721
|
+
.map_err(|e| format!("set_reuse_address: {}", e))?;
|
|
722
|
+
#[cfg(all(unix, not(any(target_os = "solaris", target_os = "illumos"))))]
|
|
723
|
+
{
|
|
724
|
+
set_reuse_port(&s);
|
|
725
|
+
}
|
|
726
|
+
s.set_nodelay(true).ok();
|
|
727
|
+
s.bind(&sa).map_err(|e| format!("bind {}: {}", port, e))?;
|
|
728
|
+
s.listen(1024).map_err(|e| format!("listen: {}", e))?;
|
|
729
|
+
out.push(s.into());
|
|
730
|
+
}
|
|
731
|
+
Ok(out)
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
fn num_workers() -> usize {
|
|
735
|
+
if let Ok(v) = std::env::var("TISH_HTTP_WORKERS") {
|
|
736
|
+
if let Ok(n) = v.parse::<usize>() {
|
|
737
|
+
if n >= 1 {
|
|
738
|
+
return n;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
std::thread::available_parallelism()
|
|
743
|
+
.map(|n| n.get())
|
|
744
|
+
.unwrap_or(1)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/// Target number of prefork worker **processes**. Distinct from
|
|
748
|
+
/// `num_workers`, which is the number of OS *threads* per process.
|
|
749
|
+
///
|
|
750
|
+
/// In prefork mode every process runs one accept thread, so this is the
|
|
751
|
+
/// knob that actually controls parallelism. Defaults to the number of
|
|
752
|
+
/// logical CPUs; `TISH_PREFORK_WORKERS=N` or `TISH_HTTP_WORKERS=N` overrides.
|
|
753
|
+
pub(crate) fn num_prefork_workers() -> usize {
|
|
754
|
+
if let Ok(v) = std::env::var("TISH_PREFORK_WORKERS") {
|
|
755
|
+
if let Ok(n) = v.parse::<usize>() {
|
|
756
|
+
if n >= 1 {
|
|
757
|
+
return n;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// Accept TISH_HTTP_WORKERS too so users don't have to learn a new var
|
|
762
|
+
// just to turn on prefork — the docs advertise a single env var.
|
|
763
|
+
if let Ok(v) = std::env::var("TISH_HTTP_WORKERS") {
|
|
764
|
+
if let Ok(n) = v.parse::<usize>() {
|
|
765
|
+
if n >= 1 {
|
|
766
|
+
return n;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
std::thread::available_parallelism()
|
|
771
|
+
.map(|n| n.get())
|
|
772
|
+
.unwrap_or(1)
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// -------- serve() ----------------------------------------------------------
|
|
776
|
+
|
|
777
|
+
/// Start an HTTP server that handles requests using the provided handler function.
|
|
778
|
+
///
|
|
779
|
+
/// When `send-values` is enabled (required for the `http` feature) the
|
|
780
|
+
/// handler is `Fn + Send + Sync`, so we run it directly on each HTTP
|
|
781
|
+
/// accept thread instead of funneling every request through a single
|
|
782
|
+
/// dispatcher. That's the multi-core unlock for the VM: handler calls
|
|
783
|
+
/// execute in parallel across N OS threads, with `Value` safely shared
|
|
784
|
+
/// via `VmRef` (`Arc<Mutex>`) on every array/object.
|
|
785
|
+
///
|
|
786
|
+
/// On builds without `send-values` (wasm / single-threaded targets)
|
|
787
|
+
/// the handler only needs `Fn`; we fall back to the mpsc-dispatch model
|
|
788
|
+
/// where all handler calls execute on one VM thread.
|
|
789
|
+
#[cfg(feature = "send-values")]
|
|
790
|
+
pub fn serve<F>(args: &[Value], handler: F) -> Value
|
|
791
|
+
where
|
|
792
|
+
F: Fn(&[Value]) -> Value + Send + Sync + 'static,
|
|
793
|
+
{
|
|
794
|
+
serve_impl_with_factory(args, None, Some(Arc::new(handler)))
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/// `serve(port, { onWorker, workers? })` — each accept thread calls the
|
|
798
|
+
/// factory once to build **its own** handler closure. This is the pattern
|
|
799
|
+
/// for per-worker state (DB connections, caches, counters) with the same
|
|
800
|
+
/// `Value: Send + Sync` parallelism as [`serve`]. The factory is invoked
|
|
801
|
+
/// with a single `Value::Number(worker_id)` arg and must return a
|
|
802
|
+
/// `Value::Function`. Panics at startup if the factory returns anything
|
|
803
|
+
/// else.
|
|
804
|
+
#[cfg(feature = "send-values")]
|
|
805
|
+
pub fn serve_per_worker<FF>(args: &[Value], factory: FF) -> Value
|
|
806
|
+
where
|
|
807
|
+
FF: Fn(usize) -> Arc<dyn Fn(&[Value]) -> Value + Send + Sync> + Send + Sync + 'static,
|
|
808
|
+
{
|
|
809
|
+
serve_impl_with_factory(
|
|
810
|
+
args,
|
|
811
|
+
Some(Arc::new(factory)
|
|
812
|
+
as Arc<dyn Fn(usize) -> Arc<dyn Fn(&[Value]) -> Value + Send + Sync> + Send + Sync>),
|
|
813
|
+
None,
|
|
814
|
+
)
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
#[cfg(not(feature = "send-values"))]
|
|
818
|
+
pub fn serve<F>(args: &[Value], handler: F) -> Value
|
|
819
|
+
where
|
|
820
|
+
F: Fn(&[Value]) -> Value,
|
|
821
|
+
{
|
|
822
|
+
serve_impl(args, handler)
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/// Shared implementation for [`serve`] (single handler) and
|
|
826
|
+
/// [`serve_per_worker`] (per-worker factory). Exactly one of
|
|
827
|
+
/// `factory` / `shared_handler` is provided; the other is `None`.
|
|
828
|
+
#[cfg(feature = "send-values")]
|
|
829
|
+
fn serve_impl_with_factory(
|
|
830
|
+
args: &[Value],
|
|
831
|
+
factory: Option<
|
|
832
|
+
Arc<dyn Fn(usize) -> Arc<dyn Fn(&[Value]) -> Value + Send + Sync> + Send + Sync>,
|
|
833
|
+
>,
|
|
834
|
+
shared_handler: Option<Arc<dyn Fn(&[Value]) -> Value + Send + Sync>>,
|
|
835
|
+
) -> Value {
|
|
836
|
+
debug_assert!(factory.is_some() ^ shared_handler.is_some());
|
|
837
|
+
let port = match args.first() {
|
|
838
|
+
Some(Value::Number(n)) => *n as u16,
|
|
839
|
+
_ => return build_error_response("serve requires a port number"),
|
|
840
|
+
};
|
|
841
|
+
|
|
842
|
+
let max_requests: Option<usize> = args.get(2).and_then(|v| match v {
|
|
843
|
+
Value::Number(n) if *n >= 1.0 => Some(*n as usize),
|
|
844
|
+
_ => None,
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
ensure_date_thread();
|
|
848
|
+
|
|
849
|
+
// --- prefork: spawn one subprocess per extra core --------------------
|
|
850
|
+
//
|
|
851
|
+
// This is the big multi-core unlock for Tish HTTP: because the Tish VM
|
|
852
|
+
// runs on `Rc`/`RefCell` (`Value` is `!Send`), we can't run multiple
|
|
853
|
+
// handler threads in one process. Instead, we fork the process — each
|
|
854
|
+
// child re-execs the same binary and runs its own single-threaded VM.
|
|
855
|
+
// All N processes `SO_REUSEPORT`-bind the same port and the kernel
|
|
856
|
+
// load-balances connections across them. Same pattern as nginx,
|
|
857
|
+
// gunicorn, puma cluster, phpfpm.
|
|
858
|
+
//
|
|
859
|
+
// * Parent (role = Parent) spawns N-1 children and keeps its own accept
|
|
860
|
+
// loop running (so we don't waste a core just coordinating).
|
|
861
|
+
// * Children (role = Child(id)) just run their accept loop.
|
|
862
|
+
// * Single (role = Single) skips forking — explicit opt-out, or we're
|
|
863
|
+
// already inside a child.
|
|
864
|
+
let role = crate::http_prefork::role_from_env();
|
|
865
|
+
let prefork_n = num_prefork_workers();
|
|
866
|
+
let mut children = Vec::new();
|
|
867
|
+
let mut prefork_stop: Option<Arc<AtomicBool>> = None;
|
|
868
|
+
match role {
|
|
869
|
+
crate::http_prefork::PreforkRole::Parent if prefork_n > 1 => {
|
|
870
|
+
match crate::http_prefork::spawn_children(prefork_n) {
|
|
871
|
+
Ok(c) => {
|
|
872
|
+
let handles = crate::http_prefork::install_parent_signal_handler(c);
|
|
873
|
+
prefork_stop = Some(handles);
|
|
874
|
+
// Children are spawned; fall through and run our own
|
|
875
|
+
// accept loop as worker 0.
|
|
876
|
+
children.push(()); // sentinel: we're in a prefork group
|
|
877
|
+
}
|
|
878
|
+
Err(e) => {
|
|
879
|
+
eprintln!(
|
|
880
|
+
"[tish http] prefork spawn failed, continuing as single process: {}",
|
|
881
|
+
e
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
_ => {}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Per-process accept threads. In prefork mode this is 1; SO_REUSEPORT
|
|
890
|
+
// between processes provides kernel-level load balancing. Without
|
|
891
|
+
// prefork we fall back to the old multi-thread-in-one-process layout.
|
|
892
|
+
let workers = if children.is_empty() {
|
|
893
|
+
num_workers()
|
|
894
|
+
} else {
|
|
895
|
+
1
|
|
896
|
+
};
|
|
897
|
+
let listeners = match bind_listeners(port, workers) {
|
|
898
|
+
Ok(l) => l,
|
|
899
|
+
Err(e) => {
|
|
900
|
+
eprintln!("[tish http] failed to bind: {}", e);
|
|
901
|
+
return build_error_response(&e);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
let worker_id = match role {
|
|
906
|
+
crate::http_prefork::PreforkRole::Child(id) => id,
|
|
907
|
+
_ => 0,
|
|
908
|
+
};
|
|
909
|
+
if matches!(role, crate::http_prefork::PreforkRole::Parent) && !children.is_empty() {
|
|
910
|
+
println!(
|
|
911
|
+
"tish http: listening on http://0.0.0.0:{} ({} process{} x {} accept thread{})",
|
|
912
|
+
port,
|
|
913
|
+
prefork_n,
|
|
914
|
+
if prefork_n == 1 { "" } else { "es" },
|
|
915
|
+
workers,
|
|
916
|
+
if workers == 1 { "" } else { "s" }
|
|
917
|
+
);
|
|
918
|
+
} else if worker_id == 0 {
|
|
919
|
+
println!(
|
|
920
|
+
"tish http: listening on http://0.0.0.0:{} ({} worker{})",
|
|
921
|
+
port,
|
|
922
|
+
workers,
|
|
923
|
+
if workers == 1 { "" } else { "s" }
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
let stop = prefork_stop.clone().unwrap_or_else(|| Arc::new(AtomicBool::new(false)));
|
|
928
|
+
|
|
929
|
+
if max_requests == Some(1) {
|
|
930
|
+
let p = port;
|
|
931
|
+
thread::spawn(move || {
|
|
932
|
+
thread::sleep(Duration::from_millis(50));
|
|
933
|
+
if let Ok(mut s) = std::net::TcpStream::connect(format!("127.0.0.1:{}", p)) {
|
|
934
|
+
let _ =
|
|
935
|
+
s.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n");
|
|
936
|
+
let _ = s.shutdown(std::net::Shutdown::Write);
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Per-thread handler: either a single shared `Arc<dyn Fn>` or one
|
|
942
|
+
// built per worker via the user-supplied factory. Because
|
|
943
|
+
// `Value: Send + Sync` under `send-values`, both variants run in
|
|
944
|
+
// parallel on N accept threads without a dispatcher queue.
|
|
945
|
+
//
|
|
946
|
+
// The worker index passed to the factory is *global across prefork
|
|
947
|
+
// processes*: parent is 0..N-1, each child is its own id, so a 14-
|
|
948
|
+
// core + 14-process layout produces worker ids 0..13 total, never
|
|
949
|
+
// duplicated.
|
|
950
|
+
let global_worker_base = match role {
|
|
951
|
+
crate::http_prefork::PreforkRole::Child(id) => id * workers,
|
|
952
|
+
_ => 0,
|
|
953
|
+
};
|
|
954
|
+
|
|
955
|
+
let processed = Arc::new(AtomicUsize::new(0));
|
|
956
|
+
let mut worker_handles = Vec::with_capacity(workers);
|
|
957
|
+
for (idx, listener) in listeners.into_iter().enumerate() {
|
|
958
|
+
let stop = Arc::clone(&stop);
|
|
959
|
+
let processed = Arc::clone(&processed);
|
|
960
|
+
let max = max_requests;
|
|
961
|
+
let worker_handler: Arc<dyn Fn(&[Value]) -> Value + Send + Sync> =
|
|
962
|
+
if let Some(f) = &factory {
|
|
963
|
+
f(global_worker_base + idx)
|
|
964
|
+
} else {
|
|
965
|
+
Arc::clone(shared_handler.as_ref().unwrap())
|
|
966
|
+
};
|
|
967
|
+
let handle = thread::Builder::new()
|
|
968
|
+
.name(format!("tish-http-w{}", idx))
|
|
969
|
+
.spawn(move || worker_loop_direct(listener, worker_handler, stop, processed, max))
|
|
970
|
+
.expect("spawn tish-http worker");
|
|
971
|
+
worker_handles.push(handle);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Wait until one of: stop flag set, a worker bumps `processed >= max`, or
|
|
975
|
+
// we're shutting down (parent received SIGINT).
|
|
976
|
+
loop {
|
|
977
|
+
if stop.load(Ordering::Relaxed) {
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
if let Some(m) = max_requests {
|
|
981
|
+
if processed.load(Ordering::Relaxed) >= m {
|
|
982
|
+
stop.store(true, Ordering::Relaxed);
|
|
983
|
+
break;
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
thread::sleep(Duration::from_millis(50));
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Kick each accept so its blocking `recv_timeout` wakes up promptly.
|
|
990
|
+
for _ in 0..worker_handles.len() {
|
|
991
|
+
let _ = std::net::TcpStream::connect(format!("127.0.0.1:{}", port));
|
|
992
|
+
}
|
|
993
|
+
for h in worker_handles {
|
|
994
|
+
let _ = h.join();
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
Value::Null
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
#[cfg(not(feature = "send-values"))]
|
|
1001
|
+
type Job = (RequestPrimitive, mpsc::SyncSender<ResponsePrimitive>);
|
|
1002
|
+
|
|
1003
|
+
#[cfg(not(feature = "send-values"))]
|
|
1004
|
+
fn serve_impl<F>(args: &[Value], handler: F) -> Value
|
|
1005
|
+
where
|
|
1006
|
+
F: Fn(&[Value]) -> Value,
|
|
1007
|
+
{
|
|
1008
|
+
// Single-threaded dispatch path: multiple accept threads push onto a
|
|
1009
|
+
// shared `mpsc::sync_channel`, one VM thread (this one) drains and runs
|
|
1010
|
+
// the handler. Used by wasm / single-threaded Rust builds where
|
|
1011
|
+
// `Value` is `Rc`-backed and not `Send`.
|
|
1012
|
+
let port = match args.first() {
|
|
1013
|
+
Some(Value::Number(n)) => *n as u16,
|
|
1014
|
+
_ => return build_error_response("serve requires a port number"),
|
|
1015
|
+
};
|
|
1016
|
+
let max_requests: Option<usize> = args.get(2).and_then(|v| match v {
|
|
1017
|
+
Value::Number(n) if *n >= 1.0 => Some(*n as usize),
|
|
1018
|
+
_ => None,
|
|
1019
|
+
});
|
|
1020
|
+
ensure_date_thread();
|
|
1021
|
+
let workers = num_workers();
|
|
1022
|
+
let listeners = match bind_listeners(port, workers) {
|
|
1023
|
+
Ok(l) => l,
|
|
1024
|
+
Err(e) => {
|
|
1025
|
+
eprintln!("[tish http] failed to bind: {}", e);
|
|
1026
|
+
return build_error_response(&e);
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
println!(
|
|
1030
|
+
"tish http: listening on http://0.0.0.0:{} ({} worker{}, single-vm)",
|
|
1031
|
+
port,
|
|
1032
|
+
workers,
|
|
1033
|
+
if workers == 1 { "" } else { "s" }
|
|
1034
|
+
);
|
|
1035
|
+
|
|
1036
|
+
let stop = Arc::new(AtomicBool::new(false));
|
|
1037
|
+
let (tx, rx) = mpsc::sync_channel::<Job>(workers * 256);
|
|
1038
|
+
|
|
1039
|
+
if max_requests == Some(1) {
|
|
1040
|
+
let p = port;
|
|
1041
|
+
thread::spawn(move || {
|
|
1042
|
+
thread::sleep(Duration::from_millis(50));
|
|
1043
|
+
if let Ok(mut s) = std::net::TcpStream::connect(format!("127.0.0.1:{}", p)) {
|
|
1044
|
+
let _ =
|
|
1045
|
+
s.write_all(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n");
|
|
1046
|
+
let _ = s.shutdown(std::net::Shutdown::Write);
|
|
1047
|
+
}
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
let mut worker_handles = Vec::with_capacity(workers);
|
|
1052
|
+
for (idx, listener) in listeners.into_iter().enumerate() {
|
|
1053
|
+
let tx = tx.clone();
|
|
1054
|
+
let stop = Arc::clone(&stop);
|
|
1055
|
+
let handle = thread::Builder::new()
|
|
1056
|
+
.name(format!("tish-http-w{}", idx))
|
|
1057
|
+
.spawn(move || worker_loop(listener, tx, stop))
|
|
1058
|
+
.expect("spawn tish-http worker");
|
|
1059
|
+
worker_handles.push(handle);
|
|
1060
|
+
}
|
|
1061
|
+
drop(tx);
|
|
1062
|
+
|
|
1063
|
+
let mut count = 0usize;
|
|
1064
|
+
while let Ok((req_prim, resp_tx)) = rx.recv() {
|
|
1065
|
+
let req_value = req_prim.into_value();
|
|
1066
|
+
let response_value = handler(&[req_value]);
|
|
1067
|
+
let resp_prim = ResponsePrimitive::from_value(&response_value);
|
|
1068
|
+
let _ = resp_tx.send(resp_prim);
|
|
1069
|
+
|
|
1070
|
+
count += 1;
|
|
1071
|
+
if max_requests.map(|m| count >= m).unwrap_or(false) {
|
|
1072
|
+
stop.store(true, Ordering::Relaxed);
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
stop.store(true, Ordering::Relaxed);
|
|
1078
|
+
for _ in 0..worker_handles.len() {
|
|
1079
|
+
let _ = std::net::TcpStream::connect(format!("127.0.0.1:{}", port));
|
|
1080
|
+
}
|
|
1081
|
+
for h in worker_handles {
|
|
1082
|
+
let _ = h.join();
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
Value::Null
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
/// Parallel accept + dispatch loop used when `send-values` is on. The
|
|
1089
|
+
/// handler runs on the same OS thread that accepted the connection, so
|
|
1090
|
+
/// there is no cross-thread queue on the hot path. Static-route fast path
|
|
1091
|
+
/// is unchanged.
|
|
1092
|
+
#[cfg(feature = "send-values")]
|
|
1093
|
+
fn worker_loop_direct(
|
|
1094
|
+
listener: std::net::TcpListener,
|
|
1095
|
+
handler: Arc<dyn Fn(&[Value]) -> Value + Send + Sync>,
|
|
1096
|
+
stop: Arc<AtomicBool>,
|
|
1097
|
+
processed: Arc<AtomicUsize>,
|
|
1098
|
+
max_requests: Option<usize>,
|
|
1099
|
+
) {
|
|
1100
|
+
let server = match tiny_http::Server::from_listener(listener, None) {
|
|
1101
|
+
Ok(s) => s,
|
|
1102
|
+
Err(e) => {
|
|
1103
|
+
eprintln!("[tish http] worker failed to adopt listener: {}", e);
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
loop {
|
|
1108
|
+
if stop.load(Ordering::Relaxed) {
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
match server.recv_timeout(Duration::from_millis(250)) {
|
|
1112
|
+
Ok(Some(mut request)) => {
|
|
1113
|
+
// Static-route fast path: serve pre-baked bytes without
|
|
1114
|
+
// touching the Tish VM at all.
|
|
1115
|
+
if let Some(route) = lookup_static_route(request.url()) {
|
|
1116
|
+
serve_static_route(request, route);
|
|
1117
|
+
} else {
|
|
1118
|
+
let req_prim = RequestPrimitive::from_tiny_http(&mut request);
|
|
1119
|
+
let req_value = req_prim.into_value();
|
|
1120
|
+
let response_value = handler(&[req_value]);
|
|
1121
|
+
let resp_prim = ResponsePrimitive::from_value(&response_value);
|
|
1122
|
+
respond_from_primitive(request, resp_prim);
|
|
1123
|
+
}
|
|
1124
|
+
if let Some(m) = max_requests {
|
|
1125
|
+
let p = processed.fetch_add(1, Ordering::Relaxed) + 1;
|
|
1126
|
+
if p >= m {
|
|
1127
|
+
stop.store(true, Ordering::Relaxed);
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
Ok(None) => {} // timeout; re-check stop flag
|
|
1133
|
+
Err(_) => break,
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
#[cfg(not(feature = "send-values"))]
|
|
1139
|
+
fn worker_loop(
|
|
1140
|
+
listener: std::net::TcpListener,
|
|
1141
|
+
dispatch: mpsc::SyncSender<Job>,
|
|
1142
|
+
stop: Arc<AtomicBool>,
|
|
1143
|
+
) {
|
|
1144
|
+
let server = match tiny_http::Server::from_listener(listener, None) {
|
|
1145
|
+
Ok(s) => s,
|
|
1146
|
+
Err(e) => {
|
|
1147
|
+
eprintln!("[tish http] worker failed to adopt listener: {}", e);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
// Per-worker buffer of in-flight requests. We interleave accept with
|
|
1153
|
+
// response drain so a slow VM-thread handler doesn't block new accepts
|
|
1154
|
+
// (up to the pending capacity) and so single-request responses still
|
|
1155
|
+
// flush immediately when accept() blocks on the next connection.
|
|
1156
|
+
let mut pending: VecDeque<(tiny_http::Request, mpsc::Receiver<ResponsePrimitive>)> =
|
|
1157
|
+
VecDeque::with_capacity(256);
|
|
1158
|
+
|
|
1159
|
+
loop {
|
|
1160
|
+
if stop.load(Ordering::Relaxed) {
|
|
1161
|
+
break;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// Short-poll accept so we can drain pending responses even when
|
|
1165
|
+
// there's no new request arriving.
|
|
1166
|
+
match server.recv_timeout(Duration::from_millis(1)) {
|
|
1167
|
+
Ok(Some(mut request)) => {
|
|
1168
|
+
// Static-route fast path: serve pre-baked bytes without a
|
|
1169
|
+
// round trip through the VM dispatcher. This is what lets
|
|
1170
|
+
// per-worker accept actually scale for /plaintext + /json.
|
|
1171
|
+
if let Some(route) = lookup_static_route(request.url()) {
|
|
1172
|
+
serve_static_route(request, route);
|
|
1173
|
+
continue;
|
|
1174
|
+
}
|
|
1175
|
+
let req_prim = RequestPrimitive::from_tiny_http(&mut request);
|
|
1176
|
+
let (resp_tx, resp_rx) = mpsc::sync_channel::<ResponsePrimitive>(1);
|
|
1177
|
+
if dispatch.send((req_prim, resp_tx)).is_err() {
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1180
|
+
pending.push_back((request, resp_rx));
|
|
1181
|
+
}
|
|
1182
|
+
Ok(None) => {
|
|
1183
|
+
// timed out; fall through to drain
|
|
1184
|
+
}
|
|
1185
|
+
Err(_) => break,
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Drain all ready responses (FIFO preserves order).
|
|
1189
|
+
while let Some((_, rx)) = pending.front() {
|
|
1190
|
+
match rx.try_recv() {
|
|
1191
|
+
Ok(resp) => {
|
|
1192
|
+
let (req, _rx) = pending.pop_front().unwrap();
|
|
1193
|
+
respond_from_primitive(req, resp);
|
|
1194
|
+
}
|
|
1195
|
+
Err(mpsc::TryRecvError::Empty) => break,
|
|
1196
|
+
Err(mpsc::TryRecvError::Disconnected) => {
|
|
1197
|
+
pending.pop_front();
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
while let Some((req, rx)) = pending.pop_front() {
|
|
1204
|
+
match rx.recv_timeout(Duration::from_millis(500)) {
|
|
1205
|
+
Ok(resp) => respond_from_primitive(req, resp),
|
|
1206
|
+
Err(_) => drop(req),
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// -------- public shims for alternate HTTP backends ------------------------
|
|
1212
|
+
//
|
|
1213
|
+
// Exposed so `http_hyper.rs` (and any future backend) can reuse the same
|
|
1214
|
+
// request / response primitives, static-route table, cached Date header,
|
|
1215
|
+
// SO_REUSEPORT binder, and worker count. Keeping these in one place
|
|
1216
|
+
// guarantees parity between tiny_http and hyper code paths — cached Date,
|
|
1217
|
+
// Server header, static-route table, key interning all work identically.
|
|
1218
|
+
|
|
1219
|
+
#[cfg(feature = "http-hyper")]
|
|
1220
|
+
impl RequestPrimitive {
|
|
1221
|
+
/// Build a `RequestPrimitive` from already-parsed parts. Used by
|
|
1222
|
+
/// backends that do their own HTTP parsing (e.g. hyper).
|
|
1223
|
+
pub fn new_pub(
|
|
1224
|
+
method: String,
|
|
1225
|
+
url: String,
|
|
1226
|
+
path: String,
|
|
1227
|
+
query: String,
|
|
1228
|
+
headers: Vec<(String, String)>,
|
|
1229
|
+
body: String,
|
|
1230
|
+
) -> Self {
|
|
1231
|
+
Self {
|
|
1232
|
+
method,
|
|
1233
|
+
url,
|
|
1234
|
+
path,
|
|
1235
|
+
query,
|
|
1236
|
+
headers,
|
|
1237
|
+
body,
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/// Same as the crate-private `into_value` but re-exported for
|
|
1242
|
+
/// alternate HTTP backends.
|
|
1243
|
+
pub fn into_value_pub(self) -> Value {
|
|
1244
|
+
self.into_value()
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
#[cfg(feature = "http-hyper")]
|
|
1249
|
+
impl ResponsePrimitive {
|
|
1250
|
+
/// Public alias of the crate-private `from_value`. Used by alternate
|
|
1251
|
+
/// HTTP backends (hyper) so Tish handlers return the same response
|
|
1252
|
+
/// shape regardless of which server is underneath.
|
|
1253
|
+
pub fn from_value_pub(value: &Value) -> Self {
|
|
1254
|
+
Self::from_value(value)
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/// Public snapshot of a static route (body + content-type), returned to
|
|
1259
|
+
/// alternate HTTP backends so they can serve pre-baked responses without
|
|
1260
|
+
/// reaching into our private types.
|
|
1261
|
+
#[cfg(feature = "http-hyper")]
|
|
1262
|
+
#[derive(Clone)]
|
|
1263
|
+
pub struct StaticRouteSnapshot {
|
|
1264
|
+
pub body: Arc<[u8]>,
|
|
1265
|
+
pub content_type: Arc<str>,
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
#[cfg(feature = "http-hyper")]
|
|
1269
|
+
pub fn lookup_static_route_pub(path: &str) -> Option<StaticRouteSnapshot> {
|
|
1270
|
+
lookup_static_route(path).map(|r| StaticRouteSnapshot {
|
|
1271
|
+
body: r.body,
|
|
1272
|
+
content_type: r.content_type,
|
|
1273
|
+
})
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
#[cfg(feature = "http-hyper")]
|
|
1277
|
+
pub fn num_workers_pub() -> usize {
|
|
1278
|
+
num_workers()
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
#[cfg(feature = "http-hyper")]
|
|
1282
|
+
pub fn num_prefork_workers_pub() -> usize {
|
|
1283
|
+
num_prefork_workers()
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
#[cfg(feature = "http-hyper")]
|
|
1287
|
+
pub fn bind_listeners_reuseport(
|
|
1288
|
+
port: u16,
|
|
1289
|
+
n: usize,
|
|
1290
|
+
) -> Result<Vec<std::net::TcpListener>, String> {
|
|
1291
|
+
bind_listeners(port, n)
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
#[cfg(feature = "http-hyper")]
|
|
1295
|
+
pub fn build_error_response_pub(msg: &str) -> Value {
|
|
1296
|
+
build_error_response(msg)
|
|
1297
|
+
}
|
|
1298
|
+
|