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