Files
smartmongo/ts/ts_tsmdb/server/handlers/AdminHandler.ts

720 lines
19 KiB
TypeScript
Raw Normal View History

import * as plugins from '../../plugins.js';
import type { ICommandHandler, IHandlerContext } from '../CommandRouter.js';
import { SessionEngine } from '../../engine/SessionEngine.js';
/**
* AdminHandler - Handles administrative commands
*/
export class AdminHandler implements ICommandHandler {
async handle(context: IHandlerContext): Promise<plugins.bson.Document> {
const { command } = context;
// Determine which command to handle
if (command.ping !== undefined) {
return this.handlePing(context);
} else if (command.listDatabases !== undefined) {
return this.handleListDatabases(context);
} else if (command.listCollections !== undefined) {
return this.handleListCollections(context);
} else if (command.drop !== undefined) {
return this.handleDrop(context);
} else if (command.dropDatabase !== undefined) {
return this.handleDropDatabase(context);
} else if (command.create !== undefined) {
return this.handleCreate(context);
} else if (command.serverStatus !== undefined) {
return this.handleServerStatus(context);
} else if (command.buildInfo !== undefined) {
return this.handleBuildInfo(context);
} else if (command.whatsmyuri !== undefined) {
return this.handleWhatsMyUri(context);
} else if (command.getLog !== undefined) {
return this.handleGetLog(context);
} else if (command.hostInfo !== undefined) {
return this.handleHostInfo(context);
} else if (command.replSetGetStatus !== undefined) {
return this.handleReplSetGetStatus(context);
} else if (command.saslStart !== undefined) {
return this.handleSaslStart(context);
} else if (command.saslContinue !== undefined) {
return this.handleSaslContinue(context);
} else if (command.endSessions !== undefined) {
return this.handleEndSessions(context);
} else if (command.abortTransaction !== undefined) {
return this.handleAbortTransaction(context);
} else if (command.commitTransaction !== undefined) {
return this.handleCommitTransaction(context);
} else if (command.collStats !== undefined) {
return this.handleCollStats(context);
} else if (command.dbStats !== undefined) {
return this.handleDbStats(context);
} else if (command.connectionStatus !== undefined) {
return this.handleConnectionStatus(context);
} else if (command.currentOp !== undefined) {
return this.handleCurrentOp(context);
} else if (command.collMod !== undefined) {
return this.handleCollMod(context);
} else if (command.renameCollection !== undefined) {
return this.handleRenameCollection(context);
}
return {
ok: 0,
errmsg: 'Unknown admin command',
code: 59,
codeName: 'CommandNotFound',
};
}
/**
* Handle ping command
*/
private async handlePing(context: IHandlerContext): Promise<plugins.bson.Document> {
return { ok: 1 };
}
/**
* Handle listDatabases command
*/
private async handleListDatabases(context: IHandlerContext): Promise<plugins.bson.Document> {
const { storage, command } = context;
const dbNames = await storage.listDatabases();
const nameOnly = command.nameOnly || false;
if (nameOnly) {
return {
ok: 1,
databases: dbNames.map(name => ({ name })),
};
}
// Build database list with sizes
const databases: plugins.bson.Document[] = [];
let totalSize = 0;
for (const name of dbNames) {
const collections = await storage.listCollections(name);
let dbSize = 0;
for (const collName of collections) {
const docs = await storage.findAll(name, collName);
// Estimate size (rough approximation)
dbSize += docs.reduce((sum, doc) => sum + JSON.stringify(doc).length, 0);
}
totalSize += dbSize;
databases.push({
name,
sizeOnDisk: dbSize,
empty: dbSize === 0,
});
}
return {
ok: 1,
databases,
totalSize,
totalSizeMb: totalSize / (1024 * 1024),
};
}
/**
* Handle listCollections command
*/
private async handleListCollections(context: IHandlerContext): Promise<plugins.bson.Document> {
const { storage, database, command } = context;
const filter = command.filter || {};
const nameOnly = command.nameOnly || false;
const cursor = command.cursor || {};
const batchSize = cursor.batchSize || 101;
const collNames = await storage.listCollections(database);
let collections: plugins.bson.Document[] = [];
for (const name of collNames) {
// Apply name filter
if (filter.name && filter.name !== name) {
// Check regex
if (filter.name.$regex) {
const regex = new RegExp(filter.name.$regex, filter.name.$options);
if (!regex.test(name)) continue;
} else {
continue;
}
}
if (nameOnly) {
collections.push({ name });
} else {
collections.push({
name,
type: 'collection',
options: {},
info: {
readOnly: false,
uuid: new plugins.bson.UUID(),
},
idIndex: {
v: 2,
key: { _id: 1 },
name: '_id_',
},
});
}
}
return {
ok: 1,
cursor: {
id: plugins.bson.Long.fromNumber(0),
ns: `${database}.$cmd.listCollections`,
firstBatch: collections,
},
};
}
/**
* Handle drop command (drop collection)
*/
private async handleDrop(context: IHandlerContext): Promise<plugins.bson.Document> {
const { storage, database, command } = context;
const collection = command.drop;
const existed = await storage.dropCollection(database, collection);
if (!existed) {
return {
ok: 0,
errmsg: `ns not found ${database}.${collection}`,
code: 26,
codeName: 'NamespaceNotFound',
};
}
return { ok: 1, ns: `${database}.${collection}` };
}
/**
* Handle dropDatabase command
*/
private async handleDropDatabase(context: IHandlerContext): Promise<plugins.bson.Document> {
const { storage, database } = context;
await storage.dropDatabase(database);
return { ok: 1, dropped: database };
}
/**
* Handle create command (create collection)
*/
private async handleCreate(context: IHandlerContext): Promise<plugins.bson.Document> {
const { storage, database, command } = context;
const collection = command.create;
// Check if already exists
const exists = await storage.collectionExists(database, collection);
if (exists) {
return {
ok: 0,
errmsg: `Collection ${database}.${collection} already exists.`,
code: 48,
codeName: 'NamespaceExists',
};
}
await storage.createCollection(database, collection);
return { ok: 1 };
}
/**
* Handle serverStatus command
*/
private async handleServerStatus(context: IHandlerContext): Promise<plugins.bson.Document> {
const { server, sessionEngine } = context;
const uptime = server.getUptime();
const connections = server.getConnectionCount();
const sessions = sessionEngine.listSessions();
const sessionsWithTxn = sessionEngine.getSessionsWithTransactions();
return {
ok: 1,
host: `${server.host}:${server.port}`,
version: '7.0.0',
process: 'tsmdb',
pid: process.pid,
uptime,
uptimeMillis: uptime * 1000,
uptimeEstimate: uptime,
localTime: new Date(),
mem: {
resident: Math.floor(process.memoryUsage().rss / (1024 * 1024)),
virtual: Math.floor(process.memoryUsage().heapTotal / (1024 * 1024)),
supported: true,
},
connections: {
current: connections,
available: 1000 - connections,
totalCreated: connections,
active: connections,
},
logicalSessionRecordCache: {
activeSessionsCount: sessions.length,
sessionsCollectionJobCount: 0,
lastSessionsCollectionJobDurationMillis: 0,
lastSessionsCollectionJobTimestamp: new Date(),
transactionReaperJobCount: 0,
lastTransactionReaperJobDurationMillis: 0,
lastTransactionReaperJobTimestamp: new Date(),
},
transactions: {
retriedCommandsCount: 0,
retriedStatementsCount: 0,
transactionsCollectionWriteCount: 0,
currentActive: sessionsWithTxn.length,
currentInactive: 0,
currentOpen: sessionsWithTxn.length,
totalStarted: sessionsWithTxn.length,
totalCommitted: 0,
totalAborted: 0,
},
network: {
bytesIn: 0,
bytesOut: 0,
numRequests: 0,
},
storageEngine: {
name: 'tsmdb',
supportsCommittedReads: true,
persistent: false,
},
};
}
/**
* Handle buildInfo command
*/
private async handleBuildInfo(context: IHandlerContext): Promise<plugins.bson.Document> {
return {
ok: 1,
version: '7.0.0',
gitVersion: 'tsmdb',
modules: [],
allocator: 'system',
javascriptEngine: 'none',
sysInfo: 'deprecated',
versionArray: [7, 0, 0, 0],
openssl: {
running: 'disabled',
compiled: 'disabled',
},
buildEnvironment: {
distmod: 'tsmdb',
distarch: process.arch,
cc: '',
ccflags: '',
cxx: '',
cxxflags: '',
linkflags: '',
target_arch: process.arch,
target_os: process.platform,
},
bits: 64,
debug: false,
maxBsonObjectSize: 16777216,
storageEngines: ['tsmdb'],
};
}
/**
* Handle whatsmyuri command
*/
private async handleWhatsMyUri(context: IHandlerContext): Promise<plugins.bson.Document> {
const { server } = context;
return {
ok: 1,
you: `127.0.0.1:${server.port}`,
};
}
/**
* Handle getLog command
*/
private async handleGetLog(context: IHandlerContext): Promise<plugins.bson.Document> {
const { command } = context;
if (command.getLog === '*') {
return {
ok: 1,
names: ['global', 'startupWarnings'],
};
}
return {
ok: 1,
totalLinesWritten: 0,
log: [],
};
}
/**
* Handle hostInfo command
*/
private async handleHostInfo(context: IHandlerContext): Promise<plugins.bson.Document> {
return {
ok: 1,
system: {
currentTime: new Date(),
hostname: 'localhost',
cpuAddrSize: 64,
memSizeMB: Math.floor(process.memoryUsage().heapTotal / (1024 * 1024)),
numCores: 1,
cpuArch: process.arch,
numaEnabled: false,
},
os: {
type: process.platform,
name: process.platform,
version: process.version,
},
extra: {},
};
}
/**
* Handle replSetGetStatus command
*/
private async handleReplSetGetStatus(context: IHandlerContext): Promise<plugins.bson.Document> {
// We're standalone, not a replica set
return {
ok: 0,
errmsg: 'not running with --replSet',
code: 76,
codeName: 'NoReplicationEnabled',
};
}
/**
* Handle saslStart command (authentication)
*/
private async handleSaslStart(context: IHandlerContext): Promise<plugins.bson.Document> {
// We don't require authentication, but we need to respond properly
// to let drivers know auth is "successful"
return {
ok: 1,
conversationId: 1,
done: true,
payload: Buffer.from([]),
};
}
/**
* Handle saslContinue command
*/
private async handleSaslContinue(context: IHandlerContext): Promise<plugins.bson.Document> {
return {
ok: 1,
conversationId: 1,
done: true,
payload: Buffer.from([]),
};
}
/**
* Handle endSessions command
*/
private async handleEndSessions(context: IHandlerContext): Promise<plugins.bson.Document> {
const { command, sessionEngine } = context;
// End each session in the array
const sessions = command.endSessions || [];
for (const sessionSpec of sessions) {
const sessionId = SessionEngine.extractSessionId(sessionSpec);
if (sessionId) {
await sessionEngine.endSession(sessionId);
}
}
return { ok: 1 };
}
/**
* Handle abortTransaction command
*/
private async handleAbortTransaction(context: IHandlerContext): Promise<plugins.bson.Document> {
const { transactionEngine, sessionEngine, txnId, sessionId } = context;
if (!txnId) {
return {
ok: 0,
errmsg: 'No transaction started',
code: 251,
codeName: 'NoSuchTransaction',
};
}
try {
await transactionEngine.abortTransaction(txnId);
transactionEngine.endTransaction(txnId);
// Update session state
if (sessionId) {
sessionEngine.endTransaction(sessionId);
}
return { ok: 1 };
} catch (error: any) {
return {
ok: 0,
errmsg: error.message || 'Abort transaction failed',
code: error.code || 1,
codeName: error.codeName || 'UnknownError',
};
}
}
/**
* Handle commitTransaction command
*/
private async handleCommitTransaction(context: IHandlerContext): Promise<plugins.bson.Document> {
const { transactionEngine, sessionEngine, txnId, sessionId } = context;
if (!txnId) {
return {
ok: 0,
errmsg: 'No transaction started',
code: 251,
codeName: 'NoSuchTransaction',
};
}
try {
await transactionEngine.commitTransaction(txnId);
transactionEngine.endTransaction(txnId);
// Update session state
if (sessionId) {
sessionEngine.endTransaction(sessionId);
}
return { ok: 1 };
} catch (error: any) {
// If commit fails, transaction should be aborted
try {
await transactionEngine.abortTransaction(txnId);
transactionEngine.endTransaction(txnId);
if (sessionId) {
sessionEngine.endTransaction(sessionId);
}
} catch {
// Ignore abort errors
}
if (error.code === 112) {
// Write conflict
return {
ok: 0,
errmsg: error.message || 'Write conflict during commit',
code: 112,
codeName: 'WriteConflict',
};
}
return {
ok: 0,
errmsg: error.message || 'Commit transaction failed',
code: error.code || 1,
codeName: error.codeName || 'UnknownError',
};
}
}
/**
* Handle collStats command
*/
private async handleCollStats(context: IHandlerContext): Promise<plugins.bson.Document> {
const { storage, database, command } = context;
const collection = command.collStats;
const exists = await storage.collectionExists(database, collection);
if (!exists) {
return {
ok: 0,
errmsg: `ns not found ${database}.${collection}`,
code: 26,
codeName: 'NamespaceNotFound',
};
}
const docs = await storage.findAll(database, collection);
const size = docs.reduce((sum, doc) => sum + JSON.stringify(doc).length, 0);
const count = docs.length;
const avgObjSize = count > 0 ? size / count : 0;
const indexes = await storage.getIndexes(database, collection);
return {
ok: 1,
ns: `${database}.${collection}`,
count,
size,
avgObjSize,
storageSize: size,
totalIndexSize: 0,
indexSizes: indexes.reduce((acc: any, idx: any) => {
acc[idx.name] = 0;
return acc;
}, {}),
nindexes: indexes.length,
};
}
/**
* Handle dbStats command
*/
private async handleDbStats(context: IHandlerContext): Promise<plugins.bson.Document> {
const { storage, database } = context;
const collections = await storage.listCollections(database);
let totalSize = 0;
let totalObjects = 0;
for (const collName of collections) {
const docs = await storage.findAll(database, collName);
totalObjects += docs.length;
totalSize += docs.reduce((sum, doc) => sum + JSON.stringify(doc).length, 0);
}
return {
ok: 1,
db: database,
collections: collections.length,
views: 0,
objects: totalObjects,
avgObjSize: totalObjects > 0 ? totalSize / totalObjects : 0,
dataSize: totalSize,
storageSize: totalSize,
indexes: collections.length, // At least _id index per collection
indexSize: 0,
totalSize,
};
}
/**
* Handle connectionStatus command
*/
private async handleConnectionStatus(context: IHandlerContext): Promise<plugins.bson.Document> {
return {
ok: 1,
authInfo: {
authenticatedUsers: [],
authenticatedUserRoles: [],
},
};
}
/**
* Handle currentOp command
*/
private async handleCurrentOp(context: IHandlerContext): Promise<plugins.bson.Document> {
return {
ok: 1,
inprog: [],
};
}
/**
* Handle collMod command
*/
private async handleCollMod(context: IHandlerContext): Promise<plugins.bson.Document> {
// We don't support modifying collection options, but acknowledge the command
return { ok: 1 };
}
/**
* Handle renameCollection command
*/
private async handleRenameCollection(context: IHandlerContext): Promise<plugins.bson.Document> {
const { storage, command } = context;
const from = command.renameCollection;
const to = command.to;
const dropTarget = command.dropTarget || false;
if (!from || !to) {
return {
ok: 0,
errmsg: 'renameCollection requires both source and target',
code: 2,
codeName: 'BadValue',
};
}
// Parse namespace (format: "db.collection")
const fromParts = from.split('.');
const toParts = to.split('.');
if (fromParts.length < 2 || toParts.length < 2) {
return {
ok: 0,
errmsg: 'Invalid namespace format',
code: 73,
codeName: 'InvalidNamespace',
};
}
const fromDb = fromParts[0];
const fromColl = fromParts.slice(1).join('.');
const toDb = toParts[0];
const toColl = toParts.slice(1).join('.');
// Check if source exists
const sourceExists = await storage.collectionExists(fromDb, fromColl);
if (!sourceExists) {
return {
ok: 0,
errmsg: `source namespace ${from} does not exist`,
code: 26,
codeName: 'NamespaceNotFound',
};
}
// Check if target exists
const targetExists = await storage.collectionExists(toDb, toColl);
if (targetExists) {
if (dropTarget) {
await storage.dropCollection(toDb, toColl);
} else {
return {
ok: 0,
errmsg: `target namespace ${to} already exists`,
code: 48,
codeName: 'NamespaceExists',
};
}
}
// Same database rename
if (fromDb === toDb) {
await storage.renameCollection(fromDb, fromColl, toColl);
} else {
// Cross-database rename: copy documents then drop source
await storage.createCollection(toDb, toColl);
const docs = await storage.findAll(fromDb, fromColl);
for (const doc of docs) {
await storage.insertOne(toDb, toColl, doc);
}
await storage.dropCollection(fromDb, fromColl);
}
return { ok: 1 };
}
}