summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMax Hirschhorn <max.hirschhorn@mongodb.com>2018-09-18 21:10:36 -0400
committerMax Hirschhorn <max.hirschhorn@mongodb.com>2018-09-18 21:10:36 -0400
commitf43ea7a6bb2a6c44f91506c32004241d3cbb24ae (patch)
tree1dff1f481a3439045f0de84a242e1ac33f201249
parent78112be586c67efa877636d596b194650e90cbed (diff)
downloadmongo-f43ea7a6bb2a6c44f91506c32004241d3cbb24ae.tar.gz
SERVER-35154 Propagate JS exceptions through ScopedThread#join().
This makes it so that if the ScopedThread exited due to an uncaught JavaScript exception, then calling .join() or .returnData() on it throws a JavaScript exception with the error message and stacktrace intact.
-rw-r--r--jstests/core/error2.js19
-rw-r--r--jstests/core/mr_tolerates_js_exception.js38
-rw-r--r--jstests/core/where_tolerates_js_exception.js35
-rw-r--r--jstests/noPassthrough/shell_scoped_thread.js103
-rw-r--r--jstests/parallel/del.js111
-rw-r--r--src/mongo/base/error_codes.err1
-rw-r--r--src/mongo/scripting/SConscript1
-rw-r--r--src/mongo/scripting/jsexception.cpp75
-rw-r--r--src/mongo/scripting/jsexception.h63
-rw-r--r--src/mongo/scripting/mozjs/implscope.cpp60
-rw-r--r--src/mongo/scripting/mozjs/jsthread.cpp27
-rw-r--r--src/mongo/scripting/mozjs/status.cpp44
-rw-r--r--src/mongo/scripting/mozjs/status.h1
13 files changed, 482 insertions, 96 deletions
diff --git a/jstests/core/error2.js b/jstests/core/error2.js
deleted file mode 100644
index d9c2fdf7f63..00000000000
--- a/jstests/core/error2.js
+++ /dev/null
@@ -1,19 +0,0 @@
-// Test that client gets stack trace on failed invoke
-// @tags: [
-// requires_non_retryable_commands,
-// ]
-
-f = db.jstests_error2;
-
-f.drop();
-
-f.save({a: 1});
-
-assert.throws(function() {
- c = f.find({
- $where: function() {
- return a();
- }
- });
- c.next();
-});
diff --git a/jstests/core/mr_tolerates_js_exception.js b/jstests/core/mr_tolerates_js_exception.js
index 47536a3cc3f..20e96eb47dc 100644
--- a/jstests/core/mr_tolerates_js_exception.js
+++ b/jstests/core/mr_tolerates_js_exception.js
@@ -1,7 +1,11 @@
-// @tags: [does_not_support_stepdowns]
-
/**
- * Test that the mapReduce command fails gracefully when user-provided JavaScript code throws.
+ * Test that the mapReduce command fails gracefully when user-provided JavaScript code throws and
+ * that the user gets back a JavaScript stacktrace.
+ *
+ * @tags: [
+ * does_not_support_stepdowns,
+ * requires_scripting,
+ * ]
*/
(function() {
"use strict";
@@ -20,18 +24,32 @@
emit(this.a, 1);
},
reduce: function(key, value) {
- throw 42;
+ (function myFunction() {
+ throw new Error("Intentionally thrown inside reduce function");
+ })();
},
out: {inline: 1}
});
assert.commandFailedWithCode(cmdOutput, ErrorCodes.JSInterpreterFailure, tojson(cmdOutput));
+ assert(/Intentionally thrown inside reduce function/.test(cmdOutput.errmsg),
+ () => "mapReduce didn't include the message from the exception thrown: " +
+ tojson(cmdOutput));
+ assert(/myFunction@/.test(cmdOutput.errmsg),
+ () => "mapReduce didn't return the JavaScript stacktrace: " + tojson(cmdOutput));
+ assert(
+ !cmdOutput.hasOwnProperty("stack"),
+ () => "mapReduce shouldn't return JavaScript stacktrace separately: " + tojson(cmdOutput));
+ assert(!cmdOutput.hasOwnProperty("originalError"),
+ () => "mapReduce shouldn't return wrapped version of the error: " + tojson(cmdOutput));
// Test that the command fails with a JS interpreter failure error when the map function
// throws.
cmdOutput = db.runCommand({
mapReduce: coll.getName(),
map: function() {
- throw 42;
+ (function myFunction() {
+ throw new Error("Intentionally thrown inside map function");
+ })();
},
reduce: function(key, value) {
return Array.sum(value);
@@ -39,4 +57,14 @@
out: {inline: 1}
});
assert.commandFailedWithCode(cmdOutput, ErrorCodes.JSInterpreterFailure, tojson(cmdOutput));
+ assert(/Intentionally thrown inside map function/.test(cmdOutput.errmsg),
+ () => "mapReduce didn't include the message from the exception thrown: " +
+ tojson(cmdOutput));
+ assert(/myFunction@/.test(cmdOutput.errmsg),
+ () => "mapReduce didn't return the JavaScript stacktrace: " + tojson(cmdOutput));
+ assert(
+ !cmdOutput.hasOwnProperty("stack"),
+ () => "mapReduce shouldn't return JavaScript stacktrace separately: " + tojson(cmdOutput));
+ assert(!cmdOutput.hasOwnProperty("originalError"),
+ () => "mapReduce shouldn't return wrapped version of the error: " + tojson(cmdOutput));
}());
diff --git a/jstests/core/where_tolerates_js_exception.js b/jstests/core/where_tolerates_js_exception.js
new file mode 100644
index 00000000000..b12a7c0a65e
--- /dev/null
+++ b/jstests/core/where_tolerates_js_exception.js
@@ -0,0 +1,35 @@
+/**
+ * Test that $where fails gracefully when user-provided JavaScript code throws and that the user
+ * gets back the JavaScript stacktrace.
+ *
+ * @tags: [
+ * requires_non_retryable_commands,
+ * requires_scripting,
+ * ]
+ */
+(function() {
+ "use strict";
+
+ const collection = db.where_tolerates_js_exception;
+ collection.drop();
+
+ assert.commandWorked(collection.save({a: 1}));
+
+ const res = collection.runCommand("find", {
+ filter: {
+ $where: function myFunction() {
+ return a();
+ }
+ }
+ });
+
+ assert.commandFailedWithCode(res, ErrorCodes.JSInterpreterFailure);
+ assert(/ReferenceError/.test(res.errmsg),
+ () => "$where didn't failed with a ReferenceError: " + tojson(res));
+ assert(/myFunction@/.test(res.errmsg),
+ () => "$where didn't return the JavaScript stacktrace: " + tojson(res));
+ assert(!res.hasOwnProperty("stack"),
+ () => "$where shouldn't return JavaScript stacktrace separately: " + tojson(res));
+ assert(!res.hasOwnProperty("originalError"),
+ () => "$where shouldn't return wrapped version of the error: " + tojson(res));
+})();
diff --git a/jstests/noPassthrough/shell_scoped_thread.js b/jstests/noPassthrough/shell_scoped_thread.js
index 7c2b92457a1..4b3bcc04167 100644
--- a/jstests/noPassthrough/shell_scoped_thread.js
+++ b/jstests/noPassthrough/shell_scoped_thread.js
@@ -93,6 +93,109 @@ load('jstests/libs/parallelTester.js'); // for ScopedThread
}
});
+ function testUncaughtException(joinFn) {
+ const thread = new ScopedThread(function myFunction() {
+ throw new Error("Intentionally thrown inside ScopedThread");
+ });
+ thread.start();
+
+ let error = assert.throws(joinFn, [thread]);
+ assert(
+ /Intentionally thrown inside ScopedThread/.test(error.message),
+ () =>
+ "Exception didn't include the message from the exception thrown in ScopedThread: " +
+ tojson(error.message));
+ assert(/myFunction@/.test(error.stack),
+ () => "Exception doesn't contain stack frames from within the ScopedThread: " +
+ tojson(error.stack));
+ assert(/testUncaughtException@/.test(error.stack),
+ () => "Exception doesn't contain stack frames from caller of the ScopedThread: " +
+ tojson(error.stack));
+
+ error = assert.throws(() => thread.join());
+ assert.eq("Thread not running",
+ error.message,
+ "join() is expected to be called only once for the thread");
+
+ assert.eq(true,
+ thread.hasFailed(),
+ "Uncaught exception didn't cause thread to be marked as having failed");
+ assert.doesNotThrow(() => thread.returnData(),
+ [],
+ "returnData() threw an exception after join() had been called");
+ assert.eq(undefined,
+ thread.returnData(),
+ "returnData() shouldn't have anything to return if the thread failed");
+ }
+
+ tests.push(function testUncaughtExceptionAndWaitUsingJoin() {
+ testUncaughtException(thread => thread.join());
+ });
+
+ // The returnData() method internally calls the join() method and should also throw an exception
+ // if the ScopedThread had an uncaught exception.
+ tests.push(function testUncaughtExceptionAndWaitUsingReturnData() {
+ testUncaughtException(thread => thread.returnData());
+ });
+
+ tests.push(function testUncaughtExceptionInNativeCode() {
+ const thread = new ScopedThread(function myFunction() {
+ new Timestamp(-1);
+ });
+ thread.start();
+
+ const error = assert.throws(() => thread.join());
+ assert(
+ /Timestamp/.test(error.message),
+ () =>
+ "Exception didn't include the message from the exception thrown in ScopedThread: " +
+ tojson(error.message));
+ assert(/myFunction@/.test(error.stack),
+ () => "Exception doesn't contain stack frames from within the ScopedThread: " +
+ tojson(error.stack));
+ });
+
+ tests.push(function testUncaughtExceptionFromNestedScopedThreads() {
+ const thread = new ScopedThread(function myFunction1() {
+ load("jstests/libs/parallelTester.js");
+
+ const thread = new ScopedThread(function myFunction2() {
+ load("jstests/libs/parallelTester.js");
+
+ const thread = new ScopedThread(function myFunction3() {
+ throw new Error("Intentionally thrown inside ScopedThread");
+ });
+
+ thread.start();
+ thread.join();
+ });
+
+ thread.start();
+ thread.join();
+ });
+ thread.start();
+
+ const error = assert.throws(() => thread.join());
+ assert(
+ /Intentionally thrown inside ScopedThread/.test(error.message),
+ () =>
+ "Exception didn't include the message from the exception thrown in ScopedThread: " +
+ tojson(error.message));
+ assert(
+ /myFunction3@/.test(error.stack),
+ () =>
+ "Exception doesn't contain stack frames from within the innermost ScopedThread: " +
+ tojson(error.stack));
+ assert(/myFunction2@/.test(error.stack),
+ () => "Exception doesn't contain stack frames from within an inner ScopedThread: " +
+ tojson(error.stack));
+ assert(
+ /myFunction1@/.test(error.stack),
+ () =>
+ "Exception doesn't contain stack frames from within the outermost ScopedThread: " +
+ tojson(error.stack));
+ });
+
/* main */
tests.forEach((test) => {
diff --git a/jstests/parallel/del.js b/jstests/parallel/del.js
index 3128f89d05e..903bb983d1f 100644
--- a/jstests/parallel/del.js
+++ b/jstests/parallel/del.js
@@ -8,54 +8,84 @@ b = db.getSisterDB("foob");
a.dropDatabase();
b.dropDatabase();
-function del1(dbname, host, max) {
- var m = new Mongo(host);
- var db = m.getDB("foo" + dbname);
- var t = db.del;
-
- while (!db.del_parallel.count()) {
- var r = Math.random();
- var n = Math.floor(Math.random() * max);
- if (r < .9) {
- t.insert({x: n});
- } else if (r < .98) {
- t.remove({x: n});
- } else if (r < .99) {
- t.remove({x: {$lt: n}});
- } else {
- t.remove({x: {$gt: n}});
+var kCursorKilledErrorCodes = [
+ ErrorCodes.OperationFailed,
+ ErrorCodes.QueryPlanKilled,
+ ErrorCodes.CursorNotFound,
+];
+
+function del1(dbname, host, max, kCursorKilledErrorCodes) {
+ try {
+ var m = new Mongo(host);
+ var db = m.getDB("foo" + dbname);
+ var t = db.del;
+
+ while (!db.del_parallel.count()) {
+ var r = Math.random();
+ var n = Math.floor(Math.random() * max);
+ if (r < .9) {
+ t.insert({x: n});
+ } else if (r < .98) {
+ t.remove({x: n});
+ } else if (r < .99) {
+ t.remove({x: {$lt: n}});
+ } else {
+ t.remove({x: {$gt: n}});
+ }
+ if (r > .9999)
+ print(t.count());
+ }
+
+ return {ok: 1};
+ } catch (e) {
+ if (kCursorKilledErrorCodes.includes(e.code)) {
+ // It is expected that the cursor may have been killed due to the database being
+ // dropped concurrently.
+ return {ok: 1};
}
- if (r > .9999)
- print(t.count());
+
+ throw e;
}
}
-function del2(dbname, host, max) {
- var m = new Mongo(host);
- var db = m.getDB("foo" + dbname);
- var t = db.del;
-
- while (!db.del_parallel.count()) {
- var r = Math.random();
- var n = Math.floor(Math.random() * max);
- var s = Math.random() > .5 ? 1 : -1;
-
- if (r < .5) {
- t.findOne({x: n});
- } else if (r < .75) {
- t.find({x: {$lt: n}}).sort({x: s}).itcount();
- } else {
- t.find({x: {$gt: n}}).sort({x: s}).itcount();
+function del2(dbname, host, max, kCursorKilledErrorCodes) {
+ try {
+ var m = new Mongo(host);
+ var db = m.getDB("foo" + dbname);
+ var t = db.del;
+
+ while (!db.del_parallel.count()) {
+ var r = Math.random();
+ var n = Math.floor(Math.random() * max);
+ var s = Math.random() > .5 ? 1 : -1;
+
+ if (r < .5) {
+ t.findOne({x: n});
+ } else if (r < .75) {
+ t.find({x: {$lt: n}}).sort({x: s}).itcount();
+ } else {
+ t.find({x: {$gt: n}}).sort({x: s}).itcount();
+ }
+ }
+
+ return {ok: 1};
+ } catch (e) {
+ if (kCursorKilledErrorCodes.includes(e.code)) {
+ // It is expected that the cursor may have been killed due to the database being
+ // dropped concurrently.
+ return {ok: 1};
}
+
+ throw e;
}
}
all = [];
-all.push(fork(del1, "a", HOST, N));
-all.push(fork(del2, "a", HOST, N));
-all.push(fork(del1, "b", HOST, N));
-all.push(fork(del2, "b", HOST, N));
+all.push(fork(del1, "a", HOST, N, kCursorKilledErrorCodes));
+all.push(fork(del2, "a", HOST, N, kCursorKilledErrorCodes));
+all.push(fork(del1, "b", HOST, N, kCursorKilledErrorCodes));
+all.push(fork(del2, "b", HOST, N, kCursorKilledErrorCodes));
for (i = 0; i < all.length; i++)
all[i].start();
@@ -70,5 +100,6 @@ for (i = 0; i < 10; i++) {
a.del_parallel.save({done: 1});
b.del_parallel.save({done: 1});
-for (i = 0; i < all.length; i++)
- all[i].join();
+for (i = 0; i < all.length; i++) {
+ assert.commandWorked(all[i].returnData());
+}
diff --git a/src/mongo/base/error_codes.err b/src/mongo/base/error_codes.err
index 483498100a6..c4d3b46dc15 100644
--- a/src/mongo/base/error_codes.err
+++ b/src/mongo/base/error_codes.err
@@ -268,6 +268,7 @@ error_code("PreparedTransactionInProgress", 267);
error_code("CannotBackup", 268);
error_code("DataModifiedByRepair", 269);
error_code("RepairedReplicaSetNode", 270);
+error_code("JSInterpreterFailureWithStack", 271, extra="JSExceptionInfo")
# Error codes 4000-8999 are reserved.
# Non-sequential error codes (for compatibility only)
diff --git a/src/mongo/scripting/SConscript b/src/mongo/scripting/SConscript
index 0c489715917..c1726fba7cd 100644
--- a/src/mongo/scripting/SConscript
+++ b/src/mongo/scripting/SConscript
@@ -15,6 +15,7 @@ env.Library(
'deadline_monitor.cpp',
'dbdirectclient_factory.cpp',
'engine.cpp',
+ 'jsexception.cpp',
'utils.cpp',
],
LIBDEPS=[
diff --git a/src/mongo/scripting/jsexception.cpp b/src/mongo/scripting/jsexception.cpp
new file mode 100644
index 00000000000..51671963e6a
--- /dev/null
+++ b/src/mongo/scripting/jsexception.cpp
@@ -0,0 +1,75 @@
+/**
+ * Copyright (C) 2018 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#include "mongo/platform/basic.h"
+
+#include "mongo/scripting/jsexception.h"
+
+#include "mongo/base/init.h"
+#include "mongo/bson/bsonobjbuilder.h"
+
+namespace mongo {
+namespace {
+
+constexpr auto kCodeFieldName = "code"_sd;
+constexpr auto kCodeNameFieldName = "codeName"_sd;
+constexpr auto kOriginalErrorFieldName = "originalError"_sd;
+constexpr auto kReasonFieldName = "errmsg"_sd;
+constexpr auto kStackFieldName = "stack"_sd;
+
+} // namespace
+
+void JSExceptionInfo::serialize(BSONObjBuilder* builder) const {
+ builder->append(kStackFieldName, this->stack);
+
+ {
+ BSONObjBuilder originalErrorBuilder(builder->subobjStart(kOriginalErrorFieldName));
+ originalErrorBuilder.append(kReasonFieldName, this->originalError.reason());
+ originalErrorBuilder.append(kCodeFieldName, this->originalError.code());
+ originalErrorBuilder.append(kCodeNameFieldName,
+ ErrorCodes::errorString(this->originalError.code()));
+ if (auto extraInfo = this->originalError.extraInfo()) {
+ extraInfo->serialize(&originalErrorBuilder);
+ }
+ }
+}
+
+std::shared_ptr<const ErrorExtraInfo> JSExceptionInfo::parse(const BSONObj& obj) {
+ auto stack = obj[kStackFieldName].String();
+
+ auto originalErrorObj = obj[kOriginalErrorFieldName].Obj();
+ auto code = originalErrorObj[kCodeFieldName].Int();
+ auto reason = originalErrorObj[kReasonFieldName].checkAndGetStringData();
+ Status status(ErrorCodes::Error(code), reason, originalErrorObj);
+
+ return std::make_shared<JSExceptionInfo>(std::move(stack), std::move(status));
+}
+
+MONGO_INIT_REGISTER_ERROR_EXTRA_INFO(JSExceptionInfo);
+
+} // namespace mongo
diff --git a/src/mongo/scripting/jsexception.h b/src/mongo/scripting/jsexception.h
new file mode 100644
index 00000000000..3871486d046
--- /dev/null
+++ b/src/mongo/scripting/jsexception.h
@@ -0,0 +1,63 @@
+/**
+ * Copyright (C) 2018 MongoDB Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License, version 3,
+ * as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * As a special exception, the copyright holders give permission to link the
+ * code of portions of this program with the OpenSSL library under certain
+ * conditions as described in each individual source file and distribute
+ * linked combinations including the program with the OpenSSL library. You
+ * must comply with the GNU Affero General Public License in all respects
+ * for all of the code used other than as permitted herein. If you modify
+ * file(s) with this exception, you may extend this exception to your
+ * version of the file(s), but you are not obligated to do so. If you do not
+ * wish to do so, delete this exception statement from your version. If you
+ * delete this exception statement from all source files in the program,
+ * then also delete it in the license file.
+ */
+
+#pragma once
+
+#include <string>
+
+#include "mongo/base/error_codes.h"
+#include "mongo/base/error_extra_info.h"
+#include "mongo/base/status.h"
+
+namespace mongo {
+
+/**
+ * Represents information about a JavaScript exception. The "message" is stored as the Status's
+ * reason(), so only the "stack" is stored here.
+ *
+ * This class wraps an existing error and serializes it in a lossless way, so any other metadata
+ * about the JavaScript exception is also preserved.
+ */
+class JSExceptionInfo final : public ErrorExtraInfo {
+public:
+ static constexpr auto code = ErrorCodes::JSInterpreterFailureWithStack;
+
+ void serialize(BSONObjBuilder*) const override;
+ static std::shared_ptr<const ErrorExtraInfo> parse(const BSONObj&);
+
+ explicit JSExceptionInfo(std::string stack_, Status originalError_)
+ : stack(std::move(stack_)), originalError(std::move(originalError_)) {
+ invariant(!stack.empty());
+ invariant(!originalError.isOK());
+ }
+
+ const std::string stack;
+ const Status originalError;
+};
+
+} // namespace mongo
diff --git a/src/mongo/scripting/mozjs/implscope.cpp b/src/mongo/scripting/mozjs/implscope.cpp
index bf35606f2ba..c852899aa46 100644
--- a/src/mongo/scripting/mozjs/implscope.cpp
+++ b/src/mongo/scripting/mozjs/implscope.cpp
@@ -41,11 +41,13 @@
#include "mongo/db/server_parameters.h"
#include "mongo/platform/decimal128.h"
#include "mongo/platform/stack_locator.h"
+#include "mongo/scripting/jsexception.h"
#include "mongo/scripting/mozjs/objectwrapper.h"
#include "mongo/scripting/mozjs/valuereader.h"
#include "mongo/scripting/mozjs/valuewriter.h"
#include "mongo/stdx/memory.h"
#include "mongo/stdx/mutex.h"
+#include "mongo/util/assert_util.h"
#include "mongo/util/log.h"
#include "mongo/util/scopeguard.h"
@@ -125,10 +127,6 @@ void MozJSImplScope::_reportError(JSContext* cx, const char* message, JSErrorRep
std::string exceptionMsg;
try {
- str::stream ss;
-
- ss << message;
-
// TODO: something far more elaborate that mimics the stack printing from v8
JS::RootedValue excn(cx);
if (JS_GetPendingException(cx, &excn) && excn.isObject()) {
@@ -138,23 +136,24 @@ void MozJSImplScope::_reportError(JSContext* cx, const char* message, JSErrorRep
ObjectWrapper o(cx, obj);
o.getValue("stack", &stack);
- auto str = ValueWriter(cx, stack).toString();
+ auto stackStr = ValueWriter(cx, stack).toString();
- if (str.empty()) {
- ss << " @" << report->filename << ":" << report->lineno << ":" << report->column
+ if (stackStr.empty()) {
+ str::stream ss;
+ ss << "@" << report->filename << ":" << report->lineno << ":" << report->column
<< "\n";
- } else {
- ss << " :\n" << str;
+ stackStr = ss;
}
- scope->_status =
+ auto status =
jsExceptionToStatus(cx, excn, ErrorCodes::JSInterpreterFailure, message)
- .withReason(ss);
+ .withReason(message);
+ scope->_status = {JSExceptionInfo(std::move(stackStr), std::move(status)), message};
return;
}
- exceptionMsg = ss;
+ exceptionMsg = message;
} catch (const DBException& dbe) {
exceptionMsg = "Unknown error occured while processing exception";
log() << exceptionMsg << ":" << dbe.toString() << ":" << message;
@@ -526,12 +525,28 @@ auto MozJSImplScope::_runSafely(ImplScopeFunction&& functionToRun) -> decltype(f
MozJSEntry entry(this);
return functionToRun();
} catch (...) {
+ // There may have already been an error reported by SpiderMonkey. If not, then we use the
+ // active C++ exception as the cause of the error.
+ if (_status.isOK()) {
+ _status = exceptionToStatus();
+ }
+
+ if (auto extraInfo = _status.extraInfo<JSExceptionInfo>()) {
+ // We intentionally don't transmit an JSInterpreterFailureWithStack error over the wire
+ // because of the complexity it'd entail on the recipient to reach inside to the
+ // underlying error for how it should be handled. Instead, the error is unwrapped and
+ // the JavaScript stacktrace is included as part of the error message.
+ str::stream reasonWithStack;
+ reasonWithStack << extraInfo->originalError.reason() << " :\n" << extraInfo->stack;
+ _status = extraInfo->originalError.withReason(reasonWithStack);
+ }
+
_error = _status.reason();
// Clear the status state
auto status = std::move(_status);
uassertStatusOK(status);
- throw;
+ MONGO_UNREACHABLE;
}
}
@@ -609,6 +624,10 @@ void MozJSImplScope::_MozJSCreateFunction(StringData raw, JS::MutableHandleValue
}
BSONObj MozJSImplScope::callThreadArgs(const BSONObj& args) {
+ // The _runSafely() function is called for all codepaths of executing JavaScript other than
+ // callThreadArgs(). We intentionally don't unwrap the JSInterpreterFailureWithStack error to
+ // make it possible for the parent thread to chain its JavaScript stacktrace with the child
+ // thread's JavaScript stacktrace.
MozJSEntry entry(this);
JS::RootedValue function(_context);
@@ -903,16 +922,21 @@ bool MozJSImplScope::_checkErrorState(bool success, bool reportError, bool asser
ss << "[" << fnameStr << ":" << lineNum << ":" << colNum << "] ";
}
ss << ValueWriter(_context, excn).toString();
- if (stackStr != "") {
- ss << "\nStack trace:\n" << stackStr << "----------\n";
- }
- _status = Status(ErrorCodes::JSInterpreterFailure, ss);
+ _status = {
+ JSExceptionInfo(std::move(stackStr), Status(ErrorCodes::JSInterpreterFailure, ss)),
+ ss};
} else {
_status = Status(ErrorCodes::UnknownError, "Unknown Failure from JSInterpreter");
}
}
- _error = _status.reason();
+ if (auto extraInfo = _status.extraInfo<JSExceptionInfo>()) {
+ str::stream reasonWithStack;
+ reasonWithStack << _status.reason() << " :\n" << extraInfo->stack;
+ _error = reasonWithStack;
+ } else {
+ _error = _status.reason();
+ }
if (reportError)
error() << redact(_error);
diff --git a/src/mongo/scripting/mozjs/jsthread.cpp b/src/mongo/scripting/mozjs/jsthread.cpp
index 5dccaf36bae..0072aa419e0 100644
--- a/src/mongo/scripting/mozjs/jsthread.cpp
+++ b/src/mongo/scripting/mozjs/jsthread.cpp
@@ -124,6 +124,8 @@ public:
PR_JoinThread(_thread);
_done = true;
+
+ uassertStatusOK(_sharedData->getErrorStatus());
}
/**
@@ -134,7 +136,7 @@ public:
bool hasFailed() const {
uassert(ErrorCodes::JSInterpreterFailure, "Thread not started", _started);
- return _sharedData->getErrored();
+ return !_sharedData->getErrorStatus().isOK();
}
BSONObj returnData() {
@@ -154,16 +156,16 @@ private:
*/
class SharedData {
public:
- SharedData() : _errored(false) {}
+ SharedData() = default;
- void setErrored(bool value) {
- stdx::lock_guard<stdx::mutex> lck(_erroredMutex);
- _errored = value;
+ void setErrorStatus(Status status) {
+ stdx::lock_guard<stdx::mutex> lck(_statusMutex);
+ _status = std::move(status);
}
- bool getErrored() {
- stdx::lock_guard<stdx::mutex> lck(_erroredMutex);
- return _errored;
+ Status getErrorStatus() {
+ stdx::lock_guard<stdx::mutex> lck(_statusMutex);
+ return _status;
}
/**
@@ -176,8 +178,8 @@ private:
std::string _stack;
private:
- stdx::mutex _erroredMutex;
- bool _errored;
+ stdx::mutex _statusMutex;
+ Status _status = Status::OK();
};
/**
@@ -197,10 +199,7 @@ private:
thisv->_sharedData->_returnData = scope.callThreadArgs(thisv->_sharedData->_args);
} catch (...) {
auto status = exceptionToStatus();
-
- log() << "js thread raised js exception: " << redact(status)
- << thisv->_sharedData->_stack;
- thisv->_sharedData->setErrored(true);
+ thisv->_sharedData->setErrorStatus(status);
thisv->_sharedData->_returnData = BSON("ret" << BSONUndefined);
}
}
diff --git a/src/mongo/scripting/mozjs/status.cpp b/src/mongo/scripting/mozjs/status.cpp
index cb4ecb1bbd3..a78238d03ce 100644
--- a/src/mongo/scripting/mozjs/status.cpp
+++ b/src/mongo/scripting/mozjs/status.cpp
@@ -30,6 +30,7 @@
#include "mongo/scripting/mozjs/status.h"
+#include "mongo/scripting/jsexception.h"
#include "mongo/scripting/mozjs/implscope.h"
#include "mongo/scripting/mozjs/internedstring.h"
#include "mongo/scripting/mozjs/objectwrapper.h"
@@ -76,6 +77,14 @@ void MongoStatusInfo::fromStatus(JSContext* cx, Status status, JS::MutableHandle
JSPROP_ENUMERATE | JSPROP_SHARED,
smUtils::wrapConstrainedMethod<Functions::reason, false, MongoStatusInfo>);
+ // We intentionally omit JSPROP_ENUMERATE to match how Error.prototype.stack is a non-enumerable
+ // property.
+ thisvObj.defineProperty(
+ InternedString::stack,
+ undef,
+ JSPROP_SHARED,
+ smUtils::wrapConstrainedMethod<Functions::stack, false, MongoStatusInfo>);
+
JS_SetPrivate(thisv, scope->trackedNew<Status>(std::move(status)));
value.setObjectOrNull(thisv);
@@ -106,6 +115,41 @@ void MongoStatusInfo::Functions::reason::call(JSContext* cx, JS::CallArgs args)
ValueReader(cx, args.rval()).fromStringData(toStatus(cx, args.thisv()).reason());
}
+void MongoStatusInfo::Functions::stack::call(JSContext* cx, JS::CallArgs args) {
+ JS::RootedObject thisv(cx, args.thisv().toObjectOrNull());
+ JS::RootedObject parent(cx);
+
+ if (!JS_GetPrototype(cx, thisv, &parent)) {
+ uasserted(ErrorCodes::JSInterpreterFailure, "Couldn't get prototype");
+ }
+
+ ObjectWrapper parentWrapper(cx, parent);
+
+ auto status = toStatus(cx, args.thisv());
+ if (auto extraInfo = status.extraInfo<JSExceptionInfo>()) {
+ // 'status' represents an uncaught JavaScript exception that was handled in C++. It is
+ // expected to have been thrown by a JSThread. We chain its stacktrace together with the
+ // stacktrace of the JavaScript exception in the current thread in order to show more
+ // context about the latter's cause.
+ JS::RootedValue stack(cx);
+ ValueReader(cx, &stack)
+ .fromStringData(extraInfo->stack + parentWrapper.getString(InternedString::stack));
+
+ // We redefine the "stack" property as the combined JavaScript stacktrace. It is important
+ // that we omit JSPROP_SHARED to the thisvObj.defineProperty() call in order to have
+ // SpiderMonkey allocate memory for the string value. We also intentionally omit
+ // JSPROP_ENUMERATE to match how Error.prototype.stack is a non-enumerable property.
+ ObjectWrapper thisvObj(cx, args.thisv());
+ thisvObj.defineProperty(InternedString::stack, stack, 0U);
+
+ // We intentionally use thisvObj.getValue() to access the "stack" property to implicitly
+ // verify it has been redefined correctly, as the alternative would be infinite recursion.
+ thisvObj.getValue(InternedString::stack, args.rval());
+ } else {
+ parentWrapper.getValue(InternedString::stack, args.rval());
+ }
+}
+
void MongoStatusInfo::postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto) {
auto scope = getScope(cx);
diff --git a/src/mongo/scripting/mozjs/status.h b/src/mongo/scripting/mozjs/status.h
index d930bdb8df9..8a066936520 100644
--- a/src/mongo/scripting/mozjs/status.h
+++ b/src/mongo/scripting/mozjs/status.h
@@ -51,6 +51,7 @@ struct MongoStatusInfo : public BaseInfo {
struct Functions {
MONGO_DECLARE_JS_FUNCTION(code);
MONGO_DECLARE_JS_FUNCTION(reason);
+ MONGO_DECLARE_JS_FUNCTION(stack);
};
static void postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto);