mirror of https://github.com/mongodb/mongo
SERVER-114857 create sharded collection + reshard collection command (#45008)
Co-authored-by: Denis Grebennicov <denis.grebennicov@mongodb.com> Co-authored-by: Jan <jsteemann@users.noreply.github.com> GitOrigin-RevId: 2e777a54831a66a500e8fcb41d2b4dcc8d3a7c4f
This commit is contained in:
parent
8febf70330
commit
fa0ac8e778
|
|
@ -4,21 +4,24 @@
|
|||
class Action {
|
||||
static INSERT_DOC = 0;
|
||||
static CREATE_DATABASE = 1;
|
||||
static CREATE_SHARDED_COLLECTION = 2;
|
||||
static CREATE_UNSPLITTABLE_COLLECTION = 3;
|
||||
static CREATE_UNTRACKED_COLLECTION = 4;
|
||||
static DROP_COLLECTION = 5;
|
||||
static DROP_DATABASE = 6;
|
||||
static RENAME_TO_NON_EXISTENT_SAME_DB = 7;
|
||||
static RENAME_TO_EXISTENT_SAME_DB = 8;
|
||||
static RENAME_TO_NON_EXISTENT_DIFFERENT_DB = 9;
|
||||
static RENAME_TO_EXISTENT_DIFFERENT_DB = 10;
|
||||
static SHARD_COLLECTION = 11;
|
||||
static UNSHARD_COLLECTION = 12;
|
||||
static RESHARD_COLLECTION = 13;
|
||||
static MOVE_PRIMARY = 14;
|
||||
static MOVE_COLLECTION = 15;
|
||||
static MOVE_CHUNK = 16;
|
||||
static CREATE_SHARDED_COLLECTION_RANGE = 2;
|
||||
static CREATE_SHARDED_COLLECTION_HASHED = 3;
|
||||
static CREATE_UNSPLITTABLE_COLLECTION = 4;
|
||||
static CREATE_UNTRACKED_COLLECTION = 5;
|
||||
static DROP_COLLECTION = 6;
|
||||
static DROP_DATABASE = 7;
|
||||
static RENAME_TO_NON_EXISTENT_SAME_DB = 8;
|
||||
static RENAME_TO_EXISTENT_SAME_DB = 9;
|
||||
static RENAME_TO_NON_EXISTENT_DIFFERENT_DB = 10;
|
||||
static RENAME_TO_EXISTENT_DIFFERENT_DB = 11;
|
||||
static SHARD_COLLECTION_RANGE = 12;
|
||||
static SHARD_COLLECTION_HASHED = 13;
|
||||
static UNSHARD_COLLECTION = 14;
|
||||
static RESHARD_COLLECTION_TO_RANGE = 15;
|
||||
static RESHARD_COLLECTION_TO_HASHED = 16;
|
||||
static MOVE_PRIMARY = 17;
|
||||
static MOVE_COLLECTION = 18;
|
||||
static MOVE_CHUNK = 19;
|
||||
|
||||
static getName(actionId) {
|
||||
switch (actionId) {
|
||||
|
|
@ -26,8 +29,10 @@ class Action {
|
|||
return "insert doc";
|
||||
case Action.CREATE_DATABASE:
|
||||
return "create database";
|
||||
case Action.CREATE_SHARDED_COLLECTION:
|
||||
return "create sharded collection";
|
||||
case Action.CREATE_SHARDED_COLLECTION_RANGE:
|
||||
return "create sharded collection (range)";
|
||||
case Action.CREATE_SHARDED_COLLECTION_HASHED:
|
||||
return "create sharded collection (hashed)";
|
||||
case Action.CREATE_UNSPLITTABLE_COLLECTION:
|
||||
return "create unsplittable collection";
|
||||
case Action.CREATE_UNTRACKED_COLLECTION:
|
||||
|
|
@ -44,12 +49,16 @@ class Action {
|
|||
return "rename to non-existent collection different database";
|
||||
case Action.RENAME_TO_EXISTENT_DIFFERENT_DB:
|
||||
return "rename to existent collection different database";
|
||||
case Action.SHARD_COLLECTION:
|
||||
return "shard collection";
|
||||
case Action.SHARD_COLLECTION_RANGE:
|
||||
return "shard collection (range)";
|
||||
case Action.SHARD_COLLECTION_HASHED:
|
||||
return "shard collection (hashed)";
|
||||
case Action.UNSHARD_COLLECTION:
|
||||
return "unshard collection";
|
||||
case Action.RESHARD_COLLECTION:
|
||||
return "reshard collection";
|
||||
case Action.RESHARD_COLLECTION_TO_RANGE:
|
||||
return "reshard collection (range)";
|
||||
case Action.RESHARD_COLLECTION_TO_HASHED:
|
||||
return "reshard collection (hashed)";
|
||||
case Action.MOVE_PRIMARY:
|
||||
return "move primary";
|
||||
case Action.MOVE_COLLECTION:
|
||||
|
|
@ -60,6 +69,18 @@ class Action {
|
|||
throw new Error(`Invalid action ID: ${actionId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all action IDs.
|
||||
* @returns {Array<number>} Array of all action IDs.
|
||||
*/
|
||||
static getAllActionIds() {
|
||||
// Static class fields are not enumerable, so Object.values() won't work.
|
||||
// Use getOwnPropertyNames and filter for numeric values.
|
||||
return Object.getOwnPropertyNames(Action)
|
||||
.map((name) => Action[name])
|
||||
.filter((value) => typeof value === "number");
|
||||
}
|
||||
}
|
||||
|
||||
export {Action};
|
||||
|
|
|
|||
|
|
@ -33,7 +33,8 @@ class CollectionTestModel {
|
|||
const states = [
|
||||
State.DATABASE_ABSENT,
|
||||
State.DATABASE_PRESENT_COLLECTION_ABSENT,
|
||||
State.COLLECTION_PRESENT_SHARDED,
|
||||
State.COLLECTION_PRESENT_SHARDED_RANGE,
|
||||
State.COLLECTION_PRESENT_SHARDED_HASHED,
|
||||
State.COLLECTION_PRESENT_UNSPLITTABLE,
|
||||
State.COLLECTION_PRESENT_UNTRACKED,
|
||||
];
|
||||
|
|
@ -51,25 +52,43 @@ class CollectionTestModel {
|
|||
// ===== DATABASE_PRESENT_COLLECTION_ABSENT state transitions =====
|
||||
this.setActions(State.DATABASE_PRESENT_COLLECTION_ABSENT, [
|
||||
[Action.INSERT_DOC, State.COLLECTION_PRESENT_UNTRACKED],
|
||||
[Action.CREATE_SHARDED_COLLECTION, State.COLLECTION_PRESENT_SHARDED], // TODO: SERVER-114857 - No-op for now
|
||||
[Action.CREATE_SHARDED_COLLECTION_RANGE, State.COLLECTION_PRESENT_SHARDED_RANGE],
|
||||
[Action.CREATE_SHARDED_COLLECTION_HASHED, State.COLLECTION_PRESENT_SHARDED_HASHED],
|
||||
[Action.CREATE_UNSPLITTABLE_COLLECTION, State.COLLECTION_PRESENT_UNSPLITTABLE],
|
||||
[Action.CREATE_UNTRACKED_COLLECTION, State.COLLECTION_PRESENT_UNTRACKED],
|
||||
[Action.DROP_DATABASE, State.DATABASE_ABSENT],
|
||||
[Action.MOVE_PRIMARY, State.DATABASE_PRESENT_COLLECTION_ABSENT],
|
||||
]);
|
||||
|
||||
// ===== COLLECTION_PRESENT_SHARDED state transitions =====
|
||||
this.setActions(State.COLLECTION_PRESENT_SHARDED, [
|
||||
[Action.INSERT_DOC, State.COLLECTION_PRESENT_SHARDED],
|
||||
// ===== COLLECTION_PRESENT_SHARDED_RANGE state transitions =====
|
||||
this.setActions(State.COLLECTION_PRESENT_SHARDED_RANGE, [
|
||||
[Action.INSERT_DOC, State.COLLECTION_PRESENT_SHARDED_RANGE],
|
||||
[Action.DROP_COLLECTION, State.DATABASE_PRESENT_COLLECTION_ABSENT],
|
||||
[Action.DROP_DATABASE, State.DATABASE_ABSENT],
|
||||
[Action.RENAME_TO_NON_EXISTENT_SAME_DB, State.DATABASE_PRESENT_COLLECTION_ABSENT],
|
||||
[Action.RENAME_TO_EXISTENT_SAME_DB, State.DATABASE_PRESENT_COLLECTION_ABSENT],
|
||||
// Cross-database renames not supported for sharded collections.
|
||||
[Action.UNSHARD_COLLECTION, State.COLLECTION_PRESENT_UNSPLITTABLE],
|
||||
[Action.RESHARD_COLLECTION, State.COLLECTION_PRESENT_SHARDED], // TODO: SERVER-114857 - No-op for now
|
||||
[Action.MOVE_PRIMARY, State.COLLECTION_PRESENT_SHARDED],
|
||||
[Action.MOVE_CHUNK, State.COLLECTION_PRESENT_SHARDED],
|
||||
[Action.RESHARD_COLLECTION_TO_RANGE, State.COLLECTION_PRESENT_SHARDED_RANGE],
|
||||
[Action.RESHARD_COLLECTION_TO_HASHED, State.COLLECTION_PRESENT_SHARDED_HASHED],
|
||||
[Action.MOVE_PRIMARY, State.COLLECTION_PRESENT_SHARDED_RANGE],
|
||||
[Action.MOVE_CHUNK, State.COLLECTION_PRESENT_SHARDED_RANGE],
|
||||
// MOVE_COLLECTION only works on unsharded collections.
|
||||
]);
|
||||
|
||||
// ===== COLLECTION_PRESENT_SHARDED_HASHED state transitions =====
|
||||
this.setActions(State.COLLECTION_PRESENT_SHARDED_HASHED, [
|
||||
[Action.INSERT_DOC, State.COLLECTION_PRESENT_SHARDED_HASHED],
|
||||
[Action.DROP_COLLECTION, State.DATABASE_PRESENT_COLLECTION_ABSENT],
|
||||
[Action.DROP_DATABASE, State.DATABASE_ABSENT],
|
||||
[Action.RENAME_TO_NON_EXISTENT_SAME_DB, State.DATABASE_PRESENT_COLLECTION_ABSENT],
|
||||
[Action.RENAME_TO_EXISTENT_SAME_DB, State.DATABASE_PRESENT_COLLECTION_ABSENT],
|
||||
// Cross-database renames not supported for sharded collections.
|
||||
[Action.UNSHARD_COLLECTION, State.COLLECTION_PRESENT_UNSPLITTABLE],
|
||||
[Action.RESHARD_COLLECTION_TO_RANGE, State.COLLECTION_PRESENT_SHARDED_RANGE],
|
||||
[Action.RESHARD_COLLECTION_TO_HASHED, State.COLLECTION_PRESENT_SHARDED_HASHED],
|
||||
[Action.MOVE_PRIMARY, State.COLLECTION_PRESENT_SHARDED_HASHED],
|
||||
[Action.MOVE_CHUNK, State.COLLECTION_PRESENT_SHARDED_HASHED],
|
||||
// MOVE_COLLECTION only works on unsharded collections.
|
||||
]);
|
||||
|
||||
|
|
@ -81,7 +100,8 @@ class CollectionTestModel {
|
|||
[Action.RENAME_TO_NON_EXISTENT_SAME_DB, State.DATABASE_PRESENT_COLLECTION_ABSENT],
|
||||
[Action.RENAME_TO_EXISTENT_SAME_DB, State.DATABASE_PRESENT_COLLECTION_ABSENT],
|
||||
// Cross-database renames not supported for tracked collections.
|
||||
[Action.SHARD_COLLECTION, State.COLLECTION_PRESENT_SHARDED], // TODO: SERVER-114857 - No-op for now
|
||||
[Action.SHARD_COLLECTION_RANGE, State.COLLECTION_PRESENT_SHARDED_RANGE],
|
||||
[Action.SHARD_COLLECTION_HASHED, State.COLLECTION_PRESENT_SHARDED_HASHED],
|
||||
[Action.MOVE_PRIMARY, State.COLLECTION_PRESENT_UNSPLITTABLE],
|
||||
[Action.MOVE_COLLECTION, State.COLLECTION_PRESENT_UNSPLITTABLE],
|
||||
]);
|
||||
|
|
@ -94,7 +114,8 @@ class CollectionTestModel {
|
|||
[Action.RENAME_TO_NON_EXISTENT_SAME_DB, State.DATABASE_PRESENT_COLLECTION_ABSENT],
|
||||
[Action.RENAME_TO_EXISTENT_SAME_DB, State.DATABASE_PRESENT_COLLECTION_ABSENT],
|
||||
// Cross-database renames require source and target on same shard.
|
||||
[Action.SHARD_COLLECTION, State.COLLECTION_PRESENT_SHARDED], // TODO: SERVER-114857 - No-op for now
|
||||
[Action.SHARD_COLLECTION_RANGE, State.COLLECTION_PRESENT_SHARDED_RANGE],
|
||||
[Action.SHARD_COLLECTION_HASHED, State.COLLECTION_PRESENT_SHARDED_HASHED],
|
||||
[Action.MOVE_PRIMARY, State.COLLECTION_PRESENT_UNTRACKED],
|
||||
[Action.MOVE_COLLECTION, State.COLLECTION_PRESENT_UNTRACKED],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,24 @@
|
|||
* 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.
|
||||
*/
|
||||
|
|
@ -45,8 +63,11 @@ class Command {
|
|||
* The insertion behavior may differ depending on whether the collection is already created.
|
||||
*/
|
||||
class InsertDocCommand extends Command {
|
||||
constructor(dbName, collName, shardSet) {
|
||||
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(),
|
||||
|
|
@ -144,32 +165,96 @@ class DropDatabaseCommand extends Command {
|
|||
}
|
||||
|
||||
/**
|
||||
* Shard collection command.
|
||||
* TODO: SERVER-114857 - For now, we create a normal untracked collection instead of
|
||||
* actually sharding it. This allows subsequent operations (rename, drop, etc.) to work
|
||||
* on an existing collection. Full sharding setup will be addressed in SERVER-114857.
|
||||
* Create index command.
|
||||
* Creates an index for the shard key (required before sharding).
|
||||
* Precondition (guaranteed by FSM): collection exists.
|
||||
*/
|
||||
class CreateShardedCollectionCommand extends Command {
|
||||
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) {
|
||||
// Create a normal untracked collection as a workaround
|
||||
// TODO: SERVER-114857 - This should actually shard the collection
|
||||
assert.commandWorked(connection.getDB(this.dbName).createCollection(this.collName));
|
||||
const coll = connection.getDB(this.dbName).getCollection(this.collName);
|
||||
assert.commandWorked(coll.createIndex(this.shardKeySpec));
|
||||
}
|
||||
|
||||
toString() {
|
||||
return "CreateShardedCollectionCommand";
|
||||
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.
|
||||
* TODO: SERVER-114857 - This is a no-op simulation for testing.
|
||||
* Since sharding is not actually set up, unsharding is also a no-op.
|
||||
* 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) {
|
||||
// No-op: Unsharding simulation left blank since sharding is not actually set up.
|
||||
// The state machine transition still occurs correctly.
|
||||
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() {
|
||||
|
|
@ -178,16 +263,42 @@ class UnshardCollectionCommand extends Command {
|
|||
}
|
||||
|
||||
/**
|
||||
* Unified rename collection command.
|
||||
* Handles all rename scenarios based on configuration.
|
||||
* 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 {
|
||||
constructor(dbName, collName, shardSet, targetShouldExist, crossDatabase) {
|
||||
super(dbName, collName, shardSet);
|
||||
// targetShouldExist is kept for distinguishing command types but not used in execution
|
||||
this.targetShouldExist = targetShouldExist;
|
||||
this.crossDatabase = crossDatabase;
|
||||
}
|
||||
// Subclasses must set: this.targetShouldExist, this.crossDatabase
|
||||
|
||||
execute(connection) {
|
||||
const targetDb = this.crossDatabase ? `${this.dbName}_target` : this.dbName;
|
||||
|
|
@ -211,45 +322,36 @@ class RenameCommand extends Command {
|
|||
}
|
||||
}
|
||||
|
||||
// Concrete rename command classes for backward compatibility.
|
||||
// Concrete rename command classes.
|
||||
class RenameToNonExistentSameDbCommand extends RenameCommand {
|
||||
constructor(dbName, collName, shardSet) {
|
||||
super(dbName, collName, shardSet, false, false);
|
||||
super(dbName, collName, shardSet);
|
||||
this.targetShouldExist = false;
|
||||
this.crossDatabase = false;
|
||||
}
|
||||
}
|
||||
|
||||
class RenameToExistentSameDbCommand extends RenameCommand {
|
||||
constructor(dbName, collName, shardSet) {
|
||||
super(dbName, collName, shardSet, true, false);
|
||||
super(dbName, collName, shardSet);
|
||||
this.targetShouldExist = true;
|
||||
this.crossDatabase = false;
|
||||
}
|
||||
}
|
||||
|
||||
class RenameToNonExistentDifferentDbCommand extends RenameCommand {
|
||||
constructor(dbName, collName, shardSet) {
|
||||
super(dbName, collName, shardSet, false, true);
|
||||
super(dbName, collName, shardSet);
|
||||
this.targetShouldExist = false;
|
||||
this.crossDatabase = true;
|
||||
}
|
||||
}
|
||||
|
||||
class RenameToExistentDifferentDbCommand extends RenameCommand {
|
||||
constructor(dbName, collName, shardSet) {
|
||||
super(dbName, collName, shardSet, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reshard collection command.
|
||||
* TODO: SERVER-114857 - This is a no-op simulation for testing.
|
||||
* Resharding is complex and changes the shard key, which is not critical for
|
||||
* state machine testing. The collection remains in the sharded state.
|
||||
*/
|
||||
class ReshardCollectionCommand extends Command {
|
||||
execute(connection) {
|
||||
// No-op: Resharding simulation left blank as it's not critical for state machine testing.
|
||||
// The collection stays sharded, which is sufficient for the state machine model.
|
||||
}
|
||||
|
||||
toString() {
|
||||
return "ReshardCollectionCommand";
|
||||
super(dbName, collName, shardSet);
|
||||
this.targetShouldExist = true;
|
||||
this.crossDatabase = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -278,7 +380,7 @@ class MoveCommandBase extends Command {
|
|||
|
||||
execute(connection) {
|
||||
// No-op: Move operations are not critical for state machine testing since
|
||||
// sharding is not actually set up (SERVER-114857).
|
||||
// sharding is not actually set up.
|
||||
// In a real implementation, this would execute:
|
||||
// const targetShardId = this._getTargetShard(connection);
|
||||
// const moveCommand = this._buildMoveCommand(targetShardId);
|
||||
|
|
@ -366,20 +468,22 @@ export {
|
|||
Command,
|
||||
InsertDocCommand,
|
||||
CreateDatabaseCommand,
|
||||
CreateShardedCollectionCommand,
|
||||
CreateIndexCommand,
|
||||
DropIndexCommand,
|
||||
ShardCollectionCommand,
|
||||
CreateUnsplittableCollectionCommand,
|
||||
CreateUntrackedCollectionCommand,
|
||||
DropCollectionCommand,
|
||||
DropDatabaseCommand,
|
||||
RenameCommand,
|
||||
RenameToNonExistentSameDbCommand,
|
||||
RenameToExistentSameDbCommand,
|
||||
RenameToNonExistentDifferentDbCommand,
|
||||
RenameToExistentDifferentDbCommand,
|
||||
UnshardCollectionCommand,
|
||||
ReshardCollectionCommand,
|
||||
MoveCommandBase,
|
||||
MovePrimaryCommand,
|
||||
MoveCollectionCommand,
|
||||
MoveChunkCommand,
|
||||
ShardingType,
|
||||
getShardKeySpec,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,16 +4,16 @@
|
|||
* Requires an explicit seed to ensure deterministic behavior.
|
||||
*/
|
||||
import {Action} from "jstests/libs/util/change_stream/change_stream_action.js";
|
||||
import {State} from "jstests/libs/util/change_stream/change_stream_state.js";
|
||||
import {ShardingCommandGeneratorParams} from "jstests/libs/util/change_stream/change_stream_sharding_command_generator_params.js";
|
||||
import {
|
||||
InsertDocCommand,
|
||||
CreateDatabaseCommand,
|
||||
CreateShardedCollectionCommand,
|
||||
ShardCollectionCommand,
|
||||
CreateUnsplittableCollectionCommand,
|
||||
CreateUntrackedCollectionCommand,
|
||||
DropCollectionCommand,
|
||||
DropDatabaseCommand,
|
||||
RenameCommand,
|
||||
RenameToNonExistentSameDbCommand,
|
||||
RenameToExistentSameDbCommand,
|
||||
RenameToNonExistentDifferentDbCommand,
|
||||
|
|
@ -23,30 +23,37 @@ import {
|
|||
MovePrimaryCommand,
|
||||
MoveCollectionCommand,
|
||||
MoveChunkCommand,
|
||||
CreateIndexCommand,
|
||||
DropIndexCommand,
|
||||
ShardingType,
|
||||
getShardKeySpec,
|
||||
} from "jstests/libs/util/change_stream/change_stream_commands.js";
|
||||
|
||||
// Maps action IDs to command classes.
|
||||
const actionToCommandClass = {
|
||||
[Action.INSERT_DOC]: InsertDocCommand,
|
||||
[Action.CREATE_DATABASE]: CreateDatabaseCommand,
|
||||
[Action.CREATE_SHARDED_COLLECTION]: CreateShardedCollectionCommand,
|
||||
[Action.CREATE_UNSPLITTABLE_COLLECTION]: CreateUnsplittableCollectionCommand,
|
||||
[Action.CREATE_UNTRACKED_COLLECTION]: CreateUntrackedCollectionCommand,
|
||||
[Action.DROP_COLLECTION]: DropCollectionCommand,
|
||||
[Action.DROP_DATABASE]: DropDatabaseCommand,
|
||||
[Action.RENAME_TO_NON_EXISTENT_SAME_DB]: RenameToNonExistentSameDbCommand,
|
||||
[Action.RENAME_TO_EXISTENT_SAME_DB]: RenameToExistentSameDbCommand,
|
||||
[Action.RENAME_TO_NON_EXISTENT_DIFFERENT_DB]: RenameToNonExistentDifferentDbCommand,
|
||||
[Action.RENAME_TO_EXISTENT_DIFFERENT_DB]: RenameToExistentDifferentDbCommand,
|
||||
[Action.SHARD_COLLECTION]: CreateShardedCollectionCommand,
|
||||
[Action.UNSHARD_COLLECTION]: UnshardCollectionCommand,
|
||||
[Action.RESHARD_COLLECTION]: ReshardCollectionCommand,
|
||||
[Action.MOVE_PRIMARY]: MovePrimaryCommand,
|
||||
[Action.MOVE_COLLECTION]: MoveCollectionCommand,
|
||||
[Action.MOVE_CHUNK]: MoveChunkCommand,
|
||||
};
|
||||
|
||||
class ShardingCommandGenerator {
|
||||
// Maps action IDs to command classes.
|
||||
static actionToCommandClass = {
|
||||
[Action.INSERT_DOC]: InsertDocCommand,
|
||||
[Action.CREATE_DATABASE]: CreateDatabaseCommand,
|
||||
[Action.CREATE_SHARDED_COLLECTION_RANGE]: ShardCollectionCommand,
|
||||
[Action.CREATE_SHARDED_COLLECTION_HASHED]: ShardCollectionCommand,
|
||||
[Action.SHARD_COLLECTION_RANGE]: ShardCollectionCommand,
|
||||
[Action.SHARD_COLLECTION_HASHED]: ShardCollectionCommand,
|
||||
[Action.RESHARD_COLLECTION_TO_RANGE]: ReshardCollectionCommand,
|
||||
[Action.RESHARD_COLLECTION_TO_HASHED]: ReshardCollectionCommand,
|
||||
[Action.CREATE_UNSPLITTABLE_COLLECTION]: CreateUnsplittableCollectionCommand,
|
||||
[Action.CREATE_UNTRACKED_COLLECTION]: CreateUntrackedCollectionCommand,
|
||||
[Action.DROP_COLLECTION]: DropCollectionCommand,
|
||||
[Action.DROP_DATABASE]: DropDatabaseCommand,
|
||||
[Action.RENAME_TO_NON_EXISTENT_SAME_DB]: RenameToNonExistentSameDbCommand,
|
||||
[Action.RENAME_TO_EXISTENT_SAME_DB]: RenameToExistentSameDbCommand,
|
||||
[Action.RENAME_TO_NON_EXISTENT_DIFFERENT_DB]: RenameToNonExistentDifferentDbCommand,
|
||||
[Action.RENAME_TO_EXISTENT_DIFFERENT_DB]: RenameToExistentDifferentDbCommand,
|
||||
[Action.UNSHARD_COLLECTION]: UnshardCollectionCommand,
|
||||
[Action.MOVE_PRIMARY]: MovePrimaryCommand,
|
||||
[Action.MOVE_COLLECTION]: MoveCollectionCommand,
|
||||
[Action.MOVE_CHUNK]: MoveChunkCommand,
|
||||
};
|
||||
|
||||
constructor(seed) {
|
||||
assert(seed !== null && seed !== undefined, "Seed must be explicitly provided to ShardingCommandGenerator");
|
||||
this.seed = seed;
|
||||
|
|
@ -60,12 +67,13 @@ class ShardingCommandGenerator {
|
|||
* Create a command instance for a given action ID.
|
||||
* @param {number} action - The action ID.
|
||||
* @param {ShardingCommandGeneratorParams} params - The generator parameters.
|
||||
* @param {Object} collectionCtx - The collection context with sharding config.
|
||||
* @returns {Command} The command instance.
|
||||
*/
|
||||
createCommand(action, params) {
|
||||
const CommandClass = actionToCommandClass[action];
|
||||
createCommand(action, params, collectionCtx) {
|
||||
const CommandClass = ShardingCommandGenerator.actionToCommandClass[action];
|
||||
assert(CommandClass !== undefined, `No command class found for action ${action}`);
|
||||
return new CommandClass(params.getDbName(), params.getCollName(), params.getShardSet());
|
||||
return new CommandClass(params.getDbName(), params.getCollName(), params.getShardSet(), {...collectionCtx});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -91,6 +99,7 @@ class ShardingCommandGenerator {
|
|||
|
||||
// Step 3: Select starting state.
|
||||
let currentState = startState;
|
||||
let collectionCtx = this._initialCollectionCtxForState(startState);
|
||||
const commands = [];
|
||||
|
||||
// Step 4: While there are unvisited actions.
|
||||
|
|
@ -98,8 +107,8 @@ class ShardingCommandGenerator {
|
|||
// 4a: For each unvisited self-loop action (state → state), visit and mark as visited.
|
||||
let selfLoopAction = this._getRandomUnvisitedSelfLoop(testModel, currentState, unvisitedActions);
|
||||
while (selfLoopAction !== null) {
|
||||
const command = this.createCommand(selfLoopAction, params);
|
||||
commands.push(command);
|
||||
this._appendAction(commands, selfLoopAction, params, collectionCtx);
|
||||
this._updateCollectionCtxForAction(selfLoopAction, collectionCtx);
|
||||
this._markActionAsVisited(unvisitedActions, currentState, selfLoopAction);
|
||||
selfLoopAction = this._getRandomUnvisitedSelfLoop(testModel, currentState, unvisitedActions);
|
||||
}
|
||||
|
|
@ -107,8 +116,8 @@ class ShardingCommandGenerator {
|
|||
// 4b: If exists unvisited non-self-loop action, visit it and move to target state.
|
||||
const action = this._getRandomUnvisitedNonSelfLoop(testModel, currentState, unvisitedActions);
|
||||
if (action !== null) {
|
||||
const command = this.createCommand(action.action, params);
|
||||
commands.push(command);
|
||||
this._appendAction(commands, action.action, params, collectionCtx);
|
||||
this._updateCollectionCtxForAction(action.action, collectionCtx);
|
||||
this._markActionAsVisited(unvisitedActions, currentState, action.action);
|
||||
currentState = action.to;
|
||||
} else if (unvisitedActions.size > 0) {
|
||||
|
|
@ -122,8 +131,8 @@ class ShardingCommandGenerator {
|
|||
|
||||
const path = shortestPaths.get(currentState).get(targetState).path;
|
||||
for (const step of path) {
|
||||
const command = this.createCommand(step.action, params);
|
||||
commands.push(command);
|
||||
this._appendAction(commands, step.action, params, collectionCtx);
|
||||
this._updateCollectionCtxForAction(step.action, collectionCtx);
|
||||
this._markActionAsVisited(unvisitedActions, step.from, step.action);
|
||||
}
|
||||
currentState = targetState;
|
||||
|
|
@ -259,6 +268,181 @@ class ShardingCommandGenerator {
|
|||
}
|
||||
return nearest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty collection context (collection does not exist).
|
||||
*/
|
||||
static _emptyCollectionCtx() {
|
||||
return {
|
||||
exists: false,
|
||||
nonEmpty: false,
|
||||
shardKeySpec: null,
|
||||
existingIndexes: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize collection context based on the starting state.
|
||||
* collectionCtx tracks: exists, nonEmpty, shardKeySpec, existingIndexes.
|
||||
* Note: Even if starting from a collection-present state, we assume the collection
|
||||
* is empty (nonEmpty: false) since we don't know its actual contents.
|
||||
*/
|
||||
_initialCollectionCtxForState(state) {
|
||||
const collectionPresent = [
|
||||
State.COLLECTION_PRESENT_SHARDED_RANGE,
|
||||
State.COLLECTION_PRESENT_SHARDED_HASHED,
|
||||
State.COLLECTION_PRESENT_UNSPLITTABLE,
|
||||
State.COLLECTION_PRESENT_UNTRACKED,
|
||||
];
|
||||
const ctx = ShardingCommandGenerator._emptyCollectionCtx();
|
||||
ctx.exists = collectionPresent.includes(state);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an index exists in the context.
|
||||
*/
|
||||
_indexExists(ctx, indexSpec) {
|
||||
return ctx.existingIndexes.some((idx) => bsonWoCompare(idx, indexSpec) === 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an index to the context's existing indexes.
|
||||
*/
|
||||
_addIndex(ctx, indexSpec) {
|
||||
if (!this._indexExists(ctx, indexSpec)) {
|
||||
ctx.existingIndexes.push(indexSpec);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update collection context after executing an action.
|
||||
* Sharding type is determined by the action itself.
|
||||
*/
|
||||
_updateCollectionCtxForAction(action, ctx) {
|
||||
switch (action) {
|
||||
case Action.INSERT_DOC:
|
||||
ctx.exists = true;
|
||||
ctx.nonEmpty = true;
|
||||
break;
|
||||
case Action.CREATE_SHARDED_COLLECTION_RANGE:
|
||||
case Action.SHARD_COLLECTION_RANGE:
|
||||
ctx.exists = true;
|
||||
ctx.shardKeySpec = getShardKeySpec(ShardingType.RANGE);
|
||||
this._addIndex(ctx, getShardKeySpec(ShardingType.RANGE));
|
||||
break;
|
||||
case Action.CREATE_SHARDED_COLLECTION_HASHED:
|
||||
case Action.SHARD_COLLECTION_HASHED:
|
||||
ctx.exists = true;
|
||||
ctx.shardKeySpec = getShardKeySpec(ShardingType.HASHED);
|
||||
this._addIndex(ctx, getShardKeySpec(ShardingType.HASHED));
|
||||
break;
|
||||
case Action.RESHARD_COLLECTION_TO_RANGE:
|
||||
case Action.RESHARD_COLLECTION_TO_HASHED: {
|
||||
// Remove old shard key index, add new one
|
||||
assert(ctx.shardKeySpec, "Reshard requires existing shard key");
|
||||
ctx.existingIndexes = ctx.existingIndexes.filter((idx) => bsonWoCompare(idx, ctx.shardKeySpec) !== 0);
|
||||
const newShardKeySpec =
|
||||
action === Action.RESHARD_COLLECTION_TO_RANGE
|
||||
? getShardKeySpec(ShardingType.RANGE)
|
||||
: getShardKeySpec(ShardingType.HASHED);
|
||||
ctx.shardKeySpec = newShardKeySpec;
|
||||
this._addIndex(ctx, newShardKeySpec);
|
||||
break;
|
||||
}
|
||||
case Action.CREATE_UNSPLITTABLE_COLLECTION:
|
||||
case Action.CREATE_UNTRACKED_COLLECTION:
|
||||
ctx.exists = true;
|
||||
ctx.nonEmpty = false;
|
||||
break;
|
||||
case Action.UNSHARD_COLLECTION:
|
||||
// Collection becomes unsplittable (single-shard) but still exists.
|
||||
// Shard key and indexes are preserved.
|
||||
break;
|
||||
case Action.DROP_COLLECTION:
|
||||
case Action.DROP_DATABASE:
|
||||
case Action.RENAME_TO_NON_EXISTENT_SAME_DB:
|
||||
case Action.RENAME_TO_EXISTENT_SAME_DB:
|
||||
case Action.RENAME_TO_NON_EXISTENT_DIFFERENT_DB:
|
||||
case Action.RENAME_TO_EXISTENT_DIFFERENT_DB:
|
||||
// Collection no longer exists (dropped or renamed away).
|
||||
Object.assign(ctx, ShardingCommandGenerator._emptyCollectionCtx());
|
||||
break;
|
||||
default:
|
||||
// Other actions do not modify collection context.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Maps actions to their sharding type (for determining shard key spec)
|
||||
static actionToShardingType = {
|
||||
[Action.CREATE_SHARDED_COLLECTION_RANGE]: ShardingType.RANGE,
|
||||
[Action.SHARD_COLLECTION_RANGE]: ShardingType.RANGE,
|
||||
[Action.RESHARD_COLLECTION_TO_RANGE]: ShardingType.RANGE,
|
||||
[Action.CREATE_SHARDED_COLLECTION_HASHED]: ShardingType.HASHED,
|
||||
[Action.SHARD_COLLECTION_HASHED]: ShardingType.HASHED,
|
||||
[Action.RESHARD_COLLECTION_TO_HASHED]: ShardingType.HASHED,
|
||||
};
|
||||
|
||||
// Actions that require shard key index to exist before execution
|
||||
static actionsRequiringIndex = new Set([
|
||||
Action.SHARD_COLLECTION_RANGE,
|
||||
Action.SHARD_COLLECTION_HASHED,
|
||||
Action.RESHARD_COLLECTION_TO_RANGE,
|
||||
Action.RESHARD_COLLECTION_TO_HASHED,
|
||||
]);
|
||||
|
||||
// Actions that require dropping the old shard key index after execution
|
||||
static actionsRequiringIndexCleanup = new Set([
|
||||
Action.RESHARD_COLLECTION_TO_RANGE,
|
||||
Action.RESHARD_COLLECTION_TO_HASHED,
|
||||
]);
|
||||
|
||||
/**
|
||||
* Append the command(s) for a given action.
|
||||
* Some actions require prerequisite commands (e.g., index creation before sharding).
|
||||
*/
|
||||
_appendAction(commands, action, params, collectionCtx) {
|
||||
// Step 1: Determine target shard key spec from action mapping.
|
||||
const shardingType = ShardingCommandGenerator.actionToShardingType[action];
|
||||
const targetShardKeySpec = shardingType ? getShardKeySpec(shardingType) : null;
|
||||
|
||||
// Step 2: Build collection context copy with appropriate shard key spec.
|
||||
const ctx = {...collectionCtx};
|
||||
if (targetShardKeySpec) {
|
||||
ctx.shardKeySpec = targetShardKeySpec;
|
||||
}
|
||||
|
||||
// Step 3: Pre-commands
|
||||
// Create shard key index if action requires it and index doesn't exist
|
||||
if (
|
||||
ShardingCommandGenerator.actionsRequiringIndex.has(action) &&
|
||||
!this._indexExists(collectionCtx, targetShardKeySpec)
|
||||
) {
|
||||
commands.push(new CreateIndexCommand(params.getDbName(), params.getCollName(), params.getShardSet(), ctx));
|
||||
}
|
||||
// Drop collection before dropping database (simplifies change event matching)
|
||||
if (action === Action.DROP_DATABASE && collectionCtx.exists) {
|
||||
commands.push(
|
||||
new DropCollectionCommand(params.getDbName(), params.getCollName(), params.getShardSet(), ctx),
|
||||
);
|
||||
}
|
||||
|
||||
// Step 4: Main command
|
||||
commands.push(this.createCommand(action, params, ctx));
|
||||
|
||||
// Step 5: Post-commands - drop old shard key index after resharding (only if key changed)
|
||||
if (ShardingCommandGenerator.actionsRequiringIndexCleanup.has(action)) {
|
||||
const oldShardKeySpec = collectionCtx.shardKeySpec;
|
||||
// Only drop old index if it's different from the new one
|
||||
if (bsonWoCompare(oldShardKeySpec, targetShardKeySpec) !== 0) {
|
||||
const dropCtx = {...ctx, shardKeySpec: oldShardKeySpec};
|
||||
commands.push(
|
||||
new DropIndexCommand(params.getDbName(), params.getCollName(), params.getShardSet(), dropCtx),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {ShardingCommandGenerator};
|
||||
|
|
|
|||
|
|
@ -1,34 +1,27 @@
|
|||
/**
|
||||
* Parameters for ShardingCommandGenerator.
|
||||
* Encapsulates database, collection, and shard information.
|
||||
* Simple struct encapsulating database, collection, and shard information.
|
||||
*/
|
||||
class ShardingCommandGeneratorParams {
|
||||
/**
|
||||
* @param {string} dbName - Database name
|
||||
* @param {string} collName - Collection name
|
||||
* @param {Array} shardSet - Array of shard objects
|
||||
*/
|
||||
constructor(dbName, collName, shardSet) {
|
||||
this.dbName = dbName;
|
||||
this.collName = collName;
|
||||
this.shardSet = shardSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the database name.
|
||||
* @returns {string} The database name.
|
||||
*/
|
||||
getDbName() {
|
||||
return this.dbName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection name.
|
||||
* @returns {string} The collection name.
|
||||
*/
|
||||
getCollName() {
|
||||
return this.collName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shard set.
|
||||
* @returns {Array} Array of shard objects.
|
||||
*/
|
||||
getShardSet() {
|
||||
return this.shardSet;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@
|
|||
class State {
|
||||
static DATABASE_ABSENT = 0;
|
||||
static DATABASE_PRESENT_COLLECTION_ABSENT = 1;
|
||||
static COLLECTION_PRESENT_SHARDED = 2;
|
||||
static COLLECTION_PRESENT_UNSPLITTABLE = 3;
|
||||
static COLLECTION_PRESENT_UNTRACKED = 4;
|
||||
static COLLECTION_PRESENT_SHARDED_RANGE = 2;
|
||||
static COLLECTION_PRESENT_SHARDED_HASHED = 3;
|
||||
static COLLECTION_PRESENT_UNSPLITTABLE = 4;
|
||||
static COLLECTION_PRESENT_UNTRACKED = 5;
|
||||
|
||||
static getName(stateId) {
|
||||
switch (stateId) {
|
||||
|
|
@ -14,8 +15,10 @@ class State {
|
|||
return "DatabaseAbsent";
|
||||
case State.DATABASE_PRESENT_COLLECTION_ABSENT:
|
||||
return "DatabasePresent::CollectionAbsent";
|
||||
case State.COLLECTION_PRESENT_SHARDED:
|
||||
return "CollectionPresent::ShardedCollection";
|
||||
case State.COLLECTION_PRESENT_SHARDED_RANGE:
|
||||
return "CollectionPresent::ShardedCollection(Range)";
|
||||
case State.COLLECTION_PRESENT_SHARDED_HASHED:
|
||||
return "CollectionPresent::ShardedCollection(Hashed)";
|
||||
case State.COLLECTION_PRESENT_UNSPLITTABLE:
|
||||
return "CollectionPresent::UnsplittableCollection";
|
||||
case State.COLLECTION_PRESENT_UNTRACKED:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
*
|
||||
* @tags: [uses_change_streams]
|
||||
*/
|
||||
import {Action} from "jstests/libs/util/change_stream/change_stream_action.js";
|
||||
import {CollectionTestModel} from "jstests/libs/util/change_stream/change_stream_collection_test_model.js";
|
||||
import {ShardingCommandGenerator} from "jstests/libs/util/change_stream/change_stream_sharding_command_generator.js";
|
||||
import {ShardingCommandGeneratorParams} from "jstests/libs/util/change_stream/change_stream_sharding_command_generator_params.js";
|
||||
|
|
@ -160,6 +161,74 @@ describe("ShardingCommandGenerator", function () {
|
|||
|
||||
jsTest.log.info("✓ Sequential multi-Writer test passed");
|
||||
});
|
||||
|
||||
it("runs the graph mutator and exercises all FSM transitions", () => {
|
||||
const dbName = "test_db_sharding";
|
||||
const collName = "test_coll_sharding";
|
||||
const seed = 314159;
|
||||
|
||||
const db = this.st.s.getDB(dbName);
|
||||
db.dropDatabase();
|
||||
|
||||
const model = new CollectionTestModel().setStartState(State.DATABASE_ABSENT);
|
||||
const params = new ShardingCommandGeneratorParams(dbName, collName, this.shards);
|
||||
const generator = new ShardingCommandGenerator(seed);
|
||||
const commands = generator.generateCommands(model, params);
|
||||
|
||||
jsTest.log.info(`\n========== Graph mutator - Full FSM traversal ==========`);
|
||||
jsTest.log.info(`Seed: ${seed}, DB: ${dbName}, Coll: ${collName}`);
|
||||
jsTest.log.info(`Total commands: ${commands.length}`);
|
||||
jsTest.log.info(`Command list:`);
|
||||
commands.forEach((cmd, idx) => {
|
||||
jsTest.log.info(` [${idx}] ${cmd.toString()}`);
|
||||
});
|
||||
|
||||
// Build set of command strings present in the generated sequence.
|
||||
const commandStrings = commands.map((c) => c.toString());
|
||||
|
||||
// Collect all unique actions from the FSM model.
|
||||
const allActionsInFsm = new Set();
|
||||
for (const state of model.states) {
|
||||
for (const action of model.collectionStateToActionsMap(state).keys()) {
|
||||
allActionsInFsm.add(action);
|
||||
}
|
||||
}
|
||||
|
||||
// Derive expected command patterns from the Action enum (single source of truth).
|
||||
jsTest.log.info(`\n--- Command coverage verification ---`);
|
||||
const missingCommands = [];
|
||||
for (const actionId of allActionsInFsm) {
|
||||
const commandClass = ShardingCommandGenerator.actionToCommandClass[actionId];
|
||||
assert(commandClass, `No command class mapped for action ${actionId}`);
|
||||
const commandClassName = commandClass.name;
|
||||
const actionName = Action.getName(actionId);
|
||||
const found = commandStrings.some((s) => s.includes(commandClassName));
|
||||
jsTest.log.info(` ${actionName}: ${found ? "✓" : "✗"}`);
|
||||
if (!found) {
|
||||
missingCommands.push(actionName);
|
||||
}
|
||||
}
|
||||
|
||||
// Assert all FSM actions produced their expected commands.
|
||||
assert.eq(
|
||||
missingCommands.length,
|
||||
0,
|
||||
`Missing command types: ${missingCommands.join(", ")}. All FSM actions must be covered.`,
|
||||
);
|
||||
|
||||
jsTest.log.info(` Total FSM actions: ${allActionsInFsm.size}`);
|
||||
jsTest.log.info(` Commands generated: ${commands.length}`);
|
||||
jsTest.log.info(`✓ All FSM actions produced expected commands`);
|
||||
|
||||
jsTest.log.info(`\n========== Executing commands ==========`);
|
||||
|
||||
commands.forEach((cmd, cmdIdx) => {
|
||||
jsTest.log.info(`Executing [${cmdIdx}]: ${cmd.toString()}`);
|
||||
cmd.execute(this.st.s);
|
||||
});
|
||||
|
||||
jsTest.log.info(`✓ All ${commands.length} commands executed successfully`);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ChangeEventMatcher helpers", function () {
|
||||
|
|
|
|||
Loading…
Reference in New Issue