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

342 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

简介

上一篇介绍了CommandOpRunner,接下来会讲一下ExecCommandDatabase的执行流程,以及Command是如何注册到MongoDB中。

Command与Invocation

Command

主要文件在 src/mongo/db/commands.h中,下来我们看下Command的结构

Command-类图.jpeg

Invocation

作为Command的内部类,其主要的是typedRun方法,处理了主要的执行命令的逻辑,并且返回Reply。

Invocation-类图.jpeg

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
        }
    });
}

总结一下

因为是逻辑有点多,所以加个流程图帮助大家捋一捋(只展示了主要的逻辑)

ExecCommandDatabase-流程图.jpeg

命令注册

这里会看一下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