0
0
mirror of https://github.com/mongodb/mongo.git synced 2024-11-21 12:39:08 +01:00

SERVER-82330 Add ability to decode resume token from bson in js tests (#28345)

GitOrigin-RevId: 177f2f3fe035d1c0f06a955d5c6784d4d108de8f
This commit is contained in:
Lee Maguire 2024-11-05 15:50:51 +00:00 committed by MongoDB Bot
parent 798b21f78e
commit d9c44b0e91
15 changed files with 240 additions and 17 deletions

View File

@ -147,6 +147,8 @@ globals:
_resultSetsEqualUnordered: true
getStringWidth: true
_compareStringsWithCollation: true
eventResumeTokenType: true
highWaterMarkResumeTokenType: true
# likely could be replaced with `path`
_copyFileRange: true
@ -200,6 +202,7 @@ globals:
Timestamp: true
MD5: true
Geo: true
decodeResumeToken: true
bsonWoCompare: true
bsonUnorderedFieldsCompare: true
bsonBinaryEqual: true

View File

@ -105,6 +105,7 @@ const cmdResCollWithCollation = assert.commandWorked(runExactCommand(db, {
}));
csCursor = new DBCommandCursor(db, cmdResCollWithCollation);
const hwmFromCollWithCollation = csCursor.getResumeToken();
assert.eq(decodeResumeToken(hwmFromCollWithCollation).tokenType, highWaterMarkResumeTokenType);
assert.neq(undefined, hwmFromCollWithCollation);
csCursor.close();
@ -164,9 +165,11 @@ assert.soon(() => {
assert.eq(csCursor.objsLeftInBatch(), 0);
hwmToken = csCursor.getResumeToken();
assert.neq(undefined, hwmToken);
return relatedEvent && bsonWoCompare(hwmToken, relatedEvent._id) > 0;
});
csCursor.close();
assert.eq(decodeResumeToken(hwmToken).tokenType, highWaterMarkResumeTokenType);
// Now write some further documents to the collection before attempting to resume.
for (let i = 0; i < 5; ++i) {
@ -261,4 +264,5 @@ assert.soon(() => {
assert.soon(() => {
return csCursor.hasNext() && csCursor.next().operationType === "invalidate";
});
csCursor.close();

View File

@ -83,6 +83,16 @@ let csCursor = testColl.watch([]);
// Record a resume token marking the start point of the test.
const testStartToken = csCursor.getResumeToken();
const decodedToken = decodeResumeToken(testStartToken);
assert.eq(decodedToken.tokenType, highWaterMarkResumeTokenType);
assert.eq(decodedToken.version, 2);
assert.eq(decodedToken.txnOpIndex, 0);
assert.eq(decodedToken.tokenType, 0);
assert.eq(decodedToken.fromInvalidate, false);
assert.gt(decodedToken.clusterTime, new Timestamp(0, 0));
assert.eq(decodedToken.uuid, undefined);
assert.eq(decodedToken.fragmentNum, undefined);
// Perform ~16MB updates which generate ~16MB change events and ~16MB post-images.
assert.commandWorked(testColl.update({_id: "aaa"}, {$set: {a: "y".repeat(kLargeStringSize)}}));
assert.commandWorked(testColl.update({_id: "bbb"}, {$set: {a: "y".repeat(kLargeStringSize)}}));
@ -139,6 +149,23 @@ function validateReconstructedEvent(event, expectedId) {
assert.eq(kLargeStringSize, event.updateDescription.updatedFields.a.length);
}
// Helper function to validate a collection of resume tokens.
function validateResumeTokens(resumeTokens, numFragments) {
resumeTokens.forEach((resumeToken, idx) => {
const decodedToken = decodeResumeToken(resumeToken);
const eventIdentifier = {"operationType": "update", "documentKey": {"_id": "aaa"}};
assert.eq(decodedToken.eventIdentifier, eventIdentifier);
assert.eq(decodedToken.tokenType, eventResumeTokenType);
assert.eq(decodedToken.version, 2);
assert.eq(decodedToken.txnOpIndex, 0);
assert.eq(decodedToken.tokenType, 128);
assert.eq(decodedToken.fromInvalidate, false);
assert.gt(decodedToken.clusterTime, new Timestamp(0, 0));
assert.eq(decodedToken.fragmentNum, idx);
assert.neq(decodedToken.uuid, undefined);
});
}
// We declare 'resumeTokens' array outside of the for-scope to collect and share resume tokens
// across several test-cases.
let resumeTokens = [];
@ -187,6 +214,7 @@ for (const postImageMode of ["required", "updateLookup"]) {
var reconstructedEvent;
[reconstructedEvent, resumeTokens] = reconstructSplitEvent(csCursor, 3);
validateReconstructedEvent(reconstructedEvent, "aaa");
validateResumeTokens(resumeTokens, 3);
const [reconstructedEvent2, _] = reconstructSplitEvent(csCursor, 3);
validateReconstructedEvent(reconstructedEvent2, "bbb");
@ -219,6 +247,7 @@ for (const postImageMode of ["required", "updateLookup"]) {
resumeAfter: testStartToken
});
assert.docEq(resumeTokens, reconstructSplitEvent(csCursor, 3)[1]);
validateResumeTokens(resumeTokens, 3);
}
{

View File

@ -65,6 +65,7 @@ const expectedGlobalVars = [
"RangeError",
"ReferenceError",
"RegExp",
"ResumeTokenDataUtility",
"Set",
"String",
"Symbol",
@ -87,6 +88,7 @@ const expectedGlobalVars = [
"bsonUnorderedFieldsCompare",
"bsonWoCompare",
"buildInfo",
"decodeResumeToken",
"decodeURI",
"decodeURIComponent",
"doassert",
@ -94,11 +96,13 @@ const expectedGlobalVars = [
"encodeURIComponent",
"escape",
"eval",
"eventResumeTokenType",
"gc",
"getJSHeapLimitMB",
"globalThis",
"hex_md5",
"isFinite",
"highWaterMarkResumeTokenType",
"isNaN",
"isNumber",
"isObject",

View File

@ -59,6 +59,7 @@ assert(mongosColl.drop());
});
const invalidateResumeToken = changeStream.getResumeToken();
assert(decodeResumeToken(invalidateResumeToken).fromInvalidate);
// Recreate and then immediately drop the collection again to make sure that change stream when
// opened with the invalidate resume token sees this invalidate event.
@ -74,4 +75,4 @@ assert(mongosColl.drop());
});
})();
st.stop();
st.stop();

View File

@ -99,6 +99,9 @@ function validateResumeTokenQueryShape(conn, coll) {
assert.soon(() => changeStream.hasNext());
changeStream.next();
const invalidateResumeToken = changeStream.getResumeToken();
const decodedToken = decodeResumeToken(invalidateResumeToken);
assert.eq(decodedToken.tokenType, eventResumeTokenType);
assert.eq(decodedToken.fromInvalidate, false);
// Resume the change stream using 'startAfter' field.
coll.watch([], {startAfter: invalidateResumeToken});

View File

@ -49,6 +49,7 @@ assert.soon(() => {
assert.neq(undefined, hwmToken);
return bsonWoCompare(hwmToken, monitoredEvent._id) > 0;
});
assert.eq(decodeResumeToken(hwmToken).tokenType, highWaterMarkResumeTokenType);
// Open a change stream on tenant 2 so we can observe a write that happens and verify that write
// advanced the global oplog timestamp.
@ -65,6 +66,7 @@ assert.soon(() => {
// greater than the last resume token we got.
assert.eq(csCursor.hasNext(), false);
const hwmToken2 = csCursor.getResumeToken();
assert.eq(decodeResumeToken(hwmToken2).tokenType, highWaterMarkResumeTokenType);
assert.neq(undefined, hwmToken2);
assert.eq(bsonWoCompare(hwmToken, hwmToken2), 0);

View File

@ -89,23 +89,29 @@ bool ResumeTokenData::operator==(const ResumeTokenData& other) const {
fragmentNum == other.fragmentNum;
}
BSONObj ResumeTokenData::toBSON() const {
// TODO SERVER-96418: Make ResumeTokenData an IDL type so that this method is auto-generated.
BSONObjBuilder builder;
builder.append("clusterTime", clusterTime);
builder.append("tokenData", tokenType);
builder.append("version", version);
builder.append("txnOpIndex", static_cast<int64_t>(txnOpIndex));
if (version > 0) {
builder.append("tokenType", tokenType);
builder.append("fromInvalidate", static_cast<bool>(fromInvalidate));
}
if (uuid) {
builder.append("uuid", uuid->toBSON());
}
if (fragmentNum) {
builder.append("fragmentNum", static_cast<int64_t>(*fragmentNum));
}
eventIdentifier.addToBsonObj(&builder, "eventIdentifier");
return builder.obj();
}
std::ostream& operator<<(std::ostream& out, const ResumeTokenData& tokenData) {
out << "{clusterTime: " << tokenData.clusterTime.toString();
out << ", version: " << tokenData.version;
if (tokenData.version > 0) {
out << ", tokenType: " << tokenData.tokenType;
}
out << ", txnOpIndex: " << tokenData.txnOpIndex;
if (tokenData.version > 0) {
out << ", fromInvalidate: " << static_cast<bool>(tokenData.fromInvalidate);
}
out << ", uuid: " << optional_io::Extension{tokenData.uuid};
out << ", eventIdentifier: " << tokenData.eventIdentifier;
if (tokenData.version >= 2) {
out << ", fragmentNum: " << optional_io::Extension{tokenData.fragmentNum};
}
out << "}";
return out;
return out << tokenData.toBSON();
}
ResumeToken::ResumeToken(const Document& resumeDoc) {

View File

@ -119,6 +119,8 @@ struct ResumeTokenData {
// Index of the current fragment, for oversized events that have been split.
boost::optional<size_t> fragmentNum;
BSONObj toBSON() const;
private:
// This private constructor should only ever be used internally or by the ResumeToken class.
ResumeTokenData() = default;

View File

@ -111,6 +111,7 @@ mongo_cc_library(
"//src/mongo/scripting/mozjs:oid.cpp",
"//src/mongo/scripting/mozjs:proxyscope.cpp",
"//src/mongo/scripting/mozjs:regexp.cpp",
"//src/mongo/scripting/mozjs:resumetoken.cpp",
"//src/mongo/scripting/mozjs:scripting_util_gen",
"//src/mongo/scripting/mozjs:session.cpp",
"//src/mongo/scripting/mozjs:status.cpp",
@ -162,6 +163,7 @@ mongo_cc_library(
"//src/mongo/scripting/mozjs:oid.h",
"//src/mongo/scripting/mozjs:proxyscope.h",
"//src/mongo/scripting/mozjs:regexp.h",
"//src/mongo/scripting/mozjs:resumetoken.h",
"//src/mongo/scripting/mozjs:session.h",
"//src/mongo/scripting/mozjs:status.h",
"//src/mongo/scripting/mozjs:timestamp.h",

View File

@ -48,6 +48,7 @@
#include "mongo/bson/bsonelement.h"
#include "mongo/bson/bsonobj_comparator_interface.h"
#include "mongo/bson/bsontypes.h"
#include "mongo/db/pipeline/resume_token.h"
#include "mongo/scripting/mozjs/bson.h"
#include "mongo/scripting/mozjs/idwrapper.h"
#include "mongo/scripting/mozjs/implscope.h"

View File

@ -513,6 +513,7 @@ MozJSImplScope::MozJSImplScope(MozJSScriptEngine* engine, boost::optional<int> j
_objectProto(_context),
_oidProto(_context),
_regExpProto(_context),
_resumeTokenDataProto(_context),
_sessionProto(_context),
_statusProto(_context),
_timestampProto(_context),
@ -1134,6 +1135,7 @@ void MozJSImplScope::installBSONTypes() {
_numberDecimalProto.install(_global);
_oidProto.install(_global);
_regExpProto.install(_global);
_resumeTokenDataProto.install(_global);
_timestampProto.install(_global);
_uriProto.install(_global);
_statusProto.install(_global);

View File

@ -89,6 +89,7 @@
#include "mongo/scripting/mozjs/object.h"
#include "mongo/scripting/mozjs/oid.h"
#include "mongo/scripting/mozjs/regexp.h"
#include "mongo/scripting/mozjs/resumetoken.h"
#include "mongo/scripting/mozjs/session.h"
#include "mongo/scripting/mozjs/status.h"
#include "mongo/scripting/mozjs/timestamp.h"
@ -328,6 +329,12 @@ public:
return _regExpProto;
}
template <typename T>
typename std::enable_if<std::is_same<T, ResumeTokenDataUtility>::value, WrapType<T>&>::type
getProto() {
return _resumeTokenDataProto;
}
template <typename T>
typename std::enable_if<std::is_same<T, SessionInfo>::value, WrapType<T>&>::type getProto() {
return _sessionProto;
@ -543,6 +550,7 @@ private:
WrapType<ObjectInfo> _objectProto;
WrapType<OIDInfo> _oidProto;
WrapType<RegExpInfo> _regExpProto;
WrapType<ResumeTokenDataUtility> _resumeTokenDataProto;
WrapType<SessionInfo> _sessionProto;
WrapType<MongoStatusInfo> _statusProto;
WrapType<TimestampInfo> _timestampProto;

View File

@ -0,0 +1,98 @@
/**
* Copyright (C) 2024-present MongoDB, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* 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
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*
* 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 Server Side 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 <js/CallArgs.h>
#include <js/TypeDecls.h>
#include <string>
#include <js/PropertySpec.h>
#include "mongo/db/pipeline/resume_token.h"
#include "mongo/scripting/mozjs/bson.h"
#include "mongo/scripting/mozjs/wrapconstrainedmethod.h" // IWYU pragma: keep
namespace mongo {
namespace mozjs {
const JSFunctionSpec ResumeTokenDataUtility::freeFunctions[2] = {
MONGO_ATTACH_JS_FUNCTION(decodeResumeToken),
JS_FS_END,
};
const char* const ResumeTokenDataUtility::className = "ResumeTokenDataUtility";
constexpr const char* kHighWaterMarkResumeTokenType = "highWaterMarkResumeTokenType";
constexpr const char* kEventResumeTokenType = "eventResumeTokenType";
void ResumeTokenDataUtility::Functions::decodeResumeToken::call(JSContext* cx, JS::CallArgs args) {
uassert(ErrorCodes::BadValue, "decodeResumeToken takes 1 argument.", args.length() == 1);
BSONObj encodedResumeTokenBson = ValueWriter(cx, args.get(0)).toBSON();
const auto resumeTokenDataBson = ResumeToken::parse(encodedResumeTokenBson).getData().toBSON();
JS::RootedObject thisValue(cx);
auto scope = getScope(cx);
scope->getProto<BSONInfo>().newObject(&thisValue);
BSONInfo::make(cx, &thisValue, std::move(resumeTokenDataBson), nullptr, true);
args.rval().setObjectOrNull(thisValue);
}
void ResumeTokenDataUtility::postInstall(JSContext* cx,
JS::HandleObject global,
JS::HandleObject proto) {
JS::RootedValue undef(cx);
undef.setUndefined();
// The following code initialises two constants which will be visible in the global JS scope.
// The constants correspond to 'ResumeTokenData::TokenType' enum type values and
// they are used to improve readability of test assertions involving the 'tokenType' field.
// highWaterMarkResumeTokenType
if (!JS_DefineProperty(
cx,
global,
kHighWaterMarkResumeTokenType,
JS::RootedValue(cx, JS::Int32Value(ResumeTokenData::kHighWaterMarkToken)),
JSPROP_READONLY | JSPROP_PERMANENT)) {
uasserted(ErrorCodes::JSInterpreterFailure, "Failed to JS_DefineProperty");
}
// eventResumeTokenType
if (!JS_DefineProperty(cx,
global,
kEventResumeTokenType,
JS::RootedValue(cx, JS::Int32Value(ResumeTokenData::kEventToken)),
JSPROP_READONLY | JSPROP_PERMANENT)) {
uasserted(ErrorCodes::JSInterpreterFailure, "Failed to JS_DefineProperty");
}
}
} // namespace mozjs
} // namespace mongo

View File

@ -0,0 +1,58 @@
/**
* Copyright (C) 2024-present MongoDB, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* 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
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*
* 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 Server Side 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 <js/PropertySpec.h>
#include "mongo/scripting/mozjs/base.h"
#include "mongo/scripting/mozjs/wraptype.h"
namespace mongo {
namespace mozjs {
/**
* Utility class offering helper methods for managing
* resume tokens in JavaScript tests.
*/
struct ResumeTokenDataUtility : public BaseInfo {
struct Functions {
MONGO_DECLARE_JS_FUNCTION(decodeResumeToken);
};
static const JSFunctionSpec freeFunctions[2];
static const char* const className;
static void postInstall(JSContext* cx, JS::HandleObject global, JS::HandleObject proto);
};
} // namespace mozjs
} // namespace mongo