diff --git a/.cargo/config.toml b/.cargo/config.toml index 028f73b9d..a2aeed6ec 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,5 @@ [env] +RUSTY_V8_MIRROR = "https://github.com/supabase/rusty_v8/releases/download" # https://supabase.com/docs/guides/functions/limits SUPABASE_RESOURCE_LIMIT_MEM_MB = "256" SUPABASE_RESOURCE_LIMIT_LOW_MEM_MULTIPLIER = "5" diff --git a/.dprint.json b/.dprint.json index 7a89aa1f8..0c3b76979 100644 --- a/.dprint.json +++ b/.dprint.json @@ -21,7 +21,8 @@ ".git", "target", "crates/base/test_cases/invalid_imports", - "crates/base/test_cases/ai-ort-rust-backend/**/__snapshot__" + "crates/base/test_cases/ai-ort-rust-backend/**/__snapshot__", + "vendor/**" ], "plugins": [ "https://plugins.dprint.dev/typescript-0.93.2.wasm", diff --git a/Cargo.lock b/Cargo.lock index 87f25f85b..884c0fe16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,6 +313,17 @@ dependencies = [ "syn 2.0.90", ] +[[package]] +name = "async-scoped" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4042078ea593edffc452eef14e99fdb2b120caa4ad9618bcdeabc4a023b98740" +dependencies = [ + "futures", + "pin-project", + "tokio", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -849,6 +860,7 @@ version = "0.1.0" dependencies = [ "anyhow", "arc-swap", + "async-scoped", "async-trait", "async-tungstenite", "base_mem_check", @@ -1026,7 +1038,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.12.1", + "itertools 0.11.0", "log", "prettyplease", "proc-macro2", @@ -1730,7 +1742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b28bfe653d79bd16c77f659305b195b82bb5ce0c0eb2a4846b82ddbd77586813" dependencies = [ "bitflags 2.6.0", - "libloading 0.8.1", + "libloading 0.7.4", "winapi", ] @@ -2569,12 +2581,11 @@ dependencies = [ [[package]] name = "deno_unsync" version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d774fd83f26b24f0805a6ab8b26834a0d06ceac0db517b769b1e4633c96a2057" dependencies = [ "futures", "parking_lot", "tokio", + "tokio-util", ] [[package]] @@ -6203,7 +6214,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "157c5a9d7ea5c2ed2d9fb8f495b64759f7816c7eaea54ba3978f0d63000162e3" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.11.0", "proc-macro2", "quote", "syn 2.0.90", @@ -8900,9 +8911,8 @@ dependencies = [ [[package]] name = "v8" -version = "130.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee0be58935708fa4d7efb970c6cf9f2d9511d24ee24246481a65b6ee167348d" +version = "130.0.7" +source = "git+https://github.com/supabase/rusty_v8?tag=v130.0.7#57d783ad71294f2677394e2aaf2db1ae162c16d0" dependencies = [ "bindgen", "bitflags 2.6.0", @@ -9169,7 +9179,7 @@ dependencies = [ "js-sys", "khronos-egl", "libc", - "libloading 0.8.1", + "libloading 0.7.4", "log", "metal", "naga", diff --git a/Cargo.toml b/Cargo.toml index e3d26430d..32c4cfabd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -200,9 +200,10 @@ winapi = "=0.3.9" windows-sys = { version = "0.59.0", features = ["Win32_Foundation", "Win32_Media", "Win32_Storage_FileSystem"] } [patch.crates-io] -# If the PR is merged upstream, remove the line below. deno_core = { git = "https://github.com/supabase/deno_core", branch = "324-supabase" } eszip = { git = "https://github.com/supabase/eszip", branch = "fix-pub-vis-0-80-1" } +v8 = { git = "https://github.com/supabase/rusty_v8", tag = "v130.0.7" } +deno_unsync = { path = "./vendor/deno_unsync" } [profile.dind] inherits = "dev" diff --git a/crates/base/Cargo.toml b/crates/base/Cargo.toml index c6da219fc..c7202be6b 100644 --- a/crates/base/Cargo.toml +++ b/crates/base/Cargo.toml @@ -70,6 +70,7 @@ urlencoding.workspace = true uuid.workspace = true arc-swap = "1.7" +async-scoped = { version = "0.9", features = ["use-tokio"] } cooked-waker = "5" flume = "0.11.0" strum = { version = "0.25", features = ["derive"] } diff --git a/crates/base/src/runtime/mod.rs b/crates/base/src/runtime/mod.rs index 62fbef474..8eccb3943 100644 --- a/crates/base/src/runtime/mod.rs +++ b/crates/base/src/runtime/mod.rs @@ -10,7 +10,6 @@ use std::rc::Rc; use std::sync::Arc; use std::sync::RwLock; use std::task::Poll; -use std::thread::ThreadId; use std::time::Duration; use anyhow::anyhow; @@ -24,8 +23,10 @@ use base_rt::get_current_cpu_time_ns; use base_rt::BlockingScopeCPUUsage; use base_rt::DenoRuntimeDropToken; use base_rt::DropToken; +use base_rt::RuntimeState; use cooked_waker::IntoWaker; use cooked_waker::WakeRef; +use cpu_timer::CPUTimer; use ctor::ctor; use deno::args::CacheSetting; use deno::deno_crypto; @@ -51,13 +52,13 @@ use deno_cache::SqliteBackedCache; use deno_core::error::AnyError; use deno_core::error::JsError; use deno_core::serde_json; -use deno_core::unsync::sync::AtomicFlag; use deno_core::url::Url; use deno_core::v8; use deno_core::v8::GCCallbackFlags; use deno_core::v8::GCType; use deno_core::v8::HeapStatistics; use deno_core::v8::Isolate; +use deno_core::v8::Locker; use deno_core::JsRuntime; use deno_core::ModuleId; use deno_core::ModuleLoader; @@ -74,6 +75,7 @@ use deno_facade::module_loader::RuntimeProviders; use deno_facade::EmitterFactory; use deno_facade::EszipPayloadKind; use deno_facade::Metadata; +use either::Either; use ext_event_worker::events::EventMetadata; use ext_event_worker::events::WorkerEventWithMetadata; use ext_runtime::cert::ValueRootCertStoreProvider; @@ -98,17 +100,15 @@ use permissions::get_default_permissions; use scopeguard::ScopeGuard; use serde::Serialize; use strum::IntoStaticStr; +use tokio::runtime::Handle; use tokio::sync::mpsc; -use tokio::sync::OwnedSemaphorePermit; -use tokio::sync::Semaphore; use tokio::time::interval; use tokio_util::sync::CancellationToken; -use tokio_util::sync::PollSemaphore; use tracing::debug; -use tracing::debug_span; use tracing::instrument; use tracing::trace; use tracing::Instrument; +use tracing::Span; use crate::inspector_server::Inspector; use crate::snapshot; @@ -122,6 +122,7 @@ use crate::worker::DuplexStreamEntry; use crate::worker::Worker; mod ops; +mod unsync; pub mod permissions; @@ -155,15 +156,6 @@ pub static SHOULD_INCLUDE_MALLOCED_MEMORY_ON_MEMCHECK: OnceCell = OnceCell::new(); pub static MAYBE_DENO_VERSION: OnceCell = OnceCell::new(); -thread_local! { - // NOTE: Suppose we have met `.await` points while initializing a - // DenoRuntime. In that case, the current v8 isolate's thread-local state - // can be corrupted by a task initializing another DenoRuntime, so we must - // prevent this with a Semaphore. - - static RUNTIME_CREATION_SEM: Arc = Arc::new(Semaphore::new(1)); -} - #[ctor] fn init_v8_platform() { set_v8_flags(); @@ -306,33 +298,6 @@ pub enum WillTerminateReason { Termination, } -#[derive(Debug, Clone, Default)] -pub struct RuntimeState { - pub evaluating_mod: Arc, - pub event_loop_completed: Arc, - pub terminated: Arc, - pub found_inspector_session: Arc, - pub mem_reached_half: Arc, -} - -impl RuntimeState { - pub fn is_evaluating_mod(&self) -> bool { - self.evaluating_mod.is_raised() - } - - pub fn is_event_loop_completed(&self) -> bool { - self.event_loop_completed.is_raised() - } - - pub fn is_terminated(&self) -> bool { - self.terminated.is_raised() - } - - pub fn is_found_inspector_session(&self) -> bool { - self.found_inspector_session.is_raised() - } -} - #[derive(Debug)] pub struct RunOptions { wait_termination_request_token: bool, @@ -412,12 +377,15 @@ pub struct DenoRuntime { pub conf: WorkerRuntimeOpts, pub s3_fs: Option, - main_module_id: ModuleId, + entrypoint: Option, + main_module_url: Url, + main_module_id: Option, + worker: Worker, promise_metrics: PromiseMetrics, mem_check: Arc, - waker: Arc, + pub waker: Arc, beforeunload_mem_threshold: Arc>, beforeunload_cpu_threshold: Arc>, @@ -434,6 +402,18 @@ impl Drop for DenoRuntime { ); } + self.assert_isolate_not_locked(); + let isolate = self.js_runtime.v8_isolate(); + let locker = unsafe { + Locker::new(std::mem::transmute::<&mut Isolate, &mut Isolate>(isolate)) + }; + + isolate.set_slot(locker); + + { + let _scope = self.js_runtime.handle_scope(); + } + unsafe { ManuallyDrop::drop(&mut self.js_runtime); } @@ -442,16 +422,18 @@ impl Drop for DenoRuntime { } } -impl DenoRuntime { - pub async fn acquire() -> OwnedSemaphorePermit { - RUNTIME_CREATION_SEM - .with(|v| v.clone()) - .acquire_owned() - .await - .unwrap() +impl DenoRuntime { + #[inline] + fn assert_isolate_not_locked(&mut self) { + assert_isolate_not_locked(self.js_runtime.v8_isolate()); } } +#[inline] +fn assert_isolate_not_locked(isolate: &v8::Isolate) { + assert!(!Locker::is_locked(isolate)); +} + impl DenoRuntime where RuntimeContext: GetRuntimeContext, @@ -478,19 +460,13 @@ where .. } = init_opts.unwrap(); - let base_dir_path = - std::env::current_dir().map(|p| p.join(&service_path))?; - - // TODO(Nyannyacha): Make sure `service_path` is an absolute path first. - let drop_token = CancellationToken::default(); + let is_user_worker = conf.is_user_worker(); + let is_some_entry_point = maybe_entrypoint.is_some(); let termination_request_token = CancellationToken::default(); let promise_metrics = PromiseMetrics::default(); let runtime_state = Arc::::default(); - let is_user_worker = conf.is_user_worker(); - let is_some_entry_point = maybe_entrypoint.is_some(); - let maybe_user_conf = conf.as_user_worker(); let context = conf.context().cloned().unwrap_or_default(); @@ -498,547 +474,645 @@ where .and_then(|it| it.permissions.clone()) .unwrap_or_else(|| get_default_permissions(conf.to_worker_kind())); - let eszip = if let Some(eszip_payload) = maybe_eszip { - eszip_payload - } else { - let Ok(base_dir_url) = Url::from_directory_path(&base_dir_path) else { - bail!( - "malformed base directory: {}", - base_dir_path.to_string_lossy() - ); - }; + struct Bootstrap { + js_runtime: JsRuntime, + mem_check: Arc, + has_inspector: bool, + main_module_url: Url, + entrypoint: Option, + context: Option>, + s3_fs: Option, + beforeunload_cpu_threshold: ArcSwapOption, + beforeunload_mem_threshold: ArcSwapOption, + } - let mut main_module_url = None; - let only_module_code = maybe_module_code.is_some() - && maybe_eszip.is_none() - && !is_some_entry_point; + let bootstrap_fn = || { + async { + // TODO(Nyannyacha): Make sure `service_path` is an absolute path first. + let base_dir_path = + std::env::current_dir().map(|p| p.join(&service_path))?; - if only_module_code { - main_module_url = None; - } else { - static POTENTIAL_EXTS: &[&str] = &["ts", "tsx", "js", "mjs", "jsx"]; - - let mut found = false; - for ext in POTENTIAL_EXTS.iter() { - let url = base_dir_url.join(format!("index.{}", ext).as_str())?; - if url.to_file_path().unwrap().exists() { - found = true; - main_module_url = Some(url); - break; + let eszip = if let Some(eszip_payload) = maybe_eszip { + eszip_payload + } else { + let Ok(base_dir_url) = Url::from_directory_path(&base_dir_path) + else { + bail!( + "malformed base directory: {}", + base_dir_path.to_string_lossy() + ); + }; + + let mut main_module_url = None; + let only_module_code = maybe_module_code.is_some() + && maybe_eszip.is_none() + && !is_some_entry_point; + + if only_module_code { + main_module_url = None; + } else { + static POTENTIAL_EXTS: &[&str] = &["ts", "tsx", "js", "mjs", "jsx"]; + + let mut found = false; + for ext in POTENTIAL_EXTS.iter() { + let url = base_dir_url.join(format!("index.{}", ext).as_str())?; + if url.to_file_path().unwrap().exists() { + found = true; + main_module_url = Some(url); + break; + } + } + if !is_some_entry_point && !found { + main_module_url = Some(base_dir_url.clone()); + } + } + if is_some_entry_point { + main_module_url = + Some(Url::parse(&maybe_entrypoint.clone().unwrap())?); } - } - if !is_some_entry_point && !found { - main_module_url = Some(base_dir_url.clone()); - } - } - if is_some_entry_point { - main_module_url = Some(Url::parse(&maybe_entrypoint.clone().unwrap())?); - } - let mut emitter_factory = EmitterFactory::new(); + let mut emitter_factory = EmitterFactory::new(); - let cache_strategy = if no_module_cache { - CacheSetting::ReloadAll - } else { - CacheSetting::Use - }; + let cache_strategy = if no_module_cache { + CacheSetting::ReloadAll + } else { + CacheSetting::Use + }; - emitter_factory - .set_permissions_options(Some(permissions_options.clone())); + emitter_factory + .set_permissions_options(Some(permissions_options.clone())); - emitter_factory.set_file_fetcher_allow_remote( - maybe_user_conf - .map(|it| it.allow_remote_modules) - .unwrap_or(true), - ); - emitter_factory.set_cache_strategy(Some(cache_strategy)); + emitter_factory.set_file_fetcher_allow_remote( + maybe_user_conf + .map(|it| it.allow_remote_modules) + .unwrap_or(true), + ); + emitter_factory.set_cache_strategy(Some(cache_strategy)); - let maybe_code = if only_module_code { - maybe_module_code - } else { - None - }; + let maybe_code = if only_module_code { + maybe_module_code + } else { + None + }; - let mut builder = DenoOptionsBuilder::new(); + let mut builder = DenoOptionsBuilder::new(); - if let Some(module_url) = main_module_url.as_ref() { - builder.set_entrypoint(Some(module_url.to_file_path().unwrap())); - } - emitter_factory.set_deno_options(builder.build()?); - - let deno_options = emitter_factory.deno_options()?; - if !is_some_entry_point - && main_module_url.is_some_and(|it| it == base_dir_url) - && deno_options - .workspace() - .root_pkg_json() - .and_then(|it| it.main(deno_package_json::NodeModuleKind::Cjs)) - .is_none() - { - bail!("could not find an appropriate entrypoint"); - } - let mut metadata = Metadata::default(); - let eszip = generate_binary_eszip( - &mut metadata, - Arc::new(emitter_factory), - maybe_code, - // here we don't want to add extra cost, so we won't use a checksum - None, - Some(static_patterns.iter().map(|s| s.as_str()).collect()), - ) - .await?; + if let Some(module_url) = main_module_url.as_ref() { + builder.set_entrypoint(Some(module_url.to_file_path().unwrap())); + } + emitter_factory.set_deno_options(builder.build()?); + + let deno_options = emitter_factory.deno_options()?; + if !is_some_entry_point + && main_module_url.is_some_and(|it| it == base_dir_url) + && deno_options + .workspace() + .root_pkg_json() + .and_then(|it| it.main(deno_package_json::NodeModuleKind::Cjs)) + .is_none() + { + bail!("could not find an appropriate entrypoint"); + } + let mut metadata = Metadata::default(); + let eszip = generate_binary_eszip( + &mut metadata, + Arc::new(emitter_factory), + maybe_code, + // here we don't want to add extra cost, so we won't use a checksum + None, + Some(static_patterns.iter().map(|s| s.as_str()).collect()), + ) + .await?; - EszipPayloadKind::Eszip(eszip) - }; + EszipPayloadKind::Eszip(eszip) + }; - let root_cert_store_provider = get_root_cert_store_provider()?; - let stdio = if is_user_worker { - let stdio_pipe = deno_io::StdioPipe::file( - tokio::fs::File::create("/dev/null").await?.into_std().await, - ); + let root_cert_store_provider = get_root_cert_store_provider()?; + let stdio = if is_user_worker { + let stdio_pipe = deno_io::StdioPipe::file( + tokio::fs::File::create("/dev/null").await?.into_std().await, + ); - deno_io::Stdio { - stdin: stdio_pipe.clone(), - stdout: stdio_pipe.clone(), - stderr: stdio_pipe, - } - } else { - Default::default() - }; + deno_io::Stdio { + stdin: stdio_pipe.clone(), + stdout: stdio_pipe.clone(), + stderr: stdio_pipe, + } + } else { + Default::default() + }; - let has_inspector = worker.inspector.is_some(); - let need_source_map = context - .get("sourceMap") - .and_then(serde_json::Value::as_bool) - .unwrap_or_default(); - let maybe_import_map_path = context - .get("importMapPath") - .and_then(|it| it.as_str()) - .map(str::to_string); - - let rt_provider = create_module_loader_for_standalone_from_eszip_kind( - eszip, - permissions_options, - has_inspector || need_source_map, - Some(MigrateOptions { - maybe_import_map_path, - }), - ) - .await?; - - let RuntimeProviders { - module_loader, - node_services, - npm_snapshot, - permissions, - metadata, - static_files, - vfs, - vfs_path, - base_url, - } = rt_provider; - - let entrypoint = metadata.entrypoint.as_ref(); - let main_module_url = match entrypoint { - Some(Entrypoint::Key(key)) => base_url.join(key)?, - Some(Entrypoint::ModuleCode(_)) | None => Url::parse( - maybe_entrypoint - .as_ref() - .with_context(|| "could not find entrypoint key")?, - )?, - }; + let has_inspector = worker.inspector.is_some(); + let need_source_map = context + .get("sourceMap") + .and_then(serde_json::Value::as_bool) + .unwrap_or_default(); + let maybe_import_map_path = context + .get("importMapPath") + .and_then(|it| it.as_str()) + .map(str::to_string); + + let rt_provider = create_module_loader_for_standalone_from_eszip_kind( + eszip, + permissions_options, + has_inspector || need_source_map, + Some(MigrateOptions { + maybe_import_map_path, + }), + ) + .await?; + + let RuntimeProviders { + module_loader, + node_services, + npm_snapshot, + permissions, + metadata, + static_files, + vfs, + vfs_path, + base_url, + } = rt_provider; + + let entrypoint = metadata.entrypoint.clone(); + let main_module_url = match entrypoint.as_ref() { + Some(Entrypoint::Key(key)) => base_url.join(key)?, + Some(Entrypoint::ModuleCode(_)) | None => Url::parse( + maybe_entrypoint + .as_ref() + .with_context(|| "could not find entrypoint key")?, + )?, + }; - let build_file_system_fn = |base_fs: Arc| -> Result< - (Arc, Option), - AnyError, - > { - let tmp_fs = TmpFs::try_from(maybe_tmp_fs_config.unwrap_or_default())?; - let tmp_fs_actual_path = tmp_fs.actual_path().to_path_buf(); - let fs = PrefixFs::new("/tmp", tmp_fs.clone(), Some(base_fs)) - .tmp_dir("/tmp") - .add_fs(tmp_fs_actual_path, tmp_fs); - - Ok( - if let Some(s3_fs) = maybe_s3_fs_config.map(S3Fs::new).transpose()? { - (Arc::new(fs.add_fs("/s3", s3_fs.clone())), Some(s3_fs)) - } else { - (Arc::new(fs), None) - }, - ) - }; + let build_file_system_fn = |base_fs: Arc| -> Result< + (Arc, Option), + AnyError, + > { + let tmp_fs = + TmpFs::try_from(maybe_tmp_fs_config.unwrap_or_default())?; + let tmp_fs_actual_path = tmp_fs.actual_path().to_path_buf(); + let fs = PrefixFs::new("/tmp", tmp_fs.clone(), Some(base_fs)) + .tmp_dir("/tmp") + .add_fs(tmp_fs_actual_path, tmp_fs); + + Ok( + if let Some(s3_fs) = + maybe_s3_fs_config.map(S3Fs::new).transpose()? + { + (Arc::new(fs.add_fs("/s3", s3_fs.clone())), Some(s3_fs)) + } else { + (Arc::new(fs), None) + }, + ) + }; - let (fs, maybe_s3_fs) = build_file_system_fn(if is_user_worker { - Arc::new(StaticFs::new( - static_files, - if matches!(entrypoint, Some(Entrypoint::ModuleCode(_)) | None) - && maybe_entrypoint.is_some() - { - // it is eszip from before v2 - base_url - .to_file_path() - .map_err(|_| anyhow!("failed to resolve base url"))? + let (fs, s3_fs) = build_file_system_fn(if is_user_worker { + Arc::new(StaticFs::new( + static_files, + if matches!(entrypoint, Some(Entrypoint::ModuleCode(_)) | None) + && maybe_entrypoint.is_some() + { + // it is eszip from before v2 + base_url + .to_file_path() + .map_err(|_| anyhow!("failed to resolve base url"))? + } else { + main_module_url + .to_file_path() + .map_err(|_| { + anyhow!("failed to resolve base dir using main module url") + }) + .and_then(|it| { + it.parent() + .map(Path::to_path_buf) + .with_context(|| "failed to determine parent directory") + })? + }, + vfs_path, + vfs, + npm_snapshot, + )) } else { - main_module_url - .to_file_path() - .map_err(|_| { - anyhow!("failed to resolve base dir using main module url") - }) - .and_then(|it| { - it.parent() - .map(Path::to_path_buf) - .with_context(|| "failed to determine parent directory") - })? - }, - vfs_path, - vfs, - npm_snapshot, - )) - } else { - Arc::new(DenoCompileFileSystem::from_rc(vfs)) - })?; - - let extensions = vec![ - deno_telemetry::deno_telemetry::init_ops(), - deno_webidl::deno_webidl::init_ops(), - deno_console::deno_console::init_ops(), - deno_url::deno_url::init_ops(), - deno_web::deno_web::init_ops::( - Arc::new(deno_web::BlobStore::default()), - None, - ), - deno_webgpu::deno_webgpu::init_ops(), - deno_canvas::deno_canvas::init_ops(), - deno_fetch::deno_fetch::init_ops::( - deno_fetch::Options { - user_agent: SUPABASE_UA.clone(), - root_cert_store_provider: Some(root_cert_store_provider.clone()), - ..Default::default() - }, - ), - deno_websocket::deno_websocket::init_ops::( - SUPABASE_UA.clone(), - Some(root_cert_store_provider.clone()), - None, - ), - // TODO: support providing a custom seed for crypto - deno_crypto::deno_crypto::init_ops(None), - deno_broadcast_channel::deno_broadcast_channel::init_ops( - deno_broadcast_channel::InMemoryBroadcastChannel::default(), - ), - deno_net::deno_net::init_ops::( - Some(root_cert_store_provider), - None, - ), - deno_tls::deno_tls::init_ops(), - deno_http::deno_http::init_ops::( - deno_http::Options::default(), - ), - deno_io::deno_io::init_ops(Some(stdio)), - deno_fs::deno_fs::init_ops::(fs.clone()), - ext_ai::ai::init_ops(), - ext_env::env::init_ops(), - ext_os::os::init_ops(), - ext_workers::user_workers::init_ops(), - ext_event_worker::user_event_worker::init_ops(), - ext_event_worker::js_interceptors::js_interceptors::init_ops(), - ext_runtime::runtime_bootstrap::init_ops::(Some( - main_module_url.clone(), - )), - ext_runtime::runtime_net::init_ops(), - ext_runtime::runtime_http::init_ops(), - ext_runtime::runtime_http_start::init_ops(), - // NOTE(AndresP): Order is matters. Otherwise, it will lead to hard - // errors such as SIGBUS depending on the platform. - ext_node::deno_node::init_ops::( - Some(node_services), - fs, - ), - deno_cache::deno_cache::init_ops::(None), - deno::runtime::ops::permissions::deno_permissions::init_ops(), - ops::permissions::base_runtime_permissions::init_ops_and_esm(permissions), - ext_runtime::runtime::init_ops(), - ]; - - let mut create_params = None; - let mut mem_check = MemCheck::default(); - - let beforeunload_cpu_threshold = ArcSwapOption::::from_pointee(None); - let beforeunload_mem_threshold = ArcSwapOption::::from_pointee(None); - - if conf.is_user_worker() { - let conf = maybe_user_conf.unwrap(); - let memory_limit_bytes = mib_to_bytes(conf.memory_limit_mb) as usize; - - beforeunload_mem_threshold.store( - flags - .beforeunload_memory_pct - .and_then(|it| percentage_value(memory_limit_bytes as u64, it)) - .map(Arc::new), - ); - - if conf.cpu_time_hard_limit_ms > 0 { - beforeunload_cpu_threshold.store( - flags - .beforeunload_cpu_pct - .and_then(|it| percentage_value(conf.cpu_time_hard_limit_ms, it)) - .map(Arc::new), - ); - } + Arc::new(DenoCompileFileSystem::from_rc(vfs)) + })?; + + let extensions = vec![ + deno_telemetry::deno_telemetry::init_ops(), + deno_webidl::deno_webidl::init_ops(), + deno_console::deno_console::init_ops(), + deno_url::deno_url::init_ops(), + deno_web::deno_web::init_ops::( + Arc::new(deno_web::BlobStore::default()), + None, + ), + deno_webgpu::deno_webgpu::init_ops(), + deno_canvas::deno_canvas::init_ops(), + deno_fetch::deno_fetch::init_ops::( + deno_fetch::Options { + user_agent: SUPABASE_UA.clone(), + root_cert_store_provider: Some(root_cert_store_provider.clone()), + ..Default::default() + }, + ), + deno_websocket::deno_websocket::init_ops::( + SUPABASE_UA.clone(), + Some(root_cert_store_provider.clone()), + None, + ), + // TODO: support providing a custom seed for crypto + deno_crypto::deno_crypto::init_ops(None), + deno_broadcast_channel::deno_broadcast_channel::init_ops( + deno_broadcast_channel::InMemoryBroadcastChannel::default(), + ), + deno_net::deno_net::init_ops::( + Some(root_cert_store_provider), + None, + ), + deno_tls::deno_tls::init_ops(), + deno_http::deno_http::init_ops::( + deno_http::Options::default(), + ), + deno_io::deno_io::init_ops(Some(stdio)), + deno_fs::deno_fs::init_ops::(fs.clone()), + ext_ai::ai::init_ops(), + ext_env::env::init_ops(), + ext_os::os::init_ops(), + ext_workers::user_workers::init_ops(), + ext_event_worker::user_event_worker::init_ops(), + ext_event_worker::js_interceptors::js_interceptors::init_ops(), + ext_runtime::runtime_bootstrap::init_ops::( + Some(main_module_url.clone()), + ), + ext_runtime::runtime_net::init_ops(), + ext_runtime::runtime_http::init_ops(), + ext_runtime::runtime_http_start::init_ops(), + // NOTE(AndresP): Order is matters. Otherwise, it will lead to hard + // errors such as SIGBUS depending on the platform. + ext_node::deno_node::init_ops::( + Some(node_services), + fs, + ), + deno_cache::deno_cache::init_ops::(None), + deno::runtime::ops::permissions::deno_permissions::init_ops(), + ops::permissions::base_runtime_permissions::init_ops_and_esm( + permissions, + ), + ext_runtime::runtime::init_ops(), + ]; + + let mut create_params = None; + let mut mem_check = MemCheck::default(); + + let beforeunload_cpu_threshold = + ArcSwapOption::::from_pointee(None); + let beforeunload_mem_threshold = + ArcSwapOption::::from_pointee(None); + + if conf.is_user_worker() { + let conf = maybe_user_conf.unwrap(); + let memory_limit_bytes = mib_to_bytes(conf.memory_limit_mb) as usize; + + beforeunload_mem_threshold.store( + flags + .beforeunload_memory_pct + .and_then(|it| percentage_value(memory_limit_bytes as u64, it)) + .map(Arc::new), + ); - let allocator = CustomAllocator::new(memory_limit_bytes); + if conf.cpu_time_hard_limit_ms > 0 { + beforeunload_cpu_threshold.store( + flags + .beforeunload_cpu_pct + .and_then(|it| { + percentage_value(conf.cpu_time_hard_limit_ms, it) + }) + .map(Arc::new), + ); + } - allocator.set_waker(mem_check.waker.clone()); + let allocator = CustomAllocator::new(memory_limit_bytes); - mem_check.limit = Some(memory_limit_bytes); - create_params = Some( - v8::CreateParams::default() - .heap_limits(mib_to_bytes(0) as usize, memory_limit_bytes) - .array_buffer_allocator(allocator.into_v8_allocator()), - ) - }; + allocator.set_waker(mem_check.waker.clone()); - let mem_check = Arc::new(mem_check); - let runtime_options = RuntimeOptions { - extensions, - is_main: true, - inspector: has_inspector, - create_params, - get_error_class_fn: Some(&deno::errors::get_error_class_name), - shared_array_buffer_store: None, - compiled_wasm_module_store: None, - startup_snapshot: snapshot::snapshot(), - module_loader: Some(module_loader), - import_meta_resolve_callback: Some(Box::new( - import_meta_resolve_callback, - )), - ..Default::default() - }; + mem_check.limit = Some(memory_limit_bytes); + create_params = Some( + v8::CreateParams::default() + .heap_limits(mib_to_bytes(0) as usize, memory_limit_bytes) + .array_buffer_allocator(allocator.into_v8_allocator()), + ) + }; - let mut js_runtime = JsRuntime::new(runtime_options); - - let dispatch_fns = { - let context = js_runtime.main_context(); - let scope = &mut js_runtime.handle_scope(); - let context_local = v8::Local::new(scope, context); - let global_obj = context_local.global(scope); - let bootstrap_str = - v8::String::new_external_onebyte_static(scope, b"bootstrap").unwrap(); - let bootstrap_ns = global_obj - .get(scope, bootstrap_str.into()) - .unwrap() - .to_object(scope) - .unwrap(); + let mem_check = Arc::new(mem_check); + let runtime_options = RuntimeOptions { + extensions, + is_main: true, + inspector: has_inspector, + create_params, + get_error_class_fn: Some(&deno::errors::get_error_class_name), + shared_array_buffer_store: None, + compiled_wasm_module_store: None, + startup_snapshot: snapshot::snapshot(), + module_loader: Some(module_loader), + import_meta_resolve_callback: Some(Box::new( + import_meta_resolve_callback, + )), + ..Default::default() + }; - macro_rules! get_global { - ($name:expr) => {{ - let dispatch_fn_str = - v8::String::new_external_onebyte_static(scope, $name).unwrap(); - let dispatch_fn = v8::Local::::try_from( - bootstrap_ns.get(scope, dispatch_fn_str.into()).unwrap(), - ) - .unwrap(); - v8::Global::new(scope, dispatch_fn) - }}; - } + let mut js_runtime = JsRuntime::new(runtime_options); + + let dispatch_fns = { + let context = js_runtime.main_context(); + let scope = &mut js_runtime.handle_scope(); + let context_local = v8::Local::new(scope, context); + let global_obj = context_local.global(scope); + let bootstrap_str = + v8::String::new_external_onebyte_static(scope, b"bootstrap") + .unwrap(); + let bootstrap_ns = global_obj + .get(scope, bootstrap_str.into()) + .unwrap() + .to_object(scope) + .unwrap(); + + macro_rules! get_global { + ($name:expr) => {{ + let dispatch_fn_str = + v8::String::new_external_onebyte_static(scope, $name).unwrap(); + let dispatch_fn = v8::Local::::try_from( + bootstrap_ns.get(scope, dispatch_fn_str.into()).unwrap(), + ) + .unwrap(); + v8::Global::new(scope, dispatch_fn) + }}; + } - DispatchEventFunctions { - dispatch_load_event_fn_global: get_global!(b"dispatchLoadEvent"), - dispatch_beforeunload_event_fn_global: get_global!( - b"dispatchBeforeUnloadEvent" - ), - dispatch_unload_event_fn_global: get_global!(b"dispatchUnloadEvent"), - dispatch_drain_event_fn_global: get_global!(b"dispatchDrainEvent"), - } - }; + DispatchEventFunctions { + dispatch_load_event_fn_global: get_global!(b"dispatchLoadEvent"), + dispatch_beforeunload_event_fn_global: get_global!( + b"dispatchBeforeUnloadEvent" + ), + dispatch_unload_event_fn_global: get_global!( + b"dispatchUnloadEvent" + ), + dispatch_drain_event_fn_global: get_global!(b"dispatchDrainEvent"), + } + }; - { - let main_context = js_runtime.main_context(); - let op_state = js_runtime.op_state(); - let mut op_state = op_state.borrow_mut(); - - op_state.put(dispatch_fns); - op_state.put(promise_metrics.clone()); - op_state.put(runtime_state.clone()); - op_state.put(GlobalMainContext(main_context)); - } + { + let main_context = js_runtime.main_context(); + let op_state = js_runtime.op_state(); + let mut op_state = op_state.borrow_mut(); + + op_state.put(dispatch_fns); + op_state.put(promise_metrics.clone()); + op_state.put(runtime_state.clone()); + op_state.put(GlobalMainContext(main_context)); + } - { - let op_state_rc = js_runtime.op_state(); - let mut op_state = op_state_rc.borrow_mut(); + { + let op_state_rc = js_runtime.op_state(); + let mut op_state = op_state_rc.borrow_mut(); - // NOTE(Andreespirela): We do this because "NODE_DEBUG" is trying to be - // read during initialization, But we need the gotham state to be - // up-to-date. - op_state.put(ext_env::EnvVars::default()); - } + // NOTE(Andreespirela): We do this because "NODE_DEBUG" is trying to be + // read during initialization, But we need the gotham state to be + // up-to-date. + op_state.put(ext_env::EnvVars::default()); + } - if let Some(inspector) = worker.inspector.as_ref() { - inspector.server.register_inspector( - main_module_url.to_string(), - &mut js_runtime, - inspector.should_wait_for_session(), - ); - } + if let Some(inspector) = worker.inspector.as_ref() { + inspector.server.register_inspector( + main_module_url.to_string(), + &mut js_runtime, + inspector.should_wait_for_session(), + ); + } - if is_user_worker { - js_runtime.v8_isolate().add_gc_prologue_callback( - mem_check_gc_prologue_callback_fn, - Arc::as_ptr(&mem_check) as *mut _, - GCType::ALL, - ); + if is_user_worker { + js_runtime.v8_isolate().add_gc_prologue_callback( + mem_check_gc_prologue_callback_fn, + Arc::as_ptr(&mem_check) as *mut _, + GCType::ALL, + ); - js_runtime - .op_state() - .borrow_mut() - .put(MemCheckWaker::from(mem_check.waker.clone())); - } + js_runtime + .op_state() + .borrow_mut() + .put(MemCheckWaker::from(mem_check.waker.clone())); + } - // Bootstrapping stage - let (runtime_context, extra_context, bootstrap_fn) = { - let runtime_context = - serde_json::json!(RuntimeContext::get_runtime_context( - &conf, + Ok(Bootstrap { + js_runtime, + mem_check, has_inspector, - option_env!("GIT_V_TAG"), - )); - - let tokens = { - let op_state = js_runtime.op_state(); - let resource_table = &mut op_state.borrow_mut().resource_table; - serde_json::json!({ - "terminationRequestToken": - resource_table - .add(DropToken(termination_request_token.clone())) + main_module_url, + entrypoint, + context: Some(context), + s3_fs, + beforeunload_cpu_threshold, + beforeunload_mem_threshold, }) - }; + } + .in_current_span() + }; - let extra_context = { - let mut extra_context = - serde_json::json!(RuntimeContext::get_extra_context()); + let span = Span::current(); + let handle = Handle::current(); + let bootstrap_ret = unsafe { + spawn_blocking_non_send(|| -> Result { + let mut bootstrap = handle.block_on(bootstrap_fn())?; + let _span = span.entered(); - json::merge_object( - &mut extra_context, - &serde_json::Value::Object(context), - ); - json::merge_object(&mut extra_context, &tokens); + debug!("bootstrap"); - extra_context - }; + bootstrap.js_runtime.v8_isolate().dispose_scope_root(); + bootstrap.js_runtime.v8_isolate().exit(); - let context = js_runtime.main_context(); - let scope = &mut js_runtime.handle_scope(); - let context_local = v8::Local::new(scope, context); - let global_obj = context_local.global(scope); - let bootstrap_str = - v8::String::new_external_onebyte_static(scope, b"bootstrapSBEdge") - .unwrap(); - let bootstrap_fn = v8::Local::::try_from( - global_obj.get(scope, bootstrap_str.into()).unwrap(), - ) - .unwrap(); + { + assert_isolate_not_locked(bootstrap.js_runtime.v8_isolate()); + let mut locker = bootstrap.js_runtime.with_locker(); + + // Bootstrapping stage + let (runtime_context, extra_context, bootstrap_fn) = { + let runtime_context = + serde_json::json!(RuntimeContext::get_runtime_context( + &conf, + bootstrap.has_inspector, + option_env!("GIT_V_TAG"), + )); + + let tokens = { + let op_state = locker.op_state(); + let resource_table = &mut op_state.borrow_mut().resource_table; + serde_json::json!({ + "terminationRequestToken": + resource_table + .add(DropToken(termination_request_token.clone())) + }) + }; - let runtime_context_local = - deno_core::serde_v8::to_v8(scope, runtime_context) - .context("failed to convert to v8 value")?; - let runtime_context_global = - v8::Global::new(scope, runtime_context_local); - let extra_context_local = - deno_core::serde_v8::to_v8(scope, extra_context) - .context("failed to convert to v8 value")?; - let extra_context_global = v8::Global::new(scope, extra_context_local); - let bootstrap_fn_global = v8::Global::new(scope, bootstrap_fn); - - ( - runtime_context_global, - extra_context_global, - bootstrap_fn_global, - ) - }; + let extra_context = { + let mut extra_context = + serde_json::json!(RuntimeContext::get_extra_context()); - js_runtime - .call_with_args(&bootstrap_fn, &[runtime_context, extra_context]) - .now_or_never() - .transpose() - .context("failed to execute bootstrap script")?; + json::merge_object( + &mut extra_context, + &serde_json::Value::Object( + bootstrap.context.take().unwrap_or_default(), + ), + ); + json::merge_object(&mut extra_context, &tokens); - { - // run inside a closure, so op_state_rc is released - let op_state_rc = js_runtime.op_state(); - let mut op_state = op_state_rc.borrow_mut(); + extra_context + }; + + let context = locker.main_context(); + let scope = &mut locker.handle_scope(); + let context_local = v8::Local::new(scope, context); + let global_obj = context_local.global(scope); + let bootstrap_str = v8::String::new_external_onebyte_static( + scope, + b"bootstrapSBEdge", + ) + .unwrap(); + let bootstrap_fn = v8::Local::::try_from( + global_obj.get(scope, bootstrap_str.into()).unwrap(), + ) + .unwrap(); + + let runtime_context_local = + deno_core::serde_v8::to_v8(scope, runtime_context) + .context("failed to convert to v8 value")?; + let runtime_context_global = + v8::Global::new(scope, runtime_context_local); + let extra_context_local = + deno_core::serde_v8::to_v8(scope, extra_context) + .context("failed to convert to v8 value")?; + let extra_context_global = + v8::Global::new(scope, extra_context_local); + let bootstrap_fn_global = v8::Global::new(scope, bootstrap_fn); + + ( + runtime_context_global, + extra_context_global, + bootstrap_fn_global, + ) + }; - let mut env_vars = env_vars.clone(); + locker + .call_with_args(&bootstrap_fn, &[runtime_context, extra_context]) + .now_or_never() + .transpose() + .context("failed to execute bootstrap script")?; + } - if let Some(opts) = conf.as_events_worker_mut() { - op_state.put::>( - opts.events_msg_rx.take().unwrap(), - ); - } + // from this moment on, using `v8::Locker` is enforced. + Ok(bootstrap) + }) + } + .await; - if conf.is_main_worker() || conf.is_user_worker() { - op_state.put::>(HashMap::new()); + let Bootstrap { + mut js_runtime, + mem_check, + main_module_url, + entrypoint, + s3_fs, + beforeunload_cpu_threshold, + beforeunload_mem_threshold, + .. + } = match bootstrap_ret { + Ok(Ok(v)) => v, + Ok(Err(err)) => { + return Err(err.context("failed to bootstrap runtime")); + } + Err(err) => { + return Err(err).context("failed to bootstrap runtime"); } + }; - if conf.is_user_worker() { - let conf = conf.as_user_worker().unwrap(); + let span = Span::current(); + let post_task_ret = unsafe { + spawn_blocking_non_send(|| { + let _span = span.entered(); - // set execution id for user workers - env_vars.insert( - "SB_EXECUTION_ID".to_string(), - conf.key.map_or("".to_string(), |k| k.to_string()), - ); + debug!("bootstrap post task"); - if let Some(events_msg_tx) = conf.events_msg_tx.clone() { - op_state.put::>( - events_msg_tx, - ); - op_state.put::(EventMetadata { - service_path: conf.service_path.clone(), - execution_id: conf.key, - }); - } - } + { + assert_isolate_not_locked(js_runtime.v8_isolate()); + let mut locker = js_runtime.with_locker(); - op_state.put(ext_env::EnvVars(env_vars)); - op_state.put(DenoRuntimeDropToken(DropToken(drop_token.clone()))); - } + // run inside a closure, so op_state_rc is released + let op_state_rc = locker.op_state(); + let mut op_state = op_state_rc.borrow_mut(); - let main_module_id = { - match entrypoint { - Some(Entrypoint::Key(_)) | None => { - js_runtime.load_main_es_module(&main_module_url).await? - } - Some(Entrypoint::ModuleCode(module_code)) => { - js_runtime - .load_main_es_module_from_code( - &main_module_url, - module_code.to_string(), - ) - .await? - } - } - }; + let mut env_vars = env_vars.clone(); - if is_user_worker { - drop(base_rt::SUPERVISOR_RT.spawn({ - let drop_token = drop_token.clone(); - let waker = mem_check.waker.clone(); - - async move { - // TODO(Nyannyacha): Should we introduce exponential backoff? - let mut int = interval(*ALLOC_CHECK_DUR); - loop { - tokio::select! { - _ = int.tick() => { - waker.wake(); - } + if let Some(opts) = conf.as_events_worker_mut() { + op_state.put::>( + opts.events_msg_rx.take().unwrap(), + ); + } - _ = drop_token.cancelled() => { - break; - } + if conf.is_main_worker() || conf.is_user_worker() { + op_state.put::>(HashMap::new()); + } + + if conf.is_user_worker() { + let conf = conf.as_user_worker().unwrap(); + + // set execution id for user workers + env_vars.insert( + "SB_EXECUTION_ID".to_string(), + conf.key.map_or("".to_string(), |k| k.to_string()), + ); + + if let Some(events_msg_tx) = conf.events_msg_tx.clone() { + op_state.put::>( + events_msg_tx, + ); + op_state.put::(EventMetadata { + service_path: conf.service_path.clone(), + execution_id: conf.key, + }); } } + + op_state.put(ext_env::EnvVars(env_vars)); + op_state.put(DenoRuntimeDropToken(DropToken(drop_token.clone()))); } - })); + + if is_user_worker { + drop(base_rt::SUPERVISOR_RT.spawn({ + let drop_token = drop_token.clone(); + let waker = mem_check.waker.clone(); + + async move { + // TODO(Nyannyacha): Should we introduce exponential backoff? + let mut int = interval(*ALLOC_CHECK_DUR); + loop { + tokio::select! { + _ = int.tick() => { + waker.wake(); + } + + _ = drop_token.cancelled() => { + break; + } + } + } + } + })); + } + }) + } + .await; + + match post_task_ret { + Ok(_) => {} + Err(err) => { + return Err(err).context("failed to bootstrap runtime"); + } } Ok(Self { @@ -1049,9 +1123,12 @@ where termination_request_token, conf, - s3_fs: maybe_s3_fs, + s3_fs, + + entrypoint, + main_module_url, + main_module_id: None, - main_module_id, worker, promise_metrics, @@ -1065,7 +1142,60 @@ where }) } + pub(crate) async fn init_main_module(&mut self) -> Result<(), Error> { + if self.main_module_id.is_some() { + return Ok(()); + } + + let span = Span::current(); + let handle = Handle::current(); + let ret = unsafe { + spawn_blocking_non_send(|| { + handle.block_on( + async { + debug!("initialize main module"); + + self.assert_isolate_not_locked(); + let mut locker = self.with_locker(); + + let entrypoint = locker.entrypoint.take(); + let url = locker.main_module_url.clone(); + + match entrypoint { + Some(Entrypoint::Key(_)) | None => { + locker.js_runtime.load_main_es_module(&url).await + } + Some(Entrypoint::ModuleCode(module_code)) => { + locker + .js_runtime + .load_main_es_module_from_code(&url, module_code.to_string()) + .await + } + } + } + .instrument(span), + ) + }) + } + .await; + + let id = match ret { + Ok(Ok(v)) => v, + Ok(Err(err)) => { + return Err(err); + } + Err(err) => { + return Err(err).context("failed to load the module"); + } + }; + + self.main_module_id = Some(id); + Ok(()) + } + pub async fn run(&mut self, options: RunOptions) -> (Result<(), Error>, i64) { + self.assert_isolate_not_locked(); + let RunOptions { wait_termination_request_token, duplex_stream_rx, @@ -1091,65 +1221,105 @@ where v.raise(); }); - // NOTE: This is unnecessary on the LIFO task scheduler that can't steal the - // task from the other threads. - let current_thread_id = std::thread::current().id(); let mut accumulated_cpu_time_ns = 0i64; - let span = debug_span!("runtime", thread_id = ?current_thread_id); + macro_rules! get_accumulated_cpu_time_ms { + () => { + accumulated_cpu_time_ns / 1_000_000 + }; + } + let inspector = self.inspector(); - let mut mod_result_rx = unsafe { - self.js_runtime.v8_isolate().enter(); + let mod_fut_ret = unsafe { + if let Err(err) = self.init_main_module().await { + return (Err(err), 0i64); + } if inspector.is_some() { - let state = self.runtime_state.clone(); - let mut this = scopeguard::guard_on_unwind(&mut *self, { - |this| { - this.js_runtime.v8_isolate().exit(); + let ret = spawn_blocking_non_send(|| { + let state = self.runtime_state.clone(); + let _guard = scopeguard::guard_on_unwind((), |_| { + state.terminated.raise(); + }); + + self.assert_isolate_not_locked(); + let mut locker = self.with_locker(); + + { + let _guard = + scopeguard::guard(state.found_inspector_session.clone(), |v| { + v.raise(); + }); + + // XXX(Nyannyacha): Suppose the user skips this function by passing + // the `--inspect` argument. In that case, the runtime may terminate + // before the inspector session is connected if the function doesn't + // have a long execution time. Should we wait for an inspector session + // to connect with the V8? + locker.wait_for_inspector_session(); + } + + if locker.termination_request_token.is_cancelled() { state.terminated.raise(); + return false; } - }); - { - let _guard = - scopeguard::guard(state.found_inspector_session.clone(), |v| { - v.raise(); - }); - - // XXX(Nyannyacha): Suppose the user skips this function by passing - // the `--inspect` argument. In that case, the runtime may terminate - // before the inspector session is connected if the function doesn't - // have a long execution time. Should we wait for an inspector session - // to connect with the V8? - this.wait_for_inspector_session(); - } + true + }) + .await + .map_err(Error::from); - if this.termination_request_token.is_cancelled() { - this.js_runtime.v8_isolate().exit(); - state.terminated.raise(); - return (Ok(()), 0i64); + match ret { + Ok(true) => {} + Ok(false) => return (Ok(()), 0i64), + Err(err) => return (Err(err), 0i64), } } - let mut js_runtime = scopeguard::guard(&mut self.js_runtime, |it| { - it.v8_isolate().exit(); - }); + let Some(main_module_id) = self.main_module_id else { + return (Err(anyhow!("failed to get main module id")), 0); + }; - with_cpu_metrics_guard( - current_thread_id, - js_runtime.op_state(), - &maybe_cpu_usage_metrics_tx, - &mut accumulated_cpu_time_ns, - || js_runtime.mod_evaluate(self.main_module_id), - ) - } - .instrument(span.clone()); + let span = Span::current(); + let handle = Handle::current(); - macro_rules! get_accumulated_cpu_time_ms { - () => { - accumulated_cpu_time_ns / 1_000_000 - }; - } + spawn_blocking_non_send(|| { + let _wall = deno_core::unsync::set_wall().drop_guard(); + let init = scopeguard::guard(self.runtime_state.init.clone(), |v| { + v.lower(); + }); + + init.raise(); + handle.block_on( + #[allow(clippy::async_yields_async)] + async { + self.assert_isolate_not_locked(); + let mut locker = self.with_locker(); + + let op_state = locker.js_runtime.op_state(); + + with_cpu_metrics_guard( + op_state, + &maybe_cpu_usage_metrics_tx, + &mut accumulated_cpu_time_ns, + || locker.js_runtime.mod_evaluate(main_module_id), + ) + } + .instrument(span), + ) + }) + .await + }; + + let mut mod_ret_rx = match mod_fut_ret { + Ok(v) => v, + Err(err) => { + return ( + Err(err).context("failed to load the module"), + get_accumulated_cpu_time_ms!(), + ); + } + }; { let evaluating_mod = @@ -1159,21 +1329,18 @@ where evaluating_mod.raise(); - let event_loop_fut = self - .run_event_loop( - current_thread_id, - wait_termination_request_token, - &maybe_cpu_usage_metrics_tx, - &mut accumulated_cpu_time_ns, - ) - .instrument(span.clone()); + let event_loop_fut = self.run_event_loop( + wait_termination_request_token, + &maybe_cpu_usage_metrics_tx, + &mut accumulated_cpu_time_ns, + ); let mod_result = tokio::select! { - // Not using biased mode leads to non-determinism for relatively simple - // programs. + // Not using biased mode leads to non-determinism for relatively + // simple programs. biased; - maybe_mod_result = &mut mod_result_rx => { + maybe_mod_result = &mut mod_ret_rx => { debug!("received module evaluate {:#?}", maybe_mod_result); maybe_mod_result } @@ -1187,7 +1354,7 @@ where ) ) } else { - mod_result_rx.await + mod_ret_rx.await } } }; @@ -1201,17 +1368,19 @@ where return (Ok(()), get_accumulated_cpu_time_ms!()); } - let mut this = self.get_v8_tls_guard(); - - if !this.termination_request_token.is_cancelled() { - if let Err(err) = with_cpu_metrics_guard( - current_thread_id, - this.js_runtime.op_state(), - &maybe_cpu_usage_metrics_tx, - &mut accumulated_cpu_time_ns, - || MaybeDenoRuntime::DenoRuntime(*this).dispatch_load_event(), - ) { - return (Err(err), get_accumulated_cpu_time_ms!()); + { + self.assert_isolate_not_locked(); + let mut locker = unsafe { self.with_locker() }; + + if !locker.termination_request_token.is_cancelled() { + if let Err(err) = with_cpu_metrics_guard( + locker.js_runtime.op_state(), + &maybe_cpu_usage_metrics_tx, + &mut accumulated_cpu_time_ns, + || MaybeDenoRuntime::DenoRuntime(*locker).dispatch_load_event(), + ) { + return (Err(err), get_accumulated_cpu_time_ms!()); + } } } } @@ -1220,12 +1389,10 @@ where if let Err(err) = self .run_event_loop( - current_thread_id, wait_termination_request_token, &maybe_cpu_usage_metrics_tx, &mut accumulated_cpu_time_ns, ) - .instrument(span) .await { return ( @@ -1235,15 +1402,15 @@ where } if !self.conf.is_user_worker() { - let mut this = self.get_v8_tls_guard(); - let mut this = this.get_v8_termination_guard(); + self.assert_isolate_not_locked(); + let mut locker = unsafe { self.with_locker() }; + let mut locker = locker.get_v8_termination_guard(); if let Err(err) = with_cpu_metrics_guard( - current_thread_id, - this.js_runtime.op_state(), + locker.js_runtime.op_state(), &maybe_cpu_usage_metrics_tx, &mut accumulated_cpu_time_ns, - || MaybeDenoRuntime::DenoRuntime(&mut this).dispatch_unload_event(), + || MaybeDenoRuntime::DenoRuntime(&mut locker).dispatch_unload_event(), ) { return (Err(err), get_accumulated_cpu_time_ms!()); } @@ -1257,7 +1424,6 @@ where fn run_event_loop<'l>( &'l mut self, - #[allow(unused_variables)] current_thread_id: ThreadId, wait_termination_request_token: bool, maybe_cpu_usage_metrics_tx: &'l Option< mpsc::UnboundedSender, @@ -1279,39 +1445,29 @@ where let state = self.runtime_state.clone(); let mem_check_state = is_user_worker.then(|| self.mem_check.clone()); - let mut poll_sem = None::; poll_fn(move |cx| { - if poll_sem.is_none() { - poll_sem = - Some(RUNTIME_CREATION_SEM.with(|v| PollSemaphore::new(v.clone()))); - } - - let Poll::Ready(Some(_permit)) = - poll_sem.as_mut().unwrap().poll_acquire(cx) - else { - return Poll::Pending; - }; - - poll_sem = None; - - // INVARIANT: Only can steal current task by other threads when LIFO task - // scheduler heuristic disabled. Turning off the heuristic is unstable - // now, so it's not considered. - #[cfg(debug_assertions)] - assert_eq!(current_thread_id, std::thread::current().id()); - let waker = cx.waker(); let woked = global_waker.take().is_none(); - let thread_id = std::thread::current().id(); global_waker.register(waker); - let mut this = self.get_v8_tls_guard(); + let mut this = { + self.assert_isolate_not_locked(); + unsafe { self.with_locker() } + }; + + if woked { + extern "C" fn dummy(_: &mut v8::Isolate, _: *mut std::ffi::c_void) {} + this + .js_runtime + .v8_isolate() + .thread_safe_handle() + .request_interrupt(dummy, std::ptr::null_mut()); + } let js_runtime = &mut this.js_runtime; let cpu_metrics_guard = get_cpu_metrics_guard( - thread_id, js_runtime.op_state(), maybe_cpu_usage_metrics_tx, accumulated_cpu_time_ns, @@ -1414,6 +1570,10 @@ where && poll_result.is_pending() && termination_request_fut.poll_unpin(cx).is_ready() { + if state.is_evaluating_mod() { + return Poll::Ready(Err(anyhow!("execution terminated"))); + } + return Poll::Ready(Ok(())); } @@ -1502,23 +1662,6 @@ where ) } - fn get_v8_tls_guard<'l>( - &'l mut self, - ) -> scopeguard::ScopeGuard< - &'l mut DenoRuntime, - impl FnOnce(&'l mut DenoRuntime) + 'l, - > { - let mut guard = scopeguard::guard(self, |v| unsafe { - v.js_runtime.v8_isolate().exit(); - }); - - unsafe { - guard.js_runtime.v8_isolate().enter(); - } - - guard - } - fn get_v8_termination_guard<'l>( &'l mut self, ) -> scopeguard::ScopeGuard< @@ -1546,13 +1689,85 @@ where } } +trait JsRuntimeLockerGuard { + fn js_runtime(&mut self) -> &mut JsRuntime; + + unsafe fn with_locker<'l>( + &'l mut self, + ) -> scopeguard::ScopeGuard<&'l mut Self, impl FnOnce(&'l mut Self) + 'l> { + let js_runtime = self.js_runtime(); + let locker = + Locker::new(std::mem::transmute::<&mut Isolate, &mut Isolate>( + js_runtime.v8_isolate(), + )); + + scopeguard::guard(self, move |_| { + drop(locker); + }) + } +} + +impl JsRuntimeLockerGuard for DenoRuntime { + fn js_runtime(&mut self) -> &mut JsRuntime { + &mut self.js_runtime + } +} + +impl JsRuntimeLockerGuard for JsRuntime { + fn js_runtime(&mut self) -> &mut JsRuntime { + self + } +} + +async unsafe fn spawn_blocking_non_send( + non_send_fn: F, +) -> Result +where + F: FnOnce() -> R, + R: 'static, +{ + let span = Span::current(); + let disguised_fn = unsync::MaskValueAsSend { value: non_send_fn }; + let (mut scope, ..) = async_scoped::TokioScope::scope(|s| { + s.spawn_blocking(move || { + let _span = span.entered(); + + debug!(current_thread = ?std::thread::current().id()); + + unsync::MaskValueAsSend { + value: disguised_fn.into_inner()(), + } + }); + }); + + assert_eq!(scope.len(), 1); + let stream = { + let stream = scope.collect().await; + + drop(scope); + stream + }; + + let mut iter = stream + .into_iter() + .map(|it| it.map(unsync::MaskValueAsSend::into_inner)); + + let ret = iter.next(); + assert!(iter.next().is_none()); + + match ret { + Some(v) => v, + None => unreachable!("scope.len() == 1"), + } +} + type TerminateExecutionIfCancelledReturnType = ScopeGuard>; #[allow(dead_code)] struct Scope<'s> { context: v8::Local<'s, v8::Context>, - scope: v8::CallbackScope<'s, ()>, + scope: Either, v8::CallbackScope<'s, ()>>, } impl<'s> Scope<'s> { @@ -1560,7 +1775,15 @@ impl<'s> Scope<'s> { &'l mut self, ) -> v8::ContextScope<'l, v8::HandleScope<'s>> { let context = self.context; - v8::ContextScope::new(&mut self.scope, context) + v8::ContextScope::new( + self + .scope + .as_mut() + .map_left(|it| &mut **it) + .map_right(|it| &mut **it) + .into_inner(), + context, + ) } } @@ -1611,19 +1834,31 @@ where let mut scope = unsafe { match self { - Self::DenoRuntime(v) => { - v8::CallbackScope::new(v.js_runtime.v8_isolate()) + MaybeDenoRuntime::DenoRuntime(v) => { + Either::Left(v8::HandleScope::with_context( + v.js_runtime.v8_isolate(), + context.0.clone(), + )) + } + MaybeDenoRuntime::Isolate(v) => { + Either::Right(v8::CallbackScope::new(&mut **v)) } - Self::Isolate(v) => v8::CallbackScope::new(&mut **v), - Self::IsolateWithCancellationToken(v) => { - v8::CallbackScope::new(&mut **v) + MaybeDenoRuntime::IsolateWithCancellationToken(v) => { + Either::Right(v8::CallbackScope::new(&mut **v)) } } }; - let context = context.to_local_context(&mut scope); + let handle_scope = scope + .as_mut() + .map_left(|it| &mut **it) + .map_right(|it| &mut **it) + .into_inner(); - Scope { context, scope } + Scope { + context: context.to_local_context(handle_scope), + scope, + } } #[allow(unused)] @@ -1771,7 +2006,6 @@ pub fn import_meta_resolve_callback( } fn with_cpu_metrics_guard<'l, F, R>( - thread_id: ThreadId, op_state: Rc>, maybe_cpu_usage_metrics_tx: &'l Option< mpsc::UnboundedSender, @@ -1783,7 +2017,6 @@ where F: FnOnce() -> R, { let _cpu_metrics_guard = get_cpu_metrics_guard( - thread_id, op_state, maybe_cpu_usage_metrics_tx, accumulated_cpu_time_ns, @@ -1793,48 +2026,84 @@ where } fn get_cpu_metrics_guard<'l>( - thread_id: ThreadId, op_state: Rc>, maybe_cpu_usage_metrics_tx: &'l Option< mpsc::UnboundedSender, >, accumulated_cpu_time_ns: &'l mut i64, -) -> scopeguard::ScopeGuard<(), impl FnOnce(()) + 'l> { +) -> scopeguard::ScopeGuard<(), Box> { + let Some(cpu_usage_metrics_tx) = maybe_cpu_usage_metrics_tx.as_ref() else { + return scopeguard::guard((), Box::new(|_| {})); + }; + + #[derive(Clone)] + struct CurrentCPUTimer { + thread_id: std::thread::ThreadId, + timer: CPUTimer, + } + + let current_thread_id = std::thread::current().id(); let send_cpu_metrics_fn = move |metric: CPUUsageMetrics| { - if let Some(cpu_metric_tx) = maybe_cpu_usage_metrics_tx.as_ref() { - let _ = cpu_metric_tx.send(metric); + let _ = cpu_usage_metrics_tx.send(metric); + }; + + let mut state = op_state.borrow_mut(); + let cpu_timer = if state.has::() { + let current_cpu_timer = state.borrow::(); + if current_cpu_timer.thread_id != current_thread_id { + state.take::(); + None + } else { + Some(current_cpu_timer.timer.clone()) } + } else { + None + }; + let cpu_timer = if let Some(timer) = cpu_timer { + timer + } else { + let cpu_timer = CurrentCPUTimer { + thread_id: current_thread_id, + timer: CPUTimer::new().unwrap(), + }; + + state.put(cpu_timer.clone()); + cpu_timer.timer }; - send_cpu_metrics_fn(CPUUsageMetrics::Enter(thread_id)); + drop(state); + send_cpu_metrics_fn(CPUUsageMetrics::Enter(current_thread_id, cpu_timer)); let current_cpu_time_ns = get_current_cpu_time_ns().unwrap(); - scopeguard::guard((), move |_| { - debug_assert_eq!(thread_id, std::thread::current().id()); - - let cpu_time_after_drop_ns = - get_current_cpu_time_ns().unwrap_or(current_cpu_time_ns); - let blocking_cpu_time_ns = - BlockingScopeCPUUsage::get_cpu_usage_ns_and_reset( - &mut op_state.borrow_mut(), - ); + scopeguard::guard( + (), + Box::new(move |_| { + debug_assert_eq!(current_thread_id, std::thread::current().id()); + + let cpu_time_after_drop_ns = + get_current_cpu_time_ns().unwrap_or(current_cpu_time_ns); + let blocking_cpu_time_ns = + BlockingScopeCPUUsage::get_cpu_usage_ns_and_reset( + &mut op_state.borrow_mut(), + ); - let diff_cpu_time_ns = cpu_time_after_drop_ns - current_cpu_time_ns; + let diff_cpu_time_ns = cpu_time_after_drop_ns - current_cpu_time_ns; - *accumulated_cpu_time_ns += diff_cpu_time_ns; - *accumulated_cpu_time_ns += blocking_cpu_time_ns; + *accumulated_cpu_time_ns += diff_cpu_time_ns; + *accumulated_cpu_time_ns += blocking_cpu_time_ns; - send_cpu_metrics_fn(CPUUsageMetrics::Leave(CPUUsage { - accumulated: *accumulated_cpu_time_ns, - diff: diff_cpu_time_ns, - })); + send_cpu_metrics_fn(CPUUsageMetrics::Leave(CPUUsage { + accumulated: *accumulated_cpu_time_ns, + diff: diff_cpu_time_ns, + })); - debug!( - accumulated_cpu_time_ms = *accumulated_cpu_time_ns / 1_000_000, - blocking_cpu_time_ms = blocking_cpu_time_ns / 1_000_000, - ); - }) + debug!( + accumulated_cpu_time_ms = *accumulated_cpu_time_ns / 1_000_000, + blocking_cpu_time_ms = blocking_cpu_time_ns / 1_000_000, + ); + }), + ) } fn terminate_execution_if_cancelled( @@ -1995,6 +2264,7 @@ mod test { use tokio::time::timeout; use crate::runtime::DenoRuntime; + use crate::runtime::JsRuntimeLockerGuard; use crate::worker::DuplexStreamEntry; use crate::worker::WorkerBuilder; @@ -2279,13 +2549,20 @@ mod test { .await; let mut rt = runtime.unwrap(); - let main_mod_ev = rt.js_runtime.mod_evaluate(rt.main_module_id); - let _ = rt + let main_module_id = rt + .init_main_module() + .await + .map(|_| rt.main_module_id.unwrap()) + .unwrap(); + + let mut locker = unsafe { rt.with_locker() }; + let main_mod_ev = locker.js_runtime.mod_evaluate(main_module_id); + let _ = locker .js_runtime .run_event_loop(PollEventLoopOptions::default()) .await; - let read_is_even_global = rt + let read_is_even_global = locker .js_runtime .execute_script( "", @@ -2298,7 +2575,7 @@ mod test { ) .unwrap(); let read_is_even = - rt.to_value_mut::(&read_is_even_global); + locker.to_value_mut::(&read_is_even_global); assert_eq!(read_is_even.unwrap().to_string(), "false"); std::mem::drop(main_mod_ev); } @@ -2359,13 +2636,20 @@ mod test { .await; let mut rt = runtime.unwrap(); - let main_mod_ev = rt.js_runtime.mod_evaluate(rt.main_module_id); - let _ = rt + let main_module_id = rt + .init_main_module() + .await + .map(|_| rt.main_module_id.unwrap()) + .unwrap(); + + let mut locker = unsafe { rt.with_locker() }; + let main_mod_ev = locker.js_runtime.mod_evaluate(main_module_id); + let _ = locker .js_runtime .run_event_loop(PollEventLoopOptions::default()) .await; - let read_is_even_global = rt + let read_is_even_global = locker .js_runtime .execute_script( "", @@ -2378,7 +2662,7 @@ mod test { ) .unwrap(); let read_is_even = - rt.to_value_mut::(&read_is_even_global); + locker.to_value_mut::(&read_is_even_global); assert_eq!(read_is_even.unwrap().to_string(), "true"); std::mem::drop(main_mod_ev); } @@ -2390,7 +2674,8 @@ mod test { let mut runtime = RuntimeBuilder::new().build().await; { - let scope = &mut runtime.js_runtime.handle_scope(); + let mut locker = unsafe { runtime.with_locker() }; + let scope = &mut locker.js_runtime.handle_scope(); let context = scope.get_current_context(); let inner_scope = &mut v8::ContextScope::new(scope, context); let global = context.global(inner_scope); @@ -2417,7 +2702,8 @@ mod test { .await; { - let scope = &mut runtime.js_runtime.handle_scope(); + let mut locker = unsafe { runtime.with_locker() }; + let scope = &mut locker.js_runtime.handle_scope(); let context = scope.get_current_context(); let inner_scope = &mut v8::ContextScope::new(scope, context); let global = context.global(inner_scope); @@ -2461,7 +2747,8 @@ mod test { .build() .await; - let global_value_deno_read_file_script = main_rt + let mut locker = unsafe { main_rt.with_locker() }; + let global_value_deno_read_file_script = locker .js_runtime .execute_script( "", @@ -2474,7 +2761,7 @@ mod test { ) .unwrap(); - let fs_read_result = main_rt + let fs_read_result = locker .to_value_mut::(&global_value_deno_read_file_script); assert_eq!( fs_read_result.unwrap().as_str().unwrap(), @@ -2490,14 +2777,20 @@ mod test { .set_path("./test_cases/jsx-preact") .build() .await; + let main_module_id = main_rt + .init_main_module() + .await + .map(|_| main_rt.main_module_id.unwrap()) + .unwrap(); - let _main_mod_ev = main_rt.js_runtime.mod_evaluate(main_rt.main_module_id); - let _ = main_rt + let mut locker = unsafe { main_rt.with_locker() }; + let _main_mod_ev = locker.js_runtime.mod_evaluate(main_module_id); + let _ = locker .js_runtime .run_event_loop(PollEventLoopOptions::default()) .await; - let global_value_deno_read_file_script = main_rt + let global_value_deno_read_file_script = locker .js_runtime .execute_script( "", @@ -2510,7 +2803,7 @@ mod test { ) .unwrap(); - let jsx_read_result = main_rt + let jsx_read_result = locker .to_value_mut::(&global_value_deno_read_file_script); assert_eq!( jsx_read_result.unwrap().to_string(), @@ -2555,7 +2848,8 @@ mod test { .build() .await; - let user_rt_execute_scripts = user_rt + let mut locker = unsafe { user_rt.with_locker() }; + let user_rt_execute_scripts = locker .js_runtime .execute_script( "", @@ -2565,7 +2859,7 @@ mod test { ), ) .unwrap(); - let serde_deno_env = user_rt + let serde_deno_env = locker .to_value_mut::(&user_rt_execute_scripts) .unwrap(); @@ -2585,7 +2879,8 @@ mod test { .build() .await; - let user_rt_execute_scripts = user_rt + let mut locker = unsafe { user_rt.with_locker() }; + let user_rt_execute_scripts =locker .js_runtime .execute_script( "", @@ -2610,7 +2905,7 @@ mod test { ), ) .unwrap(); - let serde_deno_env = user_rt + let serde_deno_env = locker .to_value_mut::(&user_rt_execute_scripts) .unwrap(); assert_eq!(serde_deno_env.get("gid").unwrap().as_i64().unwrap(), 1000); @@ -2681,7 +2976,7 @@ mod test { assert!(deno_consle_size_map.contains_key("rows")); assert!(deno_consle_size_map.contains_key("columns")); - let user_rt_execute_scripts = user_rt.js_runtime.execute_script( + let user_rt_execute_scripts = locker.js_runtime.execute_script( "", ModuleCodeString::from( r#" @@ -2710,7 +3005,9 @@ mod test { .build() .await; - let err = main_rt + let mut main_locker = unsafe { main_rt.with_locker() }; + let mut user_locker = unsafe { user_rt.with_locker() }; + let err = main_locker .js_runtime .execute_script( "", @@ -2728,7 +3025,7 @@ mod test { .to_string() .contains("NotSupported: The operation is not supported")); - let main_deno_env_get_supa_test = main_rt + let main_deno_env_get_supa_test = main_locker .js_runtime .execute_script( "", @@ -2741,13 +3038,13 @@ mod test { ), ) .unwrap(); - let serde_deno_env = - main_rt.to_value_mut::(&main_deno_env_get_supa_test); + let serde_deno_env = main_locker + .to_value_mut::(&main_deno_env_get_supa_test); assert_eq!(serde_deno_env.unwrap().as_str().unwrap(), "Supa_Value"); // User does not have this env variable because it was not provided // During the runtime creation - let user_deno_env_get_supa_test = user_rt + let user_deno_env_get_supa_test = user_locker .js_runtime .execute_script( "", @@ -2760,8 +3057,8 @@ mod test { ), ) .unwrap(); - let user_serde_deno_env = - user_rt.to_value_mut::(&user_deno_env_get_supa_test); + let user_serde_deno_env = user_locker + .to_value_mut::(&user_deno_env_get_supa_test); assert!(user_serde_deno_env.unwrap().is_null()); } @@ -2883,7 +3180,7 @@ mod test { .build() .await; - let waker = user_rt.js_runtime.op_state().borrow().waker.clone(); + let waker = user_rt.waker.clone(); let handle = user_rt.js_runtime.v8_isolate().thread_safe_handle(); user_rt.add_memory_limit_callback(move |_| { @@ -2986,7 +3283,6 @@ mod test { impl GetRuntimeContext for Ctx { fn get_extra_context() -> impl Serialize { serde_json::json!({ - "useReadSyncFileAPI": true, "shouldBootstrapMockFnThrowError": true, }) } @@ -2996,7 +3292,10 @@ mod test { "./test_cases/user-worker-san-check", None, None, - &["./test_cases/user-worker-san-check/.blocklisted"], + &[ + "./test_cases/user-worker-san-check/.blocklisted", + "./test_cases/user-worker-san-check/.whitelisted", + ], ) .set_context::() .build() diff --git a/crates/base/src/runtime/unsync.rs b/crates/base/src/runtime/unsync.rs new file mode 100644 index 000000000..b3c937172 --- /dev/null +++ b/crates/base/src/runtime/unsync.rs @@ -0,0 +1,44 @@ +use std::future::Future; +use std::pin::Pin; +use std::task::Poll; + +#[repr(transparent)] +pub struct MaskValueAsSend { + pub value: V, +} + +unsafe impl Send for MaskValueAsSend {} + +impl MaskValueAsSend { + #[inline(always)] + pub fn into_inner(self) -> R { + self.value + } +} + +pub struct MaskFutureAsSend { + pub fut: MaskValueAsSend, +} + +impl From for MaskFutureAsSend +where + Fut: Future, +{ + fn from(value: Fut) -> Self { + Self { + fut: MaskValueAsSend { value }, + } + } +} + +impl Future for MaskFutureAsSend { + type Output = Fut::Output; + + fn poll( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll { + unsafe { Pin::new_unchecked(&mut self.get_unchecked_mut().fut.value) } + .poll(cx) + } +} diff --git a/crates/base/src/server.rs b/crates/base/src/server.rs index bbac99439..a3e228f23 100644 --- a/crates/base/src/server.rs +++ b/crates/base/src/server.rs @@ -11,6 +11,8 @@ use anyhow::anyhow; use anyhow::bail; use anyhow::Context; use anyhow::Error; +use deno_core::serde_json; +use deno_core::serde_json::json; use either::Either; use either::Either::Left; use either::Either::Right; @@ -230,21 +232,24 @@ impl Service> for WorkerService { ) } - Err(e) => { + Err(err) => { error!( "request failed (uri: {:?} reason: {:?})", req_uri.to_string(), - e + err ); - // FIXME: add an error body - Response::builder() - .status(http_v02::StatusCode::INTERNAL_SERVER_ERROR) - .body(Body::wrap_stream(CancelOnDrop { - inner: Body::empty(), - cancel: Some(cancel), - })) - .unwrap() + let msg = err.message().to_string(); + let msg = err.into_cause().map(|it| it.to_string()).unwrap_or(msg); + let bytes = serde_json::to_vec(&json!({ "msg": msg })); + let builder = Response::builder() + .status(http_v02::StatusCode::INTERNAL_SERVER_ERROR); + + match bytes { + Ok(v) => builder.body(Body::from(v)), + Err(err) => builder.body(Body::from(err.to_string())), + } + .unwrap() } }; diff --git a/crates/base/src/worker/driver/managed.rs b/crates/base/src/worker/driver/managed.rs index 4de54ef3b..904401003 100644 --- a/crates/base/src/worker/driver/managed.rs +++ b/crates/base/src/worker/driver/managed.rs @@ -87,8 +87,8 @@ impl WorkerDriver for Managed { ) } else { error!( - "runtime has escaped from the event loop unexpectedly: {}", - err_string.as_str() + "runtime has escaped from the event loop unexpectedly: {:#}", + err ); Ok(WorkerEvents::UncaughtException(UncaughtExceptionEvent { @@ -124,13 +124,10 @@ impl WorkerDriver for Managed { } }; - let (waker, thread_safe_handle) = { - let js_runtime = &mut runtime.js_runtime; - ( - js_runtime.op_state().borrow().waker.clone(), - js_runtime.v8_isolate().thread_safe_handle(), - ) - }; + let (waker, thread_safe_handle) = ( + runtime.waker.clone(), + runtime.js_runtime.v8_isolate().thread_safe_handle(), + ); let wait_fut = async move { termination_token.inbound.cancelled().await; diff --git a/crates/base/src/worker/driver/user.rs b/crates/base/src/worker/driver/user.rs index 5804d8b7f..9bdf1912e 100644 --- a/crates/base/src/worker/driver/user.rs +++ b/crates/base/src/worker/driver/user.rs @@ -149,7 +149,7 @@ impl WorkerDriver for User { debug!(use_s3_fs = s3_fs.is_some()); let (cpu_tx, cpu_rx) = mpsc::unbounded_channel(); - let Ok((maybe_timer, cancel_token)) = create_supervisor( + let Ok(cancel_token) = create_supervisor( inner.worker_key.unwrap_or(Uuid::nil()), runtime, inner.supervisor_policy, @@ -167,7 +167,6 @@ impl WorkerDriver for User { *cpu_usage_metrics_tx = Some(cpu_tx); async move { - let _cpu_timer = maybe_timer; let _guard = cancel_token.drop_guard(); if let Some(fs) = s3_fs.take() { diff --git a/crates/base/src/worker/pool.rs b/crates/base/src/worker/pool.rs index 7694e463f..318a7844d 100644 --- a/crates/base/src/worker/pool.rs +++ b/crates/base/src/worker/pool.rs @@ -531,79 +531,87 @@ impl WorkerPool { let cancel = worker.cancel.clone(); let (req_start_tx, req_end_tx) = profile.timing_tx_pair.clone(); - profile.status.demand.fetch_add(1, Ordering::Release); - - // Create a closure to handle the request and send the response - let request_handler = async move { - if !policy.is_per_worker() { - if cancel.is_cancelled() { - bail!(exit - .error() - .await - .unwrap_or(anyhow!(WorkerError::RequestCancelledBySupervisor))) - } + if profile.status.is_retired.is_raised() { + if res_tx + .send(Err(anyhow!(WorkerError::WorkerAlreadyRetired))) + .is_err() + { + error!("main worker receiver dropped"); + } + } else { + profile.status.demand.fetch_add(1, Ordering::Release); + + // Create a closure to handle the request and send the response + let request_handler = async move { + if !policy.is_per_worker() { + if cancel.is_cancelled() { + bail!(exit.error().await.unwrap_or(anyhow!( + WorkerError::RequestCancelledBySupervisor + ))) + } - let fence = Arc::new(Notify::const_new()); - - if let Err(ex) = req_start_tx.send(fence.clone()) { - // NOTE(Nyannyacha): The only way to be trapped in this branch is - // if the supervisor associated with the isolate has been - // terminated for some reason, such as a wall-clock timeout. - // - // It can be expected enough if many isolates are created at once - // due to requests rapidly increasing. - // - // To prevent this, we must give a wall-clock time limit enough to - // each supervisor. - error!("failed to notify the fence to the supervisor"); - return Err(ex).with_context(|| { - "failed to notify the fence to the supervisor" - }); - } + let fence = Arc::new(Notify::const_new()); + + if let Err(ex) = req_start_tx.send(fence.clone()) { + // NOTE(Nyannyacha): The only way to be trapped in this branch + // is if the supervisor associated with the isolate has been + // terminated for some reason, such as a wall-clock timeout. + // + // It can be expected enough if many isolates are created at + // once due to requests rapidly increasing. + // + // To prevent this, we must give a wall-clock time limit enough + // to each supervisor. + error!("failed to notify the fence to the supervisor"); + return Err(ex).with_context(|| { + "failed to notify the fence to the supervisor" + }); + } - tokio::select! { - _ = fence.notified() => {} - _ = cancel.cancelled() => { - bail!( - exit - .error() - .await - .unwrap_or( - anyhow!(WorkerError::RequestCancelledBySupervisor) - ) - ) + tokio::select! { + _ = fence.notified() => {} + _ = cancel.cancelled() => { + bail!( + exit + .error() + .await + .unwrap_or( + anyhow!(WorkerError::RequestCancelledBySupervisor) + ) + ) + } } } - } - let result = send_user_worker_request( - profile.worker_request_msg_tx, - req, - cancel, - exit, - conn_token, - ) - .await; - - match result { - Ok(req) => Ok((req, req_end_tx)), - Err(err) => { - let _ = req_end_tx.send(()); - error!( - "failed to send request to user worker: {}", - err.to_string() - ); - Err(err) + let result = send_user_worker_request( + profile.worker_request_msg_tx, + req, + cancel, + exit, + conn_token, + ) + .await; + + match result { + Ok(req) => Ok((req, req_end_tx)), + Err(err) => { + let _ = req_end_tx.send(()); + error!( + "failed to send request to user worker: {}", + err.to_string() + ); + Err(err) + } } - } - }; + }; - // Spawn the closure as an async task - tokio::task::spawn(async move { - if res_tx.send(request_handler.await).is_err() { - error!("main worker receiver dropped") - } - }); + // Spawn the closure as an async task + tokio::task::spawn(async move { + if res_tx.send(request_handler.await).is_err() { + error!("main worker receiver dropped") + } + }); + } Ok(()) } diff --git a/crates/base/src/worker/supervisor/mod.rs b/crates/base/src/worker/supervisor/mod.rs index e232a4a21..631dc971f 100644 --- a/crates/base/src/worker/supervisor/mod.rs +++ b/crates/base/src/worker/supervisor/mod.rs @@ -4,7 +4,7 @@ use std::time::Duration; use anyhow::anyhow; use base_mem_check::MemCheckState; -use cpu_timer::CPUAlarmVal; +use base_rt::RuntimeState; use cpu_timer::CPUTimer; use deno_core::v8; use deno_core::InspectorSessionKind; @@ -32,7 +32,6 @@ use super::pool::SupervisorPolicy; use super::termination_token::TerminationToken; use crate::runtime::DenoRuntime; -use crate::runtime::RuntimeState; use crate::server::ServerFlags; use crate::utils::units::percentage_value; @@ -49,58 +48,6 @@ pub struct IsolateMemoryStats { pub external_memory: usize, } -#[derive(Clone, Copy)] -pub struct CPUTimerParam { - soft_limit_ms: u64, - hard_limit_ms: u64, -} - -impl CPUTimerParam { - pub fn new(soft_limit_ms: u64, hard_limit_ms: u64) -> Self { - Self { - soft_limit_ms, - hard_limit_ms, - } - } - - pub fn get_cpu_timer( - &self, - policy: SupervisorPolicy, - ) -> Option<(CPUTimer, UnboundedReceiver<()>)> { - let (cpu_alarms_tx, cpu_alarms_rx) = mpsc::unbounded_channel::<()>(); - - if self.is_disabled() { - return None; - } - - Some(( - CPUTimer::start( - if policy.is_per_worker() { - self.soft_limit_ms - } else { - self.hard_limit_ms - }, - if policy.is_per_request() { - 0 - } else { - self.hard_limit_ms - }, - CPUAlarmVal { cpu_alarms_tx }, - ) - .ok()?, - cpu_alarms_rx, - )) - } - - pub fn limits(&self) -> (u64, u64) { - (self.soft_limit_ms, self.hard_limit_ms) - } - - pub fn is_disabled(&self) -> bool { - self.soft_limit_ms == 0 && self.hard_limit_ms == 0 - } -} - pub struct Tokens { pub termination: Option, pub supervise: CancellationToken, @@ -109,9 +56,7 @@ pub struct Tokens { pub struct Arguments { pub key: Uuid, pub runtime_opts: UserWorkerRuntimeOpts, - pub cpu_timer: Option<(CPUTimer, mpsc::UnboundedReceiver<()>)>, pub cpu_usage_metrics_rx: Option>, - pub cpu_timer_param: CPUTimerParam, pub supervisor_policy: SupervisorPolicy, pub runtime_state: Arc, pub promise_metrics: PromiseMetrics, @@ -132,10 +77,22 @@ pub struct CPUUsage { #[derive(EnumAsInner)] pub enum CPUUsageMetrics { - Enter(std::thread::ThreadId), + Enter(std::thread::ThreadId, CPUTimer), Leave(CPUUsage), } +#[inline] +#[allow(unused)] +fn cpu_budget(conf: &UserWorkerRuntimeOpts) -> u64 { + conf + .cpu_time_max_budget_per_task_ms + .unwrap_or(if cfg!(debug_assertions) { + conf.cpu_time_soft_limit_ms + } else { + 1 + }) +} + async fn wait_cpu_alarm( maybe_alarm: Option<&mut UnboundedReceiver<()>>, ) -> Option<()> { @@ -173,15 +130,12 @@ pub fn create_supervisor( timing: Option, termination_token: Option, flags: Arc, -) -> Result<(Option, CancellationToken), anyhow::Error> { +) -> Result { let (memory_limit_tx, memory_limit_rx) = mpsc::unbounded_channel(); - let (waker, thread_safe_handle) = { - let js_runtime = &mut runtime.js_runtime; - ( - js_runtime.op_state().borrow().waker.clone(), - js_runtime.v8_isolate().thread_safe_handle(), - ) - }; + let (waker, thread_safe_handle) = ( + runtime.waker.clone(), + runtime.js_runtime.v8_isolate().thread_safe_handle(), + ); // we assert supervisor is only run for user workers let conf = runtime.conf.as_user_worker().unwrap().clone(); @@ -212,9 +166,9 @@ pub fn create_supervisor( if memory_limit_tx.send(()).is_err() { log::error!( - "failed to send memory limit reached notification(isolate may already be terminating): isolate: {:?}, kind: {}", - key, kind - ); + "failed to send memory limit reached notification(isolate may already be terminating): isolate: {:?}, kind: {}", + key, kind + ); } }; @@ -237,19 +191,8 @@ pub fn create_supervisor( } }); - // Note: CPU timer must be started in the same thread as the worker runtime - - let cpu_timer_param = CPUTimerParam::new( - conf.cpu_time_soft_limit_ms, - conf.cpu_time_hard_limit_ms, - ); - - let (maybe_cpu_timer, maybe_cpu_alarms_rx) = - cpu_timer_param.get_cpu_timer(policy).unzip(); - drop({ let _rt_guard = base_rt::SUPERVISOR_RT.enter(); - let maybe_cpu_timer_inner = maybe_cpu_timer.clone(); let supervise_cancel_token_inner = supervise_cancel_token.clone(); let runtime_state = runtime.runtime_state.clone(); let promise_metrics = runtime.promise_metrics(); @@ -261,9 +204,7 @@ pub fn create_supervisor( let args = Arguments { key, runtime_opts: conf.clone(), - cpu_timer: maybe_cpu_timer_inner.zip(maybe_cpu_alarms_rx), cpu_usage_metrics_rx, - cpu_timer_param, supervisor_policy: policy, runtime_state, promise_metrics, @@ -443,5 +384,5 @@ pub fn create_supervisor( }) }); - Ok((maybe_cpu_timer, supervise_cancel_token)) + Ok(supervise_cancel_token) } diff --git a/crates/base/src/worker/supervisor/strategy_per_request.rs b/crates/base/src/worker/supervisor/strategy_per_request.rs index 671f6351a..9215a5da4 100644 --- a/crates/base/src/worker/supervisor/strategy_per_request.rs +++ b/crates/base/src/worker/supervisor/strategy_per_request.rs @@ -2,14 +2,15 @@ use std::future::pending; use std::sync::atomic::Ordering; use std::time::Duration; -#[cfg(debug_assertions)] -use std::thread::ThreadId; - +use cpu_timer::CPUTimer; use ext_event_worker::events::ShutdownReason; use ext_workers::context::Timing; use ext_workers::context::TimingStatus; use ext_workers::context::UserWorkerMsgs; +use ext_workers::context::UserWorkerRuntimeOpts; +use tokio::sync::mpsc; use tokio::time::Instant; +use tracing::Instrument; use crate::runtime::WillTerminateReason; use crate::worker::supervisor::create_wall_clock_beforeunload_alert; @@ -33,8 +34,6 @@ pub async fn supervise( key, runtime_opts, timing, - cpu_timer, - cpu_timer_param, cpu_usage_metrics_rx, mut memory_limit_rx, pool_msg_tx, @@ -55,9 +54,6 @@ pub async fn supervise( .. } = timing.unwrap_or_default(); - let (cpu_timer, mut cpu_alarms_rx) = cpu_timer.unzip(); - let (_, hard_limit_ms) = cpu_timer_param.limits(); - let _guard = scopeguard::guard(is_retired, |v| { v.raise(); @@ -68,27 +64,34 @@ pub async fn supervise( } }); - #[cfg(debug_assertions)] - let mut current_thread_id = Option::::None; + let mut cpu_timer = Option::::None; - let wall_clock_limit_ms = runtime_opts.worker_timeout_ms; + let UserWorkerRuntimeOpts { + worker_timeout_ms, + cpu_time_soft_limit_ms, + cpu_time_hard_limit_ms, + .. + } = runtime_opts; - let is_wall_clock_limit_disabled = wall_clock_limit_ms == 0; + let is_wall_clock_limit_disabled = worker_timeout_ms == 0; + let is_cpu_time_limit_disabled = + cpu_time_soft_limit_ms == 0 && cpu_time_hard_limit_ms == 0; let mut is_worker_entered = false; let mut is_wall_clock_beforeunload_armed = false; let mut cpu_usage_metrics_rx = cpu_usage_metrics_rx.unwrap(); let mut cpu_usage_ms = 0i64; let mut cpu_usage_accumulated_ms = 0i64; + let mut cpu_timer_rx = None::>; let mut complete_reason = None::; let mut req_ack_count = 0usize; let mut req_start_ack = false; - let wall_clock_limit_ms = if wall_clock_limit_ms < 1 { + let wall_clock_limit_ms = if worker_timeout_ms < 1 { 1 } else { - wall_clock_limit_ms + worker_timeout_ms }; let wall_clock_duration = Duration::from_millis(wall_clock_limit_ms); @@ -98,6 +101,14 @@ pub async fn supervise( flags.beforeunload_wall_clock_pct, ); + let reset_cpu_timer_fn = |cpu_timer: Option<&CPUTimer>| { + if let Some(Err(err)) = + cpu_timer.map(|it| it.reset(cpu_time_hard_limit_ms, 0)) + { + log::error!("can't reset cpu timer: {}", err); + } + }; + tokio::pin!(wall_clock_duration_alert); tokio::pin!(wall_clock_beforeunload_alert); @@ -118,22 +129,14 @@ pub async fn supervise( Some(metrics) = cpu_usage_metrics_rx.recv() => { match metrics { - CPUUsageMetrics::Enter(_thread_id) => { - // INVARIANT: Thread ID MUST equal with previously captured Thread - // ID. - #[cfg(debug_assertions)] - { - assert!(current_thread_id.unwrap_or(_thread_id) == _thread_id); - current_thread_id = Some(_thread_id); - } - + CPUUsageMetrics::Enter(_thread_id, timer) => { assert!(!is_worker_entered); is_worker_entered = true; - if !cpu_timer_param.is_disabled() { - if let Some(Err(err)) = cpu_timer.as_ref().map(|it| it.reset()) { - log::error!("can't reset cpu timer: {}", err); - } + if !is_cpu_time_limit_disabled { + cpu_timer_rx = Some(timer.set_channel().in_current_span().await); + cpu_timer = Some(timer); + reset_cpu_timer_fn(cpu_timer.as_ref()); } } @@ -144,21 +147,27 @@ pub async fn supervise( cpu_usage_ms += diff / 1_000_000; cpu_usage_accumulated_ms = accumulated / 1_000_000; - if !cpu_timer_param.is_disabled() { - if cpu_usage_ms >= hard_limit_ms as i64 { + if !is_cpu_time_limit_disabled { + if cpu_usage_ms >= cpu_time_hard_limit_ms as i64 { log::error!("CPU time limit reached: isolate: {:?}", key); complete_reason = Some(ShutdownReason::CPUTime); } - if let Some(Err(err)) = cpu_timer.as_ref().map(|it| it.reset()) { - log::error!("can't reset cpu timer: {}", err); - } + reset_cpu_timer_fn(cpu_timer.as_ref()); } + + cpu_timer = None; } } } - Some(_) = wait_cpu_alarm(cpu_alarms_rx.as_mut()) => { + Some(_) = async { + if cpu_timer_rx.is_some() { + wait_cpu_alarm(cpu_timer_rx.as_mut()).await + } else { + pending::<_>().await + } + } => { if is_worker_entered && req_start_ack { log::error!("CPU time limit reached: isolate: {:?}", key); complete_reason = Some(ShutdownReason::CPUTime); @@ -171,12 +180,7 @@ pub async fn supervise( assert!(!req_start_ack, "supervisor has seen request start signal twice"); notify.notify_one(); - - if let Some(cpu_timer) = cpu_timer.as_ref() { - if let Err(ex) = cpu_timer.reset() { - log::error!("cannot reset the cpu timer: {}", ex); - } - } + reset_cpu_timer_fn(cpu_timer.as_ref()); cpu_usage_ms = 0; req_start_ack = true; @@ -209,18 +213,18 @@ pub async fn supervise( } _ = &mut wall_clock_beforeunload_alert, - if !is_wall_clock_limit_disabled && !is_wall_clock_beforeunload_armed + if !is_wall_clock_limit_disabled && !is_wall_clock_beforeunload_armed => { let data_ptr_mut = Box::into_raw(Box::new(V8HandleBeforeunloadData { reason: WillTerminateReason::WallClock })); if thread_safe_handle - .request_interrupt(v8_handle_beforeunload, data_ptr_mut as *mut _) + .request_interrupt(v8_handle_beforeunload, data_ptr_mut as *mut _) { - waker.wake(); + waker.wake(); } else { - drop(unsafe { Box::from_raw(data_ptr_mut)}); + drop(unsafe { Box::from_raw(data_ptr_mut)}); } is_wall_clock_beforeunload_armed = true; @@ -254,9 +258,11 @@ pub async fn supervise( isolate_memory_usage_tx: Some(isolate_memory_usage_tx), })); - if !thread_safe_handle + if thread_safe_handle .request_interrupt(v8_handle_termination, data_ptr_mut as *mut _) { + waker.wake(); + } else { drop(unsafe { Box::from_raw(data_ptr_mut) }); } diff --git a/crates/base/src/worker/supervisor/strategy_per_worker.rs b/crates/base/src/worker/supervisor/strategy_per_worker.rs index 1a5336297..ca5364393 100644 --- a/crates/base/src/worker/supervisor/strategy_per_worker.rs +++ b/crates/base/src/worker/supervisor/strategy_per_worker.rs @@ -4,20 +4,20 @@ use std::sync::atomic::Ordering; use std::sync::Arc; use std::time::Duration; -#[cfg(debug_assertions)] -use std::thread::ThreadId; - +use base_rt::RuntimeState; use deno_core::unsync::sync::AtomicFlag; use ext_event_worker::events::ShutdownReason; use ext_runtime::PromiseMetrics; use ext_workers::context::Timing; use ext_workers::context::TimingStatus; use ext_workers::context::UserWorkerMsgs; +use ext_workers::context::UserWorkerRuntimeOpts; use log::error; use log::info; +use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; +use tracing::Instrument; -use crate::runtime::RuntimeState; use crate::runtime::WillTerminateReason; use crate::worker::supervisor::create_wall_clock_beforeunload_alert; use crate::worker::supervisor::v8_handle_beforeunload; @@ -40,6 +40,7 @@ struct State { is_worker_entered: bool, is_wall_clock_limit_disabled: bool, is_wall_clock_beforeunload_armed: bool, + is_cpu_time_limit_disabled: bool, is_cpu_time_soft_limit_reached: bool, is_mem_half_reached: bool, is_waiting_for_termination: bool, @@ -115,8 +116,6 @@ pub async fn supervise(args: Arguments) -> (ShutdownReason, i64) { promise_metrics, timing, mut memory_limit_rx, - cpu_timer, - cpu_timer_param, cpu_usage_metrics_rx, pool_msg_tx, isolate_memory_usage_tx, @@ -135,17 +134,18 @@ pub async fn supervise(args: Arguments) -> (ShutdownReason, i64) { req: (_, mut req_end_rx), } = timing.unwrap_or_default(); - let (cpu_timer, mut cpu_alarms_rx) = cpu_timer.unzip(); - let (soft_limit_ms, hard_limit_ms) = cpu_timer_param.limits(); - - #[cfg(debug_assertions)] - let mut current_thread_id = Option::::None; - - let wall_clock_limit_ms = runtime_opts.worker_timeout_ms; + let UserWorkerRuntimeOpts { + worker_timeout_ms, + cpu_time_soft_limit_ms, + cpu_time_hard_limit_ms, + .. + } = runtime_opts; let mut complete_reason = None::; let mut state = State { - is_wall_clock_limit_disabled: wall_clock_limit_ms == 0, + is_wall_clock_limit_disabled: worker_timeout_ms == 0, + is_cpu_time_limit_disabled: cpu_time_soft_limit_ms == 0 + && cpu_time_hard_limit_ms == 0, is_retired: is_retired.clone(), req_demand: demand, runtime: runtime_state, @@ -155,14 +155,15 @@ pub async fn supervise(args: Arguments) -> (ShutdownReason, i64) { let mut cpu_usage_metrics_rx = cpu_usage_metrics_rx.unwrap(); let mut cpu_usage_ms = 0i64; + let mut cpu_timer_rx = None::>; - let wall_clock_limit_ms = if wall_clock_limit_ms < 2 { + let wall_clock_limit_ms = if worker_timeout_ms < 2 { 2 } else { - wall_clock_limit_ms + worker_timeout_ms }; - let wall_clock_duration = Duration::from_millis(wall_clock_limit_ms); + let wall_clock_duration = Duration::from_millis(worker_timeout_ms); // Split wall clock duration into 2 intervals. // At the first interval, we will send a msg to retire the worker. @@ -203,13 +204,13 @@ pub async fn supervise(args: Arguments) -> (ShutdownReason, i64) { let data_ptr_mut = Box::into_raw(Box::new(V8HandleEarlyDropData { token })); - if !thread_safe_handle.request_interrupt( + if thread_safe_handle.request_interrupt( v8_handle_early_drop_beforeunload, data_ptr_mut as *mut std::ffi::c_void, ) { - unsafe { Box::from_raw(data_ptr_mut) }.token.cancel(); - } else { waker.wake(); + } else { + unsafe { Box::from_raw(data_ptr_mut) }.token.cancel(); } } }); @@ -225,6 +226,7 @@ pub async fn supervise(args: Arguments) -> (ShutdownReason, i64) { let terminate_fn = { let state = state.runtime.clone(); let thread_safe_handle = thread_safe_handle.clone(); + let waker = waker.clone(); move |should_terminate: bool| { let data_ptr_mut = Box::into_raw(Box::new(V8HandleTerminationData { should_terminate, @@ -234,10 +236,12 @@ pub async fn supervise(args: Arguments) -> (ShutdownReason, i64) { if should_terminate { state.terminated.raise(); } - if !thread_safe_handle.request_interrupt( + if thread_safe_handle.request_interrupt( v8_handle_termination, data_ptr_mut as *mut std::ffi::c_void, ) { + waker.wake(); + } else { drop(unsafe { Box::from_raw(data_ptr_mut) }); } } @@ -289,20 +293,19 @@ pub async fn supervise(args: Arguments) -> (ShutdownReason, i64) { Some(metrics) = cpu_usage_metrics_rx.recv() => { match metrics { - CPUUsageMetrics::Enter(_thread_id) => { - // INVARIANT: Thread ID MUST equal with previously captured Thread - // ID. - #[cfg(debug_assertions)] - { - assert!(current_thread_id.unwrap_or(_thread_id) == _thread_id); - current_thread_id = Some(_thread_id); - } - + CPUUsageMetrics::Enter(_thread_id, timer) => { state.worker_enter(); - if !cpu_timer_param.is_disabled() { - if let Some(Err(err)) = cpu_timer.as_ref().map(|it| it.reset()) { - error!("can't reset cpu timer: {}", err); + if !state.is_cpu_time_limit_disabled { + cpu_timer_rx = Some(timer.set_channel().in_current_span().await); + + if let Err(err) = timer.reset({ + // TODO(Nyannyacha): Once a CPU budget-based scheduler is + // implemented, uncomment this line. + // cpu_budget(&runtime_opts) + runtime_opts.cpu_time_soft_limit_ms + }, cpu_time_hard_limit_ms) { + error!("can't reset cpu timer: {}, {:?}", err, std::thread::current()); } } } @@ -312,11 +315,11 @@ pub async fn supervise(args: Arguments) -> (ShutdownReason, i64) { cpu_usage_ms = accumulated / 1_000_000; - if !cpu_timer_param.is_disabled() { - if cpu_usage_ms >= hard_limit_ms as i64 { + if !state.is_cpu_time_limit_disabled { + if cpu_usage_ms >= cpu_time_hard_limit_ms as i64 { error!("CPU time hard limit reached: isolate: {:?}", key); complete_reason = Some(ShutdownReason::CPUTime); - } else if cpu_usage_ms >= soft_limit_ms as i64 + } else if cpu_usage_ms >= cpu_time_soft_limit_ms as i64 && !state.is_cpu_time_soft_limit_reached { early_retire_fn(); @@ -341,7 +344,13 @@ pub async fn supervise(args: Arguments) -> (ShutdownReason, i64) { } } - Some(_) = wait_cpu_alarm(cpu_alarms_rx.as_mut()) => { + Some(_) = async { + if cpu_timer_rx.is_some() { + wait_cpu_alarm(cpu_timer_rx.as_mut()).await + } else { + pending::<_>().await + } + } => { if state.is_worker_entered { if !state.is_cpu_time_soft_limit_reached { early_retire_fn(); @@ -412,7 +421,7 @@ pub async fn supervise(args: Arguments) -> (ShutdownReason, i64) { } _ = &mut wall_clock_beforeunload_alert, - if !state.is_wall_clock_limit_disabled && !state.is_wall_clock_beforeunload_armed + if !state.is_wall_clock_limit_disabled && !state.is_wall_clock_beforeunload_armed => { let data_ptr_mut = Box::into_raw(Box::new(V8HandleBeforeunloadData { reason: WillTerminateReason::WallClock diff --git a/crates/base/src/worker/worker_inner.rs b/crates/base/src/worker/worker_inner.rs index 56241e9b6..e4f22274b 100644 --- a/crates/base/src/worker/worker_inner.rs +++ b/crates/base/src/worker/worker_inner.rs @@ -1,8 +1,5 @@ -use crate::inspector_server::Inspector; -use crate::runtime::DenoRuntime; -use crate::server::ServerFlags; -use crate::worker::utils::get_event_metadata; -use crate::worker::utils::send_event_if_event_worker_available; +use std::future::ready; +use std::sync::Arc; use anyhow::Error; use base_rt::error::CloneableError; @@ -24,8 +21,6 @@ use ext_workers::context::WorkerRequestMsg; use futures_util::FutureExt; use log::debug; use log::error; -use std::future::ready; -use std::sync::Arc; use tokio::io; use tokio::sync::mpsc; use tokio::sync::oneshot; @@ -35,6 +30,12 @@ use tracing::debug_span; use tracing::Instrument; use uuid::Uuid; +use crate::inspector_server::Inspector; +use crate::runtime::DenoRuntime; +use crate::server::ServerFlags; +use crate::worker::utils::get_event_metadata; +use crate::worker::utils::send_event_if_event_worker_available; + use super::driver::WorkerDriver; use super::driver::WorkerDriverImpl; use super::pool::SupervisorPolicy; @@ -206,6 +207,7 @@ impl std::ops::Deref for Worker { impl Worker { pub fn start( self, + eager_module_init: bool, booter_signal: oneshot::Sender< Result<(MetricSource, CancellationToken), Error>, >, @@ -224,12 +226,22 @@ impl Worker { let rt = imp.runtime_handle(); let worker_fut = async move { - let permit = DenoRuntime::acquire().await; - let new_runtime = match DenoRuntime::new(self).await { + let new_runtime = 'scope: { + match DenoRuntime::new(self).await { + Ok(mut v) => { + if eager_module_init { + if let Err(err) = v.init_main_module().await { + break 'scope Err(err); + } + } + Ok(v) + } + Err(err) => Err(err), + } + }; + let mut new_runtime = match new_runtime { Ok(v) => v, Err(err) => { - drop(permit); - let err = CloneableError::from(err.context("worker boot error")); let _ = booter_signal.send(Err(err.clone().into())); @@ -237,22 +249,12 @@ impl Worker { } }; - let mut runtime = scopeguard::guard(new_runtime, |mut runtime| unsafe { - runtime.js_runtime.v8_isolate().enter(); - }); - - unsafe { - runtime.js_runtime.v8_isolate().exit(); - } - - drop(permit); - let metric_src = { let metric_src = - WorkerMetricSource::from_js_runtime(&mut runtime.js_runtime); + WorkerMetricSource::from_js_runtime(&mut new_runtime.js_runtime); - if let Some(opts) = runtime.conf.as_main_worker().cloned() { - let state = runtime.js_runtime.op_state(); + if let Some(opts) = new_runtime.conf.as_main_worker().cloned() { + let state = new_runtime.js_runtime.op_state(); let mut state_mut = state.borrow_mut(); let metric_src = RuntimeMetricSource::new( metric_src.clone(), @@ -269,8 +271,9 @@ impl Worker { } }; - let _ = booter_signal.send(Ok((metric_src, runtime.drop_token.clone()))); - let supervise_fut = match imp.clone().supervise(&mut runtime) { + let _ = + booter_signal.send(Ok((metric_src, new_runtime.drop_token.clone()))); + let supervise_fut = match imp.clone().supervise(&mut new_runtime) { Some(v) => v.boxed(), None if worker_kind.is_user_worker() => return None, None => ready(Ok(())).boxed(), @@ -287,7 +290,7 @@ impl Worker { } }); - let result = imp.on_created(&mut runtime).await; + let result = imp.on_created(&mut new_runtime).await; let maybe_uncaught_exception_event = match result.as_ref() { Ok(WorkerEvents::UncaughtException(ev)) => Some(ev.clone()), Err(err) => Some(UncaughtExceptionEvent { @@ -302,44 +305,53 @@ impl Worker { exit.set(WorkerExitStatus::WithUncaughtException(ev)).await; } - drop(runtime); + drop(new_runtime); let _ = supervise_fut.await; Some(result) }; - let worker_fut = async move { - let Some(result) = worker_fut.await else { - return; - }; - - match result { - Ok(event) => { - match event { - WorkerEvents::Shutdown(ShutdownEvent { cpu_time_used, .. }) - | WorkerEvents::UncaughtException(UncaughtExceptionEvent { - cpu_time_used, - .. - }) => { - debug!("CPU time used: {:?}ms", cpu_time_used); - } - - _ => {} - }; - - send_event_if_event_worker_available( - events_msg_tx.as_ref(), - event, - event_metadata.clone(), - ); - } + let worker_fut = { + let event_metadata = event_metadata.clone(); + async move { + let Some(result) = worker_fut.await else { + return; + }; + + match result { + Ok(event) => { + match event { + WorkerEvents::Shutdown(ShutdownEvent { + cpu_time_used, .. + }) + | WorkerEvents::UncaughtException(UncaughtExceptionEvent { + cpu_time_used, + .. + }) => { + debug!("CPU time used: {:?}ms", cpu_time_used); + } + + _ => {} + }; + + send_event_if_event_worker_available( + events_msg_tx.as_ref(), + event, + event_metadata, + ); + } - Err(err) => error!("unexpected worker error {}", err), - }; + Err(err) => error!("unexpected worker error {}", err), + }; + } } - .instrument( - debug_span!("worker", name = worker_name.as_str(), kind = %worker_kind), - ); + .instrument(debug_span!( + "worker", + id = worker_name.as_str(), + kind = %worker_kind, + thread = ?std::thread::current().id(), + metadata = ?event_metadata + )); drop(rt.spawn_pinned({ let worker_fut = unsafe { MaskFutureAsSend::new(worker_fut) }; diff --git a/crates/base/src/worker/worker_surface_creation.rs b/crates/base/src/worker/worker_surface_creation.rs index bd0856dd9..43e248b6a 100644 --- a/crates/base/src/worker/worker_surface_creation.rs +++ b/crates/base/src/worker/worker_surface_creation.rs @@ -263,6 +263,7 @@ pub struct WorkerSurfaceBuilder { termination_token: Option, inspector: Option, worker_builder_hook: Option, + eager_module_init: bool, } impl Default for WorkerSurfaceBuilder { @@ -280,6 +281,7 @@ impl WorkerSurfaceBuilder { termination_token: None, inspector: None, worker_builder_hook: None, + eager_module_init: false, } } @@ -319,6 +321,11 @@ impl WorkerSurfaceBuilder { self } + pub fn eager_module_init(mut self, value: bool) -> Self { + self.eager_module_init = value; + self + } + pub fn set_init_opts( &mut self, value: Option, @@ -361,6 +368,11 @@ impl WorkerSurfaceBuilder { self } + pub fn set_eager_module_init(&mut self, value: bool) -> &mut Self { + self.eager_module_init = value; + self + } + pub async fn build(self) -> Result { let Self { init_opts, @@ -369,6 +381,7 @@ impl WorkerSurfaceBuilder { termination_token, inspector, worker_builder_hook, + eager_module_init, } = self; let (worker_boot_result_tx, worker_boot_result_rx) = oneshot::channel::< @@ -396,7 +409,7 @@ impl WorkerSurfaceBuilder { let cx = worker.cx.clone(); let network_sender = worker.imp.network_sender().await; - worker.start(worker_boot_result_tx, exit.clone()); + worker.start(eager_module_init, worker_boot_result_tx, exit.clone()); // create an async task waiting for requests for worker let (worker_req_tx, mut worker_req_rx) = diff --git a/crates/base/test_cases/user-worker-san-check/.blocklisted b/crates/base/test_cases/user-worker-san-check/.blocklisted index e116c170c..9284d4947 100644 --- a/crates/base/test_cases/user-worker-san-check/.blocklisted +++ b/crates/base/test_cases/user-worker-san-check/.blocklisted @@ -23,18 +23,12 @@ link linkSync lstat lstatSync -makeTempDirSync makeTempFile makeTempFileSync -mkdirSync -openSync -readDirSync readLink readLinkSync -removeSync rename renameSync -statSync symlink symlinkSync truncate @@ -43,8 +37,6 @@ umask utime utimeSync watchFs -writeFileSync -writeTextFileSync chdir dlopen cron @@ -52,4 +44,4 @@ run kill exit addSignalListener -removeSignalListener \ No newline at end of file +removeSignalListener diff --git a/crates/base/test_cases/user-worker-san-check/.whitelisted b/crates/base/test_cases/user-worker-san-check/.whitelisted new file mode 100644 index 000000000..526aa6ec4 --- /dev/null +++ b/crates/base/test_cases/user-worker-san-check/.whitelisted @@ -0,0 +1,9 @@ +mkdirSync +readDirSync +removeSync +statSync +writeFileSync +writeTextFileSync +readTextFileSync +readFileSync +makeTempDirSync diff --git a/crates/base/test_cases/user-worker-san-check/index.ts b/crates/base/test_cases/user-worker-san-check/index.ts index cca597543..5bd201c22 100644 --- a/crates/base/test_cases/user-worker-san-check/index.ts +++ b/crates/base/test_cases/user-worker-san-check/index.ts @@ -1,29 +1,74 @@ -let blocklist: string[] = Deno.readTextFileSync(".blocklisted") - .trim() - .split("\n"); +function checkBlocklisted(list: string[]) { + for (const api of list) { + console.log(api); + if (Deno[api] === void 0) { + continue; + } -for (const api of blocklist) { - console.log(api); - if (Deno[api] === void 0) { - continue; - } + if (typeof Deno[api] !== "function") { + throw new Error(`invalid api: ${api}`); + } + + try { + Deno[api](); + throw new Error(`unreachable: ${api}`); + } catch (ex) { + if (ex instanceof Deno.errors.PermissionDenied) { + continue; + } else if (ex instanceof TypeError) { + if (ex.message === "called MOCK_FN") { + continue; + } + } + } - if (typeof Deno[api] !== "function") { throw new Error(`invalid api: ${api}`); } +} - try { - Deno[api](); - throw new Error(`unreachable: ${api}`); - } catch (ex) { - if (ex instanceof Deno.errors.PermissionDenied) { +function checkWhitelisted(list: string[]) { + for (const api of list) { + console.log(api); + if (Deno[api] === void 0) { continue; - } else if (ex instanceof TypeError) { - if (ex.message === "called MOCK_FN") { - continue; + } + + if (typeof Deno[api] !== "function") { + throw new Error(`invalid api: ${api}`); + } + + try { + Deno[api](); + throw new Error(`unreachable: ${api}`); + } catch (ex) { + if (ex instanceof Deno.errors.PermissionDenied) { + throw ex; } } } - - throw new Error(`invalid api: ${api}`); } + +const blocklist: string[] = Deno.readTextFileSync(".blocklisted") + .trim() + .split("\n"); +const whitelisted: string[] = Deno.readTextFileSync(".whitelisted") + .trim() + .split("\n"); + +checkBlocklisted(blocklist); +checkWhitelisted(whitelisted); + +const { promise: fence, resolve, reject } = Promise.withResolvers(); + +setTimeout(() => { + try { + checkBlocklisted(whitelisted); + resolve(); + } catch (ex) { + reject(ex); + } +}); + +await fence; + +export {}; diff --git a/crates/base/tests/eszip_migration.rs b/crates/base/tests/eszip_migration.rs index 26ffc7bdf..e2023a19a 100644 --- a/crates/base/tests/eszip_migration.rs +++ b/crates/base/tests/eszip_migration.rs @@ -83,6 +83,7 @@ where let (pool_msg_tx, mut pool_msg_rx) = mpsc::unbounded_channel(); let worker_surface = WorkerSurfaceBuilder::new() .termination_token(termination_token.clone()) + .eager_module_init(true) .init_opts(WorkerContextInitOpts { service_path: PathBuf::from("meow"), no_module_cache: false, diff --git a/crates/base/tests/integration_tests.rs b/crates/base/tests/integration_tests.rs index d6e4b0adf..3891db66b 100644 --- a/crates/base/tests/integration_tests.rs +++ b/crates/base/tests/integration_tests.rs @@ -915,8 +915,9 @@ async fn test_worker_boot_with_0_byte_eszip() { let result = create_test_user_worker(opts).await; assert!(result.is_err()); - assert!(format!("{:#}", result.unwrap_err()) - .starts_with("worker boot error: unexpected end of file")); + assert!(format!("{:#}", result.unwrap_err()).starts_with( + "worker boot error: failed to bootstrap runtime: unexpected end of file" + )); } #[tokio::test] @@ -941,7 +942,7 @@ async fn test_worker_boot_with_invalid_entrypoint() { assert!(result.is_err()); assert!(format!("{:#}", result.unwrap_err()) - .starts_with("worker boot error: failed to determine entrypoint")); + .starts_with("worker boot error: failed to bootstrap runtime: failed to determine entrypoint")); } #[tokio::test] @@ -1241,9 +1242,13 @@ async fn req_failure_case_op_cancel_from_server_due_to_cpu_resource_limit() { let res = res.json::().await; assert!(res.is_ok()); - assert_eq!( - res.unwrap().msg, - "WorkerRequestCancelled: request has been cancelled by supervisor" + + let msg = res.unwrap().msg; + + assert!( + msg + == "WorkerRequestCancelled: request has been cancelled by supervisor" + || msg == "broken pipe" ); }, ) @@ -1271,9 +1276,10 @@ async fn req_failure_case_op_cancel_from_server_due_to_cpu_resource_limit_2() { assert!( !msg.starts_with("TypeError: request body receiver not connected") ); - assert_eq!( - msg, - "WorkerRequestCancelled: request has been cancelled by supervisor" + assert!( + msg + == "WorkerRequestCancelled: request has been cancelled by supervisor" + || msg == "broken pipe" ); }, ) @@ -2555,7 +2561,8 @@ async fn test_should_render_detailed_failed_to_create_graph_error() { assert_eq!(status, 500); assert!(payload.msg.starts_with( - "InvalidWorkerCreation: worker boot error: failed to create the graph: \ + "InvalidWorkerCreation: worker boot error: \ + failed to bootstrap runtime: failed to create the graph: \ Relative import path \"oak\" not prefixed with" )); }), @@ -2577,7 +2584,8 @@ async fn test_should_render_detailed_failed_to_create_graph_error() { assert_eq!(status, 500); assert!(payload.msg.starts_with( - "InvalidWorkerCreation: worker boot error: failed to create the graph: \ + "InvalidWorkerCreation: worker boot error: \ + failed to bootstrap runtime: failed to create the graph: \ Module not found \"file://" )); }), diff --git a/crates/base_rt/src/lib.rs b/crates/base_rt/src/lib.rs index 74188c693..42c7cd673 100644 --- a/crates/base_rt/src/lib.rs +++ b/crates/base_rt/src/lib.rs @@ -18,8 +18,12 @@ use tracing::debug; use tracing::debug_span; use tracing::Instrument; +mod runtime_state; + pub mod error; +pub use runtime_state::RuntimeState; + pub const DEFAULT_PRIMARY_WORKER_POOL_SIZE: usize = 2; pub const DEFAULT_USER_WORKER_POOL_SIZE: usize = 1; diff --git a/crates/base_rt/src/runtime_state.rs b/crates/base_rt/src/runtime_state.rs new file mode 100644 index 000000000..3b0a64ad7 --- /dev/null +++ b/crates/base_rt/src/runtime_state.rs @@ -0,0 +1,35 @@ +use std::sync::Arc; + +use deno_core::unsync::sync::AtomicFlag; + +#[derive(Debug, Clone, Default)] +pub struct RuntimeState { + pub init: Arc, + pub evaluating_mod: Arc, + pub event_loop_completed: Arc, + pub terminated: Arc, + pub found_inspector_session: Arc, + pub mem_reached_half: Arc, +} + +impl RuntimeState { + pub fn is_init(&self) -> bool { + self.init.is_raised() + } + + pub fn is_evaluating_mod(&self) -> bool { + self.evaluating_mod.is_raised() + } + + pub fn is_event_loop_completed(&self) -> bool { + self.event_loop_completed.is_raised() + } + + pub fn is_terminated(&self) -> bool { + self.terminated.is_raised() + } + + pub fn is_found_inspector_session(&self) -> bool { + self.found_inspector_session.is_raised() + } +} diff --git a/crates/cpu_timer/src/lib.rs b/crates/cpu_timer/src/lib.rs index cb1eb8bd8..0be14d5e5 100644 --- a/crates/cpu_timer/src/lib.rs +++ b/crates/cpu_timer/src/lib.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use anyhow::Error; use tokio::sync::mpsc; +use tokio::sync::Mutex; #[cfg(target_os = "linux")] mod linux { @@ -39,16 +40,14 @@ mod linux { } #[repr(C)] -#[derive(Clone)] +#[derive(Clone, Default)] pub struct CPUAlarmVal { - pub cpu_alarms_tx: mpsc::UnboundedSender<()>, + pub cpu_alarms_tx: Arc>>>, } #[cfg(target_os = "linux")] struct CPUTimerVal { tid: linux::TimerId, - initial_expiry: u64, - interval: u64, } #[cfg(target_os = "linux")] @@ -80,20 +79,22 @@ impl Drop for CPUTimer { pub struct CPUTimer {} impl CPUTimer { + #[cfg(not(target_os = "linux"))] + pub fn new(_: u64) -> Result { + log::error!("CPU timer: not enabled (need Linux)"); + Ok(Self {}) + } + #[cfg(target_os = "linux")] - pub fn start( - initial_expiry: u64, - interval: u64, - cpu_alarm_val: CPUAlarmVal, - ) -> Result { + pub fn new() -> Result { use std::sync::atomic::Ordering; use linux::*; let id = TIMER_COUNTER.fetch_add(1, Ordering::SeqCst); - let mut timerid = TimerId(std::ptr::null_mut()); + let mut tid = TimerId(std::ptr::null_mut()); let mut sigev: libc::sigevent = unsafe { std::mem::zeroed() }; - let cpu_alarm_val = Arc::new(cpu_alarm_val); + let cpu_alarm_val = Arc::default(); sigev.sigev_notify = libc::SIGEV_SIGNAL; sigev.sigev_signo = libc::SIGALRM; @@ -106,7 +107,7 @@ impl CPUTimer { libc::timer_create( libc::CLOCK_THREAD_CPUTIME_ID, &mut sigev as *mut libc::sigevent, - &mut timerid.0 as *mut *mut libc::c_void, + &mut tid.0 as *mut *mut libc::c_void, ) } < 0 { @@ -115,38 +116,43 @@ impl CPUTimer { let this = Self { id, - timer: Arc::new(Mutex::new(CPUTimerVal { - tid: timerid, - initial_expiry, - interval, - })), + timer: Arc::new(Mutex::new(CPUTimerVal { tid })), cpu_alarm_val, }; - Ok({ - this.reset()?; + linux::SIG_MSG_CHAN + .0 + .clone() + .send(SignalMsg::Add((id, this.clone()))) + .unwrap(); - linux::SIG_MSG_CHAN - .0 - .clone() - .send(SignalMsg::Add((id, this.clone()))) - .unwrap(); + Ok(this) + } - this - }) + pub async fn set_channel(&self) -> mpsc::UnboundedReceiver<()> { + let (tx, rx) = mpsc::unbounded_channel(); + let mut val = self.cpu_alarm_val.cpu_alarms_tx.lock().await; + + *val = Some(tx); + rx + } + + #[cfg(not(target_os = "linux"))] + pub fn reset(&self, _: u64, _: u64) -> Result<(), Error> { + Ok(()) } #[cfg(target_os = "linux")] - pub fn reset(&self) -> Result<(), Error> { + pub fn reset(&self, initial_expiry: u64, interval: u64) -> Result<(), Error> { use anyhow::Context; use linux::*; let timer = self.timer.try_lock().context("failed to get the lock")?; - let initial_expiry_secs = timer.initial_expiry / 1000; - let initial_expiry_msecs = timer.initial_expiry % 1000; - let interval_secs = timer.interval / 1000; - let interval_msecs = timer.interval % 1000; + let initial_expiry_secs = initial_expiry / 1000; + let initial_expiry_msecs = initial_expiry % 1000; + let interval_secs = interval / 1000; + let interval_msecs = interval % 1000; let mut tmspec: libc::itimerspec = unsafe { std::mem::zeroed() }; tmspec.it_value.tv_sec = initial_expiry_secs as i64; @@ -238,22 +244,22 @@ fn register_sigalrm() { Some(msg) = sig_msg_rx.recv() => { match msg { SignalMsg::Alarm(ref timer_id) => { - if let Some(cpu_timer) = registry.get(timer_id) { - let tx = cpu_timer.cpu_alarm_val.cpu_alarms_tx.clone(); - + if let Some(cpu_timer) = registry.get(timer_id) { + if let Some(tx) = (*cpu_timer.cpu_alarm_val.cpu_alarms_tx.lock().await).clone() { if tx.send(()).is_err() { - debug!("failed to send cpu alarm to the provided channel"); + debug!("failed to send cpu alarm to the provided channel"); } - } else { - // NOTE: Unix signals are being delivered asynchronously, - // and there are no guarantees to cancel the signal after - // a timer has been deleted, and after a signal is - // received, there may no longer be a target to accept it. - error!( - "can't find the cpu alarm signal matched with the received timer id: {}", - *timer_id - ); } + } else { + // NOTE: Unix signals are being delivered asynchronously, + // and there are no guarantees to cancel the signal after + // a timer has been deleted, and after a signal is + // received, there may no longer be a target to accept it. + error!( + "can't find the cpu alarm signal matched with the received timer id: {}", + *timer_id + ); + } } SignalMsg::Add((timer_id, cpu_timer)) => { diff --git a/crates/deno_facade/eszip/vfs.rs b/crates/deno_facade/eszip/vfs.rs index 3a7bd77ae..a5ad68a19 100644 --- a/crates/deno_facade/eszip/vfs.rs +++ b/crates/deno_facade/eszip/vfs.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::collections::VecDeque; use std::path::Path; use std::path::PathBuf; @@ -16,7 +17,6 @@ use fs::virtual_fs::VfsEntry; use fs::virtual_fs::VfsRoot; use fs::virtual_fs::VirtualDirectory; use fs::VfsOpts; -use indexmap::IndexMap; pub fn load_npm_vfs( eszip: Arc, @@ -90,7 +90,7 @@ where builder.with_root_dir(|root_dir| { root_dir.name = "node_modules".to_string(); let mut new_entries = Vec::with_capacity(root_dir.entries.len()); - let mut localhost_entries = IndexMap::new(); + let mut localhost_entries = BTreeMap::new(); for entry in std::mem::take(&mut root_dir.entries) { match entry { VfsEntry::Dir(dir) => { @@ -113,7 +113,7 @@ where } new_entries.push(VfsEntry::Dir(VirtualDirectory { name: "localhost".to_string(), - entries: localhost_entries.into_iter().map(|(_, v)| v).collect(), + entries: localhost_entries.into_values().collect(), })); // needs to be sorted by name new_entries.sort_by(|a, b| a.name().cmp(b.name())); diff --git a/deno.json b/deno.json index 84a088db5..da5d82abc 100644 --- a/deno.json +++ b/deno.json @@ -11,7 +11,6 @@ "proseWrap": "preserve" }, "imports": { - "openai": "npm:openai" }, "compilerOptions": { "types": [ diff --git a/examples/main/index.ts b/examples/main/index.ts index 57e5a379b..a9dc3cdc9 100644 --- a/examples/main/index.ts +++ b/examples/main/index.ts @@ -2,6 +2,7 @@ import { STATUS_CODE } from "https://deno.land/std/http/status.ts"; import { handleRegistryRequest } from "./registry/mod.ts"; +import { join } from "jsr:@std/path@^1.0"; console.log("main function started"); console.log(Deno.version); @@ -10,6 +11,20 @@ addEventListener("beforeunload", () => { console.log("main worker exiting"); }); +addEventListener("unhandledrejection", (ev) => { + console.log(ev); + ev.preventDefault(); +}); + +// (async () => { +// try { +// const session = new Supabase.ai.Session("gte-small"); +// await session.init; +// } catch (e) { +// console.error("failed to init gte-small session in main worker", e); +// } +// })(); + // log system memory usage every 30s // setInterval(() => console.log(EdgeRuntime.systemMemoryInfo()), 30 * 1000); @@ -88,21 +103,50 @@ Deno.serve(async (req: Request) => { return await handleRegistryRequest(REGISTRY_PREFIX, req); } - const path_parts = pathname.split("/"); - const service_name = path_parts[1]; + if (req.method === "PUT" && pathname === "/_internal/upload") { + try { + const content = await req.text(); + const dir = await Deno.makeTempDir(); + const path = join(dir, "index.ts"); + + await Deno.writeTextFile(path, content); + return Response.json({ + path: dir, + }); + } catch (err) { + return Response.json(err, { + status: STATUS_CODE.BadRequest, + }); + } + } + + let servicePath = pathname; + if (!pathname.startsWith("/tmp/")) { + const path_parts = pathname.split("/"); + const service_name = path_parts[1]; - if (!service_name || service_name === "") { - const error = { msg: "missing function name in request" }; - return new Response( - JSON.stringify(error), - { + if (!service_name || service_name === "") { + const error = { msg: "missing function name in request" }; + return new Response( + JSON.stringify(error), + { + status: STATUS_CODE.BadRequest, + headers: { "Content-Type": "application/json" }, + }, + ); + } + + servicePath = `./examples/${service_name}`; + } else { + try { + servicePath = await Deno.realPath(servicePath); + } catch (err) { + return Response.json(err, { status: STATUS_CODE.BadRequest, - headers: { "Content-Type": "application/json" }, - }, - ); + }); + } } - const servicePath = `./examples/${service_name}`; // console.error(`serving the request with ${servicePath}`); const createWorker = async () => { @@ -138,6 +182,9 @@ Deno.serve(async (req: Request) => { cpuTimeSoftLimitMs, cpuTimeHardLimitMs, staticPatterns, + context: { + useReadSyncFileAPI: true, + }, // maybeEszip, // maybeEntrypoint, // maybeModuleCode, @@ -158,8 +205,9 @@ Deno.serve(async (req: Request) => { return await worker.fetch(req, { signal }); } catch (e) { - console.error(e); - + if (e instanceof Deno.errors.WorkerAlreadyRetired) { + return await callWorker(); + } if (e instanceof Deno.errors.WorkerRequestCancelled) { headers.append("Connection", "close"); diff --git a/ext/runtime/js/bootstrap.js b/ext/runtime/js/bootstrap.js index 61408cfaf..506232971 100644 --- a/ext/runtime/js/bootstrap.js +++ b/ext/runtime/js/bootstrap.js @@ -399,13 +399,6 @@ const globalProperties = { }; ObjectDefineProperties(globalThis, globalProperties); -let bootstrapMockFnThrowError = false; -const MOCK_FN = () => { - if (bootstrapMockFnThrowError) { - throw new TypeError("called MOCK_FN"); - } -}; - const MAKE_HARD_ERR_FN = (msg) => { return () => { throw new globalThis_.Deno.errors.PermissionDenied(msg); @@ -491,6 +484,8 @@ function processRejectionHandled(promise, reason) { } globalThis.bootstrapSBEdge = (opts, ctx) => { + let bootstrapMockFnThrowError = false; + globalThis_ = globalThis; // We should delete this after initialization, @@ -590,8 +585,8 @@ globalThis.bootstrapSBEdge = (opts, ctx) => { ); setLanguage("en"); - // Find declarative fetch handler core.addMainModuleHandler((main) => { + // Find declarative fetch handler if (ObjectHasOwn(main, "default")) { registerDeclarativeServer(main.default); } @@ -669,10 +664,20 @@ globalThis.bootstrapSBEdge = (opts, ctx) => { "makeTempDir": true, "readDir": true, - "kill": MOCK_FN, - "exit": MOCK_FN, - "addSignalListener": MOCK_FN, - "removeSignalListener": MOCK_FN, + "kill": "mock", + "exit": "mock", + "addSignalListener": "mock", + "removeSignalListener": "mock", + + "statSync": "allowIfRuntimeIsInInit", + "removeSync": "allowIfRuntimeIsInInit", + "writeFileSync": "allowIfRuntimeIsInInit", + "writeTextFileSync": "allowIfRuntimeIsInInit", + "readFileSync": "allowIfRuntimeIsInInit", + "readTextFileSync": "allowIfRuntimeIsInInit", + "mkdirSync": "allowIfRuntimeIsInInit", + "makeTempDirSync": "allowIfRuntimeIsInInit", + "readDirSync": "allowIfRuntimeIsInInit", // TODO: use a non-hardcoded path "execPath": () => "/bin/edge-runtime", @@ -682,6 +687,7 @@ globalThis.bootstrapSBEdge = (opts, ctx) => { if (ctx?.useReadSyncFileAPI) { apisToBeOverridden["readFileSync"] = true; apisToBeOverridden["readTextFileSync"] = true; + apisToBeOverridden["openSync"] = true; } const apiNames = ObjectKeys(apisToBeOverridden); @@ -693,6 +699,31 @@ globalThis.bootstrapSBEdge = (opts, ctx) => { delete Deno[name]; } else if (typeof value === "function") { Deno[name] = value; + } else if (typeof value === "string") { + switch (value) { + case "mock": { + Deno[name] = () => { + if (bootstrapMockFnThrowError) { + throw new TypeError("called MOCK_FN"); + } + }; + break; + } + case "allowIfRuntimeIsInInit": { + const originalFn = Deno[name]; + const blocklistedFn = MAKE_HARD_ERR_FN( + `Deno.${name} is blocklisted on the current context`, + ); + Deno[name] = (...args) => { + if (ops.op_is_runtime_init()) { + return originalFn(...args); + } else { + return blocklistedFn(); + } + }; + break; + } + } } } } diff --git a/ext/runtime/js/errors.js b/ext/runtime/js/errors.js index 29cb17c1e..2e6f790e5 100644 --- a/ext/runtime/js/errors.js +++ b/ext/runtime/js/errors.js @@ -28,6 +28,7 @@ const buildDomErrorClass = (name) => const InvalidWorkerResponse = buildErrorClass("InvalidWorkerResponse"); const InvalidWorkerCreation = buildErrorClass("InvalidWorkerCreation"); const WorkerRequestCancelled = buildErrorClass("WorkerRequestCancelled"); +const WorkerAlreadyRetired = buildErrorClass("WorkerAlreadyRetired"); const NotFound = buildErrorClass("NotFound"); const PermissionDenied = buildErrorClass("PermissionDenied"); const ConnectionRefused = buildErrorClass("ConnectionRefused"); @@ -60,6 +61,7 @@ function registerErrors() { core.registerErrorClass("InvalidWorkerResponse", InvalidWorkerResponse); core.registerErrorClass("InvalidWorkerCreation", InvalidWorkerCreation); core.registerErrorClass("WorkerRequestCancelled", WorkerRequestCancelled); + core.registerErrorClass("WorkerAlreadyRetired", WorkerAlreadyRetired); core.registerErrorClass("NotFound", NotFound); core.registerErrorClass("PermissionDenied", PermissionDenied); core.registerErrorClass("ConnectionRefused", ConnectionRefused); diff --git a/ext/runtime/lib.rs b/ext/runtime/lib.rs index d98a769d6..b69a14c96 100644 --- a/ext/runtime/lib.rs +++ b/ext/runtime/lib.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use base_mem_check::WorkerHeapStatistics; use base_rt::DropToken; +use base_rt::RuntimeState; use deno_core::error::AnyError; use deno_core::op2; use deno_core::v8; @@ -269,6 +270,11 @@ fn op_is_terminal(state: &mut OpState, rid: u32) -> Result { Ok(handle.is_terminal()) }*/ +#[op2(fast)] +fn op_is_runtime_init(state: &mut OpState) -> bool { + state.borrow::>().is_init() +} + #[op2(fast)] fn op_stdin_set_raw( _state: &mut OpState, @@ -439,6 +445,7 @@ deno_core::extension!( runtime, ops = [ // op_is_terminal, + op_is_runtime_init, op_stdin_set_raw, op_console_size, op_read_line_prompt, diff --git a/ext/runtime/ops/net.rs b/ext/runtime/ops/net.rs index ec26deb2d..2bf66cb3d 100644 --- a/ext/runtime/ops/net.rs +++ b/ext/runtime/ops/net.rs @@ -147,14 +147,14 @@ pub async fn op_net_accept( let mut op_state = state.borrow_mut(); ( - op_state - .try_take::)>>( - ), - op_state - .try_borrow::() - .cloned() - .unwrap(), - ) + op_state + .try_take::)>>( + ), + op_state + .try_borrow::() + .cloned() + .unwrap(), + ) }; let Some(rx) = rx else { @@ -166,8 +166,8 @@ pub async fn op_net_accept( move |value| { let mut op_state = state.borrow_mut(); op_state.put::)>>( - value, - ); + value, + ); } }); diff --git a/ext/workers/context.rs b/ext/workers/context.rs index 7573c36c4..75f9ad2a0 100644 --- a/ext/workers/context.rs +++ b/ext/workers/context.rs @@ -74,6 +74,7 @@ pub struct UserWorkerRuntimeOpts { /// Wall clock limit pub worker_timeout_ms: u64, + pub cpu_time_max_budget_per_task_ms: Option, pub cpu_time_soft_limit_ms: u64, pub cpu_time_hard_limit_ms: u64, @@ -108,6 +109,11 @@ impl Default for UserWorkerRuntimeOpts { .parse() .unwrap(), + cpu_time_max_budget_per_task_ms: if cfg!(debug_assertions) { + Some(100) + } else { + Some(1) + }, cpu_time_soft_limit_ms: env!("SUPABASE_RESOURCE_LIMIT_CPU_SOFT_MS") .parse() .unwrap(), diff --git a/ext/workers/errors.rs b/ext/workers/errors.rs index 23249d692..e86603899 100644 --- a/ext/workers/errors.rs +++ b/ext/workers/errors.rs @@ -4,4 +4,6 @@ use thiserror::Error; pub enum WorkerError { #[error("request has been cancelled by supervisor")] RequestCancelledBySupervisor, + #[error("request cannot be handled because the worker has already retired")] + WorkerAlreadyRetired, } diff --git a/ext/workers/lib.rs b/ext/workers/lib.rs index c389aa940..8bb258f41 100644 --- a/ext/workers/lib.rs +++ b/ext/workers/lib.rs @@ -577,6 +577,9 @@ pub async fn op_user_worker_fetch_send( Some(err @ WorkerError::RequestCancelledBySupervisor) => { return Err(custom_error("WorkerRequestCancelled", err.to_string())); } + Some(err @ WorkerError::WorkerAlreadyRetired) => { + return Err(custom_error("WorkerAlreadyRetired", err.to_string())); + } None => { return Err(custom_error("InvalidWorkerResponse", err.to_string())); diff --git a/k6/functions/gte-small-simple.ts b/k6/functions/gte-small-simple.ts new file mode 100644 index 000000000..7fff03084 --- /dev/null +++ b/k6/functions/gte-small-simple.ts @@ -0,0 +1,7 @@ +const model = new Supabase.ai.Session("gte-small"); +const output = await model.run("meowmeow", { + mean_pool: true, + normalize: true, +}); + +Deno.serve((_req) => Response.json(output)); diff --git a/k6/functions/gte-small.ts b/k6/functions/gte-small.ts new file mode 100644 index 000000000..04c26e0f4 --- /dev/null +++ b/k6/functions/gte-small.ts @@ -0,0 +1,16 @@ +const session = new Supabase.ai.Session("gte-small"); + +Deno.serve(async (req) => { + const payload = await req.json(); + const text_for_embedding = payload.text_for_embedding; + + // Generate embedding + const embedding = await session.run(text_for_embedding, { + mean_pool: true, + normalize: true, + }); + + return Response.json({ + length: embedding.length, + }); +}); diff --git a/k6/functions/may-upload-failure.ts b/k6/functions/may-upload-failure.ts new file mode 100644 index 000000000..22d82b5cb --- /dev/null +++ b/k6/functions/may-upload-failure.ts @@ -0,0 +1,35 @@ +import { Application } from "https://deno.land/x/oak@v12.3.1/mod.ts"; + +const MB = 1024 * 1024; + +const app = new Application(); + +app.use(async (ctx) => { + try { + const body = ctx.request.body({ type: "form-data" }); + const formData = await body.value.read({ + // Need to set the maxSize so files will be stored in memory. + // This is necessary as Edge Functions don't have disk write access. + // We are setting the max size as 10MB (an Edge Function has a max memory limit of 150MB) + // For more config options, check: https://deno.land/x/oak@v11.1.0/mod.ts?s=FormDataReadOptions + maxSize: 10 * MB, + }); + const file = formData.files[0]; + console.log(file.contentType); + + ctx.response.status = 201; + ctx.response.body = "Success!"; + } catch (e) { + console.error(e); + if ("status" in e) { + ctx.response.headers.set("Connection", "close"); + ctx.response.status = e.status; + ctx.response.body = e.message; + } else { + ctx.response.status = 500; + ctx.response.body = "Error!"; + } + } +}); + +await app.listen({ port: 8000 }); diff --git a/k6/functions/npm-supabase-js.ts b/k6/functions/npm-supabase-js.ts new file mode 100644 index 000000000..4a2dff6e4 --- /dev/null +++ b/k6/functions/npm-supabase-js.ts @@ -0,0 +1,3 @@ +import { createClient } from "npm:@supabase/supabase-js@2.40.0"; +console.log(createClient); +Deno.serve((_req) => new Response("Hello, world")); diff --git a/k6/functions/ort-rust-backend.ts b/k6/functions/ort-rust-backend.ts new file mode 100644 index 000000000..a1d8d1c1e --- /dev/null +++ b/k6/functions/ort-rust-backend.ts @@ -0,0 +1,20 @@ +import { pipeline } from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.1"; + +const pipe = await pipeline("feature-extraction", "supabase/gte-small", { + device: "auto", +}); + +Deno.serve(async (req) => { + const payload = await req.json(); + const text_for_embedding = payload.text_for_embedding; + + // Generate embedding + const embedding = await pipe(text_for_embedding, { + pooling: "mean", + normalize: true, + }); + + return Response.json({ + length: embedding.ort_tensor.size, + }); +}); diff --git a/k6/functions/simple.ts b/k6/functions/simple.ts new file mode 100644 index 000000000..964bc7fbe --- /dev/null +++ b/k6/functions/simple.ts @@ -0,0 +1 @@ +Deno.serve((_req) => new Response("Hello, world")); diff --git a/k6/functions/sleep-5000ms.ts b/k6/functions/sleep-5000ms.ts new file mode 100644 index 000000000..819f076e0 --- /dev/null +++ b/k6/functions/sleep-5000ms.ts @@ -0,0 +1,11 @@ +function sleep(ms: number) { + return new Promise((res) => { + setTimeout(() => { + res(void 0); + }, ms); + }); +} + +await sleep(5000); + +Deno.serve(() => new Response()); diff --git a/k6/functions/tmp-write-100mib.ts b/k6/functions/tmp-write-100mib.ts new file mode 100644 index 000000000..b28af5dd3 --- /dev/null +++ b/k6/functions/tmp-write-100mib.ts @@ -0,0 +1,10 @@ +const path = "/tmp/100mb_zero_file.bin"; +const file = Deno.openSync(path, { write: true, create: true }); +const chunk = new Uint8Array(1024 * 1024); + +for (let i = 0; i < 100; i++) { + file.writeSync(chunk); +} + +file.close(); +Deno.serve((_req) => new Response("Hello, world")); diff --git a/k6/functions/transformer-fill-mask.ts b/k6/functions/transformer-fill-mask.ts new file mode 100644 index 000000000..b05e93991 --- /dev/null +++ b/k6/functions/transformer-fill-mask.ts @@ -0,0 +1,12 @@ +import { + env, + pipeline, +} from "https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.0.1"; + +env.allowLocalModels = false; + +const pipe = await pipeline("fill-mask", null, { device: "auto" }); +const input = "[MASK] is the capital of England."; +const output = await pipe(input); + +Deno.serve(() => Response.json(output)); diff --git a/k6/functions/waste-cpu-time.ts b/k6/functions/waste-cpu-time.ts new file mode 100644 index 000000000..d0a991e1b --- /dev/null +++ b/k6/functions/waste-cpu-time.ts @@ -0,0 +1,15 @@ +function mySlowFunction(baseNumber) { + console.time("mySlowFunction"); + let now = Date.now(); + let result = 0; + for (var i = Math.pow(baseNumber, 7); i >= 0; i--) { + result += Math.atan(i) * Math.tan(i); + } + let duration = Date.now() - now; + console.timeEnd("mySlowFunction"); + return { result: result, duration: duration }; +} + +mySlowFunction(11); + +Deno.serve(() => new Response()); diff --git a/k6/specs/gte-small-simple.ts b/k6/specs/gte-small-simple.ts new file mode 100644 index 000000000..2de34f4ed --- /dev/null +++ b/k6/specs/gte-small-simple.ts @@ -0,0 +1,35 @@ +import http from "k6/http"; + +import { check } from "k6"; +import { Options } from "k6/options"; + +import { target } from "../config"; +import { upload } from "../utils"; + +export const options: Options = { + scenarios: { + gteSmallSimple: { + executor: "constant-vus", + vus: 12, + duration: "3m", + }, + }, +}; + +export function setup() { + return { + url: upload(open("../functions/gte-small-simple.ts")), + }; +} + +type Data = { + url: string; +}; + +export default function (data: Data) { + const res = http.get(`${target}${data.url}`); + + check(res, { + "status is 200": (r) => r.status === 200, + }); +} diff --git a/k6/specs/gte.ts b/k6/specs/gte-small.ts similarity index 86% rename from k6/specs/gte.ts rename to k6/specs/gte-small.ts index 8fd2fe295..85ec858bb 100644 --- a/k6/specs/gte.ts +++ b/k6/specs/gte-small.ts @@ -17,14 +17,15 @@ import { check, fail } from "k6"; import { Options } from "k6/options"; import { target } from "../config"; +import { MSG_CANCELED } from "../constants"; +import { upload } from "../utils"; /** @ts-ignore */ import { randomIntBetween } from "https://jslib.k6.io/k6-utils/1.2.0/index.js"; -import { MSG_CANCELED } from "../constants"; export const options: Options = { scenarios: { - simple: { + gteSmall: { executor: "constant-vus", vus: 12, duration: "3m", @@ -37,16 +38,22 @@ const GENERATORS = import("../generators"); export async function setup() { const pkg = await GENERATORS; return { + url: upload(open("../functions/gte-small.ts")), words: pkg.makeText(1000), }; } -export default function gte(data: { words: string[] }) { +type Data = { + url: string; + words: string[]; +}; + +export default function (data: Data) { const wordIdx = randomIntBetween(0, data.words.length - 1); console.debug(`WORD[${wordIdx}]: ${data.words[wordIdx]}`); const res = http.post( - `${target}/k6-gte`, + `${target}${data.url}`, JSON.stringify({ "text_for_embedding": data.words[wordIdx], }), diff --git a/k6/specs/may-upload-failure.ts b/k6/specs/may-upload-failure.ts index cd5beb668..b6e1f0e75 100644 --- a/k6/specs/may-upload-failure.ts +++ b/k6/specs/may-upload-failure.ts @@ -5,6 +5,7 @@ import { Options } from "k6/options"; import { target } from "../config"; import { MSG_CANCELED } from "../constants"; +import { upload } from "../utils"; /** @ts-ignore */ import { randomIntBetween } from "https://jslib.k6.io/k6-utils/1.2.0/index.js"; @@ -36,9 +37,19 @@ export const options: Options = { ], }; -export default function () { +export function setup() { + return { + url: upload(open("../functions/may-upload-failure.ts")), + }; +} + +type Data = { + url: string; +}; + +export default function (data: Data) { const res = http.post( - `${target}/oak-file-upload`, + `${target}${data.url}`, { "file": dummyFile, }, diff --git a/k6/specs/mixed.ts b/k6/specs/mixed.ts new file mode 100644 index 000000000..7e030d379 --- /dev/null +++ b/k6/specs/mixed.ts @@ -0,0 +1,124 @@ +import http from "k6/http"; + +import { check } from "k6"; +import { Options } from "k6/options"; + +import { target } from "../config"; +import { upload } from "../utils"; + +export const options: Options = { + scenarios: { + gteSmallSimple: { + executor: "constant-vus", + vus: 12, + duration: "3m", + exec: "gteSmallSimple", + }, + npmSupabaseJs: { + executor: "constant-vus", + vus: 12, + duration: "3m", + exec: "npmSupabaseJs", + }, + simple: { + executor: "constant-vus", + vus: 12, + duration: "3m", + exec: "simple", + }, + sleep5000ms: { + executor: "constant-vus", + vus: 12, + duration: "3m", + exec: "sleep5000ms", + }, + tmpWrite100mib: { + executor: "constant-vus", + vus: 12, + duration: "3m", + exec: "tmpWrite100mib", + }, + transformerFillMask: { + executor: "constant-vus", + vus: 12, + duration: "3m", + exec: "transformerFillMask", + }, + wasteCpuTime: { + executor: "constant-vus", + vus: 12, + duration: "3m", + exec: "wasteCpuTime", + }, + }, +}; + +export function setup() { + return { + url: { + gteSmallSimple: upload(open("../functions/gte-small-simple.ts")), + npmSupabaseJs: upload(open("../functions/npm-supabase-js.ts")), + simple: upload(open("../functions/simple.ts")), + sleep5000ms: upload(open("../functions/sleep-5000ms.ts")), + tmpWrite100mib: upload(open("../functions/tmp-write-100mib.ts")), + transformerFillMask: upload( + open("../functions/transformer-fill-mask.ts"), + ), + wasteCpuTime: upload(open("../functions/waste-cpu-time.ts")), + }, + }; +} + +type Data = { + url: { [_: string]: string }; +}; + +export function gteSmallSimple(data: Data) { + const res = http.get(`${target}${data.url["gteSmallSimple"]}`); + + check(res, { + "status is 200": (r) => r.status === 200, + }); +} +export function npmSupabaseJs(data: Data) { + const res = http.get(`${target}${data.url["npmSupabaseJs"]}`); + + check(res, { + "status is 200": (r) => r.status === 200, + }); +} +export function simple(data: Data) { + const res = http.get(`${target}${data.url["simple"]}`); + + check(res, { + "status is 200": (r) => r.status === 200, + }); +} +export function sleep5000ms(data: Data) { + const res = http.get(`${target}${data.url["sleep5000ms"]}`); + + check(res, { + "status is 200": (r) => r.status === 200, + }); +} +export function tmpWrite100mib(data: Data) { + const res = http.get(`${target}${data.url["tmpWrite100mib"]}`); + + check(res, { + "status is 200": (r) => r.status === 200, + }); +} +export function transformerFillMask(data: Data) { + const res = http.get(`${target}${data.url["transformerFillMask"]}`); + + check(res, { + "status is 200": (r) => r.status === 200, + }); +} +export function wasteCpuTime(data: Data) { + const res = http.get(`${target}${data.url["wasteCpuTime"]}`); + + check(res, { + "status is 200": (r) => r.status === 200, + }); +} diff --git a/k6/specs/npm-supabase-js.ts b/k6/specs/npm-supabase-js.ts new file mode 100644 index 000000000..ea29ef641 --- /dev/null +++ b/k6/specs/npm-supabase-js.ts @@ -0,0 +1,35 @@ +import http from "k6/http"; + +import { check } from "k6"; +import { Options } from "k6/options"; + +import { target } from "../config"; +import { upload } from "../utils"; + +export const options: Options = { + scenarios: { + npmSupabaseJs: { + executor: "constant-vus", + vus: 12, + duration: "3m", + }, + }, +}; + +export function setup() { + return { + url: upload(open("../functions/npm-supabase-js.ts")), + }; +} + +type Data = { + url: string; +}; + +export default function (data: Data) { + const res = http.get(`${target}${data.url}`); + + check(res, { + "status is 200": (r) => r.status === 200, + }); +} diff --git a/k6/specs/ort-rust-backend.ts b/k6/specs/ort-rust-backend.ts index 9bd3ded11..8b8adfb73 100644 --- a/k6/specs/ort-rust-backend.ts +++ b/k6/specs/ort-rust-backend.ts @@ -17,14 +17,15 @@ import { check, fail } from "k6"; import { Options } from "k6/options"; import { target } from "../config"; +import { upload } from "../utils"; +import { MSG_CANCELED } from "../constants"; /** @ts-ignore */ import { randomIntBetween } from "https://jslib.k6.io/k6-utils/1.2.0/index.js"; -import { MSG_CANCELED } from "../constants"; export const options: Options = { scenarios: { - simple: { + ortRustBackend: { executor: "constant-vus", vus: 12, duration: "3m", @@ -37,16 +38,22 @@ const GENERATORS = import("../generators"); export async function setup() { const pkg = await GENERATORS; return { + url: upload(open("../functions/ort-rust-backend.ts")), words: pkg.makeText(1000), }; } -export default function ort_rust_backend(data: { words: string[] }) { +type Data = { + url: string; + words: string[]; +}; + +export default function (data: Data) { const wordIdx = randomIntBetween(0, data.words.length - 1); console.debug(`WORD[${wordIdx}]: ${data.words[wordIdx]}`); const res = http.post( - `${target}/k6-ort-rust-backend`, + `${target}${data.url}`, JSON.stringify({ "text_for_embedding": data.words[wordIdx], }), diff --git a/k6/specs/simple.ts b/k6/specs/simple.ts index 7d8070fc5..0cc66b98d 100644 --- a/k6/specs/simple.ts +++ b/k6/specs/simple.ts @@ -4,6 +4,7 @@ import { check } from "k6"; import { Options } from "k6/options"; import { target } from "../config"; +import { upload } from "../utils"; export const options: Options = { scenarios: { @@ -15,8 +16,18 @@ export const options: Options = { }, }; -export default function simple() { - const res = http.get(`${target}/serve`); +export function setup() { + return { + url: upload(open("../functions/simple.ts")), + }; +} + +type Data = { + url: string; +}; + +export default function (data: Data) { + const res = http.get(`${target}${data.url}`); check(res, { "status is 200": (r) => r.status === 200, diff --git a/k6/specs/sleep-5000ms.ts b/k6/specs/sleep-5000ms.ts new file mode 100644 index 000000000..54fa17e2d --- /dev/null +++ b/k6/specs/sleep-5000ms.ts @@ -0,0 +1,35 @@ +import http from "k6/http"; + +import { check } from "k6"; +import { Options } from "k6/options"; + +import { target } from "../config"; +import { upload } from "../utils"; + +export const options: Options = { + scenarios: { + sleep5000ms: { + executor: "constant-vus", + vus: 12, + duration: "3m", + }, + }, +}; + +export function setup() { + return { + url: upload(open("../functions/sleep-5000ms.ts")), + }; +} + +type Data = { + url: string; +}; + +export default function (data: Data) { + const res = http.get(`${target}${data.url}`); + + check(res, { + "status is 200": (r) => r.status === 200, + }); +} diff --git a/k6/specs/slow-boot-functions.ts b/k6/specs/slow-boot-functions.ts new file mode 100644 index 000000000..d919ebfa1 --- /dev/null +++ b/k6/specs/slow-boot-functions.ts @@ -0,0 +1,67 @@ +import http from "k6/http"; + +import { check } from "k6"; +import { Options } from "k6/options"; + +import { target } from "../config"; +import { upload } from "../utils"; + +export const options: Options = { + noConnectionReuse: true, + scenarios: { + simple: { + executor: "constant-vus", + vus: 12, + duration: "3m", + exec: "simple", + }, + sleep5000ms: { + executor: "constant-vus", + vus: 12, + duration: "3m", + exec: "sleep5000ms", + }, + wasteCpuTime: { + executor: "constant-vus", + vus: 12, + duration: "3m", + exec: "wasteCpuTime", + }, + }, +}; + +export function setup() { + return { + url: { + simple: upload(open("../functions/simple.ts")), + sleep5000ms: upload(open("../functions/sleep-5000ms.ts")), + wasteCpuTime: upload(open("../functions/waste-cpu-time.ts")), + }, + }; +} + +type Data = { + url: { [name: string]: string }; +}; + +export function simple(data: Data) { + const res = http.get(`${target}${data.url["simple"]}`); + const passed = check(res, { "simple 200": (r) => r.status === 200 }); + if (!passed) { + console.log("simple", JSON.stringify(res)); + } +} +export function sleep5000ms(data: Data) { + const res = http.get(`${target}${data.url["sleep5000ms"]}`); + const passed = check(res, { "sleep 200": (r) => r.status === 200 }); + if (!passed) { + console.log("sleep", JSON.stringify(res)); + } +} +export function wasteCpuTime(data: Data) { + const res = http.get(`${target}${data.url["wasteCpuTime"]}`); + const passed = check(res, { "wasteCpuTime 200": (r) => r.status === 200 }); + if (!passed) { + console.log("wasteCpuTime", JSON.stringify(res)); + } +} diff --git a/k6/specs/tmp-write-100mib.ts b/k6/specs/tmp-write-100mib.ts new file mode 100644 index 000000000..66717c78c --- /dev/null +++ b/k6/specs/tmp-write-100mib.ts @@ -0,0 +1,35 @@ +import http from "k6/http"; + +import { check } from "k6"; +import { Options } from "k6/options"; + +import { target } from "../config"; +import { upload } from "../utils"; + +export const options: Options = { + scenarios: { + tmpWrite100mib: { + executor: "constant-vus", + vus: 12, + duration: "3m", + }, + }, +}; + +export function setup() { + return { + url: upload(open("../functions/tmp-write-100mib.ts")), + }; +} + +type Data = { + url: string; +}; + +export default function (data: Data) { + const res = http.get(`${target}${data.url}`); + + check(res, { + "status is 200": (r) => r.status === 200, + }); +} diff --git a/k6/specs/transformer-fill-mask.ts b/k6/specs/transformer-fill-mask.ts new file mode 100644 index 000000000..801037a72 --- /dev/null +++ b/k6/specs/transformer-fill-mask.ts @@ -0,0 +1,35 @@ +import http from "k6/http"; + +import { check } from "k6"; +import { Options } from "k6/options"; + +import { target } from "../config"; +import { upload } from "../utils"; + +export const options: Options = { + scenarios: { + transformerFillMask: { + executor: "constant-vus", + vus: 12, + duration: "3m", + }, + }, +}; + +export function setup() { + return { + url: upload(open("../functions/transformer-fill-mask.ts")), + }; +} + +type Data = { + url: string; +}; + +export default function (data: Data) { + const res = http.get(`${target}${data.url}`); + + check(res, { + "status is 200": (r) => r.status === 200, + }); +} diff --git a/k6/utils.ts b/k6/utils.ts new file mode 100644 index 000000000..3dbf55e61 --- /dev/null +++ b/k6/utils.ts @@ -0,0 +1,8 @@ +import http from "k6/http"; + +import { target } from "./config"; + +export function upload(content: string): string { + const resp = http.put(`${target}/_internal/upload`, content); + return resp.json("path") as string; +} diff --git a/k6/vite.config.ts b/k6/vite.config.ts index 2ea3268a1..b18964978 100644 --- a/k6/vite.config.ts +++ b/k6/vite.config.ts @@ -2,9 +2,10 @@ import { babel } from "@rollup/plugin-babel"; import { nodeResolve } from "@rollup/plugin-node-resolve"; -import { defineConfig } from "vite"; +import { defineConfig, Plugin } from "vite"; -import copy from "rollup-plugin-copy"; +import fs from "fs"; +import path from "path"; import fg from "fast-glob"; const getEntryPoints = (entryPoints) => { @@ -20,6 +21,28 @@ const getEntryPoints = (entryPoints) => { return Object.fromEntries(entities); }; +function inlineOpenAsString(): Plugin { + return { + name: "inline-open-as-string", + enforce: "pre", + transform(code, id) { + if (!id.endsWith(".js") && !id.endsWith(".ts")) return; + + return code.replace(/open\((['"])(.+?)\1\)/g, (_, _quote, relPath) => { + const absPath = path.resolve(path.dirname(id), relPath); + + if (!fs.existsSync(absPath)) { + throw new Error(`File not found: ${absPath}`); + } + + const rawContent = fs.readFileSync(absPath, "utf-8"); + const safeContent = rawContent.replace(/([$`\\])/g, "\\$1"); + return `\`${safeContent}\``; + }); + }, + }; +} + export default defineConfig({ mode: "production", build: { @@ -38,14 +61,7 @@ export default defineConfig({ extensions: [".ts", ".js"], }, plugins: [ - copy({ - targets: [ - { - src: "assets/**/*", - dest: "dist", - }, - ], - }), + inlineOpenAsString(), babel({ babelHelpers: "bundled", exclude: /node_modules/, diff --git a/vendor/deno_unsync/Cargo.toml b/vendor/deno_unsync/Cargo.toml new file mode 100644 index 000000000..b79da9b97 --- /dev/null +++ b/vendor/deno_unsync/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "deno_unsync" +version = "0.4.2" +edition = "2021" +authors = ["the Deno authors"] +license = "MIT" +repository = "https://github.com/denoland/deno_unsync" +description = "A collection of adapters to make working with Tokio single-threaded runtimes easier" +readme = "README.md" + +[features] +default = ["tokio"] +tokio = ["dep:tokio", "dep:tokio-util"] + +[dependencies] +futures = "0.3.21" +parking_lot = "0.12.3" +tokio = { version = "1", features = ["rt"], optional = true } +tokio-util = { version = "0.7", optional = true } + +[dev-dependencies] +tokio = { version = "1", features = ["io-util", "macros", "rt", "sync", "time"] } + +[lib] +name = "deno_unsync" diff --git a/vendor/deno_unsync/LICENSE b/vendor/deno_unsync/LICENSE new file mode 100644 index 000000000..edf73ca86 --- /dev/null +++ b/vendor/deno_unsync/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018-2024 the Deno authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/deno_unsync/README.md b/vendor/deno_unsync/README.md new file mode 100644 index 000000000..a63bc6db0 --- /dev/null +++ b/vendor/deno_unsync/README.md @@ -0,0 +1,4 @@ +# deno_unsync + +This is a collection of adapters to make working with Tokio single-threaded runtimes as easy as working with +multi-threaded runtimes. diff --git a/vendor/deno_unsync/src/flag.rs b/vendor/deno_unsync/src/flag.rs new file mode 100644 index 000000000..68c399980 --- /dev/null +++ b/vendor/deno_unsync/src/flag.rs @@ -0,0 +1,54 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +use std::cell::Cell; + +/// A flag with interior mutability that can be raised or lowered. +/// Useful for indicating if an event has occurred. +#[derive(Debug, Default)] +pub struct Flag(Cell); + +impl Flag { + /// Creates a new flag that's lowered. + pub const fn lowered() -> Self { + Self(Cell::new(false)) + } + + /// Creates a new flag that's raised. + pub const fn raised() -> Self { + Self(Cell::new(true)) + } + + /// Raises the flag returning if raised. + pub fn raise(&self) -> bool { + !self.0.replace(true) + } + + /// Lowers the flag returning if lowered. + pub fn lower(&self) -> bool { + self.0.replace(false) + } + + /// Gets if the flag is raised. + pub fn is_raised(&self) -> bool { + self.0.get() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_raise_lower() { + let flag = Flag::default(); + assert!(!flag.is_raised()); + assert!(flag.raise()); + assert!(flag.is_raised()); + assert!(!flag.raise()); + assert!(flag.is_raised()); + assert!(flag.lower()); + assert!(!flag.is_raised()); + assert!(!flag.lower()); + assert!(!flag.is_raised()); + } +} diff --git a/vendor/deno_unsync/src/future.rs b/vendor/deno_unsync/src/future.rs new file mode 100644 index 000000000..ac014d1c4 --- /dev/null +++ b/vendor/deno_unsync/src/future.rs @@ -0,0 +1,250 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +use parking_lot::Mutex; +use std::cell::RefCell; +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::Arc; +use std::task::Context; +use std::task::Wake; +use std::task::Waker; + +use crate::sync::AtomicFlag; + +impl LocalFutureExt for T where T: Future {} + +pub trait LocalFutureExt: std::future::Future { + fn shared_local(self) -> SharedLocal + where + Self: Sized, + Self::Output: Clone, + { + SharedLocal::new(self) + } +} + +enum FutureOrOutput { + Future(TFuture), + Output(TFuture::Output), +} + +impl std::fmt::Debug for FutureOrOutput +where + TFuture::Output: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Future(_) => f.debug_tuple("Future").field(&"").finish(), + Self::Output(arg0) => f.debug_tuple("Result").field(arg0).finish(), + } + } +} + +struct SharedLocalData { + future_or_output: FutureOrOutput, +} + +impl std::fmt::Debug for SharedLocalData +where + TFuture::Output: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SharedLocalData") + .field("future_or_output", &self.future_or_output) + .finish() + } +} + +struct SharedLocalInner { + data: RefCell>, + child_waker_state: Arc, +} + +impl std::fmt::Debug for SharedLocalInner +where + TFuture::Output: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SharedLocalInner") + .field("data", &self.data) + .field("child_waker_state", &self.child_waker_state) + .finish() + } +} + +/// A !Send-friendly future whose result can be awaited multiple times. +#[must_use = "futures do nothing unless you `.await` or poll them"] +pub struct SharedLocal(Rc>); + +impl Clone for SharedLocal +where + TFuture::Output: Clone, +{ + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl std::fmt::Debug for SharedLocal +where + TFuture::Output: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("SharedLocal").field(&self.0).finish() + } +} + +impl SharedLocal +where + TFuture::Output: Clone, +{ + pub fn new(future: TFuture) -> Self { + SharedLocal(Rc::new(SharedLocalInner { + data: RefCell::new(SharedLocalData { + future_or_output: FutureOrOutput::Future(future), + }), + child_waker_state: Arc::new(ChildWakerState { + can_poll: AtomicFlag::raised(), + wakers: Default::default(), + }), + })) + } +} + +impl std::future::Future for SharedLocal +where + TFuture::Output: Clone, +{ + type Output = TFuture::Output; + + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + use std::task::Poll; + + let mut inner = self.0.data.borrow_mut(); + match &mut inner.future_or_output { + FutureOrOutput::Future(fut) => { + self.0.child_waker_state.wakers.push(cx.waker().clone()); + if self.0.child_waker_state.can_poll.lower() { + let child_waker = Waker::from(self.0.child_waker_state.clone()); + let mut child_cx = Context::from_waker(&child_waker); + let fut = unsafe { Pin::new_unchecked(fut) }; + match fut.poll(&mut child_cx) { + Poll::Ready(result) => { + inner.future_or_output = FutureOrOutput::Output(result.clone()); + drop(inner); // stop borrow_mut + let wakers = self.0.child_waker_state.wakers.take_all(); + for waker in wakers { + waker.wake(); + } + Poll::Ready(result) + } + Poll::Pending => Poll::Pending, + } + } else { + Poll::Pending + } + } + FutureOrOutput::Output(result) => Poll::Ready(result.clone()), + } + } +} + +#[derive(Debug, Default)] +struct WakerStore(Mutex>); + +impl WakerStore { + pub fn take_all(&self) -> Vec { + let mut wakers = self.0.lock(); + std::mem::take(&mut *wakers) + } + + pub fn clone_all(&self) -> Vec { + self.0.lock().clone() + } + + pub fn push(&self, waker: Waker) { + self.0.lock().push(waker); + } +} + +#[derive(Debug)] +struct ChildWakerState { + can_poll: AtomicFlag, + wakers: WakerStore, +} + +impl Wake for ChildWakerState { + fn wake(self: Arc) { + self.can_poll.raise(); + let wakers = self.wakers.take_all(); + + for waker in wakers { + waker.wake(); + } + } + + fn wake_by_ref(self: &Arc) { + self.can_poll.raise(); + let wakers = self.wakers.clone_all(); + + for waker in wakers { + waker.wake_by_ref(); + } + } +} + +#[cfg(test)] +mod test { + use std::sync::Arc; + + use tokio::sync::Notify; + + use super::LocalFutureExt; + + #[tokio::test(flavor = "current_thread")] + async fn test_shared_local_future() { + let shared = super::SharedLocal::new(Box::pin(async { 42 })); + assert_eq!(shared.clone().await, 42); + assert_eq!(shared.await, 42); + } + + #[tokio::test(flavor = "current_thread")] + async fn test_shared_local() { + let shared = async { 42 }.shared_local(); + assert_eq!(shared.clone().await, 42); + assert_eq!(shared.await, 42); + } + + #[tokio::test(flavor = "current_thread")] + async fn multiple_tasks_waiting() { + let notify = Arc::new(Notify::new()); + + let shared = { + let notify = notify.clone(); + async move { + tokio::task::yield_now().await; + notify.notified().await; + tokio::task::yield_now().await; + tokio::task::yield_now().await; + } + .shared_local() + }; + let mut tasks = Vec::new(); + for _ in 0..10 { + tasks.push(crate::spawn(shared.clone())); + } + + crate::spawn(async move { + notify.notify_one(); + for task in tasks { + task.await.unwrap(); + } + }) + .await + .unwrap() + } +} diff --git a/vendor/deno_unsync/src/lib.rs b/vendor/deno_unsync/src/lib.rs new file mode 100644 index 000000000..719215baf --- /dev/null +++ b/vendor/deno_unsync/src/lib.rs @@ -0,0 +1,26 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +mod flag; +pub mod future; +pub mod mpsc; +pub mod sync; +mod task; +mod task_queue; +#[cfg(feature = "tokio")] +mod tokio; +mod waker; + +pub use flag::Flag; +pub use task_queue::TaskQueue; +pub use task_queue::TaskQueuePermit; +pub use task_queue::TaskQueuePermitAcquireFuture; +pub use waker::UnsyncWaker; + +#[cfg(feature = "tokio")] +pub use self::tokio::*; + +/// Marker for items that are ![`Send`]. +#[derive(Copy, Clone, Default, Eq, PartialEq, PartialOrd, Ord, Debug, Hash)] +pub struct UnsendMarker( + std::marker::PhantomData>, +); diff --git a/vendor/deno_unsync/src/mpsc/chunked_queue.rs b/vendor/deno_unsync/src/mpsc/chunked_queue.rs new file mode 100644 index 000000000..3d685f43e --- /dev/null +++ b/vendor/deno_unsync/src/mpsc/chunked_queue.rs @@ -0,0 +1,82 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +use std::collections::LinkedList; +use std::collections::VecDeque; + +const CHUNK_SIZE: usize = 1024; + +/// A queue that stores elements in chunks in a linked list +/// to reduce allocations. +pub(crate) struct ChunkedQueue { + chunks: LinkedList>, +} + +impl Default for ChunkedQueue { + fn default() -> Self { + Self { + chunks: Default::default(), + } + } +} + +impl ChunkedQueue { + pub fn len(&self) -> usize { + match self.chunks.len() { + 0 => 0, + 1 => self.chunks.front().unwrap().len(), + 2 => { + self.chunks.front().unwrap().len() + self.chunks.back().unwrap().len() + } + _ => { + self.chunks.front().unwrap().len() + + CHUNK_SIZE * (self.chunks.len() - 2) + + self.chunks.back().unwrap().len() + } + } + } + + pub fn push_back(&mut self, value: T) { + if let Some(tail) = self.chunks.back_mut() { + if tail.len() < CHUNK_SIZE { + tail.push_back(value); + return; + } + } + let mut new_buffer = VecDeque::with_capacity(CHUNK_SIZE); + new_buffer.push_back(value); + self.chunks.push_back(new_buffer); + } + + pub fn pop_front(&mut self) -> Option { + if let Some(head) = self.chunks.front_mut() { + let value = head.pop_front(); + if value.is_some() && head.is_empty() && self.chunks.len() > 1 { + self.chunks.pop_front().unwrap(); + } + value + } else { + None + } + } +} + +#[cfg(test)] +mod test { + use super::CHUNK_SIZE; + + #[test] + fn ensure_len_correct() { + let mut queue = super::ChunkedQueue::default(); + for _ in 0..2 { + for i in 0..CHUNK_SIZE * 20 { + queue.push_back(i); + assert_eq!(queue.len(), i + 1); + } + for i in (0..CHUNK_SIZE * 20).rev() { + queue.pop_front(); + assert_eq!(queue.len(), i); + } + assert_eq!(queue.len(), 0); + } + } +} diff --git a/vendor/deno_unsync/src/mpsc/mod.rs b/vendor/deno_unsync/src/mpsc/mod.rs new file mode 100644 index 000000000..1c86169ae --- /dev/null +++ b/vendor/deno_unsync/src/mpsc/mod.rs @@ -0,0 +1,239 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +use std::cell::RefCell; +use std::fmt::Formatter; +use std::future::Future; +use std::pin::Pin; +use std::rc::Rc; +use std::task::Context; +use std::task::Poll; + +mod chunked_queue; + +use crate::UnsyncWaker; +use chunked_queue::ChunkedQueue; + +pub struct SendError(pub T); + +impl std::fmt::Debug for SendError +where + T: std::fmt::Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_tuple("SendError").field(&self.0).finish() + } +} + +pub struct Sender { + shared: Rc>>, +} + +impl Sender { + pub fn send(&self, value: T) -> Result<(), SendError> { + let mut shared = self.shared.borrow_mut(); + if shared.closed { + return Err(SendError(value)); + } + shared.queue.push_back(value); + shared.waker.wake(); + Ok(()) + } +} + +impl Drop for Sender { + fn drop(&mut self) { + let mut shared = self.shared.borrow_mut(); + shared.closed = true; + shared.waker.wake(); + } +} + +pub struct Receiver { + shared: Rc>>, +} + +impl Drop for Receiver { + fn drop(&mut self) { + let mut shared = self.shared.borrow_mut(); + shared.closed = true; + } +} + +impl Receiver { + /// Receives a value from the channel, returning `None` if there + /// are no more items and the channel is closed. + pub async fn recv(&mut self) -> Option { + // note: this is `&mut self` so that it can't be polled + // concurrently. DO NOT change this to `&self` because + // then futures will lose their wakers. + RecvFuture { + shared: &self.shared, + } + .await + } + + /// Number of pending unread items. + pub fn len(&self) -> usize { + self.shared.borrow().queue.len() + } + + /// If the receiver has no pending items. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +struct RecvFuture<'a, T> { + shared: &'a RefCell>, +} + +impl Future for RecvFuture<'_, T> { + type Output = Option; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut shared = self.shared.borrow_mut(); + if let Some(value) = shared.queue.pop_front() { + Poll::Ready(Some(value)) + } else if shared.closed { + Poll::Ready(None) + } else { + shared.waker.register(cx.waker()); + Poll::Pending + } + } +} + +struct Shared { + queue: ChunkedQueue, + waker: UnsyncWaker, + closed: bool, +} + +/// A ![`Sync`] and ![`Sync`] equivalent to `tokio::sync::unbounded_channel`. +pub fn unbounded_channel() -> (Sender, Receiver) { + let shared = Rc::new(RefCell::new(Shared { + queue: ChunkedQueue::default(), + waker: UnsyncWaker::default(), + closed: false, + })); + ( + Sender { + shared: shared.clone(), + }, + Receiver { shared }, + ) +} + +#[cfg(test)] +mod test { + use tokio::join; + + use super::*; + + #[tokio::test(flavor = "current_thread")] + async fn sends_receives_exits() { + let (sender, mut receiver) = unbounded_channel::(); + sender.send(1).unwrap(); + assert_eq!(receiver.recv().await, Some(1)); + sender.send(2).unwrap(); + assert_eq!(receiver.recv().await, Some(2)); + drop(sender); + assert_eq!(receiver.recv().await, None); + } + + #[tokio::test(flavor = "current_thread")] + async fn sends_multiple_then_drop() { + let (sender, mut receiver) = unbounded_channel::(); + sender.send(1).unwrap(); + sender.send(2).unwrap(); + drop(sender); + assert_eq!(receiver.len(), 2); + assert!(!receiver.is_empty()); + assert_eq!(receiver.recv().await, Some(1)); + assert_eq!(receiver.recv().await, Some(2)); + assert_eq!(receiver.recv().await, None); + assert_eq!(receiver.len(), 0); + assert!(receiver.is_empty()); + } + + #[tokio::test(flavor = "current_thread")] + async fn receiver_dropped_sending() { + let (sender, receiver) = unbounded_channel::(); + drop(receiver); + let err = sender.send(1).unwrap_err(); + assert_eq!(err.0, 1); + } + + #[tokio::test(flavor = "current_thread")] + async fn receiver_recv_then_drop_sender() { + let (sender, mut receiver) = unbounded_channel::(); + let future = crate::spawn(async move { + let value = receiver.recv().await; + value.is_none() + }); + let future2 = crate::spawn(async move { + drop(sender); + true + }); + let (first, second) = join!(future, future2); + assert!(first.unwrap()); + assert!(second.unwrap()); + } + + #[tokio::test(flavor = "current_thread")] + async fn multiple_senders_divided_work() { + for receiver_ticks in [None, Some(1), Some(10)] { + for sender_ticks in [None, Some(1), Some(10)] { + for sender_count in [1000, 100, 10, 2, 1] { + let (sender, mut receiver) = unbounded_channel::(); + let future = crate::spawn(async move { + let mut values = Vec::with_capacity(1000); + for _ in 0..1000 { + if let Some(ticks) = receiver_ticks { + for _ in 0..ticks { + tokio::task::yield_now().await; + } + } + let value = receiver.recv().await; + values.push(value.unwrap()); + } + // both senders should be dropped at this point + let value = receiver.recv().await; + assert!(value.is_none()); + + values.sort(); + // ensure we received these values + #[allow(clippy::needless_range_loop)] + for i in 0..1000 { + assert_eq!(values[i], i); + } + }); + + let mut futures = Vec::with_capacity(1 + sender_count); + futures.push(future); + let sender = Rc::new(sender); + for sender_index in 0..sender_count { + let sender = sender.clone(); + let batch_count = 1000 / sender_count; + futures.push(crate::spawn(async move { + for i in 0..batch_count { + if let Some(ticks) = sender_ticks { + for _ in 0..ticks { + tokio::task::yield_now().await; + } + } + sender.send(batch_count * sender_index + i).unwrap(); + } + })); + } + drop(sender); + + // wait all futures + for future in futures { + future.await.unwrap(); + } + } + } + } + } +} diff --git a/vendor/deno_unsync/src/sync/flag.rs b/vendor/deno_unsync/src/sync/flag.rs new file mode 100644 index 000000000..1fcef121f --- /dev/null +++ b/vendor/deno_unsync/src/sync/flag.rs @@ -0,0 +1,57 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; + +/// Simplifies the use of an atomic boolean as a flag. +#[derive(Debug, Default)] +pub struct AtomicFlag(AtomicBool); + +impl AtomicFlag { + /// Creates a new flag that's lowered. + pub const fn lowered() -> AtomicFlag { + Self(AtomicBool::new(false)) + } + + /// Creates a new flag that's raised. + pub const fn raised() -> AtomicFlag { + Self(AtomicBool::new(true)) + } + + /// Raises the flag returning if the raise was successful. + pub fn raise(&self) -> bool { + !self.0.swap(true, Ordering::SeqCst) + } + + /// Lowers the flag returning if the lower was successful. + pub fn lower(&self) -> bool { + self.0.swap(false, Ordering::SeqCst) + } + + /// Gets if the flag is raised. + pub fn is_raised(&self) -> bool { + self.0.load(Ordering::SeqCst) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn atomic_flag_raises_lowers() { + let flag = AtomicFlag::default(); + assert!(!flag.is_raised()); // false by default + assert!(flag.raise()); + assert!(flag.is_raised()); + assert!(!flag.raise()); + assert!(flag.is_raised()); + assert!(flag.lower()); + assert!(flag.raise()); + assert!(flag.lower()); + assert!(!flag.lower()); + let flag = AtomicFlag::raised(); + assert!(flag.is_raised()); + assert!(flag.lower()); + } +} diff --git a/vendor/deno_unsync/src/sync/mod.rs b/vendor/deno_unsync/src/sync/mod.rs new file mode 100644 index 000000000..07be86d8c --- /dev/null +++ b/vendor/deno_unsync/src/sync/mod.rs @@ -0,0 +1,9 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +mod flag; +#[cfg(feature = "tokio")] +mod value_creator; + +pub use flag::AtomicFlag; +#[cfg(feature = "tokio")] +pub use value_creator::MultiRuntimeAsyncValueCreator; diff --git a/vendor/deno_unsync/src/sync/value_creator.rs b/vendor/deno_unsync/src/sync/value_creator.rs new file mode 100644 index 000000000..b3d7da398 --- /dev/null +++ b/vendor/deno_unsync/src/sync/value_creator.rs @@ -0,0 +1,213 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +use std::sync::Arc; + +use futures::future::BoxFuture; +use futures::future::LocalBoxFuture; +use futures::future::Shared; +use futures::FutureExt; +use parking_lot::Mutex; +use tokio::task::JoinError; + +type JoinResult = Result>; +type CreateFutureFn = + Box LocalBoxFuture<'static, TResult> + Send + Sync>; + +#[derive(Debug)] +struct State { + retry_index: usize, + future: Option>>>, +} + +/// Attempts to create a shared value asynchronously on one tokio runtime while +/// many runtimes are requesting the value. +/// +/// This is only useful when the value needs to get created once across +/// many runtimes. +/// +/// This handles the case where the tokio runtime creating the value goes down +/// while another one is waiting on the value. +pub struct MultiRuntimeAsyncValueCreator { + create_future: CreateFutureFn, + state: Mutex>, +} + +impl std::fmt::Debug + for MultiRuntimeAsyncValueCreator +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MultiRuntimeAsyncValueCreator").finish() + } +} + +impl MultiRuntimeAsyncValueCreator { + pub fn new(create_future: CreateFutureFn) -> Self { + Self { + state: Mutex::new(State { + retry_index: 0, + future: None, + }), + create_future, + } + } + + pub async fn get(&self) -> TResult { + let (mut future, mut retry_index) = { + let mut state = self.state.lock(); + let future = match &state.future { + Some(future) => future.clone(), + None => { + let future = self.create_shared_future(); + state.future = Some(future.clone()); + future + } + }; + (future, state.retry_index) + }; + + loop { + let result = future.await; + + match result { + Ok(result) => return result, + Err(join_error) => { + if join_error.is_cancelled() { + let mut state = self.state.lock(); + + if state.retry_index == retry_index { + // we were the first one to retry, so create a new future + // that we'll run from the current runtime + state.retry_index += 1; + state.future = Some(self.create_shared_future()); + } + + retry_index = state.retry_index; + future = state.future.as_ref().unwrap().clone(); + + // just in case we're stuck in a loop + if retry_index > 1000 { + panic!("Something went wrong.") // should never happen + } + } else { + panic!("{}", join_error); + } + } + } + } + } + + fn create_shared_future( + &self, + ) -> Shared>> { + let future = (self.create_future)(); + crate::spawn(future) + .map(|result| result.map_err(Arc::new)) + .boxed() + .shared() + } +} + +#[cfg(test)] +mod test { + use crate::spawn; + + use super::*; + + #[tokio::test] + async fn single_runtime() { + let value_creator = MultiRuntimeAsyncValueCreator::new(Box::new(|| { + async { 1 }.boxed_local() + })); + let value = value_creator.get().await; + assert_eq!(value, 1); + } + + #[test] + fn multi_runtimes() { + let value_creator = + Arc::new(MultiRuntimeAsyncValueCreator::new(Box::new(|| { + async { + tokio::task::yield_now().await; + 1 + } + .boxed_local() + }))); + let handles = (0..3) + .map(|_| { + let value_creator = value_creator.clone(); + std::thread::spawn(|| { + create_runtime().block_on(async move { value_creator.get().await }) + }) + }) + .collect::>(); + for handle in handles { + assert_eq!(handle.join().unwrap(), 1); + } + } + + #[test] + fn multi_runtimes_first_never_finishes() { + let is_first_run = Arc::new(Mutex::new(true)); + let (tx, rx) = std::sync::mpsc::channel::<()>(); + let value_creator = Arc::new(MultiRuntimeAsyncValueCreator::new({ + let is_first_run = is_first_run.clone(); + Box::new(move || { + let is_first_run = is_first_run.clone(); + let tx = tx.clone(); + async move { + let is_first_run = { + let mut is_first_run = is_first_run.lock(); + let initial_value = *is_first_run; + *is_first_run = false; + tx.send(()).unwrap(); + initial_value + }; + if is_first_run { + tokio::time::sleep(std::time::Duration::from_millis(30_000)).await; + panic!("TIMED OUT"); // should not happen + } else { + tokio::task::yield_now().await; + } + 1 + } + .boxed_local() + }) + })); + std::thread::spawn({ + let value_creator = value_creator.clone(); + let is_first_run = is_first_run.clone(); + move || { + create_runtime().block_on(async { + let value_creator = value_creator.clone(); + // spawn a task that will never complete + spawn(async move { value_creator.get().await }); + // wait for the task to set is_first_run to false + while *is_first_run.lock() { + tokio::time::sleep(std::time::Duration::from_millis(20)).await; + } + // now exit the runtime while the value_creator is still pending + }) + } + }); + let handle = { + let value_creator = value_creator.clone(); + std::thread::spawn(|| { + create_runtime().block_on(async move { + let value_creator = value_creator.clone(); + rx.recv().unwrap(); + // even though the other runtime shutdown, this get() should + // recover and still get the value + value_creator.get().await + }) + }) + }; + assert_eq!(handle.join().unwrap(), 1); + } + + fn create_runtime() -> tokio::runtime::Runtime { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + } +} diff --git a/vendor/deno_unsync/src/task.rs b/vendor/deno_unsync/src/task.rs new file mode 100644 index 000000000..d3f5a12fa --- /dev/null +++ b/vendor/deno_unsync/src/task.rs @@ -0,0 +1 @@ + diff --git a/vendor/deno_unsync/src/task_queue.rs b/vendor/deno_unsync/src/task_queue.rs new file mode 100644 index 000000000..8e71eb79e --- /dev/null +++ b/vendor/deno_unsync/src/task_queue.rs @@ -0,0 +1,345 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +use std::cell::RefCell; +use std::collections::LinkedList; +use std::future::Future; +use std::rc::Rc; +use std::task::Waker; + +use crate::Flag; + +#[derive(Debug, Default)] +struct TaskQueueTaskItem { + is_ready: Flag, + is_future_dropped: Flag, + waker: RefCell>, +} + +#[derive(Debug, Default)] +struct TaskQueueTasks { + is_running: bool, + items: LinkedList>, +} + +/// A queue that executes tasks sequentially one after the other +/// ensuring order and that no task runs at the same time as another. +#[derive(Debug, Default)] +pub struct TaskQueue { + tasks: RefCell, +} + +impl TaskQueue { + /// Acquires a permit where the tasks are executed one at a time + /// and in the order that they were acquired. + pub fn acquire(self: &Rc) -> TaskQueuePermitAcquireFuture { + TaskQueuePermitAcquireFuture::new(self.clone()) + } + + /// Alternate API that acquires a permit internally + /// for the duration of the future. + pub fn run( + self: &Rc, + future: impl Future, + ) -> impl Future { + let acquire_future = self.acquire(); + async move { + let permit = acquire_future.await; + let result = future.await; + drop(permit); // explicit for clarity + result + } + } + + fn raise_next(&self) { + let front_item = { + let mut tasks = self.tasks.borrow_mut(); + + // clear out any wakers for futures that were dropped + while let Some(front_waker) = tasks.items.front() { + if front_waker.is_future_dropped.is_raised() { + tasks.items.pop_front(); + } else { + break; + } + } + let front_item = tasks.items.pop_front(); + tasks.is_running = front_item.is_some(); + front_item + }; + + // wake up the next waker + if let Some(front_item) = front_item { + front_item.is_ready.raise(); + let maybe_waker = front_item.waker.borrow_mut().take(); + if let Some(waker) = maybe_waker { + waker.wake(); + } + } + } +} + +/// A permit that when dropped will allow another task to proceed. +pub struct TaskQueuePermit(Rc); + +impl Drop for TaskQueuePermit { + fn drop(&mut self) { + self.0.raise_next(); + } +} + +pub struct TaskQueuePermitAcquireFuture { + task_queue: Option>, + item: Option>, +} + +impl Drop for TaskQueuePermitAcquireFuture { + fn drop(&mut self) { + if let Some(task_queue) = self.task_queue.take() { + if let Some(item) = self.item.take() { + if item.is_ready.is_raised() { + task_queue.raise_next(); + } else { + item.is_future_dropped.raise(); + } + } else { + // this was the first item, so raise the next one + task_queue.raise_next(); + } + } + } +} + +impl TaskQueuePermitAcquireFuture { + pub fn new(task_queue: Rc) -> Self { + // acquire the position synchronously + let mut tasks = task_queue.tasks.borrow_mut(); + if !tasks.is_running { + tasks.is_running = true; + drop(tasks); + Self { + task_queue: Some(task_queue), + item: None, // avoid boxing for the fast path + } + } else { + let item = Rc::new(TaskQueueTaskItem::default()); + tasks.items.push_back(item.clone()); + drop(tasks); + Self { + task_queue: Some(task_queue), + item: Some(item), + } + } + } +} + +impl Future for TaskQueuePermitAcquireFuture { + type Output = TaskQueuePermit; + + fn poll( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + // check if we're ready to run + let Some(item) = &self.item else { + // no item means this was the first queued future, so we're ready to run + return std::task::Poll::Ready(TaskQueuePermit( + self.task_queue.take().unwrap(), + )); + }; + if item.is_ready.is_raised() { + // we're done, move the task queue out + std::task::Poll::Ready(TaskQueuePermit(self.task_queue.take().unwrap())) + } else { + // store the waker for next time + let mut stored_waker = item.waker.borrow_mut(); + // update with the latest waker if it's different or not set + if stored_waker + .as_ref() + .map(|w| !w.will_wake(cx.waker())) + .unwrap_or(true) + { + *stored_waker = Some(cx.waker().clone()); + } + + std::task::Poll::Pending + } + } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + use std::sync::Mutex; + + use crate::tokio::JoinSet; + + use super::*; + + #[tokio::test] + async fn task_queue_runs_one_after_other() { + let task_queue = Rc::new(TaskQueue::default()); + let mut set = JoinSet::default(); + let data = Arc::new(Mutex::new(0)); + for i in 0..100 { + let data = data.clone(); + let task_queue = task_queue.clone(); + let acquire = task_queue.acquire(); + set.spawn(async move { + let permit = acquire.await; + crate::spawn_blocking(move || { + let mut data = data.lock().unwrap(); + assert_eq!(i, *data); + *data = i + 1; + }) + .await + .unwrap(); + drop(permit); + drop(task_queue); + }); + } + while let Some(res) = set.join_next().await { + assert!(res.is_ok()); + } + } + + #[tokio::test] + async fn tasks_run_in_sequence() { + let task_queue = Rc::new(TaskQueue::default()); + let data = RefCell::new(0); + + let first = task_queue.run(async { + *data.borrow_mut() = 1; + }); + let second = task_queue.run(async { + assert_eq!(*data.borrow(), 1); + *data.borrow_mut() = 2; + }); + let _ = tokio::join!(first, second); + + assert_eq!(*data.borrow(), 2); + } + + #[tokio::test] + async fn future_dropped_before_poll() { + let task_queue = Rc::new(TaskQueue::default()); + + // acquire a future, but do not await it + let future = task_queue.acquire(); + + // this task tries to acquire another permit, but will be blocked by the first permit. + let enter_flag = Rc::new(Flag::default()); + let delayed_task = crate::spawn({ + let task_queue = task_queue.clone(); + let enter_flag = enter_flag.clone(); + async move { + enter_flag.raise(); + task_queue.acquire().await; + true + } + }); + + // ensure the task gets a chance to be scheduled and blocked + tokio::task::yield_now().await; + assert!(enter_flag.is_raised()); + + // now, drop the first future + drop(future); + + assert!(delayed_task.await.unwrap()); + } + + #[tokio::test] + async fn many_future_dropped_before_poll() { + let task_queue = Rc::new(TaskQueue::default()); + + // acquire a future, but do not await it + let mut futures = Vec::new(); + for _ in 0..=10_000 { + futures.push(task_queue.acquire()); + } + + // this task tries to acquire another permit, but will be blocked by the first permit. + let enter_flag = Rc::new(Flag::default()); + let delayed_task = crate::spawn({ + let task_queue = task_queue.clone(); + let enter_flag = enter_flag.clone(); + async move { + enter_flag.raise(); + task_queue.acquire().await; + true + } + }); + + // ensure the task gets a chance to be scheduled and blocked + tokio::task::yield_now().await; + assert!(enter_flag.is_raised()); + + // now, drop the futures + drop(futures); + + assert!(delayed_task.await.unwrap()); + } + + #[tokio::test] + async fn acquires_position_synchronously() { + let task_queue = Rc::new(TaskQueue::default()); + + let fut1 = task_queue.acquire(); + let fut2 = task_queue.acquire(); + let fut3 = task_queue.acquire(); + let fut4 = task_queue.acquire(); + let value = Rc::new(RefCell::new(0)); + + let task1 = crate::spawn({ + let value = value.clone(); + async move { + let permit = fut2.await; + assert_eq!(*value.borrow(), 1); + *value.borrow_mut() += 1; + drop(permit); + // dropping this future without awaiting it + // should cause the next future to be polled + drop(fut3); + } + }); + let task2 = crate::spawn({ + let value = value.clone(); + async move { + // give the other task some time + tokio::task::yield_now().await; + let permit = fut1.await; + assert_eq!(*value.borrow(), 0); + *value.borrow_mut() += 1; + drop(permit); + } + }); + let task3 = crate::spawn({ + let value = value.clone(); + async move { + // give the other tasks some time + tokio::task::yield_now().await; + let permit = fut4.await; + assert_eq!(*value.borrow(), 2); + *value.borrow_mut() += 1; + drop(permit); + } + }); + + tokio::try_join!(task1, task2, task3).unwrap(); + assert_eq!(*value.borrow(), 3); + } + + #[tokio::test] + async fn middle_future_dropped_while_permit_acquired() { + let task_queue = Rc::new(TaskQueue::default()); + + let fut1 = task_queue.acquire(); + let fut2 = task_queue.acquire(); + let fut3 = task_queue.acquire(); + + // should not hang + drop(fut2); + drop(fut1.await); + drop(fut3.await); + } +} diff --git a/vendor/deno_unsync/src/tokio/joinset.rs b/vendor/deno_unsync/src/tokio/joinset.rs new file mode 100644 index 000000000..7242adcc0 --- /dev/null +++ b/vendor/deno_unsync/src/tokio/joinset.rs @@ -0,0 +1,127 @@ +// Copyright 2018-2024 the Deno authors. MIT license. +// Some code and comments under MIT license where adapted from Tokio code +// Copyright (c) 2023 Tokio Contributors + +use std::future::Future; +use std::task::Context; +use std::task::Poll; +use std::task::Waker; +use tokio::task::AbortHandle; +use tokio::task::JoinError; + +use super::task::MaskFutureAsSend; +use super::task::MaskResultAsSend; + +/// Wraps the tokio [`JoinSet`] to make it !Send-friendly and to make it easier and safer for us to +/// poll while empty. +pub struct JoinSet { + joinset: tokio::task::JoinSet>, + /// If join_next returns Ready(None), we stash the waker + waker: Option, +} + +impl Default for JoinSet { + fn default() -> Self { + Self { + joinset: Default::default(), + waker: None, + } + } +} + +impl JoinSet { + /// Spawn the provided task on the `JoinSet`, returning an [`AbortHandle`] + /// that can be used to remotely cancel the task. + /// + /// The provided future will start running in the background immediately + /// when this method is called, even if you don't await anything on this + /// `JoinSet`. + /// + /// # Panics + /// + /// This method panics if called outside of a Tokio runtime. + /// + /// [`AbortHandle`]: tokio::task::AbortHandle + #[track_caller] + pub fn spawn(&mut self, task: F) -> AbortHandle + where + F: Future, + F: 'static, + T: 'static, + { + // SAFETY: We only use this with the single-thread executor + let handle = self.joinset.spawn(unsafe { MaskFutureAsSend::new(task) }); + + // If someone had called poll_join_next while we were empty, ask them to poll again + // so we can properly register the waker with the underlying JoinSet. + if let Some(waker) = self.waker.take() { + waker.wake(); + } + handle + } + + /// Returns the number of tasks currently in the `JoinSet`. + pub fn len(&self) -> usize { + self.joinset.len() + } + + /// Returns whether the `JoinSet` is empty. + pub fn is_empty(&self) -> bool { + self.joinset.is_empty() + } + + /// Waits until one of the tasks in the set completes and returns its output. + /// + /// # Cancel Safety + /// + /// This method is cancel safe. If `join_next` is used as the event in a `tokio::select!` + /// statement and some other branch completes first, it is guaranteed that no tasks were + /// removed from this `JoinSet`. + pub fn poll_join_next( + &mut self, + cx: &mut Context, + ) -> Poll> { + match self.joinset.poll_join_next(cx) { + Poll::Ready(Some(res)) => Poll::Ready(res.map(|res| res.into_inner())), + Poll::Ready(None) => { + // Stash waker + self.waker = Some(cx.waker().clone()); + Poll::Pending + } + Poll::Pending => Poll::Pending, + } + } + + /// Waits until one of the tasks in the set completes and returns its output. + /// + /// Returns `None` if the set is empty. + /// + /// # Cancel Safety + /// + /// This method is cancel safe. If `join_next` is used as the event in a `tokio::select!` + /// statement and some other branch completes first, it is guaranteed that no tasks were + /// removed from this `JoinSet`. + pub async fn join_next(&mut self) -> Option> { + self + .joinset + .join_next() + .await + .map(|result| result.map(|res| res.into_inner())) + } + + /// Aborts all tasks on this `JoinSet`. + /// + /// This does not remove the tasks from the `JoinSet`. To wait for the tasks to complete + /// cancellation, you should call `join_next` in a loop until the `JoinSet` is empty. + pub fn abort_all(&mut self) { + self.joinset.abort_all(); + } + + /// Removes all tasks from this `JoinSet` without aborting them. + /// + /// The tasks removed by this call will continue to run in the background even if the `JoinSet` + /// is dropped. + pub fn detach_all(&mut self) { + self.joinset.detach_all(); + } +} diff --git a/vendor/deno_unsync/src/tokio/mod.rs b/vendor/deno_unsync/src/tokio/mod.rs new file mode 100644 index 000000000..75c63abe3 --- /dev/null +++ b/vendor/deno_unsync/src/tokio/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +pub use joinset::JoinSet; +pub use split::split_io; +pub use split::IOReadHalf; +pub use split::IOWriteHalf; + +pub use task::is_wall_set; +pub use task::set_wall; +pub use task::spawn; +pub use task::spawn_blocking; +pub use task::JoinHandle; +pub use task::MaskFutureAsSend; + +mod joinset; +mod split; +mod task; diff --git a/vendor/deno_unsync/src/tokio/split.rs b/vendor/deno_unsync/src/tokio/split.rs new file mode 100644 index 000000000..0db8318f7 --- /dev/null +++ b/vendor/deno_unsync/src/tokio/split.rs @@ -0,0 +1,183 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +use crate::UnsendMarker; +use std::cell::Cell; +use std::cell::UnsafeCell; +use std::pin::Pin; +use std::rc::Rc; +use std::task::Poll; + +use tokio::io::AsyncRead; +use tokio::io::AsyncWrite; + +/// Create a ![`Send`] I/O split on top of a stream. The split reader and writer halves are safe to use +/// only in a single-threaded context, and are not legal to send to another thread. +pub fn split_io(stream: S) -> (IOReadHalf, IOWriteHalf) +where + S: AsyncRead + AsyncWrite + Unpin, +{ + let is_write_vectored = stream.is_write_vectored(); + let stream = Rc::new(Split { + stream: UnsafeCell::new(stream), + lock: Cell::new(false), + }); + ( + IOReadHalf { + split: stream.clone(), + _marker: UnsendMarker::default(), + }, + IOWriteHalf { + split: stream, + is_write_vectored, + _marker: UnsendMarker::default(), + }, + ) +} + +struct Split { + stream: UnsafeCell, + lock: Cell, +} + +pub struct IOReadHalf { + split: Rc>, + _marker: UnsendMarker, +} + +pub struct IOWriteHalf { + split: Rc>, + is_write_vectored: bool, + _marker: UnsendMarker, +} + +impl AsyncRead for IOReadHalf +where + S: AsyncRead + Unpin, +{ + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + let lock = &self.split.lock; + if lock.clone().into_inner() { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Re-entrant read while writing", + ))); + } + lock.set(true); + // SAFETY: This is !Send and the lock is set, so we can guarantee we won't get another &mut to the stream now + let s = unsafe { self.split.stream.get().as_mut().unwrap() }; + let res = Pin::new(s).poll_read(cx, buf); + lock.set(false); + res + } +} + +impl AsyncWrite for IOWriteHalf +where + S: AsyncWrite + Unpin, +{ + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let lock = &self.split.lock; + if lock.clone().into_inner() { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Re-entrant write while reading", + ))); + } + lock.set(true); + // SAFETY: This is !Send and the lock is set, so we can guarantee we won't get another &mut to the stream now + let s = unsafe { self.split.stream.get().as_mut().unwrap() }; + let res = Pin::new(s).poll_flush(cx); + lock.set(false); + res + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let lock = &self.split.lock; + if lock.clone().into_inner() { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Re-entrant write while reading", + ))); + } + lock.set(true); + // SAFETY: This is !Send and the lock is set, so we can guarantee we won't get another &mut to the stream now + let s = unsafe { self.split.stream.get().as_mut().unwrap() }; + let res = Pin::new(s).poll_shutdown(cx); + lock.set(false); + res + } + + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + let lock = &self.split.lock; + if lock.clone().into_inner() { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Re-entrant write while reading", + ))); + } + lock.set(true); + // SAFETY: This is !Send and the lock is set, so we can guarantee we won't get another &mut to the stream now + let s = unsafe { self.split.stream.get().as_mut().unwrap() }; + let res = Pin::new(s).poll_write(cx, buf); + lock.set(false); + res + } + + fn poll_write_vectored( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + bufs: &[std::io::IoSlice<'_>], + ) -> std::task::Poll> { + let lock = &self.split.lock; + if lock.clone().into_inner() { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::PermissionDenied, + "Re-entrant write while reading", + ))); + } + lock.set(true); + // SAFETY: This is !Send and the lock is set, so we can guarantee we won't get another &mut to the stream now + let s = unsafe { self.split.stream.get().as_mut().unwrap() }; + let res = Pin::new(s).poll_write_vectored(cx, bufs); + lock.set(false); + res + } + + fn is_write_vectored(&self) -> bool { + self.is_write_vectored + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::AsyncReadExt; + use tokio::io::AsyncWriteExt; + + #[tokio::test(flavor = "current_thread")] + async fn split_duplex() { + let (a, b) = tokio::io::duplex(1024); + let (mut ar, mut aw) = split_io(a); + let (mut br, mut bw) = split_io(b); + + bw.write_i8(123).await.unwrap(); + assert_eq!(ar.read_i8().await.unwrap(), 123); + + aw.write_i8(123).await.unwrap(); + assert_eq!(br.read_i8().await.unwrap(), 123); + } +} diff --git a/vendor/deno_unsync/src/tokio/task.rs b/vendor/deno_unsync/src/tokio/task.rs new file mode 100644 index 000000000..c41c7cfb5 --- /dev/null +++ b/vendor/deno_unsync/src/tokio/task.rs @@ -0,0 +1,167 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +use core::pin::Pin; +use core::task::Context; +use core::task::Poll; +use std::cell::RefCell; +use std::future::Future; +use std::marker::PhantomData; +use tokio::runtime::Handle; +use tokio::runtime::RuntimeFlavor; +use tokio_util::sync::CancellationToken; + +thread_local! { + static WALL: RefCell> = RefCell::default(); +} + +/// Equivalent to [`tokio::task::JoinHandle`]. +#[repr(transparent)] +pub struct JoinHandle { + handle: tokio::task::JoinHandle>, + _r: PhantomData, +} + +impl JoinHandle { + /// Equivalent to [`tokio::task::JoinHandle::abort`]. + pub fn abort(&self) { + self.handle.abort() + } + + pub fn abort_handle(&self) -> tokio::task::AbortHandle { + self.handle.abort_handle() + } +} + +impl Future for JoinHandle { + type Output = Result; + + fn poll( + self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll { + // SAFETY: We are sure that handle is valid here + unsafe { + let me: &mut Self = Pin::into_inner_unchecked(self); + let handle = Pin::new_unchecked(&mut me.handle); + match handle.poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(Ok(r)) => Poll::Ready(Ok(r.into_inner())), + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + } + } + } +} + +pub fn is_wall_set() -> bool { + WALL.with_borrow(|it| it.is_some()) +} + +pub fn set_wall() -> CancellationToken { + let token = CancellationToken::new(); + WALL.with_borrow_mut(|it| { + if let Some(old_token) = it.take() { + old_token.cancel(); + } + *it = Some(token.clone()); + }); + token +} + +/// Equivalent to [`tokio::task::spawn`], but does not require the future to be [`Send`]. Must only be +/// used on a [`RuntimeFlavor::CurrentThread`] executor, though this is only checked when running with +/// debug assertions. +#[inline(always)] +pub fn spawn + 'static, R: 'static>( + f: F, +) -> JoinHandle { + debug_assert!( + Handle::current().runtime_flavor() == RuntimeFlavor::CurrentThread + ); + // SAFETY: we know this is a current-thread executor + let token = WALL.with_borrow(|it| it.clone()); + let future = unsafe { MaskFutureAsSend::new(f) }; + JoinHandle { + handle: tokio::task::spawn(async move { + if let Some(token) = token { + token.cancelled().await; + } + future.await + }), + _r: Default::default(), + } +} + +/// Equivalent to [`tokio::task::spawn_blocking`]. Currently a thin wrapper around the tokio API, but this +/// may change in the future. +#[inline(always)] +pub fn spawn_blocking< + F: (FnOnce() -> R) + Send + 'static, + R: Send + 'static, +>( + f: F, +) -> JoinHandle { + let handle = tokio::task::spawn_blocking(|| MaskResultAsSend { result: f() }); + JoinHandle { + handle, + _r: Default::default(), + } +} + +#[repr(transparent)] +#[doc(hidden)] +pub struct MaskResultAsSend { + result: R, +} + +/// SAFETY: We ensure that Send bounds are only faked when tokio is running on a current-thread executor +unsafe impl Send for MaskResultAsSend {} + +impl MaskResultAsSend { + #[inline(always)] + pub fn into_inner(self) -> R { + self.result + } +} + +#[repr(transparent)] +pub struct MaskFutureAsSend { + future: F, +} + +impl MaskFutureAsSend { + /// Mark a non-`Send` future as `Send`. This is a trick to be able to use + /// `tokio::spawn()` (which requires `Send` futures) in a current thread + /// runtime. + /// + /// # Safety + /// + /// You must ensure that the future is actually used on the same + /// thread, ie. always use current thread runtime flavor from Tokio. + #[inline(always)] + pub unsafe fn new(future: F) -> Self { + Self { future } + } +} + +// SAFETY: we are cheating here - this struct is NOT really Send, +// but we need to mark it Send so that we can use `spawn()` in Tokio. +unsafe impl Send for MaskFutureAsSend {} + +impl Future for MaskFutureAsSend { + type Output = MaskResultAsSend; + + fn poll( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + // SAFETY: We are sure that future is valid here + unsafe { + let me: &mut MaskFutureAsSend = Pin::into_inner_unchecked(self); + let future = Pin::new_unchecked(&mut me.future); + match future.poll(cx) { + Poll::Pending => Poll::Pending, + Poll::Ready(result) => Poll::Ready(MaskResultAsSend { result }), + } + } + } +} diff --git a/vendor/deno_unsync/src/waker.rs b/vendor/deno_unsync/src/waker.rs new file mode 100644 index 000000000..55b5575e7 --- /dev/null +++ b/vendor/deno_unsync/src/waker.rs @@ -0,0 +1,51 @@ +// Copyright 2018-2024 the Deno authors. MIT license. + +use std::cell::UnsafeCell; +use std::task::Waker; + +use crate::UnsendMarker; + +/// A ![`Sync`] and ![`Sync`] equivalent to `AtomicWaker`. +#[derive(Default)] +pub struct UnsyncWaker { + waker: UnsafeCell>, + _unsend_marker: UnsendMarker, +} + +impl UnsyncWaker { + /// Register a waker if the waker represents a different waker than is already stored. + pub fn register(&self, waker: &Waker) { + // SAFETY: This is We can guarantee no other threads are accessing this field as + // we are !Send and !Sync. + unsafe { + if let Some(old_waker) = &mut *self.waker.get() { + if old_waker.will_wake(waker) { + return; + } + } + *self.waker.get() = Some(waker.clone()) + } + } + + /// If a waker has been registered, wake the contained [`Waker`], unregistering it at the same time. + pub fn wake(&self) { + // SAFETY: This is We can guarantee no other threads are accessing this field as + // we are !Send and !Sync. + unsafe { + if let Some(waker) = (*self.waker.get()).take() { + waker.wake(); + } + } + } + + /// If a waker has been registered, wake the contained [`Waker`], maintaining it for later use. + pub fn wake_by_ref(&self) { + // SAFETY: This is We can guarantee no other threads are accessing this field as + // we are !Send and !Sync. + unsafe { + if let Some(waker) = &mut *self.waker.get() { + waker.wake_by_ref(); + } + } + } +}