mirror of https://github.com/mongodb/mongo
490 lines
15 KiB
JavaScript
490 lines
15 KiB
JavaScript
/**
|
|
* Commands for change stream configuration testing.
|
|
* Defines all command classes that perform operations on database/collection states.
|
|
*
|
|
* Note: Random is a global object provided by the MongoDB shell.
|
|
*/
|
|
|
|
/**
|
|
* Sharding type constants.
|
|
*/
|
|
const ShardingType = {
|
|
RANGE: "range",
|
|
HASHED: "hashed",
|
|
};
|
|
|
|
/**
|
|
* Get the shard key spec for a given sharding type.
|
|
* Uses 'data' field which requires explicit index creation before sharding.
|
|
* @param {string} shardingType - ShardingType.RANGE or ShardingType.HASHED
|
|
* @returns {Object} The shard key spec, e.g. {data: 1} or {data: "hashed"}
|
|
*/
|
|
function getShardKeySpec(shardingType) {
|
|
return shardingType === ShardingType.HASHED ? {data: "hashed"} : {data: 1};
|
|
}
|
|
|
|
/**
|
|
* Base command class.
|
|
*/
|
|
class Command {
|
|
constructor(dbName, collName, shardSet) {
|
|
this.dbName = dbName;
|
|
this.collName = collName;
|
|
this.shardSet = shardSet;
|
|
}
|
|
|
|
/**
|
|
* Execute the command.
|
|
* @param {Object} connection - The MongoDB connection.
|
|
*/
|
|
execute(connection) {
|
|
throw new Error("execute() method must be implemented by subclasses");
|
|
}
|
|
|
|
/**
|
|
* String representation of the command.
|
|
*/
|
|
toString() {
|
|
throw new Error("toString() method must be implemented by subclasses");
|
|
}
|
|
|
|
/**
|
|
* Get change event for this command.
|
|
* TODO: Implement change event generation for each command type.
|
|
*/
|
|
getChangeEvents() {
|
|
throw new Error("getChangeEvents() not implemented yet");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert document command.
|
|
* TODO: SERVER-114857 - This command needs to know if the collection already exists.
|
|
* The insertion behavior may differ depending on whether the collection is already created.
|
|
*/
|
|
class InsertDocCommand extends Command {
|
|
constructor(dbName, collName, shardSet, collectionCtx) {
|
|
super(dbName, collName, shardSet);
|
|
// Store context state needed for event matching
|
|
this.collectionExists = collectionCtx.exists;
|
|
this.collectionNonEmpty = collectionCtx.nonEmpty;
|
|
// Create the document in the constructor so it can be used by both execute() and getChangeEvents()
|
|
this.document = {
|
|
_id: new ObjectId(),
|
|
timestamp: new Date(),
|
|
data: `test_data`,
|
|
};
|
|
}
|
|
|
|
execute(connection) {
|
|
assert.commandWorked(connection.getDB(this.dbName).getCollection(this.collName).insertOne(this.document));
|
|
}
|
|
|
|
toString() {
|
|
return "InsertDocCommand";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create database command.
|
|
*/
|
|
class CreateDatabaseCommand extends Command {
|
|
execute(connection) {
|
|
assert.commandWorked(connection.adminCommand({enableSharding: this.dbName}));
|
|
}
|
|
|
|
toString() {
|
|
return "CreateDatabaseCommand";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create unsplittable collection command.
|
|
*/
|
|
class CreateUnsplittableCollectionCommand extends Command {
|
|
execute(connection) {
|
|
assert(
|
|
this.shardSet && this.shardSet.length > 0,
|
|
"Shard set must be provided for CreateUnsplittableCollectionCommand",
|
|
);
|
|
const targetShard = this.shardSet[Random.randInt(this.shardSet.length)];
|
|
|
|
const db = connection.getDB(this.dbName);
|
|
assert.commandWorked(
|
|
db.runCommand({
|
|
createUnsplittableCollection: this.collName,
|
|
dataShard: targetShard._id,
|
|
}),
|
|
);
|
|
}
|
|
|
|
toString() {
|
|
return "CreateUnsplittableCollectionCommand";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create untracked collection command.
|
|
*/
|
|
class CreateUntrackedCollectionCommand extends Command {
|
|
execute(connection) {
|
|
assert.commandWorked(connection.getDB(this.dbName).createCollection(this.collName));
|
|
}
|
|
|
|
toString() {
|
|
return "CreateUntrackedCollectionCommand";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Drop collection command.
|
|
*/
|
|
class DropCollectionCommand extends Command {
|
|
execute(connection) {
|
|
const db = connection.getDB(this.dbName);
|
|
assert.commandWorked(db.runCommand({drop: this.collName}));
|
|
}
|
|
|
|
toString() {
|
|
return "DropCollectionCommand";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Drop database command.
|
|
*/
|
|
class DropDatabaseCommand extends Command {
|
|
execute(connection) {
|
|
const db = connection.getDB(this.dbName);
|
|
assert.commandWorked(db.dropDatabase());
|
|
}
|
|
|
|
toString() {
|
|
return "DropDatabaseCommand";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create index command.
|
|
* Creates an index for the shard key (required before sharding).
|
|
* Precondition (guaranteed by FSM): collection exists.
|
|
*/
|
|
class CreateIndexCommand extends Command {
|
|
constructor(dbName, collName, shardSet, collectionCtx) {
|
|
super(dbName, collName, shardSet);
|
|
assert(collectionCtx.shardKeySpec, "shardKeySpec must be provided to CreateIndexCommand");
|
|
this.shardKeySpec = collectionCtx.shardKeySpec;
|
|
}
|
|
|
|
execute(connection) {
|
|
const coll = connection.getDB(this.dbName).getCollection(this.collName);
|
|
assert.commandWorked(coll.createIndex(this.shardKeySpec));
|
|
}
|
|
|
|
toString() {
|
|
return `CreateIndexCommand(${tojson(this.shardKeySpec)})`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Drop index command.
|
|
* Drops the shard key index (cleanup after resharding).
|
|
* Preconditions (guaranteed by generator): collection exists, index exists.
|
|
*/
|
|
class DropIndexCommand extends Command {
|
|
constructor(dbName, collName, shardSet, collectionCtx) {
|
|
super(dbName, collName, shardSet);
|
|
assert(collectionCtx.shardKeySpec, "shardKeySpec must be provided to DropIndexCommand");
|
|
this.shardKeySpec = collectionCtx.shardKeySpec;
|
|
}
|
|
|
|
execute(connection) {
|
|
const coll = connection.getDB(this.dbName).getCollection(this.collName);
|
|
assert.commandWorked(coll.dropIndex(this.shardKeySpec));
|
|
}
|
|
|
|
toString() {
|
|
return `DropIndexCommand(${tojson(this.shardKeySpec)})`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shard existing collection command.
|
|
* Sharding type (range vs hashed) is determined by the shardKeySpec in collectionCtx.
|
|
* Preconditions (guaranteed by FSM): collection exists, shard key index exists.
|
|
*/
|
|
class ShardCollectionCommand extends Command {
|
|
constructor(dbName, collName, shardSet, collectionCtx) {
|
|
super(dbName, collName, shardSet);
|
|
assert(collectionCtx.shardKeySpec, "shardKeySpec must be provided to ShardCollectionCommand");
|
|
this.shardKeySpec = collectionCtx.shardKeySpec;
|
|
}
|
|
|
|
execute(connection) {
|
|
const ns = `${this.dbName}.${this.collName}`;
|
|
|
|
// Shard the collection
|
|
assert.commandWorked(
|
|
connection.adminCommand({
|
|
shardCollection: ns,
|
|
key: this.shardKeySpec,
|
|
}),
|
|
);
|
|
}
|
|
|
|
toString() {
|
|
const type = Object.values(this.shardKeySpec).some((v) => v === "hashed") ? "hashed" : "range";
|
|
return `ShardCollectionCommand(${type})`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unshard collection command.
|
|
* Converts a sharded collection to an unsplittable (single-shard) collection.
|
|
* Picks a random shard as the destination.
|
|
* Precondition (guaranteed by FSM): collection exists and is sharded.
|
|
*/
|
|
class UnshardCollectionCommand extends Command {
|
|
execute(connection) {
|
|
assert(this.shardSet && this.shardSet.length > 0, "Shard set must be provided for UnshardCollectionCommand");
|
|
const targetShard = this.shardSet[Random.randInt(this.shardSet.length)];
|
|
const ns = `${this.dbName}.${this.collName}`;
|
|
assert.commandWorked(
|
|
connection.adminCommand({
|
|
unshardCollection: ns,
|
|
toShard: targetShard._id,
|
|
}),
|
|
);
|
|
}
|
|
|
|
toString() {
|
|
return "UnshardCollectionCommand";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reshard collection command.
|
|
* Resharding type (range vs hashed) is determined by shardKeySpec in collectionCtx.
|
|
* Precondition (guaranteed by FSM): collection exists and is sharded, index exists.
|
|
*/
|
|
class ReshardCollectionCommand extends Command {
|
|
constructor(dbName, collName, shardSet, collectionCtx) {
|
|
super(dbName, collName, shardSet);
|
|
assert(collectionCtx.shardKeySpec, "shardKeySpec must be provided to ReshardCollectionCommand");
|
|
this.shardKeySpec = collectionCtx.shardKeySpec;
|
|
}
|
|
|
|
execute(connection) {
|
|
const ns = `${this.dbName}.${this.collName}`;
|
|
|
|
// Use numInitialChunks: 1 to avoid cardinality errors in test environments with little data.
|
|
assert.commandWorked(
|
|
connection.adminCommand({
|
|
reshardCollection: ns,
|
|
key: this.shardKeySpec,
|
|
numInitialChunks: 1,
|
|
}),
|
|
);
|
|
}
|
|
|
|
toString() {
|
|
const type = Object.values(this.shardKeySpec).some((v) => v === "hashed") ? "hashed" : "range";
|
|
return `ReshardCollectionCommand(${type})`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base rename collection command.
|
|
* Subclasses define targetShouldExist and crossDatabase flags.
|
|
*/
|
|
class RenameCommand extends Command {
|
|
// Subclasses must set: this.targetShouldExist, this.crossDatabase
|
|
|
|
execute(connection) {
|
|
const targetDb = this.crossDatabase ? `${this.dbName}_target` : this.dbName;
|
|
const targetColl = `${this.collName}_renamed`;
|
|
|
|
assert.commandWorked(
|
|
connection.adminCommand({
|
|
renameCollection: `${this.dbName}.${this.collName}`,
|
|
to: `${targetDb}.${targetColl}`,
|
|
}),
|
|
);
|
|
|
|
// Drop the renamed collection to ensure the target doesn't already exist for subsequent renames
|
|
assert.commandWorked(connection.getDB(targetDb).runCommand({drop: targetColl}));
|
|
}
|
|
|
|
toString() {
|
|
const targetType = this.targetShouldExist ? "Existent" : "NonExistent";
|
|
const dbType = this.crossDatabase ? "DifferentDb" : "SameDb";
|
|
return `RenameTo${targetType}${dbType}Command`;
|
|
}
|
|
}
|
|
|
|
// Concrete rename command classes.
|
|
class RenameToNonExistentSameDbCommand extends RenameCommand {
|
|
constructor(dbName, collName, shardSet) {
|
|
super(dbName, collName, shardSet);
|
|
this.targetShouldExist = false;
|
|
this.crossDatabase = false;
|
|
}
|
|
}
|
|
|
|
class RenameToExistentSameDbCommand extends RenameCommand {
|
|
constructor(dbName, collName, shardSet) {
|
|
super(dbName, collName, shardSet);
|
|
this.targetShouldExist = true;
|
|
this.crossDatabase = false;
|
|
}
|
|
}
|
|
|
|
class RenameToNonExistentDifferentDbCommand extends RenameCommand {
|
|
constructor(dbName, collName, shardSet) {
|
|
super(dbName, collName, shardSet);
|
|
this.targetShouldExist = false;
|
|
this.crossDatabase = true;
|
|
}
|
|
}
|
|
|
|
class RenameToExistentDifferentDbCommand extends RenameCommand {
|
|
constructor(dbName, collName, shardSet) {
|
|
super(dbName, collName, shardSet);
|
|
this.targetShouldExist = true;
|
|
this.crossDatabase = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Base class for move operations.
|
|
* Handles shard selection and provides common move functionality.
|
|
*/
|
|
class MoveCommandBase extends Command {
|
|
/**
|
|
* Get the target shard for the move operation.
|
|
* @param {Object} connection - The MongoDB connection.
|
|
* @returns {string|null} The target shard ID, or null if no suitable shard exists.
|
|
*/
|
|
_getTargetShard(connection) {
|
|
throw new Error("_getTargetShard() method must be implemented by subclasses");
|
|
}
|
|
|
|
/**
|
|
* Build the move command to execute.
|
|
* @param {string} targetShardId - The target shard ID.
|
|
* @returns {Object} The command object to send to adminCommand.
|
|
*/
|
|
_buildMoveCommand(targetShardId) {
|
|
throw new Error("_buildMoveCommand() method must be implemented by subclasses");
|
|
}
|
|
|
|
execute(connection) {
|
|
// No-op: Move operations are not critical for state machine testing since
|
|
// sharding is not actually set up.
|
|
// In a real implementation, this would execute:
|
|
// const targetShardId = this._getTargetShard(connection);
|
|
// const moveCommand = this._buildMoveCommand(targetShardId);
|
|
// assert.commandWorked(connection.adminCommand(moveCommand));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move primary command.
|
|
*/
|
|
class MovePrimaryCommand extends MoveCommandBase {
|
|
_getTargetShard(connection) {
|
|
assert(this.shardSet && this.shardSet.length > 0, "Shard set must be provided for MovePrimaryCommand");
|
|
assert.gt(this.shardSet.length, 1, "MovePrimaryCommand requires at least 2 shards");
|
|
const dbInfo = assert.commandWorked(connection.adminCommand({getDatabaseVersion: this.dbName}));
|
|
const otherShards = this.shardSet.filter((s) => s._id !== dbInfo.primary);
|
|
assert.gt(
|
|
otherShards.length,
|
|
0,
|
|
`MovePrimaryCommand requires at least one shard other than primary (${dbInfo.primary})`,
|
|
);
|
|
return otherShards[Random.randInt(otherShards.length)]._id;
|
|
}
|
|
|
|
_buildMoveCommand(targetShardId) {
|
|
return {
|
|
movePrimary: this.dbName,
|
|
to: targetShardId,
|
|
};
|
|
}
|
|
|
|
toString() {
|
|
return "MovePrimaryCommand";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move collection command.
|
|
*/
|
|
class MoveCollectionCommand extends MoveCommandBase {
|
|
_getTargetShard(connection) {
|
|
assert(this.shardSet && this.shardSet.length > 0, "Shard set must be provided for MoveCollectionCommand");
|
|
assert.gt(this.shardSet.length, 1, "MoveCollectionCommand requires at least 2 shards");
|
|
return this.shardSet[Random.randInt(this.shardSet.length)]._id;
|
|
}
|
|
|
|
_buildMoveCommand(targetShardId) {
|
|
return {
|
|
moveCollection: `${this.dbName}.${this.collName}`,
|
|
toShard: targetShardId,
|
|
};
|
|
}
|
|
|
|
toString() {
|
|
return "MoveCollectionCommand";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Move chunk command.
|
|
* TODO: SERVER-114858 - Improve chunk selection logic.
|
|
*/
|
|
class MoveChunkCommand extends MoveCommandBase {
|
|
_getTargetShard(connection) {
|
|
assert(this.shardSet && this.shardSet.length > 0, "Shard set must be provided for MoveChunkCommand");
|
|
assert.gt(this.shardSet.length, 1, "MoveChunkCommand requires at least 2 shards");
|
|
return this.shardSet[Random.randInt(this.shardSet.length)]._id;
|
|
}
|
|
|
|
_buildMoveCommand(targetShardId) {
|
|
return {
|
|
moveChunk: `${this.dbName}.${this.collName}`,
|
|
find: {_id: MinKey},
|
|
to: targetShardId,
|
|
};
|
|
}
|
|
|
|
toString() {
|
|
return "MoveChunkCommand";
|
|
}
|
|
}
|
|
|
|
// Export classes.
|
|
export {
|
|
Command,
|
|
InsertDocCommand,
|
|
CreateDatabaseCommand,
|
|
CreateIndexCommand,
|
|
DropIndexCommand,
|
|
ShardCollectionCommand,
|
|
CreateUnsplittableCollectionCommand,
|
|
CreateUntrackedCollectionCommand,
|
|
DropCollectionCommand,
|
|
DropDatabaseCommand,
|
|
RenameToNonExistentSameDbCommand,
|
|
RenameToExistentSameDbCommand,
|
|
RenameToNonExistentDifferentDbCommand,
|
|
RenameToExistentDifferentDbCommand,
|
|
UnshardCollectionCommand,
|
|
ReshardCollectionCommand,
|
|
MovePrimaryCommand,
|
|
MoveCollectionCommand,
|
|
MoveChunkCommand,
|
|
ShardingType,
|
|
getShardKeySpec,
|
|
};
|