我的第一次实质性开源贡献——Apache IoTDB

前言

虽然之前也在Github上尝试提过一些PR,但都是一些doc、typo等类型的入门实践。真正算得上有一定实质性工作,要数最近在Apache IoTDB上提交的一个功能PR.如果大家对开源感兴趣的话,可以看我的一篇关于开源介绍的文章(开源介绍).如果大家对文中提到的比如Github、PR等词汇不熟悉建议可以先去学习一下Git,注册使用Github网站。

如何开始入手开源呢?

首先可以根据自身情况选择一个开源项目进行着手,熟悉项目,在社区学习,先做些简单issue,慢慢推进,默默发育,升级打怪,最后成为Committer甚至PMC,可以hold住功能模块开发,重要bug修复,架构设计等等~。由于本人对于Java语言相对熟悉,同时因为某些原因对于时序数据库有一定了解,经过一番搜索最终选择了Apache IoTDB.

好了~废话不多说,回归正传。

入门熟悉项目

一般来说,一个质量比较好的开源项目,其代码仓库的README文件或者官方文档会有比较详细的开发指南。Apache IoTDB也不例外,下面是我列的几个在这当中对我帮助比较大的几个文档。

用户指南: 用户指南

代码贡献指南:代码贡献指南、如何提交PR

设计实现文档:  设计实现文档

  • 用户指南:重点是学习该项目的一些功能、概念,这在后面阅读熟悉其代码是很有帮助的,比如在Apache IoTDB里的数据模型:存储组、路径等概念对于理解里面一些类的命名与作用会起到事半功倍的效果。
  • 代码贡献指南: 参与Apache IoTDB的一些流程,重点就是订阅官方邮件,在进行相关功能开发时可以先发一个邮件给社区的成员同步一下;在JIRA上领取issue;下载代码、格式化以及调试。特别是调试,在熟悉代码时如果实在不理解可以考虑进行调试。(正所谓一言不合就调试~)。最后就是感觉开发完成,如何提交PR的一个流程介绍。
  • 设计实现文档:是社区的一些开发人员编写的一些项目的设计实现流程,不过实话实说,IoTDB的这个文档写的对于小白不是特别友好,我一般更愿意去看代码,如果有困难再去找找相关部分的设计文档辅助代码阅读。

领取issue进行开发

我在Aapche IoTDB的JIRA上领取了issue,这个issue是关于一个语法功能上的扩展。

原先IoTDB在返回结果集上进行空值过滤仅支持比较粗粒度的without null all(即针对结果集中的如果所有列都为null则过滤该行)和without null any(即针对结果集中的如果所有列都为null则过滤该行)。

现在需要支持without null子句针对某几列生效,原先默认是对一行中所有列生效。

查询总体流程熟悉

由于该功能总体上可以归结为查询功能的一个小模块,所以我首先先要对IoTDB在执行一条查询语句的总体流程有一个大致的全局概念,后面再慢慢深挖without null子句在其中的一些处理逻辑。其总体的处理逻辑如下:

  • IoTDB在总体上是属于一种客户端-服务器架构,命令行客户端在与数据库服务器通信使用的是thrift RPC框架,一个查询SQL语句过来基本都是RPC调用的TSServiceImpl的executeStatement()方法;
  • 一条SQL字符串会先经过语法分析,在IoTDB里使用的Antlr进行SQL的语法解析,在使用Visitor模式遍历SQL的AST树时会将去转换成其内部的一个QueryOperator,在经过逻辑计划优化(目前好像优化没有像传统关系型数据库一样,不是很多),然后转化成一个物理执行计划。最后将其封装成一个QueryTask对象放进线程池去执行,执行完就返回RPC执行结果。

梳理without null处理逻辑的代码位置

本着以目标为导向,开始梳理原有代码中without null子句处理逻辑位置,从中获取需要改动的位置、边界以及如何改动的灵感。在原有代码中对without null语句的处理有四处,如下所示:

1. UDTFAlignByTimeDataSet类的fillBuffer()方法

