EOS中plugin之producer_plugin
EOS中的插件是非常重要的工具,其中大大小小总共有26个插件,其中比较重要的插件有chain_plugin、producer_plugin、http_plugin、net_plugin等四个插件。这四个插件在EOS服务器端启动后也开始启动进行工作。
abstract_plugin
EOS中所有插件继承于plugin类,而plugin类又继承于abstract_plugin类。
abstract_plugin中规定了每个插件的4个状态,这4个状态依次如下。
- registered,表示插件已经注册, 每个插件初始化后就是注册的状态。
- initialized,表示插件已经初始化,插件之后注册之后才能初始化。
- started, 表示插件正在运行中。
- stopped,表示插件停止运行。
下面是abstract_plugin的源代码,由于是个抽象类,abstract_plugin定义在appbase\appbase\plugin.hpp中。
1 | class abstract_plugin { |
plugin
plugin这个类继承了abstract_plugin,plugin的代码在appbase\apppbase\apllication.hpp中,这里比较奇怪的是,为什么plugin的定义不再plugin.hpp文件中,而plugin.hpp中定义了abstract_plugin类。
plugin是一个模板类,在plugin中,除去handle_sighup函数之外,已经实现了abstract_plugin中的全部纯虚函数。handle_sighup函数的含义,目前我不太清楚。
1 | template<typename Impl> |
首先可以看到plugin的初始化函数,由于plugin是一个模板类,在初始化过程中,传入的模板类的名称赋值给了plugin的protected变量_name,里面唯一不太理解的是,为什么这个_name是protected的类型而不是private类型。typeid在头文件typeindex中,主要是获得某个类的名称。
此外,plugin中实现了插件的startup()、initialized()和shutdown()三个功能。在initialize()函数中,首先判断当前状态是否是registered状态,随后更改状态为initialized状态,然后查询当前插件依赖的其他插件并且将其他插件初始化,随后初始化插件自身。注意到插件启动完成后有如下一句代码:
1 | app().plugin_initialized(*this); |
app()函数返回客户端的引用,随后客户端将当前初始化的插件变量放入到当前客户端中已初始化插件的vector中。
在startup()之前,可以看到,插件启动之前,其状态必须是已经初始化的状态,这表明插件启动之前必须初始化。随后设置状态为启动状态。然后查询当前插件启动需要的其他插件并启动这些插件,随后再启动插件自身,随后客户端将当前已启动的插件变量放入到当前客户端中已启动插件的vector中。
在shutdown()函数中,特别厉害的一个设计就是关闭插件的调用,因为不同的插件,可能关闭方式不一样,但是在plugin中仍然也实现了这个方法,实在是太巧妙了。最精髓的就是这句代码:
1 | static_cast<Impl*>(this)->plugin_shutdown(); |
将当前指针转换成模板类初始化时传进的类,然后调用这个类的plugin_shutdown()函数。好像很牛批,可是我仔细一想,最后不还是调用每个类自己的plugin_shutdown()函数了吗,每个类中自己实现这个功能好像也可以,这样设计就是看起来很牛批,但是没什么卵用,奇技淫巧而已~
现在介绍下EOS具体的插件,producer_plugin,这个插件主要负责超级节点的区块生产、同步以及新区快的校验工作,是4个插件里面最重要的一个插件。
producer_plugin
(1). producer_plugin.hpp
producer_plugin插件的实现位于plugins\producer_plugin下。producer_plugin类的定义,就很头铁,定义方式如下。
1 | class producer_plugin : public appbase::plugin<producer_plugin>{ |
在定义的过程中就传入了producer_plugin的类名,这个名称会传给plugin中的_name变量。
为什么会有appbase这个玩意儿呢,因为这是一个命名空间,plugin就定义在appbase命名空间中。
接下来我们看看producer_plugin的头文件,由于头文件比较长,口味略重,非战斗人员请迅速撤离。
1 | class producer_plugin : public appbase::plugin<producer_plugin> { |
producer_plugin定义完毕之后第一个public函数就很恶心咯,这是什么鬼!!!仔细看看这些大写字母的定义,转换成小写字母,即appbase_plugin_requires,还记得前面说每个插件初始化或者启动的时候都要初始化或者启动它依赖的插件这个过程吗?这感觉应该是说producer_plugin类启动的时候依赖chain_plugin和http_client_plugin两个插件。
1 | APPBASE_PLUGIN_REQUIRES((chain_plugin)(http_client_plugin)) |
大写的显示,应该是说明这一个宏定义,果然,我们翻了一下,宏定义在plugin.hpp文件中,具体定义如下:
1 |
哇~~~,越来越恶心了,这个宏定义,指向一个模板函数,这个模板函数的参数是一个函数,里面用到了BOOST_PP_SEQ_FOR_EACH,这个一般如下用法:
- BOOST_PP_SEQ_FOR_EACH(macro, r, data)
- macro,一个以格式macro(r, data, elem)定义的三元宏。该宏被BOOST_PP_SEQ_FOR_EACH按照seq中每个元素进行展开。展开该宏,需要用到下一个BOOST_PP_FOR的重复项、备用数据data和当前元素。
- data,备用数据,用于传给macro。
- seq,用于供macro按照哪个序列进行展开。
具体的细节尚未弄清楚,但是明确的是,这个hpp文件可以看出producer_plugin插件依赖两个插件,即chain_plugin和http_client_plugin。
随后再producer_plugin中定义了一系列结构体。这些结构体都用到了一个optional的类。简单地来说,这个类类似于boost::optional的概念,主要用于实现未初始化的概念。函数并不能总是返回有意义的结果,有时候函数可能返回“无意义”的值,一般来说我们通常使用一个不再正常解空间的一个哨兵来表示无意义的概念,如NULL,-1,end()或者EOF.然后对于不同的应用场合,这些哨兵并不是通用的,而且有时候可能不存在这种解空间之外的哨兵。optional很像一个仅能存放一个元素的容器,它实现了"未初始化"的概念:如果元素未初始化,那么容器就是空的,否则,容器内就是有效的,已经初始化的值。optional的真实接口很复杂,因为它要能包装任何的类型。
定义的这些结构体,我想应该是producer_plugin运行时配置的一些参数,大致知道这个概念即可。随后是producer_plugin的构造函数、析构函数以及设置参数的函数。更加详细的内容,我已经写在注释中,可以参看,至于producer_plugin中一些方法的具体实现,暂时不予理会,知道其作用就可以了。
(2). plugin_startup
producer_plugin.cpp中插件的plugin_shutdown, pause, resume等函数不是很重要,因此这里不再介绍。但是中最重要的一个函数是plugin_startup函数,nodeos节点启动之后,启动producer_plugin插件的接口就是这个函数,这里需对这个函数进行详细的解析,但是我们仍然会跳过一些细枝末节的东西,重点看其主要脉络。由于源码中这个函数内容比较多,这里不再展示源码,直接从主要流程开始。
在函数里面首先获取一个变量:chain
1 | chain::controller &chain = my->chain->chain_plug->chain() |
my是一个produer_plugin_impl类的智能指针,这个指针具体负责交易的打包、区块的生产和检查等工作。通过chain可以访问到区块链上的一些信息。
随后chain连接两个信号量,分别执行的函数是on_block()和on_irreversible_block。
1 | my->_accepted_block_connection.emplace(chain.accepted_block.connect([this](const auto &bsp) { my->on_block(bsp); })); |
在boost中,进行connect之后返回一个connection的对象,my将返回的两个对象放入自己的connection容器中。
随后获得了区块链上最近的一个不可逆区块的块号和其指针
1 | const auto lib_num = chain.last_irreversible_block_num();// 返回块号 |
如果当前区块指针不为空,则进入my->on_irreversible_block(lib)函数。my->on_irreversible_block函数就是将不可逆区块的时间设置为lib指向的区块的时间。
如果lib指针为空,设置不可逆区块的时间为当前时间的最大值,即设置为0xffffffff秒,时间大致为139年,那时候BM应该早就跪了~
1 | if (lib){ |
接下来,判断my中的区块生产者列表是否为空,若当前节点可以生产区块,如果是第一次接入的超节点,则展示new_chain_banner标志。随后开始进入区块生产的大循环中,区块生产的函是由my控制的。看起来my这个插件无法避免的需要介绍了。
1 | if (my->_production_enabled){ |
至此,plugin_startup函数的主要情况已经介绍完毕,my->schedule_produetion_loop函数的介绍,将在produer_plugin_impl类的介绍中进行。
producer_plugin_impl
producer_plugin_impl类中的变量
produer_plugin_impl类的定义,上来我就懵逼了,这是什么!!!enable_shared_from_this也是一个类,但是这种继承的形式到底是怎么肥四?
1 | class producer_plugin_impl : public std::enable_shared_from_this<producer_plugin_impl> |
查了一下网上的解释,enable_shared_from_this是c++ 11的新特性,使用这个特性就可以获得一个对象的多个shared_ptr指针,但是又不会造成对象的多次释放问题。具体的解释可以参看下面两个链接:
其初始化函数只有1个,里面_timer完成io操作,_transaction_ack_channel主要接收交易,具体形式如下:
1 | public: |
老实说,produer_plugin_impl中定义的变量很多,这些变量都是public属性。因为这个类具体负责区块生产,因此涉及到的事务非常繁杂,所以变量比较多也算正常。
1 |
|
可能会奇怪,为什么my指针中的变量,在其初始化函数中都没有看到初始化的工作,例如其中的chain_plugin和producer_plugin指针。实际上,my中这些变量的初始化工作已经在producer_plugin的initialized函数中初始化完毕了。
schedule_production_loop 函数
producer_plugin的startup函数中就是调用了producer_plugin_impl中的shcedule_production_loop函数开始生产区块。因此,我们首先从这个函数入手,开始分析producer_plugin_impl是如何生产区块的。
函数首先获得了chain的引用和指向producer_plugin_impl的一个weak_ptr指针,关闭了_timer的io操作。
1 | chain::controller &chain = chain_plug->chain(); |
随后尝试生产区块,调用了start_block函数,start_block函数返回结果有succeed,failed,waiting,exhausted4种,针对4中情况分别执行不用的流程。
-
failed, 获取各种调度信息异常,则重新获取数据进行调度;
-
waitting,其它节点正在出块,则进行等待;
-
producing,轮到本节点出块,则进行出块操作;
-
succeed,生产区块成功,计算下一个生产者出块的时间。
当start_block()返回结果为failed的时,进入异步等待状态,稍后再尝试生产区块。
1 | if (result == start_block_result::failed) |
如果返回结果是waiting,表示本地的区块链还在从eos网络中下载区块已进行信息同步过程中,这个过程中还无法生产区块。随后查看当前是否还能继续生产区块,如果可以,则待会儿生产区块,即调用函数schedule_delayed_production_loop,这个函数这里不再进行仔细分析,主要就是过一段时间后这个函数中还会调用schedule_production_loop函数进行区块生产。
返回结果时waiting的情况下,对应代码如下:
1 | else if (result == start_block_result::waiting) |
如果返回结果是speculateing模式,并且还可以继续进行区块生产,但是生产者不确定这个区块是否合法,所以小心翼翼的过一会儿继续生产区块,过程如下:
1 | else if (_pending_block_mode == pending_block_mode::speculating && !_producers.empty() && !production_disabled_by_policy()) |
如果返回结果是producing模式,表示已经成功的生产了一个区块,但是还需要其他一些验证工作。这里有个问题就是,为什么speculating模式下没有验证,非得在producing模式下验证生产的区块呢?
首先验证区块截止时间是否大于当前时间,如果大于当前时间,表明生产区块成功在截止日期内,是合法的区块,然后记录日志。如果截止日期超过现在,表明生产的区块已经过期了,此时将生产区块的时间减小0.5s,看是否超时并记录日志,这一步完成之后进行区块的同步操作。
代码如下:
1 | else if (_pending_block_mode == pending_block_mode::producing) |
如果对其他一些细节的代码不太明白,也不必在意,因为我也不太清楚。但是我们注意到同步操作中的一段代码,如下:
1 | _timer.async_wait( |
_timer.async_wait应该是将生产出来的在群里面广播。这个函数接受一个lambda函数,表示同步的方法,同步过程中有一个maybe_produce_block()函数,我想应该是某个block procuder(以后简称BP)生产了一个区块,但是还没有得到其他BP的确认,这是个进行同步请求确认的过程。因此,我们再仔细看看进行同步的细节(不过貌似很多细节我也看不懂哎…囧…)。
mayb_produce_block函数的主要内容如下:
1 | bool producer_plugin_impl::maybe_produce_block() |
上来就是一句我不懂的函数:
1 | auto reschedule = fc::make_scoped_exit( |
也就是说,往里面传了一个lambda函数,函数就是重复生产区块的函数。但是fc::make_scope_exit又是什么意思呢?这里面大概介绍一下,make_scoped_exit函数中传入一个lambda函数,may_produe_block函数结束时reschedule函数只是定义了一下,reschedule隶属于scoped_exit类,这个类的析构函数中调用了schedule_production_loop();表明函数结束之后继续生产区块。
随后may_produce_block中有一个重要的函数,即produce_block()函数,我们继续追踪,这个函数代码如下:
1 | void producer_plugin_impl::produce_block() |
至此,大致对producer_plugin这个插件的内容以及主要功能有一个大致的了解。
这个插件主要负责区块的接收、检验、打包和本地写入功能。