写点什么

MongoDB 源码学习:Command 的执行与注册

作者:云里有只猫
  • 2022-11-25
    广东
  • 本文字数:3020 字

    阅读完需:约 10 分钟

MongoDB源码学习:Command的执行与注册

简介

上一篇介绍了 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::runInvokeCommand::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.hsrc/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 {}
复制代码


看到这里应该就了解了吧。

to be continue

发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2020-03-31 加入

还未添加个人简介

评论

发布
暂无评论
MongoDB源码学习:Command的执行与注册_mongodb_云里有只猫_InfoQ写作社区