From f9a68b545df5337420da2dc129bd07de5da41022 Mon Sep 17 00:00:00 2001 From: Sujay Jayakar Date: Tue, 16 Apr 2024 18:40:29 -0400 Subject: [PATCH] Wire up error handling to isolate2 (#24731) GitOrigin-RevId: 7214af8b4ad6faf6c204feec96b16d1009052763 --- crates/isolate/src/environment/helpers/mod.rs | 9 +- .../src/environment/helpers/promise.rs | 27 +- crates/isolate/src/error.rs | 114 ++--- crates/isolate/src/execution_scope.rs | 28 -- crates/isolate/src/isolate2/client.rs | 30 +- crates/isolate/src/isolate2/context_state.rs | 32 +- .../isolate/src/isolate2/entered_context.rs | 98 +++-- crates/isolate/src/isolate2/runner.rs | 275 +++++++----- crates/isolate/src/test_helpers.rs | 4 +- crates/isolate/src/tests/source_maps.rs | 14 +- crates/isolate/src/tests/user_error.rs | 403 ++++++++++-------- 11 files changed, 566 insertions(+), 468 deletions(-) diff --git a/crates/isolate/src/environment/helpers/mod.rs b/crates/isolate/src/environment/helpers/mod.rs index b354e6cb..fee9fa0f 100644 --- a/crates/isolate/src/environment/helpers/mod.rs +++ b/crates/isolate/src/environment/helpers/mod.rs @@ -9,7 +9,6 @@ mod syscall_trace; pub mod validation; mod version; -use common::runtime::Runtime; use deno_core::{ serde_v8, v8, @@ -28,10 +27,6 @@ pub use self::{ syscall_trace::SyscallTrace, version::parse_version, }; -use crate::{ - environment::IsolateEnvironment, - execution_scope::ExecutionScope, -}; pub const MAX_LOG_LINE_LENGTH: usize = 32768; pub const MAX_LOG_LINES: usize = 256; @@ -65,8 +60,8 @@ pub enum Phase { Executing, } -pub fn json_to_v8<'a, 'b: 'a, RT: Runtime, E: IsolateEnvironment>( - scope: &mut ExecutionScope<'a, 'b, RT, E>, +pub fn json_to_v8<'a>( + scope: &mut v8::HandleScope<'a>, json: JsonValue, ) -> anyhow::Result> { let value_v8 = serde_v8::to_v8(scope, json)?; diff --git a/crates/isolate/src/environment/helpers/promise.rs b/crates/isolate/src/environment/helpers/promise.rs index 59d1c1e6..d74cdac1 100644 --- a/crates/isolate/src/environment/helpers/promise.rs +++ b/crates/isolate/src/environment/helpers/promise.rs @@ -1,24 +1,17 @@ use anyhow::anyhow; -use common::{ - errors::{ - report_error, - JsError, - }, - runtime::Runtime, +use common::errors::{ + report_error, + JsError, }; use deno_core::v8; use errors::ErrorMetadataAnyhowExt; use serde_json::Value as JsonValue; use super::json_to_v8; -use crate::{ - environment::IsolateEnvironment, - execution_scope::ExecutionScope, - strings, -}; +use crate::strings; -pub fn resolve_promise>( - scope: &mut ExecutionScope, +pub fn resolve_promise( + scope: &mut v8::HandleScope<'_>, resolver: v8::Global, result: anyhow::Result>, ) -> anyhow::Result<()> { @@ -27,16 +20,16 @@ pub fn resolve_promise>( // Like `resolve_promise` but returns JS error even when the // error might have been caused by Convex, not by the user. -pub fn resolve_promise_allow_all_errors>( - scope: &mut ExecutionScope, +pub fn resolve_promise_allow_all_errors( + scope: &mut v8::HandleScope<'_>, resolver: v8::Global, result: anyhow::Result>, ) -> anyhow::Result<()> { resolve_promise_inner(scope, resolver, result, true) } -fn resolve_promise_inner>( - scope: &mut ExecutionScope, +fn resolve_promise_inner( + scope: &mut v8::HandleScope<'_>, resolver: v8::Global, result: anyhow::Result>, allow_all_errors: bool, diff --git a/crates/isolate/src/error.rs b/crates/isolate/src/error.rs index 6d50920b..f0e6a5ca 100644 --- a/crates/isolate/src/error.rs +++ b/crates/isolate/src/error.rs @@ -10,6 +10,7 @@ use deno_core::{ ModuleSpecifier, }; use sourcemap::SourceMap; +use value::ConvexValue; use crate::{ environment::IsolateEnvironment, @@ -43,6 +44,16 @@ impl<'a, 'b, RT: Runtime, E: IsolateEnvironment> ExecutionScope<'a, 'b, RT, Ok(error) } + fn extract_source_mapped_error( + &mut self, + exception: v8::Local, + ) -> anyhow::Result { + let (message, frame_data, custom_data) = extract_source_mapped_error(self, exception)?; + JsError::from_frames(message, frame_data, custom_data, |s| { + self.lookup_source_map(s) + }) + } + pub fn lookup_source_map( &mut self, specifier: &ModuleSpecifier, @@ -56,64 +67,59 @@ impl<'a, 'b, RT: Runtime, E: IsolateEnvironment> ExecutionScope<'a, 'b, RT, }; Ok(Some(SourceMap::from_slice(source_map.as_bytes())?)) } +} - fn extract_source_mapped_error( - &mut self, - exception: v8::Local, - ) -> anyhow::Result { - if !(is_instance_of_error(self, exception)) { - anyhow::bail!("Exception wasn't an instance of `Error`"); - } - let exception_obj: v8::Local = exception.try_into()?; - - // Get the message by formatting error.name and error.message. - let name = get_property(self, exception_obj, "name")? - .filter(|v| !v.is_undefined()) - .and_then(|m| m.to_string(self)) - .map(|s| s.to_rust_string_lossy(self)) - .unwrap_or_else(|| "Error".to_string()); - let message_prop = get_property(self, exception_obj, "message")? - .filter(|v| !v.is_undefined()) - .and_then(|m| m.to_string(self)) - .map(|s| s.to_rust_string_lossy(self)) - .unwrap_or_else(|| "".to_string()); - let message = format_uncaught_error(message_prop, name); +pub fn extract_source_mapped_error( + scope: &mut v8::HandleScope<'_>, + exception: v8::Local, +) -> anyhow::Result<(String, Vec, Option)> { + if !(is_instance_of_error(scope, exception)) { + anyhow::bail!("Exception wasn't an instance of `Error`"); + } + let exception_obj: v8::Local = exception.try_into()?; - // Access the `stack` property to ensure `prepareStackTrace` has been called. - // NOTE if this is the first time accessing `stack`, it will call the op - // `error/stack` which does a redundant source map lookup. - let _stack: v8::Local = get_property(self, exception_obj, "stack")? - .ok_or_else(|| anyhow::anyhow!("Exception was missing the `stack` property"))? - .try_into()?; + // Get the message by formatting error.name and error.message. + let name = get_property(scope, exception_obj, "name")? + .filter(|v| !v.is_undefined()) + .and_then(|m| m.to_string(scope)) + .map(|s| s.to_rust_string_lossy(scope)) + .unwrap_or_else(|| "Error".to_string()); + let message_prop = get_property(scope, exception_obj, "message")? + .filter(|v| !v.is_undefined()) + .and_then(|m| m.to_string(scope)) + .map(|s| s.to_rust_string_lossy(scope)) + .unwrap_or_else(|| "".to_string()); + let message = format_uncaught_error(message_prop, name); - let frame_data: v8::Local = get_property(self, exception_obj, "__frameData")? - .ok_or_else(|| anyhow::anyhow!("Exception was missing the `__frameData` property"))? - .try_into()?; - let frame_data = to_rust_string(self, &frame_data)?; - let frame_data: Vec = serde_json::from_str(&frame_data)?; + // Access the `stack` property to ensure `prepareStackTrace` has been called. + // NOTE if this is the first time accessing `stack`, it will call the op + // `error/stack` which does a redundant source map lookup. + let _stack: v8::Local = get_property(scope, exception_obj, "stack")? + .ok_or_else(|| anyhow::anyhow!("Exception was missing the `stack` property"))? + .try_into()?; - // error[error.ConvexErrorSymbol] === true - let convex_error_symbol = get_property(self, exception_obj, "ConvexErrorSymbol")?; - let is_convex_error = convex_error_symbol.map_or(false, |symbol| { - exception_obj - .get(self, symbol) - .map_or(false, |v| v.is_true()) - }); + let frame_data: v8::Local = get_property(scope, exception_obj, "__frameData")? + .ok_or_else(|| anyhow::anyhow!("Exception was missing the `__frameData` property"))? + .try_into()?; + let frame_data = to_rust_string(scope, &frame_data)?; + let frame_data: Vec = serde_json::from_str(&frame_data)?; - let custom_data = if is_convex_error { - let custom_data: v8::Local = get_property(self, exception_obj, "data")? - .ok_or_else(|| { - anyhow::anyhow!("The thrown ConvexError is missing `data` property") - })? - .try_into()?; - Some(to_rust_string(self, &custom_data)?) - } else { - None - }; - let (message, custom_data) = deserialize_udf_custom_error(message, custom_data)?; + // error[error.ConvexErrorSymbol] === true + let convex_error_symbol = get_property(scope, exception_obj, "ConvexErrorSymbol")?; + let is_convex_error = convex_error_symbol.map_or(false, |symbol| { + exception_obj + .get(scope, symbol) + .map_or(false, |v| v.is_true()) + }); - JsError::from_frames(message, frame_data, custom_data, |s| { - self.lookup_source_map(s) - }) - } + let custom_data = if is_convex_error { + let custom_data: v8::Local = get_property(scope, exception_obj, "data")? + .ok_or_else(|| anyhow::anyhow!("The thrown ConvexError is missing `data` property"))? + .try_into()?; + Some(to_rust_string(scope, &custom_data)?) + } else { + None + }; + let (message, custom_data) = deserialize_udf_custom_error(message, custom_data)?; + Ok((message, frame_data, custom_data)) } diff --git a/crates/isolate/src/execution_scope.rs b/crates/isolate/src/execution_scope.rs index a16dcc6d..12c49a22 100644 --- a/crates/isolate/src/execution_scope.rs +++ b/crates/isolate/src/execution_scope.rs @@ -19,7 +19,6 @@ use common::{ types::UdfType, }; use deno_core::{ - serde_v8, v8::{ self, HeapStatistics, @@ -374,33 +373,6 @@ impl<'a, 'b: 'a, RT: Runtime, E: IsolateEnvironment> ExecutionScope<'a, 'b, Ok(id) } - pub fn start_dynamic_import<'s>( - &mut self, - resource_name: v8::Local<'s, v8::Value>, - specifier: v8::Local<'s, v8::String>, - ) -> anyhow::Result> { - let promise_resolver = v8::PromiseResolver::new(self) - .ok_or_else(|| anyhow::anyhow!("Failed to create v8::PromiseResolver"))?; - - let promise = promise_resolver.get_promise(self); - let resolver = v8::Global::new(self, promise_resolver); - - let referrer_name: String = serde_v8::from_v8(self, resource_name)?; - let specifier_str = helpers::to_rust_string(self, &specifier)?; - - let resolved_specifier = deno_core::resolve_import(&specifier_str, &referrer_name) - .map_err(|e| ErrorMetadata::bad_request("InvalidImport", e.to_string()))?; - - let dynamic_imports = self.pending_dynamic_imports_mut(); - anyhow::ensure!( - dynamic_imports.allow_dynamic_imports, - "dynamic_import_callback registered without allow_dynamic_imports?" - ); - dynamic_imports.push(resolved_specifier, resolver); - - Ok(promise) - } - async fn lookup_source( &mut self, module_specifier: &ModuleSpecifier, diff --git a/crates/isolate/src/isolate2/client.rs b/crates/isolate/src/isolate2/client.rs index a9bbb87f..5c6fda80 100644 --- a/crates/isolate/src/isolate2/client.rs +++ b/crates/isolate/src/isolate2/client.rs @@ -7,7 +7,6 @@ use std::{ }; use common::{ - errors::JsError, runtime::Runtime, types::UdfType, }; @@ -36,38 +35,41 @@ pub enum IsolateThreadRequest { RegisterModule { name: ModuleSpecifier, source: String, - response: oneshot::Sender>, + source_map: Option, + response: oneshot::Sender>>, }, EvaluateModule { name: ModuleSpecifier, - // XXX: how do we want to pipe through JS errors across threads? - response: oneshot::Sender<()>, + response: oneshot::Sender>, }, StartFunction { udf_type: UdfType, module: ModuleSpecifier, name: String, args: ConvexObject, - response: oneshot::Sender<(FunctionId, EvaluateResult)>, + response: oneshot::Sender>, }, PollFunction { function_id: FunctionId, completions: Vec, - response: oneshot::Sender, + response: oneshot::Sender>, }, } #[derive(Debug)] pub enum EvaluateResult { - Ready { - result: ConvexValue, - outcome: EnvironmentOutcome, - }, + Ready(ReadyEvaluateResult), Pending { async_syscalls: Vec, }, } +#[derive(Debug)] +pub struct ReadyEvaluateResult { + pub result: ConvexValue, + pub outcome: EnvironmentOutcome, +} + #[derive(Debug)] pub struct PendingAsyncSyscall { pub promise_id: PromiseId, @@ -77,7 +79,7 @@ pub struct PendingAsyncSyscall { pub struct AsyncSyscallCompletion { pub promise_id: PromiseId, - pub result: Result, + pub result: anyhow::Result, } pub struct IsolateThreadClient { @@ -105,7 +107,7 @@ impl IsolateThreadClient { pub async fn send( &mut self, request: IsolateThreadRequest, - mut rx: oneshot::Receiver, + mut rx: oneshot::Receiver>, ) -> anyhow::Result { if self.user_time_remaining.is_zero() { anyhow::bail!("User time exhausted"); @@ -136,19 +138,21 @@ impl IsolateThreadClient { // Tokio thread to talk to its V8 thread. drop(permit); - Ok(result?) + result? } pub async fn register_module( &mut self, name: ModuleSpecifier, source: String, + source_map: Option, ) -> anyhow::Result> { let (tx, rx) = oneshot::channel(); self.send( IsolateThreadRequest::RegisterModule { name, source, + source_map, response: tx, }, rx, diff --git a/crates/isolate/src/isolate2/context_state.rs b/crates/isolate/src/isolate2/context_state.rs index 2e373d40..4a551c87 100644 --- a/crates/isolate/src/isolate2/context_state.rs +++ b/crates/isolate/src/isolate2/context_state.rs @@ -53,9 +53,14 @@ pub enum ContextFailure { UncatchableDeveloperError(JsError), } +struct LoadedModule { + pub handle: v8::Global, + pub source_map: Option, +} + pub struct ModuleMap { - pub modules: BTreeMap>, - pub by_v8_module: HashMap, ModuleSpecifier>, + modules: BTreeMap, + by_v8_module: HashMap, ModuleSpecifier>, } impl ModuleMap { @@ -70,17 +75,34 @@ impl ModuleMap { self.modules.contains_key(name) } + pub fn lookup_module(&self, name: &ModuleSpecifier) -> Option<&v8::Global> { + self.modules.get(name).map(|m| &m.handle) + } + + pub fn lookup_by_v8_module(&self, handle: &v8::Global) -> Option<&ModuleSpecifier> { + self.by_v8_module.get(handle) + } + + pub fn lookup_source_map(&self, name: &ModuleSpecifier) -> Option<&str> { + self.modules.get(name).and_then(|m| m.source_map.as_deref()) + } + pub fn register( &mut self, name: ModuleSpecifier, - module: v8::Global, + v8_module: v8::Global, + source_map: Option, ) -> anyhow::Result<()> { anyhow::ensure!( !self.modules.contains_key(&name), "Module already registered" ); - self.modules.insert(name.clone(), module.clone()); - self.by_v8_module.insert(module, name); + let module = LoadedModule { + handle: v8_module.clone(), + source_map, + }; + self.modules.insert(name.clone(), module); + self.by_v8_module.insert(v8_module, name); Ok(()) } } diff --git a/crates/isolate/src/isolate2/entered_context.rs b/crates/isolate/src/isolate2/entered_context.rs index 336ad144..2920264b 100644 --- a/crates/isolate/src/isolate2/entered_context.rs +++ b/crates/isolate/src/isolate2/entered_context.rs @@ -6,9 +6,8 @@ use common::{ types::UdfType, }; use deno_core::{ - v8::{ - self, - }, + serde_v8, + v8, ModuleSpecifier, }; use errors::ErrorMetadata; @@ -23,17 +22,22 @@ use super::{ AsyncSyscallCompletion, EvaluateResult, PendingAsyncSyscall, + ReadyEvaluateResult, }, context_state::ContextState, }; use crate::{ bundled_js::system_udf_file, + environment::helpers::resolve_promise, + error::extract_source_mapped_error, helpers::{ self, to_rust_string, }, isolate::SETUP_URL, isolate2::context::Context, + metrics, + ops::OpProvider, strings, }; @@ -70,9 +74,10 @@ impl<'enter, 'scope: 'enter> EnteredContext<'enter, 'scope> { pub fn run_setup_module(&mut self) -> anyhow::Result<()> { let setup_url = ModuleSpecifier::parse(SETUP_URL)?; - let (source, _) = + let (source, source_map) = system_udf_file("setup.js").ok_or_else(|| anyhow!("Setup module not found"))?; - let unresolved_imports = self.register_module(&setup_url, source)?; + let unresolved_imports = + self.register_module(&setup_url, source, source_map.map(|s| s.to_string()))?; anyhow::ensure!( unresolved_imports.is_empty(), "Unexpected import specifiers for setup module" @@ -128,9 +133,7 @@ impl<'enter, 'scope: 'enter> EnteredContext<'enter, 'scope> { if let Some((_promise, error_global)) = promise_rejection { let error = v8::Local::new(self.scope, error_global); let err = self.format_traceback(error)?; - - // XXX: how do we want to plumb this to the termination stuff? - anyhow::bail!("Unhandled promise rejection: {err:?}"); + return Err(err.into()); } Ok(r) } @@ -139,6 +142,7 @@ impl<'enter, 'scope: 'enter> EnteredContext<'enter, 'scope> { &mut self, url: &ModuleSpecifier, source: &str, + source_map: Option, ) -> anyhow::Result> { { let context_state = self.context_state_mut()?; @@ -176,7 +180,9 @@ impl<'enter, 'scope: 'enter> EnteredContext<'enter, 'scope> { let unresolved_imports = { let context_state = self.context_state_mut()?; import_specifiers.retain(|s| !context_state.module_map.contains_module(s)); - context_state.module_map.register(url.clone(), module)?; + context_state + .module_map + .register(url.clone(), module, source_map)?; import_specifiers }; Ok(unresolved_imports) @@ -190,8 +196,7 @@ impl<'enter, 'scope: 'enter> EnteredContext<'enter, 'scope> { let context_state = self.context_state()?; context_state .module_map - .modules - .get(url) + .lookup_module(url) .ok_or_else(|| anyhow!("Module not registered"))? .clone() }; @@ -248,15 +253,13 @@ impl<'enter, 'scope: 'enter> EnteredContext<'enter, 'scope> { let context_state = self.context_state()?; let referrer_name = context_state .module_map - .by_v8_module - .get(&referrer_global) + .lookup_by_v8_module(&referrer_global) .ok_or_else(|| anyhow!("Module not registered"))? .to_string(); let resolved_specifier = deno_core::resolve_import(&specifier_str, &referrer_name)?; let module = context_state .module_map - .modules - .get(&resolved_specifier) + .lookup_module(&resolved_specifier) .ok_or_else(|| anyhow!("Couldn't find {resolved_specifier}"))? .clone(); v8::Local::new(self.scope, module) @@ -308,8 +311,7 @@ impl<'enter, 'scope: 'enter> EnteredContext<'enter, 'scope> { context_state.environment.start_execution()?; context_state .module_map - .modules - .get(url) + .lookup_module(url) .ok_or_else(|| anyhow!("Module not registered"))? .clone() }; @@ -414,21 +416,11 @@ impl<'enter, 'scope: 'enter> EnteredContext<'enter, 'scope> { completed }; for (resolver, result) in completed { - let resolver = v8::Local::new(self.scope, resolver); - match result { - Ok(v) => { - let s = serde_json::to_string(&v)?; - let v = v8::String::new(self.scope, &s) - .ok_or_else(|| anyhow!("Failed to create result string"))?; - resolver.resolve(self.scope, v.into()); - }, - Err(e) => { - let message = v8::String::new(self.scope, &e.message) - .ok_or_else(|| anyhow!("Failed to create error message string"))?; - let exception = v8::Exception::error(self.scope, message); - resolver.reject(self.scope, exception); - }, + let result_v8 = match result { + Ok(v) => Ok(serde_v8::to_v8(self.scope, v)?), + Err(e) => Err(e), }; + resolve_promise(self.scope, resolver, result_v8)?; } self.execute_user_code(|s| s.perform_microtask_checkpoint())?; @@ -454,7 +446,10 @@ impl<'enter, 'scope: 'enter> EnteredContext<'enter, 'scope> { let result_json: JsonValue = serde_json::from_str(&result)?; let result = ConvexValue::try_from(result_json)?; let outcome = self.context_state_mut()?.environment.finish_execution()?; - Ok(EvaluateResult::Ready { result, outcome }) + Ok(EvaluateResult::Ready(ReadyEvaluateResult { + result, + outcome, + })) }, v8::PromiseState::Rejected => { todo!() @@ -586,13 +581,30 @@ impl<'enter, 'scope: 'enter> EnteredContext<'enter, 'scope> { } pub fn format_traceback(&mut self, exception: v8::Local) -> anyhow::Result { - // XXX: check if terminated - // XXX: collect unsourcemapped error here and let the tokio thread do - // sourcemapping if needed. - let message = v8::Exception::create_message(self.scope, exception); - let message = message.get(self.scope); - let message = to_rust_string(self.scope, &message)?; - Ok(JsError::from_message(message)) + // Check if we hit a system error or timeout and can't run any JavaScript now. + // Abort with a system error here, and we'll (in the best case) pull out + // the original system error that initiated the termination. + if self.scope.is_execution_terminating() { + anyhow::bail!("Execution terminated"); + } + let err: anyhow::Result<_> = try { + let (message, frame_data, custom_data) = + extract_source_mapped_error(self.scope, exception)?; + JsError::from_frames(message, frame_data, custom_data, |s| { + self.lookup_source_map(s) + })? + }; + let err = match err { + Ok(e) => e, + Err(e) => { + let message = v8::Exception::create_message(self.scope, exception); + let message = message.get(self.scope); + let message = to_rust_string(self.scope, &message)?; + metrics::log_source_map_failure(&message, &e); + JsError::from_message(message) + }, + }; + Ok(err) } } @@ -641,9 +653,13 @@ mod op_provider { fn lookup_source_map( &mut self, - _specifier: &ModuleSpecifier, + specifier: &ModuleSpecifier, ) -> anyhow::Result> { - todo!() + let context_state = self.context_state()?; + let Some(source_map) = context_state.module_map.lookup_source_map(specifier) else { + return Ok(None); + }; + Ok(Some(SourceMap::from_slice(source_map.as_bytes())?)) } fn trace(&mut self, level: LogLevel, messages: Vec) -> anyhow::Result<()> { diff --git a/crates/isolate/src/isolate2/runner.rs b/crates/isolate/src/isolate2/runner.rs index 56f42d14..eeb20565 100644 --- a/crates/isolate/src/isolate2/runner.rs +++ b/crates/isolate/src/isolate2/runner.rs @@ -1,3 +1,15 @@ +// TODO: +// - QueryManager, lazy query initialization +// - Source maps on V8 thread +// - Sending table mappings to V8 thread +// - Environment variables and lazy read set size check +// - Log streaming +// - Changing invocation API to be less UDF centric +// - Timer for logging user time from tokio thread +// - Error handling +// - Regular actions +// - HTTP actions +// - Other environments (schema, auth.config.js, analyze) use std::{ cmp::Ordering, sync::Arc, @@ -5,6 +17,7 @@ use std::{ }; use common::{ + errors::JsError, execution_context::ExecutionContext, log_lines::{ LogLevel, @@ -55,6 +68,7 @@ use super::{ EvaluateResult, IsolateThreadClient, IsolateThreadRequest, + ReadyEvaluateResult, }, context::Context, environment::{ @@ -89,6 +103,7 @@ use crate::{ }, JsonPackedValue, ModuleLoader, + ModuleNotFoundError, SyscallTrace, UdfOutcome, }; @@ -102,17 +117,24 @@ fn handle_request( IsolateThreadRequest::RegisterModule { name, source, + source_map, response, } => { - let imports = context.enter(session, |mut ctx| ctx.register_module(&name, &source))?; - let _ = response.send(imports); + let result = context.enter(session, |mut ctx| { + ctx.register_module(&name, &source, source_map) + }); + response + .send(result) + .map_err(|_| anyhow::anyhow!("Canceled"))?; }, IsolateThreadRequest::EvaluateModule { name, response } => { - context.enter(session, |mut ctx| { + let result = context.enter(session, |mut ctx| { ctx.evaluate_module(&name)?; anyhow::Ok(()) - })?; - let _ = response.send(()); + }); + response + .send(result) + .map_err(|_| anyhow::anyhow!("Canceled"))?; }, IsolateThreadRequest::StartFunction { udf_type, @@ -121,16 +143,16 @@ fn handle_request( args, response, } => { - let r = context.start_function(session, udf_type, &module, &name, args)?; - let _ = response.send(r); + let r = context.start_function(session, udf_type, &module, &name, args); + response.send(r).map_err(|_| anyhow::anyhow!("Canceled"))?; }, IsolateThreadRequest::PollFunction { function_id, completions, response, } => { - let r = context.poll_function(session, function_id, completions)?; - let _ = response.send(r); + let r = context.poll_function(session, function_id, completions); + response.send(r).map_err(|_| anyhow::anyhow!("Canceled"))?; }, } Ok(()) @@ -343,25 +365,57 @@ async fn run_request( udf_path: CanonicalizedUdfPath, args: ConvexObject, ) -> anyhow::Result { - let mut stack = vec![udf_path.module().clone()]; - - while let Some(module_path) = stack.pop() { - let module_specifier = module_specifier_from_path(&module_path)?; - let Some(module_metadata) = module_loader.get_module(tx, module_path.clone()).await? else { - anyhow::bail!("Module not found: {module_path:?}") - }; - let requests = client - .register_module(module_specifier, module_metadata.source.clone()) - .await?; - for requested_module_specifier in requests { - let module_path = path_from_module_specifier(&requested_module_specifier)?; - stack.push(module_path); + // Phase 1: Load and register all source needed, and evaluate the UDF's module. + let r: anyhow::Result<_> = try { + let mut stack = vec![udf_path.module().clone()]; + + while let Some(module_path) = stack.pop() { + let module_specifier = module_specifier_from_path(&module_path)?; + let Some(module_metadata) = module_loader.get_module(tx, module_path.clone()).await? + else { + let err = ModuleNotFoundError::new(module_path.as_str()); + Err(JsError::from_message(format!("{err}")))? + }; + let requests = client + .register_module( + module_specifier, + module_metadata.source.clone(), + module_metadata.source_map.clone(), + ) + .await?; + for requested_module_specifier in requests { + let module_path = path_from_module_specifier(&requested_module_specifier)?; + stack.push(module_path); + } } - } - let udf_module_specifier = module_specifier_from_path(udf_path.module())?; - - client.evaluate_module(udf_module_specifier.clone()).await?; + let udf_module_specifier = module_specifier_from_path(udf_path.module())?; + client.evaluate_module(udf_module_specifier.clone()).await?; + udf_module_specifier + }; + let udf_module_specifier = match r { + Ok(m) => m, + Err(e) => match e.downcast::() { + Ok(js_error) => { + let outcome = UdfOutcome { + udf_path, + arguments: vec![ConvexValue::Object(args)].try_into()?, + identity: tx.inert_identity(), + rng_seed: execution_time_seed.rng_seed, + observed_rng: false, + unix_timestamp: execution_time_seed.unix_timestamp, + observed_time: false, + log_lines: vec![].into(), + journal: QueryJournal::new(), + result: Err(js_error), + syscall_trace: SyscallTrace::new(), + udf_server_version: None, + }; + return Ok(outcome); + }, + Err(e) => return Err(e), + }, + }; let mut provider = Isolate2SyscallProvider { tx, @@ -373,90 +427,99 @@ async fn run_request( is_system: udf_path.is_system(), }; - let (function_id, mut result) = client - .start_function( - udf_type, - udf_module_specifier.clone(), - udf_path.function_name().to_string(), - args.clone(), - ) - .await?; - - loop { - let async_syscalls = match result { - EvaluateResult::Ready { result, outcome } => { - return Ok(UdfOutcome { - udf_path, - arguments: vec![ConvexValue::Object(args)].try_into()?, - identity: provider.tx.inert_identity(), - rng_seed: execution_time_seed.rng_seed, - observed_rng: outcome.observed_rng, - unix_timestamp: execution_time_seed.unix_timestamp, - observed_time: outcome.observed_time, - log_lines: vec![].into(), - journal: provider.next_journal, - result: Ok(JsonPackedValue::pack(result)), - syscall_trace: SyscallTrace::new(), - udf_server_version: None, - }); - }, - EvaluateResult::Pending { async_syscalls } => async_syscalls, - }; - - let mut completions = vec![]; - - let mut syscall_batch = None; - let mut batch_promise_ids = vec![]; - - for async_syscall in async_syscalls { - let promise_id = async_syscall.promise_id; - match syscall_batch { - None => { - syscall_batch = Some(AsyncSyscallBatch::new( - async_syscall.name, - async_syscall.args, - )); - assert!(batch_promise_ids.is_empty()); - batch_promise_ids.push(promise_id); - }, - Some(ref mut batch) if batch.can_push(&async_syscall.name, &async_syscall.args) => { - batch.push(async_syscall.name, async_syscall.args)?; - batch_promise_ids.push(promise_id); - }, - Some(batch) => { - let results = - DatabaseSyscallsV1::run_async_syscall_batch(&mut provider, batch).await?; - assert_eq!(results.len(), batch_promise_ids.len()); - - for (promise_id, result) in batch_promise_ids.drain(..).zip(results) { - // TODO: Avoid reparsing the result here. - let result: JsonValue = serde_json::from_str(&(result?))?; - completions.push(AsyncSyscallCompletion { - promise_id, - result: Ok(result), - }); - } - - syscall_batch = None; - }, + // Phase 2: Start the UDF, execute its async syscalls, and poll until + // completion. + let r: anyhow::Result<_> = try { + let (function_id, mut result) = client + .start_function( + udf_type, + udf_module_specifier.clone(), + udf_path.function_name().to_string(), + args.clone(), + ) + .await?; + loop { + let async_syscalls = match result { + EvaluateResult::Ready(r) => break r, + EvaluateResult::Pending { async_syscalls } => async_syscalls, + }; + let mut completions = vec![]; + + let mut syscall_batch = None; + let mut batch_promise_ids = vec![]; + + for async_syscall in async_syscalls { + let promise_id = async_syscall.promise_id; + match syscall_batch { + None => { + syscall_batch = Some(AsyncSyscallBatch::new( + async_syscall.name, + async_syscall.args, + )); + assert!(batch_promise_ids.is_empty()); + batch_promise_ids.push(promise_id); + }, + Some(ref mut batch) + if batch.can_push(&async_syscall.name, &async_syscall.args) => + { + batch.push(async_syscall.name, async_syscall.args)?; + batch_promise_ids.push(promise_id); + }, + Some(batch) => { + let results = + DatabaseSyscallsV1::run_async_syscall_batch(&mut provider, batch) + .await?; + assert_eq!(results.len(), batch_promise_ids.len()); + + for (promise_id, result) in batch_promise_ids.drain(..).zip(results) { + completions.push(AsyncSyscallCompletion { promise_id, result }); + } + + syscall_batch = None; + }, + } } - } - if let Some(batch) = syscall_batch { - let results = DatabaseSyscallsV1::run_async_syscall_batch(&mut provider, batch).await?; - assert_eq!(results.len(), batch_promise_ids.len()); - - for (promise_id, result) in batch_promise_ids.into_iter().zip(results) { - // TODO: Avoid reparsing the result here. - let result: JsonValue = serde_json::from_str(&(result?))?; - completions.push(AsyncSyscallCompletion { - promise_id, - result: Ok(result), - }); + if let Some(batch) = syscall_batch { + let results = + DatabaseSyscallsV1::run_async_syscall_batch(&mut provider, batch).await?; + assert_eq!(results.len(), batch_promise_ids.len()); + + for (promise_id, result) in batch_promise_ids.into_iter().zip(results) { + completions.push(AsyncSyscallCompletion { promise_id, result }); + } } + + result = client.poll_function(function_id, completions).await?; } + }; - result = client.poll_function(function_id, completions).await?; - } + let (result, outcome) = match r { + Ok(ReadyEvaluateResult { result, outcome }) => (Ok(JsonPackedValue::pack(result)), outcome), + Err(e) => { + let js_error = e.downcast::()?; + // TODO: Ask the V8 thread for its outcome. + let outcome = EnvironmentOutcome { + observed_rng: false, + observed_time: false, + }; + (Err(js_error), outcome) + }, + }; + let outcome = UdfOutcome { + udf_path, + arguments: vec![ConvexValue::Object(args)].try_into()?, + identity: provider.tx.inert_identity(), + rng_seed: execution_time_seed.rng_seed, + observed_rng: outcome.observed_rng, + unix_timestamp: execution_time_seed.unix_timestamp, + observed_time: outcome.observed_time, + log_lines: vec![].into(), + journal: provider.next_journal, + result, + syscall_trace: SyscallTrace::new(), + udf_server_version: None, + }; + Ok(outcome) } struct Isolate2SyscallProvider<'a, RT: Runtime> { diff --git a/crates/isolate/src/test_helpers.rs b/crates/isolate/src/test_helpers.rs index b783db97..15216200 100644 --- a/crates/isolate/src/test_helpers.rs +++ b/crates/isolate/src/test_helpers.rs @@ -979,11 +979,11 @@ impl UdfTest { Fut: Future>, { let test = Self::default(rt.clone()).await?; - f(test).await?; + f(test).await.context("test failed on isolate1")?; let mut test = Self::default(rt.clone()).await?; test.enable_isolate_v2(); - f(test).await?; + f(test).await.context("test failed on isolate2")?; Ok(()) } diff --git a/crates/isolate/src/tests/source_maps.rs b/crates/isolate/src/tests/source_maps.rs index 07d6ee97..cb925a37 100644 --- a/crates/isolate/src/tests/source_maps.rs +++ b/crates/isolate/src/tests/source_maps.rs @@ -11,10 +11,12 @@ Uncaught Error: Oh bother! #[convex_macro::test_runtime] async fn test_source_mapping(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - let e = t - .query_js_error("sourceMaps:throwsError", assert_obj!()) - .await?; - assert!(format!("{e}").starts_with(EXPECTED.trim()), "{e:?}"); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + let e = t + .query_js_error("sourceMaps:throwsError", assert_obj!()) + .await?; + assert!(format!("{e}").starts_with(EXPECTED.trim()), "{e:?}"); + Ok(()) + }) + .await } diff --git a/crates/isolate/src/tests/user_error.rs b/crates/isolate/src/tests/user_error.rs index 70d947f7..86fd7933 100644 --- a/crates/isolate/src/tests/user_error.rs +++ b/crates/isolate/src/tests/user_error.rs @@ -29,279 +29,304 @@ use crate::test_helpers::UdfTest; #[convex_macro::test_runtime] async fn test_not_found(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - let err = t - .query_js_error_no_validation("nonexistent", assert_obj!()) - .await?; + UdfTest::run_test_with_isolate2(rt, async move |t| { + let err = t + .query_js_error_no_validation("nonexistent", assert_obj!()) + .await?; - // TODO: It'd be nice to be able to downcast from `anyhow` here, but we - // intentionally stringify the error when stuffing it in the `UdfOutcome` - // structure. This way we could provide additional context to the user on - // error, especially in "development mode," without having to store it all - // in the database. - assert!(format!("{}", err).contains("Couldn't find JavaScript module")); + // TODO: It'd be nice to be able to downcast from `anyhow` here, but we + // intentionally stringify the error when stuffing it in the `UdfOutcome` + // structure. This way we could provide additional context to the user on + // error, especially in "development mode," without having to store it all + // in the database. + assert!(format!("{}", err).contains("Couldn't find JavaScript module")); - let err = t - .query_js_error_no_validation("userError:aPrivateFunction", assert_obj!()) - .await?; - assert!(format!("{}", err).contains(r#"Couldn't find "aPrivateFunction" in module"#)); + let err = t + .query_js_error_no_validation("userError:aPrivateFunction", assert_obj!()) + .await?; + assert!(format!("{}", err).contains(r#"Couldn't find "aPrivateFunction" in module"#)); - let err = t - .query_js_error_no_validation("userError:aNonexistentFunction", assert_obj!()) - .await?; - assert!(format!("{}", err).contains(r#"Couldn't find "aNonexistentFunction" in module"#)); - - Ok(()) + let err = t + .query_js_error_no_validation("userError:aNonexistentFunction", assert_obj!()) + .await?; + assert!(format!("{}", err).contains(r#"Couldn't find "aNonexistentFunction" in module"#)); + Ok(()) + }) + .await } #[convex_macro::test_runtime] async fn test_bad_arguments_error(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - must_let!(let Ok(ConvexValue::String(s)) = t.query("userError:badArgumentsError", assert_obj!()).await); - assert!(s.contains("Invalid argument `id` for `db.get`"), "{s}"); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + must_let!(let Ok(ConvexValue::String(s)) = t.query("userError:badArgumentsError", assert_obj!()).await); + assert!(s.contains("Invalid argument `id` for `db.get`"), "{s}"); + Ok(()) + }).await } #[convex_macro::test_runtime] async fn test_bad_id_error(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - must_let!(let Ok(ConvexValue::String(s)) = t.query("userError:badIdError", assert_obj!()).await); - // A system UDF (listById) relies on this error message being invariant. - assert!(s.contains("Unable to decode ID"), "{s}"); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + must_let!(let Ok(ConvexValue::String(s)) = t.query("userError:badIdError", assert_obj!()).await); + // A system UDF (listById) relies on this error message being invariant. + assert!(s.contains("Unable to decode ID"), "{s}"); + Ok(()) + }).await } #[convex_macro::test_runtime] async fn test_insertion_error(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:insertError", assert_obj!()).await); - assert!( - s.contains("System tables (prefixed with `_`) are read-only."), - "{s}" - ); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:insertError", assert_obj!()).await); + assert!( + s.contains("System tables (prefixed with `_`) are read-only."), + "{s}" + ); + Ok(()) + }).await } // BigInts cause JSON.stringify() to crash, so they're worth checking for // specifically. Ensure that the error is catchable in JavaScript. #[convex_macro::test_runtime] async fn test_insert_error_with_bigint(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:insertErrorWithBigint", assert_obj!()).await); - assert!( - s.contains("undefined is not a valid Convex value (present at path .bad"), - "{s}" - ); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:insertErrorWithBigint", assert_obj!()).await); + assert!( + s.contains("undefined is not a valid Convex value (present at path .bad"), + "{s}" + ); + Ok(()) + }).await } #[convex_macro::test_runtime] async fn test_patch_error(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:patchError", assert_obj!()).await); - assert!(s.contains("Update on nonexistent document ID"), "{s}"); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:patchError", assert_obj!()).await); + assert!(s.contains("Update on nonexistent document ID"), "{s}"); + Ok(()) + }).await } #[convex_macro::test_runtime] async fn test_patch_value_not_an_object(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:patchValueNotAnObject", assert_obj!()).await); - assert!( - s.contains("Invalid argument `value` for `db.patch`: Value must be an Object"), - "{s}" - ); - Ok(()) + // TODO: Reenable isolate2 when we implement table filter. + UdfTest::run_test_with_isolate(rt, async move |t| { + must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:patchValueNotAnObject", assert_obj!()).await); + assert!( + s.contains("Invalid argument `value` for `db.patch`: Value must be an Object"), + "{s}" + ); + Ok(()) + }).await } #[convex_macro::test_runtime] async fn test_replace_error(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:replaceError", assert_obj!()).await); - assert!(s.contains("Replace on nonexistent document ID"), "{s}"); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:replaceError", assert_obj!()).await); + assert!(s.contains("Replace on nonexistent document ID"), "{s}"); + Ok(()) + }).await } #[convex_macro::test_runtime] async fn test_delete_error(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:deleteError", assert_obj!()).await); - assert!(s.contains("Delete on nonexistent document ID"), "{s}"); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:deleteError", assert_obj!()).await); + assert!(s.contains("Delete on nonexistent document ID"), "{s}"); + Ok(()) + }).await } #[convex_macro::test_runtime] async fn test_nonexistent_table(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - t.create_index("boatVotes.by_boat", "boat").await?; - t.backfill_indexes().await?; - let mut tx = t.database.begin(Identity::system()).await?; - let table_number = TableModel::new(&mut tx).next_user_table_number().await?; - let nonexistent_id = DocumentIdV6::new(table_number, InternalId::MIN); - t.mutation( - "userError:nonexistentTable", - assert_obj!("nonexistentId" => nonexistent_id), - ) - .await?; - Ok(()) + // TODO: Reenable isolate2 when we implement table filter. + UdfTest::run_test_with_isolate(rt, async move |t| { + t.create_index("boatVotes.by_boat", "boat").await?; + t.backfill_indexes().await?; + let mut tx = t.database.begin(Identity::system()).await?; + let table_number = TableModel::new(&mut tx).next_user_table_number().await?; + let nonexistent_id = DocumentIdV6::new(table_number, InternalId::MIN); + t.mutation( + "userError:nonexistentTable", + assert_obj!("nonexistentId" => nonexistent_id), + ) + .await?; + Ok(()) + }) + .await } #[convex_macro::test_runtime] async fn test_nonexistent_id(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - let mut tx: database::Transaction = t.database.begin(Identity::system()).await?; - let table_number = 8000.try_into()?; - let table_name: TableName = "_my_system_table".parse()?; + UdfTest::run_test_with_isolate2(rt, async move |t| { + let mut tx = t.database.begin(Identity::system()).await?; + let table_number = 8000.try_into()?; + let table_name: TableName = "_my_system_table".parse()?; - assert!( - tx.create_system_table_testing(&table_name, Some(table_number)) - .await? - ); - let nonexistent_system_table_id = DocumentIdV6::new(table_number, InternalId::MIN); + assert!( + tx.create_system_table_testing(&table_name, Some(table_number)) + .await? + ); + let nonexistent_system_table_id = DocumentIdV6::new(table_number, InternalId::MIN); - let virtual_table_number = tx - .virtual_table_mapping() - .number(&FILE_STORAGE_VIRTUAL_TABLE)?; - let nonexistent_virtual_table_id = DocumentIdV6::new(virtual_table_number, InternalId::MIN); - let user_document = TestFacingModel::new(&mut tx) - .insert_and_get("table".parse()?, assert_obj!()) - .await?; - let user_table_number = user_document.id().table().table_number; - let nonexistent_user_table_id = DocumentIdV6::new(user_table_number, InternalId::MIN); - t.database.commit(tx).await?; - t.mutation( - "userError:nonexistentId", - assert_obj!("nonexistentSystemId" => nonexistent_system_table_id, "nonexistentUserId" => nonexistent_user_table_id), - ) - .await?; - // Using db.get with an ID on a private system table is like the table doesn't - // exist => returns null. - t.mutation( - "userError:nonexistentSystemIdFails", - assert_obj!("nonexistentSystemId" => nonexistent_system_table_id), - ) - .await?; - // Using db.get with an ID on a virtual table, even if the ID doesn't exist, - // throws error. - let err = t - .mutation_js_error( - "userError:nonexistentSystemIdFails", - assert_obj!("nonexistentSystemId" => nonexistent_virtual_table_id), + let virtual_table_number = tx + .virtual_table_mapping() + .number(&FILE_STORAGE_VIRTUAL_TABLE)?; + let nonexistent_virtual_table_id = DocumentIdV6::new(virtual_table_number, InternalId::MIN); + let user_document = TestFacingModel::new(&mut tx) + .insert_and_get("table".parse()?, assert_obj!()) + .await?; + let user_table_number = user_document.id().table().table_number; + let nonexistent_user_table_id = DocumentIdV6::new(user_table_number, InternalId::MIN); + t.database.commit(tx).await?; + t.mutation( + "userError:nonexistentId", + assert_obj!("nonexistentSystemId" => nonexistent_system_table_id, "nonexistentUserId" => nonexistent_user_table_id), ) .await?; - assert!(err - .message - .contains("System tables can only be accessed with db.system.")); - let err = t - .mutation_js_error( - "userError:nonexistentUserIdFails", - assert_obj!("nonexistentUserId" => nonexistent_user_table_id), + // Using db.get with an ID on a private system table is like the table doesn't + // exist => returns null. + t.mutation( + "userError:nonexistentSystemIdFails", + assert_obj!("nonexistentSystemId" => nonexistent_system_table_id), ) .await?; - assert!(err - .message - .contains("User tables cannot be accessed with db.system.")); - Ok(()) + // Using db.get with an ID on a virtual table, even if the ID doesn't exist, + // throws error. + let err = t + .mutation_js_error( + "userError:nonexistentSystemIdFails", + assert_obj!("nonexistentSystemId" => nonexistent_virtual_table_id), + ) + .await?; + assert!(err + .message + .contains("System tables can only be accessed with db.system.")); + let err = t + .mutation_js_error( + "userError:nonexistentUserIdFails", + assert_obj!("nonexistentUserId" => nonexistent_user_table_id), + ) + .await?; + assert!(err + .message + .contains("User tables cannot be accessed with db.system.")); + Ok(()) + }).await } #[convex_macro::test_runtime] async fn test_private_system_table(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - let mut tx: database::Transaction = t.database.begin(Identity::system()).await?; + // TODO: Reenable isolate2 when we implement table filter. + UdfTest::run_test_with_isolate(rt, async move |t| { + let mut tx = t.database.begin(Identity::system()).await?; - // backend state automatically created by with_model(). - let backend_state = ResolvedQuery::new( - &mut tx, - Query::full_table_scan(BACKEND_STATE_TABLE.clone(), Order::Asc), - )? - .expect_one(&mut tx) - .await?; + // backend state automatically created by with_model(). + let backend_state = ResolvedQuery::new( + &mut tx, + Query::full_table_scan(BACKEND_STATE_TABLE.clone(), Order::Asc), + )? + .expect_one(&mut tx) + .await?; - // But developer UDFs can't query it because it's a private system table. - must_let!(let ConvexValue::Array(results) = t.query( - "userError:privateSystemQuery", - assert_obj!("tableName" => BACKEND_STATE_TABLE.to_string()), - ) - .await?); - assert!(results.is_empty()); - must_let!(let ConvexValue::Null = t.query( - "userError:privateSystemGet", - assert_obj!("id" => backend_state.id().to_string()), - ) - .await?); - Ok(()) + // But developer UDFs can't query it because it's a private system table. + must_let!(let ConvexValue::Array(results) = t.query( + "userError:privateSystemQuery", + assert_obj!("tableName" => BACKEND_STATE_TABLE.to_string()), + ) + .await?); + assert!(results.is_empty()); + must_let!(let ConvexValue::Null = t.query( + "userError:privateSystemGet", + assert_obj!("id" => backend_state.id().to_string()), + ) + .await?); + Ok(()) + }) + .await } #[convex_macro::test_runtime] async fn test_unhandled_promise_rejection(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - // Check that an unhandled promise rejection fails the UDF. - let e = t - .mutation_js_error("userError:unhandledRejection", assert_obj!()) - .await?; - assert!(format!("{e}").contains("Unable to decode ID")); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + // Check that an unhandled promise rejection fails the UDF. + let e = t + .mutation_js_error("userError:unhandledRejection", assert_obj!()) + .await?; + assert!(format!("{e}").contains("Unable to decode ID")); + Ok(()) + }) + .await } #[convex_macro::test_runtime] async fn test_catching_async_exception_thrown_before_await(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:asyncExceptionBeforeAwait", assert_obj!()).await); - assert!(s.contains("This is a custom exception"), "{s}"); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:asyncExceptionBeforeAwait", assert_obj!()).await); + assert!(s.contains("This is a custom exception"), "{s}"); + Ok(()) + }).await } #[convex_macro::test_runtime] async fn test_catching_async_exception_thrown_after_await(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:asyncExceptionAfterAwait", assert_obj!()).await); - assert!(s.contains("This is a custom exception"), "{s}"); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:asyncExceptionAfterAwait", assert_obj!()).await); + assert!(s.contains("This is a custom exception"), "{s}"); + Ok(()) + }).await } #[convex_macro::test_runtime] async fn test_throw_string(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:throwString", assert_obj!()).await); - assert!(s.contains("string - a string"), "{s}"); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + must_let!(let Ok(ConvexValue::String(s)) = t.mutation("userError:throwString", assert_obj!()).await); + assert!(s.contains("string - a string"), "{s}"); + Ok(()) + }).await } #[convex_macro::test_runtime] async fn test_async_syscall_error(rt: TestRuntime) -> anyhow::Result<()> { - let t = UdfTest::default(rt).await?; - let e = t - .mutation_js_error("userError:syscallError", assert_obj!()) - .await?; - assert!( - !e.frames.as_ref().unwrap().0.is_empty(), - "message: {}, frames: {:?}", - e.message, - e.frames - ); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + let e = t + .mutation_js_error("userError:syscallError", assert_obj!()) + .await?; + assert!( + !e.frames.as_ref().unwrap().0.is_empty(), + "message: {}, frames: {:?}", + e.message, + e.frames + ); + Ok(()) + }) + .await } #[convex_macro::test_runtime] async fn test_insert_with_creation_time(rt: TestRuntime) -> anyhow::Result<()> { - let t: UdfTest = - UdfTest::default(rt).await?; - let e = t - .mutation_js_error("adversarial:insertWithCreationTime", assert_obj!()) - .await?; - - assert_contains(&e, "Provided creation time"); - Ok(()) + UdfTest::run_test_with_isolate2(rt, async move |t| { + let e = t + .mutation_js_error("adversarial:insertWithCreationTime", assert_obj!()) + .await?; + assert_contains(&e, "Provided creation time"); + Ok(()) + }) + .await } #[convex_macro::test_runtime] async fn test_insert_with_id(rt: TestRuntime) -> anyhow::Result<()> { - let t: UdfTest = - UdfTest::default(rt).await?; - let e = t - .mutation_js_error("adversarial:insertWithId", assert_obj!()) - .await?; + UdfTest::run_test_with_isolate2(rt, async move |t| { + let e = t + .mutation_js_error("adversarial:insertWithId", assert_obj!()) + .await?; - assert_contains(&e, "Provided document ID"); - Ok(()) + assert_contains(&e, "Provided document ID"); + Ok(()) + }) + .await }