Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/cli/src/subcommands/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ pub fn cli() -> clap::Command {
.conflicts_with("build_options")
.help("The system path (absolute or relative) to the compiled wasm binary we should publish, instead of building the project."),
)
// TODO(v8): needs better UX but good enough for a demo...
.arg(
Arg::new("javascript")
.long("javascript")
.action(SetTrue)
.requires("wasm_file")
.hide(true)
.help("UNSTABLE: interpret `--bin-path` as a JS module"),
)
.arg(
Arg::new("num_replicas")
.value_parser(clap::value_parser!(u8))
Expand Down Expand Up @@ -84,6 +93,8 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
let force = args.get_flag("force");
let anon_identity = args.get_flag("anon_identity");
let wasm_file = args.get_one::<PathBuf>("wasm_file");
// TODO(v8): needs better UX but good enough for a demo...
let wasm_file_is_really_js = args.get_flag("javascript");
let database_host = config.get_host_url(server)?;
let build_options = args.get_one::<String>("build_options").unwrap();
let num_replicas = args.get_one::<u8>("num_replicas");
Expand Down Expand Up @@ -123,6 +134,11 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
};
let program_bytes = fs::read(path_to_wasm)?;

// TODO(v8): needs better UX but good enough for a demo...
if wasm_file_is_really_js {
builder = builder.query(&[("host_type", "Js")]);
}

