SERVER-116055 Add support for $accumulator in MozJS WASM engine. (#53341)
GitOrigin-RevId: 575a1be1e9bf6e8510aab16cdd924428473e7ca2
This commit is contained in:
parent
0289f65aed
commit
da7af1bad1
@ -391,6 +391,12 @@ crate.annotation(
|
||||
crate = "wasmtime-c-api-impl",
|
||||
extra_aliased_targets = {"wasmtime_c": "wasmtime_c"},
|
||||
gen_build_script = "off",
|
||||
patch_args = ["-p1"],
|
||||
patches = [
|
||||
"//bazel/wasmtime:store_rs.patch",
|
||||
"//bazel/wasmtime:store_h.patch",
|
||||
"//bazel/wasmtime:store_hh.patch",
|
||||
],
|
||||
)
|
||||
|
||||
# Disable default features and re-enable everything except "objdump/explore" so we don't pull in capstone/capstone-sys.
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"checksum": "09e089e4d857eb519291450cae1e356a0e8486203c17893a9ded5d57f1857484",
|
||||
"checksum": "c5167d945fea0a88114d893d2437bad55ac3b4e295eaf0ed5348d3ba97edf335",
|
||||
"crates": {
|
||||
"addr2line 0.26.1": {
|
||||
"name": "addr2line",
|
||||
@ -17534,7 +17534,15 @@
|
||||
"repository": {
|
||||
"Http": {
|
||||
"url": "https://static.crates.io/crates/wasmtime-c-api-impl/44.0.1/download",
|
||||
"sha256": "af63f5db854133f67cdec5faabff9db2764221968231c42fc18e8a9d63f849d5"
|
||||
"sha256": "af63f5db854133f67cdec5faabff9db2764221968231c42fc18e8a9d63f849d5",
|
||||
"patch_args": [
|
||||
"-p1"
|
||||
],
|
||||
"patches": [
|
||||
"@@//bazel/wasmtime:store_h.patch",
|
||||
"@@//bazel/wasmtime:store_hh.patch",
|
||||
"@@//bazel/wasmtime:store_rs.patch"
|
||||
]
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
|
||||
5
bazel/wasmtime/BUILD.bazel
Normal file
5
bazel/wasmtime/BUILD.bazel
Normal file
@ -0,0 +1,5 @@
|
||||
exports_files([
|
||||
"store_rs.patch",
|
||||
"store_h.patch",
|
||||
"store_hh.patch",
|
||||
])
|
||||
19
bazel/wasmtime/store_h.patch
Normal file
19
bazel/wasmtime/store_h.patch
Normal file
@ -0,0 +1,19 @@
|
||||
--- a/include/wasmtime/store.h
|
||||
+++ b/include/wasmtime/store.h
|
||||
@@ -164,6 +164,16 @@
|
||||
WASM_API_EXTERN wasmtime_error_t *
|
||||
wasmtime_context_set_fuel(wasmtime_context_t *store, uint64_t fuel);
|
||||
|
||||
+/**
|
||||
+ * \brief Set the per-Store hostcall fuel cap (component model).
|
||||
+ *
|
||||
+ * Bounds how much data wasmtime will copy between host and guest across
|
||||
+ * component-model calls. Pass `SIZE_MAX` to disable the cap. Mirrors
|
||||
+ * `Store::set_hostcall_fuel` on the Rust side.
|
||||
+ */
|
||||
+WASM_API_EXTERN void
|
||||
+wasmtime_context_set_hostcall_fuel(wasmtime_context_t *store, size_t fuel);
|
||||
+
|
||||
/**
|
||||
* \brief Returns the amount of fuel remaining in this context's store.
|
||||
*
|
||||
18
bazel/wasmtime/store_hh.patch
Normal file
18
bazel/wasmtime/store_hh.patch
Normal file
@ -0,0 +1,18 @@
|
||||
--- a/include/wasmtime/store.hh
|
||||
+++ b/include/wasmtime/store.hh
|
||||
@@ -118,6 +118,15 @@
|
||||
return std::monostate();
|
||||
}
|
||||
|
||||
+ /// Sets the per-Store hostcall fuel cap (component model).
|
||||
+ ///
|
||||
+ /// Bounds how much data wasmtime will copy between host and guest across
|
||||
+ /// component-model calls. Pass `SIZE_MAX` to disable the cap.
|
||||
+ /// Mirrors `Store::set_hostcall_fuel` on the Rust side.
|
||||
+ void set_hostcall_fuel(size_t fuel) {
|
||||
+ wasmtime_context_set_hostcall_fuel(ptr, fuel);
|
||||
+ }
|
||||
+
|
||||
/// Returns the amount of fuel consumed so far by executing WebAssembly.
|
||||
///
|
||||
/// Returns `std::nullopt` if fuel consumption is not enabled.
|
||||
20
bazel/wasmtime/store_rs.patch
Normal file
20
bazel/wasmtime/store_rs.patch
Normal file
@ -0,0 +1,20 @@
|
||||
--- a/src/store.rs
|
||||
+++ b/src/store.rs
|
||||
@@ -263,6 +263,17 @@
|
||||
crate::handle_result(store.set_fuel(fuel), |()| {})
|
||||
}
|
||||
|
||||
+/// Sets the per-Store hostcall fuel cap that bounds how much data wasmtime
|
||||
+/// will copy between the host and the guest across component-model calls.
|
||||
+/// Pass `usize::MAX` for "unlimited".
|
||||
+#[unsafe(no_mangle)]
|
||||
+pub extern "C" fn wasmtime_context_set_hostcall_fuel(
|
||||
+ mut store: WasmtimeStoreContextMut<'_>,
|
||||
+ fuel: usize,
|
||||
+) {
|
||||
+ store.set_hostcall_fuel(fuel);
|
||||
+}
|
||||
+
|
||||
#[unsafe(no_mangle)]
|
||||
pub extern "C" fn wasmtime_context_get_fuel(
|
||||
store: WasmtimeStoreContext<'_>,
|
||||
@ -778,9 +778,9 @@ flags in common: {common_set}
|
||||
|
||||
_config.MONGOD_EXECUTABLE = _expand_user(config.pop("mongod_executable"))
|
||||
|
||||
# TODO SERVER-116054, SERVER-116052, SERVER-116055, SERVER-116053: Remove this js_engine handling
|
||||
# section and_detect_js_engine once mozjs-wasm supports $where, $function, $accumulator, and
|
||||
# mapReduce, eliminating the need for this startup-time binary invocation.
|
||||
# TODO SERVER-116054, SERVER-116052, SERVER-116053: Remove this js_engine handling
|
||||
# section and _detect_js_engine once mozjs-wasm supports $where, $function, and mapReduce,
|
||||
# eliminating the need for this startup-time binary invocation.
|
||||
_config.JS_ENGINE = _detect_js_engine(_config.MONGOD_EXECUTABLE)
|
||||
if _config.JS_ENGINE == "mozjs-wasm":
|
||||
_config.EXCLUDE_WITH_ANY_TAGS.append("mozjs_wasm_unsupported")
|
||||
@ -1198,8 +1198,6 @@ _MOZJS_PATTERNS = (
|
||||
"mapreduce",
|
||||
# TODO SERVER-116054: Add support for $where.
|
||||
'"$where"',
|
||||
# TODO SERVER-116055: Add support for $accumulator.
|
||||
'"$accumulator"',
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -376,3 +376,149 @@ command.pipeline = [
|
||||
res = assert.commandWorked(db.runCommand(command));
|
||||
expectedResults = [{_id: 1, value: {len: 3, types: ["object", "object", "object"], values: [null, null, null]}}];
|
||||
assert(resultsEq(res.cursor.firstBatch, expectedResults), res.cursor);
|
||||
|
||||
// Test that throwing inside accumulate causes the command to fail.
|
||||
assert(db.accumulator_js.drop());
|
||||
assert.commandWorked(db.accumulator_js.insert({val: 1}));
|
||||
assert.commandFailedWithCode(
|
||||
db.runCommand({
|
||||
aggregate: "accumulator_js",
|
||||
cursor: {},
|
||||
pipeline: [
|
||||
{
|
||||
$group: {
|
||||
_id: 1,
|
||||
value: {
|
||||
$accumulator: {
|
||||
init: function () {
|
||||
return 0;
|
||||
},
|
||||
accumulateArgs: ["$val"],
|
||||
accumulate: function (state, val) {
|
||||
throw new Error("accumulate error");
|
||||
},
|
||||
merge: function (s1, s2) {
|
||||
return s1 + s2;
|
||||
},
|
||||
lang: "js",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
ErrorCodes.JSInterpreterFailure,
|
||||
);
|
||||
|
||||
// Test that init returning null is handled: accumulate receives null as initial state.
|
||||
assert(db.accumulator_js.drop());
|
||||
assert.commandWorked(db.accumulator_js.insert([{val: 10}, {val: 32}]));
|
||||
res = assert.commandWorked(
|
||||
db.runCommand({
|
||||
aggregate: "accumulator_js",
|
||||
cursor: {},
|
||||
pipeline: [
|
||||
{
|
||||
$group: {
|
||||
_id: 1,
|
||||
value: {
|
||||
$accumulator: {
|
||||
init: function () {
|
||||
return null;
|
||||
},
|
||||
accumulateArgs: ["$val"],
|
||||
accumulate: function (state, val) {
|
||||
return (state || 0) + val;
|
||||
},
|
||||
merge: function (s1, s2) {
|
||||
return (s1 || 0) + (s2 || 0);
|
||||
},
|
||||
lang: "js",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
assert(resultsEq(res.cursor.firstBatch, [{_id: 1, value: 42}]), res.cursor);
|
||||
|
||||
// Test that string state works: accumulate builds a comma-separated string.
|
||||
assert(db.accumulator_js.drop());
|
||||
assert.commandWorked(db.accumulator_js.insert([{word: "hello"}, {word: "world"}]));
|
||||
res = assert.commandWorked(
|
||||
db.runCommand({
|
||||
aggregate: "accumulator_js",
|
||||
cursor: {},
|
||||
pipeline: [
|
||||
{
|
||||
$sort: {word: 1},
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: 1,
|
||||
value: {
|
||||
$accumulator: {
|
||||
init: function () {
|
||||
return "";
|
||||
},
|
||||
accumulateArgs: ["$word"],
|
||||
accumulate: function (state, val) {
|
||||
return state ? state + "," + val : val;
|
||||
},
|
||||
merge: function (s1, s2) {
|
||||
return s1 ? s1 + "," + s2 : s2;
|
||||
},
|
||||
finalize: function (state) {
|
||||
return state;
|
||||
},
|
||||
lang: "js",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
assert.eq(res.cursor.firstBatch.length, 1);
|
||||
// The two words appear in the result separated by a comma (order may vary across shards).
|
||||
const words = res.cursor.firstBatch[0].value.split(",").sort();
|
||||
assert.sameMembers(words, ["hello", "world"]);
|
||||
|
||||
// Test that when no documents match a group, finalize is called on the init state.
|
||||
assert(db.accumulator_js.drop());
|
||||
assert.commandWorked(db.accumulator_js.insert([{x: 1}, {x: 1}]));
|
||||
res = assert.commandWorked(
|
||||
db.runCommand({
|
||||
aggregate: "accumulator_js",
|
||||
cursor: {},
|
||||
pipeline: [
|
||||
{$match: {x: 999}},
|
||||
{
|
||||
$group: {
|
||||
_id: 1,
|
||||
value: {
|
||||
$accumulator: {
|
||||
init: function () {
|
||||
return {count: 0, sum: 0};
|
||||
},
|
||||
accumulateArgs: ["$x"],
|
||||
accumulate: function (state, val) {
|
||||
return {count: state.count + 1, sum: state.sum + val};
|
||||
},
|
||||
merge: function (s1, s2) {
|
||||
return {count: s1.count + s2.count, sum: s1.sum + s2.sum};
|
||||
},
|
||||
finalize: function (state) {
|
||||
return state.count > 0 ? state.sum / state.count : 0;
|
||||
},
|
||||
lang: "js",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
// No documents matched, so the group produces no output.
|
||||
assert.eq(res.cursor.firstBatch.length, 0, res.cursor);
|
||||
|
||||
@ -2,8 +2,6 @@
|
||||
// @tags: [
|
||||
// requires_scripting,
|
||||
// resource_intensive,
|
||||
// # TODO SERVER-116055: Add support for $accumulate.
|
||||
// mozjs_wasm_unsupported,
|
||||
// ]
|
||||
const coll = db.accumulator_js_size_limits;
|
||||
|
||||
@ -42,7 +40,9 @@ let res = runExample(1, {
|
||||
},
|
||||
lang: "js",
|
||||
});
|
||||
assert.commandFailedWithCode(res, [ErrorCodes.BSONObjectTooLarge, 10334]);
|
||||
// WASM path: objectwrapper.cpp throws 17260 ("Object size exceeds limit").
|
||||
// Legacy MozJS path: may throw 10334 (BSONObjectTooLarge) from BSONObjBuilder.
|
||||
assert.commandFailedWithCode(res, [ErrorCodes.BSONObjectTooLarge, 17260, 10334]);
|
||||
|
||||
// Accumulator tries to return BSON larger than 16MB from JS.
|
||||
assert(coll.drop());
|
||||
@ -88,6 +88,8 @@ res = runExample(1, {
|
||||
},
|
||||
lang: "js",
|
||||
});
|
||||
// 4545000 is thrown by accumulator_js_reduce.cpp on the server before JS runs,
|
||||
// so it surfaces identically on legacy and WASM builds.
|
||||
assert.commandFailedWithCode(res, [4545000]);
|
||||
|
||||
// $group size limit exceeded, and cannot spill.
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
// exclude_from_timeseries_crud_passthrough,
|
||||
// # TODO SERVER-116052: Add support for $function.
|
||||
// # TODO SERVER-116054: Add support for $where.
|
||||
// # TODO SERVER-116055: Add support for $accumulate.
|
||||
// mozjs_wasm_unsupported,
|
||||
// ]
|
||||
|
||||
|
||||
@ -1190,6 +1190,10 @@ mongo_cc_unit_test(
|
||||
],
|
||||
"//bazel/config:js_engine_none": [
|
||||
],
|
||||
"//bazel/config:js_engine_wasm": [
|
||||
"accumulator_js_test.cpp",
|
||||
"expression_javascript_test.cpp",
|
||||
],
|
||||
}],
|
||||
tags = [
|
||||
"code_coverage_quarantine",
|
||||
|
||||
@ -124,14 +124,12 @@ void exitWithError(const int statusCode, const std::string& msg) {
|
||||
// Operators checked (will be removed from this check in the future when mozjs-wasm supports them):
|
||||
// TODO SERVER-116054: Add support for $where.
|
||||
// TODO SERVER-116052: Add support for $function.
|
||||
// TODO SERVER-116055: Add support for $accumulator.
|
||||
// TODO SERVER-116053: Add support for mapReduce.
|
||||
bool containsUnsupportedJSWasmOperators(const BSONObj& obj) {
|
||||
for (const auto& elem : obj) {
|
||||
const auto fieldName = elem.fieldNameStringData();
|
||||
if (fieldName == "$where"_sd || fieldName == "$function"_sd ||
|
||||
fieldName == "$accumulator"_sd || fieldName == "mapReduce"_sd ||
|
||||
fieldName == "mapreduce"_sd) {
|
||||
fieldName == "mapReduce"_sd || fieldName == "mapreduce"_sd) {
|
||||
return true;
|
||||
}
|
||||
if (elem.type() == BSONType::object || elem.type() == BSONType::array) {
|
||||
@ -151,7 +149,7 @@ bool shouldSkipFile(const QueryFile& currFile, DBClientConnection* conn) {
|
||||
|
||||
// If the server is running mozjs-wasm, we need to check if any queries contain MozJS
|
||||
// operators, and if so, skip the file since those queries won't run successfully.
|
||||
// TODO SERVER-116054, SERVER-116052, SERVER-116055, SERVER-116053: Remove this check once
|
||||
// TODO SERVER-116054, SERVER-116052, SERVER-116053: Remove this check once
|
||||
// mozjs-wasm supports all MozJS operators used in the test files.
|
||||
static constexpr auto kMozJsWasmEngine = "mozjs-wasm"_sd;
|
||||
auto bob = BSONObjBuilder{};
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
#include "mongo/bson/util/builder.h"
|
||||
#include "mongo/platform/decimal128.h"
|
||||
#include "mongo/scripting/js_regex.h"
|
||||
#include "mongo/scripting/mozjs/common/exception.h"
|
||||
#include "mongo/scripting/mozjs/common/idwrapper.h"
|
||||
#include "mongo/scripting/mozjs/common/runtime.h"
|
||||
#include "mongo/scripting/mozjs/common/types/bson.h"
|
||||
@ -673,8 +674,6 @@ BSONObj ObjectWrapper::toBSON() {
|
||||
if (frames.size() == 1) {
|
||||
IdWrapper idw(_context, id);
|
||||
|
||||
// TODO: check if it's cheaper to just compare with an interned
|
||||
// string of "_id" rather than with ascii
|
||||
if (idw.isString() && idw.equalsAscii("_id")) {
|
||||
continue;
|
||||
}
|
||||
@ -687,11 +686,12 @@ BSONObj ObjectWrapper::toBSON() {
|
||||
}
|
||||
|
||||
const int sizeWithEOO = b.len() + 1 /*EOO*/ - 4 /*BSONObj::Holder ref count*/;
|
||||
uassert(17260,
|
||||
str::stream() << "Converting from JavaScript to BSON failed: "
|
||||
<< "Object size " << sizeWithEOO << " exceeds limit of "
|
||||
<< BSONObjMaxInternalSize << " bytes.",
|
||||
sizeWithEOO <= BSONObjMaxInternalSize);
|
||||
if (sizeWithEOO > BSONObjMaxInternalSize) {
|
||||
std::string msg = str::stream() << "Converting from JavaScript to BSON failed: "
|
||||
<< "Object size " << sizeWithEOO << " exceeds limit of "
|
||||
<< BSONObjMaxInternalSize << " bytes.";
|
||||
uasserted(17260, msg);
|
||||
}
|
||||
|
||||
return b.obj();
|
||||
}
|
||||
|
||||
@ -231,6 +231,7 @@ mongo_cc_unit_test(
|
||||
deps = [
|
||||
":wasmtime_engine",
|
||||
"//src/mongo:base",
|
||||
"//src/mongo/db/query:query_knobs",
|
||||
"//src/mongo/scripting:scripting_common",
|
||||
],
|
||||
)
|
||||
@ -245,9 +246,9 @@ mongo_cc_library(
|
||||
"wasmtime_engine.h",
|
||||
"//src/mongo/scripting/mozjs/wasm/scope:scope.h",
|
||||
],
|
||||
private_hdrs = ["embedded_wasm_resource.h"],
|
||||
deps = [
|
||||
":bridge",
|
||||
":embed_mozjs_wasm_obj",
|
||||
"//src/mongo:base",
|
||||
"//src/mongo/db:server_options",
|
||||
"//src/mongo/db:service_context",
|
||||
@ -255,5 +256,8 @@ mongo_cc_library(
|
||||
"//src/mongo/scripting:scripting_common",
|
||||
"//src/mongo/util/concurrency:spin_lock",
|
||||
"@crates//:wasmtime_c",
|
||||
],
|
||||
] + select({
|
||||
"@platforms//os:windows": [":embed_mozjs_wasm_rc"],
|
||||
"//conditions:default": [":embed_mozjs_wasm_obj"],
|
||||
}),
|
||||
)
|
||||
|
||||
@ -117,6 +117,12 @@ MozJSWasmBridge::MozJSWasmBridge(std::shared_ptr<WasmEngineContext> ctx, Options
|
||||
// This is used to signal process killing.
|
||||
storeCtx.set_epoch_deadline(1);
|
||||
|
||||
// The default 128 MiB hostcall fuel cap is exhausted by long-running $accumulator /
|
||||
// mapReduce pipelines that pass multi-megabyte BSON state across WIT calls.
|
||||
// Disable it here; resource use is already bounded by the linear-memory limiter
|
||||
// (opts.linearMemoryLimitMB), internalQueryMaxJsEmitBytes, and BSON 16 MiB per object.
|
||||
storeCtx.set_hostcall_fuel(SIZE_MAX);
|
||||
|
||||
wt::WasiConfig wasiConfig;
|
||||
wasiConfig.inherit_stdout();
|
||||
wasiConfig.inherit_stderr();
|
||||
|
||||
@ -397,14 +397,6 @@ err_code_t MozJSScriptEngine::invokeFunction(uint64_t handle,
|
||||
return err ? err->code : SM_E_RUNTIME;
|
||||
}
|
||||
|
||||
if (_emitByteLimit > 0 && _emitBytesUsed > _emitByteLimit) {
|
||||
if (err) {
|
||||
err->code = SM_E_RUNTIME;
|
||||
set_string(&err->msg, &err->msg_len, "emit() exceeded memory limit");
|
||||
}
|
||||
return SM_E_RUNTIME;
|
||||
}
|
||||
|
||||
// Store return value on global (same key as implscope) so getReturnValueBson can read it.
|
||||
ObjectWrapper(_cx, _global).setValue(kInvokeResult, out);
|
||||
|
||||
@ -693,9 +685,15 @@ err_code_t MozJSScriptEngine::setGlobalValue(const char* name,
|
||||
BSONObj MozJSScriptEngine::_emitCallback(const BSONObj& args, void* data) {
|
||||
auto* engine = static_cast<MozJSScriptEngine*>(data);
|
||||
|
||||
int nArgs = args.nFields();
|
||||
if (nArgs != 2) {
|
||||
constexpr int kEmitArgCountCode = 31220;
|
||||
uasserted(ErrorCodes::Error(kEmitArgCountCode), "emit takes 2 args");
|
||||
}
|
||||
|
||||
BSONObjIterator it(args);
|
||||
BSONElement keyElem = it.more() ? it.next() : BSONElement();
|
||||
BSONElement valElem = it.more() ? it.next() : BSONElement();
|
||||
BSONElement keyElem = it.next();
|
||||
BSONElement valElem = it.next();
|
||||
|
||||
BSONObjBuilder b;
|
||||
if (keyElem.type() == BSONType::undefined || keyElem.eoo())
|
||||
|
||||
@ -31,6 +31,8 @@
|
||||
|
||||
#include "mongo/scripting/mozjs/common/exception.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "js/ErrorReport.h"
|
||||
#include "js/Exception.h"
|
||||
|
||||
|
||||
@ -98,6 +98,7 @@ void WasmtimeImplScope::init(const BSONObj* data) {
|
||||
opts.jsHeapLimitMB = static_cast<uint32_t>(*_jsHeapLimitMB);
|
||||
}
|
||||
opts.linearMemoryLimitMB = gWasmtimeStoreMemoryLimitMB.load();
|
||||
_storeLinearMemBytes = static_cast<int64_t>(opts.linearMemoryLimitMB) * 1024 * 1024;
|
||||
_bridge = std::make_unique<wasm::MozJSWasmBridge>(_wasmEngineCtx, opts);
|
||||
bool initialized = _bridge->initialize();
|
||||
uassert(ErrorCodes::BadValue, "MozJS WASM bridge failed to initialize", initialized);
|
||||
@ -188,7 +189,13 @@ void WasmtimeImplScope::injectNative(const char* field, NativeFunction func, voi
|
||||
_emitCallbackData = data;
|
||||
// Margin lets WASM buffer one over-limit doc so the host's EmitState sees
|
||||
// it during drain and can throw, instead of WASM silently dropping it.
|
||||
_bridge->setupEmit(internalQueryMaxJsEmitBytes.load() + BSONObjMaxInternalSize);
|
||||
const int64_t emitBufBytes =
|
||||
static_cast<int64_t>(internalQueryMaxJsEmitBytes.load()) + BSONObjMaxInternalSize;
|
||||
uassert(ErrorCodes::BadValue,
|
||||
"internalQueryMaxJsEmitBytes exceeds wasmtimeStoreMemoryLimitMB: the emit buffer "
|
||||
"must fit within the WASM store's linear memory",
|
||||
emitBufBytes <= _storeLinearMemBytes);
|
||||
_bridge->setupEmit(emitBufBytes);
|
||||
}
|
||||
|
||||
BSONObj WasmtimeImplScope::_resolveGlobal(const char* field) const {
|
||||
|
||||
@ -120,6 +120,7 @@ private:
|
||||
const boost::optional<int> _jsHeapLimitMB;
|
||||
|
||||
std::unique_ptr<wasm::MozJSWasmBridge> _bridge;
|
||||
int64_t _storeLinearMemBytes = 0;
|
||||
DeadlineMonitor<WasmtimeImplScope> _deadlineMonitor;
|
||||
void _drainEmitToCallback();
|
||||
void _installHelpers();
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
|
||||
#include "mongo/bson/bsontypes.h"
|
||||
#include "mongo/bson/bsontypes_util.h"
|
||||
#include "mongo/db/query/query_execution_knobs_gen.h"
|
||||
#include "mongo/scripting/config_engine_gen.h"
|
||||
#include "mongo/scripting/js_regex.h"
|
||||
#include "mongo/scripting/mozjs/wasm/wasmtime_engine.h"
|
||||
@ -111,46 +112,6 @@ TEST(WasmtimeScope, FunctionPattern_WithArgs) {
|
||||
ASSERT_EQ(retVal.getIntField("sum"), 42);
|
||||
}
|
||||
|
||||
// $accumulator init/accumulate/merge pattern
|
||||
TEST(WasmtimeScope, AccumulatorPattern_InitAccumulateMerge) {
|
||||
WasmtimeScriptEngine engine;
|
||||
std::unique_ptr<Scope> scope(engine.createScopeForCurrentThread(boost::none));
|
||||
ASSERT(scope);
|
||||
|
||||
ScriptingFunction initFn = scope->createFunction("function() { return 0; }");
|
||||
ASSERT(initFn != 0);
|
||||
|
||||
ScriptingFunction accFn = scope->createFunction("function(state, val) { return state + val; }");
|
||||
ASSERT(accFn != 0);
|
||||
|
||||
ScriptingFunction mergeFn = scope->createFunction("function(s1, s2) { return s1 + s2; }");
|
||||
ASSERT(mergeFn != 0);
|
||||
|
||||
// init
|
||||
BSONObj emptyArgs;
|
||||
ASSERT_EQ(0, scope->invoke(initFn, &emptyArgs, nullptr, 0));
|
||||
double state = scope->getNumber("__returnValue");
|
||||
ASSERT_EQ(state, 0.0);
|
||||
|
||||
// accumulate: state + 10
|
||||
BSONObj accArgs1 = BSON("0" << state << "1" << 10);
|
||||
ASSERT_EQ(0, scope->invoke(accFn, &accArgs1, nullptr, 0));
|
||||
state = scope->getNumber("__returnValue");
|
||||
ASSERT_EQ(state, 10.0);
|
||||
|
||||
// accumulate: state + 20
|
||||
BSONObj accArgs2 = BSON("0" << state << "1" << 20);
|
||||
ASSERT_EQ(0, scope->invoke(accFn, &accArgs2, nullptr, 0));
|
||||
state = scope->getNumber("__returnValue");
|
||||
ASSERT_EQ(state, 30.0);
|
||||
|
||||
// merge: 30 + 12
|
||||
BSONObj mergeArgs = BSON("0" << state << "1" << 12.0);
|
||||
ASSERT_EQ(0, scope->invoke(mergeFn, &mergeArgs, nullptr, 0));
|
||||
double merged = scope->getNumber("__returnValue");
|
||||
ASSERT_EQ(merged, 42.0);
|
||||
}
|
||||
|
||||
// mapReduce.map pattern: injectNative("emit", ...) + invoke(func, nullptr, &doc, timeout, true)
|
||||
TEST(WasmtimeScope, MapReducePattern_EmitAndDrain) {
|
||||
WasmtimeScriptEngine engine;
|
||||
@ -736,6 +697,33 @@ TEST(WasmtimeScope, MemoryLimit_ResetWithInvalidParamsFails) {
|
||||
ASSERT_THROWS_CODE(scope->reset(), DBException, ErrorCodes::BadValue);
|
||||
}
|
||||
|
||||
// Emit buffer must fit within the WASM store's linear memory.
|
||||
TEST(WasmtimeScope, EmitBufferExceedsStoreLimitFails) {
|
||||
auto savedHeap = gJSHeapLimitMB.load();
|
||||
auto savedStore = gWasmtimeStoreMemoryLimitMB.load();
|
||||
auto savedEmit = internalQueryMaxJsEmitBytes.load();
|
||||
ON_BLOCK_EXIT([&] {
|
||||
gJSHeapLimitMB.store(savedHeap);
|
||||
gWasmtimeStoreMemoryLimitMB.store(savedStore);
|
||||
internalQueryMaxJsEmitBytes.store(savedEmit);
|
||||
});
|
||||
|
||||
// heap=64 MB, overhead=max(64, 6)=64 MB → min store=128 MB for scope init to pass.
|
||||
// With store=128 MB, emitBuf=128MB+16MB=144MB > 128MB → injectNative throws BadValue.
|
||||
gJSHeapLimitMB.store(64);
|
||||
gWasmtimeStoreMemoryLimitMB.store(128);
|
||||
internalQueryMaxJsEmitBytes.store(128 * 1024 * 1024);
|
||||
|
||||
WasmtimeScriptEngine engine;
|
||||
std::unique_ptr<Scope> scope(engine.createScopeForCurrentThread(boost::none));
|
||||
ASSERT(scope);
|
||||
|
||||
ASSERT_THROWS_CODE(scope->injectNative(
|
||||
"emit", [](const BSONObj&, void*) { return BSONObj(); }, nullptr),
|
||||
DBException,
|
||||
ErrorCodes::BadValue);
|
||||
}
|
||||
|
||||
// --- OOM detection ---
|
||||
|
||||
// hasOutOfMemoryException() starts false and is set when an OOM occurs.
|
||||
|
||||
@ -36,17 +36,11 @@
|
||||
#include "mongo/scripting/config_gen.h"
|
||||
#include "mongo/scripting/engine.h"
|
||||
#include "mongo/scripting/mozjs/wasm/bridge/bridge.h"
|
||||
#include "mongo/scripting/mozjs/wasm/embedded_wasm_resource.h"
|
||||
#include "mongo/scripting/mozjs/wasm/scope/scope.h"
|
||||
|
||||
#define MONGO_LOGV2_DEFAULT_COMPONENT ::mongo::logv2::LogComponent::kQuery
|
||||
|
||||
// Symbols produced by objcopy from the AOT-compiled mozjs_wasm_api.cwasm.
|
||||
// See embed_mozjs_wasm_obj in BUILD.bazel.
|
||||
extern "C" {
|
||||
extern const uint8_t _binary_mozjs_wasm_api_cwasm_start[];
|
||||
extern const uint8_t _binary_mozjs_wasm_api_cwasm_end[];
|
||||
}
|
||||
|
||||
namespace mongo {
|
||||
|
||||
bool isExternalScriptingEnabled() {
|
||||
@ -88,9 +82,8 @@ WasmtimeScriptEngine::WasmtimeScriptEngine() {}
|
||||
WasmtimeScriptEngine::~WasmtimeScriptEngine() {}
|
||||
|
||||
std::shared_ptr<wasm::WasmEngineContext> WasmtimeScriptEngine::createWasmEngineContext() const {
|
||||
size_t size =
|
||||
static_cast<size_t>(_binary_mozjs_wasm_api_cwasm_end - _binary_mozjs_wasm_api_cwasm_start);
|
||||
return wasm::WasmEngineContext::createFromPrecompiled(_binary_mozjs_wasm_api_cwasm_start, size);
|
||||
auto [data, size] = wasm::getEmbeddedWasmResource();
|
||||
return wasm::WasmEngineContext::createFromPrecompiled(data, size);
|
||||
}
|
||||
|
||||
mongo::Scope* WasmtimeScriptEngine::createScope() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user