public TSQueryDataSet fillBuffer(int fetchSize, WatermarkEncoder encoder) {
、、、  
  if (withoutAllNull || withoutAnyNull) {
    int nullFieldsCnt = 0;
    for (LayerPointReader reader : transformers) {
      if (!reader.next() || reader.currentTime() != minTime || reader.isCurrentNull()) {
        nullFieldsCnt++;
      }
    }
、、、
}

2. RawQueryDataSetWithoutValueFilter类的fillBuffer()方法

public TSQueryDataSet fillBuffer(int fetchSize, WatermarkEncoder encoder) {
、、、
    if (withoutAnyNull && filterRowRecord(seriesNum, minTime)) {
	    continue;
    }
、、、
}

3. QueryDataSetUtils类的convertQueryDataSetByFetchSize()方法

public static TSQueryDataSet convertQueryDataSetByFetchSize() {
、、、
// filter rows whose columns are null according to the rule
  if ((queryDataSet.isWithoutAllNull() && rowRecord.isAllNull())
      || (queryDataSet.isWithoutAnyNull() && rowRecord.hasNullField())) {
    // if the current RowRecord doesn't satisfy, we should also decrease AlreadyReturnedRowNum
    queryDataSet.decreaseAlreadyReturnedRowNum();
    i--;
    continue;
  }
、、、
}

4. QueryDataSet类的hasNext()方法

public boolean hasNext() throws IOException {
  // proceed to the OFFSET row by skipping rows
  while (rowOffset > 0) {
    if (hasNextWithoutConstraint()) {
      RowRecord rowRecord = nextWithoutConstraint(); // DO NOT use next()
      // filter rows whose columns are null according to the rule
      if ((withoutAllNull && rowRecord.isAllNull())
          || (withoutAnyNull && rowRecord.hasNullField())) {
        continue;
      }
      rowOffset--;
    } else {
      return false;
    }
  }

  // make sure within the LIMIT constraint if exists
  if (rowLimit > 0 && alreadyReturnedRowNum >= rowLimit) {
    return false;
  }

  return hasNextWithoutConstraint();
}

解决方案初步形成

通过前面对于without null处理逻辑的梳理,不难发现都是在对结果集的每一行进行判断是不是所有列都为null或者存在null.

所以其实只要我们在这些位置如果将判断null的范围限定到without null指定的那些列,至于其他列直接忽略就行。于是一个很朴素的想法就出现了,由于它每一行是按数组索引的形式遍历每一列的值,有的不包括列名信息,如果我能找到without null指定的列名在遍历的结果集的每一行的与其相应的索引的映射关系就可以了。

所以在到达上述的四个位置时,我要维护得到一个WithoutNullColumnIndexSet集合,在遍历每一个RowRecord时去判断其为null列的索引是否在WithoutNullColumnIndexSet中即可。

遇到的一些困难

1. select语句后面跟的列名与without null指定的列名并不是那么简单的字符串相等关系,而且还有可能有别名信息,以及一些整合名(与from后的路径拼接、group by level等);一个比较典型的例子就是select * from xxx without null (s1);

解决:从Antlr语法层面,select 后面跟的column是一个expression,而刚好without null后面跟的column也是一个expression。在原有逻辑中已经包含了对解析到的select的column的表达式的一些transform(比如去*、拼接路径等等),我再处理without null可以复用这段逻辑跟着进行变换。这样在其内部处理表示中就会是一个equals的关系,这样就可以很方便地进行元数据校验(without null里指定的列是否与select的column名相一致),以及填充WithoutNullColumnIndexSet,当然这里要注意别名的考虑。

2. 原有IoTDB的Query类型众多,而且处理逻辑并不是完全统一,要考虑一些特殊类型的查询,比如AlignByDevice、对齐时间序列等

在原有的正常的AlignByTime查询中,存在一个pathToIndex变量可以帮助我更快形成WithoutNullColumnIndexSet,但在AlignByDevice中这个pathToIndex是空的,它有另一套逻辑去维护结果集列名与设备名之间的关系。

而对齐时间序列的数据类型,并不想原始数据类型,它是一个类似数组的结构,在原有RowRecord中它只占一列但其实其内部会展开成多列,原来逻辑只会告诉你它内部包含的列是否全为null或者部分为null。如果without null指定其中的某几列就会出现不正确的问题。针对这种情况在维护index信息时就不能使用外部的那个只占一列信息,而应该去遍历其内部包含的列名,从而得到真正的正确的WithoutNullColumnIndexSet。

3. 原有代码中,without null存在一个bug

提bug issue,先把这个bug解决再继续接下来的开发。

提交PR

在开发完成,编写自测通过、编写相应的用户使用文档后,根据上面的提交PR指南,提了PR,功能PR是有两位社区人员进行Review,根据他们提出的一些建议与问题,前前后后又改了几天,最后终于Merge了~。

总结

本次的实践,算是对自己开源入门的一次实战。在这当中,自己熟悉了整个的一套开源开发流程,熟悉了Apache IoTDB的部分源码,同时对自己代码阅读能力有一定提升,促进了自己的代码规范,总而言之还是好处多多。当然这个Merge了不是说没有问题,他内部可能还会要进行测试,没准哪天就会通知我修bug了。好了,今天就说到这,希望自己可以坚持下去,持续做好开源贡献!

你可能感兴趣的:(开源,项目,Java,IoTDB,database,开源,Apache)