开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
简介
上一篇介绍了CommandOpRunner,接下来会讲一下ExecCommandDatabase的执行流程,以及Command是如何注册到MongoDB中。
Command与Invocation
Command
主要文件在 src/mongo/db/commands.h中,下来我们看下Command的结构
Invocation
作为Command的内部类,其主要的是typedRun方法,处理了主要的执行命令的逻辑,并且返回Reply。
ExecCommandDatabase
上一篇OpRunner的时候讲到,在CommandOpRunner::executeCommand的时候,会做以下几个步骤:
- 初始化ExecCommandDatabase,初始化的时候会执行执行ExecCommandDatabase::_parseCommand
- 执行ExecCommandDatabase::_initiateCommand
- 执行ExecCommandDatabase::_commandExec
_parseCommand
这一步骤的主要逻辑是调用Command的parse方法,根据request生成Invocation。见下面伪代码
void _parseCommand() {
auto command = _execContext->getCommand(); // command在调用ExecCommandDatabase之前就已经设置进context中
_invocation = command->parse(opCtx, request);
CommandInvocation::set(opCtx, _invocation); // 将生成的invocation设置到CommandInvocation中
}
_initiateCommand
这里会处理几个事情:
- 统计命令执行时间、客户端连接数等
- 设置tracking
- 初步处理命令,例如help命令、设置超时时间等
- 调用_invocation->checkAuthorization校验权限
- 检查命令是否可以执行,例如调用commandCanRunHere检查,调用command->adminOnly()检查、事务的情况下检查ReadConcern等
- 事务的情况下需要加锁
_commandExec
这一步比较简单:
- 等待ReadConcern,以及发生ReadConcern的时候的处理
- 如果是GetMore操作,调用RunCommandAndWaitForWriteConcern::run,否则调用RunCommandImpl::run(没有,又要看另外的实现了T_T)
Future<void> ExecCommandDatabase::_commandExec() {
// ReadConcern的处理
_execContext->behaviors->waitForReadConcern(opCtx, _invocation.get(), request);
_execContext->behaviors->setPrepareConflictBehaviorForReadConcern(opCtx, _invocation.get());
auto runCommand = [&] {
if (getInvocation()->supportsWriteConcern() ||
getInvocation()->definition()->getLogicalOp() == LogicalOp::opGetMore) {
return future_util::makeState<RunCommandAndWaitForWriteConcern>(this).thenWithState(
[](auto* runner) { return runner->run(); });
} else {
return future_util::makeState<RunCommandImpl>(this).thenWithState(
[](auto* runner) { return runner->run(); });
}
}
return runCommand()
}
从代码可以看到,当支持WriteConcern并且是GetMore操作的时候,会使用RunCommandAndWaitForWriteConcern::run,除此之外都是RunCommandImpl::run
RunCommandImpl
先来看看RunCommandImpl的run方法,会先后调用两个方法:
- _prologue 记录ReadConcern的信息(应该是统计用吧)
- _runImpl 调用RunCommandImpl::_runCommand执行命令,然后这里又区分了两种情况(没错,真的很复杂)
- CheckoutSessionAndInvokeCommand::run 需要检查session的时候调用
- 其余情况调用InvokeCommand::run
CheckoutSessionAndInvokeCommand::run和InvokeCommand::run的区别是CheckoutSessionAndInvokeCommand会调用_checkOutSession,两者最后都是调用runCommandInvocation方法,通过CommandHelpers::runCommandInvocation最终调用invocation->run
RunCommandAndWaitForWriteConcern
接下来看下RunCommandAndWaitForWriteConcern,看名字就是执行命令并且等到WriteConcern。看下其定义
class RunCommandAndWaitForWriteConcern final : public RunCommandImpl {
}
没错,它是继承自RunCommandImpl的,与RunCommandImpl的主要不同在于覆盖了_runImpl,在调用RunCommandImpl::_runCommand之后进行了以下操作
Future<void> RunCommandAndWaitForWriteConcern::_runImpl() {
_setup() // 主要是做一些检查,然后调用opCtx->setWriteConcern(*_extractedWriteConcern)
return _runCommandWithFailPoint() // 这里其实最后调用了RunCommandImpl::_runCommand()
.onCompletion([this](Status status) mutable {
if (status.isOK()) {
return _checkWriteConcern(); // 会调用_waitForWriteConcern等待WriteConcern
} else {
return _handleError(std::move(status)); // 处理错误,有可能会调用_waitForWriteConcern
}
});
}
总结一下
因为是逻辑有点多,所以加个流程图帮助大家捋一捋(只展示了主要的逻辑)
命令注册
这里会看一下Command是怎样注册到MongoDB中的。首先要知道通过代码构建MongoDB需要执行执行python3 buildscripts/scons.py install-mongod,这个过程会执行buildscripts中的脚本。
根据IDL生成VersionGen文件
IDL
我们先来看看创建collection的命令CmdCreate对应的idl文件src/mongo/db/commands/create.idl伪代码
structs:
CreateCommandReply:
description: 'Reply from the {create: ...} command'
strict: true
commands:
create:
description: "Parser for the 'create' Command"
command_name: create
namespace: concatenate_with_db
cpp_name: CreateCommand
api_version: "1"
这里描述了CmdCreate的基本信息。
根据IDL生成文件
在buildscripts有一个目录idl,这里负责根据src中的idl生成文件。其中主要看buildscripts/idl/idl/generator.py文件,其中有一段逻辑
for command in spec.commands:
if command.api_version:
self.generate_versioned_command_base_class(command)
结合上面的IDL例子,可以看到意思就是遍历commands数据,调用generate_versioned_command_base_class生成Command相关文件。同样以CmdCreate为例子,大概会生成这几个对象。
class CreateCmdVersion1Gen<Derived> : public TypedCommand<Derived> {
use _TypedCommandInvocationBase = typename TypedCommand<Derived>::InvocationBase
class InvocationBaseGen : public _TypedCommandInvocationBase {
}
}
然后我们的CmdCreate是长这个样子的
class CmdCreate final : public CreateCmdVersion1Gen<CmdCreate> {
public:
class Invocation final : public InvocationBaseGen {
}
}
命令注册
知道如何通过IDL生成命令相关文件,那么这个Command又是如何注册的咧?接下来我们要看看src/mongo/db/commands/commands.h和src/mongo/db/commands/commands.cpp
// 这里是Command初始化时候的代码,可以看到初始化的时候会调用registerCommand进行注册
Command::Command(StringData name, std::vector<StringData> aliases)
: _name(name.toString()),
_aliases(std::move(aliases)),
_commandsExecutedMetric("commands." + _name + ".total", &_commandsExecuted),
_commandsFailedMetric("commands." + _name + ".failed", &_commandsFailed) {
globalCommandRegistry()->registerCommand(this, _name, _aliases);
}
// 然后TypedCommand是继承Command的
template <typename Derived>
class TypedCommand : public Command {
}
看到这里应该就了解了吧。