feat: dedicated nativets (#8021)
* feat: dedicated nativets * review nits * prewarm isolates * ref * chore: update ee-repo-ref to 5f8105b808f3f0186fdf5132d2ee602d8a14aa17 This commit updates the EE repository reference after PR #424 was merged in windmill-ee-private. Previous ee-repo-ref: b7906acabb8ce359230bbd3e30dbb3bba4c42adb New ee-repo-ref: 5f8105b808f3f0186fdf5132d2ee602d8a14aa17 Automated by sync-ee-ref workflow. --------- Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
1
backend/Cargo.lock
generated
1
backend/Cargo.lock
generated
@@ -15778,6 +15778,7 @@ dependencies = [
|
||||
"windmill-indexer",
|
||||
"windmill-object-store",
|
||||
"windmill-operator",
|
||||
"windmill-parser-ts",
|
||||
"windmill-queue",
|
||||
"windmill-runtime-nativets",
|
||||
"windmill-test-utils",
|
||||
|
||||
@@ -254,6 +254,7 @@ axum.workspace = true
|
||||
serde.workspace = true
|
||||
windmill-api-client.workspace = true
|
||||
tempfile.workspace = true
|
||||
windmill-parser-ts.workspace = true
|
||||
rumqttc.workspace = true
|
||||
rdkafka.workspace = true
|
||||
async-nats.workspace = true
|
||||
|
||||
@@ -1 +1 @@
|
||||
d842747738a2f10fc2fd0cd61f536efffcb45e41
|
||||
5f8105b808f3f0186fdf5132d2ee602d8a14aa17
|
||||
|
||||
275
backend/tests/nativets_dedicated.rs
Normal file
275
backend/tests/nativets_dedicated.rs
Normal file
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Tests for the PrewarmedIsolate used by nativets dedicated workers.
|
||||
*
|
||||
* Run with:
|
||||
* cargo test -p windmill --features "deno_core" --test nativets_dedicated -- --nocapture
|
||||
*/
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
mod prewarmed_isolate_tests {
|
||||
use std::process::Command;
|
||||
use windmill_runtime_nativets::{NativeAnnotation, PrewarmedIsolate};
|
||||
use windmill_worker::{build_loader, LoaderMode, BUN_PATH};
|
||||
|
||||
fn default_annotation() -> NativeAnnotation {
|
||||
NativeAnnotation { useragent: None, proxy: None }
|
||||
}
|
||||
|
||||
/// Bundle a TypeScript script into JS suitable for `PrewarmedIsolate`.
|
||||
///
|
||||
/// Returns `(ts_source, js_bundle, arg_names)`.
|
||||
async fn bundle_script(script: &str) -> (String, String, Vec<String>) {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let dir = temp_dir.path();
|
||||
let dir_str = dir.to_str().unwrap();
|
||||
|
||||
std::fs::write(dir.join("main.ts"), script).unwrap();
|
||||
|
||||
build_loader(
|
||||
dir_str,
|
||||
"http://localhost:8000",
|
||||
"test_token",
|
||||
"test-workspace",
|
||||
"f/test/script",
|
||||
LoaderMode::BrowserBundle,
|
||||
)
|
||||
.await
|
||||
.expect("build_loader failed");
|
||||
|
||||
let output = Command::new(BUN_PATH.as_str())
|
||||
.args(["run", dir.join("node_builder.ts").to_str().unwrap()])
|
||||
.current_dir(dir)
|
||||
.output()
|
||||
.expect("Failed to run bun build");
|
||||
|
||||
if !output.status.success() {
|
||||
panic!(
|
||||
"Bun build failed: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
let ts = std::fs::read_to_string(dir.join("main.ts")).unwrap();
|
||||
let js = std::fs::read_to_string(dir.join("main.js")).unwrap();
|
||||
let parsed = windmill_parser_ts::parse_deno_signature(&ts, true, false, None)
|
||||
.expect("failed to parse signature");
|
||||
let arg_names: Vec<String> = parsed.args.into_iter().map(|a| a.name).collect();
|
||||
(ts, js, arg_names)
|
||||
}
|
||||
|
||||
const TEST_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
async fn run_prewarmed_test(
|
||||
script: &str,
|
||||
jobs: Vec<serde_json::Value>,
|
||||
) -> Vec<Result<serde_json::Value, String>> {
|
||||
tokio::time::timeout(TEST_TIMEOUT, run_prewarmed_test_inner(script, jobs))
|
||||
.await
|
||||
.expect("test timed out after 30s")
|
||||
}
|
||||
|
||||
async fn run_prewarmed_test_inner(
|
||||
script: &str,
|
||||
jobs: Vec<serde_json::Value>,
|
||||
) -> Vec<Result<serde_json::Value, String>> {
|
||||
windmill_runtime_nativets::setup_deno_runtime().expect("V8 init failed");
|
||||
|
||||
let (_ts, js, arg_names) = bundle_script(script).await;
|
||||
let ann = default_annotation();
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for job_args in &jobs {
|
||||
let mut isolate =
|
||||
PrewarmedIsolate::spawn("".to_string(), js.clone(), ann.clone(), arg_names.clone());
|
||||
isolate.wait_ready().await.expect("isolate failed to warm");
|
||||
|
||||
let args = serde_json::to_string(job_args).unwrap();
|
||||
let executing = isolate.start_execution(args);
|
||||
let prewarmed_result = executing.wait().await.expect("isolate execution failed");
|
||||
|
||||
match prewarmed_result.result {
|
||||
Ok(raw) => {
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(raw.get()).unwrap_or(serde_json::Value::Null);
|
||||
results.push(Ok(value));
|
||||
}
|
||||
Err(e) => {
|
||||
results.push(Err(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_simple() {
|
||||
let script = r#"
|
||||
export function main(x: number, y: number): number {
|
||||
return x + y;
|
||||
}
|
||||
"#;
|
||||
let results = run_prewarmed_test(script, vec![serde_json::json!({"x": 2, "y": 3})]).await;
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0], Ok(serde_json::json!(5)));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_isolation() {
|
||||
let script = r#"
|
||||
let counter = 0;
|
||||
export function main(): number {
|
||||
counter++;
|
||||
return counter;
|
||||
}
|
||||
"#;
|
||||
let results =
|
||||
run_prewarmed_test(script, vec![serde_json::json!({}), serde_json::json!({})]).await;
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
// Each job gets a fresh isolate, so counter should be 1 both times
|
||||
assert_eq!(results[0], Ok(serde_json::json!(1)));
|
||||
assert_eq!(results[1], Ok(serde_json::json!(1)));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_error() {
|
||||
let script = r#"
|
||||
export function main(msg: string): never {
|
||||
throw new Error(msg);
|
||||
}
|
||||
"#;
|
||||
let results =
|
||||
run_prewarmed_test(script, vec![serde_json::json!({"msg": "test error"})]).await;
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert!(results[0].is_err());
|
||||
assert!(
|
||||
results[0].as_ref().unwrap_err().contains("test error"),
|
||||
"Error should contain 'test error', got: {}",
|
||||
results[0].as_ref().unwrap_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_async() {
|
||||
let script = r#"
|
||||
export async function main(x: number): Promise<number> {
|
||||
const val = await Promise.resolve(x * 10);
|
||||
return val + 1;
|
||||
}
|
||||
"#;
|
||||
let results = run_prewarmed_test(script, vec![serde_json::json!({"x": 7})]).await;
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(results[0], Ok(serde_json::json!(71)));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_pipeline() {
|
||||
windmill_runtime_nativets::setup_deno_runtime().expect("V8 init failed");
|
||||
|
||||
let script = r#"
|
||||
export function main(n: number): number {
|
||||
return n * 2;
|
||||
}
|
||||
"#;
|
||||
let (_ts, js, arg_names) = bundle_script(script).await;
|
||||
let ann = default_annotation();
|
||||
|
||||
// Pre-warm first isolate
|
||||
let mut warm =
|
||||
PrewarmedIsolate::spawn("".to_string(), js.clone(), ann.clone(), arg_names.clone());
|
||||
warm.wait_ready()
|
||||
.await
|
||||
.expect("first isolate failed to warm");
|
||||
|
||||
let mut results = Vec::new();
|
||||
|
||||
for i in 1..=3 {
|
||||
let args = serde_json::to_string(&serde_json::json!({"n": i})).unwrap();
|
||||
let executing = warm.start_execution(args);
|
||||
|
||||
// Pipeline: start pre-warming next isolate while current one runs
|
||||
warm =
|
||||
PrewarmedIsolate::spawn("".to_string(), js.clone(), ann.clone(), arg_names.clone());
|
||||
|
||||
let prewarmed_result = executing.wait().await.expect("isolate execution failed");
|
||||
match prewarmed_result.result {
|
||||
Ok(raw) => {
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(raw.get()).unwrap_or(serde_json::Value::Null);
|
||||
results.push(value);
|
||||
}
|
||||
Err(e) => panic!("unexpected error: {e}"),
|
||||
}
|
||||
|
||||
warm.wait_ready()
|
||||
.await
|
||||
.expect("next isolate failed to warm");
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
results,
|
||||
vec![
|
||||
serde_json::json!(2),
|
||||
serde_json::json!(4),
|
||||
serde_json::json!(6),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_complex_return() {
|
||||
let script = r#"
|
||||
export function main(name: string, items: number[]): any {
|
||||
return {
|
||||
greeting: `hello ${name}`,
|
||||
sum: items.reduce((a, b) => a + b, 0),
|
||||
items: items.map(x => x * 2),
|
||||
};
|
||||
}
|
||||
"#;
|
||||
let results = run_prewarmed_test(
|
||||
script,
|
||||
vec![serde_json::json!({"name": "world", "items": [1, 2, 3]})],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(results.len(), 1);
|
||||
assert_eq!(
|
||||
results[0],
|
||||
Ok(serde_json::json!({
|
||||
"greeting": "hello world",
|
||||
"sum": 6,
|
||||
"items": [2, 4, 6],
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_prewarmed_null_undefined() {
|
||||
let script = r#"
|
||||
export function main(returnNull: boolean): any {
|
||||
if (returnNull) {
|
||||
return null;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
"#;
|
||||
let results = run_prewarmed_test(
|
||||
script,
|
||||
vec![
|
||||
serde_json::json!({"returnNull": true}),
|
||||
serde_json::json!({"returnNull": false}),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(results.len(), 2);
|
||||
assert_eq!(results[0], Ok(serde_json::Value::Null));
|
||||
assert_eq!(results[1], Ok(serde_json::Value::Null));
|
||||
}
|
||||
}
|
||||
155
backend/windmill-runtime-nativets/src/dedicated.rs
Normal file
155
backend/windmill-runtime-nativets/src/dedicated.rs
Normal file
@@ -0,0 +1,155 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use serde_json::value::RawValue;
|
||||
|
||||
use crate::{
|
||||
create_nativets_runtime, execute_main, load_client_module, load_user_module, CreatedRuntime,
|
||||
ExecuteError, MainArgs, NativeAnnotation,
|
||||
};
|
||||
|
||||
pub struct PrewarmedResult {
|
||||
pub result: Result<Box<RawValue>, String>,
|
||||
pub logs: String,
|
||||
}
|
||||
|
||||
pub struct ExecutingIsolate {
|
||||
result_rx: tokio::sync::oneshot::Receiver<PrewarmedResult>,
|
||||
handle: tokio::task::JoinHandle<anyhow::Result<()>>,
|
||||
}
|
||||
|
||||
impl ExecutingIsolate {
|
||||
pub async fn wait(self) -> anyhow::Result<PrewarmedResult> {
|
||||
let result = self
|
||||
.result_rx
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("isolate result channel closed"))?;
|
||||
self.handle
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("isolate thread panicked: {e}"))??;
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PrewarmedIsolate {
|
||||
args_tx: Option<tokio::sync::oneshot::Sender<String>>,
|
||||
result_rx: Option<tokio::sync::oneshot::Receiver<PrewarmedResult>>,
|
||||
ready_rx: Option<tokio::sync::oneshot::Receiver<()>>,
|
||||
handle: Option<tokio::task::JoinHandle<anyhow::Result<()>>>,
|
||||
}
|
||||
|
||||
/// Parse a JSON args object and reorder into positional args matching `arg_names`.
|
||||
fn args_to_positional(args_json: &str, arg_names: &[String]) -> Vec<Option<Box<RawValue>>> {
|
||||
let map: HashMap<String, Box<RawValue>> = serde_json::from_str(args_json).unwrap_or_default();
|
||||
arg_names
|
||||
.iter()
|
||||
.map(|name| map.get(name).cloned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl PrewarmedIsolate {
|
||||
/// Spawn a new isolate on a blocking thread.
|
||||
///
|
||||
/// The isolate loads `env_code` + WINDMILL_CLIENT as `windmill.ts`,
|
||||
/// then loads `js_code` as `eval.ts`, and waits for args to execute.
|
||||
pub fn spawn(
|
||||
env_code: String,
|
||||
js_code: String,
|
||||
ann: NativeAnnotation,
|
||||
arg_names: Vec<String>,
|
||||
) -> Self {
|
||||
let (args_tx, args_rx) = tokio::sync::oneshot::channel::<String>();
|
||||
let (result_tx, result_rx) = tokio::sync::oneshot::channel::<PrewarmedResult>();
|
||||
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel::<()>();
|
||||
|
||||
let handle = tokio::task::spawn_blocking(move || {
|
||||
let CreatedRuntime { mut js_runtime, log_receiver, mut memory_limit_rx } =
|
||||
create_nativets_runtime(ann, vec![])?;
|
||||
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()?;
|
||||
|
||||
runtime.block_on(async {
|
||||
load_client_module(&mut js_runtime, &env_code).await?;
|
||||
load_user_module(&mut js_runtime, format!("{env_code}\n{js_code}")).await?;
|
||||
|
||||
let _ = ready_tx.send(());
|
||||
|
||||
let args_json = match args_rx.await {
|
||||
Ok(a) => a,
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
|
||||
let positional = args_to_positional(&args_json, &arg_names);
|
||||
{
|
||||
let op_state = js_runtime.op_state();
|
||||
let mut op_state = op_state.borrow_mut();
|
||||
op_state.put(MainArgs { args: positional });
|
||||
}
|
||||
|
||||
let log_handle = tokio::spawn(async move {
|
||||
let mut log_receiver = log_receiver;
|
||||
let mut logs = String::new();
|
||||
while let Some(log) = log_receiver.recv().await {
|
||||
logs.push_str(&log);
|
||||
logs.push('\n');
|
||||
}
|
||||
logs
|
||||
});
|
||||
|
||||
let exec_result = tokio::select! {
|
||||
r = execute_main(&mut js_runtime, None, false, None) => r,
|
||||
_ = memory_limit_rx.recv() => {
|
||||
Err(ExecuteError::Script("Memory limit reached, killing isolate".to_string()))
|
||||
}
|
||||
};
|
||||
|
||||
let result = match exec_result {
|
||||
Ok(raw) => Ok(raw),
|
||||
Err(ExecuteError::Script(msg)) => Err(msg),
|
||||
Err(ExecuteError::Js { message, stack, .. }) => {
|
||||
let msg = message.unwrap_or_default();
|
||||
let err = match stack {
|
||||
Some(s) => format!("{msg}\n{s}"),
|
||||
None => msg,
|
||||
};
|
||||
Err(err)
|
||||
}
|
||||
};
|
||||
|
||||
drop(js_runtime);
|
||||
let logs = log_handle.await.unwrap_or_default();
|
||||
let _ = result_tx.send(PrewarmedResult { result, logs });
|
||||
Ok(())
|
||||
})
|
||||
});
|
||||
|
||||
PrewarmedIsolate {
|
||||
args_tx: Some(args_tx),
|
||||
result_rx: Some(result_rx),
|
||||
ready_rx: Some(ready_rx),
|
||||
handle: Some(handle),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wait for the isolate to finish loading modules.
|
||||
pub async fn wait_ready(&mut self) -> anyhow::Result<()> {
|
||||
if let Some(rx) = self.ready_rx.take() {
|
||||
rx.await
|
||||
.map_err(|_| anyhow::anyhow!("isolate failed during pre-warm"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Send args and start execution. Returns an `ExecutingIsolate` that
|
||||
/// can be awaited independently, allowing the caller to pre-warm
|
||||
/// the next isolate in parallel.
|
||||
pub fn start_execution(mut self, args: String) -> ExecutingIsolate {
|
||||
let args_tx = self.args_tx.take().expect("start_execution called twice");
|
||||
let _ = args_tx.send(args);
|
||||
ExecutingIsolate {
|
||||
result_rx: self.result_rx.take().expect("result_rx missing"),
|
||||
handle: self.handle.take().expect("handle missing"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,9 @@
|
||||
//! TypeScript scripts via the nativets runtime. By isolating this here,
|
||||
//! deno_core compilation no longer blocks windmill-worker or windmill-api.
|
||||
|
||||
mod dedicated;
|
||||
pub use dedicated::{ExecutingIsolate, PrewarmedIsolate, PrewarmedResult};
|
||||
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
cell::RefCell,
|
||||
@@ -112,14 +115,15 @@ impl NetPermissions for PermissionsContainer {
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────
|
||||
|
||||
struct MainArgs {
|
||||
args: Vec<Option<Box<RawValue>>>,
|
||||
pub(crate) struct MainArgs {
|
||||
pub(crate) args: Vec<Option<Box<RawValue>>>,
|
||||
}
|
||||
|
||||
struct LogString {
|
||||
pub s: mpsc::UnboundedSender<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct NativeAnnotation {
|
||||
pub useragent: Option<String>,
|
||||
pub proxy: Option<(String, Option<(String, String)>)>,
|
||||
@@ -145,7 +149,7 @@ impl Drop for IsolateDropGuard {
|
||||
|
||||
static RUNTIME_SNAPSHOT: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/FETCH_SNAPSHOT.bin"));
|
||||
|
||||
const WINDMILL_CLIENT: &str = include_str!("./windmill-client.js");
|
||||
pub(crate) const WINDMILL_CLIENT: &str = include_str!("./windmill-client.js");
|
||||
|
||||
const ERROR_DIR: &str = const_format::concatcp!(TMP_DIR, "/native_errors");
|
||||
|
||||
@@ -177,7 +181,10 @@ pub fn setup_deno_runtime() -> anyhow::Result<()> {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if !unrecognized_v8_flags.is_empty() {
|
||||
init_err = Some(format!("Unrecognized V8 flags: {:?}", unrecognized_v8_flags));
|
||||
init_err = Some(format!(
|
||||
"Unrecognized V8 flags: {:?}",
|
||||
unrecognized_v8_flags
|
||||
));
|
||||
}
|
||||
|
||||
// Use an unprotected platform that doesn't enforce thread-isolated allocations
|
||||
@@ -349,6 +356,140 @@ fn op_log(op_state: Rc<RefCell<OpState>>, #[string] log: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shared V8 runtime creation ───────────────────────────────────────
|
||||
|
||||
pub(crate) struct CreatedRuntime {
|
||||
pub(crate) js_runtime: JsRuntime,
|
||||
pub(crate) log_receiver: mpsc::UnboundedReceiver<String>,
|
||||
pub(crate) memory_limit_rx: mpsc::UnboundedReceiver<()>,
|
||||
}
|
||||
|
||||
/// Create a JsRuntime with the standard nativets extensions, heap limit
|
||||
/// callback, and log channel. Must be called on a blocking thread (not
|
||||
/// on the async tokio runtime) because V8 isolate creation is
|
||||
/// synchronous and potentially heavy.
|
||||
pub(crate) fn create_nativets_runtime(
|
||||
ann: NativeAnnotation,
|
||||
initial_args: Vec<Option<Box<RawValue>>>,
|
||||
) -> anyhow::Result<CreatedRuntime> {
|
||||
let ops = vec![op_get_static_args(), op_log()];
|
||||
let ext = Extension { name: "windmill", ops: ops.into(), ..Default::default() };
|
||||
|
||||
let fetch_options = deno_fetch::Options {
|
||||
root_cert_store_provider: None,
|
||||
user_agent: ann.useragent.unwrap_or_else(|| "windmill/beta".to_string()),
|
||||
proxy: ann.proxy.map(|x| deno_tls::Proxy {
|
||||
url: x.0,
|
||||
basic_auth: x
|
||||
.1
|
||||
.map(|(username, password)| deno_tls::BasicAuth { username, password }),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let exts: Vec<Extension> = vec![
|
||||
deno_telemetry::deno_telemetry::init_ops(),
|
||||
deno_webidl::deno_webidl::init_ops(),
|
||||
deno_url::deno_url::init_ops(),
|
||||
deno_console::deno_console::init_ops(),
|
||||
deno_web::deno_web::init_ops::<PermissionsContainer>(Arc::new(BlobStore::default()), None),
|
||||
deno_fetch::deno_fetch::init_ops::<PermissionsContainer>(fetch_options),
|
||||
deno_net::deno_net::init_ops::<PermissionsContainer>(None, None),
|
||||
ext,
|
||||
];
|
||||
|
||||
let options = RuntimeOptions {
|
||||
is_main: true,
|
||||
extensions: exts,
|
||||
create_params: Some(
|
||||
deno_core::v8::CreateParams::default().heap_limits(0, 1024 * 1024 * 128),
|
||||
),
|
||||
startup_snapshot: Some(RUNTIME_SNAPSHOT),
|
||||
module_loader: Some(Rc::new(deno_core::FsModuleLoader)),
|
||||
extension_transpiler: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (memory_limit_tx, memory_limit_rx) = mpsc::unbounded_channel::<()>();
|
||||
|
||||
setup_deno_runtime().expect("V8 platform init failed");
|
||||
|
||||
let mut js_runtime = {
|
||||
let _v8_lock = V8_ISOLATE_CREATE_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
JsRuntime::new(options)
|
||||
};
|
||||
|
||||
js_runtime.add_near_heap_limit_callback(move |x, y| {
|
||||
tracing::error!("heap limit reached: {x} {y}");
|
||||
if memory_limit_tx.send(()).is_err() {
|
||||
tracing::warn!(
|
||||
"memory limit notification channel closed - isolate may already be terminating"
|
||||
);
|
||||
}
|
||||
y * 2
|
||||
});
|
||||
|
||||
let (log_sender, log_receiver) = mpsc::unbounded_channel::<String>();
|
||||
|
||||
{
|
||||
let op_state = js_runtime.op_state();
|
||||
let mut op_state = op_state.borrow_mut();
|
||||
op_state.put(PermissionsContainer {});
|
||||
op_state.put(MainArgs { args: initial_args });
|
||||
op_state.put(LogString { s: log_sender });
|
||||
}
|
||||
|
||||
Ok(CreatedRuntime { js_runtime, log_receiver, memory_limit_rx })
|
||||
}
|
||||
|
||||
// ── Shared module-loading helpers ────────────────────────────────────
|
||||
|
||||
pub(crate) async fn load_client_module(
|
||||
js_runtime: &mut JsRuntime,
|
||||
env_code: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
js_runtime
|
||||
.load_side_es_module_from_code(
|
||||
&deno_core::resolve_url("file:///windmill.ts")
|
||||
.map_err(windmill_common::error::to_anyhow)?,
|
||||
format!("{env_code}\n{WINDMILL_CLIENT}"),
|
||||
)
|
||||
.await
|
||||
.map_err(windmill_common::error::to_anyhow)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn load_user_module(
|
||||
js_runtime: &mut JsRuntime,
|
||||
source: String,
|
||||
) -> anyhow::Result<()> {
|
||||
use anyhow::Context;
|
||||
js_runtime
|
||||
.load_side_es_module_from_code(
|
||||
&deno_core::resolve_url("file:///eval.ts")
|
||||
.map_err(windmill_common::error::to_anyhow)?,
|
||||
source,
|
||||
)
|
||||
.await
|
||||
.context("failed to load module")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Extract a string result from a resolved V8 global and convert to `Box<RawValue>`.
|
||||
pub(crate) fn extract_global_string(
|
||||
js_runtime: &mut JsRuntime,
|
||||
global: v8::Global<v8::Value>,
|
||||
) -> Result<Box<RawValue>, String> {
|
||||
let scope = &mut js_runtime.handle_scope();
|
||||
let local = v8::Local::new(scope, global);
|
||||
match serde_v8::from_v8::<Option<String>>(scope, local) {
|
||||
Ok(s) => Ok(unsafe_raw(s.unwrap_or_else(|| "null".to_string()))),
|
||||
Err(e) => Err(format!("failed to deserialize result: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
// ── eval_fetch_timeout ───────────────────────────────────────────────
|
||||
|
||||
/// Execute a NativeTS script using deno_core/V8.
|
||||
@@ -436,63 +577,9 @@ pub async fn eval_fetch_timeout(
|
||||
}
|
||||
|
||||
let result_f = tokio::task::spawn_blocking(move || {
|
||||
let ops = vec![op_get_static_args(), op_log()];
|
||||
let ext = Extension { name: "windmill", ops: ops.into(), ..Default::default() };
|
||||
let CreatedRuntime { mut js_runtime, mut log_receiver, mut memory_limit_rx } =
|
||||
create_nativets_runtime(ann, spread)?;
|
||||
|
||||
let fetch_options = deno_fetch::Options {
|
||||
root_cert_store_provider: None,
|
||||
user_agent: ann.useragent.unwrap_or_else(|| "windmill/beta".to_string()),
|
||||
proxy: ann.proxy.map(|x| deno_tls::Proxy {
|
||||
url: x.0,
|
||||
basic_auth: x
|
||||
.1
|
||||
.map(|(username, password)| deno_tls::BasicAuth { username, password }),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let exts: Vec<Extension> = vec![
|
||||
deno_telemetry::deno_telemetry::init_ops(),
|
||||
deno_webidl::deno_webidl::init_ops(),
|
||||
deno_url::deno_url::init_ops(),
|
||||
deno_console::deno_console::init_ops(),
|
||||
deno_web::deno_web::init_ops::<PermissionsContainer>(
|
||||
Arc::new(BlobStore::default()),
|
||||
None,
|
||||
),
|
||||
deno_fetch::deno_fetch::init_ops::<PermissionsContainer>(fetch_options),
|
||||
deno_net::deno_net::init_ops::<PermissionsContainer>(None, None),
|
||||
ext,
|
||||
];
|
||||
|
||||
let options = RuntimeOptions {
|
||||
is_main: true,
|
||||
extensions: exts,
|
||||
create_params: Some(
|
||||
deno_core::v8::CreateParams::default().heap_limits(0, 1024 * 1024 * 128),
|
||||
),
|
||||
startup_snapshot: Some(RUNTIME_SNAPSHOT),
|
||||
module_loader: Some(Rc::new(deno_core::FsModuleLoader)),
|
||||
extension_transpiler: None,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (memory_limit_tx, mut memory_limit_rx) = mpsc::unbounded_channel::<()>();
|
||||
|
||||
// Ensure V8 platform is initialized (idempotent, no-op if already done).
|
||||
setup_deno_runtime().expect("V8 platform init failed");
|
||||
|
||||
// Serialize isolate creation as extra safety net against concurrent V8
|
||||
// isolate creation races. The main fix is the unprotected platform in
|
||||
// setup_deno_runtime(), but this provides defense in depth.
|
||||
let mut js_runtime = {
|
||||
let _v8_lock = V8_ISOLATE_CREATE_LOCK
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner());
|
||||
JsRuntime::new(options)
|
||||
};
|
||||
|
||||
// Bootstrap OpenTelemetry for fetch auto-instrumentation if OTEL was initialized.
|
||||
if otel_initialized {
|
||||
if let Err(e) =
|
||||
js_runtime.execute_script("<otel_bootstrap>", "globalThis.__bootstrapOtel()")
|
||||
@@ -501,24 +588,6 @@ pub async fn eval_fetch_timeout(
|
||||
}
|
||||
}
|
||||
|
||||
js_runtime.add_near_heap_limit_callback(move |x, y| {
|
||||
tracing::error!("heap limit reached: {x} {y}");
|
||||
if memory_limit_tx.send(()).is_err() {
|
||||
tracing::error!("failed to send memory limit reached notification - isolate may already be terminating");
|
||||
};
|
||||
y * 2
|
||||
});
|
||||
|
||||
let (log_sender, mut log_receiver) = mpsc::unbounded_channel::<String>();
|
||||
|
||||
{
|
||||
let op_state = js_runtime.op_state();
|
||||
let mut op_state = op_state.borrow_mut();
|
||||
op_state.put(PermissionsContainer {});
|
||||
op_state.put(MainArgs { args: spread });
|
||||
op_state.put(LogString { s: log_sender });
|
||||
}
|
||||
|
||||
*isolate_handle.lock().unwrap_or_else(|e| e.into_inner()) =
|
||||
Some(js_runtime.v8_isolate().thread_safe_handle());
|
||||
|
||||
@@ -601,42 +670,97 @@ async fn eval_fetch(
|
||||
script_entrypoint_override: Option<String>,
|
||||
load_client: bool,
|
||||
job_id: &Uuid,
|
||||
_otel_initialized: bool,
|
||||
otel_initialized: bool,
|
||||
) -> windmill_common::error::Result<Box<RawValue>> {
|
||||
if load_client {
|
||||
if let Some(env_code) = env_code.as_ref() {
|
||||
let _ = js_runtime
|
||||
.load_side_es_module_from_code(
|
||||
&deno_core::resolve_url("file:///windmill.ts")
|
||||
.map_err(windmill_common::error::to_anyhow)?,
|
||||
format!("{env_code}\n{}", WINDMILL_CLIENT.to_string()),
|
||||
)
|
||||
.await
|
||||
.map_err(windmill_common::error::to_anyhow)?;
|
||||
load_client_module(js_runtime, env_code).await?;
|
||||
}
|
||||
}
|
||||
use anyhow::Context;
|
||||
use deno_core::error::CoreError;
|
||||
use windmill_common::worker::to_raw_value;
|
||||
let source = format!("{}\n{expr}", env_code.unwrap_or_default());
|
||||
let _ = js_runtime
|
||||
.load_side_es_module_from_code(
|
||||
&deno_core::resolve_url("file:///eval.ts")
|
||||
.map_err(windmill_common::error::to_anyhow)?,
|
||||
source.to_string(),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
write_error_expr(expr, &job_id);
|
||||
e
|
||||
})
|
||||
.context("failed to load module")?;
|
||||
if let Err(e) = load_user_module(js_runtime, source.clone()).await {
|
||||
write_error_expr(expr, job_id);
|
||||
return Err(e.into());
|
||||
}
|
||||
|
||||
let main_override = script_entrypoint_override.unwrap_or("main".to_string());
|
||||
let result = execute_main(
|
||||
js_runtime,
|
||||
script_entrypoint_override.as_deref(),
|
||||
otel_initialized,
|
||||
Some(job_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(raw) => Ok(raw),
|
||||
Err(ExecuteError::Script(msg)) => {
|
||||
write_error_expr(expr, job_id);
|
||||
Err(Error::ExecutionErr(msg))
|
||||
}
|
||||
Err(ExecuteError::Js { message, stack, name, source: eval_source }) => {
|
||||
write_error_expr(expr, job_id);
|
||||
use windmill_common::worker::to_raw_value;
|
||||
let stack_head = eval_source.and_then(|(file, line_no)| {
|
||||
if file == "file:///eval.ts" {
|
||||
source
|
||||
.lines()
|
||||
.nth(line_no.saturating_sub(1))
|
||||
.map(|l| format!("{l}\n"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
let stack_s = format!(
|
||||
"{}{}",
|
||||
stack_head.unwrap_or_default(),
|
||||
stack.as_deref().unwrap_or_default()
|
||||
);
|
||||
let stack = if stack_s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(stack_s)
|
||||
};
|
||||
Err(Error::ExecutionRawError(to_raw_value(&serde_json::json!({
|
||||
"message": message,
|
||||
"stack": stack,
|
||||
"name": name,
|
||||
}))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Shared execution engine ──────────────────────────────────────────
|
||||
|
||||
pub(crate) enum ExecuteError {
|
||||
/// Non-JS error (V8 internal, init failure, deserialization)
|
||||
Script(String),
|
||||
/// JS exception with structured error info
|
||||
Js {
|
||||
message: Option<String>,
|
||||
stack: Option<String>,
|
||||
name: Option<String>,
|
||||
/// (file_name, line_number) from the first stack frame, if in user code
|
||||
source: Option<(String, usize)>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Execute the `main` function from the already-loaded `eval.ts` module.
|
||||
///
|
||||
/// Args must already be set in `MainArgs` in the runtime's OpState.
|
||||
/// Modules (`windmill.ts` and `eval.ts`) must already be loaded.
|
||||
pub(crate) async fn execute_main(
|
||||
js_runtime: &mut JsRuntime,
|
||||
entrypoint: Option<&str>,
|
||||
_otel_initialized: bool,
|
||||
_job_id: Option<&Uuid>,
|
||||
) -> Result<Box<RawValue>, ExecuteError> {
|
||||
let main_fn = entrypoint.unwrap_or("main");
|
||||
|
||||
#[cfg(all(feature = "private", feature = "enterprise"))]
|
||||
let otel_context_inject = if _otel_initialized {
|
||||
let trace_id = job_id.as_simple().to_string();
|
||||
let trace_id = _job_id
|
||||
.map(|id| id.as_simple().to_string())
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
r#"globalThis.__enterSpan?.({{
|
||||
isRecording: () => true,
|
||||
@@ -687,7 +811,7 @@ function processStreamIterative(res) {{
|
||||
{otel_context_inject}
|
||||
|
||||
let args = Deno.core.ops.op_get_static_args().map(JSON.parse)
|
||||
import("file:///eval.ts").then((module) => module.{main_override}(...args))
|
||||
import("file:///eval.ts").then((module) => module.{main_fn}(...args))
|
||||
.then(res => {{
|
||||
if (isAsyncIterable(res)) {{
|
||||
return processStreamIterative(res)
|
||||
@@ -698,60 +822,25 @@ import("file:///eval.ts").then((module) => module.{main_override}(...args))
|
||||
"#
|
||||
),
|
||||
)
|
||||
.map_err(|e| {
|
||||
write_error_expr(expr, &job_id);
|
||||
e
|
||||
})
|
||||
.context("native script initialization")?;
|
||||
.map_err(|e| ExecuteError::Script(format!("native script initialization: {e}")))?;
|
||||
|
||||
let fut = js_runtime.resolve(script);
|
||||
let global = js_runtime
|
||||
.with_event_loop_promise(fut, PollEventLoopOptions::default())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
write_error_expr(expr, &job_id);
|
||||
e
|
||||
});
|
||||
.await;
|
||||
|
||||
match global {
|
||||
Ok(global) => {
|
||||
let scope = &mut js_runtime.handle_scope();
|
||||
let local = v8::Local::new(scope, global);
|
||||
let r = serde_v8::from_v8::<Option<String>>(scope, local)
|
||||
.map_err(windmill_common::error::to_anyhow)?;
|
||||
Ok(unsafe_raw(r.unwrap_or_else(|| "null".to_string())))
|
||||
extract_global_string(js_runtime, global).map_err(|e| ExecuteError::Script(e))
|
||||
}
|
||||
Err(CoreError::Js(e)) => {
|
||||
let stack_head = e.frames.first().and_then(|f| {
|
||||
if f.file_name.as_ref().is_some_and(|x| x == "file:///eval.ts") {
|
||||
Some(format!(
|
||||
"{}\n",
|
||||
source
|
||||
.lines()
|
||||
.nth((f.line_number.unwrap_or(1)) as usize - 1)
|
||||
.unwrap_or("")
|
||||
.to_string()
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
Err(deno_core::error::CoreError::Js(e)) => {
|
||||
let source = e.frames.first().and_then(|f| {
|
||||
f.file_name
|
||||
.as_ref()
|
||||
.map(|name| (name.clone(), f.line_number.unwrap_or(1) as usize))
|
||||
});
|
||||
let stack_s = format!(
|
||||
"{}{}",
|
||||
stack_head.unwrap_or("".to_string()),
|
||||
e.stack.unwrap_or("".to_string())
|
||||
);
|
||||
let stack = if stack_s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(stack_s)
|
||||
};
|
||||
Err(Error::ExecutionRawError(to_raw_value(&serde_json::json!({
|
||||
"message": e.message,
|
||||
"stack": stack,
|
||||
"name": e.name,
|
||||
}))))
|
||||
Err(ExecuteError::Js { message: e.message, stack: e.stack, name: e.name, source })
|
||||
}
|
||||
Err(e) => Err(Error::ExecutionErr(e.print_with_cause())),
|
||||
Err(e) => Err(ExecuteError::Script(e.print_with_cause())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,9 +47,9 @@ use windmill_common::{
|
||||
DB,
|
||||
};
|
||||
|
||||
use crate::global_cache::{exists_in_cache, save_cache};
|
||||
#[cfg(all(feature = "enterprise", feature = "parquet"))]
|
||||
use windmill_object_store::attempt_fetch_bytes;
|
||||
use crate::global_cache::{exists_in_cache, save_cache};
|
||||
|
||||
use windmill_parser::Typ;
|
||||
|
||||
@@ -977,8 +977,7 @@ pub async fn handle_bun_job(
|
||||
}
|
||||
};
|
||||
|
||||
let (cache, logs) =
|
||||
crate::global_cache::load_cache(&local_path, &remote_path, false).await;
|
||||
let (cache, logs) = crate::global_cache::load_cache(&local_path, &remote_path, false).await;
|
||||
(cache, logs, local_path, remote_path)
|
||||
} else {
|
||||
(false, "".to_string(), "".to_string(), "".to_string())
|
||||
@@ -1400,13 +1399,7 @@ try {{
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
{
|
||||
let env_code = format!(
|
||||
"const process = {{ env: {{}} }};\nconst BASE_URL = '{base_internal_url}';\nconst BASE_INTERNAL_URL = '{base_internal_url}';\nprocess.env['BASE_URL'] = BASE_URL;process.env['BASE_INTERNAL_URL'] = BASE_INTERNAL_URL;\n{}",
|
||||
reserved_variables
|
||||
.iter()
|
||||
.map(|(k, v)| format!("process.env['{}'] = '{}';\n", k, v))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n"));
|
||||
let env_code = build_nativets_env_code(base_internal_url, &reserved_variables);
|
||||
let js_code = read_file_content(&format!("{job_dir}/main.js")).await?;
|
||||
let started_at = Instant::now();
|
||||
let args = crate::common::build_args_map(job, client, conn)
|
||||
@@ -1661,6 +1654,21 @@ pub async fn get_common_bun_proc_envs(base_internal_url: Option<&str>) -> HashMa
|
||||
return bun_envs;
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "deno_core", feature = "private"))]
|
||||
pub fn build_nativets_env_code(
|
||||
base_internal_url: &str,
|
||||
reserved_variables: &HashMap<String, String>,
|
||||
) -> String {
|
||||
format!(
|
||||
"const process = {{ env: {{}} }};\nconst BASE_URL = '{base_internal_url}';\nconst BASE_INTERNAL_URL = '{base_internal_url}';\nprocess.env['BASE_URL'] = BASE_URL;process.env['BASE_INTERNAL_URL'] = BASE_INTERNAL_URL;\n{}",
|
||||
reserved_variables
|
||||
.iter()
|
||||
.map(|(k, v)| format!("process.env['{}'] = '{}';", k, v))
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "private")]
|
||||
use crate::{
|
||||
common::build_envs_map, dedicated_worker_oss::handle_dedicated_process, JobCompletedSender,
|
||||
@@ -1672,6 +1680,212 @@ use windmill_common::variables;
|
||||
#[cfg(feature = "private")]
|
||||
use windmill_queue::DedicatedWorkerJob;
|
||||
|
||||
#[cfg(feature = "private")]
|
||||
async fn handle_dedicated_bunnative(
|
||||
inner_content: &str,
|
||||
js_code: &str,
|
||||
env_code: &str,
|
||||
token: &str,
|
||||
worker_name: &str,
|
||||
_w_id: &str,
|
||||
script_path: &str,
|
||||
db: &DB,
|
||||
jobs_rx: Receiver<DedicatedWorkerJob>,
|
||||
killpill_rx: tokio::sync::broadcast::Receiver<()>,
|
||||
job_completed_tx: JobCompletedSender,
|
||||
client: &windmill_common::client::AuthedClient,
|
||||
) -> Result<()> {
|
||||
#[cfg(not(feature = "deno_core"))]
|
||||
{
|
||||
let _ = (
|
||||
inner_content,
|
||||
js_code,
|
||||
env_code,
|
||||
token,
|
||||
worker_name,
|
||||
script_path,
|
||||
db,
|
||||
jobs_rx,
|
||||
killpill_rx,
|
||||
job_completed_tx,
|
||||
client,
|
||||
);
|
||||
return Err(error::Error::internal_err(
|
||||
"deno_core feature is not activated but native dedicated worker was started"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "deno_core")]
|
||||
{
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::common::transform_json;
|
||||
use windmill_common::worker::to_raw_value;
|
||||
use windmill_queue::{append_logs, JobCompleted, MiniCompletedJob};
|
||||
use windmill_runtime_nativets::PrewarmedIsolate;
|
||||
|
||||
let ann = windmill_runtime_nativets::get_annotation(inner_content);
|
||||
let parsed_args =
|
||||
windmill_parser_ts::parse_deno_signature(inner_content, true, false, None)?.args;
|
||||
let arg_names: Vec<String> = parsed_args.into_iter().map(|x| x.name).collect();
|
||||
|
||||
let env_code = env_code.to_string();
|
||||
let js_code = js_code.to_string();
|
||||
|
||||
let mut warm = PrewarmedIsolate::spawn(
|
||||
env_code.clone(),
|
||||
js_code.clone(),
|
||||
ann.clone(),
|
||||
arg_names.clone(),
|
||||
);
|
||||
|
||||
let init_log = format!("dedicated worker nativets: {worker_name}\n\n");
|
||||
let alive = true;
|
||||
let mut killpill_rx = killpill_rx;
|
||||
let mut jobs_rx = jobs_rx;
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = killpill_rx.recv(), if alive => {
|
||||
tracing::info!("received killpill for nativets dedicated worker");
|
||||
break;
|
||||
},
|
||||
job = jobs_rx.recv(), if alive => {
|
||||
if let Some(DedicatedWorkerJob { job, flow_runners, done_tx }) = job {
|
||||
let id = job.id;
|
||||
tracing::info!(
|
||||
"received job on nativets dedicated worker for {script_path}: {id}"
|
||||
);
|
||||
|
||||
let args = if let Some(args) = job.args.as_ref() {
|
||||
if let Some(x) = transform_json(
|
||||
client, &job.workspace_id, &args.0, &job, &db.into(),
|
||||
).await? {
|
||||
serde_json::to_string(&x)
|
||||
.unwrap_or_else(|_| "{}".to_string())
|
||||
} else {
|
||||
serde_json::to_string(&args)
|
||||
.unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
} else {
|
||||
"{}".to_string()
|
||||
};
|
||||
|
||||
if let Err(e) = warm.wait_ready().await {
|
||||
tracing::error!("pre-warmed isolate failed during init: {e}");
|
||||
let result = Arc::new(to_raw_value(&serde_json::json!({
|
||||
"message": format!("isolate init failed: {e}"),
|
||||
"name": "Error",
|
||||
})));
|
||||
append_logs(&id, &job.workspace_id, init_log.clone(), &db.into()).await;
|
||||
job_completed_tx.send_job(JobCompleted {
|
||||
job: MiniCompletedJob::from(job),
|
||||
result,
|
||||
result_columns: None,
|
||||
mem_peak: 0,
|
||||
canceled_by: None,
|
||||
success: false,
|
||||
cached_res_path: None,
|
||||
token: token.to_string(),
|
||||
duration: None,
|
||||
preprocessed_args: None,
|
||||
has_stream: Some(false),
|
||||
from_cache: None,
|
||||
flow_runners,
|
||||
done_tx,
|
||||
}, true).await?;
|
||||
warm = PrewarmedIsolate::spawn(
|
||||
env_code.clone(),
|
||||
js_code.clone(),
|
||||
ann.clone(),
|
||||
arg_names.clone(),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let executing = warm.start_execution(args);
|
||||
|
||||
warm = PrewarmedIsolate::spawn(
|
||||
env_code.clone(),
|
||||
js_code.clone(),
|
||||
ann.clone(),
|
||||
arg_names.clone(),
|
||||
);
|
||||
|
||||
match executing.wait().await {
|
||||
Ok(prewarmed_result) => {
|
||||
let mut logs = init_log.clone();
|
||||
if !prewarmed_result.logs.is_empty() {
|
||||
logs.push_str(&prewarmed_result.logs);
|
||||
}
|
||||
append_logs(&id, &job.workspace_id, logs, &db.into()).await;
|
||||
|
||||
let (result, success) = match prewarmed_result.result {
|
||||
Ok(raw) => (Arc::new(raw), true),
|
||||
Err(e) => (
|
||||
Arc::new(to_raw_value(&serde_json::json!({
|
||||
"message": e,
|
||||
"name": "Error",
|
||||
}))),
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
job_completed_tx.send_job(JobCompleted {
|
||||
job: MiniCompletedJob::from(job),
|
||||
result,
|
||||
result_columns: None,
|
||||
mem_peak: 0,
|
||||
canceled_by: None,
|
||||
success,
|
||||
cached_res_path: None,
|
||||
token: token.to_string(),
|
||||
duration: None,
|
||||
preprocessed_args: None,
|
||||
has_stream: Some(false),
|
||||
from_cache: None,
|
||||
flow_runners,
|
||||
done_tx,
|
||||
}, true).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("isolate execution failed: {e}");
|
||||
append_logs(&id, &job.workspace_id, init_log.clone(), &db.into()).await;
|
||||
let result = Arc::new(to_raw_value(&serde_json::json!({
|
||||
"message": format!("{e}"),
|
||||
"name": "Error",
|
||||
})));
|
||||
job_completed_tx.send_job(JobCompleted {
|
||||
job: MiniCompletedJob::from(job),
|
||||
result,
|
||||
result_columns: None,
|
||||
mem_peak: 0,
|
||||
canceled_by: None,
|
||||
success: false,
|
||||
cached_res_path: None,
|
||||
token: token.to_string(),
|
||||
duration: None,
|
||||
preprocessed_args: None,
|
||||
has_stream: Some(false),
|
||||
from_cache: None,
|
||||
flow_runners: None,
|
||||
done_tx: None,
|
||||
}, true).await?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tracing::debug!("job channel closed for nativets dedicated worker");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "private")]
|
||||
pub async fn start_worker(
|
||||
requirements_o: Option<String>,
|
||||
@@ -1704,7 +1918,9 @@ pub async fn start_worker(
|
||||
let mut annotation = windmill_common::worker::TypeScriptAnnotations::parse(inner_content);
|
||||
|
||||
//TODO: remove this when bun dedicated workers work without issues
|
||||
annotation.nodejs = true;
|
||||
if !annotation.native {
|
||||
annotation.nodejs = true;
|
||||
}
|
||||
|
||||
let context = variables::get_reserved_variables(
|
||||
&Connection::from(db.clone()),
|
||||
@@ -1728,6 +1944,81 @@ pub async fn start_worker(
|
||||
.await;
|
||||
let context_envs = build_envs_map(context.to_vec()).await;
|
||||
|
||||
if annotation.native {
|
||||
// Native (V8) dedicated worker: bundle the code and dispatch to V8 instead of a subprocess.
|
||||
let main_code = remove_pinned_imports(inner_content)?;
|
||||
write_file(job_dir, "main.ts", &main_code)?;
|
||||
|
||||
if let Some(reqs) = requirements_o.as_ref() {
|
||||
let (pkg, lock, empty, is_binary) = split_lockfile(reqs);
|
||||
write_file(job_dir, "package.json", pkg)?;
|
||||
if let Some(lock) = lock {
|
||||
if !empty {
|
||||
write_lock(lock, job_dir, is_binary).await?;
|
||||
install_bun_lockfile(
|
||||
&mut mem_peak,
|
||||
&mut canceled_by,
|
||||
&Uuid::nil(),
|
||||
w_id,
|
||||
Some(&Connection::from(db.clone())),
|
||||
job_dir,
|
||||
worker_name,
|
||||
common_bun_proc_envs.clone(),
|
||||
annotation.npm,
|
||||
&mut None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
build_loader(
|
||||
job_dir,
|
||||
base_internal_url,
|
||||
token,
|
||||
w_id,
|
||||
script_path,
|
||||
LoaderMode::BrowserBundle,
|
||||
)
|
||||
.await?;
|
||||
generate_bun_bundle(
|
||||
job_dir,
|
||||
w_id,
|
||||
&Uuid::nil(),
|
||||
worker_name,
|
||||
Some(&Connection::from(db.clone())),
|
||||
None,
|
||||
&mut mem_peak,
|
||||
&mut canceled_by,
|
||||
&common_bun_proc_envs,
|
||||
&mut None,
|
||||
)
|
||||
.await?;
|
||||
let js_code = read_file_content(&format!("{job_dir}/main.js")).await?;
|
||||
|
||||
let reserved_variables: HashMap<String, String> = context
|
||||
.iter()
|
||||
.map(|x| (x.name.clone(), x.value.clone()))
|
||||
.collect();
|
||||
let env_code = build_nativets_env_code(base_internal_url, &reserved_variables);
|
||||
|
||||
return handle_dedicated_bunnative(
|
||||
inner_content,
|
||||
&js_code,
|
||||
&env_code,
|
||||
token,
|
||||
worker_name,
|
||||
w_id,
|
||||
script_path,
|
||||
db,
|
||||
jobs_rx,
|
||||
killpill_rx,
|
||||
job_completed_tx,
|
||||
&client,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
let mut format = BundleFormat::Cjs;
|
||||
if let Some(codebase) = codebase.as_ref() {
|
||||
let pulled_codebase = pull_codebase(w_id, codebase, job_dir).await?;
|
||||
|
||||
@@ -37,7 +37,7 @@ async function verifyOutputs(uuids: string[], workspace: string) {
|
||||
console.log(`Incorrect results: ${incorrectResults}`);
|
||||
}
|
||||
|
||||
export const NON_TEST_TAGS = ["deno", "python", "go", "bash", "dedicated", "bun", "nativets", "flow"]
|
||||
export const NON_TEST_TAGS = ["deno", "python", "go", "bash", "dedicated", "bun", "nativets", "dedicated_nativets", "flow"]
|
||||
export async function main({
|
||||
host,
|
||||
email,
|
||||
@@ -146,7 +146,7 @@ export async function main({
|
||||
}
|
||||
|
||||
if (
|
||||
["deno", "python", "go", "bash", "dedicated", "bun", "nativets"].includes(
|
||||
["deno", "python", "go", "bash", "dedicated", "bun", "nativets", "dedicated_nativets"].includes(
|
||||
kind
|
||||
)
|
||||
) {
|
||||
@@ -165,7 +165,7 @@ export async function main({
|
||||
kind: "noop",
|
||||
});
|
||||
} else if (
|
||||
["deno", "python", "go", "bash", "dedicated", "bun", "nativets"].includes(
|
||||
["deno", "python", "go", "bash", "dedicated", "bun", "nativets", "dedicated_nativets"].includes(
|
||||
kind
|
||||
)
|
||||
) {
|
||||
@@ -336,6 +336,7 @@ export async function main({
|
||||
!noVerify &&
|
||||
kind !== "noop" &&
|
||||
kind !== "nativets" &&
|
||||
kind !== "dedicated_nativets" &&
|
||||
!kind.startsWith("flow:") &&
|
||||
!kind.startsWith("script:")
|
||||
) {
|
||||
@@ -386,7 +387,7 @@ if (import.meta.main) {
|
||||
)
|
||||
.option(
|
||||
"--kind <kind:string>",
|
||||
"Specifiy the benchmark kind among: deno, identity, python, go, bash, dedicated, bun, noop, 2steps, nativets",
|
||||
"Specifiy the benchmark kind among: deno, identity, python, go, bash, dedicated, bun, noop, 2steps, nativets, dedicated_nativets",
|
||||
{
|
||||
required: true,
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ async function waitForDeployment(workspace: string, hash: string) {
|
||||
if (resp.lock !== null) {
|
||||
return;
|
||||
}
|
||||
} catch (err) { }
|
||||
} catch (err) {}
|
||||
await sleep(0.5);
|
||||
}
|
||||
throw new Error("Script did not deploy in time");
|
||||
@@ -49,7 +49,7 @@ async function waitForDedicatedWorker(workspace: string, path: string) {
|
||||
|
||||
export async function createBenchScript(
|
||||
scriptPattern: string,
|
||||
workspace: string
|
||||
workspace: string,
|
||||
) {
|
||||
const path = `f/benchmarks/${scriptPattern}`;
|
||||
const exists = await windmill.ScriptService.existsScriptByPath({
|
||||
@@ -93,11 +93,14 @@ export async function createBenchScript(
|
||||
language = "deno";
|
||||
} else if (scriptPattern === "nativets") {
|
||||
scriptContent =
|
||||
'export async function main(){ return (await fetch(BASE_URL + "/api/version")).text() }';
|
||||
language = "nativets";
|
||||
'//native\nexport async function main(){ return (await fetch(BASE_URL + "/api/version")).text() }';
|
||||
language = "bunnative";
|
||||
} else if (scriptPattern === "dedicated_nativets") {
|
||||
scriptContent = "//native\nexport function main(){ return 42; }";
|
||||
language = "bunnative";
|
||||
} else {
|
||||
throw new Error(
|
||||
"Could not create script for script pattern " + scriptPattern
|
||||
"Could not create script for script pattern " + scriptPattern,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -109,7 +112,8 @@ export async function createBenchScript(
|
||||
summary: scriptPattern + " benchmark",
|
||||
description: "",
|
||||
language: language as api.NewScript.language,
|
||||
dedicated_worker: scriptPattern === "dedicated",
|
||||
dedicated_worker:
|
||||
scriptPattern === "dedicated" || scriptPattern === "dedicated_nativets",
|
||||
schema: {
|
||||
$schema: "https://json-schema.org/draft/2020-12/schema",
|
||||
properties: schemaProperties,
|
||||
@@ -123,7 +127,7 @@ export async function createBenchScript(
|
||||
|
||||
console.log("Created benchmark script at path", path);
|
||||
|
||||
if (scriptPattern === "dedicated") {
|
||||
if (scriptPattern === "dedicated" || scriptPattern === "dedicated_nativets") {
|
||||
await waitForDedicatedWorker(workspace, path);
|
||||
}
|
||||
}
|
||||
@@ -246,11 +250,15 @@ export const getFlowPayload = (flowPattern: string): api.FlowPreview => {
|
||||
input_transforms: {},
|
||||
language: api.RawScript.language.BASH,
|
||||
type: "rawscript",
|
||||
content: "# let's bloat that bash script, 3.. 2.. 1.. BOOM\n".repeat(100) + `if [[ -z $\{WM_FLOW_JOB_ID+x\} ]]; then\necho "not set"\nelif [[ -z "$WM_FLOW_JOB_ID" ]]; then\necho "empty"\nelse\necho "$WM_FLOW_JOB_ID"\nfi`,
|
||||
content:
|
||||
"# let's bloat that bash script, 3.. 2.. 1.. BOOM\n".repeat(
|
||||
100,
|
||||
) +
|
||||
`if [[ -z $\{WM_FLOW_JOB_ID+x\} ]]; then\necho "not set"\nelif [[ -z "$WM_FLOW_JOB_ID" ]]; then\necho "empty"\nelse\necho "$WM_FLOW_JOB_ID"\nfi`,
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
||||
6
benchmarks/suite_dedicated_nativets.json
Normal file
6
benchmarks/suite_dedicated_nativets.json
Normal file
@@ -0,0 +1,6 @@
|
||||
[
|
||||
{ "kind": "nativets", "jobs": 5000, "noSave": true },
|
||||
{ "kind": "nativets", "jobs": 10000 },
|
||||
{ "kind": "dedicated_nativets", "jobs": 5000, "noSave": true },
|
||||
{ "kind": "dedicated_nativets", "jobs": 10000 }
|
||||
]
|
||||
@@ -61,7 +61,7 @@
|
||||
let selectedTagsInfo: SvelteMap<string, SelectedTagInfo> = $state(new SvelteMap())
|
||||
|
||||
// Languages that support dedicated workers
|
||||
const DEDICATED_WORKER_LANGUAGES = ['python3', 'bun', 'deno']
|
||||
const DEDICATED_WORKER_LANGUAGES = ['python3', 'bun', 'bunnative', 'deno']
|
||||
|
||||
// Resolve workspace script languages and filter to supported languages
|
||||
async function resolveAndFilterRunners(
|
||||
@@ -603,7 +603,7 @@
|
||||
<div class="bg-surface-secondary border-t">
|
||||
{#if runnable.runners.length === 0}
|
||||
<div class="px-9 py-1.5 text-xs text-tertiary italic">
|
||||
No eligible steps (python3/bun/deno)
|
||||
No eligible steps (python3/bun/bunnative/deno)
|
||||
</div>
|
||||
{:else}
|
||||
{#each runnable.runners as runner (runner.stepId)}
|
||||
|
||||
@@ -1426,6 +1426,7 @@
|
||||
disabled={!$enterpriseLicense ||
|
||||
isCloudHosted() ||
|
||||
(script.language != 'bun' &&
|
||||
script.language != 'bunnative' &&
|
||||
script.language != 'python3' &&
|
||||
script.language != 'deno')}
|
||||
size="sm"
|
||||
@@ -1455,8 +1456,8 @@
|
||||
>
|
||||
In this mode, the script is meant to be run on dedicated workers that run
|
||||
the script at native speed. Can reach >1500rps per dedicated worker. Only
|
||||
available on enterprise edition and for Python3, Deno and Bun. For other
|
||||
languages, the efficiency is already on par with deidcated workers since
|
||||
available on enterprise edition and for Python3, Deno, Bun and Bunnative. For other
|
||||
languages, the efficiency is already on par with dedicated workers since
|
||||
they do not spawn a full runtime</Tooltip
|
||||
>
|
||||
{/snippet}
|
||||
|
||||
Reference in New Issue
Block a user