let server_address = {
let url = Url::parse(&database_host)?;
url.host_str().unwrap_or("<default>").to_string()
Expand Down
3 changes: 3 additions & 0 deletions crates/client-api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,6 @@ toml.workspace = true

[lints]
workspace = true

[features]
unstable = []
17 changes: 16 additions & 1 deletion crates/client-api/src/routes/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,8 @@ pub struct PublishDatabaseQueryParams {
#[serde(default)]
clear: bool,
num_replicas: Option<usize>,
#[serde(default)]
host_type: HostType,
/// [`Hash`] of [`MigrationToken`]` to be checked if `MigrationPolicy::BreakClients` is set.
///
/// Users obtain such a hash via the `/database/:name_or_identity/pre-publish POST` route.
Expand Down Expand Up @@ -532,12 +534,25 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
Query(PublishDatabaseQueryParams {
clear,
num_replicas,
host_type,
token,
policy,
}): Query<PublishDatabaseQueryParams>,
Extension(auth): Extension<SpacetimeAuth>,
body: Bytes,
) -> axum::response::Result<axum::Json<PublishResult>> {
// Feature gate V8 modules.
// The host must've been compiled with the `unstable` feature.
// TODO(v8): ungate this when V8 is ready to ship.
#[cfg(not(feature = "unstable"))]
if host_type == HostType::Js {
return Err((
StatusCode::BAD_REQUEST,
"JS host type requires a host with unstable features",
)
.into());
}

// You should not be able to publish to a database that you do not own
// so, unless you are the owner, this will fail.

Expand Down Expand Up @@ -638,7 +653,7 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate>(
database_identity,
program_bytes: body.into(),
num_replicas,
host_type: HostType::Wasm,
host_type,
},
policy,
)
Expand Down
51 changes: 50 additions & 1 deletion crates/core/src/host/instance_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ use super::scheduler::{get_schedule_from_row, ScheduleError, Scheduler};
use crate::database_logger::{BacktraceProvider, LogLevel, Record};
use crate::db::relational_db::{MutTx, RelationalDB};
use crate::error::{DBError, DatastoreError, IndexError, NodesError};
use crate::host::wasm_common::TimingSpan;
use crate::replica_context::ReplicaContext;
use core::mem;
use parking_lot::{Mutex, MutexGuard};
use smallvec::SmallVec;
use spacetimedb_datastore::locking_tx_datastore::MutTxId;
use spacetimedb_lib::Timestamp;
use spacetimedb_lib::{Identity, Timestamp};
use spacetimedb_primitives::{ColId, ColList, IndexId, TableId};
use spacetimedb_sats::{
bsatn::{self, ToBsatn},
Expand All @@ -18,6 +19,7 @@ use spacetimedb_table::indexes::RowPointer;
use spacetimedb_table::table::RowRef;
use std::ops::DerefMut;
use std::sync::Arc;
use std::vec::IntoIter;

#[derive(Clone)]
pub struct InstanceEnv {
Expand Down Expand Up @@ -170,6 +172,11 @@ impl InstanceEnv {
}
}

/// Returns the database's identity.
pub fn database_identity(&self) -> &Identity {
&self.replica_ctx.database.database_identity
}

/// Signal to this `InstanceEnv` that a reducer call is beginning.
pub fn start_reducer(&mut self, ts: Timestamp) {
self.start_time = ts;
Expand All @@ -189,6 +196,21 @@ impl InstanceEnv {
);
}

/// End a console timer by logging the span at INFO level.
pub(crate) fn console_timer_end(&self, span: &TimingSpan, bt: &dyn BacktraceProvider) {
let elapsed = span.start.elapsed();
let message = format!("Timing span {:?}: {:?}", &span.name, elapsed);

let record = Record {
ts: chrono::Utc::now(),
target: None,
filename: None,
line_number: None,
message: &message,
};
self.console_log(LogLevel::Info, &record, bt);
}

/// Project `cols` in `row_ref` encoded in BSATN to `buffer`
/// and return the full length of the BSATN.
///
Expand Down Expand Up @@ -469,6 +491,33 @@ impl InstanceEnv {

Ok(chunks)
}

pub fn fill_buffer_from_iter(
iter: &mut IntoIter<Vec<u8>>,
mut buffer: &mut [u8],
chunk_pool: &mut ChunkPool,
) -> usize {
let mut written = 0;
// Fill the buffer as much as possible.
while let Some(chunk) = iter.as_slice().first() {
let Some((buf_chunk, rest)) = buffer.split_at_mut_checked(chunk.len()) else {
// Cannot fit chunk into the buffer,
// either because we already filled it too much,
// or because it is too small.
break;
};
buf_chunk.copy_from_slice(chunk);
written += chunk.len();
buffer = rest;

// Advance the iterator, as we used a chunk.
// SAFETY: We peeked one `chunk`, so there must be one at least.
let chunk = unsafe { iter.next().unwrap_unchecked() };
chunk_pool.put(chunk);
}

written
}
}

impl TxSlot {
Expand Down
20 changes: 20 additions & 0 deletions crates/core/src/host/module_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use crate::{
energy::EnergyMonitor,
host::{
module_host::{DynModule, ModuleInfo},
wasm_common::{module_host_actor::DescribeError, DESCRIBE_MODULE_DUNDER},
Scheduler,
},
module_host_context::ModuleCreationContext,
Expand Down Expand Up @@ -88,3 +89,22 @@ impl DynModule for ModuleCommon {
&self.scheduler
}
}

/// Runs the describer of modules in `run` and does some logging around it.
pub(crate) fn run_describer<T>(
log_traceback: impl FnOnce(&str, &str, &anyhow::Error),
run: impl FnOnce() -> anyhow::Result<T>,
) -> Result<T, DescribeError> {
let describer_func_name = DESCRIBE_MODULE_DUNDER;
let start = std::time::Instant::now();
log::trace!("Start describer \"{describer_func_name}\"...");

let result = run();

let duration = start.elapsed();
log::trace!("Describer \"{}\" ran: {} us", describer_func_name, duration.as_micros());

result
.inspect_err(|err| log_traceback("describer", describer_func_name, err))
.map_err(DescribeError::RuntimeError)
}
2 changes: 1 addition & 1 deletion crates/core/src/host/v8/de.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ impl de::Error for Error<'_> {
}

/// Returns a scratch buffer to fill when deserializing strings.
fn scratch_buf<const N: usize>() -> [MaybeUninit<u8>; N] {
pub(crate) fn scratch_buf<const N: usize>() -> [MaybeUninit<u8>; N] {
[const { MaybeUninit::uninit() }; N]
}

Expand Down
20 changes: 16 additions & 4 deletions crates/core/src/host/v8/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ impl<'scope, M: IntoJsString> IntoException<'scope> for RangeError<M> {
}

#[derive(Debug)]
pub(super) struct ExceptionThrown {
pub(crate) struct ExceptionThrown {
_priv: (),
}

/// A result where the error indicates that an exception has already been thrown in V8.
pub(super) type ExcResult<T> = Result<T, ExceptionThrown>;
pub(crate) type ExcResult<T> = Result<T, ExceptionThrown>;

/// Indicates that the JS side had thrown an exception.
pub(super) fn exception_already_thrown() -> ExceptionThrown {
Expand Down Expand Up @@ -126,7 +126,9 @@ pub(super) struct JsError {
impl fmt::Display for JsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "js error {}", self.msg)?;
writeln!(f, "{}", self.trace)?;
if !f.alternate() {
writeln!(f, "{}", self.trace)?;
}
Ok(())
}
}
Expand Down Expand Up @@ -204,7 +206,7 @@ impl fmt::Display for JsStackTraceFrame {

// This isn't exactly the same format as chrome uses,
// but it's close enough for now.
// TODO(centril): make it more like chrome in the future.
// TODO(v8): make it more like chrome in the future.
f.write_fmt(format_args!(
"at {} ({}:{}:{})",
fn_name, script_name, &self.line, &self.column
Expand Down Expand Up @@ -249,6 +251,16 @@ impl JsError {
}
}

pub(super) fn log_traceback(func_type: &str, func: &str, e: &anyhow::Error) {
log::info!("{func_type} \"{func}\" runtime error: {e:#}");
if let Some(js_err) = e.downcast_ref::<JsError>() {
log::info!("js error {}", js_err.msg);
for (index, frame) in js_err.trace.frames.iter().enumerate() {
log::info!(" Frame #{index}: {frame}");
}
}
}

/// Run `body` within a try-catch context and capture any JS exception thrown as a [`JsError`].
pub(super) fn catch_exception<'scope, T>(
scope: &mut HandleScope<'scope>,
Expand Down
Loading
Loading