diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000000..eb7cbe4b12 --- /dev/null +++ b/NOTICE @@ -0,0 +1,39 @@ +======================================================== +DataX 是阿里云 DataWorks数据集成 的开源版本,在阿里巴巴集团内被广泛使用的离线数据同步工具/平台。DataX 实现了包括 MySQL、Oracle、OceanBase、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、Hologres、DRDS 等各种异构数据源之间高效的数据同步功能。 + +DataX is an open source offline data synchronization tool / platform widely used in Alibaba group and other companies. DataX implements efficient data synchronization between heterogeneous data sources including mysql, Oracle, oceanbase, sqlserver, postgre, HDFS, hive, ads, HBase, tablestore (OTS), maxcompute (ODPs), hologres, DRDS, etc. + +Copyright 1999-2022 Alibaba Group Holding Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +=================================================================== +文级别引用,按许可证 +This product contains various third-party components under other open source licenses. +This section summarizes those components and their licenses. +GNU Lesser General Public License +-------------------------------------- +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/CliQuery.java +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/Connection4TSDB.java +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/DataPoint4TSDB.java +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/DumpSeries.java +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/OpenTSDBConnection.java +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/OpenTSDBDump.java +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/Constant.java +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/Key.java +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/OpenTSDBReader.java +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/OpenTSDBReaderErrorCode.java +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/HttpUtils.java +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/TSDBUtils.java +opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/TimeUtils.java +=================================================================== diff --git a/README.md b/README.md index a8f5672d70..0125bee8cd 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ ![Datax-logo](https://github.com/alibaba/DataX/blob/master/images/DataX-logo.jpg) - # DataX -DataX 是阿里巴巴集团内被广泛使用的离线数据同步工具/平台,实现包括 MySQL、Oracle、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、DRDS 等各种异构数据源之间高效的数据同步功能。 +[![Leaderboard](https://img.shields.io/badge/DataX-%E6%9F%A5%E7%9C%8B%E8%B4%A1%E7%8C%AE%E6%8E%92%E8%A1%8C%E6%A6%9C-orange)](https://opensource.alibaba.com/contribution_leaderboard/details?projectValue=datax) + +DataX 是阿里云 [DataWorks数据集成](https://www.aliyun.com/product/bigdata/ide) 的开源版本,在阿里巴巴集团内被广泛使用的离线数据同步工具/平台。DataX 实现了包括 MySQL、Oracle、OceanBase、SqlServer、Postgre、HDFS、Hive、ADS、HBase、TableStore(OTS)、MaxCompute(ODPS)、Hologres、DRDS, databend 等各种异构数据源之间高效的数据同步功能。 # DataX 商业版本 -阿里云DataWorks数据集成是DataX团队在阿里云上的商业化产品,致力于提供复杂网络环境下、丰富的异构数据源之间高速稳定的数据移动能力,以及繁杂业务背景下的数据同步解决方案。目前已经支持云上近3000家客户,单日同步数据超过3万亿条。DataWorks数据集成目前支持离线50+种数据源,可以进行整库迁移、批量上云、增量同步、分库分表等各类同步解决方案。2020年更新实时同步能力,2020年更新实时同步能力,支持10+种数据源的读写任意组合。提供MySQL,Oracle等多种数据源到阿里云MaxCompute,Hologres等大数据引擎的一键全增量同步解决方案。 +阿里云DataWorks数据集成是DataX团队在阿里云上的商业化产品,致力于提供复杂网络环境下、丰富的异构数据源之间高速稳定的数据移动能力,以及繁杂业务背景下的数据同步解决方案。目前已经支持云上近3000家客户,单日同步数据超过3万亿条。DataWorks数据集成目前支持离线50+种数据源,可以进行整库迁移、批量上云、增量同步、分库分表等各类同步解决方案。2020年更新实时同步能力,支持10+种数据源的读写任意组合。提供MySQL,Oracle等多种数据源到阿里云MaxCompute,Hologres等大数据引擎的一键全增量同步解决方案。 -https://www.aliyun.com/product/bigdata/ide +商业版本参见: https://www.aliyun.com/product/bigdata/ide # Features @@ -25,7 +26,8 @@ DataX本身作为数据同步框架,将不同数据源的同步抽象为从源 # Quick Start -##### Download [DataX下载地址](http://datax-opensource.oss-cn-hangzhou.aliyuncs.com/datax.tar.gz) +##### Download [DataX下载地址](https://datax-opensource.oss-cn-hangzhou.aliyuncs.com/202308/datax.tar.gz) + ##### 请点击:[Quick Start](https://github.com/alibaba/DataX/blob/master/userGuid.md) @@ -35,36 +37,52 @@ DataX本身作为数据同步框架,将不同数据源的同步抽象为从源 DataX目前已经有了比较全面的插件体系,主流的RDBMS数据库、NOSQL、大数据计算系统都已经接入,目前支持数据如下图,详情请点击:[DataX数据源参考指南](https://github.com/alibaba/DataX/wiki/DataX-all-data-channels) -| 类型 | 数据源 | Reader(读) | Writer(写) |文档| -| ------------ | ---------- | :-------: | :-------: |:-------: | -| RDBMS 关系型数据库 | MySQL | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/mysqlreader/doc/mysqlreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/mysqlwriter/doc/mysqlwriter.md)| -|             | Oracle     |     √     |     √     |[读](https://github.com/alibaba/DataX/blob/master/oraclereader/doc/oraclereader.md) 、[写](https://github.com/alibaba/DataX/blob/master/oraclewriter/doc/oraclewriter.md)| -| | SQLServer | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/sqlserverreader/doc/sqlserverreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/sqlserverwriter/doc/sqlserverwriter.md)| -| | PostgreSQL | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/postgresqlreader/doc/postgresqlreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/postgresqlwriter/doc/postgresqlwriter.md)| -| | DRDS | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/drdsreader/doc/drdsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/drdswriter/doc/drdswriter.md)| -| | 通用RDBMS(支持所有关系型数据库) | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/rdbmsreader/doc/rdbmsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/rdbmswriter/doc/rdbmswriter.md)| -| 阿里云数仓数据存储 | ODPS | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/odpsreader/doc/odpsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/odpswriter/doc/odpswriter.md)| -| | ADS | | √ |[写](https://github.com/alibaba/DataX/blob/master/adswriter/doc/adswriter.md)| -| | OSS | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/ossreader/doc/ossreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/osswriter/doc/osswriter.md)| -| | OCS | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/ocsreader/doc/ocsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/ocswriter/doc/ocswriter.md)| -| NoSQL数据存储 | OTS | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/otsreader/doc/otsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/otswriter/doc/otswriter.md)| -| | Hbase0.94 | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/hbase094xreader/doc/hbase094xreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/hbase094xwriter/doc/hbase094xwriter.md)| -| | Hbase1.1 | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/hbase11xreader/doc/hbase11xreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/hbase11xwriter/doc/hbase11xwriter.md)| -| | Phoenix4.x | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/hbase11xsqlreader/doc/hbase11xsqlreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/hbase11xsqlwriter/doc/hbase11xsqlwriter.md)| -| | Phoenix5.x | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/hbase20xsqlreader/doc/hbase20xsqlreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/hbase20xsqlwriter/doc/hbase20xsqlwriter.md)| -| | MongoDB | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/mongodbreader/doc/mongodbreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/mongodbwriter/doc/mongodbwriter.md)| -| | Hive | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/hdfsreader/doc/hdfsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/hdfswriter/doc/hdfswriter.md)| -| | Cassandra | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/cassandrareader/doc/cassandrareader.md) 、[写](https://github.com/alibaba/DataX/blob/master/cassandrawriter/doc/cassandrawriter.md)| -| 无结构化数据存储 | TxtFile | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/txtfilereader/doc/txtfilereader.md) 、[写](https://github.com/alibaba/DataX/blob/master/txtfilewriter/doc/txtfilewriter.md)| -| | FTP | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/ftpreader/doc/ftpreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/ftpwriter/doc/ftpwriter.md)| -| | HDFS | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/hdfsreader/doc/hdfsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/hdfswriter/doc/hdfswriter.md)| -| | Elasticsearch | | √ |[写](https://github.com/alibaba/DataX/blob/master/elasticsearchwriter/doc/elasticsearchwriter.md)| -| 时间序列数据库 | OpenTSDB | √ | |[读](https://github.com/alibaba/DataX/blob/master/opentsdbreader/doc/opentsdbreader.md)| -| | TSDB | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/tsdbreader/doc/tsdbreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/tsdbwriter/doc/tsdbhttpwriter.md)| +| 类型 | 数据源 | Reader(读) | Writer(写) | 文档 | +|--------------|---------------------------|:---------:|:---------:|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| +| RDBMS 关系型数据库 | MySQL | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/mysqlreader/doc/mysqlreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/mysqlwriter/doc/mysqlwriter.md) | +| | Oracle | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/oraclereader/doc/oraclereader.md) 、[写](https://github.com/alibaba/DataX/blob/master/oraclewriter/doc/oraclewriter.md) | +| | OceanBase | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/oceanbasev10reader/doc/oceanbasev10reader.md) 、[写](https://github.com/alibaba/DataX/blob/master/oceanbasev10writer/doc/oceanbasev10writer.md) | +| | SQLServer | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/sqlserverreader/doc/sqlserverreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/sqlserverwriter/doc/sqlserverwriter.md) | +| | PostgreSQL | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/postgresqlreader/doc/postgresqlreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/postgresqlwriter/doc/postgresqlwriter.md) | +| | DRDS | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/drdsreader/doc/drdsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/drdswriter/doc/drdswriter.md) | +| | Kingbase | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/drdsreader/doc/drdsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/drdswriter/doc/drdswriter.md) | +| | 通用RDBMS(支持所有关系型数据库) | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/rdbmsreader/doc/rdbmsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/rdbmswriter/doc/rdbmswriter.md) | +| 阿里云数仓数据存储 | ODPS | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/odpsreader/doc/odpsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/odpswriter/doc/odpswriter.md) | +| | ADB | | √ | [写](https://github.com/alibaba/DataX/blob/master/adbmysqlwriter/doc/adbmysqlwriter.md) | +| | ADS | | √ | [写](https://github.com/alibaba/DataX/blob/master/adswriter/doc/adswriter.md) | +| | OSS | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/ossreader/doc/ossreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/osswriter/doc/osswriter.md) | +| | OCS | | √ | [写](https://github.com/alibaba/DataX/blob/master/ocswriter/doc/ocswriter.md) | +| | Hologres | | √ | [写](https://github.com/alibaba/DataX/blob/master/hologresjdbcwriter/doc/hologresjdbcwriter.md) | +| | AnalyticDB For PostgreSQL | | √ | 写 | +| 阿里云中间件 | datahub | √ | √ | 读 、写 | +| | SLS | √ | √ | 读 、写 | +| 图数据库 | 阿里云 GDB | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/gdbreader/doc/gdbreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/gdbwriter/doc/gdbwriter.md) | +| | Neo4j | | √ | [写](https://github.com/alibaba/DataX/blob/master/neo4jwriter/doc/neo4jwriter.md) | +| NoSQL数据存储 | OTS | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/otsreader/doc/otsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/otswriter/doc/otswriter.md) | +| | Hbase0.94 | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/hbase094xreader/doc/hbase094xreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/hbase094xwriter/doc/hbase094xwriter.md) | +| | Hbase1.1 | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/hbase11xreader/doc/hbase11xreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/hbase11xwriter/doc/hbase11xwriter.md) | +| | Phoenix4.x | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/hbase11xsqlreader/doc/hbase11xsqlreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/hbase11xsqlwriter/doc/hbase11xsqlwriter.md) | +| | Phoenix5.x | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/hbase20xsqlreader/doc/hbase20xsqlreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/hbase20xsqlwriter/doc/hbase20xsqlwriter.md) | +| | MongoDB | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/mongodbreader/doc/mongodbreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/mongodbwriter/doc/mongodbwriter.md) | +| | Cassandra | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/cassandrareader/doc/cassandrareader.md) 、[写](https://github.com/alibaba/DataX/blob/master/cassandrawriter/doc/cassandrawriter.md) | +| 数仓数据存储 | StarRocks | √ | √ | 读 、[写](https://github.com/alibaba/DataX/blob/master/starrockswriter/doc/starrockswriter.md) | +| | ApacheDoris | | √ | [写](https://github.com/alibaba/DataX/blob/master/doriswriter/doc/doriswriter.md) | +| | ClickHouse | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/clickhousereader/doc/clickhousereader.md) 、[写](https://github.com/alibaba/DataX/blob/master/clickhousewriter/doc/clickhousewriter.md) | +| | Databend | | √ | [写](https://github.com/alibaba/DataX/blob/master/databendwriter/doc/databendwriter.md) | +| | Hive | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/hdfsreader/doc/hdfsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/hdfswriter/doc/hdfswriter.md) | +| | kudu | | √ | [写](https://github.com/alibaba/DataX/blob/master/hdfswriter/doc/hdfswriter.md) | +| | selectdb | | √ | [写](https://github.com/alibaba/DataX/blob/master/selectdbwriter/doc/selectdbwriter.md) | +| 无结构化数据存储 | TxtFile | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/txtfilereader/doc/txtfilereader.md) 、[写](https://github.com/alibaba/DataX/blob/master/txtfilewriter/doc/txtfilewriter.md) | +| | FTP | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/ftpreader/doc/ftpreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/ftpwriter/doc/ftpwriter.md) | +| | HDFS | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/hdfsreader/doc/hdfsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/hdfswriter/doc/hdfswriter.md) | +| | Elasticsearch | | √ | [写](https://github.com/alibaba/DataX/blob/master/elasticsearchwriter/doc/elasticsearchwriter.md) | +| 时间序列数据库 | OpenTSDB | √ | | [读](https://github.com/alibaba/DataX/blob/master/opentsdbreader/doc/opentsdbreader.md) | +| | TSDB | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/tsdbreader/doc/tsdbreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/tsdbwriter/doc/tsdbhttpwriter.md) | +| | TDengine | √ | √ | [读](https://github.com/alibaba/DataX/blob/master/tdenginereader/doc/tdenginereader-CN.md) 、[写](https://github.com/alibaba/DataX/blob/master/tdenginewriter/doc/tdenginewriter-CN.md) | # 阿里云DataWorks数据集成 -目前DataX的已有能力已经全部融和进阿里云的数据集成,并且比DataX更加高效、安全,同时数据集成具备DataX不具备的其它高级特性和功能。可以理解问数据集成是DataX的全面升级的商业化用版本,为企业可以提供稳定、可靠、安全的数据传输服务。于DataX相比,数据集成主要有以下几大突出特点: +目前DataX的已有能力已经全部融和进阿里云的数据集成,并且比DataX更加高效、安全,同时数据集成具备DataX不具备的其它高级特性和功能。可以理解为数据集成是DataX的全面升级的商业化用版本,为企业可以提供稳定、可靠、安全的数据传输服务。与DataX相比,数据集成主要有以下几大突出特点: 支持实时同步: @@ -82,12 +100,53 @@ DataX目前已经有了比较全面的插件体系,主流的RDBMS数据库、N - 整库迁移:https://help.aliyun.com/document_detail/137809.html - 批量上云:https://help.aliyun.com/document_detail/146671.html - 更新更多能力请访问:https://help.aliyun.com/document_detail/137663.html - + - # 我要开发新的插件 请点击:[DataX插件开发宝典](https://github.com/alibaba/DataX/blob/master/dataxPluginDev.md) +# 重要版本更新说明 + +DataX 后续计划月度迭代更新,也欢迎感兴趣的同学提交 Pull requests,月度更新内容如下。 + +- [datax_v202309](https://github.com/alibaba/DataX/releases/tag/datax_v202309) + - 支持Phoenix 同步数据添加 where条件 + - 支持华为 GuassDB读写插件 + - 修复ClickReader 插件运行报错 Can't find bundle for base name + - 增加 DataX调试模块 + - 修复 orc空文件报错问题 + - 优化obwriter性能 + - txtfilewriter 增加导出为insert语句功能支持 + - HdfsReader/HdfsWriter 支持parquet读写能力 + +- [datax_v202308](https://github.com/alibaba/DataX/releases/tag/datax_v202308) + - OTS 插件更新 + - databend 插件更新 + - Oceanbase驱动修复 + + +- [datax_v202306](https://github.com/alibaba/DataX/releases/tag/datax_v202306) + - 精简代码 + - 新增插件(neo4jwriter、clickhousewriter) + - 优化插件、修复问题(oceanbase、hdfs、databend、txtfile) + + +- [datax_v202303](https://github.com/alibaba/DataX/releases/tag/datax_v202303) + - 精简代码 + - 新增插件(adbmysqlwriter、databendwriter、selectdbwriter) + - 优化插件、修复问题(sqlserver、hdfs、cassandra、kudu、oss) + - fastjson 升级到 fastjson2 + +- [datax_v202210](https://github.com/alibaba/DataX/releases/tag/datax_v202210) + - 涉及通道能力更新(OceanBase、Tdengine、Doris等) + +- [datax_v202209](https://github.com/alibaba/DataX/releases/tag/datax_v202209) + - 涉及通道能力更新(MaxCompute、Datahub、SLS等)、安全漏洞更新、通用打包更新等 + +- [datax_v202205](https://github.com/alibaba/DataX/releases/tag/datax_v202205) + - 涉及通道能力更新(MaxCompute、Hologres、OSS、Tdengine等)、安全漏洞更新、通用打包更新等 + # 项目成员 @@ -135,20 +194,10 @@ This software is free to use under the Apache License [Apache license](https://g 8. 对高并发、高稳定可用性、高性能、大数据处理有过实际项目及产品经验者优先考虑; 9. 有大数据产品、云产品、中间件技术解决方案者优先考虑。 ```` -钉钉用户群: - -- DataX开源用户交流群 - - -- DataX开源用户交流群2 - - +用户咨询支持: -- DataX开源用户交流群3 - - +钉钉群目前暂时受到了一些管控策略影响,建议大家有问题优先在这里提交问题 Issue,DataX研发和社区会定期回答Issue中的问题,知识库丰富后也能帮助到后来的使用者。 -- DataX开源用户交流群4 - - -- DataX开源用户交流群5 - - diff --git a/adbmysqlwriter/doc/adbmysqlwriter.md b/adbmysqlwriter/doc/adbmysqlwriter.md new file mode 100644 index 0000000000..27ac6b109e --- /dev/null +++ b/adbmysqlwriter/doc/adbmysqlwriter.md @@ -0,0 +1,338 @@ +# DataX AdbMysqlWriter + + +--- + + +## 1 快速介绍 + +AdbMysqlWriter 插件实现了写入数据到 ADB MySQL 目的表的功能。在底层实现上, AdbMysqlWriter 通过 JDBC 连接远程 ADB MySQL 数据库,并执行相应的 `insert into ...` 或者 ( `replace into ...` ) 的 SQL 语句将数据写入 ADB MySQL,内部会分批次提交入库。 + +AdbMysqlWriter 面向ETL开发工程师,他们使用 AdbMysqlWriter 从数仓导入数据到 ADB MySQL。同时 AdbMysqlWriter 亦可以作为数据迁移工具为DBA等用户提供服务。 + + +## 2 实现原理 + +AdbMysqlWriter 通过 DataX 框架获取 Reader 生成的协议数据,AdbMysqlWriter 通过 JDBC 连接远程 ADB MySQL 数据库,并执行相应的 `insert into ...` 或者 ( `replace into ...` ) 的 SQL 语句将数据写入 ADB MySQL。 + + +* `insert into...`(遇到主键重复时会自动忽略当前写入数据,不做更新,作用等同于`insert ignore into`) + +##### 或者 + +* `replace into...`(没有遇到主键/唯一性索引冲突时,与 insert into 行为一致,冲突时会用新行替换原有行所有字段) 的语句写入数据到 MySQL。出于性能考虑,采用了 `PreparedStatement + Batch`,并且设置了:`rewriteBatchedStatements=true`,将数据缓冲到线程上下文 Buffer 中,当 Buffer 累计到预定阈值时,才发起写入请求。 + +
+ + 注意:整个任务至少需要具备 `insert/replace into...` 的权限,是否需要其他权限,取决于你任务配置中在 preSql 和 postSql 中指定的语句。 + + +## 3 功能说明 + +### 3.1 配置样例 + +* 这里使用一份从内存产生到 ADB MySQL 导入的数据。 + +```json +{ + "job": { + "setting": { + "speed": { + "channel": 1 + } + }, + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "column" : [ + { + "value": "DataX", + "type": "string" + }, + { + "value": 19880808, + "type": "long" + }, + { + "value": "1988-08-08 08:08:08", + "type": "date" + }, + { + "value": true, + "type": "bool" + }, + { + "value": "test", + "type": "bytes" + } + ], + "sliceRecordCount": 1000 + } + }, + "writer": { + "name": "adbmysqlwriter", + "parameter": { + "writeMode": "replace", + "username": "root", + "password": "root", + "column": [ + "*" + ], + "preSql": [ + "truncate table @table" + ], + "connection": [ + { + "jdbcUrl": "jdbc:mysql://ip:port/database?useUnicode=true", + "table": [ + "test" + ] + } + ] + } + } + } + ] + } +} + +``` + + +### 3.2 参数说明 + +* **jdbcUrl** + + * 描述:目的数据库的 JDBC 连接信息。作业运行时,DataX 会在你提供的 jdbcUrl 后面追加如下属性:yearIsDateType=false&zeroDateTimeBehavior=convertToNull&rewriteBatchedStatements=true + + 注意:1、在一个数据库上只能配置一个 jdbcUrl + 2、一个 AdbMySQL 写入任务仅能配置一个 jdbcUrl + 3、jdbcUrl按照MySQL官方规范,并可以填写连接附加控制信息,比如想指定连接编码为 gbk ,则在 jdbcUrl 后面追加属性 useUnicode=true&characterEncoding=gbk。具体请参看 Mysql官方文档或者咨询对应 DBA。 + + * 必选:是
+ + * 默认值:无
+ +* **username** + + * 描述:目的数据库的用户名
+ + * 必选:是
+ + * 默认值:无
+ +* **password** + + * 描述:目的数据库的密码
+ + * 必选:是
+ + * 默认值:无
+ +* **table** + + * 描述:目的表的表名称。只能配置一个 AdbMySQL 的表名称。 + + 注意:table 和 jdbcUrl 必须包含在 connection 配置单元中 + + * 必选:是
+ + * 默认值:无
+ +* **column** + + * 描述:目的表需要写入数据的字段,字段之间用英文逗号分隔。例如: "column": ["id", "name", "age"]。如果要依次写入全部列,使用`*`表示, 例如: `"column": ["*"]`。 + + **column配置项必须指定,不能留空!** + + 注意:1、我们强烈不推荐你这样配置,因为当你目的表字段个数、类型等有改动时,你的任务可能运行不正确或者失败 + 2、 column 不能配置任何常量值 + + * 必选:是
+ + * 默认值:否
+ +* **session** + + * 描述: DataX在获取 ADB MySQL 连接时,执行session指定的SQL语句,修改当前connection session属性 + + * 必须: 否 + + * 默认值: 空 + +* **preSql** + + * 描述:写入数据到目的表前,会先执行这里的标准语句。如果 Sql 中有你需要操作到的表名称,请使用 `@table` 表示,这样在实际执行 SQL 语句时,会对变量按照实际表名称进行替换。比如希望导入数据前,先对表中数据进行删除操作,那么你可以这样配置:`"preSql":["truncate table @table"]`,效果是:在执行到每个表写入数据前,会先执行对应的 `truncate table 对应表名称`
+ + * 必选:否
+ + * 默认值:无
+ +* **postSql** + + * 描述:写入数据到目的表后,会执行这里的标准语句。(原理同 preSql )
+ + * 必选:否
+ + * 默认值:无
+ +* **writeMode** + + * 描述:控制写入数据到目标表采用 `insert into` 或者 `replace into` 或者 `ON DUPLICATE KEY UPDATE` 语句
+ + * 必选:是
+ + * 所有选项:insert/replace/update
+ + * 默认值:replace
+ +* **batchSize** + + * 描述:一次性批量提交的记录数大小,该值可以极大减少DataX与 Adb MySQL 的网络交互次数,并提升整体吞吐量。但是该值设置过大可能会造成DataX运行进程OOM情况。
+ + * 必选:否
+ + * 默认值:2048
+ + +### 3.3 类型转换 + +目前 AdbMysqlWriter 支持大部分 MySQL 类型,但也存在部分个别类型没有支持的情况,请注意检查你的类型。 + +下面列出 AdbMysqlWriter 针对 MySQL 类型转换列表: + +| DataX 内部类型 | AdbMysql 数据类型 | +|---------------|---------------------------------| +| Long | tinyint, smallint, int, bigint | +| Double | float, double, decimal | +| String | varchar | +| Date | date, time, datetime, timestamp | +| Boolean | boolean | +| Bytes | binary | + +## 4 性能报告 + +### 4.1 环境准备 + +#### 4.1.1 数据特征 +TPC-H 数据集 lineitem 表,共 17 个字段, 随机生成总记录行数 59986052。未压缩总数据量:7.3GiB + +建表语句: + + CREATE TABLE `datax_adbmysqlwriter_perf_lineitem` ( + `l_orderkey` bigint NOT NULL COMMENT '', + `l_partkey` int NOT NULL COMMENT '', + `l_suppkey` int NOT NULL COMMENT '', + `l_linenumber` int NOT NULL COMMENT '', + `l_quantity` decimal(15,2) NOT NULL COMMENT '', + `l_extendedprice` decimal(15,2) NOT NULL COMMENT '', + `l_discount` decimal(15,2) NOT NULL COMMENT '', + `l_tax` decimal(15,2) NOT NULL COMMENT '', + `l_returnflag` varchar(1024) NOT NULL COMMENT '', + `l_linestatus` varchar(1024) NOT NULL COMMENT '', + `l_shipdate` date NOT NULL COMMENT '', + `l_commitdate` date NOT NULL COMMENT '', + `l_receiptdate` date NOT NULL COMMENT '', + `l_shipinstruct` varchar(1024) NOT NULL COMMENT '', + `l_shipmode` varchar(1024) NOT NULL COMMENT '', + `l_comment` varchar(1024) NOT NULL COMMENT '', + `dummy` varchar(1024), + PRIMARY KEY (`l_orderkey`, `l_linenumber`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='datax perf test'; + +单行记录类似于: + + l_orderkey: 2122789 + l_partkey: 1233571 + l_suppkey: 8608 + l_linenumber: 1 + l_quantity: 35.00 + l_extendedprice: 52657.85 + l_discount: 0.02 + l_tax: 0.07 + l_returnflag: N + l_linestatus: O + l_shipdate: 1996-11-03 + l_commitdate: 1996-12-07 + l_receiptdate: 1996-11-16 + l_shipinstruct: COLLECT COD + l_shipmode: FOB + l_comment: ld, regular theodolites. + dummy: + +#### 4.1.2 机器参数 + +* DataX ECS: 24Core48GB + +* Adb MySQL 数据库 + * 计算资源:16Core64GB(集群版) + * 弹性IO资源:3 + +#### 4.1.3 DataX jvm 参数 + + -Xms1G -Xmx10G -XX:+HeapDumpOnOutOfMemoryError + +### 4.2 测试报告 + +| 通道数 | 批量提交行数 | DataX速度(Rec/s) | DataX流量(MB/s) | 导入用时(s) | +|-----|-------|------------------|---------------|---------| +| 1 | 512 | 23071 | 2.34 | 2627 | +| 1 | 1024 | 26080 | 2.65 | 2346 | +| 1 | 2048 | 28162 | 2.86 | 2153 | +| 1 | 4096 | 28978 | 2.94 | 2119 | +| 4 | 512 | 56590 | 5.74 | 1105 | +| 4 | 1024 | 81062 | 8.22 | 763 | +| 4 | 2048 | 107117 | 10.87 | 605 | +| 4 | 4096 | 113181 | 11.48 | 579 | +| 8 | 512 | 81062 | 8.22 | 786 | +| 8 | 1024 | 127629 | 12.95 | 519 | +| 8 | 2048 | 187456 | 19.01 | 369 | +| 8 | 4096 | 206848 | 20.98 | 341 | +| 16 | 512 | 130404 | 13.23 | 513 | +| 16 | 1024 | 214235 | 21.73 | 335 | +| 16 | 2048 | 299930 | 30.42 | 253 | +| 16 | 4096 | 333255 | 33.80 | 227 | +| 32 | 512 | 206848 | 20.98 | 347 | +| 32 | 1024 | 315716 | 32.02 | 241 | +| 32 | 2048 | 399907 | 40.56 | 199 | +| 32 | 4096 | 461431 | 46.80 | 184 | +| 64 | 512 | 333255 | 33.80 | 231 | +| 64 | 1024 | 399907 | 40.56 | 204 | +| 64 | 2048 | 428471 | 43.46 | 199 | +| 64 | 4096 | 461431 | 46.80 | 187 | +| 128 | 512 | 333255 | 33.80 | 235 | +| 128 | 1024 | 399907 | 40.56 | 203 | +| 128 | 2048 | 425432 | 43.15 | 197 | +| 128 | 4096 | 387006 | 39.26 | 211 | + +说明: + +1. datax 使用 txtfilereader 读取本地文件,避免源端存在性能瓶颈。 + +#### 性能测试小结 +1. channel通道个数和batchSize对性能影响比较大 +2. 通常不建议写入数据库时,通道个数 > 32 + +## 5 约束限制 + +## FAQ + +*** + +**Q: AdbMysqlWriter 执行 postSql 语句报错,那么数据导入到目标数据库了吗?** + +A: DataX 导入过程存在三块逻辑,pre 操作、导入操作、post 操作,其中任意一环报错,DataX 作业报错。由于 DataX 不能保证在同一个事务完成上述几个操作,因此有可能数据已经落入到目标端。 + +*** + +**Q: 按照上述说法,那么有部分脏数据导入数据库,如果影响到线上数据库怎么办?** + +A: 目前有两种解法,第一种配置 pre 语句,该 sql 可以清理当天导入数据, DataX 每次导入时候可以把上次清理干净并导入完整数据。第二种,向临时表导入数据,完成后再 rename 到线上表。 + +*** + +**Q: 上面第二种方法可以避免对线上数据造成影响,那我具体怎样操作?** + +A: 可以配置临时表导入 diff --git a/adbmysqlwriter/pom.xml b/adbmysqlwriter/pom.xml new file mode 100755 index 0000000000..6ffcab8530 --- /dev/null +++ b/adbmysqlwriter/pom.xml @@ -0,0 +1,79 @@ + + 4.0.0 + + com.alibaba.datax + datax-all + 0.0.1-SNAPSHOT + + adbmysqlwriter + adbmysqlwriter + jar + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + + mysql + mysql-connector-java + 5.1.40 + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + diff --git a/adbmysqlwriter/src/main/assembly/package.xml b/adbmysqlwriter/src/main/assembly/package.xml new file mode 100755 index 0000000000..7192e5312e --- /dev/null +++ b/adbmysqlwriter/src/main/assembly/package.xml @@ -0,0 +1,35 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/writer/adbmysqlwriter + + + target/ + + adbmysqlwriter-0.0.1-SNAPSHOT.jar + + plugin/writer/adbmysqlwriter + + + + + + false + plugin/writer/adbmysqlwriter/libs + runtime + + + diff --git a/adbmysqlwriter/src/main/java/com/alibaba/datax/plugin/writer/adbmysqlwriter/AdbMysqlWriter.java b/adbmysqlwriter/src/main/java/com/alibaba/datax/plugin/writer/adbmysqlwriter/AdbMysqlWriter.java new file mode 100755 index 0000000000..762c4934b1 --- /dev/null +++ b/adbmysqlwriter/src/main/java/com/alibaba/datax/plugin/writer/adbmysqlwriter/AdbMysqlWriter.java @@ -0,0 +1,138 @@ +package com.alibaba.datax.plugin.writer.adbmysqlwriter; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter; +import com.alibaba.datax.plugin.rdbms.writer.Key; +import org.apache.commons.lang3.StringUtils; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +public class AdbMysqlWriter extends Writer { + private static final DataBaseType DATABASE_TYPE = DataBaseType.ADB; + + public static class Job extends Writer.Job { + private Configuration originalConfig = null; + private CommonRdbmsWriter.Job commonRdbmsWriterJob; + + @Override + public void preCheck(){ + this.init(); + this.commonRdbmsWriterJob.writerPreCheck(this.originalConfig, DATABASE_TYPE); + } + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + this.commonRdbmsWriterJob = new CommonRdbmsWriter.Job(DATABASE_TYPE); + this.commonRdbmsWriterJob.init(this.originalConfig); + } + + // 一般来说,是需要推迟到 task 中进行pre 的执行(单表情况例外) + @Override + public void prepare() { + //实跑先不支持 权限 检验 + //this.commonRdbmsWriterJob.privilegeValid(this.originalConfig, DATABASE_TYPE); + this.commonRdbmsWriterJob.prepare(this.originalConfig); + } + + @Override + public List split(int mandatoryNumber) { + return this.commonRdbmsWriterJob.split(this.originalConfig, mandatoryNumber); + } + + // 一般来说,是需要推迟到 task 中进行post 的执行(单表情况例外) + @Override + public void post() { + this.commonRdbmsWriterJob.post(this.originalConfig); + } + + @Override + public void destroy() { + this.commonRdbmsWriterJob.destroy(this.originalConfig); + } + + } + + public static class Task extends Writer.Task { + + private Configuration writerSliceConfig; + private CommonRdbmsWriter.Task commonRdbmsWriterTask; + + public static class DelegateClass extends CommonRdbmsWriter.Task { + private long writeTime = 0L; + private long writeCount = 0L; + private long lastLogTime = 0; + + public DelegateClass(DataBaseType dataBaseType) { + super(dataBaseType); + } + + @Override + protected void doBatchInsert(Connection connection, List buffer) + throws SQLException { + long startTime = System.currentTimeMillis(); + + super.doBatchInsert(connection, buffer); + + writeCount = writeCount + buffer.size(); + writeTime = writeTime + (System.currentTimeMillis() - startTime); + + // log write metrics every 10 seconds + if (System.currentTimeMillis() - lastLogTime > 10000) { + lastLogTime = System.currentTimeMillis(); + logTotalMetrics(); + } + } + + public void logTotalMetrics() { + LOG.info(Thread.currentThread().getName() + ", AdbMySQL writer take " + writeTime + " ms, write " + writeCount + " records."); + } + } + + @Override + public void init() { + this.writerSliceConfig = super.getPluginJobConf(); + + if (StringUtils.isBlank(this.writerSliceConfig.getString(Key.WRITE_MODE))) { + this.writerSliceConfig.set(Key.WRITE_MODE, "REPLACE"); + } + + this.commonRdbmsWriterTask = new DelegateClass(DATABASE_TYPE); + this.commonRdbmsWriterTask.init(this.writerSliceConfig); + } + + @Override + public void prepare() { + this.commonRdbmsWriterTask.prepare(this.writerSliceConfig); + } + + //TODO 改用连接池,确保每次获取的连接都是可用的(注意:连接可能需要每次都初始化其 session) + public void startWrite(RecordReceiver recordReceiver) { + this.commonRdbmsWriterTask.startWrite(recordReceiver, this.writerSliceConfig, + super.getTaskPluginCollector()); + } + + @Override + public void post() { + this.commonRdbmsWriterTask.post(this.writerSliceConfig); + } + + @Override + public void destroy() { + this.commonRdbmsWriterTask.destroy(this.writerSliceConfig); + } + + @Override + public boolean supportFailOver(){ + String writeMode = writerSliceConfig.getString(Key.WRITE_MODE); + return "replace".equalsIgnoreCase(writeMode); + } + + } +} diff --git a/adbmysqlwriter/src/main/resources/plugin.json b/adbmysqlwriter/src/main/resources/plugin.json new file mode 100755 index 0000000000..58c695339e --- /dev/null +++ b/adbmysqlwriter/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "adbmysqlwriter", + "class": "com.alibaba.datax.plugin.writer.adbmysqlwriter.AdbMysqlWriter", + "description": "useScene: prod. mechanism: Jdbc connection using the database, execute insert sql. warn: The more you know about the database, the less problems you encounter.", + "developer": "alibaba" +} \ No newline at end of file diff --git a/adbmysqlwriter/src/main/resources/plugin_job_template.json b/adbmysqlwriter/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..9537ee5a39 --- /dev/null +++ b/adbmysqlwriter/src/main/resources/plugin_job_template.json @@ -0,0 +1,20 @@ +{ + "name": "adbmysqlwriter", + "parameter": { + "username": "username", + "password": "password", + "column": ["col1", "col2", "col3"], + "connection": [ + { + "jdbcUrl": "jdbc:mysql://:[/]", + "table": ["table1", "table2"] + } + ], + "preSql": [], + "postSql": [], + "batchSize": 65536, + "batchByteSize": 134217728, + "dryRun": false, + "writeMode": "insert" + } +} \ No newline at end of file diff --git a/adbpgwriter/src/main/doc/adbpgwriter.md b/adbpgwriter/src/main/doc/adbpgwriter.md index 804272418e..c03447d416 100644 --- a/adbpgwriter/src/main/doc/adbpgwriter.md +++ b/adbpgwriter/src/main/doc/adbpgwriter.md @@ -65,9 +65,9 @@ COPY命令将数据写入ADB PG数据库中。 "writer": { "name": "adbpgwriter", "parameter": { - "username": "username", - "password": "password", - "host": "host", + "username": "", + "password": "", + "host": "127.0.0.1", "port": "1234", "database": "database", "schema": "schema", @@ -149,6 +149,7 @@ COPY命令将数据写入ADB PG数据库中。 注意:1、我们强烈不推荐你这样配置,因为当你目的表字段个数、类型等有改动时,你的任务可能运行不正确或者失败 2、此处 column 不能配置任何常量值 + 3、大写字段名,此处配置时,不需要拼接转义符号:\" * 必选:是
@@ -229,4 +230,4 @@ create table schematest.test_datax ( #### 4.2.2 性能测试小结 1. `channel数对性能影响很大` -2. `通常不建议写入数据库时,通道个数 > 32` \ No newline at end of file +2. `通常不建议写入数据库时,通道个数 > 32` diff --git a/adbpgwriter/src/main/java/com/alibaba/datax/plugin/writer/adbpgwriter/copy/Adb4pgClientProxy.java b/adbpgwriter/src/main/java/com/alibaba/datax/plugin/writer/adbpgwriter/copy/Adb4pgClientProxy.java index 4998607683..d09f4e2b8a 100644 --- a/adbpgwriter/src/main/java/com/alibaba/datax/plugin/writer/adbpgwriter/copy/Adb4pgClientProxy.java +++ b/adbpgwriter/src/main/java/com/alibaba/datax/plugin/writer/adbpgwriter/copy/Adb4pgClientProxy.java @@ -56,7 +56,7 @@ public Adb4pgClientProxy(Configuration configuration,TaskPluginCollector taskPl int retryIntervalTime = configuration.getInt(Key.RETRY_INTERVAL_TIME, 1000); databaseConfig.setRetryIntervalTime(retryIntervalTime); - // 设置自动提交的SQL长度(单位Byte),默认为32KB,一般不建议设置 + // 设置自动提交的SQL长度(单位Byte),默认为10MB,一般不建议设置 int commitSize = configuration.getInt("commitSize", 10 * 1024 * 1024); databaseConfig.setCommitSize(commitSize); diff --git a/adswriter/doc/adswriter.md b/adswriter/doc/adswriter.md index 4a0fd9619f..c02f80187a 100644 --- a/adswriter/doc/adswriter.md +++ b/adswriter/doc/adswriter.md @@ -110,7 +110,6 @@ DataX 将数据直连ADS接口,利用ADS暴露的INSERT接口直写到ADS。 "account": "xxx@aliyun.com", "odpsServer": "xxx", "tunnelServer": "xxx", - "accountType": "aliyun", "project": "transfer_project" }, "writeMode": "load", diff --git a/adswriter/src/main/java/com/alibaba/datax/plugin/writer/adswriter/insert/AdsClientProxy.java b/adswriter/src/main/java/com/alibaba/datax/plugin/writer/adswriter/insert/AdsClientProxy.java index 8fdc70d6fd..326b464d1a 100644 --- a/adswriter/src/main/java/com/alibaba/datax/plugin/writer/adswriter/insert/AdsClientProxy.java +++ b/adswriter/src/main/java/com/alibaba/datax/plugin/writer/adswriter/insert/AdsClientProxy.java @@ -18,7 +18,7 @@ import com.alibaba.datax.plugin.writer.adswriter.ads.TableInfo; import com.alibaba.datax.plugin.writer.adswriter.util.Constant; import com.alibaba.datax.plugin.writer.adswriter.util.Key; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; diff --git a/adswriter/src/main/java/com/alibaba/datax/plugin/writer/adswriter/load/TransferProjectConf.java b/adswriter/src/main/java/com/alibaba/datax/plugin/writer/adswriter/load/TransferProjectConf.java index bff4b7b900..3d28a8339e 100644 --- a/adswriter/src/main/java/com/alibaba/datax/plugin/writer/adswriter/load/TransferProjectConf.java +++ b/adswriter/src/main/java/com/alibaba/datax/plugin/writer/adswriter/load/TransferProjectConf.java @@ -12,7 +12,6 @@ public class TransferProjectConf { public final static String KEY_ACCOUNT = "odps.account"; public final static String KEY_ODPS_SERVER = "odps.odpsServer"; public final static String KEY_ODPS_TUNNEL = "odps.tunnelServer"; - public final static String KEY_ACCOUNT_TYPE = "odps.accountType"; public final static String KEY_PROJECT = "odps.project"; private String accessId; @@ -20,7 +19,6 @@ public class TransferProjectConf { private String account; private String odpsServer; private String odpsTunnel; - private String accountType; private String project; public static TransferProjectConf create(Configuration adsWriterConf) { @@ -30,7 +28,6 @@ public static TransferProjectConf create(Configuration adsWriterConf) { res.account = adsWriterConf.getString(KEY_ACCOUNT); res.odpsServer = adsWriterConf.getString(KEY_ODPS_SERVER); res.odpsTunnel = adsWriterConf.getString(KEY_ODPS_TUNNEL); - res.accountType = adsWriterConf.getString(KEY_ACCOUNT_TYPE, "aliyun"); res.project = adsWriterConf.getString(KEY_PROJECT); return res; } @@ -55,9 +52,6 @@ public String getOdpsTunnel() { return odpsTunnel; } - public String getAccountType() { - return accountType; - } public String getProject() { return project; diff --git a/adswriter/src/main/java/com/alibaba/datax/plugin/writer/adswriter/odps/DataType.java b/adswriter/src/main/java/com/alibaba/datax/plugin/writer/adswriter/odps/DataType.java index 595b1dfd26..f625336e15 100644 --- a/adswriter/src/main/java/com/alibaba/datax/plugin/writer/adswriter/odps/DataType.java +++ b/adswriter/src/main/java/com/alibaba/datax/plugin/writer/adswriter/odps/DataType.java @@ -70,7 +70,7 @@ public static byte convertToDataType(String type) throws IllegalArgumentExceptio } else if ("datetime".equals(type)) { return DATETIME; } else { - throw new IllegalArgumentException("unkown type: " + type); + throw new IllegalArgumentException("unknown type: " + type); } } diff --git a/cassandrareader/src/main/java/com/alibaba/datax/plugin/reader/cassandrareader/CassandraReaderHelper.java b/cassandrareader/src/main/java/com/alibaba/datax/plugin/reader/cassandrareader/CassandraReaderHelper.java index 0a4e83fa1d..f5937c2feb 100644 --- a/cassandrareader/src/main/java/com/alibaba/datax/plugin/reader/cassandrareader/CassandraReaderHelper.java +++ b/cassandrareader/src/main/java/com/alibaba/datax/plugin/reader/cassandrareader/CassandraReaderHelper.java @@ -23,7 +23,7 @@ import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.plugin.TaskPluginCollector; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import com.datastax.driver.core.Cluster; import com.datastax.driver.core.CodecRegistry; @@ -298,6 +298,7 @@ static Record buildRecord(Record record, Row rs, ColumnDefinitions metaData, int record.addColumn(new LongColumn(rs.getInt(i))); break; + case COUNTER: case BIGINT: record.addColumn(new LongColumn(rs.getLong(i))); break; @@ -558,26 +559,6 @@ public static void checkConfig(Configuration jobConfig,Cluster cluster) { String.format( "配置信息有错误.列信息中需要包含'%s'字段 .",Key.COLUMN_NAME)); } - if( name.startsWith(Key.WRITE_TIME) ) { - String colName = name.substring(Key.WRITE_TIME.length(),name.length() - 1 ); - ColumnMetadata col = tableMetadata.getColumn(colName); - if( col == null ) { - throw DataXException - .asDataXException( - CassandraReaderErrorCode.CONF_ERROR, - String.format( - "配置信息有错误.列'%s'不存在 .",colName)); - } - } else { - ColumnMetadata col = tableMetadata.getColumn(name); - if( col == null ) { - throw DataXException - .asDataXException( - CassandraReaderErrorCode.CONF_ERROR, - String.format( - "配置信息有错误.列'%s'不存在 .",name)); - } - } } } diff --git a/cassandrawriter/src/main/java/com/alibaba/datax/plugin/writer/cassandrawriter/CassandraWriterHelper.java b/cassandrawriter/src/main/java/com/alibaba/datax/plugin/writer/cassandrawriter/CassandraWriterHelper.java index b68af28165..5ac392b751 100644 --- a/cassandrawriter/src/main/java/com/alibaba/datax/plugin/writer/cassandrawriter/CassandraWriterHelper.java +++ b/cassandrawriter/src/main/java/com/alibaba/datax/plugin/writer/cassandrawriter/CassandraWriterHelper.java @@ -18,10 +18,10 @@ import com.alibaba.datax.common.element.Column; import com.alibaba.datax.common.exception.DataXException; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONArray; -import com.alibaba.fastjson.JSONException; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.JSONObject; import com.datastax.driver.core.BoundStatement; import com.datastax.driver.core.CodecRegistry; @@ -204,7 +204,7 @@ public static Object parseFromJson(Object jsonObject,DataType type) throws Excep case MAP: { Map m = new HashMap(); - for (JSONObject.Entry e : ((JSONObject)jsonObject).entrySet()) { + for (Map.Entry e : ((JSONObject)jsonObject).entrySet()) { Object k = parseFromString((String) e.getKey(), type.getTypeArguments().get(0)); Object v = parseFromJson(e.getValue(), type.getTypeArguments().get(1)); m.put(k,v); @@ -233,7 +233,7 @@ public static Object parseFromJson(Object jsonObject,DataType type) throws Excep case UDT: { UDTValue t = ((UserType) type).newValue(); UserType userType = t.getType(); - for (JSONObject.Entry e : ((JSONObject)jsonObject).entrySet()) { + for (Map.Entry e : ((JSONObject)jsonObject).entrySet()) { DataType eleType = userType.getFieldType((String)e.getKey()); t.set((String)e.getKey(), parseFromJson(e.getValue(), eleType), registry.codecFor(eleType).getJavaType()); } diff --git a/clickhousereader/doc/clickhousereader.md b/clickhousereader/doc/clickhousereader.md new file mode 100644 index 0000000000..bf3cd203f5 --- /dev/null +++ b/clickhousereader/doc/clickhousereader.md @@ -0,0 +1,344 @@ + +# ClickhouseReader 插件文档 + + +___ + + +## 1 快速介绍 + +ClickhouseReader插件实现了从Clickhouse读取数据。在底层实现上,ClickhouseReader通过JDBC连接远程Clickhouse数据库,并执行相应的sql语句将数据从Clickhouse库中SELECT出来。 + +## 2 实现原理 + +简而言之,ClickhouseReader通过JDBC连接器连接到远程的Clickhouse数据库,并根据用户配置的信息生成查询SELECT SQL语句并发送到远程Clickhouse数据库,并将该SQL执行返回结果使用DataX自定义的数据类型拼装为抽象的数据集,并传递给下游Writer处理。 + +对于用户配置Table、Column、Where的信息,ClickhouseReader将其拼接为SQL语句发送到Clickhouse数据库;对于用户配置querySql信息,Clickhouse直接将其发送到Clickhouse数据库。 + + +## 3 功能说明 + +### 3.1 配置样例 + +* 配置一个从Clickhouse数据库同步抽取数据到本地的作业: + +``` +{ + "job": { + "setting": { + "speed": { + //设置传输速度 byte/s 尽量逼近这个速度但是不高于它. + // channel 表示通道数量,byte表示通道速度,如果单通道速度1MB,配置byte为1048576表示一个channel + "byte": 1048576 + }, + //出错限制 + "errorLimit": { + //先选择record + "record": 0, + //百分比 1表示100% + "percentage": 0.02 + } + }, + "content": [ + { + "reader": { + "name": "clickhousereader", + "parameter": { + // 数据库连接用户名 + "username": "root", + // 数据库连接密码 + "password": "root", + "column": [ + "id","name" + ], + "connection": [ + { + "table": [ + "table" + ], + "jdbcUrl": [ + "jdbc:clickhouse://[HOST_NAME]:PORT/[DATABASE_NAME]" + ] + } + ] + } + }, + "writer": { + //writer类型 + "name": "streamwriter", + // 是否打印内容 + "parameter": { + "print": true + } + } + } + ] + } +} + +``` + +* 配置一个自定义SQL的数据库同步任务到本地内容的作业: + +``` +{ + "job": { + "setting": { + "speed": { + "channel": 5 + } + }, + "content": [ + { + "reader": { + "name": "clickhousereader", + "parameter": { + "username": "root", + "password": "root", + "where": "", + "connection": [ + { + "querySql": [ + "select db_id,on_line_flag from db_info where db_id < 10" + ], + "jdbcUrl": [ + "jdbc:clickhouse://1.1.1.1:8123/default" + ] + } + ] + } + }, + "writer": { + "name": "streamwriter", + "parameter": { + "visible": false, + "encoding": "UTF-8" + } + } + } + ] + } +} +``` + + +### 3.2 参数说明 + +* **jdbcUrl** + + * 描述:描述的是到对端数据库的JDBC连接信息,使用JSON的数组描述,并支持一个库填写多个连接地址。之所以使用JSON数组描述连接信息,是因为阿里集团内部支持多个IP探测,如果配置了多个,ClickhouseReader可以依次探测ip的可连接性,直到选择一个合法的IP。如果全部连接失败,ClickhouseReader报错。 注意,jdbcUrl必须包含在connection配置单元中。对于阿里集团外部使用情况,JSON数组填写一个JDBC连接即可。 + + jdbcUrl按照Clickhouse官方规范,并可以填写连接附件控制信息。具体请参看[Clickhouse官方文档](https://clickhouse.com/docs/en/engines/table-engines/integrations/jdbc)。 + + * 必选:是
+ + * 默认值:无
+ +* **username** + + * 描述:数据源的用户名
+ + * 必选:是
+ + * 默认值:无
+ +* **password** + + * 描述:数据源指定用户名的密码
+ + * 必选:是
+ + * 默认值:无
+ +* **table** + + * 描述:所选取的需要同步的表。使用JSON的数组描述,因此支持多张表同时抽取。当配置为多张表时,用户自己需保证多张表是同一schema结构,ClickhouseReader不予检查表是否同一逻辑表。注意,table必须包含在connection配置单元中。
+ + * 必选:是
+ + * 默认值:无
+ +* **column** + + * 描述:所配置的表中需要同步的列名集合,使用JSON的数组描述字段信息。用户使用\*代表默认使用所有列配置,例如['\*']。 + + 支持列裁剪,即列可以挑选部分列进行导出。 + + 支持列换序,即列可以不按照表schema信息进行导出。 + + 支持常量配置,用户需要按照JSON格式: + ["id", "`table`", "1", "'bazhen.csy'", "null", "to_char(a + 1)", "2.3" , "true"] + id为普通列名,\`table\`为包含保留在的列名,1为整形数字常量,'bazhen.csy'为字符串常量,null为空指针,to_char(a + 1)为表达式,2.3为浮点数,true为布尔值。 + + Column必须显示填写,不允许为空! + + * 必选:是
+ + * 默认值:无
+ +* **splitPk** + + * 描述:ClickhouseReader进行数据抽取时,如果指定splitPk,表示用户希望使用splitPk代表的字段进行数据分片,DataX因此会启动并发任务进行数据同步,这样可以大大提供数据同步的效能。 + + 推荐splitPk用户使用表主键,因为表主键通常情况下比较均匀,因此切分出来的分片也不容易出现数据热点。 + + 目前splitPk仅支持整形数据切分,`不支持浮点、日期等其他类型`。如果用户指定其他非支持类型,ClickhouseReader将报错! + + splitPk如果不填写,将视作用户不对单表进行切分,ClickhouseReader使用单通道同步全量数据。 + + * 必选:否
+ + * 默认值:无
+ +* **where** + + * 描述:筛选条件,MysqlReader根据指定的column、table、where条件拼接SQL,并根据这个SQL进行数据抽取。在实际业务场景中,往往会选择当天的数据进行同步,可以将where条件指定为gmt_create > $bizdate 。注意:不可以将where条件指定为limit 10,limit不是SQL的合法where子句。
+ + where条件可以有效地进行业务增量同步。 + + * 必选:否
+ + * 默认值:无
+ +* **querySql** + + * 描述:在有些业务场景下,where这一配置项不足以描述所筛选的条件,用户可以通过该配置型来自定义筛选SQL。当用户配置了这一项之后,DataX系统就会忽略table,column这些配置型,直接使用这个配置项的内容对数据进行筛选,例如需要进行多表join后同步数据,使用select a,b from table_a join table_b on table_a.id = table_b.id
+ + `当用户配置querySql时,ClickhouseReader直接忽略table、column、where条件的配置`。 + + * 必选:否
+ + * 默认值:无
+ +* **fetchSize** + + * 描述:该配置项定义了插件和数据库服务器端每次批量数据获取条数,该值决定了DataX和服务器端的网络交互次数,能够较大的提升数据抽取性能。
+ + `注意,该值过大(>2048)可能造成DataX进程OOM。`。 + + * 必选:否
+ + * 默认值:1024
+ +* **session** + + * 描述:控制写入数据的时间格式,时区等的配置,如果表中有时间字段,配置该值以明确告知写入 clickhouse 的时间格式。通常配置的参数为:NLS_DATE_FORMAT,NLS_TIME_FORMAT。其配置的值为 json 格式,例如: +``` +"session": [ + "alter session set NLS_DATE_FORMAT='yyyy-mm-dd hh24:mi:ss'", + "alter session set NLS_TIMESTAMP_FORMAT='yyyy-mm-dd hh24:mi:ss'", + "alter session set NLS_TIMESTAMP_TZ_FORMAT='yyyy-mm-dd hh24:mi:ss'", + "alter session set TIME_ZONE='US/Pacific'" + ] +``` + `(注意"是 " 的转义字符串)`。 + + * 必选:否
+ + * 默认值:无
+ + +### 3.3 类型转换 + +目前ClickhouseReader支持大部分Clickhouse类型,但也存在部分个别类型没有支持的情况,请注意检查你的类型。 + +下面列出ClickhouseReader针对Clickhouse类型转换列表: + + +| DataX 内部类型| Clickhouse 数据类型 | +| -------- |--------------------------------------------------------------------------------------------| +| Long | UInt8, UInt16, UInt32, UInt64, UInt128, UInt256, Int8, Int16, Int32, Int64, Int128, Int256 | +| Double | Float32, Float64, Decimal | +| String | String, FixedString | +| Date | DATE, Date32, DateTime, DateTime64 | +| Boolean | Boolean | +| Bytes | BLOB,BFILE,RAW,LONG RAW | + + + +请注意: + +* `除上述罗列字段类型外,其他类型均不支持`。 + + +## 4 性能报告 + +### 4.1 环境准备 + +#### 4.1.1 数据特征 + +为了模拟线上真实数据,我们设计两个Clickhouse数据表,分别为: + +#### 4.1.2 机器参数 + +* 执行DataX的机器参数为: + +* Clickhouse数据库机器参数为: + +### 4.2 测试报告 + +#### 4.2.1 表1测试报告 + + +| 并发任务数| DataX速度(Rec/s)|DataX流量|网卡流量|DataX运行负载|DB运行负载| +|--------| --------|--------|--------|--------|--------| +|1| DataX 统计速度(Rec/s)|DataX统计流量|网卡流量|DataX运行负载|DB运行负载| + +## 5 约束限制 + +### 5.1 主备同步数据恢复问题 + +主备同步问题指Clickhouse使用主从灾备,备库从主库不间断通过binlog恢复数据。由于主备数据同步存在一定的时间差,特别在于某些特定情况,例如网络延迟等问题,导致备库同步恢复的数据与主库有较大差别,导致从备库同步的数据不是一份当前时间的完整镜像。 + +针对这个问题,我们提供了preSql功能,该功能待补充。 + +### 5.2 一致性约束 + +Clickhouse在数据存储划分中属于RDBMS系统,对外可以提供强一致性数据查询接口。例如当一次同步任务启动运行过程中,当该库存在其他数据写入方写入数据时,ClickhouseReader完全不会获取到写入更新数据,这是由于数据库本身的快照特性决定的。关于数据库快照特性,请参看[MVCC Wikipedia](https://en.wikipedia.org/wiki/Multiversion_concurrency_control) + +上述是在ClickhouseReader单线程模型下数据同步一致性的特性,由于ClickhouseReader可以根据用户配置信息使用了并发数据抽取,因此不能严格保证数据一致性:当ClickhouseReader根据splitPk进行数据切分后,会先后启动多个并发任务完成数据同步。由于多个并发任务相互之间不属于同一个读事务,同时多个并发任务存在时间间隔。因此这份数据并不是`完整的`、`一致的`数据快照信息。 + +针对多线程的一致性快照需求,在技术上目前无法实现,只能从工程角度解决,工程化的方式存在取舍,我们提供几个解决思路给用户,用户可以自行选择: + +1. 使用单线程同步,即不再进行数据切片。缺点是速度比较慢,但是能够很好保证一致性。 + +2. 关闭其他数据写入方,保证当前数据为静态数据,例如,锁表、关闭备库同步等等。缺点是可能影响在线业务。 + +### 5.3 数据库编码问题 + + +ClickhouseReader底层使用JDBC进行数据抽取,JDBC天然适配各类编码,并在底层进行了编码转换。因此ClickhouseReader不需用户指定编码,可以自动获取编码并转码。 + +对于Clickhouse底层写入编码和其设定的编码不一致的混乱情况,ClickhouseReader对此无法识别,对此也无法提供解决方案,对于这类情况,`导出有可能为乱码`。 + +### 5.4 增量数据同步 + +ClickhouseReader使用JDBC SELECT语句完成数据抽取工作,因此可以使用SELECT...WHERE...进行增量数据抽取,方式有多种: + +* 数据库在线应用写入数据库时,填充modify字段为更改时间戳,包括新增、更新、删除(逻辑删)。对于这类应用,ClickhouseReader只需要WHERE条件跟上一同步阶段时间戳即可。 +* 对于新增流水型数据,ClickhouseReader可以WHERE条件后跟上一阶段最大自增ID即可。 + +对于业务上无字段区分新增、修改数据情况,ClickhouseReader也无法进行增量数据同步,只能同步全量数据。 + +### 5.5 Sql安全性 + +ClickhouseReader提供querySql语句交给用户自己实现SELECT抽取语句,ClickhouseReader本身对querySql不做任何安全性校验。这块交由DataX用户方自己保证。 + +## 6 FAQ + +*** + +**Q: ClickhouseReader同步报错,报错信息为XXX** + + A: 网络或者权限问题,请使用Clickhouse命令行测试 + + +如果上述命令也报错,那可以证实是环境问题,请联系你的DBA。 + + +**Q: ClickhouseReader抽取速度很慢怎么办?** + + A: 影响抽取时间的原因大概有如下几个:(来自专业 DBA 卫绾) + 1. 由于SQL的plan异常,导致的抽取时间长; 在抽取时,尽可能使用全表扫描代替索引扫描; + 2. 合理sql的并发度,减少抽取时间; + 3. 抽取sql要简单,尽量不用replace等函数,这个非常消耗cpu,会严重影响抽取速度; diff --git a/clickhousereader/pom.xml b/clickhousereader/pom.xml new file mode 100644 index 0000000000..4b095796db --- /dev/null +++ b/clickhousereader/pom.xml @@ -0,0 +1,91 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + + 4.0.0 + clickhousereader + clickhousereader + jar + + + + ru.yandex.clickhouse + clickhouse-jdbc + 0.2.4 + + + com.alibaba.datax + datax-core + ${datax-project-version} + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + org.slf4j + slf4j-api + + + + ch.qos.logback + logback-classic + + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + + + + + src/main/java + + **/*.properties + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + + + \ No newline at end of file diff --git a/clickhousereader/src/main/assembly/package.xml b/clickhousereader/src/main/assembly/package.xml new file mode 100644 index 0000000000..9dc7fc13f3 --- /dev/null +++ b/clickhousereader/src/main/assembly/package.xml @@ -0,0 +1,35 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/reader/clickhousereader + + + target/ + + clickhousereader-0.0.1-SNAPSHOT.jar + + plugin/reader/clickhousereader + + + + + + false + plugin/reader/clickhousereader/libs + runtime + + + \ No newline at end of file diff --git a/clickhousereader/src/main/java/com/alibaba/datax/plugin/reader/clickhousereader/ClickhouseReader.java b/clickhousereader/src/main/java/com/alibaba/datax/plugin/reader/clickhousereader/ClickhouseReader.java new file mode 100644 index 0000000000..cfa6be999b --- /dev/null +++ b/clickhousereader/src/main/java/com/alibaba/datax/plugin/reader/clickhousereader/ClickhouseReader.java @@ -0,0 +1,85 @@ +package com.alibaba.datax.plugin.reader.clickhousereader; + +import java.sql.Array; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Types; +import java.util.List; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.element.StringColumn; +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.spi.Reader; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.MessageSource; +import com.alibaba.datax.plugin.rdbms.reader.CommonRdbmsReader; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.fastjson2.JSON; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ClickhouseReader extends Reader { + + private static final DataBaseType DATABASE_TYPE = DataBaseType.ClickHouse; + private static final Logger LOG = LoggerFactory.getLogger(ClickhouseReader.class); + + public static class Job extends Reader.Job { + private Configuration jobConfig = null; + private CommonRdbmsReader.Job commonRdbmsReaderMaster; + + @Override + public void init() { + this.jobConfig = super.getPluginJobConf(); + this.commonRdbmsReaderMaster = new CommonRdbmsReader.Job(DATABASE_TYPE); + this.commonRdbmsReaderMaster.init(this.jobConfig); + } + + @Override + public List split(int mandatoryNumber) { + return this.commonRdbmsReaderMaster.split(this.jobConfig, mandatoryNumber); + } + + @Override + public void post() { + this.commonRdbmsReaderMaster.post(this.jobConfig); + } + + @Override + public void destroy() { + this.commonRdbmsReaderMaster.destroy(this.jobConfig); + } + } + + public static class Task extends Reader.Task { + + private Configuration jobConfig; + private CommonRdbmsReader.Task commonRdbmsReaderSlave; + + @Override + public void init() { + this.jobConfig = super.getPluginJobConf(); + this.commonRdbmsReaderSlave = new CommonRdbmsReader.Task(DATABASE_TYPE, super.getTaskGroupId(), super.getTaskId()); + this.commonRdbmsReaderSlave.init(this.jobConfig); + } + + @Override + public void startRead(RecordSender recordSender) { + int fetchSize = this.jobConfig.getInt(com.alibaba.datax.plugin.rdbms.reader.Constant.FETCH_SIZE, 1000); + + this.commonRdbmsReaderSlave.startRead(this.jobConfig, recordSender, super.getTaskPluginCollector(), fetchSize); + } + + @Override + public void post() { + this.commonRdbmsReaderSlave.post(this.jobConfig); + } + + @Override + public void destroy() { + this.commonRdbmsReaderSlave.destroy(this.jobConfig); + } + } +} diff --git a/clickhousereader/src/main/resources/plugin.json b/clickhousereader/src/main/resources/plugin.json new file mode 100644 index 0000000000..5d608f6c66 --- /dev/null +++ b/clickhousereader/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "clickhousereader", + "class": "com.alibaba.datax.plugin.reader.clickhousereader.ClickhouseReader", + "description": "useScene: prod. mechanism: Jdbc connection using the database, execute select sql.", + "developer": "alibaba" +} \ No newline at end of file diff --git a/clickhousereader/src/main/resources/plugin_job_template.json b/clickhousereader/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..1814e51063 --- /dev/null +++ b/clickhousereader/src/main/resources/plugin_job_template.json @@ -0,0 +1,16 @@ +{ + "name": "clickhousereader", + "parameter": { + "username": "username", + "password": "password", + "column": ["col1", "col2", "col3"], + "connection": [ + { + "jdbcUrl": "jdbc:clickhouse://:[/]", + "table": ["table1", "table2"] + } + ], + "preSql": [], + "postSql": [] + } +} \ No newline at end of file diff --git a/clickhousereader/src/test/resources/basic1.json b/clickhousereader/src/test/resources/basic1.json new file mode 100755 index 0000000000..c45a45e7f6 --- /dev/null +++ b/clickhousereader/src/test/resources/basic1.json @@ -0,0 +1,57 @@ +{ + "job": { + "setting": { + "speed": { + "channel": 5 + } + }, + "content": [ + { + "reader": { + "name": "clickhousereader", + "parameter": { + "username": "XXXX", + "password": "XXXX", + "column": [ + "uint8_col", + "uint16_col", + "uint32_col", + "uint64_col", + "int8_col", + "int16_col", + "int32_col", + "int64_col", + "float32_col", + "float64_col", + "bool_col", + "str_col", + "fixedstr_col", + "uuid_col", + "date_col", + "datetime_col", + "enum_col", + "ary_uint8_col", + "ary_str_col", + "tuple_col", + "nullable_col", + "nested_col.nested_id", + "nested_col.nested_str", + "ipv4_col", + "ipv6_col", + "decimal_col" + ], + "connection": [ + { + "table": [ + "all_type_tbl" + ], + "jdbcUrl":["jdbc:clickhouse://XXXX:8123/default"] + } + ] + } + }, + "writer": {} + } + ] + } +} \ No newline at end of file diff --git a/clickhousereader/src/test/resources/basic1.sql b/clickhousereader/src/test/resources/basic1.sql new file mode 100644 index 0000000000..f937b889ff --- /dev/null +++ b/clickhousereader/src/test/resources/basic1.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS default.all_type_tbl +( +`uint8_col` UInt8, +`uint16_col` UInt16, +uint32_col UInt32, +uint64_col UInt64, +int8_col Int8, +int16_col Int16, +int32_col Int32, +int64_col Int64, +float32_col Float32, +float64_col Float64, +bool_col UInt8, +str_col String, +fixedstr_col FixedString(3), +uuid_col UUID, +date_col Date, +datetime_col DateTime, +enum_col Enum('hello' = 1, 'world' = 2), +ary_uint8_col Array(UInt8), +ary_str_col Array(String), +tuple_col Tuple(UInt8, String), +nullable_col Nullable(UInt8), +nested_col Nested + ( + nested_id UInt32, + nested_str String + ), +ipv4_col IPv4, +ipv6_col IPv6, +decimal_col Decimal(5,3) +) +ENGINE = MergeTree() +ORDER BY (uint8_col); \ No newline at end of file diff --git a/clickhousewriter/src/main/java/com/alibaba/datax/plugin/writer/clickhousewriter/ClickhouseWriter.java b/clickhousewriter/src/main/java/com/alibaba/datax/plugin/writer/clickhousewriter/ClickhouseWriter.java index b928d4218e..83c421eedc 100644 --- a/clickhousewriter/src/main/java/com/alibaba/datax/plugin/writer/clickhousewriter/ClickhouseWriter.java +++ b/clickhousewriter/src/main/java/com/alibaba/datax/plugin/writer/clickhousewriter/ClickhouseWriter.java @@ -10,8 +10,8 @@ import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; import com.alibaba.datax.plugin.rdbms.util.DataBaseType; import com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; import java.sql.Array; import java.sql.Connection; @@ -68,7 +68,7 @@ public void init() { this.commonRdbmsWriterSlave = new CommonRdbmsWriter.Task(DATABASE_TYPE) { @Override - protected PreparedStatement fillPreparedStatementColumnType(PreparedStatement preparedStatement, int columnIndex, int columnSqltype, Column column) throws SQLException { + protected PreparedStatement fillPreparedStatementColumnType(PreparedStatement preparedStatement, int columnIndex, int columnSqltype, String typeName, Column column) throws SQLException { try { if (column.getRawData() == null) { preparedStatement.setNull(columnIndex + 1, columnSqltype); diff --git a/clickhousewriter/src/main/resources/plugin.json b/clickhousewriter/src/main/resources/plugin.json index ff1acf01bf..d70e2b1de5 100755 --- a/clickhousewriter/src/main/resources/plugin.json +++ b/clickhousewriter/src/main/resources/plugin.json @@ -2,5 +2,5 @@ "name": "clickhousewriter", "class": "com.alibaba.datax.plugin.writer.clickhousewriter.ClickhouseWriter", "description": "useScene: prod. mechanism: Jdbc connection using the database, execute insert sql.", - "developer": "jiye.tjy" + "developer": "alibaba" } \ No newline at end of file diff --git a/common/pom.xml b/common/pom.xml index 1a57cccda9..59d7073d5a 100755 --- a/common/pom.xml +++ b/common/pom.xml @@ -17,8 +17,8 @@ commons-lang3 - com.alibaba - fastjson + com.alibaba.fastjson2 + fastjson2 commons-io @@ -61,6 +61,14 @@ + + + src/main/java + + **/*.properties + + + maven-compiler-plugin diff --git a/common/src/main/java/com/alibaba/datax/common/element/BoolColumn.java b/common/src/main/java/com/alibaba/datax/common/element/BoolColumn.java index 7699e152ae..0978074bb9 100755 --- a/common/src/main/java/com/alibaba/datax/common/element/BoolColumn.java +++ b/common/src/main/java/com/alibaba/datax/common/element/BoolColumn.java @@ -92,7 +92,13 @@ public Date asDate() { throw DataXException.asDataXException( CommonErrorCode.CONVERT_NOT_SUPPORT, "Bool类型不能转为Date ."); } - + + @Override + public Date asDate(String dateFormat) { + throw DataXException.asDataXException( + CommonErrorCode.CONVERT_NOT_SUPPORT, "Bool类型不能转为Date ."); + } + @Override public byte[] asBytes() { throw DataXException.asDataXException( diff --git a/common/src/main/java/com/alibaba/datax/common/element/BytesColumn.java b/common/src/main/java/com/alibaba/datax/common/element/BytesColumn.java index d3cc599361..bc1eeb79d3 100755 --- a/common/src/main/java/com/alibaba/datax/common/element/BytesColumn.java +++ b/common/src/main/java/com/alibaba/datax/common/element/BytesColumn.java @@ -75,6 +75,12 @@ public Date asDate() { throw DataXException.asDataXException( CommonErrorCode.CONVERT_NOT_SUPPORT, "Bytes类型不能转为Date ."); } + + @Override + public Date asDate(String dateFormat) { + throw DataXException.asDataXException( + CommonErrorCode.CONVERT_NOT_SUPPORT, "Bytes类型不能转为Date ."); + } @Override public Boolean asBoolean() { diff --git a/common/src/main/java/com/alibaba/datax/common/element/Column.java b/common/src/main/java/com/alibaba/datax/common/element/Column.java index ed68e88d6b..13cfc7de5e 100755 --- a/common/src/main/java/com/alibaba/datax/common/element/Column.java +++ b/common/src/main/java/com/alibaba/datax/common/element/Column.java @@ -1,6 +1,6 @@ package com.alibaba.datax.common.element; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import java.math.BigDecimal; import java.math.BigInteger; @@ -55,6 +55,8 @@ protected void setByteSize(int byteSize) { public abstract String asString(); public abstract Date asDate(); + + public abstract Date asDate(String dateFormat); public abstract byte[] asBytes(); diff --git a/common/src/main/java/com/alibaba/datax/common/element/ColumnCast.java b/common/src/main/java/com/alibaba/datax/common/element/ColumnCast.java index 89d0a7c627..85d62ecc1c 100755 --- a/common/src/main/java/com/alibaba/datax/common/element/ColumnCast.java +++ b/common/src/main/java/com/alibaba/datax/common/element/ColumnCast.java @@ -22,6 +22,11 @@ public static Date string2Date(final StringColumn column) throws ParseException { return StringCast.asDate(column); } + + public static Date string2Date(final StringColumn column, String dateFormat) + throws ParseException { + return StringCast.asDate(column, dateFormat); + } public static byte[] string2Bytes(final StringColumn column) throws UnsupportedEncodingException { @@ -115,6 +120,16 @@ static Date asDate(final StringColumn column) throws ParseException { } throw e; } + + static Date asDate(final StringColumn column, String dateFormat) throws ParseException { + ParseException e; + try { + return FastDateFormat.getInstance(dateFormat, StringCast.timeZoner).parse(column.asString()); + } catch (ParseException ignored) { + e = ignored; + } + throw e; + } static byte[] asBytes(final StringColumn column) throws UnsupportedEncodingException { diff --git a/common/src/main/java/com/alibaba/datax/common/element/DateColumn.java b/common/src/main/java/com/alibaba/datax/common/element/DateColumn.java index 6626a6fbdd..df5e1e4a17 100755 --- a/common/src/main/java/com/alibaba/datax/common/element/DateColumn.java +++ b/common/src/main/java/com/alibaba/datax/common/element/DateColumn.java @@ -5,6 +5,7 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.sql.Time; import java.util.Date; /** @@ -12,18 +13,54 @@ */ public class DateColumn extends Column { - private DateType subType = DateType.DATETIME; - - public static enum DateType { - DATE, TIME, DATETIME - } - - /** - * 构建值为null的DateColumn,使用Date子类型为DATETIME - * */ - public DateColumn() { - this((Long)null); - } + private DateType subType = DateType.DATETIME; + + private int nanos = 0; + + private int precision = -1; + + public static enum DateType { + DATE, TIME, DATETIME + } + + /** + * 构建值为time(java.sql.Time)的DateColumn,使用Date子类型为TIME,只有时间,没有日期 + */ + public DateColumn(Time time, int nanos, int jdbcPrecision) { + this(time); + if (time != null) { + setNanos(nanos); + } + if (jdbcPrecision == 10) { + setPrecision(0); + } + if (jdbcPrecision >= 12 && jdbcPrecision <= 17) { + setPrecision(jdbcPrecision - 11); + } + } + + public long getNanos() { + return nanos; + } + + public void setNanos(int nanos) { + this.nanos = nanos; + } + + public int getPrecision() { + return precision; + } + + public void setPrecision(int precision) { + this.precision = precision; + } + + /** + * 构建值为null的DateColumn,使用Date子类型为DATETIME + */ + public DateColumn() { + this((Long) null); + } /** * 构建值为stamp(Unix时间戳)的DateColumn,使用Date子类型为DATETIME @@ -89,6 +126,11 @@ public Date asDate() { return new Date((Long)this.getRawData()); } + + @Override + public Date asDate(String dateFormat) { + return asDate(); + } @Override public byte[] asBytes() { diff --git a/common/src/main/java/com/alibaba/datax/common/element/DoubleColumn.java b/common/src/main/java/com/alibaba/datax/common/element/DoubleColumn.java index 17170ea6c4..915bd8effb 100755 --- a/common/src/main/java/com/alibaba/datax/common/element/DoubleColumn.java +++ b/common/src/main/java/com/alibaba/datax/common/element/DoubleColumn.java @@ -132,6 +132,12 @@ public Date asDate() { throw DataXException.asDataXException( CommonErrorCode.CONVERT_NOT_SUPPORT, "Double类型无法转为Date类型 ."); } + + @Override + public Date asDate(String dateFormat) { + throw DataXException.asDataXException( + CommonErrorCode.CONVERT_NOT_SUPPORT, "Double类型无法转为Date类型 ."); + } @Override public byte[] asBytes() { diff --git a/common/src/main/java/com/alibaba/datax/common/element/LongColumn.java b/common/src/main/java/com/alibaba/datax/common/element/LongColumn.java index d8113f7c05..e0f8d86525 100755 --- a/common/src/main/java/com/alibaba/datax/common/element/LongColumn.java +++ b/common/src/main/java/com/alibaba/datax/common/element/LongColumn.java @@ -125,6 +125,11 @@ public Date asDate() { } return new Date(this.asLong()); } + + @Override + public Date asDate(String dateFormat) { + return this.asDate(); + } @Override public byte[] asBytes() { diff --git a/common/src/main/java/com/alibaba/datax/common/element/Record.java b/common/src/main/java/com/alibaba/datax/common/element/Record.java index d06d80aafb..7abf45ddaa 100755 --- a/common/src/main/java/com/alibaba/datax/common/element/Record.java +++ b/common/src/main/java/com/alibaba/datax/common/element/Record.java @@ -1,5 +1,7 @@ package com.alibaba.datax.common.element; +import java.util.Map; + /** * Created by jingxing on 14-8-24. */ @@ -20,4 +22,8 @@ public interface Record { public int getMemorySize(); + public void setMeta(Map meta); + + public Map getMeta(); + } diff --git a/common/src/main/java/com/alibaba/datax/common/element/StringColumn.java b/common/src/main/java/com/alibaba/datax/common/element/StringColumn.java index 11209f4688..c1e7a84e0d 100755 --- a/common/src/main/java/com/alibaba/datax/common/element/StringColumn.java +++ b/common/src/main/java/com/alibaba/datax/common/element/StringColumn.java @@ -149,6 +149,16 @@ public Date asDate() { String.format("String[\"%s\"]不能转为Date .", this.asString())); } } + + @Override + public Date asDate(String dateFormat) { + try { + return ColumnCast.string2Date(this, dateFormat); + } catch (Exception e) { + throw DataXException.asDataXException(CommonErrorCode.CONVERT_NOT_SUPPORT, + String.format("String[\"%s\"]不能转为Date .", this.asString())); + } + } @Override public byte[] asBytes() { diff --git a/common/src/main/java/com/alibaba/datax/common/exception/DataXException.java b/common/src/main/java/com/alibaba/datax/common/exception/DataXException.java index f360e69900..09d00adcf1 100755 --- a/common/src/main/java/com/alibaba/datax/common/exception/DataXException.java +++ b/common/src/main/java/com/alibaba/datax/common/exception/DataXException.java @@ -16,6 +16,10 @@ public DataXException(ErrorCode errorCode, String errorMessage) { this.errorCode = errorCode; } + public DataXException(String errorMessage) { + super(errorMessage); + } + private DataXException(ErrorCode errorCode, String errorMessage, Throwable cause) { super(errorCode.toString() + " - " + getMessage(errorMessage) + " - " + getMessage(cause), cause); @@ -26,6 +30,10 @@ public static DataXException asDataXException(ErrorCode errorCode, String messag return new DataXException(errorCode, message); } + public static DataXException asDataXException(String message) { + return new DataXException(message); + } + public static DataXException asDataXException(ErrorCode errorCode, String message, Throwable cause) { if (cause instanceof DataXException) { return (DataXException) cause; diff --git a/common/src/main/java/com/alibaba/datax/common/plugin/AbstractPlugin.java b/common/src/main/java/com/alibaba/datax/common/plugin/AbstractPlugin.java index 184ee89ece..0323a97689 100755 --- a/common/src/main/java/com/alibaba/datax/common/plugin/AbstractPlugin.java +++ b/common/src/main/java/com/alibaba/datax/common/plugin/AbstractPlugin.java @@ -3,6 +3,8 @@ import com.alibaba.datax.common.base.BaseObject; import com.alibaba.datax.common.util.Configuration; +import java.util.List; + public abstract class AbstractPlugin extends BaseObject implements Pluginable { //作业的config private Configuration pluginJobConf; @@ -15,6 +17,8 @@ public abstract class AbstractPlugin extends BaseObject implements Pluginable { private String peerPluginName; + private List readerPluginSplitConf; + @Override public String getPluginName() { assert null != this.pluginConf; @@ -84,4 +88,12 @@ public void preHandler(Configuration jobConfiguration){ public void postHandler(Configuration jobConfiguration){ } + + public List getReaderPluginSplitConf(){ + return this.readerPluginSplitConf; + } + + public void setReaderPluginSplitConf(List readerPluginSplitConf){ + this.readerPluginSplitConf = readerPluginSplitConf; + } } diff --git a/common/src/main/java/com/alibaba/datax/common/statistics/PerfTrace.java b/common/src/main/java/com/alibaba/datax/common/statistics/PerfTrace.java index ea9aa42110..cf0457bca1 100644 --- a/common/src/main/java/com/alibaba/datax/common/statistics/PerfTrace.java +++ b/common/src/main/java/com/alibaba/datax/common/statistics/PerfTrace.java @@ -31,7 +31,6 @@ public class PerfTrace { private int taskGroupId; private int channelNumber; - private int priority; private int batchSize = 500; private volatile boolean perfReportEnable = true; @@ -54,12 +53,12 @@ public class PerfTrace { * @param taskGroupId * @return */ - public static PerfTrace getInstance(boolean isJob, long jobId, int taskGroupId, int priority, boolean enable) { + public static PerfTrace getInstance(boolean isJob, long jobId, int taskGroupId, boolean enable) { if (instance == null) { synchronized (lock) { if (instance == null) { - instance = new PerfTrace(isJob, jobId, taskGroupId, priority, enable); + instance = new PerfTrace(isJob, jobId, taskGroupId, enable); } } } @@ -76,22 +75,21 @@ public static PerfTrace getInstance() { LOG.error("PerfTrace instance not be init! must have some error! "); synchronized (lock) { if (instance == null) { - instance = new PerfTrace(false, -1111, -1111, 0, false); + instance = new PerfTrace(false, -1111, -1111, false); } } } return instance; } - private PerfTrace(boolean isJob, long jobId, int taskGroupId, int priority, boolean enable) { + private PerfTrace(boolean isJob, long jobId, int taskGroupId, boolean enable) { try { this.perfTraceId = isJob ? "job_" + jobId : String.format("taskGroup_%s_%s", jobId, taskGroupId); this.enable = enable; this.isJob = isJob; this.taskGroupId = taskGroupId; this.instId = jobId; - this.priority = priority; - LOG.info(String.format("PerfTrace traceId=%s, isEnable=%s, priority=%s", this.perfTraceId, this.enable, this.priority)); + LOG.info(String.format("PerfTrace traceId=%s, isEnable=%s", this.perfTraceId, this.enable)); } catch (Exception e) { // do nothing @@ -398,7 +396,6 @@ public synchronized JobStatisticsDto2 getReports(String mode) { jdo.setWindowEnd(this.windowEnd); jdo.setJobStartTime(jobStartTime); jdo.setJobRunTimeMs(System.currentTimeMillis() - jobStartTime.getTime()); - jdo.setJobPriority(this.priority); jdo.setChannelNum(this.channelNumber); jdo.setCluster(this.cluster); jdo.setJobDomain(this.jobDomain); @@ -609,7 +606,6 @@ class JobStatisticsDto2 { private Date jobStartTime; private Date jobEndTime; private Long jobRunTimeMs; - private Integer jobPriority; private Integer channelNum; private String cluster; private String jobDomain; @@ -680,10 +676,6 @@ public Long getJobRunTimeMs() { return jobRunTimeMs; } - public Integer getJobPriority() { - return jobPriority; - } - public Integer getChannelNum() { return channelNum; } @@ -816,10 +808,6 @@ public void setJobRunTimeMs(Long jobRunTimeMs) { this.jobRunTimeMs = jobRunTimeMs; } - public void setJobPriority(Integer jobPriority) { - this.jobPriority = jobPriority; - } - public void setChannelNum(Integer channelNum) { this.channelNum = channelNum; } diff --git a/common/src/main/java/com/alibaba/datax/common/statistics/VMInfo.java b/common/src/main/java/com/alibaba/datax/common/statistics/VMInfo.java index cab42a4b94..423c794e3a 100644 --- a/common/src/main/java/com/alibaba/datax/common/statistics/VMInfo.java +++ b/common/src/main/java/com/alibaba/datax/common/statistics/VMInfo.java @@ -77,8 +77,8 @@ private VMInfo() { garbageCollectorMXBeanList = java.lang.management.ManagementFactory.getGarbageCollectorMXBeans(); memoryPoolMXBeanList = java.lang.management.ManagementFactory.getMemoryPoolMXBeans(); - osInfo = runtimeMXBean.getVmVendor() + " " + runtimeMXBean.getSpecVersion() + " " + runtimeMXBean.getVmVersion(); - jvmInfo = osMXBean.getName() + " " + osMXBean.getArch() + " " + osMXBean.getVersion(); + jvmInfo = runtimeMXBean.getVmVendor() + " " + runtimeMXBean.getSpecVersion() + " " + runtimeMXBean.getVmVersion(); + osInfo = osMXBean.getName() + " " + osMXBean.getArch() + " " + osMXBean.getVersion(); totalProcessorCount = osMXBean.getAvailableProcessors(); //构建startPhyOSStatus diff --git a/common/src/main/java/com/alibaba/datax/common/util/Configuration.java b/common/src/main/java/com/alibaba/datax/common/util/Configuration.java index f570dd00c2..ef29320dd2 100755 --- a/common/src/main/java/com/alibaba/datax/common/util/Configuration.java +++ b/common/src/main/java/com/alibaba/datax/common/util/Configuration.java @@ -3,8 +3,8 @@ import com.alibaba.datax.common.exception.CommonErrorCode; import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.spi.ErrorCode; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONWriter; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.CharUtils; import org.apache.commons.lang3.StringUtils; @@ -411,6 +411,15 @@ public List getList(final String path) { return list; } + public List getListWithJson(final String path, Class t) { + Object object = this.get(path, List.class); + if (null == object) { + return null; + } + + return JSON.parseArray(JSON.toJSONString(object),t); + } + /** * 根据用户提供的json path,寻址List对象,如果对象不存在,返回null */ @@ -577,7 +586,7 @@ public T get(final String path, Class clazz) { */ public String beautify() { return JSON.toJSONString(this.getInternal(), - SerializerFeature.PrettyFormat); + JSONWriter.Feature.PrettyFormat); } /** @@ -1038,7 +1047,7 @@ private void checkPath(final String path) { "系统编程错误, 该异常代表系统编程错误, 请联系DataX开发团队!."); } - for (final String each : StringUtils.split(".")) { + for (final String each : StringUtils.split(path, ".")) { if (StringUtils.isBlank(each)) { throw new IllegalArgumentException(String.format( "系统编程错误, 路径[%s]不合法, 路径层次之间不能出现空白字符 .", path)); diff --git a/common/src/main/java/com/alibaba/datax/common/util/ConfigurationUtil.java b/common/src/main/java/com/alibaba/datax/common/util/ConfigurationUtil.java new file mode 100644 index 0000000000..e5e075475a --- /dev/null +++ b/common/src/main/java/com/alibaba/datax/common/util/ConfigurationUtil.java @@ -0,0 +1,37 @@ +package com.alibaba.datax.common.util; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import org.apache.commons.lang3.StringUtils; + +public class ConfigurationUtil { + private static final List SENSITIVE_KEYS = Arrays.asList("password", "accessKey", "securityToken", + "AccessKeyId", "AccessKeySecert", "AccessKeySecret", "clientPassword"); + + public static Configuration filterSensitive(Configuration origin) { + // shell 任务configuration metric 可能为null。 + if (origin == null) { + return origin; + } + // 确保不影响入参的对象 + Configuration configuration = origin.clone(); + Set keys = configuration.getKeys(); + for (final String key : keys) { + boolean isSensitive = false; + for (String sensitiveKey : SENSITIVE_KEYS) { + if (StringUtils.endsWithIgnoreCase(key, sensitiveKey)) { + isSensitive = true; + break; + } + } + + if (isSensitive && configuration.get(key) instanceof String) { + configuration.set(key, configuration.getString(key).replaceAll(".", "*")); + } + + } + return configuration; + } +} \ No newline at end of file diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/DESCipher.java b/common/src/main/java/com/alibaba/datax/common/util/DESCipher.java old mode 100644 new mode 100755 similarity index 60% rename from odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/DESCipher.java rename to common/src/main/java/com/alibaba/datax/common/util/DESCipher.java index dad82d501d..0692a7b3e3 --- a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/DESCipher.java +++ b/common/src/main/java/com/alibaba/datax/common/util/DESCipher.java @@ -1,5 +1,5 @@ /** - * (C) 2010-2014 Alibaba Group Holding Limited. + * (C) 2010-2022 Alibaba Group Holding Limited. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,342 +14,216 @@ * limitations under the License. */ -package com.alibaba.datax.plugin.reader.odpsreader.util; +package com.alibaba.datax.common.util; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.DESKeySpec; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.security.SecureRandom; /** - *   * DES加解密,支持与delphi交互(字符串编码需统一为UTF-8) - * - *   * - * - *   * @author wym - * - *    + * DES加解密,支持与delphi交互(字符串编码需统一为UTF-8) + * 将这个工具类抽取到 common 中,方便后续代码复用 */ - public class DESCipher { - + private static Logger LOGGER = LoggerFactory.getLogger(DESCipher.class); /** - *   * 密钥 - * - *    + * 密钥 */ - - public static final String KEY = "u4Gqu4Z8"; - + public static final String KEY = ""; private final static String DES = "DES"; /** - *   * 加密 - * - *   * - * - *   * @param src - * - *   * 明文(字节) - * - *   * @param key - * - *   * 密钥,长度必须是8的倍数 - * - *   * @return 密文(字节) - * - *   * @throws Exception - * - *    + * 加密 + * @param src 明文(字节) + * @param key 密钥,长度必须是8的倍数 + * @return 密文(字节) + * @throws Exception */ - public static byte[] encrypt(byte[] src, byte[] key) throws Exception { - // DES算法要求有一个可信任的随机数源 - SecureRandom sr = new SecureRandom(); - + // 从原始密匙数据创建DESKeySpec对象 - DESKeySpec dks = new DESKeySpec(key); - + // 创建一个密匙工厂,然后用它把DESKeySpec转换成 - // 一个SecretKey对象 - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES); - SecretKey securekey = keyFactory.generateSecret(dks); - + // Cipher对象实际完成加密操作 - Cipher cipher = Cipher.getInstance(DES); // 用密匙初始化Cipher对象 - cipher.init(Cipher.ENCRYPT_MODE, securekey, sr); // 现在,获取数据并加密 - // 正式执行加密操作 - return cipher.doFinal(src); - } /** - *   * 解密 - * - *   * - * - *   * @param src - * - *   * 密文(字节) - * - *   * @param key - * - *   * 密钥,长度必须是8的倍数 - * - *   * @return 明文(字节) - * - *   * @throws Exception - * - *    + * * 解密 + * * @param src + * * 密文(字节) + * * @param key + * * 密钥,长度必须是8的倍数 + * * @return 明文(字节) + * * @throws Exception */ - public static byte[] decrypt(byte[] src, byte[] key) throws Exception { - // DES算法要求有一个可信任的随机数源 - SecureRandom sr = new SecureRandom(); // 从原始密匙数据创建一个DESKeySpec对象 - DESKeySpec dks = new DESKeySpec(key); // 创建一个密匙工厂,然后用它把DESKeySpec对象转换成 - // 一个SecretKey对象 - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES); - SecretKey securekey = keyFactory.generateSecret(dks); // Cipher对象实际完成解密操作 - Cipher cipher = Cipher.getInstance(DES); // 用密匙初始化Cipher对象 - cipher.init(Cipher.DECRYPT_MODE, securekey, sr); // 现在,获取数据并解密 - // 正式执行解密操作 - return cipher.doFinal(src); - } /** - *   * 加密 - * - *   * - * - *   * @param src - * - *   * 明文(字节) - * - *   * @return 密文(字节) - * - *   * @throws Exception - * - *    + * 加密 + * @param src * 明文(字节) + * @return 密文(字节) + * @throws Exception */ - public static byte[] encrypt(byte[] src) throws Exception { - return encrypt(src, KEY.getBytes()); - } /** - *   * 解密 - * - *   * - * - *   * @param src - * - *   * 密文(字节) - * - *   * @return 明文(字节) - * - *   * @throws Exception - * - *    + * 解密 + * @param src 密文(字节) + * @return 明文(字节) + * @throws Exception */ - public static byte[] decrypt(byte[] src) throws Exception { - return decrypt(src, KEY.getBytes()); - } /** - *   * 加密 - * - *   * - * - *   * @param src - * - *   * 明文(字符串) - * - *   * @return 密文(16进制字符串) - * - *   * @throws Exception - * - *    + * 加密 + * @param src 明文(字符串) + * @return 密文(16进制字符串) + * @throws Exception */ - public final static String encrypt(String src) { - try { - return byte2hex(encrypt(src.getBytes(), KEY.getBytes())); - } catch (Exception e) { - - e.printStackTrace(); - + LOGGER.warn(e.getMessage(), e); } - return null; - } - + /** - *   * 解密 - * - *   * - * - *   * @param src - * - *   * 密文(字符串) - * - *   * @return 明文(字符串) - * - *   * @throws Exception - * - *    + * 加密 + * @param src 明文(字符串) + * @param encryptKey 加密用的秘钥 + * @return 密文(16进制字符串) + * @throws Exception */ + public final static String encrypt(String src, String encryptKey) { + try { + return byte2hex(encrypt(src.getBytes(), encryptKey.getBytes())); + } catch (Exception e) { + LOGGER.warn(e.getMessage(), e); + } + return null; + } + /** + * 解密 + * @param src 密文(字符串) + * @return 明文(字符串) + * @throws Exception + */ public final static String decrypt(String src) { try { - return new String(decrypt(hex2byte(src.getBytes()), KEY.getBytes())); - } catch (Exception e) { - - e.printStackTrace(); - + LOGGER.warn(e.getMessage(), e); } - return null; - } - + /** - *   * 加密 - * - *   * - * - *   * @param src - * - *   * 明文(字节) - * - *   * @return 密文(16进制字符串) - * - *   * @throws Exception - * - *    + * 解密 + * @param src 密文(字符串) + * @param decryptKey 解密用的秘钥 + * @return 明文(字符串) + * @throws Exception */ + public final static String decrypt(String src, String decryptKey) { + try { + return new String(decrypt(hex2byte(src.getBytes()), decryptKey.getBytes())); + } catch (Exception e) { + LOGGER.warn(e.getMessage(), e); + } + return null; + } + /** + * 加密 + * @param src + * 明文(字节) + * @return 密文(16进制字符串) + * @throws Exception + */ public static String encryptToString(byte[] src) throws Exception { - return encrypt(new String(src)); - } /** - *   * 解密 - * - *   * - * - *   * @param src - * - *   * 密文(字节) - * - *   * @return 明文(字符串) - * - *   * @throws Exception - * - *    + * 解密 + * @param src 密文(字节) + * @return 明文(字符串) + * @throws Exception */ - public static String decryptToString(byte[] src) throws Exception { - return decrypt(new String(src)); - } public static String byte2hex(byte[] b) { - String hs = ""; - String stmp = ""; - for (int n = 0; n < b.length; n++) { - stmp = (Integer.toHexString(b[n] & 0XFF)); - if (stmp.length() == 1) - hs = hs + "0" + stmp; - else - hs = hs + stmp; - } - return hs.toUpperCase(); - } public static byte[] hex2byte(byte[] b) { - if ((b.length % 2) != 0) - - throw new IllegalArgumentException("长度不是偶数"); - + throw new IllegalArgumentException("The length is not an even number"); byte[] b2 = new byte[b.length / 2]; - for (int n = 0; n < b.length; n += 2) { - String item = new String(b, n, 2); - b2[n / 2] = (byte) Integer.parseInt(item, 16); - } return b2; - } - - /* - * public static void main(String[] args) { try { String src = "cheetah"; - * String crypto = DESCipher.encrypt(src); System.out.println("密文[" + src + - * "]:" + crypto); System.out.println("解密后:" + DESCipher.decrypt(crypto)); } - * catch (Exception e) { e.printStackTrace(); } } - */ } diff --git a/common/src/main/java/com/alibaba/datax/common/util/DataXCaseEnvUtil.java b/common/src/main/java/com/alibaba/datax/common/util/DataXCaseEnvUtil.java new file mode 100644 index 0000000000..ca137b94de --- /dev/null +++ b/common/src/main/java/com/alibaba/datax/common/util/DataXCaseEnvUtil.java @@ -0,0 +1,33 @@ +package com.alibaba.datax.common.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class DataXCaseEnvUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger(DataXCaseEnvUtil.class); + + // datax回归测试效率提升 + private static String DATAX_AUTOTEST_RETRY_TIME = System.getenv("DATAX_AUTOTEST_RETRY_TIME"); + private static String DATAX_AUTOTEST_RETRY_INTERVAL = System.getenv("DATAX_AUTOTEST_RETRY_INTERVAL"); + private static String DATAX_AUTOTEST_RETRY_EXPONENTIAL = System.getenv("DATAX_AUTOTEST_RETRY_EXPONENTIAL"); + + public static int getRetryTimes(int retryTimes) { + int actualRetryTimes = DATAX_AUTOTEST_RETRY_TIME != null ? Integer.valueOf(DATAX_AUTOTEST_RETRY_TIME) : retryTimes; + // LOGGER.info("The actualRetryTimes is {}", actualRetryTimes); + return actualRetryTimes; + } + + public static long getRetryInterval(long retryInterval) { + long actualRetryInterval = DATAX_AUTOTEST_RETRY_INTERVAL != null ? Long.valueOf(DATAX_AUTOTEST_RETRY_INTERVAL) : retryInterval; + // LOGGER.info("The actualRetryInterval is {}", actualRetryInterval); + return actualRetryInterval; + } + + public static boolean getRetryExponential(boolean retryExponential) { + boolean actualRetryExponential = DATAX_AUTOTEST_RETRY_EXPONENTIAL != null ? Boolean.valueOf(DATAX_AUTOTEST_RETRY_EXPONENTIAL) : retryExponential; + // LOGGER.info("The actualRetryExponential is {}", actualRetryExponential); + return actualRetryExponential; + } +} diff --git a/common/src/main/java/com/alibaba/datax/common/util/LimitLogger.java b/common/src/main/java/com/alibaba/datax/common/util/LimitLogger.java new file mode 100644 index 0000000000..a307e0fb4e --- /dev/null +++ b/common/src/main/java/com/alibaba/datax/common/util/LimitLogger.java @@ -0,0 +1,34 @@ +package com.alibaba.datax.common.util; + +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author jitongchen + * @date 2023/9/7 9:47 AM + */ +public class LimitLogger { + + private static Map lastPrintTime = new HashMap<>(); + + public static void limit(String name, long limit, LoggerFunction function) { + if (StringUtils.isBlank(name)) { + name = "__all__"; + } + if (limit <= 0) { + function.apply(); + } else { + if (!lastPrintTime.containsKey(name)) { + lastPrintTime.put(name, System.currentTimeMillis()); + function.apply(); + } else { + if (System.currentTimeMillis() > lastPrintTime.get(name) + limit) { + lastPrintTime.put(name, System.currentTimeMillis()); + function.apply(); + } + } + } + } +} diff --git a/common/src/main/java/com/alibaba/datax/common/util/ListUtil.java b/common/src/main/java/com/alibaba/datax/common/util/ListUtil.java index d7a5b76462..a381bb90fc 100755 --- a/common/src/main/java/com/alibaba/datax/common/util/ListUtil.java +++ b/common/src/main/java/com/alibaba/datax/common/util/ListUtil.java @@ -6,6 +6,7 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; /** @@ -136,4 +137,25 @@ public static List valueToLowerCase(List aList) { return result; } + + public static Boolean checkIfHasSameValue(List listA, List listB) { + if (null == listA || listA.isEmpty() || null == listB || listB.isEmpty()) { + return false; + } + + for (String oneValue : listA) { + if (listB.contains(oneValue)) { + return true; + } + } + + return false; + } + + public static boolean checkIfAllSameValue(List listA, List listB) { + if (null == listA || listA.isEmpty() || null == listB || listB.isEmpty() || listA.size() != listB.size()) { + return false; + } + return new HashSet<>(listA).containsAll(new HashSet<>(listB)); + } } diff --git a/common/src/main/java/com/alibaba/datax/common/util/LocalStrings.properties b/common/src/main/java/com/alibaba/datax/common/util/LocalStrings.properties new file mode 100644 index 0000000000..25661f7a67 --- /dev/null +++ b/common/src/main/java/com/alibaba/datax/common/util/LocalStrings.properties @@ -0,0 +1,54 @@ +very_like_yixiao=\u4e00{0}\u4e8c{1}\u4e09 + + +configuration.1=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef\uff0c\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6[{0}]\u4e0d\u5b58\u5728. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6. +configuration.2=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6[{0}]\u8bfb\u53d6\u5931\u8d25\uff0c\u9519\u8bef\u539f\u56e0: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6\u7684\u6743\u9650\u8bbe\u7f6e. +configuration.3=\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6. \u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u8bfb\u53d6\u5931\u8d25\uff0c\u9519\u8bef\u539f\u56e0: {0}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6\u7684\u6743\u9650\u8bbe\u7f6e. +configuration.4=\u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6\u6709\u8bef\uff0c[{0}]\u662f\u5fc5\u586b\u53c2\u6570\uff0c\u4e0d\u5141\u8bb8\u4e3a\u7a7a\u6216\u8005\u7559\u767d . +configuration.5=\u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6\u6709\u8bef\uff0c[{0}]\u662f\u5fc5\u586b\u53c2\u6570\uff0c\u4e0d\u5141\u8bb8\u4e3a\u7a7a\u6216\u8005\u7559\u767d . +configuration.6=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u56e0\u4e3a\u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5\uff0c\u671f\u671b\u662f\u5b57\u7b26\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.7=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u6709\u8bef\uff0c\u56e0\u4e3a\u4ece[{0}]\u83b7\u53d6\u7684\u503c[{1}]\u65e0\u6cd5\u8f6c\u6362\u4e3abool\u7c7b\u578b. \u8bf7\u68c0\u67e5\u6e90\u8868\u7684\u914d\u7f6e\u5e76\u4e14\u505a\u51fa\u76f8\u5e94\u7684\u4fee\u6539. +configuration.8=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6574\u6570\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.9=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6574\u6570\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.10=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6d6e\u70b9\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.11=\u914d\u7f6e\u6587\u4ef6\u5bf9\u5e94Key[{0}]\u5e76\u4e0d\u5b58\u5728\uff0c\u8be5\u60c5\u51b5\u662f\u4ee3\u7801\u7f16\u7a0b\u9519\u8bef. \u8bf7\u8054\u7cfbDataX\u56e2\u961f\u7684\u540c\u5b66. +configuration.12=\u503c[{0}]\u65e0\u6cd5\u9002\u914d\u60a8\u63d0\u4f9b[{1}]\uff0c \u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f! +configuration.13=Path\u4e0d\u80fd\u4e3anull\uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.14=\u8def\u5f84[{0}]\u51fa\u73b0\u975e\u6cd5\u503c\u7c7b\u578b[{1}]\uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f! . +configuration.15=\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.16=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u9700\u8981\u914d\u7f6eJson\u683c\u5f0f\u7684Map\u5bf9\u8c61\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{1}]. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.17=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u503c\u4e3anull\uff0cdatax\u65e0\u6cd5\u8bc6\u522b\u8be5\u914d\u7f6e. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.18=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u9700\u8981\u914d\u7f6eJson\u683c\u5f0f\u7684Map\u5bf9\u8c61\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{1}]. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.19=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef\uff0c\u5217\u8868\u4e0b\u6807\u5fc5\u987b\u4e3a\u6570\u5b57\u7c7b\u578b\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{0}] \uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.20=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f!. +configuration.21=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8def\u5f84[{0}]\u4e0d\u5408\u6cd5, \u8def\u5f84\u5c42\u6b21\u4e4b\u95f4\u4e0d\u80fd\u51fa\u73b0\u7a7a\u767d\u5b57\u7b26 . +configuration.22=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u56e0\u4e3a\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u662f\u5408\u6cd5\u7684JSON\u683c\u5f0f, JSON\u4e0d\u80fd\u4e3a\u7a7a\u767d. \u8bf7\u6309\u7167\u6807\u51c6json\u683c\u5f0f\u63d0\u4f9b\u914d\u7f6e\u4fe1\u606f. +configuration.23=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u662f\u5408\u6cd5\u7684JSON\u683c\u5f0f: {0} . \u8bf7\u6309\u7167\u6807\u51c6json\u683c\u5f0f\u63d0\u4f9b\u914d\u7f6e\u4fe1\u606f. + + +listutil.1=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef\uff0cList\u4e0d\u80fd\u4e3a\u7a7a. +listutil.2=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.3=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u4fe1\u606f\u6709\u8bef, String:[{0}] \u4e0d\u5141\u8bb8\u91cd\u590d\u51fa\u73b0\u5728\u5217\u8868\u4e2d: [{1}]. +listutil.4=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.5=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.6=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u4fe1\u606f\u6709\u8bef, String:[{0}] \u4e0d\u5b58\u5728\u4e8e\u5217\u8868\u4e2d:[{1}]. +listutil.7=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.8=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. + + +rangesplitutil.1=\u5207\u5206\u4efd\u6570\u4e0d\u80fd\u5c0f\u4e8e1. \u6b64\u5904:expectSliceNumber=[{0}]. +rangesplitutil.2=\u5bf9 BigInteger \u8fdb\u884c\u5207\u5206\u65f6\uff0c\u5176\u5de6\u53f3\u533a\u95f4\u4e0d\u80fd\u4e3a null. \u6b64\u5904:left=[{0}],right=[{1}]. +rangesplitutil.3=\u53c2\u6570 bigInteger \u4e0d\u80fd\u4e3a\u7a7a. +rangesplitutil.4=\u6839\u636e\u5b57\u7b26\u4e32\u8fdb\u884c\u5207\u5206\u65f6\u4ec5\u652f\u6301 ASCII \u5b57\u7b26\u4e32\uff0c\u800c\u5b57\u7b26\u4e32:[{0}]\u975e ASCII \u5b57\u7b26\u4e32. +rangesplitutil.5=\u53c2\u6570 bigInteger \u4e0d\u80fd\u4e3a\u7a7a. +rangesplitutil.6=\u6839\u636e\u5b57\u7b26\u4e32\u8fdb\u884c\u5207\u5206\u65f6\u4ec5\u652f\u6301 ASCII \u5b57\u7b26\u4e32\uff0c\u800c\u5b57\u7b26\u4e32:[{0}]\u975e ASCII \u5b57\u7b26\u4e32. + + +retryutil.1=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u5165\u53c2callable\u4e0d\u80fd\u4e3a\u7a7a ! +retryutil.2=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u5165\u53c2retrytime[%d]\u4e0d\u80fd\u5c0f\u4e8e1 ! +retryutil.3=Exception when calling callable, \u5f02\u5e38Msg:{0} +retryutil.4=Exception when calling callable, \u5373\u5c06\u5c1d\u8bd5\u6267\u884c\u7b2c{0}\u6b21\u91cd\u8bd5,\u5171\u8ba1\u91cd\u8bd5{1}\u6b21.\u672c\u6b21\u91cd\u8bd5\u8ba1\u5212\u7b49\u5f85[{2}]ms,\u5b9e\u9645\u7b49\u5f85[{3}]ms, \u5f02\u5e38Msg:[{4}] + + +httpclientutil.1=\u8BF7\u6C42\u5730\u5740\uFF1A{0}, \u8BF7\u6C42\u65B9\u6CD5\uFF1A{1}, STATUS CODE = {2}, Response Entity: {3} +httpclientutil.2=\u8FDC\u7A0B\u63A5\u53E3\u8FD4\u56DE-1,\u5C06\u91CD\u8BD5 diff --git a/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_en_US.properties b/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_en_US.properties new file mode 100644 index 0000000000..2074bbb952 --- /dev/null +++ b/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_en_US.properties @@ -0,0 +1,53 @@ +very_like_yixiao=1{0}2{1}3 + + +configuration.1=Configuration information error. The configuration file [{0}] you provided does not exist. Please check your configuration files. +configuration.2=Configuration information error. Failed to read the configuration file [{0}] you provided. Error reason: {1}. Please check the permission settings of your configuration files. +configuration.3=Please check your configuration files. Failed to read the configuration file you provided. Error reason: {0}. Please check the permission settings of your configuration files. +configuration.4=The configuration file you provided contains errors. [{0}] is a required parameter and cannot be empty or blank. +configuration.5=The configuration file you provided contains errors. [{0}] is a required parameter and cannot be empty or blank. +configuration.6=Task reading configuration file error. Invalid configuration file path [{0}] value. The expected value should be of the character type: {1}. Please check your configuration and make corrections. +configuration.7=The configuration information you provided contains errors. The value [{1}] obtained from [{0}] cannot be converted to the Bool type. Please check the source table configuration and make corrections. +configuration.8=Task reading configuration file error. Invalid configuration file path [{0}] value. The expected value should be of the integer type: {1}. Please check your configuration and make corrections. +configuration.9=Task reading configuration file error. Invalid configuration file path [{0}] value. The expected value should be of the integer type: {1}. Please check your configuration and make corrections. +configuration.10=Task reading configuration file error. Invalid configuration file path [{0}] value. The expected value should be of the floating-point type: {1}. Please check your configuration and make corrections. +configuration.11=The Key [{0}] for the configuration file does not exist. This is a code programming error. Please contact the DataX team. +configuration.12=The value [{0}] cannot adapt to the [{1}] you provided. This exception represents a system programming error. Please contact the DataX developer team. +configuration.13=The path cannot be null. This exception represents a system programming error. Please contact the DataX developer team. +configuration.14=The path [{0}] has an invalid value type [{1}]. This exception represents a system programming error. Please contact the DataX developer team. +configuration.15=This exception represents a system programming error. Please contact the DataX developer team. +configuration.16=The configuration file you provided contains errors. The path [{0}] requires you to configure a Map object in JSON format, but the actual type found on the node is [{1}]. Please check your configuration and make corrections. +configuration.17=The configuration file you provided contains errors. The value of the path [{0}] is null and DataX cannot recognize the configuration. Please check your configuration and make corrections. +configuration.18=The configuration file you provided contains errors. The path [{0}] requires you to configure a Map object in JSON format, but the actual type found on the node is [{1}]. Please check your configuration and make corrections. +configuration.19=System programming error. The list subscript must be of the numeric type, but the actual type found on this node is [{0}]. This exception represents a system programming error. Please contact the DataX developer team. +configuration.20=System programming error. This exception represents a system programming error. Please contact the DataX developer team. +configuration.21=System programming error. Invalid path [{0}]. No spaces are allowed between path layers. +configuration.22=Configuration information error. The configuration information you provided is not in a legal JSON format. JSON cannot be blank. Please provide the configuration information in the standard JSON format. +configuration.23=Configuration information error. The configuration information you provided is not in a valid JSON format: {0}. Please provide the configuration information in the standard JSON format. + + +listutil.1=The job configuration you provided contains errors. The list cannot be empty. +listutil.2=The job configuration you provided contains errors. The list cannot be empty. +listutil.3=The job configuration information you provided contains errors. String: [{0}] is not allowed to be repeated in the list: [{1}]. +listutil.4=The job configuration you provided contains errors. The list cannot be empty. +listutil.5=The job configuration you provided contains errors. The list cannot be empty. +listutil.6=The job configuration information you provided contains errors. String: [{0}] does not exist in the list: [{1}]. +listutil.7=The job configuration you provided contains errors. The list cannot be empty. +listutil.8=The job configuration you provided contains errors. The list cannot be empty. + + +rangesplitutil.1=The slice number cannot be less than 1. Here: [expectSliceNumber]=[{0}]. +rangesplitutil.2=The left or right intervals of BigInteger character strings cannot be null when they are sliced. Here: [left]=[{0}], [right]=[{1}]. +rangesplitutil.3=The [bigInteger] parameter cannot be null. +rangesplitutil.4=Only ASCII character strings are supported for character string slicing, but the [{0}] character string is not of the ASCII type. +rangesplitutil.5=The [bigInteger] parameter cannot be null. +rangesplitutil.6=Only ASCII character strings are supported for character string slicing, but the [{0}] character string is not of the ASCII type. + + +retryutil.1=System programming error. The “callable” input parameter cannot be null. +retryutil.2=System programming error. The “retrytime[%d]” input parameter cannot be less than 1. +retryutil.3=Exception when calling callable. Exception Msg: {0} +retryutil.4=Exception when calling callable. Retry Attempt: {0} will start soon. {1} attempts in total. This attempt planned to wait for [{2}]ms, and actually waited for [{3}]ms. Exception Msg: [{4}]. + +httpclientutil.1=Request address: {0}. Request method: {1}. STATUS CODE = {2}, Response Entity: {3} +httpclientutil.2=The remote interface returns -1. We will try again \ No newline at end of file diff --git a/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_ja_JP.properties b/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..d4409a8df3 --- /dev/null +++ b/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_ja_JP.properties @@ -0,0 +1,53 @@ +very_like_yixiao=1{0}2{1}3 + + +configuration.1=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef\uff0c\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6[{0}]\u4e0d\u5b58\u5728. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6. +configuration.2=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6[{0}]\u8bfb\u53d6\u5931\u8d25\uff0c\u9519\u8bef\u539f\u56e0: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6\u7684\u6743\u9650\u8bbe\u7f6e. +configuration.3=\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6. \u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u8bfb\u53d6\u5931\u8d25\uff0c\u9519\u8bef\u539f\u56e0: {0}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6\u7684\u6743\u9650\u8bbe\u7f6e. +configuration.4=\u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6\u6709\u8bef\uff0c[{0}]\u662f\u5fc5\u586b\u53c2\u6570\uff0c\u4e0d\u5141\u8bb8\u4e3a\u7a7a\u6216\u8005\u7559\u767d . +configuration.5=\u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6\u6709\u8bef\uff0c[{0}]\u662f\u5fc5\u586b\u53c2\u6570\uff0c\u4e0d\u5141\u8bb8\u4e3a\u7a7a\u6216\u8005\u7559\u767d . +configuration.6=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u56e0\u4e3a\u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5\uff0c\u671f\u671b\u662f\u5b57\u7b26\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.7=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u6709\u8bef\uff0c\u56e0\u4e3a\u4ece[{0}]\u83b7\u53d6\u7684\u503c[{1}]\u65e0\u6cd5\u8f6c\u6362\u4e3abool\u7c7b\u578b. \u8bf7\u68c0\u67e5\u6e90\u8868\u7684\u914d\u7f6e\u5e76\u4e14\u505a\u51fa\u76f8\u5e94\u7684\u4fee\u6539. +configuration.8=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6574\u6570\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.9=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6574\u6570\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.10=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6d6e\u70b9\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.11=\u914d\u7f6e\u6587\u4ef6\u5bf9\u5e94Key[{0}]\u5e76\u4e0d\u5b58\u5728\uff0c\u8be5\u60c5\u51b5\u662f\u4ee3\u7801\u7f16\u7a0b\u9519\u8bef. \u8bf7\u8054\u7cfbDataX\u56e2\u961f\u7684\u540c\u5b66. +configuration.12=\u503c[{0}]\u65e0\u6cd5\u9002\u914d\u60a8\u63d0\u4f9b[{1}]\uff0c \u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f! +configuration.13=Path\u4e0d\u80fd\u4e3anull\uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.14=\u8def\u5f84[{0}]\u51fa\u73b0\u975e\u6cd5\u503c\u7c7b\u578b[{1}]\uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f! . +configuration.15=\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.16=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u9700\u8981\u914d\u7f6eJson\u683c\u5f0f\u7684Map\u5bf9\u8c61\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{1}]. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.17=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u503c\u4e3anull\uff0cdatax\u65e0\u6cd5\u8bc6\u522b\u8be5\u914d\u7f6e. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.18=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u9700\u8981\u914d\u7f6eJson\u683c\u5f0f\u7684Map\u5bf9\u8c61\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{1}]. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.19=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef\uff0c\u5217\u8868\u4e0b\u6807\u5fc5\u987b\u4e3a\u6570\u5b57\u7c7b\u578b\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{0}] \uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.20=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f!. +configuration.21=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8def\u5f84[{0}]\u4e0d\u5408\u6cd5, \u8def\u5f84\u5c42\u6b21\u4e4b\u95f4\u4e0d\u80fd\u51fa\u73b0\u7a7a\u767d\u5b57\u7b26 . +configuration.22=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u56e0\u4e3a\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u662f\u5408\u6cd5\u7684JSON\u683c\u5f0f, JSON\u4e0d\u80fd\u4e3a\u7a7a\u767d. \u8bf7\u6309\u7167\u6807\u51c6json\u683c\u5f0f\u63d0\u4f9b\u914d\u7f6e\u4fe1\u606f. +configuration.23=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u662f\u5408\u6cd5\u7684JSON\u683c\u5f0f: {0} . \u8bf7\u6309\u7167\u6807\u51c6json\u683c\u5f0f\u63d0\u4f9b\u914d\u7f6e\u4fe1\u606f. + + +listutil.1=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef\uff0cList\u4e0d\u80fd\u4e3a\u7a7a. +listutil.2=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.3=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u4fe1\u606f\u6709\u8bef, String:[{0}] \u4e0d\u5141\u8bb8\u91cd\u590d\u51fa\u73b0\u5728\u5217\u8868\u4e2d: [{1}]. +listutil.4=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.5=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.6=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u4fe1\u606f\u6709\u8bef, String:[{0}] \u4e0d\u5b58\u5728\u4e8e\u5217\u8868\u4e2d:[{1}]. +listutil.7=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.8=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. + + +rangesplitutil.1=\u5207\u5206\u4efd\u6570\u4e0d\u80fd\u5c0f\u4e8e1. \u6b64\u5904:expectSliceNumber=[{0}]. +rangesplitutil.2=\u5bf9 BigInteger \u8fdb\u884c\u5207\u5206\u65f6\uff0c\u5176\u5de6\u53f3\u533a\u95f4\u4e0d\u80fd\u4e3a null. \u6b64\u5904:left=[{0}],right=[{1}]. +rangesplitutil.3=\u53c2\u6570 bigInteger \u4e0d\u80fd\u4e3a\u7a7a. +rangesplitutil.4=\u6839\u636e\u5b57\u7b26\u4e32\u8fdb\u884c\u5207\u5206\u65f6\u4ec5\u652f\u6301 ASCII \u5b57\u7b26\u4e32\uff0c\u800c\u5b57\u7b26\u4e32:[{0}]\u975e ASCII \u5b57\u7b26\u4e32. +rangesplitutil.5=\u53c2\u6570 bigInteger \u4e0d\u80fd\u4e3a\u7a7a. +rangesplitutil.6=\u6839\u636e\u5b57\u7b26\u4e32\u8fdb\u884c\u5207\u5206\u65f6\u4ec5\u652f\u6301 ASCII \u5b57\u7b26\u4e32\uff0c\u800c\u5b57\u7b26\u4e32:[{0}]\u975e ASCII \u5b57\u7b26\u4e32. + + +retryutil.1=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u5165\u53c2callable\u4e0d\u80fd\u4e3a\u7a7a ! +retryutil.2=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u5165\u53c2retrytime[%d]\u4e0d\u80fd\u5c0f\u4e8e1 ! +retryutil.3=Exception when calling callable, \u5f02\u5e38Msg:{0} +retryutil.4=Exception when calling callable, \u5373\u5c06\u5c1d\u8bd5\u6267\u884c\u7b2c{0}\u6b21\u91cd\u8bd5,\u5171\u8ba1\u91cd\u8bd5{1}\u6b21.\u672c\u6b21\u91cd\u8bd5\u8ba1\u5212\u7b49\u5f85[{2}]ms,\u5b9e\u9645\u7b49\u5f85[{3}]ms, \u5f02\u5e38Msg:[{4}] + +httpclientutil.1=\u8BF7\u6C42\u5730\u5740\uFF1A{0}, \u8BF7\u6C42\u65B9\u6CD5\uFF1A{1},STATUS CODE = {2}, Response Entity: {3} +httpclientutil.2=\u8FDC\u7A0B\u63A5\u53E3\u8FD4\u56DE-1,\u5C06\u91CD\u8BD5 \ No newline at end of file diff --git a/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_zh_CN.properties b/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..533dcd52dd --- /dev/null +++ b/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_zh_CN.properties @@ -0,0 +1,54 @@ +very_like_yixiao=\u4e00{0}\u4e8c{1}\u4e09 + + +configuration.1=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef\uff0c\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6[{0}]\u4e0d\u5b58\u5728. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6. +configuration.2=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6[{0}]\u8bfb\u53d6\u5931\u8d25\uff0c\u9519\u8bef\u539f\u56e0: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6\u7684\u6743\u9650\u8bbe\u7f6e. +configuration.3=\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6. \u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u8bfb\u53d6\u5931\u8d25\uff0c\u9519\u8bef\u539f\u56e0: {0}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6\u7684\u6743\u9650\u8bbe\u7f6e. +configuration.4=\u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6\u6709\u8bef\uff0c[{0}]\u662f\u5fc5\u586b\u53c2\u6570\uff0c\u4e0d\u5141\u8bb8\u4e3a\u7a7a\u6216\u8005\u7559\u767d . +configuration.5=\u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6\u6709\u8bef\uff0c[{0}]\u662f\u5fc5\u586b\u53c2\u6570\uff0c\u4e0d\u5141\u8bb8\u4e3a\u7a7a\u6216\u8005\u7559\u767d . +configuration.6=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u56e0\u4e3a\u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5\uff0c\u671f\u671b\u662f\u5b57\u7b26\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.7=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u6709\u8bef\uff0c\u56e0\u4e3a\u4ece[{0}]\u83b7\u53d6\u7684\u503c[{1}]\u65e0\u6cd5\u8f6c\u6362\u4e3abool\u7c7b\u578b. \u8bf7\u68c0\u67e5\u6e90\u8868\u7684\u914d\u7f6e\u5e76\u4e14\u505a\u51fa\u76f8\u5e94\u7684\u4fee\u6539. +configuration.8=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6574\u6570\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.9=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6574\u6570\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.10=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6d6e\u70b9\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.11=\u914d\u7f6e\u6587\u4ef6\u5bf9\u5e94Key[{0}]\u5e76\u4e0d\u5b58\u5728\uff0c\u8be5\u60c5\u51b5\u662f\u4ee3\u7801\u7f16\u7a0b\u9519\u8bef. \u8bf7\u8054\u7cfbDataX\u56e2\u961f\u7684\u540c\u5b66. +configuration.12=\u503c[{0}]\u65e0\u6cd5\u9002\u914d\u60a8\u63d0\u4f9b[{1}]\uff0c \u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f! +configuration.13=Path\u4e0d\u80fd\u4e3anull\uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.14=\u8def\u5f84[{0}]\u51fa\u73b0\u975e\u6cd5\u503c\u7c7b\u578b[{1}]\uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f! . +configuration.15=\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.16=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u9700\u8981\u914d\u7f6eJson\u683c\u5f0f\u7684Map\u5bf9\u8c61\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{1}]. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.17=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u503c\u4e3anull\uff0cdatax\u65e0\u6cd5\u8bc6\u522b\u8be5\u914d\u7f6e. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.18=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u9700\u8981\u914d\u7f6eJson\u683c\u5f0f\u7684Map\u5bf9\u8c61\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{1}]. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.19=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef\uff0c\u5217\u8868\u4e0b\u6807\u5fc5\u987b\u4e3a\u6570\u5b57\u7c7b\u578b\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{0}] \uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.20=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f!. +configuration.21=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8def\u5f84[{0}]\u4e0d\u5408\u6cd5, \u8def\u5f84\u5c42\u6b21\u4e4b\u95f4\u4e0d\u80fd\u51fa\u73b0\u7a7a\u767d\u5b57\u7b26 . +configuration.22=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u56e0\u4e3a\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u662f\u5408\u6cd5\u7684JSON\u683c\u5f0f, JSON\u4e0d\u80fd\u4e3a\u7a7a\u767d. \u8bf7\u6309\u7167\u6807\u51c6json\u683c\u5f0f\u63d0\u4f9b\u914d\u7f6e\u4fe1\u606f. +configuration.23=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u662f\u5408\u6cd5\u7684JSON\u683c\u5f0f: {0} . \u8bf7\u6309\u7167\u6807\u51c6json\u683c\u5f0f\u63d0\u4f9b\u914d\u7f6e\u4fe1\u606f. + + +listutil.1=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef\uff0cList\u4e0d\u80fd\u4e3a\u7a7a. +listutil.2=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.3=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u4fe1\u606f\u6709\u8bef, String:[{0}] \u4e0d\u5141\u8bb8\u91cd\u590d\u51fa\u73b0\u5728\u5217\u8868\u4e2d: [{1}]. +listutil.4=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.5=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.6=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u4fe1\u606f\u6709\u8bef, String:[{0}] \u4e0d\u5b58\u5728\u4e8e\u5217\u8868\u4e2d:[{1}]. +listutil.7=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.8=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. + + +rangesplitutil.1=\u5207\u5206\u4efd\u6570\u4e0d\u80fd\u5c0f\u4e8e1. \u6b64\u5904:expectSliceNumber=[{0}]. +rangesplitutil.2=\u5bf9 BigInteger \u8fdb\u884c\u5207\u5206\u65f6\uff0c\u5176\u5de6\u53f3\u533a\u95f4\u4e0d\u80fd\u4e3a null. \u6b64\u5904:left=[{0}],right=[{1}]. +rangesplitutil.3=\u53c2\u6570 bigInteger \u4e0d\u80fd\u4e3a\u7a7a. +rangesplitutil.4=\u6839\u636e\u5b57\u7b26\u4e32\u8fdb\u884c\u5207\u5206\u65f6\u4ec5\u652f\u6301 ASCII \u5b57\u7b26\u4e32\uff0c\u800c\u5b57\u7b26\u4e32:[{0}]\u975e ASCII \u5b57\u7b26\u4e32. +rangesplitutil.5=\u53c2\u6570 bigInteger \u4e0d\u80fd\u4e3a\u7a7a. +rangesplitutil.6=\u6839\u636e\u5b57\u7b26\u4e32\u8fdb\u884c\u5207\u5206\u65f6\u4ec5\u652f\u6301 ASCII \u5b57\u7b26\u4e32\uff0c\u800c\u5b57\u7b26\u4e32:[{0}]\u975e ASCII \u5b57\u7b26\u4e32. + + +retryutil.1=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u5165\u53c2callable\u4e0d\u80fd\u4e3a\u7a7a ! +retryutil.2=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u5165\u53c2retrytime[%d]\u4e0d\u80fd\u5c0f\u4e8e1 ! +retryutil.3=Exception when calling callable, \u5f02\u5e38Msg:{0} +retryutil.4=Exception when calling callable, \u5373\u5c06\u5c1d\u8bd5\u6267\u884c\u7b2c{0}\u6b21\u91cd\u8bd5,\u5171\u8ba1\u91cd\u8bd5{1}\u6b21.\u672c\u6b21\u91cd\u8bd5\u8ba1\u5212\u7b49\u5f85[{2}]ms,\u5b9e\u9645\u7b49\u5f85[{3}]ms, \u5f02\u5e38Msg:[{4}] + + +httpclientutil.1=\u8BF7\u6C42\u5730\u5740\uFF1A{0}, \u8BF7\u6C42\u65B9\u6CD5\uFF1A{1},STATUS CODE = {2}, Response Entity: {3} +httpclientutil.2=\u8FDC\u7A0B\u63A5\u53E3\u8FD4\u56DE-1,\u5C06\u91CD\u8BD5 \ No newline at end of file diff --git a/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_zh_HK.properties b/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..ab26ac52af --- /dev/null +++ b/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_zh_HK.properties @@ -0,0 +1,104 @@ +very_like_yixiao=\u4e00{0}\u4e8c{1}\u4e09 + + +configuration.1=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef\uff0c\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6[{0}]\u4e0d\u5b58\u5728. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6. +configuration.2=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6[{0}]\u8bfb\u53d6\u5931\u8d25\uff0c\u9519\u8bef\u539f\u56e0: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6\u7684\u6743\u9650\u8bbe\u7f6e. +configuration.3=\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6. \u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u8bfb\u53d6\u5931\u8d25\uff0c\u9519\u8bef\u539f\u56e0: {0}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6\u7684\u6743\u9650\u8bbe\u7f6e. +configuration.4=\u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6\u6709\u8bef\uff0c[{0}]\u662f\u5fc5\u586b\u53c2\u6570\uff0c\u4e0d\u5141\u8bb8\u4e3a\u7a7a\u6216\u8005\u7559\u767d . +configuration.5=\u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6\u6709\u8bef\uff0c[{0}]\u662f\u5fc5\u586b\u53c2\u6570\uff0c\u4e0d\u5141\u8bb8\u4e3a\u7a7a\u6216\u8005\u7559\u767d . +configuration.6=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u56e0\u4e3a\u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5\uff0c\u671f\u671b\u662f\u5b57\u7b26\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.7=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u6709\u8bef\uff0c\u56e0\u4e3a\u4ece[{0}]\u83b7\u53d6\u7684\u503c[{1}]\u65e0\u6cd5\u8f6c\u6362\u4e3abool\u7c7b\u578b. \u8bf7\u68c0\u67e5\u6e90\u8868\u7684\u914d\u7f6e\u5e76\u4e14\u505a\u51fa\u76f8\u5e94\u7684\u4fee\u6539. +configuration.8=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6574\u6570\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.9=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6574\u6570\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.10=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6d6e\u70b9\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.11=\u914d\u7f6e\u6587\u4ef6\u5bf9\u5e94Key[{0}]\u5e76\u4e0d\u5b58\u5728\uff0c\u8be5\u60c5\u51b5\u662f\u4ee3\u7801\u7f16\u7a0b\u9519\u8bef. \u8bf7\u8054\u7cfbDataX\u56e2\u961f\u7684\u540c\u5b66. +configuration.12=\u503c[{0}]\u65e0\u6cd5\u9002\u914d\u60a8\u63d0\u4f9b[{1}]\uff0c \u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f! +configuration.13=Path\u4e0d\u80fd\u4e3anull\uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.14=\u8def\u5f84[{0}]\u51fa\u73b0\u975e\u6cd5\u503c\u7c7b\u578b[{1}]\uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f! . +configuration.15=\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.16=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u9700\u8981\u914d\u7f6eJson\u683c\u5f0f\u7684Map\u5bf9\u8c61\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{1}]. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.17=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u503c\u4e3anull\uff0cdatax\u65e0\u6cd5\u8bc6\u522b\u8be5\u914d\u7f6e. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.18=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u9700\u8981\u914d\u7f6eJson\u683c\u5f0f\u7684Map\u5bf9\u8c61\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{1}]. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.19=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef\uff0c\u5217\u8868\u4e0b\u6807\u5fc5\u987b\u4e3a\u6570\u5b57\u7c7b\u578b\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{0}] \uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.20=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f!. +configuration.21=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8def\u5f84[{0}]\u4e0d\u5408\u6cd5, \u8def\u5f84\u5c42\u6b21\u4e4b\u95f4\u4e0d\u80fd\u51fa\u73b0\u7a7a\u767d\u5b57\u7b26 . +configuration.22=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u56e0\u4e3a\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u662f\u5408\u6cd5\u7684JSON\u683c\u5f0f, JSON\u4e0d\u80fd\u4e3a\u7a7a\u767d. \u8bf7\u6309\u7167\u6807\u51c6json\u683c\u5f0f\u63d0\u4f9b\u914d\u7f6e\u4fe1\u606f. +configuration.23=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u662f\u5408\u6cd5\u7684JSON\u683c\u5f0f: {0} . \u8bf7\u6309\u7167\u6807\u51c6json\u683c\u5f0f\u63d0\u4f9b\u914d\u7f6e\u4fe1\u606f. + + +listutil.1=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef\uff0cList\u4e0d\u80fd\u4e3a\u7a7a. +listutil.2=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.3=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u4fe1\u606f\u6709\u8bef, String:[{0}] \u4e0d\u5141\u8bb8\u91cd\u590d\u51fa\u73b0\u5728\u5217\u8868\u4e2d: [{1}]. +listutil.4=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.5=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.6=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u4fe1\u606f\u6709\u8bef, String:[{0}] \u4e0d\u5b58\u5728\u4e8e\u5217\u8868\u4e2d:[{1}]. +listutil.7=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.8=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. + + +rangesplitutil.1=\u5207\u5206\u4efd\u6570\u4e0d\u80fd\u5c0f\u4e8e1. \u6b64\u5904:expectSliceNumber=[{0}]. +rangesplitutil.2=\u5bf9 BigInteger \u8fdb\u884c\u5207\u5206\u65f6\uff0c\u5176\u5de6\u53f3\u533a\u95f4\u4e0d\u80fd\u4e3a null. \u6b64\u5904:left=[{0}],right=[{1}]. +rangesplitutil.3=\u53c2\u6570 bigInteger \u4e0d\u80fd\u4e3a\u7a7a. +rangesplitutil.4=\u6839\u636e\u5b57\u7b26\u4e32\u8fdb\u884c\u5207\u5206\u65f6\u4ec5\u652f\u6301 ASCII \u5b57\u7b26\u4e32\uff0c\u800c\u5b57\u7b26\u4e32:[{0}]\u975e ASCII \u5b57\u7b26\u4e32. +rangesplitutil.5=\u53c2\u6570 bigInteger \u4e0d\u80fd\u4e3a\u7a7a. +rangesplitutil.6=\u6839\u636e\u5b57\u7b26\u4e32\u8fdb\u884c\u5207\u5206\u65f6\u4ec5\u652f\u6301 ASCII \u5b57\u7b26\u4e32\uff0c\u800c\u5b57\u7b26\u4e32:[{0}]\u975e ASCII \u5b57\u7b26\u4e32. + + +retryutil.1=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u5165\u53c2callable\u4e0d\u80fd\u4e3a\u7a7a ! +retryutil.2=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u5165\u53c2retrytime[%d]\u4e0d\u80fd\u5c0f\u4e8e1 ! +retryutil.3=Exception when calling callable, \u5f02\u5e38Msg:{0} +retryutil.4=Exception when calling callable, \u5373\u5c06\u5c1d\u8bd5\u6267\u884c\u7b2c{0}\u6b21\u91cd\u8bd5,\u5171\u8ba1\u91cd\u8bd5{1}\u6b21.\u672c\u6b21\u91cd\u8bd5\u8ba1\u5212\u7b49\u5f85[{2}]ms,\u5b9e\u9645\u7b49\u5f85[{3}]ms, \u5f02\u5e38Msg:[{4}] + +very_like_yixiao=一{0}二{1}三 + + +configuration.1=配置資訊錯誤,您提供的配置檔案[{0}]不存在. 請檢查您的配置檔案. +configuration.2=配置資訊錯誤. 您提供配置檔案[{0}]讀取失敗,錯誤原因: {1}. 請檢查您的配置檔案的權限設定. +configuration.3=請檢查您的配置檔案. 您提供的配置檔案讀取失敗,錯誤原因: {0}. 請檢查您的配置檔案的權限設定. +configuration.4=您提供配置檔案有誤,[{0}]是必填參數,不允許為空或者留白 . +configuration.5=您提供配置檔案有誤,[{0}]是必填參數,不允許為空或者留白 . +configuration.6=任務讀取配置檔案出錯. 因為配置檔案路徑[{0}] 值不合法,期望是字符類型: {1}. 請檢查您的配置並作出修改. +configuration.7=您提供的配置資訊有誤,因為從[{0}]獲取的值[{1}]無法轉換為bool類型. 請檢查源表的配置並且做出相應的修改. +configuration.8=任務讀取配置檔案出錯. 配置檔案路徑[{0}] 值不合法, 期望是整數類型: {1}. 請檢查您的配置並作出修改. +configuration.9=任務讀取配置檔案出錯. 配置檔案路徑[{0}] 值不合法, 期望是整數類型: {1}. 請檢查您的配置並作出修改. +configuration.10=任務讀取配置檔案出錯. 配置檔案路徑[{0}] 值不合法, 期望是浮點類型: {1}. 請檢查您的配置並作出修改. +configuration.11=配置檔案對應Key[{0}]並不存在,該情況是代碼編程錯誤. 請聯絡DataX團隊的同學. +configuration.12=值[{0}]無法適配您提供[{1}], 該異常代表系統編程錯誤, 請聯絡DataX開發團隊! +configuration.13=Path不能為null,該異常代表系統編程錯誤, 請聯絡DataX開發團隊 ! +configuration.14=路徑[{0}]出現不合法值類型[{1}],該異常代表系統編程錯誤, 請聯絡DataX開發團隊! . +configuration.15=該異常代表系統編程錯誤, 請聯絡DataX開發團隊 ! +configuration.16=您提供的配置檔案有誤. 路徑[{0}]需要配置Json格式的Map對象,但該節點發現實際類型是[{1}]. 請檢查您的配置並作出修改. +configuration.17=您提供的配置檔案有誤. 路徑[{0}]值為null,datax無法識別該配置. 請檢查您的配置並作出修改. +configuration.18=您提供的配置檔案有誤. 路徑[{0}]需要配置Json格式的Map對象,但該節點發現實際類型是[{1}]. 請檢查您的配置並作出修改. +configuration.19=系統編程錯誤,清單下標必須為數字類型,但該節點發現實際類型是[{0}] ,該異常代表系統編程錯誤, 請聯絡DataX開發團隊 ! +configuration.20=系統編程錯誤, 該異常代表系統編程錯誤, 請聯絡DataX開發團隊!. +configuration.21=系統編程錯誤, 路徑[{0}]不合法, 路徑層次之間不能出現空白字符 . +configuration.22=配置資訊錯誤. 因為您提供的配置資訊不是合法的JSON格式, JSON不能為空白. 請按照標準json格式提供配置資訊. +configuration.23=配置資訊錯誤. 您提供的配置資訊不是合法的JSON格式: {0}. 請按照標準json格式提供配置資訊. + + +listutil.1=您提供的作業配置有誤,List不能為空. +listutil.2=您提供的作業配置有誤, List不能為空. +listutil.3=您提供的作業配置資訊有誤, String:[{0}]不允許重複出現在清單中: [{1}]. +listutil.4=您提供的作業配置有誤, List不能為空. +listutil.5=您提供的作業配置有誤, List不能為空. +listutil.6=您提供的作業配置資訊有誤, String:[{0}]不存在於清單中:[{1}]. +listutil.7=您提供的作業配置有誤, List不能為空. +listutil.8=您提供的作業配置有誤, List不能為空. + + +rangesplitutil.1=切分份數不能小於1. 此處:expectSliceNumber=[{0}]. +rangesplitutil.2=對 BigInteger 進行切分時,其左右區間不能為 null. 此處:left=[{0}],right=[{1}]. +rangesplitutil.3=參數 bigInteger 不能為空. +rangesplitutil.4=根據字符串進行切分時僅支援 ASCII 字符串,而字符串:[{0}]非 ASCII 字符串. +rangesplitutil.5=參數 bigInteger 不能為空. +rangesplitutil.6=根據字符串進行切分時僅支援 ASCII 字符串,而字符串:[{0}]非 ASCII 字符串. + + +retryutil.1=系統編程錯誤, 入參callable不能為空 ! +retryutil.2=系統編程錯誤, 入參retrytime[%d]不能小於1 ! +retryutil.3=Exception when calling callable, 異常Msg:{0} +retryutil.4=Exception when calling callable, 即將嘗試執行第{0}次重試,共計重試{1}次.本次重試計劃等待[{2}]ms,實際等待[{3}]ms, 異常Msg:[{4}] + +httpclientutil.1=\u8ACB\u6C42\u5730\u5740\uFF1A{0}, \u8ACB\u6C42\u65B9\u6CD5\uFF1A{1},STATUS CODE = {2}, Response Entity: {3} +httpclientutil.2=\u9060\u7A0B\u63A5\u53E3\u8FD4\u56DE-1,\u5C07\u91CD\u8A66 \ No newline at end of file diff --git a/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_zh_TW.properties b/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..89eb1eae92 --- /dev/null +++ b/common/src/main/java/com/alibaba/datax/common/util/LocalStrings_zh_TW.properties @@ -0,0 +1,104 @@ +very_like_yixiao=\u4e00{0}\u4e8c{1}\u4e09 + + +configuration.1=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef\uff0c\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6[{0}]\u4e0d\u5b58\u5728. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6. +configuration.2=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6[{0}]\u8bfb\u53d6\u5931\u8d25\uff0c\u9519\u8bef\u539f\u56e0: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6\u7684\u6743\u9650\u8bbe\u7f6e. +configuration.3=\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6. \u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u8bfb\u53d6\u5931\u8d25\uff0c\u9519\u8bef\u539f\u56e0: {0}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u6587\u4ef6\u7684\u6743\u9650\u8bbe\u7f6e. +configuration.4=\u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6\u6709\u8bef\uff0c[{0}]\u662f\u5fc5\u586b\u53c2\u6570\uff0c\u4e0d\u5141\u8bb8\u4e3a\u7a7a\u6216\u8005\u7559\u767d . +configuration.5=\u60a8\u63d0\u4f9b\u914d\u7f6e\u6587\u4ef6\u6709\u8bef\uff0c[{0}]\u662f\u5fc5\u586b\u53c2\u6570\uff0c\u4e0d\u5141\u8bb8\u4e3a\u7a7a\u6216\u8005\u7559\u767d . +configuration.6=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u56e0\u4e3a\u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5\uff0c\u671f\u671b\u662f\u5b57\u7b26\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.7=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u6709\u8bef\uff0c\u56e0\u4e3a\u4ece[{0}]\u83b7\u53d6\u7684\u503c[{1}]\u65e0\u6cd5\u8f6c\u6362\u4e3abool\u7c7b\u578b. \u8bf7\u68c0\u67e5\u6e90\u8868\u7684\u914d\u7f6e\u5e76\u4e14\u505a\u51fa\u76f8\u5e94\u7684\u4fee\u6539. +configuration.8=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6574\u6570\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.9=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6574\u6570\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.10=\u4efb\u52a1\u8bfb\u53d6\u914d\u7f6e\u6587\u4ef6\u51fa\u9519. \u914d\u7f6e\u6587\u4ef6\u8def\u5f84[{0}] \u503c\u975e\u6cd5, \u671f\u671b\u662f\u6d6e\u70b9\u7c7b\u578b: {1}. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.11=\u914d\u7f6e\u6587\u4ef6\u5bf9\u5e94Key[{0}]\u5e76\u4e0d\u5b58\u5728\uff0c\u8be5\u60c5\u51b5\u662f\u4ee3\u7801\u7f16\u7a0b\u9519\u8bef. \u8bf7\u8054\u7cfbDataX\u56e2\u961f\u7684\u540c\u5b66. +configuration.12=\u503c[{0}]\u65e0\u6cd5\u9002\u914d\u60a8\u63d0\u4f9b[{1}]\uff0c \u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f! +configuration.13=Path\u4e0d\u80fd\u4e3anull\uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.14=\u8def\u5f84[{0}]\u51fa\u73b0\u975e\u6cd5\u503c\u7c7b\u578b[{1}]\uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f! . +configuration.15=\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.16=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u9700\u8981\u914d\u7f6eJson\u683c\u5f0f\u7684Map\u5bf9\u8c61\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{1}]. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.17=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u503c\u4e3anull\uff0cdatax\u65e0\u6cd5\u8bc6\u522b\u8be5\u914d\u7f6e. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.18=\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u6587\u4ef6\u6709\u8bef. \u8def\u5f84[{0}]\u9700\u8981\u914d\u7f6eJson\u683c\u5f0f\u7684Map\u5bf9\u8c61\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{1}]. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +configuration.19=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef\uff0c\u5217\u8868\u4e0b\u6807\u5fc5\u987b\u4e3a\u6570\u5b57\u7c7b\u578b\uff0c\u4f46\u8be5\u8282\u70b9\u53d1\u73b0\u5b9e\u9645\u7c7b\u578b\u662f[{0}] \uff0c\u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f ! +configuration.20=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8be5\u5f02\u5e38\u4ee3\u8868\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8bf7\u8054\u7cfbDataX\u5f00\u53d1\u56e2\u961f!. +configuration.21=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u8def\u5f84[{0}]\u4e0d\u5408\u6cd5, \u8def\u5f84\u5c42\u6b21\u4e4b\u95f4\u4e0d\u80fd\u51fa\u73b0\u7a7a\u767d\u5b57\u7b26 . +configuration.22=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u56e0\u4e3a\u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u662f\u5408\u6cd5\u7684JSON\u683c\u5f0f, JSON\u4e0d\u80fd\u4e3a\u7a7a\u767d. \u8bf7\u6309\u7167\u6807\u51c6json\u683c\u5f0f\u63d0\u4f9b\u914d\u7f6e\u4fe1\u606f. +configuration.23=\u914d\u7f6e\u4fe1\u606f\u9519\u8bef. \u60a8\u63d0\u4f9b\u7684\u914d\u7f6e\u4fe1\u606f\u4e0d\u662f\u5408\u6cd5\u7684JSON\u683c\u5f0f: {0} . \u8bf7\u6309\u7167\u6807\u51c6json\u683c\u5f0f\u63d0\u4f9b\u914d\u7f6e\u4fe1\u606f. + + +listutil.1=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef\uff0cList\u4e0d\u80fd\u4e3a\u7a7a. +listutil.2=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.3=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u4fe1\u606f\u6709\u8bef, String:[{0}] \u4e0d\u5141\u8bb8\u91cd\u590d\u51fa\u73b0\u5728\u5217\u8868\u4e2d: [{1}]. +listutil.4=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.5=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.6=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u4fe1\u606f\u6709\u8bef, String:[{0}] \u4e0d\u5b58\u5728\u4e8e\u5217\u8868\u4e2d:[{1}]. +listutil.7=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. +listutil.8=\u60a8\u63d0\u4f9b\u7684\u4f5c\u4e1a\u914d\u7f6e\u6709\u8bef, List\u4e0d\u80fd\u4e3a\u7a7a. + + +rangesplitutil.1=\u5207\u5206\u4efd\u6570\u4e0d\u80fd\u5c0f\u4e8e1. \u6b64\u5904:expectSliceNumber=[{0}]. +rangesplitutil.2=\u5bf9 BigInteger \u8fdb\u884c\u5207\u5206\u65f6\uff0c\u5176\u5de6\u53f3\u533a\u95f4\u4e0d\u80fd\u4e3a null. \u6b64\u5904:left=[{0}],right=[{1}]. +rangesplitutil.3=\u53c2\u6570 bigInteger \u4e0d\u80fd\u4e3a\u7a7a. +rangesplitutil.4=\u6839\u636e\u5b57\u7b26\u4e32\u8fdb\u884c\u5207\u5206\u65f6\u4ec5\u652f\u6301 ASCII \u5b57\u7b26\u4e32\uff0c\u800c\u5b57\u7b26\u4e32:[{0}]\u975e ASCII \u5b57\u7b26\u4e32. +rangesplitutil.5=\u53c2\u6570 bigInteger \u4e0d\u80fd\u4e3a\u7a7a. +rangesplitutil.6=\u6839\u636e\u5b57\u7b26\u4e32\u8fdb\u884c\u5207\u5206\u65f6\u4ec5\u652f\u6301 ASCII \u5b57\u7b26\u4e32\uff0c\u800c\u5b57\u7b26\u4e32:[{0}]\u975e ASCII \u5b57\u7b26\u4e32. + + +retryutil.1=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u5165\u53c2callable\u4e0d\u80fd\u4e3a\u7a7a ! +retryutil.2=\u7cfb\u7edf\u7f16\u7a0b\u9519\u8bef, \u5165\u53c2retrytime[%d]\u4e0d\u80fd\u5c0f\u4e8e1 ! +retryutil.3=Exception when calling callable, \u5f02\u5e38Msg:{0} +retryutil.4=Exception when calling callable, \u5373\u5c06\u5c1d\u8bd5\u6267\u884c\u7b2c{0}\u6b21\u91cd\u8bd5,\u5171\u8ba1\u91cd\u8bd5{1}\u6b21.\u672c\u6b21\u91cd\u8bd5\u8ba1\u5212\u7b49\u5f85[{2}]ms,\u5b9e\u9645\u7b49\u5f85[{3}]ms, \u5f02\u5e38Msg:[{4}] + +very_like_yixiao=一{0}二{1}三 + + +configuration.1=配置資訊錯誤,您提供的配置檔案[{0}]不存在. 請檢查您的配置檔案. +configuration.2=配置資訊錯誤. 您提供配置檔案[{0}]讀取失敗,錯誤原因: {1}. 請檢查您的配置檔案的權限設定. +configuration.3=請檢查您的配置檔案. 您提供的配置檔案讀取失敗,錯誤原因: {0}. 請檢查您的配置檔案的權限設定. +configuration.4=您提供配置檔案有誤,[{0}]是必填參數,不允許為空或者留白 . +configuration.5=您提供配置檔案有誤,[{0}]是必填參數,不允許為空或者留白 . +configuration.6=任務讀取配置檔案出錯. 因為配置檔案路徑[{0}] 值不合法,期望是字符類型: {1}. 請檢查您的配置並作出修改. +configuration.7=您提供的配置資訊有誤,因為從[{0}]獲取的值[{1}]無法轉換為bool類型. 請檢查源表的配置並且做出相應的修改. +configuration.8=任務讀取配置檔案出錯. 配置檔案路徑[{0}] 值不合法, 期望是整數類型: {1}. 請檢查您的配置並作出修改. +configuration.9=任務讀取配置檔案出錯. 配置檔案路徑[{0}] 值不合法, 期望是整數類型: {1}. 請檢查您的配置並作出修改. +configuration.10=任務讀取配置檔案出錯. 配置檔案路徑[{0}] 值不合法, 期望是浮點類型: {1}. 請檢查您的配置並作出修改. +configuration.11=配置檔案對應Key[{0}]並不存在,該情況是代碼編程錯誤. 請聯絡DataX團隊的同學. +configuration.12=值[{0}]無法適配您提供[{1}], 該異常代表系統編程錯誤, 請聯絡DataX開發團隊! +configuration.13=Path不能為null,該異常代表系統編程錯誤, 請聯絡DataX開發團隊 ! +configuration.14=路徑[{0}]出現不合法值類型[{1}],該異常代表系統編程錯誤, 請聯絡DataX開發團隊! . +configuration.15=該異常代表系統編程錯誤, 請聯絡DataX開發團隊 ! +configuration.16=您提供的配置檔案有誤. 路徑[{0}]需要配置Json格式的Map對象,但該節點發現實際類型是[{1}]. 請檢查您的配置並作出修改. +configuration.17=您提供的配置檔案有誤. 路徑[{0}]值為null,datax無法識別該配置. 請檢查您的配置並作出修改. +configuration.18=您提供的配置檔案有誤. 路徑[{0}]需要配置Json格式的Map對象,但該節點發現實際類型是[{1}]. 請檢查您的配置並作出修改. +configuration.19=系統編程錯誤,清單下標必須為數字類型,但該節點發現實際類型是[{0}] ,該異常代表系統編程錯誤, 請聯絡DataX開發團隊 ! +configuration.20=系統編程錯誤, 該異常代表系統編程錯誤, 請聯絡DataX開發團隊!. +configuration.21=系統編程錯誤, 路徑[{0}]不合法, 路徑層次之間不能出現空白字符 . +configuration.22=配置資訊錯誤. 因為您提供的配置資訊不是合法的JSON格式, JSON不能為空白. 請按照標準json格式提供配置資訊. +configuration.23=配置資訊錯誤. 您提供的配置資訊不是合法的JSON格式: {0}. 請按照標準json格式提供配置資訊. + + +listutil.1=您提供的作業配置有誤,List不能為空. +listutil.2=您提供的作業配置有誤, List不能為空. +listutil.3=您提供的作業配置資訊有誤, String:[{0}]不允許重複出現在清單中: [{1}]. +listutil.4=您提供的作業配置有誤, List不能為空. +listutil.5=您提供的作業配置有誤, List不能為空. +listutil.6=您提供的作業配置資訊有誤, String:[{0}]不存在於清單中:[{1}]. +listutil.7=您提供的作業配置有誤, List不能為空. +listutil.8=您提供的作業配置有誤, List不能為空. + + +rangesplitutil.1=切分份數不能小於1. 此處:expectSliceNumber=[{0}]. +rangesplitutil.2=對 BigInteger 進行切分時,其左右區間不能為 null. 此處:left=[{0}],right=[{1}]. +rangesplitutil.3=參數 bigInteger 不能為空. +rangesplitutil.4=根據字符串進行切分時僅支援 ASCII 字符串,而字符串:[{0}]非 ASCII 字符串. +rangesplitutil.5=參數 bigInteger 不能為空. +rangesplitutil.6=根據字符串進行切分時僅支援 ASCII 字符串,而字符串:[{0}]非 ASCII 字符串. + + +retryutil.1=系統編程錯誤, 入參callable不能為空 ! +retryutil.2=系統編程錯誤, 入參retrytime[%d]不能小於1 ! +retryutil.3=Exception when calling callable, 異常Msg:{0} +retryutil.4=Exception when calling callable, 即將嘗試執行第{0}次重試,共計重試{1}次.本次重試計劃等待[{2}]ms,實際等待[{3}]ms, 異常Msg:[{4}] + +httpclientutil.1=\u8BF7\u6C42\u5730\u5740\uFF1A{0}, \u8BF7\u6C42\u65B9\u6CD5\uFF1A{1},STATUS CODE = {2}, Response Entity: {3} +httpclientutil.2=\u8FDC\u7A0B\u63A5\u53E3\u8FD4\u56DE-1,\u5C06\u91CD\u8BD5 \ No newline at end of file diff --git a/common/src/main/java/com/alibaba/datax/common/util/LoggerFunction.java b/common/src/main/java/com/alibaba/datax/common/util/LoggerFunction.java new file mode 100644 index 0000000000..ef24504f9f --- /dev/null +++ b/common/src/main/java/com/alibaba/datax/common/util/LoggerFunction.java @@ -0,0 +1,10 @@ +package com.alibaba.datax.common.util; + +/** + * @author molin.lxd + * @date 2021-05-09 + */ +public interface LoggerFunction { + + void apply(); +} diff --git a/common/src/main/java/com/alibaba/datax/common/util/MessageSource.java b/common/src/main/java/com/alibaba/datax/common/util/MessageSource.java new file mode 100644 index 0000000000..d2411328da --- /dev/null +++ b/common/src/main/java/com/alibaba/datax/common/util/MessageSource.java @@ -0,0 +1,207 @@ +package com.alibaba.datax.common.util; + +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.ResourceBundle; +import java.util.TimeZone; + +import org.apache.commons.lang3.LocaleUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class MessageSource { + private static final Logger LOG = LoggerFactory.getLogger(MessageSource.class); + private static Map resourceBundleCache = new HashMap(); + public static Locale locale = null; + public static TimeZone timeZone = null; + private ResourceBundle resourceBundle = null; + + private MessageSource(ResourceBundle resourceBundle) { + this.resourceBundle = resourceBundle; + } + + /** + * @param baseName + * demo: javax.servlet.http.LocalStrings + * + * @throws MissingResourceException + * - if no resource bundle for the specified base name can be + * found + * */ + public static MessageSource loadResourceBundle(String baseName) { + return loadResourceBundle(baseName, MessageSource.locale, + MessageSource.timeZone); + } + + /** + * @param clazz + * 根据其获取package name + * */ + public static MessageSource loadResourceBundle(Class clazz) { + return loadResourceBundle(clazz.getPackage().getName()); + } + + /** + * @param clazz + * 根据其获取package name + * */ + public static MessageSource loadResourceBundle(Class clazz, + Locale locale, TimeZone timeZone) { + return loadResourceBundle(clazz.getPackage().getName(), locale, + timeZone); + } + + /** + * warn: + * ok: ResourceBundle.getBundle("xxx.LocalStrings", Locale.getDefault(), LoadUtil.getJarLoader(PluginType.WRITER, "odpswriter")) + * error: ResourceBundle.getBundle("xxx.LocalStrings", Locale.getDefault(), LoadUtil.getJarLoader(PluginType.WRITER, "odpswriter")) + * @param baseName + * demo: javax.servlet.http.LocalStrings + * + * @throws MissingResourceException + * - if no resource bundle for the specified base name can be + * found + * + * */ + public static MessageSource loadResourceBundle(String baseName, + Locale locale, TimeZone timeZone) { + ResourceBundle resourceBundle = null; + if (null == locale) { + locale = LocaleUtils.toLocale("en_US"); + } + if (null == timeZone) { + timeZone = TimeZone.getDefault(); + } + String resourceBaseName = String.format("%s.LocalStrings", baseName); + LOG.debug( + "initEnvironment MessageSource.locale[{}], MessageSource.timeZone[{}]", + MessageSource.locale, MessageSource.timeZone); + LOG.debug( + "loadResourceBundle with locale[{}], timeZone[{}], baseName[{}]", + locale, timeZone, resourceBaseName); + // warn: 这个map的维护需要考虑Local吗, no? + if (!MessageSource.resourceBundleCache.containsKey(resourceBaseName)) { + ClassLoader clazzLoader = Thread.currentThread() + .getContextClassLoader(); + LOG.debug("loadResourceBundle classLoader:{}", clazzLoader); + resourceBundle = ResourceBundle.getBundle(resourceBaseName, locale, + clazzLoader); + MessageSource.resourceBundleCache.put(resourceBaseName, + resourceBundle); + } else { + resourceBundle = MessageSource.resourceBundleCache + .get(resourceBaseName); + } + + return new MessageSource(resourceBundle); + } + + public static boolean unloadResourceBundle(Class clazz) { + String baseName = clazz.getPackage().getName(); + String resourceBaseName = String.format("%s.LocalStrings", baseName); + if (!MessageSource.resourceBundleCache.containsKey(resourceBaseName)) { + return false; + } else { + MessageSource.resourceBundleCache.remove(resourceBaseName); + return true; + } + } + + public static MessageSource reloadResourceBundle(Class clazz) { + MessageSource.unloadResourceBundle(clazz); + return MessageSource.loadResourceBundle(clazz); + } + + public static void setEnvironment(Locale locale, TimeZone timeZone) { + // warn: 设置默认? @2018.03.21 将此处注释移除,否则在国际化多时区下会遇到问题 + Locale.setDefault(locale); + TimeZone.setDefault(timeZone); + MessageSource.locale = locale; + MessageSource.timeZone = timeZone; + LOG.info("use Locale: {} timeZone: {}", locale, timeZone); + } + + public static void init(final Configuration configuration) { + Locale locale2Set = Locale.getDefault(); + String localeStr = configuration.getString("common.column.locale", "zh_CN");// 默认操作系统的 + if (StringUtils.isNotBlank(localeStr)) { + try { + locale2Set = LocaleUtils.toLocale(localeStr); + } catch (Exception e) { + LOG.warn("ignored locale parse exception: {}", e.getMessage()); + } + } + + TimeZone timeZone2Set = TimeZone.getDefault(); + String timeZoneStr = configuration.getString("common.column.timeZone");// 默认操作系统的 + if (StringUtils.isNotBlank(timeZoneStr)) { + try { + timeZone2Set = TimeZone.getTimeZone(timeZoneStr); + } catch (Exception e) { + LOG.warn("ignored timezone parse exception: {}", e.getMessage()); + } + } + + LOG.info("JVM TimeZone: {}, Locale: {}", timeZone2Set.getID(), locale2Set); + MessageSource.setEnvironment(locale2Set, timeZone2Set); + } + + public static void clearCache() { + MessageSource.resourceBundleCache.clear(); + } + + public String message(String code) { + return this.messageWithDefaultMessage(code, null); + } + + public String message(String code, String args1) { + return this.messageWithDefaultMessage(code, null, + new Object[] { args1 }); + } + + public String message(String code, String args1, String args2) { + return this.messageWithDefaultMessage(code, null, new Object[] { args1, + args2 }); + } + + public String message(String code, String args1, String args2, String args3) { + return this.messageWithDefaultMessage(code, null, new Object[] { args1, + args2, args3 }); + } + + // 上面几个重载可以应对大多数情况, 避免使用这个可以提高性能的 + public String message(String code, Object... args) { + return this.messageWithDefaultMessage(code, null, args); + } + + public String messageWithDefaultMessage(String code, String defaultMessage) { + return this.messageWithDefaultMessage(code, defaultMessage, + new Object[] {}); + } + + /** + * @param args + * MessageFormat会依次调用对应对象的toString方法 + * */ + public String messageWithDefaultMessage(String code, String defaultMessage, + Object... args) { + String messageStr = null; + try { + messageStr = this.resourceBundle.getString(code); + } catch (MissingResourceException e) { + messageStr = defaultMessage; + } + if (null != messageStr && null != args && args.length > 0) { + // warn: see loadResourceBundle set default locale + return MessageFormat.format(messageStr, args); + } else { + return messageStr; + } + + } +} diff --git a/common/src/main/java/com/alibaba/datax/common/util/RangeSplitUtil.java b/common/src/main/java/com/alibaba/datax/common/util/RangeSplitUtil.java index 791f9ea12c..ec353730ab 100755 --- a/common/src/main/java/com/alibaba/datax/common/util/RangeSplitUtil.java +++ b/common/src/main/java/com/alibaba/datax/common/util/RangeSplitUtil.java @@ -206,4 +206,27 @@ private static boolean isPureAscii(String aString) { return true; } + + /** + * List拆分工具函数,主要用于reader插件的split拆分逻辑 + * */ + public static List> doListSplit(List objects, int adviceNumber) { + List> splitLists = new ArrayList>(); + if (null == objects) { + return splitLists; + } + long[] splitPoint = RangeSplitUtil.doLongSplit(0, objects.size(), adviceNumber); + for (int startIndex = 0; startIndex < splitPoint.length - 1; startIndex++) { + List objectsForTask = new ArrayList(); + int endIndex = startIndex + 1; + for (long i = splitPoint[startIndex]; i < splitPoint[endIndex]; i++) { + objectsForTask.add(objects.get((int) i)); + } + if (!objectsForTask.isEmpty()) { + splitLists.add(objectsForTask); + } + } + return splitLists; + } + } diff --git a/common/src/main/java/com/alibaba/datax/common/util/StrUtil.java b/common/src/main/java/com/alibaba/datax/common/util/StrUtil.java index 82222b0d48..867a9516ab 100755 --- a/common/src/main/java/com/alibaba/datax/common/util/StrUtil.java +++ b/common/src/main/java/com/alibaba/datax/common/util/StrUtil.java @@ -3,6 +3,8 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.text.DecimalFormat; import java.util.HashMap; import java.util.Map; @@ -82,4 +84,20 @@ public static String compressMiddle(String s, int headLength, int tailLength) { return s.substring(0, headLength) + "..." + s.substring(s.length() - tailLength); } + public static String getMd5(String plainText) { + try { + StringBuilder builder = new StringBuilder(); + for (byte b : MessageDigest.getInstance("MD5").digest(plainText.getBytes())) { + int i = b & 0xff; + if (i < 0x10) { + builder.append('0'); + } + builder.append(Integer.toHexString(i)); + } + return builder.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + } diff --git a/core/pom.xml b/core/pom.xml index ea3d53e79c..7685001b3e 100755 --- a/core/pom.xml +++ b/core/pom.xml @@ -41,12 +41,12 @@ org.apache.httpcomponents httpclient - 4.4 + 4.5.13 org.apache.httpcomponents fluent-hc - 4.4 + 4.5 org.slf4j @@ -100,6 +100,14 @@ + + + src/main/java + + **/*.properties + + + org.apache.maven.plugins diff --git a/core/src/main/bin/datax.py b/core/src/main/bin/datax.py index 1099ed3a08..4811ae8dd5 100755 --- a/core/src/main/bin/datax.py +++ b/core/src/main/bin/datax.py @@ -1,23 +1,26 @@ #!/usr/bin/env python # -*- coding:utf-8 -*- -import sys +import codecs +import json import os +import platform +import re import signal +import socket import subprocess +import sys import time -import re -import socket -import json -from optparse import OptionParser from optparse import OptionGroup +from optparse import OptionParser from string import Template -import codecs -import platform + +ispy2 = sys.version_info.major == 2 def isWindows(): return platform.system() == 'Windows' + DATAX_HOME = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATAX_VERSION = 'DATAX-OPENSOURCE-3.0' @@ -52,13 +55,19 @@ def getLocalIp(): def suicide(signum, e): global child_process - print >> sys.stderr, "[Error] DataX receive unexpected signal %d, starts to suicide." % (signum) + if ispy2: + print >> sys.stderr, "[Error] DataX receive unexpected signal %d, starts to suicide." % (signum) + else: + print("[Error] DataX receive unexpected signal %d, starts to suicide." % (signum), sys.stderr) if child_process: child_process.send_signal(signal.SIGQUIT) time.sleep(1) child_process.kill() - print >> sys.stderr, "DataX Process was killed ! you did ?" + if ispy2: + print >> sys.stderr, "DataX Process was killed ! you did ?" + else: + print("DataX Process was killed ! you did ?", sys.stderr) sys.exit(RET_STATE["KILL"]) @@ -92,10 +101,10 @@ def getOptionParser(): 'if you have mutiple parameters: -p"-DtableName=your-table-name -DcolumnName=your-column-name".' 'Note: you should config in you job tableName with ${tableName}.') prodEnvOptionGroup.add_option("-r", "--reader", metavar="", - action="store", dest="reader",type="string", + action="store", dest="reader", type="string", help='View job config[reader] template, eg: mysqlreader,streamreader') prodEnvOptionGroup.add_option("-w", "--writer", metavar="", - action="store", dest="writer",type="string", + action="store", dest="writer", type="string", help='View job config[writer] template, eg: mysqlwriter,streamwriter') parser.add_option_group(prodEnvOptionGroup) @@ -108,45 +117,50 @@ def getOptionParser(): parser.add_option_group(devEnvOptionGroup) return parser + def generateJobConfigTemplate(reader, writer): - readerRef = "Please refer to the %s document:\n https://github.com/alibaba/DataX/blob/master/%s/doc/%s.md \n" % (reader,reader,reader) - writerRef = "Please refer to the %s document:\n https://github.com/alibaba/DataX/blob/master/%s/doc/%s.md \n " % (writer,writer,writer) - print readerRef - print writerRef + readerRef = "Please refer to the %s document:\n https://github.com/alibaba/DataX/blob/master/%s/doc/%s.md \n" % ( + reader, reader, reader) + writerRef = "Please refer to the %s document:\n https://github.com/alibaba/DataX/blob/master/%s/doc/%s.md \n " % ( + writer, writer, writer) + print(readerRef) + print(writerRef) jobGuid = 'Please save the following configuration as a json file and use\n python {DATAX_HOME}/bin/datax.py {JSON_FILE_NAME}.json \nto run the job.\n' - print jobGuid - jobTemplate={ - "job": { - "setting": { - "speed": { - "channel": "" - } - }, - "content": [ - { - "reader": {}, - "writer": {} - } - ] - } + print(jobGuid) + jobTemplate = { + "job": { + "setting": { + "speed": { + "channel": "" + } + }, + "content": [ + { + "reader": {}, + "writer": {} + } + ] + } } - readerTemplatePath = "%s/plugin/reader/%s/plugin_job_template.json" % (DATAX_HOME,reader) - writerTemplatePath = "%s/plugin/writer/%s/plugin_job_template.json" % (DATAX_HOME,writer) + readerTemplatePath = "%s/plugin/reader/%s/plugin_job_template.json" % (DATAX_HOME, reader) + writerTemplatePath = "%s/plugin/writer/%s/plugin_job_template.json" % (DATAX_HOME, writer) try: - readerPar = readPluginTemplate(readerTemplatePath); - except Exception, e: - print "Read reader[%s] template error: can\'t find file %s" % (reader,readerTemplatePath) + readerPar = readPluginTemplate(readerTemplatePath) + except: + print("Read reader[%s] template error: can\'t find file %s" % (reader, readerTemplatePath)) try: - writerPar = readPluginTemplate(writerTemplatePath); - except Exception, e: - print "Read writer[%s] template error: : can\'t find file %s" % (writer,writerTemplatePath) - jobTemplate['job']['content'][0]['reader'] = readerPar; - jobTemplate['job']['content'][0]['writer'] = writerPar; - print json.dumps(jobTemplate, indent=4, sort_keys=True) + writerPar = readPluginTemplate(writerTemplatePath) + except: + print("Read writer[%s] template error: : can\'t find file %s" % (writer, writerTemplatePath)) + jobTemplate['job']['content'][0]['reader'] = readerPar + jobTemplate['job']['content'][0]['writer'] = writerPar + print(json.dumps(jobTemplate, indent=4, sort_keys=True)) + def readPluginTemplate(plugin): with open(plugin, 'r') as f: - return json.load(f) + return json.load(f) + def isUrl(path): if not path: @@ -168,7 +182,7 @@ def buildStartCommand(options, args): if options.remoteDebug: tempJVMCommand = tempJVMCommand + " " + REMOTE_DEBUG_CONFIG - print 'local ip: ', getLocalIp() + print('local ip: ', getLocalIp()) if options.loglevel: tempJVMCommand = tempJVMCommand + " " + ("-Dloglevel=%s" % (options.loglevel)) @@ -198,11 +212,11 @@ def buildStartCommand(options, args): def printCopyright(): - print ''' + print(''' DataX (%s), From Alibaba ! Copyright (C) 2010-2017, Alibaba Group. All Rights Reserved. -''' % DATAX_VERSION +''' % DATAX_VERSION) sys.stdout.flush() @@ -211,7 +225,7 @@ def printCopyright(): parser = getOptionParser() options, args = parser.parse_args(sys.argv[1:]) if options.reader is not None and options.writer is not None: - generateJobConfigTemplate(options.reader,options.writer) + generateJobConfigTemplate(options.reader, options.writer) sys.exit(RET_STATE['OK']) if len(args) != 1: parser.print_help() diff --git a/core/src/main/java/com/alibaba/datax/core/Engine.java b/core/src/main/java/com/alibaba/datax/core/Engine.java index f80d792f3c..4ba9fc18ec 100755 --- a/core/src/main/java/com/alibaba/datax/core/Engine.java +++ b/core/src/main/java/com/alibaba/datax/core/Engine.java @@ -6,6 +6,7 @@ import com.alibaba.datax.common.statistics.PerfTrace; import com.alibaba.datax.common.statistics.VMInfo; import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.MessageSource; import com.alibaba.datax.core.job.JobContainer; import com.alibaba.datax.core.taskgroup.TaskGroupContainer; import com.alibaba.datax.core.util.ConfigParser; @@ -73,21 +74,14 @@ public void start(Configuration allConf) { boolean traceEnable = allConf.getBool(CoreConstant.DATAX_CORE_CONTAINER_TRACE_ENABLE, true); boolean perfReportEnable = allConf.getBool(CoreConstant.DATAX_CORE_REPORT_DATAX_PERFLOG, true); - //standlone模式的datax shell任务不进行汇报 + //standalone模式的 datax shell任务不进行汇报 if(instanceId == -1){ perfReportEnable = false; } - int priority = 0; - try { - priority = Integer.parseInt(System.getenv("SKYNET_PRIORITY")); - }catch (NumberFormatException e){ - LOG.warn("prioriy set to 0, because NumberFormatException, the value is: "+System.getProperty("PROIORY")); - } - Configuration jobInfoConfig = allConf.getConfiguration(CoreConstant.DATAX_JOB_JOBINFO); //初始化PerfTrace - PerfTrace perfTrace = PerfTrace.getInstance(isJob, instanceId, taskGroupId, priority, traceEnable); + PerfTrace perfTrace = PerfTrace.getInstance(isJob, instanceId, taskGroupId, traceEnable); perfTrace.setJobInfo(jobInfoConfig,perfReportEnable,channelNumber); container.start(); @@ -135,6 +129,9 @@ public static void entry(final String[] args) throws Throwable { RUNTIME_MODE = cl.getOptionValue("mode"); Configuration configuration = ConfigParser.parse(jobPath); + // 绑定i18n信息 + MessageSource.init(configuration); + MessageSource.reloadResourceBundle(Configuration.class); long jobId; if (!"-1".equalsIgnoreCase(jobIdString)) { diff --git a/core/src/main/java/com/alibaba/datax/core/LocalStrings.properties b/core/src/main/java/com/alibaba/datax/core/LocalStrings.properties new file mode 100644 index 0000000000..97d46f07f5 --- /dev/null +++ b/core/src/main/java/com/alibaba/datax/core/LocalStrings.properties @@ -0,0 +1,5 @@ +very_like_yixiao=\u4e00{0}\u4e8c{1}\u4e09 + +engine.1=\u975e standalone \u6a21\u5f0f\u5fc5\u987b\u5728 URL \u4e2d\u63d0\u4f9b\u6709\u6548\u7684 jobId. +engine.2=\n\n\u7ecfDataX\u667a\u80fd\u5206\u6790,\u8be5\u4efb\u52a1\u6700\u53ef\u80fd\u7684\u9519\u8bef\u539f\u56e0\u662f:\n{0} + diff --git a/core/src/main/java/com/alibaba/datax/core/LocalStrings_en_US.properties b/core/src/main/java/com/alibaba/datax/core/LocalStrings_en_US.properties new file mode 100644 index 0000000000..7ff93838bc --- /dev/null +++ b/core/src/main/java/com/alibaba/datax/core/LocalStrings_en_US.properties @@ -0,0 +1,5 @@ +very_like_yixiao=1{0}2{1}3 + +engine.1=A valid job ID must be provided in the URL for the non-standalone mode. +engine.2=\n\nThrough the intelligent analysis by DataX, the most likely error reason of this task is: \n{0} + diff --git a/core/src/main/java/com/alibaba/datax/core/LocalStrings_ja_JP.properties b/core/src/main/java/com/alibaba/datax/core/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..dfbad97035 --- /dev/null +++ b/core/src/main/java/com/alibaba/datax/core/LocalStrings_ja_JP.properties @@ -0,0 +1,5 @@ +very_like_yixiao=1{0}2{1}3 + +engine.1=\u975e standalone \u6a21\u5f0f\u5fc5\u987b\u5728 URL \u4e2d\u63d0\u4f9b\u6709\u6548\u7684 jobId. +engine.2=\n\n\u7ecfDataX\u667a\u80fd\u5206\u6790,\u8be5\u4efb\u52a1\u6700\u53ef\u80fd\u7684\u9519\u8bef\u539f\u56e0\u662f:\n{0} + diff --git a/core/src/main/java/com/alibaba/datax/core/LocalStrings_zh_CN.properties b/core/src/main/java/com/alibaba/datax/core/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..97d46f07f5 --- /dev/null +++ b/core/src/main/java/com/alibaba/datax/core/LocalStrings_zh_CN.properties @@ -0,0 +1,5 @@ +very_like_yixiao=\u4e00{0}\u4e8c{1}\u4e09 + +engine.1=\u975e standalone \u6a21\u5f0f\u5fc5\u987b\u5728 URL \u4e2d\u63d0\u4f9b\u6709\u6548\u7684 jobId. +engine.2=\n\n\u7ecfDataX\u667a\u80fd\u5206\u6790,\u8be5\u4efb\u52a1\u6700\u53ef\u80fd\u7684\u9519\u8bef\u539f\u56e0\u662f:\n{0} + diff --git a/core/src/main/java/com/alibaba/datax/core/LocalStrings_zh_HK.properties b/core/src/main/java/com/alibaba/datax/core/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..2587e0ab51 --- /dev/null +++ b/core/src/main/java/com/alibaba/datax/core/LocalStrings_zh_HK.properties @@ -0,0 +1,10 @@ +very_like_yixiao=\u4e00{0}\u4e8c{1}\u4e09 + +engine.1=\u975e standalone \u6a21\u5f0f\u5fc5\u987b\u5728 URL \u4e2d\u63d0\u4f9b\u6709\u6548\u7684 jobId. +engine.2=\n\n\u7ecfDataX\u667a\u80fd\u5206\u6790,\u8be5\u4efb\u52a1\u6700\u53ef\u80fd\u7684\u9519\u8bef\u539f\u56e0\u662f:\n{0} + +very_like_yixiao=一{0}二{1}三 + +engine.1=非 standalone 模式必須在 URL 中提供有效的 jobId. +engine.2=\n\n經DataX智能分析,該任務最可能的錯誤原因是:\n{0} + diff --git a/core/src/main/java/com/alibaba/datax/core/LocalStrings_zh_TW.properties b/core/src/main/java/com/alibaba/datax/core/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..2587e0ab51 --- /dev/null +++ b/core/src/main/java/com/alibaba/datax/core/LocalStrings_zh_TW.properties @@ -0,0 +1,10 @@ +very_like_yixiao=\u4e00{0}\u4e8c{1}\u4e09 + +engine.1=\u975e standalone \u6a21\u5f0f\u5fc5\u987b\u5728 URL \u4e2d\u63d0\u4f9b\u6709\u6548\u7684 jobId. +engine.2=\n\n\u7ecfDataX\u667a\u80fd\u5206\u6790,\u8be5\u4efb\u52a1\u6700\u53ef\u80fd\u7684\u9519\u8bef\u539f\u56e0\u662f:\n{0} + +very_like_yixiao=一{0}二{1}三 + +engine.1=非 standalone 模式必須在 URL 中提供有效的 jobId. +engine.2=\n\n經DataX智能分析,該任務最可能的錯誤原因是:\n{0} + diff --git a/core/src/main/java/com/alibaba/datax/core/container/util/JobAssignUtil.java b/core/src/main/java/com/alibaba/datax/core/container/util/JobAssignUtil.java index 31ba60a4dd..cbd0d2a1d5 100755 --- a/core/src/main/java/com/alibaba/datax/core/container/util/JobAssignUtil.java +++ b/core/src/main/java/com/alibaba/datax/core/container/util/JobAssignUtil.java @@ -114,7 +114,7 @@ private static LinkedHashMap> parseAndGetResourceMarkAndTa * 需要实现的效果通过例子来说是: *
      * a 库上有表:0, 1, 2
-     * a 库上有表:3, 4
+     * b 库上有表:3, 4
      * c 库上有表:5, 6, 7
      *
      * 如果有 4个 taskGroup
diff --git a/core/src/main/java/com/alibaba/datax/core/job/JobContainer.java b/core/src/main/java/com/alibaba/datax/core/job/JobContainer.java
index 26b2989f95..49f5a0a14c 100755
--- a/core/src/main/java/com/alibaba/datax/core/job/JobContainer.java
+++ b/core/src/main/java/com/alibaba/datax/core/job/JobContainer.java
@@ -27,7 +27,7 @@
 import com.alibaba.datax.core.util.container.CoreConstant;
 import com.alibaba.datax.core.util.container.LoadUtil;
 import com.alibaba.datax.dataxservice.face.domain.enums.ExecuteMode;
-import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson2.JSON;
 import org.apache.commons.lang.StringUtils;
 import org.apache.commons.lang.Validate;
 import org.slf4j.Logger;
diff --git a/core/src/main/java/com/alibaba/datax/core/statistics/communication/CommunicationTool.java b/core/src/main/java/com/alibaba/datax/core/statistics/communication/CommunicationTool.java
index 51a601aeb6..1815ea024d 100755
--- a/core/src/main/java/com/alibaba/datax/core/statistics/communication/CommunicationTool.java
+++ b/core/src/main/java/com/alibaba/datax/core/statistics/communication/CommunicationTool.java
@@ -2,7 +2,7 @@
 
 import com.alibaba.datax.common.statistics.PerfTrace;
 import com.alibaba.datax.common.util.StrUtil;
-import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson2.JSON;
 import org.apache.commons.lang.Validate;
 
 import java.text.DecimalFormat;
diff --git a/core/src/main/java/com/alibaba/datax/core/statistics/plugin/task/StdoutPluginCollector.java b/core/src/main/java/com/alibaba/datax/core/statistics/plugin/task/StdoutPluginCollector.java
index 8b2a837811..d88ad0a8e6 100755
--- a/core/src/main/java/com/alibaba/datax/core/statistics/plugin/task/StdoutPluginCollector.java
+++ b/core/src/main/java/com/alibaba/datax/core/statistics/plugin/task/StdoutPluginCollector.java
@@ -6,7 +6,7 @@
 import com.alibaba.datax.core.statistics.communication.Communication;
 import com.alibaba.datax.core.util.container.CoreConstant;
 import com.alibaba.datax.core.statistics.plugin.task.util.DirtyRecord;
-import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson2.JSON;
 
 import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
diff --git a/core/src/main/java/com/alibaba/datax/core/statistics/plugin/task/util/DirtyRecord.java b/core/src/main/java/com/alibaba/datax/core/statistics/plugin/task/util/DirtyRecord.java
index fdc5d8215d..caa4cb5bfd 100755
--- a/core/src/main/java/com/alibaba/datax/core/statistics/plugin/task/util/DirtyRecord.java
+++ b/core/src/main/java/com/alibaba/datax/core/statistics/plugin/task/util/DirtyRecord.java
@@ -4,22 +4,25 @@
 import com.alibaba.datax.common.element.Record;
 import com.alibaba.datax.common.exception.DataXException;
 import com.alibaba.datax.core.util.FrameworkErrorCode;
-import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson2.JSON;
 
 import java.math.BigDecimal;
 import java.math.BigInteger;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.Map;
 
 public class DirtyRecord implements Record {
 	private List columns = new ArrayList();
+	private Map meta;
 
 	public static DirtyRecord asDirtyRecord(final Record record) {
 		DirtyRecord result = new DirtyRecord();
 		for (int i = 0; i < record.getColumnNumber(); i++) {
 			result.addColumn(record.getColumn(i));
 		}
+		result.setMeta(record.getMeta());
 
 		return result;
 	}
@@ -65,6 +68,16 @@ public int getMemorySize() {
 				"该方法不支持!");
 	}
 
+	@Override
+	public void setMeta(Map meta) {
+		this.meta = meta;
+	}
+
+	@Override
+	public Map getMeta() {
+		return this.meta;
+	}
+
 	public List getColumns() {
 		return columns;
 	}
@@ -119,6 +132,12 @@ public Date asDate() {
 		throw DataXException.asDataXException(FrameworkErrorCode.RUNTIME_ERROR,
 				"该方法不支持!");
 	}
+	
+	@Override
+	public Date asDate(String dateFormat) {
+		throw DataXException.asDataXException(FrameworkErrorCode.RUNTIME_ERROR,
+				"该方法不支持!");
+	}
 
 	@Override
 	public byte[] asBytes() {
diff --git a/core/src/main/java/com/alibaba/datax/core/taskgroup/TaskGroupContainer.java b/core/src/main/java/com/alibaba/datax/core/taskgroup/TaskGroupContainer.java
index c30c94d9b3..b4b45695ab 100755
--- a/core/src/main/java/com/alibaba/datax/core/taskgroup/TaskGroupContainer.java
+++ b/core/src/main/java/com/alibaba/datax/core/taskgroup/TaskGroupContainer.java
@@ -27,7 +27,7 @@
 import com.alibaba.datax.core.util.container.CoreConstant;
 import com.alibaba.datax.core.util.container.LoadUtil;
 import com.alibaba.datax.dataxservice.face.domain.enums.State;
-import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson2.JSON;
 import org.apache.commons.lang3.Validate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
diff --git a/core/src/main/java/com/alibaba/datax/core/transport/channel/memory/MemoryChannel.java b/core/src/main/java/com/alibaba/datax/core/transport/channel/memory/MemoryChannel.java
index e49c7878c7..5bce085f15 100755
--- a/core/src/main/java/com/alibaba/datax/core/transport/channel/memory/MemoryChannel.java
+++ b/core/src/main/java/com/alibaba/datax/core/transport/channel/memory/MemoryChannel.java
@@ -29,7 +29,7 @@ public class MemoryChannel extends Channel {
 
 	private ReentrantLock lock;
 
-	private Condition notInsufficient, notEmpty;
+	private Condition notSufficient, notEmpty;
 
 	public MemoryChannel(final Configuration configuration) {
 		super(configuration);
@@ -37,7 +37,7 @@ public MemoryChannel(final Configuration configuration) {
 		this.bufferSize = configuration.getInt(CoreConstant.DATAX_CORE_TRANSPORT_EXCHANGER_BUFFERSIZE);
 
 		lock = new ReentrantLock();
-		notInsufficient = lock.newCondition();
+		notSufficient = lock.newCondition();
 		notEmpty = lock.newCondition();
 	}
 
@@ -75,7 +75,7 @@ protected void doPushAll(Collection rs) {
 			lock.lockInterruptibly();
 			int bytes = getRecordBytes(rs);
 			while (memoryBytes.get() + bytes > this.byteCapacity || rs.size() > this.queue.remainingCapacity()) {
-				notInsufficient.await(200L, TimeUnit.MILLISECONDS);
+				notSufficient.await(200L, TimeUnit.MILLISECONDS);
             }
 			this.queue.addAll(rs);
 			waitWriterTime += System.nanoTime() - startTime;
@@ -116,7 +116,7 @@ protected void doPullAll(Collection rs) {
 			waitReaderTime += System.nanoTime() - startTime;
 			int bytes = getRecordBytes(rs);
 			memoryBytes.addAndGet(-bytes);
-			notInsufficient.signalAll();
+			notSufficient.signalAll();
 		} catch (InterruptedException e) {
 			throw DataXException.asDataXException(
 					FrameworkErrorCode.RUNTIME_ERROR, e);
diff --git a/core/src/main/java/com/alibaba/datax/core/transport/record/DefaultRecord.java b/core/src/main/java/com/alibaba/datax/core/transport/record/DefaultRecord.java
index 2598bc8c80..1dfa02e882 100755
--- a/core/src/main/java/com/alibaba/datax/core/transport/record/DefaultRecord.java
+++ b/core/src/main/java/com/alibaba/datax/core/transport/record/DefaultRecord.java
@@ -5,7 +5,7 @@
 import com.alibaba.datax.common.exception.DataXException;
 import com.alibaba.datax.core.util.ClassSize;
 import com.alibaba.datax.core.util.FrameworkErrorCode;
-import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson2.JSON;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -27,6 +27,8 @@ public class DefaultRecord implements Record {
 	// 首先是Record本身需要的内存
 	private int memorySize = ClassSize.DefaultRecordHead;
 
+	private Map meta;
+
 	public DefaultRecord() {
 		this.columns = new ArrayList(RECORD_AVERGAE_COLUMN_NUMBER);
 	}
@@ -83,6 +85,16 @@ public int getMemorySize(){
 		return memorySize;
 	}
 
+	@Override
+	public void setMeta(Map meta) {
+		this.meta = meta;
+	}
+
+	@Override
+	public Map getMeta() {
+		return this.meta;
+	}
+
 	private void decrByteSize(final Column column) {
 		if (null == column) {
 			return;
diff --git a/core/src/main/java/com/alibaba/datax/core/transport/record/TerminateRecord.java b/core/src/main/java/com/alibaba/datax/core/transport/record/TerminateRecord.java
index 928609abda..7cb1cff104 100755
--- a/core/src/main/java/com/alibaba/datax/core/transport/record/TerminateRecord.java
+++ b/core/src/main/java/com/alibaba/datax/core/transport/record/TerminateRecord.java
@@ -3,6 +3,8 @@
 import com.alibaba.datax.common.element.Column;
 import com.alibaba.datax.common.element.Record;
 
+import java.util.Map;
+
 /**
  * 作为标示 生产者已经完成生产的标志
  * 
@@ -41,6 +43,16 @@ public int getMemorySize() {
 		return 0;
 	}
 
+	@Override
+	public void setMeta(Map meta) {
+
+	}
+
+	@Override
+	public Map getMeta() {
+		return null;
+	}
+
 	@Override
 	public void setColumn(int i, Column column) {
 		return;
diff --git a/core/src/main/java/com/alibaba/datax/core/transport/transformer/DigestTransformer.java b/core/src/main/java/com/alibaba/datax/core/transport/transformer/DigestTransformer.java
new file mode 100644
index 0000000000..d2bf143147
--- /dev/null
+++ b/core/src/main/java/com/alibaba/datax/core/transport/transformer/DigestTransformer.java
@@ -0,0 +1,87 @@
+package com.alibaba.datax.core.transport.transformer;
+
+import com.alibaba.datax.common.element.Column;
+import com.alibaba.datax.common.element.Record;
+import com.alibaba.datax.common.element.StringColumn;
+import com.alibaba.datax.common.exception.DataXException;
+import com.alibaba.datax.transformer.Transformer;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang.StringUtils;
+
+import java.util.Arrays;
+
+/**
+ * no comments.
+ *
+ * @author XuDaojie
+ * @since 2021-08-16
+ */
+public class DigestTransformer extends Transformer {
+
+    private static final String MD5 = "md5";
+    private static final String SHA1 = "sha1";
+    private static final String TO_UPPER_CASE = "toUpperCase";
+    private static final String TO_LOWER_CASE = "toLowerCase";
+
+    public DigestTransformer() {
+        setTransformerName("dx_digest");
+    }
+
+    @Override
+    public Record evaluate(Record record, Object... paras) {
+
+        int columnIndex;
+        String type;
+        String charType;
+
+        try {
+            if (paras.length != 3) {
+                throw new RuntimeException("dx_digest paras length must be 3");
+            }
+
+            columnIndex = (Integer) paras[0];
+            type = (String) paras[1];
+            charType = (String) paras[2];
+
+            if (!StringUtils.equalsIgnoreCase(MD5, type) && !StringUtils.equalsIgnoreCase(SHA1, type)) {
+                throw new RuntimeException("dx_digest paras index 1 must be md5 or sha1");
+            }
+            if (!StringUtils.equalsIgnoreCase(TO_UPPER_CASE, charType) && !StringUtils.equalsIgnoreCase(TO_LOWER_CASE, charType)) {
+                throw new RuntimeException("dx_digest paras index 2 must be toUpperCase or toLowerCase");
+            }
+        } catch (Exception e) {
+            throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_ILLEGAL_PARAMETER, "paras:" + Arrays.asList(paras) + " => " + e.getMessage());
+        }
+
+        Column column = record.getColumn(columnIndex);
+
+        try {
+            String oriValue = column.asString();
+
+            // 如果字段为空,作为空字符串处理
+            if (oriValue == null) {
+                oriValue = "";
+            }
+            String newValue;
+            if (MD5.equals(type)) {
+                newValue = DigestUtils.md5Hex(oriValue);
+            } else {
+                newValue = DigestUtils.sha1Hex(oriValue);
+            }
+
+            if (TO_UPPER_CASE.equals(charType)) {
+                newValue = newValue.toUpperCase();
+            } else {
+                newValue = newValue.toLowerCase();
+            }
+
+            record.setColumn(columnIndex, new StringColumn(newValue));
+
+        } catch (Exception e) {
+            throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_RUN_EXCEPTION, e.getMessage(), e);
+        }
+        return record;
+    }
+
+}
diff --git a/core/src/main/java/com/alibaba/datax/core/transport/transformer/FilterTransformer.java b/core/src/main/java/com/alibaba/datax/core/transport/transformer/FilterTransformer.java
index 8f6492fa11..a3251715d7 100644
--- a/core/src/main/java/com/alibaba/datax/core/transport/transformer/FilterTransformer.java
+++ b/core/src/main/java/com/alibaba/datax/core/transport/transformer/FilterTransformer.java
@@ -61,7 +61,7 @@ public Record evaluate(Record record, Object... paras) {
             } else if (code.equalsIgnoreCase("<=")) {
                 return doLess(record, value, column, true);
             } else {
-                throw new RuntimeException("dx_filter can't suport code:" + code);
+                throw new RuntimeException("dx_filter can't support code:" + code);
             }
         } catch (Exception e) {
             throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_RUN_EXCEPTION, e.getMessage(), e);
diff --git a/core/src/main/java/com/alibaba/datax/core/transport/transformer/GroovyTransformerStaticUtil.java b/core/src/main/java/com/alibaba/datax/core/transport/transformer/GroovyTransformerStaticUtil.java
index 4c872993ab..487a8be8f2 100644
--- a/core/src/main/java/com/alibaba/datax/core/transport/transformer/GroovyTransformerStaticUtil.java
+++ b/core/src/main/java/com/alibaba/datax/core/transport/transformer/GroovyTransformerStaticUtil.java
@@ -1,10 +1,18 @@
 package com.alibaba.datax.core.transport.transformer;
 
+import org.apache.commons.codec.digest.DigestUtils;
+
 /**
  * GroovyTransformer的帮助类,供groovy代码使用,必须全是static的方法
  * Created by liqiang on 16/3/4.
  */
 public class GroovyTransformerStaticUtil  {
 
+    public static String md5(final String data) {
+        return DigestUtils.md5Hex(data);
+    }
 
+    public static String sha1(final String data) {
+        return DigestUtils.sha1Hex(data);
+    }
 }
diff --git a/core/src/main/java/com/alibaba/datax/core/transport/transformer/TransformerRegistry.java b/core/src/main/java/com/alibaba/datax/core/transport/transformer/TransformerRegistry.java
index 96a0d98845..3c62515340 100644
--- a/core/src/main/java/com/alibaba/datax/core/transport/transformer/TransformerRegistry.java
+++ b/core/src/main/java/com/alibaba/datax/core/transport/transformer/TransformerRegistry.java
@@ -36,6 +36,7 @@ public class TransformerRegistry {
         registTransformer(new ReplaceTransformer());
         registTransformer(new FilterTransformer());
         registTransformer(new GroovyTransformer());
+        registTransformer(new DigestTransformer());
     }
 
     public static void loadTransformerFromLocalStorage() {
diff --git a/core/src/main/java/com/alibaba/datax/core/util/ConfigParser.java b/core/src/main/java/com/alibaba/datax/core/util/ConfigParser.java
index 20039864b8..24f43d55d5 100755
--- a/core/src/main/java/com/alibaba/datax/core/util/ConfigParser.java
+++ b/core/src/main/java/com/alibaba/datax/core/util/ConfigParser.java
@@ -168,6 +168,7 @@ public static Configuration parseOnePluginConfig(final String path,
         boolean isDefaultPath = StringUtils.isBlank(pluginPath);
         if (isDefaultPath) {
             configuration.set("path", path);
+            configuration.set("loadType","jarLoader");
         }
 
         Configuration result = Configuration.newDefault();
diff --git a/core/src/main/java/com/alibaba/datax/core/util/LocalStrings.properties b/core/src/main/java/com/alibaba/datax/core/util/LocalStrings.properties
new file mode 100644
index 0000000000..a90f782972
--- /dev/null
+++ b/core/src/main/java/com/alibaba/datax/core/util/LocalStrings.properties
@@ -0,0 +1,58 @@
+configparser.1=\u63D2\u4EF6[{0},{1}]\u52A0\u8F7D\u5931\u8D25\uFF0C1s\u540E\u91CD\u8BD5... Exception:{2}
+configparser.2=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.3=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.4=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.5=\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25\uFF0C\u672A\u5B8C\u6210\u6307\u5B9A\u63D2\u4EF6\u52A0\u8F7D:{0}
+configparser.6=\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25,\u5B58\u5728\u91CD\u590D\u63D2\u4EF6:{0}
+
+dataxserviceutil.1=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38NoSuchAlgorithmException, [{0}]
+dataxserviceutil.2=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38InvalidKeyException, [{0}]
+dataxserviceutil.3=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38UnsupportedEncodingException, [{0}]
+
+errorrecordchecker.1=\u810F\u6570\u636E\u767E\u5206\u6BD4\u9650\u5236\u5E94\u8BE5\u5728[0.0, 1.0]\u4E4B\u95F4
+errorrecordchecker.2=\u810F\u6570\u636E\u6761\u6570\u73B0\u5728\u5E94\u8BE5\u4E3A\u975E\u8D1F\u6574\u6570
+errorrecordchecker.3=\u810F\u6570\u636E\u6761\u6570\u68C0\u67E5\u4E0D\u901A\u8FC7\uFF0C\u9650\u5236\u662F[{0}]\u6761\uFF0C\u4F46\u5B9E\u9645\u4E0A\u6355\u83B7\u4E86[{1}]\u6761.
+errorrecordchecker.4=\u810F\u6570\u636E\u767E\u5206\u6BD4\u68C0\u67E5\u4E0D\u901A\u8FC7\uFF0C\u9650\u5236\u662F[{0}]\uFF0C\u4F46\u5B9E\u9645\u4E0A\u6355\u83B7\u5230[{1}].
+
+
+errorcode.install_error=DataX\u5F15\u64CE\u5B89\u88C5\u9519\u8BEF, \u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.argument_error=DataX\u5F15\u64CE\u8FD0\u884C\u9519\u8BEF\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8E\u5185\u90E8\u7F16\u7A0B\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3 .
+errorcode.runtime_error=DataX\u5F15\u64CE\u8FD0\u884C\u8FC7\u7A0B\u51FA\u9519\uFF0C\u5177\u4F53\u539F\u56E0\u8BF7\u53C2\u770BDataX\u8FD0\u884C\u7ED3\u675F\u65F6\u7684\u9519\u8BEF\u8BCA\u65AD\u4FE1\u606F  .
+errorcode.config_error=DataX\u5F15\u64CE\u914D\u7F6E\u9519\u8BEF\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.secret_error=DataX\u5F15\u64CE\u52A0\u89E3\u5BC6\u51FA\u9519\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.hook_load_error=\u52A0\u8F7D\u5916\u90E8Hook\u51FA\u73B0\u9519\u8BEF\uFF0C\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u5F15\u8D77\u7684
+errorcode.hook_fail_error=\u6267\u884C\u5916\u90E8Hook\u51FA\u73B0\u9519\u8BEF
+errorcode.plugin_install_error=DataX\u63D2\u4EF6\u5B89\u88C5\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_not_found=DataX\u63D2\u4EF6\u914D\u7F6E\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_init_error=DataX\u63D2\u4EF6\u521D\u59CB\u5316\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_runtime_error=DataX\u63D2\u4EF6\u8FD0\u884C\u65F6\u51FA\u9519, \u5177\u4F53\u539F\u56E0\u8BF7\u53C2\u770BDataX\u8FD0\u884C\u7ED3\u675F\u65F6\u7684\u9519\u8BEF\u8BCA\u65AD\u4FE1\u606F .
+errorcode.plugin_dirty_data_limit_exceed=DataX\u4F20\u8F93\u810F\u6570\u636E\u8D85\u8FC7\u7528\u6237\u9884\u671F\uFF0C\u8BE5\u9519\u8BEF\u901A\u5E38\u662F\u7531\u4E8E\u6E90\u7AEF\u6570\u636E\u5B58\u5728\u8F83\u591A\u4E1A\u52A1\u810F\u6570\u636E\u5BFC\u81F4\uFF0C\u8BF7\u4ED4\u7EC6\u68C0\u67E5DataX\u6C47\u62A5\u7684\u810F\u6570\u636E\u65E5\u5FD7\u4FE1\u606F, \u6216\u8005\u60A8\u53EF\u4EE5\u9002\u5F53\u8C03\u5927\u810F\u6570\u636E\u9608\u503C .
+errorcode.plugin_split_error=DataX\u63D2\u4EF6\u5207\u5206\u51FA\u9519, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5404\u4E2A\u63D2\u4EF6\u7F16\u7A0B\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3
+errorcode.kill_job_timeout_error=kill \u4EFB\u52A1\u8D85\u65F6\uFF0C\u8BF7\u8054\u7CFBPE\u89E3\u51B3
+errorcode.start_taskgroup_error=taskGroup\u542F\u52A8\u5931\u8D25,\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3
+errorcode.call_datax_service_failed=\u8BF7\u6C42 DataX Service \u51FA\u9519.
+errorcode.call_remote_failed=\u8FDC\u7A0B\u8C03\u7528\u5931\u8D25
+errorcode.killed_exit_value=Job \u6536\u5230\u4E86 Kill \u547D\u4EE4.
+
+
+httpclientutil.1=\u8BF7\u6C42\u5730\u5740\uFF1A{0}, \u8BF7\u6C42\u65B9\u6CD5\uFF1A{1}, STATUS CODE = {2}, Response Entity: {3} 
+httpclientutil.2=\u8FDC\u7A0B\u63A5\u53E3\u8FD4\u56DE-1,\u5C06\u91CD\u8BD5
+
+
+secretutil.1=\u7CFB\u7EDF\u7F16\u7A0B\u9519\u8BEF,\u4E0D\u652F\u6301\u7684\u52A0\u5BC6\u7C7B\u578B
+secretutil.2=\u7CFB\u7EDF\u7F16\u7A0B\u9519\u8BEF,\u4E0D\u652F\u6301\u7684\u52A0\u5BC6\u7C7B\u578B
+secretutil.3=rsa\u52A0\u5BC6\u51FA\u9519
+secretutil.4=rsa\u89E3\u5BC6\u51FA\u9519
+secretutil.5=3\u91CDDES\u52A0\u5BC6\u51FA\u9519
+secretutil.6=rsa\u89E3\u5BC6\u51FA\u9519
+secretutil.7=\u6784\u5EFA\u4E09\u91CDDES\u5BC6\u5319\u51FA\u9519
+secretutil.8=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u65E0\u6CD5\u627E\u5230\u5BC6\u94A5\u7684\u914D\u7F6E\u6587\u4EF6
+secretutil.9=\u8BFB\u53D6\u52A0\u89E3\u5BC6\u914D\u7F6E\u6587\u4EF6\u51FA\u9519
+secretutil.10=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C
+secretutil.11=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7EDF\u7EF4\u62A4\u95EE\u9898
+secretutil.12=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C
+secretutil.13=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7EDF\u7EF4\u62A4\u95EE\u9898
+secretutil.14=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C[{0}]\u5B58\u5728\u5BC6\u94A5\u4E3A\u7A7A\u7684\u60C5\u51B5
+secretutil.15=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u516C\u79C1\u94A5\u5BF9\u5B58\u5728\u4E3A\u7A7A\u7684\u60C5\u51B5\uFF0C\u7248\u672C[{0}]
+secretutil.16=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u65E0\u6CD5\u627E\u5230\u52A0\u89E3\u5BC6\u914D\u7F6E
+
diff --git a/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_en_US.properties b/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_en_US.properties
new file mode 100644
index 0000000000..8e01b15386
--- /dev/null
+++ b/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_en_US.properties
@@ -0,0 +1,58 @@
+configparser.1=Failed to load the plug-in [{0},{1}]. We will retry in 1s... Exception: {2}
+configparser.2=Failed to obtain the job configuration information: {0}
+configparser.3=Failed to obtain the job configuration information: {0}
+configparser.4=Failed to obtain the job configuration information: {0}
+configparser.5=Failed to load the plug-in. Loading of the specific plug-in:{0} is not completed
+configparser.6=Failed to load the plug-in. A duplicate plug-in: {0} exists
+
+dataxserviceutil.1=Exception in creating signature. NoSuchAlgorithmException, [{0}]
+dataxserviceutil.2=Exception in creating signature. InvalidKeyException, [{0}]
+dataxserviceutil.3=Exception in creating signature. UnsupportedEncodingException, [{0}]
+
+errorrecordchecker.1=The percentage of dirty data should be limited to within [0.0, 1.0]
+errorrecordchecker.2=The number of dirty data entries should now be a nonnegative integer
+errorrecordchecker.3=Check for the number of dirty data entries has not passed. The limit is [{0}] entries, but [{1}] entries have been captured. 
+errorrecordchecker.4=Check for the percentage of dirty data has not passed. The limit is [{0}], but [{1}] of dirty data has been captured. 
+
+
+errorcode.install_error=Error in installing DataX engine. Please contact your O&M team to solve the problem.
+errorcode.argument_error=Error in running DataX engine. This problem is generally caused by an internal programming error. Please contact the DataX developer team to solve the problem. 
+errorcode.runtime_error=The DataX engine encountered an error during running. For the specific cause, refer to the error diagnosis after DataX stops running. 
+errorcode.config_error=Error in DataX engine configuration. This problem is generally caused by a DataX installation error. Please contact your O&M team to solve the problem. 
+errorcode.secret_error=Error in DataX engine encryption or decryption. This problem is generally caused by a DataX key configuration error. Please contact your O&M team to solve the problem. 
+errorcode.hook_load_error=Error in loading the external hook. This problem is generally caused by the DataX installation. 
+errorcode.hook_fail_error=Error in executing the external hook
+errorcode.plugin_install_error=Error in installing DataX plug-in. This problem is generally caused by a DataX installation error. Please contact your O&M team to solve the problem. 
+errorcode.plugin_not_found=Error in DataX plug-in configuration. This problem is generally caused by a DataX installation error. Please contact your O&M team to solve the problem. 
+errorcode.plugin_init_error=Error in DataX plug-in initialization. This problem is generally caused by a DataX installation error. Please contact your O&M team to solve the problem. 
+errorcode.plugin_runtime_error=The DataX plug-in encountered an error during running. For the specific cause, refer to the error diagnosis after DataX stops running. 
+errorcode.plugin_dirty_data_limit_exceed=The dirty data transmitted by DataX exceeds user expectations. This error often occurs when a lot dirty data exists in the source data. Please carefully check the dirty data log information reported by DataX, or you can tune up the dirty data threshold value. 
+errorcode.plugin_split_error=Error in DataX plug-in slicing. This problem is generally caused by a programming error in some DataX plug-in. Please contact the DataX developer team to solve the problem. 
+errorcode.kill_job_timeout_error=The kill task times out. Please contact the PE to solve the problem
+errorcode.start_taskgroup_error=Failed to start the task group. Please contact the DataX developer team to solve the problem
+errorcode.call_datax_service_failed=Error in requesting DataX Service.
+errorcode.call_remote_failed=Remote call failure
+errorcode.killed_exit_value=The job has received a Kill command.
+
+
+httpclientutil.1=Request address: {0}. Request method: {1}. STATUS CODE = {2}, Response Entity: {3}
+httpclientutil.2=The remote interface returns -1. We will try again
+
+
+secretutil.1=System programing error. Unsupported encryption type
+secretutil.2=System programing error. Unsupported encryption type
+secretutil.3=RSA encryption error
+secretutil.4=RSA decryption error
+secretutil.5=Triple DES encryption error
+secretutil.6=RSA decryption error
+secretutil.7=Error in building Triple DES key
+secretutil.8=DataX configuration requires encryption and decryption, but unable to find the key configuration file
+secretutil.9=Error in reading the encryption and decryption configuration file
+secretutil.10=The version of the DataX-configured key is [{0}], but there is no configuration in the system. Error in task key configuration. The key version you configured does not exist
+secretutil.11=The version of the DataX-configured key is [{0}], but there is no configuration in the system. There may be an error in task key configuration, or a problem in system maintenance
+secretutil.12=The version of the DataX-configured key is [{0}], but there is no configuration in the system. Error in task key configuration. The key version you configured does not exist
+secretutil.13=The version of the DataX-configured key is [{0}], but there is no configuration in the system. There may be an error in task key configuration, or a problem in system maintenance
+secretutil.14=DataX configuration requires encryption and decryption, but some key in the configured key version [{0}] is empty
+secretutil.15=DataX configuration requires encryption and decryption, but some configured public/private key pairs are empty and the version is [{0}]
+secretutil.16=DataX configuration requires encryption and decryption, but the encryption and decryption configuration cannot be found
+
diff --git a/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_ja_JP.properties b/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_ja_JP.properties
new file mode 100644
index 0000000000..7a0c95ac9d
--- /dev/null
+++ b/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_ja_JP.properties
@@ -0,0 +1,58 @@
+configparser.1=\u63D2\u4EF6[{0},{1}]\u52A0\u8F7D\u5931\u8D25\uFF0C1s\u540E\u91CD\u8BD5... Exception:{2}
+configparser.2=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.3=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.4=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.5=\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25\uFF0C\u672A\u5B8C\u6210\u6307\u5B9A\u63D2\u4EF6\u52A0\u8F7D:{0}
+configparser.6=\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25,\u5B58\u5728\u91CD\u590D\u63D2\u4EF6:{0}
+
+dataxserviceutil.1=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38NoSuchAlgorithmException, [{0}]
+dataxserviceutil.2=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38InvalidKeyException, [{0}]
+dataxserviceutil.3=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38UnsupportedEncodingException, [{0}]
+
+errorrecordchecker.1=\u810F\u6570\u636E\u767E\u5206\u6BD4\u9650\u5236\u5E94\u8BE5\u5728[0.0, 1.0]\u4E4B\u95F4
+errorrecordchecker.2=\u810F\u6570\u636E\u6761\u6570\u73B0\u5728\u5E94\u8BE5\u4E3A\u975E\u8D1F\u6574\u6570
+errorrecordchecker.3=\u810F\u6570\u636E\u6761\u6570\u68C0\u67E5\u4E0D\u901A\u8FC7\uFF0C\u9650\u5236\u662F[{0}]\u6761\uFF0C\u4F46\u5B9E\u9645\u4E0A\u6355\u83B7\u4E86[{1}]\u6761.
+errorrecordchecker.4=\u810F\u6570\u636E\u767E\u5206\u6BD4\u68C0\u67E5\u4E0D\u901A\u8FC7\uFF0C\u9650\u5236\u662F[{0}]\uFF0C\u4F46\u5B9E\u9645\u4E0A\u6355\u83B7\u5230[{1}].
+
+
+errorcode.install_error=DataX\u5F15\u64CE\u5B89\u88C5\u9519\u8BEF, \u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.argument_error=DataX\u5F15\u64CE\u8FD0\u884C\u9519\u8BEF\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8E\u5185\u90E8\u7F16\u7A0B\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3 .
+errorcode.runtime_error=DataX\u5F15\u64CE\u8FD0\u884C\u8FC7\u7A0B\u51FA\u9519\uFF0C\u5177\u4F53\u539F\u56E0\u8BF7\u53C2\u770BDataX\u8FD0\u884C\u7ED3\u675F\u65F6\u7684\u9519\u8BEF\u8BCA\u65AD\u4FE1\u606F  .
+errorcode.config_error=DataX\u5F15\u64CE\u914D\u7F6E\u9519\u8BEF\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.secret_error=DataX\u5F15\u64CE\u52A0\u89E3\u5BC6\u51FA\u9519\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.hook_load_error=\u52A0\u8F7D\u5916\u90E8Hook\u51FA\u73B0\u9519\u8BEF\uFF0C\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u5F15\u8D77\u7684
+errorcode.hook_fail_error=\u6267\u884C\u5916\u90E8Hook\u51FA\u73B0\u9519\u8BEF
+errorcode.plugin_install_error=DataX\u63D2\u4EF6\u5B89\u88C5\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_not_found=DataX\u63D2\u4EF6\u914D\u7F6E\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_init_error=DataX\u63D2\u4EF6\u521D\u59CB\u5316\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_runtime_error=DataX\u63D2\u4EF6\u8FD0\u884C\u65F6\u51FA\u9519, \u5177\u4F53\u539F\u56E0\u8BF7\u53C2\u770BDataX\u8FD0\u884C\u7ED3\u675F\u65F6\u7684\u9519\u8BEF\u8BCA\u65AD\u4FE1\u606F .
+errorcode.plugin_dirty_data_limit_exceed=DataX\u4F20\u8F93\u810F\u6570\u636E\u8D85\u8FC7\u7528\u6237\u9884\u671F\uFF0C\u8BE5\u9519\u8BEF\u901A\u5E38\u662F\u7531\u4E8E\u6E90\u7AEF\u6570\u636E\u5B58\u5728\u8F83\u591A\u4E1A\u52A1\u810F\u6570\u636E\u5BFC\u81F4\uFF0C\u8BF7\u4ED4\u7EC6\u68C0\u67E5DataX\u6C47\u62A5\u7684\u810F\u6570\u636E\u65E5\u5FD7\u4FE1\u606F, \u6216\u8005\u60A8\u53EF\u4EE5\u9002\u5F53\u8C03\u5927\u810F\u6570\u636E\u9608\u503C .
+errorcode.plugin_split_error=DataX\u63D2\u4EF6\u5207\u5206\u51FA\u9519, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5404\u4E2A\u63D2\u4EF6\u7F16\u7A0B\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3
+errorcode.kill_job_timeout_error=kill \u4EFB\u52A1\u8D85\u65F6\uFF0C\u8BF7\u8054\u7CFBPE\u89E3\u51B3
+errorcode.start_taskgroup_error=taskGroup\u542F\u52A8\u5931\u8D25,\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3
+errorcode.call_datax_service_failed=\u8BF7\u6C42 DataX Service \u51FA\u9519.
+errorcode.call_remote_failed=\u8FDC\u7A0B\u8C03\u7528\u5931\u8D25
+errorcode.killed_exit_value=Job \u6536\u5230\u4E86 Kill \u547D\u4EE4.
+
+
+httpclientutil.1=\u8BF7\u6C42\u5730\u5740\uFF1A{0}, \u8BF7\u6C42\u65B9\u6CD5\uFF1A{1},STATUS CODE = {2}, Response Entity: {3}
+httpclientutil.2=\u8FDC\u7A0B\u63A5\u53E3\u8FD4\u56DE-1,\u5C06\u91CD\u8BD5
+
+
+secretutil.1=\u7CFB\u7EDF\u7F16\u7A0B\u9519\u8BEF,\u4E0D\u652F\u6301\u7684\u52A0\u5BC6\u7C7B\u578B
+secretutil.2=\u7CFB\u7EDF\u7F16\u7A0B\u9519\u8BEF,\u4E0D\u652F\u6301\u7684\u52A0\u5BC6\u7C7B\u578B
+secretutil.3=rsa\u52A0\u5BC6\u51FA\u9519
+secretutil.4=rsa\u89E3\u5BC6\u51FA\u9519
+secretutil.5=3\u91CDDES\u52A0\u5BC6\u51FA\u9519
+secretutil.6=rsa\u89E3\u5BC6\u51FA\u9519
+secretutil.7=\u6784\u5EFA\u4E09\u91CDDES\u5BC6\u5319\u51FA\u9519
+secretutil.8=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u65E0\u6CD5\u627E\u5230\u5BC6\u94A5\u7684\u914D\u7F6E\u6587\u4EF6
+secretutil.9=\u8BFB\u53D6\u52A0\u89E3\u5BC6\u914D\u7F6E\u6587\u4EF6\u51FA\u9519
+secretutil.10=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C
+secretutil.11=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7EDF\u7EF4\u62A4\u95EE\u9898
+secretutil.12=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C
+secretutil.13=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7EDF\u7EF4\u62A4\u95EE\u9898
+secretutil.14=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C[{0}]\u5B58\u5728\u5BC6\u94A5\u4E3A\u7A7A\u7684\u60C5\u51B5
+secretutil.15=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u516C\u79C1\u94A5\u5BF9\u5B58\u5728\u4E3A\u7A7A\u7684\u60C5\u51B5\uFF0C\u7248\u672C[{0}]
+secretutil.16=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u65E0\u6CD5\u627E\u5230\u52A0\u89E3\u5BC6\u914D\u7F6E
+
diff --git a/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_zh_CN.properties b/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_zh_CN.properties
new file mode 100644
index 0000000000..7a0c95ac9d
--- /dev/null
+++ b/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_zh_CN.properties
@@ -0,0 +1,58 @@
+configparser.1=\u63D2\u4EF6[{0},{1}]\u52A0\u8F7D\u5931\u8D25\uFF0C1s\u540E\u91CD\u8BD5... Exception:{2}
+configparser.2=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.3=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.4=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.5=\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25\uFF0C\u672A\u5B8C\u6210\u6307\u5B9A\u63D2\u4EF6\u52A0\u8F7D:{0}
+configparser.6=\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25,\u5B58\u5728\u91CD\u590D\u63D2\u4EF6:{0}
+
+dataxserviceutil.1=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38NoSuchAlgorithmException, [{0}]
+dataxserviceutil.2=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38InvalidKeyException, [{0}]
+dataxserviceutil.3=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38UnsupportedEncodingException, [{0}]
+
+errorrecordchecker.1=\u810F\u6570\u636E\u767E\u5206\u6BD4\u9650\u5236\u5E94\u8BE5\u5728[0.0, 1.0]\u4E4B\u95F4
+errorrecordchecker.2=\u810F\u6570\u636E\u6761\u6570\u73B0\u5728\u5E94\u8BE5\u4E3A\u975E\u8D1F\u6574\u6570
+errorrecordchecker.3=\u810F\u6570\u636E\u6761\u6570\u68C0\u67E5\u4E0D\u901A\u8FC7\uFF0C\u9650\u5236\u662F[{0}]\u6761\uFF0C\u4F46\u5B9E\u9645\u4E0A\u6355\u83B7\u4E86[{1}]\u6761.
+errorrecordchecker.4=\u810F\u6570\u636E\u767E\u5206\u6BD4\u68C0\u67E5\u4E0D\u901A\u8FC7\uFF0C\u9650\u5236\u662F[{0}]\uFF0C\u4F46\u5B9E\u9645\u4E0A\u6355\u83B7\u5230[{1}].
+
+
+errorcode.install_error=DataX\u5F15\u64CE\u5B89\u88C5\u9519\u8BEF, \u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.argument_error=DataX\u5F15\u64CE\u8FD0\u884C\u9519\u8BEF\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8E\u5185\u90E8\u7F16\u7A0B\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3 .
+errorcode.runtime_error=DataX\u5F15\u64CE\u8FD0\u884C\u8FC7\u7A0B\u51FA\u9519\uFF0C\u5177\u4F53\u539F\u56E0\u8BF7\u53C2\u770BDataX\u8FD0\u884C\u7ED3\u675F\u65F6\u7684\u9519\u8BEF\u8BCA\u65AD\u4FE1\u606F  .
+errorcode.config_error=DataX\u5F15\u64CE\u914D\u7F6E\u9519\u8BEF\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.secret_error=DataX\u5F15\u64CE\u52A0\u89E3\u5BC6\u51FA\u9519\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.hook_load_error=\u52A0\u8F7D\u5916\u90E8Hook\u51FA\u73B0\u9519\u8BEF\uFF0C\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u5F15\u8D77\u7684
+errorcode.hook_fail_error=\u6267\u884C\u5916\u90E8Hook\u51FA\u73B0\u9519\u8BEF
+errorcode.plugin_install_error=DataX\u63D2\u4EF6\u5B89\u88C5\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_not_found=DataX\u63D2\u4EF6\u914D\u7F6E\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_init_error=DataX\u63D2\u4EF6\u521D\u59CB\u5316\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_runtime_error=DataX\u63D2\u4EF6\u8FD0\u884C\u65F6\u51FA\u9519, \u5177\u4F53\u539F\u56E0\u8BF7\u53C2\u770BDataX\u8FD0\u884C\u7ED3\u675F\u65F6\u7684\u9519\u8BEF\u8BCA\u65AD\u4FE1\u606F .
+errorcode.plugin_dirty_data_limit_exceed=DataX\u4F20\u8F93\u810F\u6570\u636E\u8D85\u8FC7\u7528\u6237\u9884\u671F\uFF0C\u8BE5\u9519\u8BEF\u901A\u5E38\u662F\u7531\u4E8E\u6E90\u7AEF\u6570\u636E\u5B58\u5728\u8F83\u591A\u4E1A\u52A1\u810F\u6570\u636E\u5BFC\u81F4\uFF0C\u8BF7\u4ED4\u7EC6\u68C0\u67E5DataX\u6C47\u62A5\u7684\u810F\u6570\u636E\u65E5\u5FD7\u4FE1\u606F, \u6216\u8005\u60A8\u53EF\u4EE5\u9002\u5F53\u8C03\u5927\u810F\u6570\u636E\u9608\u503C .
+errorcode.plugin_split_error=DataX\u63D2\u4EF6\u5207\u5206\u51FA\u9519, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5404\u4E2A\u63D2\u4EF6\u7F16\u7A0B\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3
+errorcode.kill_job_timeout_error=kill \u4EFB\u52A1\u8D85\u65F6\uFF0C\u8BF7\u8054\u7CFBPE\u89E3\u51B3
+errorcode.start_taskgroup_error=taskGroup\u542F\u52A8\u5931\u8D25,\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3
+errorcode.call_datax_service_failed=\u8BF7\u6C42 DataX Service \u51FA\u9519.
+errorcode.call_remote_failed=\u8FDC\u7A0B\u8C03\u7528\u5931\u8D25
+errorcode.killed_exit_value=Job \u6536\u5230\u4E86 Kill \u547D\u4EE4.
+
+
+httpclientutil.1=\u8BF7\u6C42\u5730\u5740\uFF1A{0}, \u8BF7\u6C42\u65B9\u6CD5\uFF1A{1},STATUS CODE = {2}, Response Entity: {3}
+httpclientutil.2=\u8FDC\u7A0B\u63A5\u53E3\u8FD4\u56DE-1,\u5C06\u91CD\u8BD5
+
+
+secretutil.1=\u7CFB\u7EDF\u7F16\u7A0B\u9519\u8BEF,\u4E0D\u652F\u6301\u7684\u52A0\u5BC6\u7C7B\u578B
+secretutil.2=\u7CFB\u7EDF\u7F16\u7A0B\u9519\u8BEF,\u4E0D\u652F\u6301\u7684\u52A0\u5BC6\u7C7B\u578B
+secretutil.3=rsa\u52A0\u5BC6\u51FA\u9519
+secretutil.4=rsa\u89E3\u5BC6\u51FA\u9519
+secretutil.5=3\u91CDDES\u52A0\u5BC6\u51FA\u9519
+secretutil.6=rsa\u89E3\u5BC6\u51FA\u9519
+secretutil.7=\u6784\u5EFA\u4E09\u91CDDES\u5BC6\u5319\u51FA\u9519
+secretutil.8=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u65E0\u6CD5\u627E\u5230\u5BC6\u94A5\u7684\u914D\u7F6E\u6587\u4EF6
+secretutil.9=\u8BFB\u53D6\u52A0\u89E3\u5BC6\u914D\u7F6E\u6587\u4EF6\u51FA\u9519
+secretutil.10=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C
+secretutil.11=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7EDF\u7EF4\u62A4\u95EE\u9898
+secretutil.12=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C
+secretutil.13=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7EDF\u7EF4\u62A4\u95EE\u9898
+secretutil.14=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C[{0}]\u5B58\u5728\u5BC6\u94A5\u4E3A\u7A7A\u7684\u60C5\u51B5
+secretutil.15=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u516C\u79C1\u94A5\u5BF9\u5B58\u5728\u4E3A\u7A7A\u7684\u60C5\u51B5\uFF0C\u7248\u672C[{0}]
+secretutil.16=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u65E0\u6CD5\u627E\u5230\u52A0\u89E3\u5BC6\u914D\u7F6E
+
diff --git a/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_zh_HK.properties b/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_zh_HK.properties
new file mode 100644
index 0000000000..59ce9fd943
--- /dev/null
+++ b/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_zh_HK.properties
@@ -0,0 +1,116 @@
+configparser.1=\u63D2\u4EF6[{0},{1}]\u52A0\u8F7D\u5931\u8D25\uFF0C1s\u540E\u91CD\u8BD5... Exception:{2}
+configparser.2=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.3=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.4=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.5=\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25\uFF0C\u672A\u5B8C\u6210\u6307\u5B9A\u63D2\u4EF6\u52A0\u8F7D:{0}
+configparser.6=\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25,\u5B58\u5728\u91CD\u590D\u63D2\u4EF6:{0}
+
+dataxserviceutil.1=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38NoSuchAlgorithmException, [{0}]
+dataxserviceutil.2=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38InvalidKeyException, [{0}]
+dataxserviceutil.3=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38UnsupportedEncodingException, [{0}]
+
+errorrecordchecker.1=\u810F\u6570\u636E\u767E\u5206\u6BD4\u9650\u5236\u5E94\u8BE5\u5728[0.0, 1.0]\u4E4B\u95F4
+errorrecordchecker.2=\u810F\u6570\u636E\u6761\u6570\u73B0\u5728\u5E94\u8BE5\u4E3A\u975E\u8D1F\u6574\u6570
+errorrecordchecker.3=\u810F\u6570\u636E\u6761\u6570\u68C0\u67E5\u4E0D\u901A\u8FC7\uFF0C\u9650\u5236\u662F[{0}]\u6761\uFF0C\u4F46\u5B9E\u9645\u4E0A\u6355\u83B7\u4E86[{1}]\u6761.
+errorrecordchecker.4=\u810F\u6570\u636E\u767E\u5206\u6BD4\u68C0\u67E5\u4E0D\u901A\u8FC7\uFF0C\u9650\u5236\u662F[{0}]\uFF0C\u4F46\u5B9E\u9645\u4E0A\u6355\u83B7\u5230[{1}].
+
+
+errorcode.install_error=DataX\u5F15\u64CE\u5B89\u88C5\u9519\u8BEF, \u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.argument_error=DataX\u5F15\u64CE\u8FD0\u884C\u9519\u8BEF\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8E\u5185\u90E8\u7F16\u7A0B\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3 .
+errorcode.runtime_error=DataX\u5F15\u64CE\u8FD0\u884C\u8FC7\u7A0B\u51FA\u9519\uFF0C\u5177\u4F53\u539F\u56E0\u8BF7\u53C2\u770BDataX\u8FD0\u884C\u7ED3\u675F\u65F6\u7684\u9519\u8BEF\u8BCA\u65AD\u4FE1\u606F  .
+errorcode.config_error=DataX\u5F15\u64CE\u914D\u7F6E\u9519\u8BEF\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.secret_error=DataX\u5F15\u64CE\u52A0\u89E3\u5BC6\u51FA\u9519\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.hook_load_error=\u52A0\u8F7D\u5916\u90E8Hook\u51FA\u73B0\u9519\u8BEF\uFF0C\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u5F15\u8D77\u7684
+errorcode.hook_fail_error=\u6267\u884C\u5916\u90E8Hook\u51FA\u73B0\u9519\u8BEF
+errorcode.plugin_install_error=DataX\u63D2\u4EF6\u5B89\u88C5\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_not_found=DataX\u63D2\u4EF6\u914D\u7F6E\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_init_error=DataX\u63D2\u4EF6\u521D\u59CB\u5316\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_runtime_error=DataX\u63D2\u4EF6\u8FD0\u884C\u65F6\u51FA\u9519, \u5177\u4F53\u539F\u56E0\u8BF7\u53C2\u770BDataX\u8FD0\u884C\u7ED3\u675F\u65F6\u7684\u9519\u8BEF\u8BCA\u65AD\u4FE1\u606F .
+errorcode.plugin_dirty_data_limit_exceed=DataX\u4F20\u8F93\u810F\u6570\u636E\u8D85\u8FC7\u7528\u6237\u9884\u671F\uFF0C\u8BE5\u9519\u8BEF\u901A\u5E38\u662F\u7531\u4E8E\u6E90\u7AEF\u6570\u636E\u5B58\u5728\u8F83\u591A\u4E1A\u52A1\u810F\u6570\u636E\u5BFC\u81F4\uFF0C\u8BF7\u4ED4\u7EC6\u68C0\u67E5DataX\u6C47\u62A5\u7684\u810F\u6570\u636E\u65E5\u5FD7\u4FE1\u606F, \u6216\u8005\u60A8\u53EF\u4EE5\u9002\u5F53\u8C03\u5927\u810F\u6570\u636E\u9608\u503C .
+errorcode.plugin_split_error=DataX\u63D2\u4EF6\u5207\u5206\u51FA\u9519, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5404\u4E2A\u63D2\u4EF6\u7F16\u7A0B\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3
+errorcode.kill_job_timeout_error=kill \u4EFB\u52A1\u8D85\u65F6\uFF0C\u8BF7\u8054\u7CFBPE\u89E3\u51B3
+errorcode.start_taskgroup_error=taskGroup\u542F\u52A8\u5931\u8D25,\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3
+errorcode.call_datax_service_failed=\u8BF7\u6C42 DataX Service \u51FA\u9519.
+errorcode.call_remote_failed=\u8FDC\u7A0B\u8C03\u7528\u5931\u8D25
+errorcode.killed_exit_value=Job \u6536\u5230\u4E86 Kill \u547D\u4EE4.
+
+
+httpclientutil.1=\u8BF7\u6C42\u5730\u5740\uFF1A{0}, \u8BF7\u6C42\u65B9\u6CD5\uFF1A{1},STATUS CODE = {2}, Response Entity: {3}
+httpclientutil.2=\u8FDC\u7A0B\u63A5\u53E3\u8FD4\u56DE-1,\u5C06\u91CD\u8BD5
+
+
+secretutil.1=\u7CFB\u7EDF\u7F16\u7A0B\u9519\u8BEF,\u4E0D\u652F\u6301\u7684\u52A0\u5BC6\u7C7B\u578B
+secretutil.2=\u7CFB\u7EDF\u7F16\u7A0B\u9519\u8BEF,\u4E0D\u652F\u6301\u7684\u52A0\u5BC6\u7C7B\u578B
+secretutil.3=rsa\u52A0\u5BC6\u51FA\u9519
+secretutil.4=rsa\u89E3\u5BC6\u51FA\u9519
+secretutil.5=3\u91CDDES\u52A0\u5BC6\u51FA\u9519
+secretutil.6=rsa\u89E3\u5BC6\u51FA\u9519
+secretutil.7=\u6784\u5EFA\u4E09\u91CDDES\u5BC6\u5319\u51FA\u9519
+secretutil.8=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u65E0\u6CD5\u627E\u5230\u5BC6\u94A5\u7684\u914D\u7F6E\u6587\u4EF6
+secretutil.9=\u8BFB\u53D6\u52A0\u89E3\u5BC6\u914D\u7F6E\u6587\u4EF6\u51FA\u9519
+secretutil.10=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C
+secretutil.11=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7EDF\u7EF4\u62A4\u95EE\u9898
+secretutil.12=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C
+secretutil.13=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7EDF\u7EF4\u62A4\u95EE\u9898
+secretutil.14=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C[{0}]\u5B58\u5728\u5BC6\u94A5\u4E3A\u7A7A\u7684\u60C5\u51B5
+secretutil.15=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u516C\u79C1\u94A5\u5BF9\u5B58\u5728\u4E3A\u7A7A\u7684\u60C5\u51B5\uFF0C\u7248\u672C[{0}]
+secretutil.16=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u65E0\u6CD5\u627E\u5230\u52A0\u89E3\u5BC6\u914D\u7F6E
+
+configparser.1=\u5916\u639B\u7A0B\u5F0F[{0},{1}]\u8F09\u5165\u5931\u6557\uFF0C1s\u5F8C\u91CD\u8A66... Exception:{2}
+configparser.2=\u7372\u53D6\u4F5C\u696D\u914D\u7F6E\u8CC7\u8A0A\u5931\u6557:{0}
+configparser.3=\u7372\u53D6\u4F5C\u696D\u914D\u7F6E\u8CC7\u8A0A\u5931\u6557:{0}
+configparser.4=\u7372\u53D6\u4F5C\u696D\u914D\u7F6E\u8CC7\u8A0A\u5931\u6557:{0}
+configparser.5=\u5916\u639B\u7A0B\u5F0F\u8F09\u5165\u5931\u6557\uFF0C\u672A\u5B8C\u6210\u6307\u5B9A\u5916\u639B\u7A0B\u5F0F\u8F09\u5165:{0}
+configparser.6=\u5916\u639B\u7A0B\u5F0F\u8F09\u5165\u5931\u6557,\u5B58\u5728\u91CD\u8907\u5916\u639B\u7A0B\u5F0F:{0}
+
+dataxserviceutil.1=\u5EFA\u7ACB\u7C3D\u540D\u7570\u5E38NoSuchAlgorithmException, [{0}]
+dataxserviceutil.2=\u5EFA\u7ACB\u7C3D\u540D\u7570\u5E38InvalidKeyException, [{0}]
+dataxserviceutil.3=\u5EFA\u7ACB\u7C3D\u540D\u7570\u5E38UnsupportedEncodingException, [{0}]
+
+errorrecordchecker.1=\u9AD2\u6578\u64DA\u767E\u5206\u6BD4\u9650\u5236\u61C9\u8A72\u5728[0.0, 1.0]\u4E4B\u9593
+errorrecordchecker.2=\u9AD2\u6578\u64DA\u689D\u6578\u73FE\u5728\u61C9\u8A72\u70BA\u975E\u8CA0\u6574\u6578
+errorrecordchecker.3=\u9AD2\u6578\u64DA\u689D\u6578\u6AA2\u67E5\u4E0D\u901A\u904E\uFF0C\u9650\u5236\u662F[{0}]\u689D\uFF0C\u4F46\u5BE6\u969B\u4E0A\u6355\u7372\u4E86[{1}]\u689D.
+errorrecordchecker.4=\u9AD2\u6578\u64DA\u767E\u5206\u6BD4\u6AA2\u67E5\u4E0D\u901A\u904E\uFF0C\u9650\u5236\u662F[{0}]\uFF0C\u4F46\u5BE6\u969B\u4E0A\u6355\u7372\u5230[{1}].
+
+
+errorcode.install_error=DataX\u5F15\u64CE\u5B89\u88DD\u932F\u8AA4, \u8ACB\u806F\u7D61\u60A8\u7684\u904B\u7DAD\u89E3\u6C7A .
+errorcode.argument_error=DataX\u5F15\u64CE\u904B\u884C\u932F\u8AA4\uFF0C\u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BC\u5167\u90E8\u7DE8\u7A0B\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61DataX\u958B\u767C\u5718\u968A\u89E3\u6C7A .
+errorcode.runtime_error=DataX\u5F15\u64CE\u904B\u884C\u904E\u7A0B\u51FA\u932F\uFF0C\u5177\u9AD4\u539F\u56E0\u8ACB\u53C3\u770BDataX\u904B\u884C\u7D50\u675F\u6642\u7684\u932F\u8AA4\u8A3A\u65B7\u8CC7\u8A0A  .
+errorcode.config_error=DataX\u5F15\u64CE\u914D\u7F6E\u932F\u8AA4\uFF0C\u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BCDataX\u5B89\u88DD\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61\u60A8\u7684\u904B\u7DAD\u89E3\u6C7A .
+errorcode.secret_error=DataX\u5F15\u64CE\u52A0\u89E3\u5BC6\u51FA\u932F\uFF0C\u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BCDataX\u5BC6\u9470\u914D\u7F6E\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61\u60A8\u7684\u904B\u7DAD\u89E3\u6C7A .
+errorcode.hook_load_error=\u8F09\u5165\u5916\u90E8Hook\u51FA\u73FE\u932F\u8AA4\uFF0C\u901A\u5E38\u662F\u7531\u65BCDataX\u5B89\u88DD\u5F15\u8D77\u7684
+errorcode.hook_fail_error=\u57F7\u884C\u5916\u90E8Hook\u51FA\u73FE\u932F\u8AA4
+errorcode.plugin_install_error=DataX\u5916\u639B\u7A0B\u5F0F\u5B89\u88DD\u932F\u8AA4, \u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BCDataX\u5B89\u88DD\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61\u60A8\u7684\u904B\u7DAD\u89E3\u6C7A .
+errorcode.plugin_not_found=DataX\u5916\u639B\u7A0B\u5F0F\u914D\u7F6E\u932F\u8AA4, \u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BCDataX\u5B89\u88DD\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61\u60A8\u7684\u904B\u7DAD\u89E3\u6C7A .
+errorcode.plugin_init_error=DataX\u5916\u639B\u7A0B\u5F0F\u521D\u59CB\u5316\u932F\u8AA4, \u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BCDataX\u5B89\u88DD\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61\u60A8\u7684\u904B\u7DAD\u89E3\u6C7A .
+errorcode.plugin_runtime_error=DataX\u5916\u639B\u7A0B\u5F0F\u904B\u884C\u6642\u51FA\u932F, \u5177\u9AD4\u539F\u56E0\u8ACB\u53C3\u770BDataX\u904B\u884C\u7D50\u675F\u6642\u7684\u932F\u8AA4\u8A3A\u65B7\u8CC7\u8A0A .
+errorcode.plugin_dirty_data_limit_exceed=DataX\u50B3\u8F38\u9AD2\u6578\u64DA\u8D85\u904E\u7528\u6236\u9810\u671F\uFF0C\u8A72\u932F\u8AA4\u901A\u5E38\u662F\u7531\u65BC\u6E90\u7AEF\u6578\u64DA\u5B58\u5728\u8F03\u591A\u696D\u52D9\u9AD2\u6578\u64DA\u5C0E\u81F4\uFF0C\u8ACB\u4ED4\u7D30\u6AA2\u67E5DataX\u5F59\u5831\u7684\u9AD2\u6578\u64DA\u65E5\u8A8C\u8CC7\u8A0A, \u6216\u8005\u60A8\u53EF\u4EE5\u9069\u7576\u8ABF\u5927\u9AD2\u6578\u64DA\u95BE\u503C .
+errorcode.plugin_split_error=DataX\u5916\u639B\u7A0B\u5F0F\u5207\u5206\u51FA\u932F, \u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BCDataX\u5404\u500B\u5916\u639B\u7A0B\u5F0F\u7DE8\u7A0B\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61DataX\u958B\u767C\u5718\u968A\u89E3\u6C7A
+errorcode.kill_job_timeout_error=kill \u4EFB\u52D9\u903E\u6642\uFF0C\u8ACB\u806F\u7D61PE\u89E3\u6C7A
+errorcode.start_taskgroup_error=taskGroup\u555F\u52D5\u5931\u6557,\u8ACB\u806F\u7D61DataX\u958B\u767C\u5718\u968A\u89E3\u6C7A
+errorcode.call_datax_service_failed=\u8ACB\u6C42 DataX Service \u51FA\u932F.
+errorcode.call_remote_failed=\u9060\u7A0B\u8ABF\u7528\u5931\u6557
+errorcode.killed_exit_value=Job \u6536\u5230\u4E86 Kill \u547D\u4EE4.
+
+
+httpclientutil.1=\u8ACB\u6C42\u5730\u5740\uFF1A{0}, \u8ACB\u6C42\u65B9\u6CD5\uFF1A{1},STATUS CODE = {2}, Response Entity: {3}
+httpclientutil.2=\u9060\u7A0B\u63A5\u53E3\u8FD4\u56DE-1,\u5C07\u91CD\u8A66
+
+
+secretutil.1=\u7CFB\u7D71\u7DE8\u7A0B\u932F\u8AA4,\u4E0D\u652F\u63F4\u7684\u52A0\u5BC6\u985E\u578B
+secretutil.2=\u7CFB\u7D71\u7DE8\u7A0B\u932F\u8AA4,\u4E0D\u652F\u63F4\u7684\u52A0\u5BC6\u985E\u578B
+secretutil.3=rsa\u52A0\u5BC6\u51FA\u932F
+secretutil.4=rsa\u89E3\u5BC6\u51FA\u932F
+secretutil.5=3\u91CDDES\u52A0\u5BC6\u51FA\u932F
+secretutil.6=rsa\u89E3\u5BC6\u51FA\u932F
+secretutil.7=\u69CB\u5EFA\u4E09\u91CDDES\u5BC6\u5319\u51FA\u932F
+secretutil.8=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u7121\u6CD5\u627E\u5230\u5BC6\u9470\u7684\u914D\u7F6E\u6A94\u6848
+secretutil.9=\u8B80\u53D6\u52A0\u89E3\u5BC6\u914D\u7F6E\u6A94\u6848\u51FA\u932F
+secretutil.10=DataX\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C\u70BA[{0}]\uFF0C\u4F46\u5728\u7CFB\u7D71\u4E2D\u6C92\u6709\u914D\u7F6E\uFF0C\u4EFB\u52D9\u5BC6\u9470\u914D\u7F6E\u932F\u8AA4\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C
+secretutil.11=DataX\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C\u70BA[{0}]\uFF0C\u4F46\u5728\u7CFB\u7D71\u4E2D\u6C92\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52D9\u5BC6\u9470\u914D\u7F6E\u932F\u8AA4\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7D71\u7DAD\u8B77\u554F\u984C
+secretutil.12=DataX\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C\u70BA[{0}]\uFF0C\u4F46\u5728\u7CFB\u7D71\u4E2D\u6C92\u6709\u914D\u7F6E\uFF0C\u4EFB\u52D9\u5BC6\u9470\u914D\u7F6E\u932F\u8AA4\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C
+secretutil.13=DataX\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C\u70BA[{0}]\uFF0C\u4F46\u5728\u7CFB\u7D71\u4E2D\u6C92\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52D9\u5BC6\u9470\u914D\u7F6E\u932F\u8AA4\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7D71\u7DAD\u8B77\u554F\u984C
+secretutil.14=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C[{0}]\u5B58\u5728\u5BC6\u9470\u70BA\u7A7A\u7684\u60C5\u6CC1
+secretutil.15=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u516C\u79C1\u9470\u5C0D\u5B58\u5728\u70BA\u7A7A\u7684\u60C5\u6CC1\uFF0C\u7248\u672C[{0}]
+secretutil.16=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u7121\u6CD5\u627E\u5230\u52A0\u89E3\u5BC6\u914D\u7F6E
+
diff --git a/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_zh_TW.properties b/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_zh_TW.properties
new file mode 100644
index 0000000000..59ce9fd943
--- /dev/null
+++ b/core/src/main/java/com/alibaba/datax/core/util/LocalStrings_zh_TW.properties
@@ -0,0 +1,116 @@
+configparser.1=\u63D2\u4EF6[{0},{1}]\u52A0\u8F7D\u5931\u8D25\uFF0C1s\u540E\u91CD\u8BD5... Exception:{2}
+configparser.2=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.3=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.4=\u83B7\u53D6\u4F5C\u4E1A\u914D\u7F6E\u4FE1\u606F\u5931\u8D25:{0}
+configparser.5=\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25\uFF0C\u672A\u5B8C\u6210\u6307\u5B9A\u63D2\u4EF6\u52A0\u8F7D:{0}
+configparser.6=\u63D2\u4EF6\u52A0\u8F7D\u5931\u8D25,\u5B58\u5728\u91CD\u590D\u63D2\u4EF6:{0}
+
+dataxserviceutil.1=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38NoSuchAlgorithmException, [{0}]
+dataxserviceutil.2=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38InvalidKeyException, [{0}]
+dataxserviceutil.3=\u521B\u5EFA\u7B7E\u540D\u5F02\u5E38UnsupportedEncodingException, [{0}]
+
+errorrecordchecker.1=\u810F\u6570\u636E\u767E\u5206\u6BD4\u9650\u5236\u5E94\u8BE5\u5728[0.0, 1.0]\u4E4B\u95F4
+errorrecordchecker.2=\u810F\u6570\u636E\u6761\u6570\u73B0\u5728\u5E94\u8BE5\u4E3A\u975E\u8D1F\u6574\u6570
+errorrecordchecker.3=\u810F\u6570\u636E\u6761\u6570\u68C0\u67E5\u4E0D\u901A\u8FC7\uFF0C\u9650\u5236\u662F[{0}]\u6761\uFF0C\u4F46\u5B9E\u9645\u4E0A\u6355\u83B7\u4E86[{1}]\u6761.
+errorrecordchecker.4=\u810F\u6570\u636E\u767E\u5206\u6BD4\u68C0\u67E5\u4E0D\u901A\u8FC7\uFF0C\u9650\u5236\u662F[{0}]\uFF0C\u4F46\u5B9E\u9645\u4E0A\u6355\u83B7\u5230[{1}].
+
+
+errorcode.install_error=DataX\u5F15\u64CE\u5B89\u88C5\u9519\u8BEF, \u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.argument_error=DataX\u5F15\u64CE\u8FD0\u884C\u9519\u8BEF\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8E\u5185\u90E8\u7F16\u7A0B\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3 .
+errorcode.runtime_error=DataX\u5F15\u64CE\u8FD0\u884C\u8FC7\u7A0B\u51FA\u9519\uFF0C\u5177\u4F53\u539F\u56E0\u8BF7\u53C2\u770BDataX\u8FD0\u884C\u7ED3\u675F\u65F6\u7684\u9519\u8BEF\u8BCA\u65AD\u4FE1\u606F  .
+errorcode.config_error=DataX\u5F15\u64CE\u914D\u7F6E\u9519\u8BEF\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.secret_error=DataX\u5F15\u64CE\u52A0\u89E3\u5BC6\u51FA\u9519\uFF0C\u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.hook_load_error=\u52A0\u8F7D\u5916\u90E8Hook\u51FA\u73B0\u9519\u8BEF\uFF0C\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u5F15\u8D77\u7684
+errorcode.hook_fail_error=\u6267\u884C\u5916\u90E8Hook\u51FA\u73B0\u9519\u8BEF
+errorcode.plugin_install_error=DataX\u63D2\u4EF6\u5B89\u88C5\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_not_found=DataX\u63D2\u4EF6\u914D\u7F6E\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_init_error=DataX\u63D2\u4EF6\u521D\u59CB\u5316\u9519\u8BEF, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5B89\u88C5\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFB\u60A8\u7684\u8FD0\u7EF4\u89E3\u51B3 .
+errorcode.plugin_runtime_error=DataX\u63D2\u4EF6\u8FD0\u884C\u65F6\u51FA\u9519, \u5177\u4F53\u539F\u56E0\u8BF7\u53C2\u770BDataX\u8FD0\u884C\u7ED3\u675F\u65F6\u7684\u9519\u8BEF\u8BCA\u65AD\u4FE1\u606F .
+errorcode.plugin_dirty_data_limit_exceed=DataX\u4F20\u8F93\u810F\u6570\u636E\u8D85\u8FC7\u7528\u6237\u9884\u671F\uFF0C\u8BE5\u9519\u8BEF\u901A\u5E38\u662F\u7531\u4E8E\u6E90\u7AEF\u6570\u636E\u5B58\u5728\u8F83\u591A\u4E1A\u52A1\u810F\u6570\u636E\u5BFC\u81F4\uFF0C\u8BF7\u4ED4\u7EC6\u68C0\u67E5DataX\u6C47\u62A5\u7684\u810F\u6570\u636E\u65E5\u5FD7\u4FE1\u606F, \u6216\u8005\u60A8\u53EF\u4EE5\u9002\u5F53\u8C03\u5927\u810F\u6570\u636E\u9608\u503C .
+errorcode.plugin_split_error=DataX\u63D2\u4EF6\u5207\u5206\u51FA\u9519, \u8BE5\u95EE\u9898\u901A\u5E38\u662F\u7531\u4E8EDataX\u5404\u4E2A\u63D2\u4EF6\u7F16\u7A0B\u9519\u8BEF\u5F15\u8D77\uFF0C\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3
+errorcode.kill_job_timeout_error=kill \u4EFB\u52A1\u8D85\u65F6\uFF0C\u8BF7\u8054\u7CFBPE\u89E3\u51B3
+errorcode.start_taskgroup_error=taskGroup\u542F\u52A8\u5931\u8D25,\u8BF7\u8054\u7CFBDataX\u5F00\u53D1\u56E2\u961F\u89E3\u51B3
+errorcode.call_datax_service_failed=\u8BF7\u6C42 DataX Service \u51FA\u9519.
+errorcode.call_remote_failed=\u8FDC\u7A0B\u8C03\u7528\u5931\u8D25
+errorcode.killed_exit_value=Job \u6536\u5230\u4E86 Kill \u547D\u4EE4.
+
+
+httpclientutil.1=\u8BF7\u6C42\u5730\u5740\uFF1A{0}, \u8BF7\u6C42\u65B9\u6CD5\uFF1A{1},STATUS CODE = {2}, Response Entity: {3}
+httpclientutil.2=\u8FDC\u7A0B\u63A5\u53E3\u8FD4\u56DE-1,\u5C06\u91CD\u8BD5
+
+
+secretutil.1=\u7CFB\u7EDF\u7F16\u7A0B\u9519\u8BEF,\u4E0D\u652F\u6301\u7684\u52A0\u5BC6\u7C7B\u578B
+secretutil.2=\u7CFB\u7EDF\u7F16\u7A0B\u9519\u8BEF,\u4E0D\u652F\u6301\u7684\u52A0\u5BC6\u7C7B\u578B
+secretutil.3=rsa\u52A0\u5BC6\u51FA\u9519
+secretutil.4=rsa\u89E3\u5BC6\u51FA\u9519
+secretutil.5=3\u91CDDES\u52A0\u5BC6\u51FA\u9519
+secretutil.6=rsa\u89E3\u5BC6\u51FA\u9519
+secretutil.7=\u6784\u5EFA\u4E09\u91CDDES\u5BC6\u5319\u51FA\u9519
+secretutil.8=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u65E0\u6CD5\u627E\u5230\u5BC6\u94A5\u7684\u914D\u7F6E\u6587\u4EF6
+secretutil.9=\u8BFB\u53D6\u52A0\u89E3\u5BC6\u914D\u7F6E\u6587\u4EF6\u51FA\u9519
+secretutil.10=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C
+secretutil.11=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7EDF\u7EF4\u62A4\u95EE\u9898
+secretutil.12=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C
+secretutil.13=DataX\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C\u4E3A[{0}]\uFF0C\u4F46\u5728\u7CFB\u7EDF\u4E2D\u6CA1\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52A1\u5BC6\u94A5\u914D\u7F6E\u9519\u8BEF\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7EDF\u7EF4\u62A4\u95EE\u9898
+secretutil.14=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u5BC6\u94A5\u7248\u672C[{0}]\u5B58\u5728\u5BC6\u94A5\u4E3A\u7A7A\u7684\u60C5\u51B5
+secretutil.15=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u516C\u79C1\u94A5\u5BF9\u5B58\u5728\u4E3A\u7A7A\u7684\u60C5\u51B5\uFF0C\u7248\u672C[{0}]
+secretutil.16=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u65E0\u6CD5\u627E\u5230\u52A0\u89E3\u5BC6\u914D\u7F6E
+
+configparser.1=\u5916\u639B\u7A0B\u5F0F[{0},{1}]\u8F09\u5165\u5931\u6557\uFF0C1s\u5F8C\u91CD\u8A66... Exception:{2}
+configparser.2=\u7372\u53D6\u4F5C\u696D\u914D\u7F6E\u8CC7\u8A0A\u5931\u6557:{0}
+configparser.3=\u7372\u53D6\u4F5C\u696D\u914D\u7F6E\u8CC7\u8A0A\u5931\u6557:{0}
+configparser.4=\u7372\u53D6\u4F5C\u696D\u914D\u7F6E\u8CC7\u8A0A\u5931\u6557:{0}
+configparser.5=\u5916\u639B\u7A0B\u5F0F\u8F09\u5165\u5931\u6557\uFF0C\u672A\u5B8C\u6210\u6307\u5B9A\u5916\u639B\u7A0B\u5F0F\u8F09\u5165:{0}
+configparser.6=\u5916\u639B\u7A0B\u5F0F\u8F09\u5165\u5931\u6557,\u5B58\u5728\u91CD\u8907\u5916\u639B\u7A0B\u5F0F:{0}
+
+dataxserviceutil.1=\u5EFA\u7ACB\u7C3D\u540D\u7570\u5E38NoSuchAlgorithmException, [{0}]
+dataxserviceutil.2=\u5EFA\u7ACB\u7C3D\u540D\u7570\u5E38InvalidKeyException, [{0}]
+dataxserviceutil.3=\u5EFA\u7ACB\u7C3D\u540D\u7570\u5E38UnsupportedEncodingException, [{0}]
+
+errorrecordchecker.1=\u9AD2\u6578\u64DA\u767E\u5206\u6BD4\u9650\u5236\u61C9\u8A72\u5728[0.0, 1.0]\u4E4B\u9593
+errorrecordchecker.2=\u9AD2\u6578\u64DA\u689D\u6578\u73FE\u5728\u61C9\u8A72\u70BA\u975E\u8CA0\u6574\u6578
+errorrecordchecker.3=\u9AD2\u6578\u64DA\u689D\u6578\u6AA2\u67E5\u4E0D\u901A\u904E\uFF0C\u9650\u5236\u662F[{0}]\u689D\uFF0C\u4F46\u5BE6\u969B\u4E0A\u6355\u7372\u4E86[{1}]\u689D.
+errorrecordchecker.4=\u9AD2\u6578\u64DA\u767E\u5206\u6BD4\u6AA2\u67E5\u4E0D\u901A\u904E\uFF0C\u9650\u5236\u662F[{0}]\uFF0C\u4F46\u5BE6\u969B\u4E0A\u6355\u7372\u5230[{1}].
+
+
+errorcode.install_error=DataX\u5F15\u64CE\u5B89\u88DD\u932F\u8AA4, \u8ACB\u806F\u7D61\u60A8\u7684\u904B\u7DAD\u89E3\u6C7A .
+errorcode.argument_error=DataX\u5F15\u64CE\u904B\u884C\u932F\u8AA4\uFF0C\u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BC\u5167\u90E8\u7DE8\u7A0B\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61DataX\u958B\u767C\u5718\u968A\u89E3\u6C7A .
+errorcode.runtime_error=DataX\u5F15\u64CE\u904B\u884C\u904E\u7A0B\u51FA\u932F\uFF0C\u5177\u9AD4\u539F\u56E0\u8ACB\u53C3\u770BDataX\u904B\u884C\u7D50\u675F\u6642\u7684\u932F\u8AA4\u8A3A\u65B7\u8CC7\u8A0A  .
+errorcode.config_error=DataX\u5F15\u64CE\u914D\u7F6E\u932F\u8AA4\uFF0C\u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BCDataX\u5B89\u88DD\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61\u60A8\u7684\u904B\u7DAD\u89E3\u6C7A .
+errorcode.secret_error=DataX\u5F15\u64CE\u52A0\u89E3\u5BC6\u51FA\u932F\uFF0C\u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BCDataX\u5BC6\u9470\u914D\u7F6E\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61\u60A8\u7684\u904B\u7DAD\u89E3\u6C7A .
+errorcode.hook_load_error=\u8F09\u5165\u5916\u90E8Hook\u51FA\u73FE\u932F\u8AA4\uFF0C\u901A\u5E38\u662F\u7531\u65BCDataX\u5B89\u88DD\u5F15\u8D77\u7684
+errorcode.hook_fail_error=\u57F7\u884C\u5916\u90E8Hook\u51FA\u73FE\u932F\u8AA4
+errorcode.plugin_install_error=DataX\u5916\u639B\u7A0B\u5F0F\u5B89\u88DD\u932F\u8AA4, \u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BCDataX\u5B89\u88DD\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61\u60A8\u7684\u904B\u7DAD\u89E3\u6C7A .
+errorcode.plugin_not_found=DataX\u5916\u639B\u7A0B\u5F0F\u914D\u7F6E\u932F\u8AA4, \u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BCDataX\u5B89\u88DD\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61\u60A8\u7684\u904B\u7DAD\u89E3\u6C7A .
+errorcode.plugin_init_error=DataX\u5916\u639B\u7A0B\u5F0F\u521D\u59CB\u5316\u932F\u8AA4, \u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BCDataX\u5B89\u88DD\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61\u60A8\u7684\u904B\u7DAD\u89E3\u6C7A .
+errorcode.plugin_runtime_error=DataX\u5916\u639B\u7A0B\u5F0F\u904B\u884C\u6642\u51FA\u932F, \u5177\u9AD4\u539F\u56E0\u8ACB\u53C3\u770BDataX\u904B\u884C\u7D50\u675F\u6642\u7684\u932F\u8AA4\u8A3A\u65B7\u8CC7\u8A0A .
+errorcode.plugin_dirty_data_limit_exceed=DataX\u50B3\u8F38\u9AD2\u6578\u64DA\u8D85\u904E\u7528\u6236\u9810\u671F\uFF0C\u8A72\u932F\u8AA4\u901A\u5E38\u662F\u7531\u65BC\u6E90\u7AEF\u6578\u64DA\u5B58\u5728\u8F03\u591A\u696D\u52D9\u9AD2\u6578\u64DA\u5C0E\u81F4\uFF0C\u8ACB\u4ED4\u7D30\u6AA2\u67E5DataX\u5F59\u5831\u7684\u9AD2\u6578\u64DA\u65E5\u8A8C\u8CC7\u8A0A, \u6216\u8005\u60A8\u53EF\u4EE5\u9069\u7576\u8ABF\u5927\u9AD2\u6578\u64DA\u95BE\u503C .
+errorcode.plugin_split_error=DataX\u5916\u639B\u7A0B\u5F0F\u5207\u5206\u51FA\u932F, \u8A72\u554F\u984C\u901A\u5E38\u662F\u7531\u65BCDataX\u5404\u500B\u5916\u639B\u7A0B\u5F0F\u7DE8\u7A0B\u932F\u8AA4\u5F15\u8D77\uFF0C\u8ACB\u806F\u7D61DataX\u958B\u767C\u5718\u968A\u89E3\u6C7A
+errorcode.kill_job_timeout_error=kill \u4EFB\u52D9\u903E\u6642\uFF0C\u8ACB\u806F\u7D61PE\u89E3\u6C7A
+errorcode.start_taskgroup_error=taskGroup\u555F\u52D5\u5931\u6557,\u8ACB\u806F\u7D61DataX\u958B\u767C\u5718\u968A\u89E3\u6C7A
+errorcode.call_datax_service_failed=\u8ACB\u6C42 DataX Service \u51FA\u932F.
+errorcode.call_remote_failed=\u9060\u7A0B\u8ABF\u7528\u5931\u6557
+errorcode.killed_exit_value=Job \u6536\u5230\u4E86 Kill \u547D\u4EE4.
+
+
+httpclientutil.1=\u8ACB\u6C42\u5730\u5740\uFF1A{0}, \u8ACB\u6C42\u65B9\u6CD5\uFF1A{1},STATUS CODE = {2}, Response Entity: {3}
+httpclientutil.2=\u9060\u7A0B\u63A5\u53E3\u8FD4\u56DE-1,\u5C07\u91CD\u8A66
+
+
+secretutil.1=\u7CFB\u7D71\u7DE8\u7A0B\u932F\u8AA4,\u4E0D\u652F\u63F4\u7684\u52A0\u5BC6\u985E\u578B
+secretutil.2=\u7CFB\u7D71\u7DE8\u7A0B\u932F\u8AA4,\u4E0D\u652F\u63F4\u7684\u52A0\u5BC6\u985E\u578B
+secretutil.3=rsa\u52A0\u5BC6\u51FA\u932F
+secretutil.4=rsa\u89E3\u5BC6\u51FA\u932F
+secretutil.5=3\u91CDDES\u52A0\u5BC6\u51FA\u932F
+secretutil.6=rsa\u89E3\u5BC6\u51FA\u932F
+secretutil.7=\u69CB\u5EFA\u4E09\u91CDDES\u5BC6\u5319\u51FA\u932F
+secretutil.8=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u7121\u6CD5\u627E\u5230\u5BC6\u9470\u7684\u914D\u7F6E\u6A94\u6848
+secretutil.9=\u8B80\u53D6\u52A0\u89E3\u5BC6\u914D\u7F6E\u6A94\u6848\u51FA\u932F
+secretutil.10=DataX\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C\u70BA[{0}]\uFF0C\u4F46\u5728\u7CFB\u7D71\u4E2D\u6C92\u6709\u914D\u7F6E\uFF0C\u4EFB\u52D9\u5BC6\u9470\u914D\u7F6E\u932F\u8AA4\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C
+secretutil.11=DataX\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C\u70BA[{0}]\uFF0C\u4F46\u5728\u7CFB\u7D71\u4E2D\u6C92\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52D9\u5BC6\u9470\u914D\u7F6E\u932F\u8AA4\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7D71\u7DAD\u8B77\u554F\u984C
+secretutil.12=DataX\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C\u70BA[{0}]\uFF0C\u4F46\u5728\u7CFB\u7D71\u4E2D\u6C92\u6709\u914D\u7F6E\uFF0C\u4EFB\u52D9\u5BC6\u9470\u914D\u7F6E\u932F\u8AA4\uFF0C\u4E0D\u5B58\u5728\u60A8\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C
+secretutil.13=DataX\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C\u70BA[{0}]\uFF0C\u4F46\u5728\u7CFB\u7D71\u4E2D\u6C92\u6709\u914D\u7F6E\uFF0C\u53EF\u80FD\u662F\u4EFB\u52D9\u5BC6\u9470\u914D\u7F6E\u932F\u8AA4\uFF0C\u4E5F\u53EF\u80FD\u662F\u7CFB\u7D71\u7DAD\u8B77\u554F\u984C
+secretutil.14=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u5BC6\u9470\u7248\u672C[{0}]\u5B58\u5728\u5BC6\u9470\u70BA\u7A7A\u7684\u60C5\u6CC1
+secretutil.15=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u914D\u7F6E\u7684\u516C\u79C1\u9470\u5C0D\u5B58\u5728\u70BA\u7A7A\u7684\u60C5\u6CC1\uFF0C\u7248\u672C[{0}]
+secretutil.16=DataX\u914D\u7F6E\u8981\u6C42\u52A0\u89E3\u5BC6\uFF0C\u4F46\u7121\u6CD5\u627E\u5230\u52A0\u89E3\u5BC6\u914D\u7F6E
+
diff --git a/core/src/main/java/com/alibaba/datax/core/util/container/CoreConstant.java b/core/src/main/java/com/alibaba/datax/core/util/container/CoreConstant.java
index 6a0b6205e2..a1ca164d94 100755
--- a/core/src/main/java/com/alibaba/datax/core/util/container/CoreConstant.java
+++ b/core/src/main/java/com/alibaba/datax/core/util/container/CoreConstant.java
@@ -105,7 +105,7 @@ public class CoreConstant {
 
     public static final String DATAX_JOB_POSTHANDLER_PLUGINNAME = "job.postHandler.pluginName";
     // ----------------------------- 局部使用的变量
-    public static final String JOB_WRITER = "reader";
+    public static final String JOB_WRITER = "writer";
 
 	public static final String JOB_READER = "reader";
 
diff --git a/core/src/main/java/com/alibaba/datax/core/util/container/JarLoader.java b/core/src/main/java/com/alibaba/datax/core/util/container/JarLoader.java
index 9fc113dc6a..ddf22baef6 100755
--- a/core/src/main/java/com/alibaba/datax/core/util/container/JarLoader.java
+++ b/core/src/main/java/com/alibaba/datax/core/util/container/JarLoader.java
@@ -15,7 +15,7 @@
 /**
  * 提供Jar隔离的加载机制,会把传入的路径、及其子路径、以及路径中的jar文件加入到class path。
  */
-public class JarLoader extends URLClassLoader {
+public class JarLoader extends URLClassLoader{
     public JarLoader(String[] paths) {
         this(paths, JarLoader.class.getClassLoader());
     }
diff --git a/core/src/main/java/com/alibaba/datax/core/util/container/LoadUtil.java b/core/src/main/java/com/alibaba/datax/core/util/container/LoadUtil.java
index 30e926c385..9a6a830291 100755
--- a/core/src/main/java/com/alibaba/datax/core/util/container/LoadUtil.java
+++ b/core/src/main/java/com/alibaba/datax/core/util/container/LoadUtil.java
@@ -49,7 +49,7 @@ public String value() {
     /**
      * jarLoader的缓冲
      */
-    private static Map jarLoaderCenter = new HashMap();
+    private static Map jarLoaderCenter = new HashMap();
 
     /**
      * 设置pluginConfigs,方便后面插件来获取
diff --git a/core/src/main/job/job.json b/core/src/main/job/job.json
index 582065929a..ad5d4a85c5 100755
--- a/core/src/main/job/job.json
+++ b/core/src/main/job/job.json
@@ -2,11 +2,10 @@
     "job": {
         "setting": {
             "speed": {
-                "byte":10485760
+                "channel": 2
             },
             "errorLimit": {
-                "record": 0,
-                "percentage": 0.02
+                "record": 0
             }
         },
         "content": [
@@ -14,17 +13,17 @@
                 "reader": {
                     "name": "streamreader",
                     "parameter": {
-                        "column" : [
+                        "column": [
                             {
                                 "value": "DataX",
                                 "type": "string"
                             },
                             {
-                                "value": 19890604,
+                                "value": 1724154616370,
                                 "type": "long"
                             },
                             {
-                                "value": "1989-06-04 00:00:00",
+                                "value": "2024-01-01 00:00:00",
                                 "type": "date"
                             },
                             {
@@ -32,11 +31,11 @@
                                 "type": "bool"
                             },
                             {
-                                "value": "test",
+                                "value": "TestRawData",
                                 "type": "bytes"
                             }
                         ],
-                        "sliceRecordCount": 100000
+                        "sliceRecordCount": 100
                     }
                 },
                 "writer": {
@@ -49,4 +48,4 @@
             }
         ]
     }
-}
+}
\ No newline at end of file
diff --git a/databendwriter/doc/databendwriter-CN.md b/databendwriter/doc/databendwriter-CN.md
new file mode 100644
index 0000000000..5b26ed7ed5
--- /dev/null
+++ b/databendwriter/doc/databendwriter-CN.md
@@ -0,0 +1,183 @@
+# DataX DatabendWriter
+[简体中文](./databendwriter-CN.md) | [English](./databendwriter.md)
+
+## 1 快速介绍
+
+Databend Writer 是一个 DataX 的插件,用于从 DataX 中写入数据到 Databend 表中。
+该插件基于[databend JDBC driver](https://github.com/databendcloud/databend-jdbc) ,它使用 [RESTful http protocol](https://databend.rs/doc/integrations/api/rest)
+在开源的 databend 和 [databend cloud](https://app.databend.com/) 上执行查询。
+
+在每个写入批次中,databend writer 将批量数据上传到内部的 S3 stage,然后执行相应的 insert SQL 将数据上传到 databend 表中。
+
+为了最佳的用户体验,如果您使用的是 databend 社区版本,您应该尝试采用 [S3](https://aws.amazon.com/s3/)/[minio](https://min.io/)/[OSS](https://www.alibabacloud.com/product/object-storage-service) 作为其底层存储层,因为
+它们支持预签名上传操作,否则您可能会在数据传输上浪费不必要的成本。
+
+您可以在[文档](https://databend.rs/doc/deploy/deploying-databend)中了解更多详细信息
+
+## 2 实现原理
+
+Databend Writer 将使用 DataX 从 DataX Reader 中获取生成的记录,并将记录批量插入到 databend 表中指定的列中。
+
+## 3 功能说明
+
+### 3.1 配置样例
+
+* 以下配置将从内存中读取一些生成的数据,并将数据上传到databend表中
+
+#### 准备工作
+```sql
+--- create table in databend
+drop table if exists datax.sample1;
+drop database if exists datax;
+create database if not exists datax;
+create table if not exsits datax.sample1(a string, b int64, c date, d timestamp, e bool, f string, g variant);
+```
+
+#### 配置样例
+```json
+{
+  "job": {
+    "content": [
+      {
+        "reader": {
+          "name": "streamreader",
+          "parameter": {
+            "column" : [
+              {
+                "value": "DataX",
+                "type": "string"
+              },
+              {
+                "value": 19880808,
+                "type": "long"
+              },
+              {
+                "value": "1926-08-08 08:08:08",
+                "type": "date"
+              },
+              {
+                "value": "1988-08-08 08:08:08",
+                "type": "date"
+              },
+              {
+                "value": true,
+                "type": "bool"
+              },
+              {
+                "value": "test",
+                "type": "bytes"
+              },
+              {
+                "value": "{\"type\": \"variant\", \"value\": \"test\"}",
+                "type": "string"
+              }
+
+            ],
+            "sliceRecordCount": 10000
+          }
+        },
+        "writer": {
+          "name": "databendwriter",
+          "parameter": {
+            "writeMode": "replace", 
+            "onConflictColumn": ["id"],
+            "username": "databend",
+            "password": "databend",
+            "column": ["a", "b", "c", "d", "e", "f", "g"],
+            "batchSize": 1000,
+            "preSql": [
+            ],
+            "postSql": [
+            ],
+            "connection": [
+              {
+                "jdbcUrl": "jdbc:databend://localhost:8000/datax",
+                "table": [
+                  "sample1"
+                ]
+              }
+            ]
+          }
+        }
+      }
+    ],
+    "setting": {
+      "speed": {
+        "channel": 1
+       }
+    }
+  }
+}
+```
+
+### 3.2 参数说明
+* jdbcUrl
+    * 描述: JDBC 数据源 url。请参阅仓库中的详细[文档](https://github.com/databendcloud/databend-jdbc)
+    * 必选: 是
+    * 默认值: 无
+    * 示例: jdbc:databend://localhost:8000/datax
+* username
+    * 描述: JDBC 数据源用户名
+    * 必选: 是
+    * 默认值: 无
+    * 示例: databend
+* password
+    * 描述: JDBC 数据源密码
+    * 必选: 是
+    * 默认值: 无
+    * 示例: databend
+* table
+    * 描述: 表名的集合,table应该包含column参数中的所有列。
+    * 必选: 是
+    * 默认值: 无
+    * 示例: ["sample1"]
+* column
+    * 描述: 表中的列名集合,字段顺序应该与reader的record中的column类型对应
+    * 必选: 是
+    * 默认值: 无
+    * 示例: ["a", "b", "c", "d", "e", "f", "g"]
+* batchSize
+    * 描述: 每个批次的记录数
+    * 必选: 否
+    * 默认值: 1000
+    * 示例: 1000
+* preSql
+    * 描述: 在写入数据之前执行的SQL语句
+    * 必选: 否
+    * 默认值: 无
+    * 示例: ["delete from datax.sample1"]
+* postSql
+    * 描述: 在写入数据之后执行的SQL语句
+    * 必选: 否
+    * 默认值: 无
+    * 示例: ["select count(*) from datax.sample1"]
+* writeMode
+    * 描述:写入模式,支持 insert 和 replace 两种模式,默认为 insert。若为 replace,务必填写 onConflictColumn 参数
+    * 必选:否
+    * 默认值:insert
+    * 示例:"replace"
+* onConflictColumn
+    * 描述:on conflict 字段,指定 writeMode 为 replace 后,需要此参数
+    * 必选:否
+    * 默认值:无
+    * 示例:["id","user"]
+
+### 3.3 类型转化
+DataX中的数据类型可以转换为databend中的相应数据类型。下表显示了两种类型之间的对应关系。
+
+| DataX 内部类型 | Databend 数据类型                                             |
+|------------|-----------------------------------------------------------|
+| INT        | TINYINT, INT8, SMALLINT, INT16, INT, INT32, BIGINT, INT64 |
+| LONG       | TINYINT, INT8, SMALLINT, INT16, INT, INT32, BIGINT, INT64 |
+| STRING     | STRING, VARCHAR                                           |
+| DOUBLE     | FLOAT, DOUBLE                                             |
+| BOOL       | BOOLEAN, BOOL                                             |
+| DATE       | DATE, TIMESTAMP                                           |
+| BYTES      | STRING, VARCHAR                                           |
+
+## 4 性能测试
+
+## 5 约束限制
+目前,复杂数据类型支持不稳定,如果您想使用复杂数据类型,例如元组,数组,请检查databend和jdbc驱动程序的进一步版本。
+
+## FAQ
\ No newline at end of file
diff --git a/databendwriter/doc/databendwriter.md b/databendwriter/doc/databendwriter.md
new file mode 100644
index 0000000000..c92d6387fd
--- /dev/null
+++ b/databendwriter/doc/databendwriter.md
@@ -0,0 +1,176 @@
+# DataX DatabendWriter
+[简体中文](./databendwriter-CN.md) | [English](./databendwriter.md)
+
+## 1 Introduction
+Databend Writer is a plugin for DataX to write data to Databend Table from dataX records.
+The plugin is based on [databend JDBC driver](https://github.com/databendcloud/databend-jdbc) which use [RESTful http protocol](https://databend.rs/doc/integrations/api/rest)
+to execute query on open source databend and [databend cloud](https://app.databend.com/).
+
+During each write batch, databend writer will upload batch data into internal S3 stage and execute corresponding insert SQL to upload data into databend table.
+
+For best user experience, if you are using databend community distribution, you should try to adopt [S3](https://aws.amazon.com/s3/)/[minio](https://min.io/)/[OSS](https://www.alibabacloud.com/product/object-storage-service) as its underlying storage layer since 
+they support presign upload operation otherwise you may expend unneeded cost on data transfer.
+
+You could see more details on the [doc](https://databend.rs/doc/deploy/deploying-databend)
+
+## 2 Detailed Implementation
+Databend Writer would use DataX to fetch records generated by DataX Reader, and then batch insert records to the designated columns for your databend table.
+
+## 3 Features
+### 3.1 Example Configurations
+* the following configuration would read some generated data in memory and upload data into databend table
+
+#### Preparation
+```sql
+--- create table in databend
+drop table if exists datax.sample1;
+drop database if exists datax;
+create database if not exists datax;
+create table if not exsits datax.sample1(a string, b int64, c date, d timestamp, e bool, f string, g variant);
+```
+
+#### Configurations
+```json
+{
+  "job": {
+    "content": [
+      {
+        "reader": {
+          "name": "streamreader",
+          "parameter": {
+            "column" : [
+              {
+                "value": "DataX",
+                "type": "string"
+              },
+              {
+                "value": 19880808,
+                "type": "long"
+              },
+              {
+                "value": "1926-08-08 08:08:08",
+                "type": "date"
+              },
+              {
+                "value": "1988-08-08 08:08:08",
+                "type": "date"
+              },
+              {
+                "value": true,
+                "type": "bool"
+              },
+              {
+                "value": "test",
+                "type": "bytes"
+              },
+              {
+                "value": "{\"type\": \"variant\", \"value\": \"test\"}",
+                "type": "string"
+              }
+
+            ],
+            "sliceRecordCount": 10000
+          }
+        },
+        "writer": {
+          "name": "databendwriter",
+          "parameter": {
+            "username": "databend",
+            "password": "databend",
+            "column": ["a", "b", "c", "d", "e", "f", "g"],
+            "batchSize": 1000,
+            "preSql": [
+            ],
+            "postSql": [
+            ],
+            "connection": [
+              {
+                "jdbcUrl": "jdbc:databend://localhost:8000/datax",
+                "table": [
+                  "sample1"
+                ]
+              }
+            ]
+          }
+        }
+      }
+    ],
+    "setting": {
+      "speed": {
+        "channel": 1
+       }
+    }
+  }
+}
+```
+
+### 3.2 Configuration Description
+* jdbcUrl
+  * Description: JDBC Data source url in Databend. Please take a look at repository for detailed [doc](https://github.com/databendcloud/databend-jdbc)
+  * Required: yes
+  * Default: none
+  * Example: jdbc:databend://localhost:8000/datax
+* username
+  * Description: Databend user name
+  * Required: yes
+  * Default: none
+  * Example: databend
+* password
+  * Description: Databend user password
+  * Required: yes
+  * Default: none
+  * Example: databend
+* table
+  * Description: A list of table names that should contain all of the columns in the column parameter.
+  * Required: yes
+  * Default: none
+  * Example: ["sample1"]
+* column
+  * Description: A list of column field names that should be inserted into the table. if you want to insert all column fields use `["*"]` instead.
+  * Required: yes
+  * Default: none
+  * Example: ["a", "b", "c", "d", "e", "f", "g"]
+* batchSize
+  * Description: The number of records to be inserted in each batch.
+  * Required: no
+  * Default: 1024
+* preSql
+  * Description: A list of SQL statements that will be executed before the write operation.
+  * Required: no
+  * Default: none
+* postSql
+  * Description: A list of SQL statements that will be executed after the write operation.
+  * Required: no
+  * Default: none
+* writeMode
+  * Description:The write mode, support `insert` and `replace` two mode.
+  * Required:no
+  * Default:insert
+  * Example:"replace"
+* onConflictColumn
+  * Description:On conflict fields list.
+  * Required:no
+  * Default:none
+  * Example:["id","user"]
+
+### 3.3 Type Convert
+Data types in datax can be converted to the corresponding data types in databend. The following table shows the correspondence between the two types.
+
+| DataX Type | Databend Type                                             |
+|------------|-----------------------------------------------------------|
+| INT        | TINYINT, INT8, SMALLINT, INT16, INT, INT32, BIGINT, INT64 |
+| LONG       | TINYINT, INT8, SMALLINT, INT16, INT, INT32, BIGINT, INT64 |
+| STRING     | STRING, VARCHAR                                           |
+| DOUBLE     | FLOAT, DOUBLE                                             |
+| BOOL       | BOOLEAN, BOOL                                             |
+| DATE       | DATE, TIMESTAMP                                           |
+| BYTES      | STRING, VARCHAR                                           |
+
+
+## 4 Performance Test
+
+
+## 5 Restrictions
+Currently, complex data type support is not stable, if you want to use complex data type such as tuple, array, please check further release version of databend and jdbc driver.
+
+## FAQ
diff --git a/databendwriter/pom.xml b/databendwriter/pom.xml
new file mode 100644
index 0000000000..b99ca5d804
--- /dev/null
+++ b/databendwriter/pom.xml
@@ -0,0 +1,101 @@
+
+
+    
+        datax-all
+        com.alibaba.datax
+        0.0.1-SNAPSHOT
+    
+
+    4.0.0
+    databendwriter
+    databendwriter
+    jar
+
+    
+        
+            com.databend
+            databend-jdbc
+            0.1.0
+        
+        
+            com.alibaba.datax
+            datax-core
+            ${datax-project-version}
+        
+        
+            com.alibaba.datax
+            datax-common
+            ${datax-project-version}
+        
+        
+            org.slf4j
+            slf4j-api
+        
+
+        
+            ch.qos.logback
+            logback-classic
+        
+
+        
+            com.alibaba.datax
+            plugin-rdbms-util
+            ${datax-project-version}
+            
+                
+                    com.google.guava
+                    guava
+                
+            
+        
+
+
+        
+            junit
+            junit
+            test
+        
+    
+    
+        
+            
+                src/main/java
+                
+                    **/*.properties
+                
+            
+        
+        
+            
+            
+                maven-compiler-plugin
+                
+                    ${jdk-version}
+                    ${jdk-version}
+                    ${project-sourceEncoding}
+                
+            
+            
+            
+                maven-assembly-plugin
+                
+                    
+                        src/main/assembly/package.xml
+                    
+                    datax
+                
+                
+                    
+                        dwzip
+                        package
+                        
+                            single
+                        
+                    
+                
+            
+        
+    
+
diff --git a/databendwriter/src/main/assembly/package.xml b/databendwriter/src/main/assembly/package.xml
new file mode 100755
index 0000000000..8a9ba1b2b8
--- /dev/null
+++ b/databendwriter/src/main/assembly/package.xml
@@ -0,0 +1,34 @@
+
+    
+    
+        dir
+    
+    false
+    
+        
+            src/main/resources
+            
+                plugin.json
+ 				plugin_job_template.json
+ 			
+            plugin/writer/databendwriter
+        
+        
+            target/
+            
+                databendwriter-0.0.1-SNAPSHOT.jar
+            
+            plugin/writer/databendwriter
+        
+    
+
+    
+        
+            false
+            plugin/writer/databendwriter/libs
+        
+    
+
diff --git a/databendwriter/src/main/java/com/alibaba/datax/plugin/writer/databendwriter/DatabendWriter.java b/databendwriter/src/main/java/com/alibaba/datax/plugin/writer/databendwriter/DatabendWriter.java
new file mode 100644
index 0000000000..ddb8fc9a53
--- /dev/null
+++ b/databendwriter/src/main/java/com/alibaba/datax/plugin/writer/databendwriter/DatabendWriter.java
@@ -0,0 +1,241 @@
+package com.alibaba.datax.plugin.writer.databendwriter;
+
+import com.alibaba.datax.common.element.Column;
+import com.alibaba.datax.common.element.StringColumn;
+import com.alibaba.datax.common.exception.CommonErrorCode;
+import com.alibaba.datax.common.exception.DataXException;
+import com.alibaba.datax.common.plugin.RecordReceiver;
+import com.alibaba.datax.common.spi.Writer;
+import com.alibaba.datax.common.util.Configuration;
+import com.alibaba.datax.plugin.rdbms.util.DataBaseType;
+import com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter;
+import com.alibaba.datax.plugin.writer.databendwriter.util.DatabendWriterUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.*;
+import java.util.List;
+import java.util.regex.Pattern;
+
+public class DatabendWriter extends Writer {
+    private static final DataBaseType DATABASE_TYPE = DataBaseType.Databend;
+
+    public static class Job
+            extends Writer.Job {
+        private static final Logger LOG = LoggerFactory.getLogger(Job.class);
+        private Configuration originalConfig;
+        private CommonRdbmsWriter.Job commonRdbmsWriterMaster;
+
+        @Override
+        public void init() throws DataXException {
+            this.originalConfig = super.getPluginJobConf();
+            this.commonRdbmsWriterMaster = new CommonRdbmsWriter.Job(DATABASE_TYPE);
+            this.commonRdbmsWriterMaster.init(this.originalConfig);
+            // placeholder currently not supported by databend driver, needs special treatment
+            DatabendWriterUtil.dealWriteMode(this.originalConfig);
+        }
+
+        @Override
+        public void preCheck() {
+            this.init();
+            this.commonRdbmsWriterMaster.writerPreCheck(this.originalConfig, DATABASE_TYPE);
+        }
+
+        @Override
+        public void prepare() {
+            this.commonRdbmsWriterMaster.prepare(this.originalConfig);
+        }
+
+        @Override
+        public List split(int mandatoryNumber) {
+            return this.commonRdbmsWriterMaster.split(this.originalConfig, mandatoryNumber);
+        }
+
+        @Override
+        public void post() {
+            this.commonRdbmsWriterMaster.post(this.originalConfig);
+        }
+
+        @Override
+        public void destroy() {
+            this.commonRdbmsWriterMaster.destroy(this.originalConfig);
+        }
+    }
+
+
+    public static class Task extends Writer.Task {
+        private static final Logger LOG = LoggerFactory.getLogger(Task.class);
+
+        private Configuration writerSliceConfig;
+
+        private CommonRdbmsWriter.Task commonRdbmsWriterSlave;
+
+        @Override
+        public void init() {
+            this.writerSliceConfig = super.getPluginJobConf();
+
+            this.commonRdbmsWriterSlave = new CommonRdbmsWriter.Task(DataBaseType.Databend) {
+                @Override
+                protected PreparedStatement fillPreparedStatementColumnType(PreparedStatement preparedStatement, int columnIndex, int columnSqltype, String typeName, Column column) throws SQLException {
+                    try {
+                        if (column.getRawData() == null) {
+                            preparedStatement.setNull(columnIndex + 1, columnSqltype);
+                            return preparedStatement;
+                        }
+
+                        java.util.Date utilDate;
+                        switch (columnSqltype) {
+
+                            case Types.TINYINT:
+                            case Types.SMALLINT:
+                            case Types.INTEGER:
+                                preparedStatement.setInt(columnIndex + 1, column.asBigInteger().intValue());
+                                break;
+                            case Types.BIGINT:
+                                preparedStatement.setLong(columnIndex + 1, column.asLong());
+                                break;
+                            case Types.DECIMAL:
+                                preparedStatement.setBigDecimal(columnIndex + 1, column.asBigDecimal());
+                                break;
+                            case Types.FLOAT:
+                            case Types.REAL:
+                                preparedStatement.setFloat(columnIndex + 1, column.asDouble().floatValue());
+                                break;
+                            case Types.DOUBLE:
+                                preparedStatement.setDouble(columnIndex + 1, column.asDouble());
+                                break;
+                            case Types.DATE:
+                                java.sql.Date sqlDate = null;
+                                try {
+                                    utilDate = column.asDate();
+                                } catch (DataXException e) {
+                                    throw new SQLException(String.format(
+                                            "Date type conversion error: [%s]", column));
+                                }
+
+                                if (null != utilDate) {
+                                    sqlDate = new java.sql.Date(utilDate.getTime());
+                                }
+                                preparedStatement.setDate(columnIndex + 1, sqlDate);
+                                break;
+
+                            case Types.TIME:
+                                java.sql.Time sqlTime = null;
+                                try {
+                                    utilDate = column.asDate();
+                                } catch (DataXException e) {
+                                    throw new SQLException(String.format(
+                                            "Date type conversion error: [%s]", column));
+                                }
+
+                                if (null != utilDate) {
+                                    sqlTime = new java.sql.Time(utilDate.getTime());
+                                }
+                                preparedStatement.setTime(columnIndex + 1, sqlTime);
+                                break;
+
+                            case Types.TIMESTAMP:
+                                Timestamp sqlTimestamp = null;
+                                if (column instanceof StringColumn && column.asString() != null) {
+                                    String timeStampStr = column.asString();
+                                    // JAVA TIMESTAMP 类型入参必须是 "2017-07-12 14:39:00.123566" 格式
+                                    String pattern = "^\\d+-\\d+-\\d+ \\d+:\\d+:\\d+.\\d+";
+                                    boolean isMatch = Pattern.matches(pattern, timeStampStr);
+                                    if (isMatch) {
+                                        sqlTimestamp = Timestamp.valueOf(timeStampStr);
+                                        preparedStatement.setTimestamp(columnIndex + 1, sqlTimestamp);
+                                        break;
+                                    }
+                                }
+                                try {
+                                    utilDate = column.asDate();
+                                } catch (DataXException e) {
+                                    throw new SQLException(String.format(
+                                            "Date type conversion error: [%s]", column));
+                                }
+
+                                if (null != utilDate) {
+                                    sqlTimestamp = new Timestamp(
+                                            utilDate.getTime());
+                                }
+                                preparedStatement.setTimestamp(columnIndex + 1, sqlTimestamp);
+                                break;
+
+                            case Types.BINARY:
+                            case Types.VARBINARY:
+                            case Types.BLOB:
+                            case Types.LONGVARBINARY:
+                                preparedStatement.setBytes(columnIndex + 1, column
+                                        .asBytes());
+                                break;
+
+                            case Types.BOOLEAN:
+
+                                // warn: bit(1) -> Types.BIT 可使用setBoolean
+                                // warn: bit(>1) -> Types.VARBINARY 可使用setBytes
+                            case Types.BIT:
+                                if (this.dataBaseType == DataBaseType.MySql) {
+                                    Boolean asBoolean = column.asBoolean();
+                                    if (asBoolean != null) {
+                                        preparedStatement.setBoolean(columnIndex + 1, asBoolean);
+                                    } else {
+                                        preparedStatement.setNull(columnIndex + 1, Types.BIT);
+                                    }
+                                } else {
+                                    preparedStatement.setString(columnIndex + 1, column.asString());
+                                }
+                                break;
+
+                            default:
+                                // cast variant / array into string is fine.
+                                preparedStatement.setString(columnIndex + 1, column.asString());
+                                break;
+                        }
+                        return preparedStatement;
+                    } catch (DataXException e) {
+                        // fix类型转换或者溢出失败时,将具体哪一列打印出来
+                        if (e.getErrorCode() == CommonErrorCode.CONVERT_NOT_SUPPORT ||
+                                e.getErrorCode() == CommonErrorCode.CONVERT_OVER_FLOW) {
+                            throw DataXException
+                                    .asDataXException(
+                                            e.getErrorCode(),
+                                            String.format(
+                                                    "type conversion error. columnName: [%s], columnType:[%d], columnJavaType: [%s]. please change the data type in given column field or do not sync on the column.",
+                                                    this.resultSetMetaData.getLeft()
+                                                            .get(columnIndex),
+                                                    this.resultSetMetaData.getMiddle()
+                                                            .get(columnIndex),
+                                                    this.resultSetMetaData.getRight()
+                                                            .get(columnIndex)));
+                        } else {
+                            throw e;
+                        }
+                    }
+                }
+
+            };
+            this.commonRdbmsWriterSlave.init(this.writerSliceConfig);
+        }
+
+        @Override
+        public void destroy() {
+            this.commonRdbmsWriterSlave.destroy(this.writerSliceConfig);
+        }
+
+        @Override
+        public void prepare() {
+            this.commonRdbmsWriterSlave.prepare(this.writerSliceConfig);
+        }
+
+        @Override
+        public void post() {
+            this.commonRdbmsWriterSlave.post(this.writerSliceConfig);
+        }
+
+        @Override
+        public void startWrite(RecordReceiver lineReceiver) {
+            this.commonRdbmsWriterSlave.startWrite(lineReceiver, this.writerSliceConfig, this.getTaskPluginCollector());
+        }
+
+    }
+}
diff --git a/databendwriter/src/main/java/com/alibaba/datax/plugin/writer/databendwriter/DatabendWriterErrorCode.java b/databendwriter/src/main/java/com/alibaba/datax/plugin/writer/databendwriter/DatabendWriterErrorCode.java
new file mode 100644
index 0000000000..21cbf428f3
--- /dev/null
+++ b/databendwriter/src/main/java/com/alibaba/datax/plugin/writer/databendwriter/DatabendWriterErrorCode.java
@@ -0,0 +1,33 @@
+package com.alibaba.datax.plugin.writer.databendwriter;
+
+import com.alibaba.datax.common.spi.ErrorCode;
+
+
+public enum DatabendWriterErrorCode implements ErrorCode {
+    CONF_ERROR("DatabendWriter-00", "配置错误."),
+    WRITE_DATA_ERROR("DatabendWriter-01", "写入数据时失败."),
+    ;
+
+    private final String code;
+    private final String description;
+
+    private DatabendWriterErrorCode(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    @Override
+    public String getCode() {
+        return this.code;
+    }
+
+    @Override
+    public String getDescription() {
+        return this.description;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("Code:[%s], Description:[%s].", this.code, this.description);
+    }
+}
\ No newline at end of file
diff --git a/databendwriter/src/main/java/com/alibaba/datax/plugin/writer/databendwriter/util/DatabendWriterUtil.java b/databendwriter/src/main/java/com/alibaba/datax/plugin/writer/databendwriter/util/DatabendWriterUtil.java
new file mode 100644
index 0000000000..516a75eb15
--- /dev/null
+++ b/databendwriter/src/main/java/com/alibaba/datax/plugin/writer/databendwriter/util/DatabendWriterUtil.java
@@ -0,0 +1,72 @@
+package com.alibaba.datax.plugin.writer.databendwriter.util;
+
+import com.alibaba.datax.common.exception.DataXException;
+import com.alibaba.datax.common.util.Configuration;
+import com.alibaba.datax.plugin.rdbms.writer.Constant;
+import com.alibaba.datax.plugin.rdbms.writer.Key;
+
+import com.alibaba.datax.plugin.writer.databendwriter.DatabendWriterErrorCode;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.xml.crypto.Data;
+import java.util.List;
+import java.util.StringJoiner;
+
+public final class DatabendWriterUtil {
+    private static final Logger LOG = LoggerFactory.getLogger(DatabendWriterUtil.class);
+
+    private DatabendWriterUtil() {
+    }
+
+    public static void dealWriteMode(Configuration originalConfig) throws DataXException {
+        List columns = originalConfig.getList(Key.COLUMN, String.class);
+        List onConflictColumns = originalConfig.getList(Key.ONCONFLICT_COLUMN, String.class);
+        StringBuilder writeDataSqlTemplate = new StringBuilder();
+
+        String jdbcUrl = originalConfig.getString(String.format("%s[0].%s",
+                Constant.CONN_MARK, Key.JDBC_URL, String.class));
+
+        String writeMode = originalConfig.getString(Key.WRITE_MODE, "INSERT");
+        LOG.info("write mode is {}", writeMode);
+        if (writeMode.toLowerCase().contains("replace")) {
+            if (onConflictColumns == null || onConflictColumns.size() == 0) {
+                throw DataXException
+                        .asDataXException(
+                                DatabendWriterErrorCode.CONF_ERROR,
+                                String.format(
+                                        "Replace mode must has onConflictColumn config."
+                                ));
+            }
+
+            // for databend if you want to use replace mode, the writeMode should be:  "writeMode": "replace"
+            writeDataSqlTemplate.append("REPLACE INTO %s (")
+                    .append(StringUtils.join(columns, ",")).append(") ").append(onConFlictDoString(onConflictColumns))
+                    .append(" VALUES");
+
+            LOG.info("Replace data [\n{}\n], which jdbcUrl like:[{}]", writeDataSqlTemplate, jdbcUrl);
+            originalConfig.set(Constant.INSERT_OR_REPLACE_TEMPLATE_MARK, writeDataSqlTemplate);
+        } else {
+            writeDataSqlTemplate.append("INSERT INTO %s");
+            StringJoiner columnString = new StringJoiner(",");
+
+            for (String column : columns) {
+                columnString.add(column);
+            }
+            writeDataSqlTemplate.append(String.format("(%s)", columnString));
+            writeDataSqlTemplate.append(" VALUES");
+
+            LOG.info("Insert data [\n{}\n], which jdbcUrl like:[{}]", writeDataSqlTemplate, jdbcUrl);
+
+            originalConfig.set(Constant.INSERT_OR_REPLACE_TEMPLATE_MARK, writeDataSqlTemplate);
+        }
+
+    }
+
+    public static String onConFlictDoString(List conflictColumns) {
+        return " ON " +
+                "(" +
+                StringUtils.join(conflictColumns, ",") + ") ";
+    }
+}
diff --git a/databendwriter/src/main/resources/plugin.json b/databendwriter/src/main/resources/plugin.json
new file mode 100644
index 0000000000..bab0130db4
--- /dev/null
+++ b/databendwriter/src/main/resources/plugin.json
@@ -0,0 +1,6 @@
+{
+  "name": "databendwriter",
+  "class": "com.alibaba.datax.plugin.writer.databendwriter.DatabendWriter",
+  "description": "execute batch insert sql to write dataX data into databend",
+  "developer": "databend"
+}
\ No newline at end of file
diff --git a/databendwriter/src/main/resources/plugin_job_template.json b/databendwriter/src/main/resources/plugin_job_template.json
new file mode 100644
index 0000000000..34d4b2512c
--- /dev/null
+++ b/databendwriter/src/main/resources/plugin_job_template.json
@@ -0,0 +1,19 @@
+{
+  "name": "databendwriter",
+  "parameter": {
+    "username": "username",
+    "password": "password",
+    "column": ["col1", "col2", "col3"],
+    "connection": [
+      {
+        "jdbcUrl": "jdbc:databend://:[/]",
+        "table": "table1"
+      }
+    ],
+    "preSql": [],
+    "postSql": [],
+
+    "maxBatchRows": 65536,
+    "maxBatchSize": 134217728
+  }
+}
\ No newline at end of file
diff --git a/datahubreader/pom.xml b/datahubreader/pom.xml
new file mode 100644
index 0000000000..c0022b44a9
--- /dev/null
+++ b/datahubreader/pom.xml
@@ -0,0 +1,79 @@
+
+
+    
+        datax-all
+        com.alibaba.datax
+        0.0.1-SNAPSHOT
+    
+    4.0.0
+
+    datahubreader
+
+    0.0.1-SNAPSHOT
+
+    
+        
+            com.alibaba.datax
+            datax-common
+            ${datax-project-version}
+            
+                
+                    slf4j-log4j12
+                    org.slf4j
+                
+            
+        
+        
+            org.slf4j
+            slf4j-api
+        
+        
+            ch.qos.logback
+            logback-classic
+        
+        
+            com.aliyun.datahub
+            aliyun-sdk-datahub
+            2.21.6-public
+        
+        
+            junit
+            junit
+            4.12
+            test
+        
+    
+
+    
+        
+            
+            
+                maven-compiler-plugin
+                
+                    ${jdk-version}
+                    ${jdk-version}
+                    ${project-sourceEncoding}
+                
+            
+            
+            
+                maven-assembly-plugin
+                
+                    
+                        src/main/assembly/package.xml
+                    
+                    datax
+                
+                
+                    
+                        dwzip
+                        package
+                        
+                            single
+                        
+                    
+                
+            
+        
+    
+
diff --git a/datahubreader/src/main/assembly/package.xml b/datahubreader/src/main/assembly/package.xml
new file mode 100644
index 0000000000..d14ea98192
--- /dev/null
+++ b/datahubreader/src/main/assembly/package.xml
@@ -0,0 +1,34 @@
+
+    
+    
+        dir
+    
+    false
+    
+        
+            src/main/resources
+            
+                plugin.json
+            
+            plugin/reader/datahubreader
+        
+        
+            target/
+            
+                datahubreader-0.0.1-SNAPSHOT.jar
+            
+            plugin/reader/datahubreader
+        
+    
+
+    
+        
+            false
+            plugin/reader/datahubreader/libs
+            runtime
+        
+    
+
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/Constant.java b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/Constant.java
new file mode 100644
index 0000000000..bee3ccd797
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/Constant.java
@@ -0,0 +1,8 @@
+package com.alibaba.datax.plugin.reader.datahubreader;
+
+public class Constant {
+
+    public static String DATETIME_FORMAT = "yyyyMMddHHmmss";
+    public static String DATE_FORMAT = "yyyyMMdd";
+
+}
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubClientHelper.java b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubClientHelper.java
new file mode 100644
index 0000000000..2b7bcec45d
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubClientHelper.java
@@ -0,0 +1,42 @@
+package com.alibaba.datax.plugin.reader.datahubreader;
+
+import com.alibaba.datax.common.util.Configuration;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.TypeReference;
+import com.aliyun.datahub.client.DatahubClient;
+import com.aliyun.datahub.client.DatahubClientBuilder;
+import com.aliyun.datahub.client.auth.Account;
+import com.aliyun.datahub.client.auth.AliyunAccount;
+import com.aliyun.datahub.client.common.DatahubConfig;
+import com.aliyun.datahub.client.http.HttpConfig;
+import org.apache.commons.lang3.StringUtils;
+
+public class DatahubClientHelper {
+    public static DatahubClient getDatahubClient(Configuration jobConfig) {
+        String accessId = jobConfig.getNecessaryValue(Key.CONFIG_KEY_ACCESS_ID,
+                DatahubWriterErrorCode.MISSING_REQUIRED_VALUE);
+        String accessKey = jobConfig.getNecessaryValue(Key.CONFIG_KEY_ACCESS_KEY,
+                DatahubWriterErrorCode.MISSING_REQUIRED_VALUE);
+        String endpoint = jobConfig.getNecessaryValue(Key.CONFIG_KEY_ENDPOINT,
+                DatahubWriterErrorCode.MISSING_REQUIRED_VALUE);
+        Account account = new AliyunAccount(accessId, accessKey);
+        // 是否开启二进制传输,服务端2.12版本开始支持
+        boolean enableBinary = jobConfig.getBool("enableBinary", false);
+        DatahubConfig datahubConfig = new DatahubConfig(endpoint, account, enableBinary);
+        // HttpConfig可不设置,不设置时采用默认值
+        // 读写数据推荐打开网络传输 LZ4压缩
+        HttpConfig httpConfig = null;
+        String httpConfigStr = jobConfig.getString("httpConfig");
+        if (StringUtils.isNotBlank(httpConfigStr)) {
+            httpConfig = JSON.parseObject(httpConfigStr, new TypeReference() {
+            });
+        }
+
+        DatahubClientBuilder builder = DatahubClientBuilder.newBuilder().setDatahubConfig(datahubConfig);
+        if (null != httpConfig) {
+            builder.setHttpConfig(httpConfig);
+        }
+        DatahubClient datahubClient = builder.build();
+        return datahubClient;
+    }
+}
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubReader.java b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubReader.java
new file mode 100644
index 0000000000..4792ac39c9
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubReader.java
@@ -0,0 +1,292 @@
+package com.alibaba.datax.plugin.reader.datahubreader;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import com.aliyun.datahub.client.model.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.alibaba.datax.common.element.Column;
+import com.alibaba.datax.common.element.Record;
+import com.alibaba.datax.common.element.StringColumn;
+import com.alibaba.datax.common.exception.DataXException;
+import com.alibaba.datax.common.plugin.RecordSender;
+import com.alibaba.datax.common.spi.Reader;
+import com.alibaba.datax.common.util.Configuration;
+
+
+import com.aliyun.datahub.client.DatahubClient;
+
+
+public class DatahubReader extends Reader {
+    public static class Job extends Reader.Job {
+        private static final Logger LOG = LoggerFactory.getLogger(Job.class);
+        
+        private Configuration originalConfig;
+        
+        private Long beginTimestampMillis;
+        private Long endTimestampMillis;
+        
+        DatahubClient datahubClient;
+        
+        @Override
+        public void init() {
+            LOG.info("datahub reader job init begin ...");
+            this.originalConfig = super.getPluginJobConf();
+            validateParameter(originalConfig);
+            this.datahubClient = DatahubClientHelper.getDatahubClient(this.originalConfig);
+            LOG.info("datahub reader job init end.");
+        }
+        
+        private void validateParameter(Configuration conf){
+            conf.getNecessaryValue(Key.ENDPOINT,DatahubReaderErrorCode.REQUIRE_VALUE);
+            conf.getNecessaryValue(Key.ACCESSKEYID,DatahubReaderErrorCode.REQUIRE_VALUE);
+            conf.getNecessaryValue(Key.ACCESSKEYSECRET,DatahubReaderErrorCode.REQUIRE_VALUE);
+            conf.getNecessaryValue(Key.PROJECT,DatahubReaderErrorCode.REQUIRE_VALUE);
+            conf.getNecessaryValue(Key.TOPIC,DatahubReaderErrorCode.REQUIRE_VALUE);
+            conf.getNecessaryValue(Key.COLUMN,DatahubReaderErrorCode.REQUIRE_VALUE);
+            conf.getNecessaryValue(Key.BEGINDATETIME,DatahubReaderErrorCode.REQUIRE_VALUE);
+            conf.getNecessaryValue(Key.ENDDATETIME,DatahubReaderErrorCode.REQUIRE_VALUE);
+            
+            int batchSize = this.originalConfig.getInt(Key.BATCHSIZE, 1024);
+            if (batchSize > 10000) {
+                throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                        "Invalid batchSize[" + batchSize + "] value (0,10000]!");
+            }
+            
+            String beginDateTime = this.originalConfig.getString(Key.BEGINDATETIME);            
+            if (beginDateTime != null) {
+                try {
+                    beginTimestampMillis = DatahubReaderUtils.getUnixTimeFromDateTime(beginDateTime);
+                } catch (ParseException e) {
+                    throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                            "Invalid beginDateTime[" + beginDateTime + "], format [yyyyMMddHHmmss]!");    
+                }
+            }
+            
+            if (beginTimestampMillis != null && beginTimestampMillis <= 0) {
+                throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                        "Invalid beginTimestampMillis[" + beginTimestampMillis + "]!");               
+            }
+            
+            String endDateTime = this.originalConfig.getString(Key.ENDDATETIME);            
+            if (endDateTime != null) {
+                try {
+                    endTimestampMillis = DatahubReaderUtils.getUnixTimeFromDateTime(endDateTime);
+                } catch (ParseException e) {
+                    throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                            "Invalid beginDateTime[" + endDateTime + "], format [yyyyMMddHHmmss]!");    
+                }
+            }
+            
+            if (endTimestampMillis != null && endTimestampMillis <= 0) {
+                throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                        "Invalid endTimestampMillis[" + endTimestampMillis + "]!");                
+            }
+            
+            if (beginTimestampMillis != null && endTimestampMillis != null
+                    && endTimestampMillis <= beginTimestampMillis) {
+                throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                        "endTimestampMillis[" + endTimestampMillis + "] must bigger than beginTimestampMillis[" + beginTimestampMillis + "]!");  
+            }
+        }
+        
+        @Override
+        public void prepare() {
+            // create datahub client
+            String project = originalConfig.getNecessaryValue(Key.PROJECT, DatahubReaderErrorCode.REQUIRE_VALUE);
+            String topic = originalConfig.getNecessaryValue(Key.TOPIC, DatahubReaderErrorCode.REQUIRE_VALUE);
+            RecordType recordType = null;
+            try {
+                DatahubClient client = DatahubClientHelper.getDatahubClient(this.originalConfig);
+                GetTopicResult getTopicResult = client.getTopic(project, topic);
+                recordType = getTopicResult.getRecordType();
+            } catch (Exception e) {
+                LOG.warn("get topic type error: {}", e.getMessage());
+            }
+            if (null != recordType) {
+                if (recordType == RecordType.BLOB) {
+                    throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                            "DatahubReader only support 'Tuple' RecordType now, but your RecordType is 'BLOB'");
+                }
+            }
+        }
+
+        @Override
+        public void destroy() {
+        }
+
+        @Override
+        public List split(int adviceNumber) {
+            LOG.info("split() begin...");
+            
+            List readerSplitConfigs = new ArrayList();
+            
+            String project = this.originalConfig.getString(Key.PROJECT);
+            String topic = this.originalConfig.getString(Key.TOPIC);
+            
+            List shardEntrys = DatahubReaderUtils.getShardsWithRetry(this.datahubClient, project, topic);
+            if (shardEntrys == null || shardEntrys.isEmpty() || shardEntrys.size() == 0) {
+                throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                        "Project [" + project + "] Topic [" + topic + "] has no shards, please check !");       
+            }
+            
+            for (ShardEntry shardEntry : shardEntrys) {
+                Configuration splitedConfig = this.originalConfig.clone();
+                splitedConfig.set(Key.SHARDID, shardEntry.getShardId());
+                readerSplitConfigs.add(splitedConfig);
+            }
+            
+            LOG.info("split() ok and end...");
+            return readerSplitConfigs;
+        }
+        
+    }
+    
+    public static class Task extends Reader.Task {
+        private static final Logger LOG = LoggerFactory.getLogger(Task.class);
+        
+        private Configuration taskConfig;
+        
+        private String accessId;
+        private String accessKey;
+        private String endpoint;
+        private String project;
+        private String topic;
+        private String shardId;
+        private Long beginTimestampMillis;
+        private Long endTimestampMillis;
+        private int batchSize;
+        private List columns;
+        private RecordSchema schema;
+        private String timeStampUnit;
+        
+        DatahubClient datahubClient;
+        
+        @Override
+        public void init() {
+            this.taskConfig = super.getPluginJobConf();
+            
+            this.accessId = this.taskConfig.getString(Key.ACCESSKEYID);
+            this.accessKey = this.taskConfig.getString(Key.ACCESSKEYSECRET);
+            this.endpoint = this.taskConfig.getString(Key.ENDPOINT);
+            this.project = this.taskConfig.getString(Key.PROJECT);
+            this.topic = this.taskConfig.getString(Key.TOPIC);
+            this.shardId = this.taskConfig.getString(Key.SHARDID);
+            this.batchSize = this.taskConfig.getInt(Key.BATCHSIZE, 1024);
+            this.timeStampUnit = this.taskConfig.getString(Key.TIMESTAMP_UNIT, "MICROSECOND");
+            try {
+                this.beginTimestampMillis = DatahubReaderUtils.getUnixTimeFromDateTime(this.taskConfig.getString(Key.BEGINDATETIME));
+            } catch (ParseException e) {                
+            }
+            
+            try {
+                this.endTimestampMillis = DatahubReaderUtils.getUnixTimeFromDateTime(this.taskConfig.getString(Key.ENDDATETIME));
+            } catch (ParseException e) {                
+            }
+            
+            this.columns = this.taskConfig.getList(Key.COLUMN, String.class);
+            
+            this.datahubClient = DatahubClientHelper.getDatahubClient(this.taskConfig);
+
+
+            this.schema = DatahubReaderUtils.getDatahubSchemaWithRetry(this.datahubClient, this.project, topic);
+            
+            LOG.info("init datahub reader task finished.project:{} topic:{} batchSize:{}", project, topic, batchSize);
+        }
+
+        @Override
+        public void destroy() {
+        }
+
+        @Override
+        public void startRead(RecordSender recordSender) {
+            LOG.info("read start");
+            
+            String beginCursor = DatahubReaderUtils.getCursorWithRetry(this.datahubClient, this.project, 
+                    this.topic, this.shardId, this.beginTimestampMillis);
+            String endCursor = DatahubReaderUtils.getCursorWithRetry(this.datahubClient, this.project, 
+                    this.topic, this.shardId, this.endTimestampMillis);
+            
+            if (beginCursor == null) {
+                LOG.info("Shard:{} has no data!", this.shardId);
+                return;
+            } else if (endCursor == null) {
+                endCursor = DatahubReaderUtils.getLatestCursorWithRetry(this.datahubClient, this.project,
+                        this.topic, this.shardId);
+            }
+            
+            String curCursor = beginCursor;
+            
+            boolean exit = false;
+            
+            while (true) {
+                
+                GetRecordsResult result = DatahubReaderUtils.getRecordsResultWithRetry(this.datahubClient, this.project, this.topic,
+                        this.shardId, this.batchSize, curCursor, this.schema);
+                                
+                List records = result.getRecords();
+                if (records.size() > 0) {
+                    for (RecordEntry record : records) {
+                        if (record.getSystemTime() >= this.endTimestampMillis) {
+                            exit = true;
+                            break;
+                        }
+                        
+                        HashMap dataMap = new HashMap();
+                        List fields = ((TupleRecordData) record.getRecordData()).getRecordSchema().getFields();
+                        for (int i = 0; i < fields.size(); i++) {
+                            Field field = fields.get(i);
+                            Column column = DatahubReaderUtils.getColumnFromField(record, field, this.timeStampUnit);
+                            dataMap.put(field.getName(), column);
+                        }
+                        
+                        Record dataxRecord = recordSender.createRecord();
+                        
+                        if (null != this.columns && 1 == this.columns.size()) {
+                            String columnsInStr = columns.get(0).toString();
+                            if ("\"*\"".equals(columnsInStr) || "*".equals(columnsInStr)) {
+                                for (int i = 0; i < fields.size(); i++) {
+                                    dataxRecord.addColumn(dataMap.get(fields.get(i).getName()));
+                                }
+
+                            } else {
+                                if (dataMap.containsKey(columnsInStr)) {
+                                    dataxRecord.addColumn(dataMap.get(columnsInStr));
+                                } else {
+                                    dataxRecord.addColumn(new StringColumn(null));
+                                }
+                            }
+                        } else {
+                            for (String col : this.columns) {
+                                if (dataMap.containsKey(col)) {
+                                    dataxRecord.addColumn(dataMap.get(col));
+                                } else {
+                                    dataxRecord.addColumn(new StringColumn(null));
+                                }
+                            }
+                        }                         
+
+                        recordSender.sendToWriter(dataxRecord);                           
+                    }
+                } else {
+                    break;
+                }
+                
+                if (exit) {
+                    break;
+                }
+                
+                curCursor = result.getNextCursor();
+            }
+            
+            
+            LOG.info("end read datahub shard...");
+        }
+        
+    }
+
+}
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubReaderErrorCode.java b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubReaderErrorCode.java
new file mode 100644
index 0000000000..949a66f0b8
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubReaderErrorCode.java
@@ -0,0 +1,35 @@
+package com.alibaba.datax.plugin.reader.datahubreader;
+
+import com.alibaba.datax.common.spi.ErrorCode;
+
+public enum DatahubReaderErrorCode implements ErrorCode {
+    BAD_CONFIG_VALUE("DatahubReader-00", "The value you configured is invalid."),
+    LOG_HUB_ERROR("DatahubReader-01","Datahub exception"),
+    REQUIRE_VALUE("DatahubReader-02","Missing parameters"),
+    EMPTY_LOGSTORE_VALUE("DatahubReader-03","There is no shard under this LogStore");
+
+
+    private final String code;
+    private final String description;
+
+    private DatahubReaderErrorCode(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    @Override
+    public String getCode() {
+        return this.code;
+    }
+
+    @Override
+    public String getDescription() {
+        return this.description;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("Code:[%s], Description:[%s]. ", this.code,
+                this.description);
+    }
+}
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubReaderUtils.java b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubReaderUtils.java
new file mode 100644
index 0000000000..6c3455df67
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubReaderUtils.java
@@ -0,0 +1,200 @@
+package com.alibaba.datax.plugin.reader.datahubreader;
+
+import java.math.BigDecimal;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+import com.alibaba.datax.common.element.*;
+import com.alibaba.datax.common.exception.DataXException;
+import com.alibaba.datax.common.util.DataXCaseEnvUtil;
+import com.alibaba.datax.common.util.RetryUtil;
+
+import com.aliyun.datahub.client.DatahubClient;
+import com.aliyun.datahub.client.exception.InvalidParameterException;
+import com.aliyun.datahub.client.model.*;
+
+public class DatahubReaderUtils {
+
+    public static long getUnixTimeFromDateTime(String dateTime) throws ParseException {
+        try {
+            String format = Constant.DATETIME_FORMAT;
+            SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
+            return simpleDateFormat.parse(dateTime).getTime();
+        } catch (ParseException ignored) {
+            throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                    "Invalid DateTime[" + dateTime + "]!");   
+        }
+    }
+    
+    public static List getShardsWithRetry(final DatahubClient datahubClient, final String project, final String topic) {
+        
+        List shards = null;
+        try {
+            shards = RetryUtil.executeWithRetry(new Callable>() {
+                @Override
+                public List call() throws Exception {
+                    ListShardResult listShardResult = datahubClient.listShard(project, topic);
+                    return listShardResult.getShards(); 
+                }
+            }, DataXCaseEnvUtil.getRetryTimes(7), DataXCaseEnvUtil.getRetryInterval(1000L), DataXCaseEnvUtil.getRetryExponential(true));
+            
+        } catch (Exception e) {
+            throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                    "get Shards error, please check ! detail error messsage: " + e.toString());
+        }         
+        return shards;
+    }
+    
+    public static String getCursorWithRetry(final DatahubClient datahubClient, final String project, final String topic, 
+            final String shardId, final long timestamp) {
+        
+        String cursor;
+        try {
+            cursor = RetryUtil.executeWithRetry(new Callable() {
+                @Override
+                public String call() throws Exception {
+                    try {
+                        return datahubClient.getCursor(project, topic, shardId, CursorType.SYSTEM_TIME, timestamp).getCursor();
+                    } catch (InvalidParameterException e) {
+                        if (e.getErrorMessage().indexOf("Time in seek request is out of range") >= 0) {
+                            return null;
+                        } else {
+                            throw e;
+                        }
+                        
+                    }
+                }
+            }, DataXCaseEnvUtil.getRetryTimes(7), DataXCaseEnvUtil.getRetryInterval(1000L), DataXCaseEnvUtil.getRetryExponential(true));
+            
+        } catch (Exception e) {
+            throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                    "get Cursor error, please check ! detail error messsage: " + e.toString());
+        }         
+        return cursor;
+    }
+    
+    public static String getLatestCursorWithRetry(final DatahubClient datahubClient, final String project, final String topic,
+            final String shardId) {
+        
+        String cursor;
+        try {
+            cursor = RetryUtil.executeWithRetry(new Callable() {
+                @Override
+                public String call() throws Exception {
+                    return datahubClient.getCursor(project, topic, shardId, CursorType.LATEST).getCursor();
+                }
+            }, DataXCaseEnvUtil.getRetryTimes(7), DataXCaseEnvUtil.getRetryInterval(1000L), DataXCaseEnvUtil.getRetryExponential(true));
+            
+        } catch (Exception e) {
+            throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                    "get Cursor error, please check ! detail error messsage: " + e.toString());
+        }         
+        return cursor;
+    }    
+    
+    public static RecordSchema getDatahubSchemaWithRetry(final DatahubClient datahubClient, final String project, final String topic) {
+        
+        RecordSchema schema;
+        try {
+            schema = RetryUtil.executeWithRetry(new Callable() {
+                @Override
+                public RecordSchema call() throws Exception {
+                    return datahubClient.getTopic(project, topic).getRecordSchema();
+                }
+            }, DataXCaseEnvUtil.getRetryTimes(7), DataXCaseEnvUtil.getRetryInterval(1000L), DataXCaseEnvUtil.getRetryExponential(true));
+            
+        } catch (Exception e) {
+            throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                    "get Topic Schema error, please check ! detail error messsage: " + e.toString());
+        }         
+        return schema;
+    } 
+    
+    public static GetRecordsResult getRecordsResultWithRetry(final DatahubClient datahubClient, final String project,
+            final String topic, final String shardId, final int batchSize, final String cursor, final RecordSchema schema) {
+        
+        GetRecordsResult result;
+        try  {
+            result = RetryUtil.executeWithRetry(new Callable() {
+                @Override
+                public GetRecordsResult call() throws Exception {
+                    return datahubClient.getRecords(project, topic, shardId, schema, cursor, batchSize);
+                }
+            }, DataXCaseEnvUtil.getRetryTimes(7), DataXCaseEnvUtil.getRetryInterval(1000L), DataXCaseEnvUtil.getRetryExponential(true));
+            
+        } catch (Exception e) {
+            throw DataXException.asDataXException(DatahubReaderErrorCode.BAD_CONFIG_VALUE,
+                    "get Record Result error, please check ! detail error messsage: " + e.toString());
+        }     
+        return result;
+        
+    }
+    
+    public static Column getColumnFromField(RecordEntry record, Field field, String timeStampUnit) {
+        Column col = null;
+        TupleRecordData o = (TupleRecordData) record.getRecordData();
+
+        switch (field.getType()) {
+            case SMALLINT:
+                Short shortValue = ((Short) o.getField(field.getName()));
+                col = new LongColumn(shortValue == null ? null: shortValue.longValue());
+                break;
+            case INTEGER:
+                col = new LongColumn((Integer) o.getField(field.getName()));
+                break;
+            case BIGINT: {
+                col = new LongColumn((Long) o.getField(field.getName()));
+                break;
+            }
+            case TINYINT: {
+                Byte byteValue = ((Byte) o.getField(field.getName()));
+                col = new LongColumn(byteValue == null ? null : byteValue.longValue());
+                break;
+            }
+            case BOOLEAN: {
+                col = new BoolColumn((Boolean) o.getField(field.getName()));
+                break;
+            }
+            case FLOAT:
+                col = new DoubleColumn((Float) o.getField(field.getName()));
+                break;
+            case DOUBLE: {
+                col = new DoubleColumn((Double) o.getField(field.getName()));
+                break;
+            }
+            case STRING: {
+                col = new StringColumn((String) o.getField(field.getName()));
+                break;
+            }
+            case DECIMAL: {
+                BigDecimal value = (BigDecimal) o.getField(field.getName());
+                col = new DoubleColumn(value == null ? null : value.doubleValue());
+                break;
+            }
+            case TIMESTAMP: {
+                Long value = (Long) o.getField(field.getName());
+
+                if ("MILLISECOND".equals(timeStampUnit)) {
+                    // MILLISECOND, 13位精度,直接 new Date()
+                    col = new DateColumn(value == null ? null : new Date(value));
+                }
+                else if ("SECOND".equals(timeStampUnit)){
+                    col = new DateColumn(value == null ? null : new Date(value * 1000));
+                }
+                else {
+                    // 默认都是 MICROSECOND, 16位精度, 和之前的逻辑保持一致。
+                    col = new DateColumn(value == null ? null : new Date(value / 1000));
+                }
+                break;
+            }
+            default:
+                throw new RuntimeException("Unknown column type: " + field.getType());
+        }
+        
+        return col;
+    }
+    
+}
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubWriterErrorCode.java b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubWriterErrorCode.java
new file mode 100644
index 0000000000..c8633ea8a8
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/DatahubWriterErrorCode.java
@@ -0,0 +1,37 @@
+package com.alibaba.datax.plugin.reader.datahubreader;
+
+import com.alibaba.datax.common.spi.ErrorCode;
+import com.alibaba.datax.common.util.MessageSource;
+
+public enum DatahubWriterErrorCode implements ErrorCode {
+    MISSING_REQUIRED_VALUE("DatahubWriter-01", MessageSource.loadResourceBundle(DatahubWriterErrorCode.class).message("errorcode.missing_required_value")),
+    INVALID_CONFIG_VALUE("DatahubWriter-02", MessageSource.loadResourceBundle(DatahubWriterErrorCode.class).message("errorcode.invalid_config_value")),
+    GET_TOPOIC_INFO_FAIL("DatahubWriter-03", MessageSource.loadResourceBundle(DatahubWriterErrorCode.class).message("errorcode.get_topic_info_fail")),
+    WRITE_DATAHUB_FAIL("DatahubWriter-04", MessageSource.loadResourceBundle(DatahubWriterErrorCode.class).message("errorcode.write_datahub_fail")),
+    SCHEMA_NOT_MATCH("DatahubWriter-05", MessageSource.loadResourceBundle(DatahubWriterErrorCode.class).message("errorcode.schema_not_match")),
+    ;
+
+    private final String code;
+    private final String description;
+
+    private DatahubWriterErrorCode(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    @Override
+    public String getCode() {
+        return this.code;
+    }
+
+    @Override
+    public String getDescription() {
+        return this.description;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("Code:[%s], Description:[%s]. ", this.code,
+                this.description);
+    }
+}
\ No newline at end of file
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/Key.java b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/Key.java
new file mode 100644
index 0000000000..3cb84b4bc9
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/Key.java
@@ -0,0 +1,35 @@
+package com.alibaba.datax.plugin.reader.datahubreader;
+
+public final class Key {
+
+    /**
+     * 此处声明插件用到的需要插件使用者提供的配置项
+     */
+    public static final String ENDPOINT = "endpoint";
+
+    public static final String ACCESSKEYID = "accessId";
+
+    public static final String ACCESSKEYSECRET = "accessKey";
+
+    public static final String PROJECT = "project";
+    
+    public static final String TOPIC = "topic";
+        
+    public static final String BEGINDATETIME = "beginDateTime";
+    
+    public static final String ENDDATETIME = "endDateTime";
+
+    public static final String BATCHSIZE = "batchSize";
+    
+    public static final String COLUMN = "column";
+    
+    public static final String SHARDID = "shardId";
+
+    public static final String CONFIG_KEY_ENDPOINT = "endpoint";
+    public static final String CONFIG_KEY_ACCESS_ID = "accessId";
+    public static final String CONFIG_KEY_ACCESS_KEY = "accessKey";
+
+
+    public static final String TIMESTAMP_UNIT = "timeStampUnit";
+    
+}
\ No newline at end of file
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings.properties b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings.properties
new file mode 100644
index 0000000000..e85c8ab321
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings.properties
@@ -0,0 +1,5 @@
+errorcode.missing_required_value=\u60A8\u7F3A\u5931\u4E86\u5FC5\u987B\u586B\u5199\u7684\u53C2\u6570\u503C.
+errorcode.invalid_config_value=\u60A8\u7684\u53C2\u6570\u914D\u7F6E\u9519\u8BEF.
+errorcode.get_topic_info_fail=\u83B7\u53D6shard\u5217\u8868\u5931\u8D25.
+errorcode.write_datahub_fail=\u5199\u6570\u636E\u5931\u8D25.
+errorcode.schema_not_match=\u6570\u636E\u683C\u5F0F\u9519\u8BEF.
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_en_US.properties b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_en_US.properties
new file mode 100644
index 0000000000..31a291e6db
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_en_US.properties
@@ -0,0 +1,5 @@
+errorcode.missing_required_value=\u60A8\u7F3A\u5931\u4E86\u5FC5\u987B\u586B\u5199\u7684\u53C2\u6570\u503C.
+errorcode.invalid_config_value=\u60A8\u7684\u53C2\u6570\u914D\u7F6E\u9519\u8BEF.
+errorcode.get_topic_info_fail=\u83B7\u53D6shard\u5217\u8868\u5931\u8D25.
+errorcode.write_datahub_fail=\u5199\u6570\u636E\u5931\u8D25.
+errorcode.schema_not_match=\u6570\u636E\u683C\u5F0F\u9519\u8BEF.
\ No newline at end of file
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_ja_JP.properties b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_ja_JP.properties
new file mode 100644
index 0000000000..31a291e6db
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_ja_JP.properties
@@ -0,0 +1,5 @@
+errorcode.missing_required_value=\u60A8\u7F3A\u5931\u4E86\u5FC5\u987B\u586B\u5199\u7684\u53C2\u6570\u503C.
+errorcode.invalid_config_value=\u60A8\u7684\u53C2\u6570\u914D\u7F6E\u9519\u8BEF.
+errorcode.get_topic_info_fail=\u83B7\u53D6shard\u5217\u8868\u5931\u8D25.
+errorcode.write_datahub_fail=\u5199\u6570\u636E\u5931\u8D25.
+errorcode.schema_not_match=\u6570\u636E\u683C\u5F0F\u9519\u8BEF.
\ No newline at end of file
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_zh_CN.properties b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_zh_CN.properties
new file mode 100644
index 0000000000..31a291e6db
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_zh_CN.properties
@@ -0,0 +1,5 @@
+errorcode.missing_required_value=\u60A8\u7F3A\u5931\u4E86\u5FC5\u987B\u586B\u5199\u7684\u53C2\u6570\u503C.
+errorcode.invalid_config_value=\u60A8\u7684\u53C2\u6570\u914D\u7F6E\u9519\u8BEF.
+errorcode.get_topic_info_fail=\u83B7\u53D6shard\u5217\u8868\u5931\u8D25.
+errorcode.write_datahub_fail=\u5199\u6570\u636E\u5931\u8D25.
+errorcode.schema_not_match=\u6570\u636E\u683C\u5F0F\u9519\u8BEF.
\ No newline at end of file
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_zh_HK.properties b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_zh_HK.properties
new file mode 100644
index 0000000000..c6a3a0e01d
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_zh_HK.properties
@@ -0,0 +1,9 @@
+errorcode.missing_required_value=\u60A8\u7F3A\u5931\u4E86\u5FC5\u987B\u586B\u5199\u7684\u53C2\u6570\u503C.
+errorcode.invalid_config_value=\u60A8\u7684\u53C2\u6570\u914D\u7F6E\u9519\u8BEF.
+errorcode.get_topic_info_fail=\u83B7\u53D6shard\u5217\u8868\u5931\u8D25.
+errorcode.write_datahub_fail=\u5199\u6570\u636E\u5931\u8D25.
+errorcode.schema_not_match=\u6570\u636E\u683C\u5F0F\u9519\u8BEF.errorcode.missing_required_value=您缺失了必須填寫的參數值.
+errorcode.invalid_config_value=您的參數配寘錯誤.
+errorcode.get_topic_info_fail=獲取shard清單失敗.
+errorcode.write_datahub_fail=寫數據失敗.
+errorcode.schema_not_match=數據格式錯誤.
diff --git a/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_zh_TW.properties b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_zh_TW.properties
new file mode 100644
index 0000000000..c6a3a0e01d
--- /dev/null
+++ b/datahubreader/src/main/java/com/alibaba/datax/plugin/reader/datahubreader/LocalStrings_zh_TW.properties
@@ -0,0 +1,9 @@
+errorcode.missing_required_value=\u60A8\u7F3A\u5931\u4E86\u5FC5\u987B\u586B\u5199\u7684\u53C2\u6570\u503C.
+errorcode.invalid_config_value=\u60A8\u7684\u53C2\u6570\u914D\u7F6E\u9519\u8BEF.
+errorcode.get_topic_info_fail=\u83B7\u53D6shard\u5217\u8868\u5931\u8D25.
+errorcode.write_datahub_fail=\u5199\u6570\u636E\u5931\u8D25.
+errorcode.schema_not_match=\u6570\u636E\u683C\u5F0F\u9519\u8BEF.errorcode.missing_required_value=您缺失了必須填寫的參數值.
+errorcode.invalid_config_value=您的參數配寘錯誤.
+errorcode.get_topic_info_fail=獲取shard清單失敗.
+errorcode.write_datahub_fail=寫數據失敗.
+errorcode.schema_not_match=數據格式錯誤.
diff --git a/datahubreader/src/main/resources/job_config_template.json b/datahubreader/src/main/resources/job_config_template.json
new file mode 100644
index 0000000000..eaf89804c6
--- /dev/null
+++ b/datahubreader/src/main/resources/job_config_template.json
@@ -0,0 +1,14 @@
+{
+    "name": "datahubreader",
+    "parameter": {
+        "endpoint":"",
+        "accessId": "",
+        "accessKey": "",
+        "project": "",
+        "topic": "",
+        "beginDateTime": "20180913121019",
+        "endDateTime": "20180913121119",
+        "batchSize": 1024,
+        "column": []
+    }
+}
\ No newline at end of file
diff --git a/datahubreader/src/main/resources/plugin.json b/datahubreader/src/main/resources/plugin.json
new file mode 100644
index 0000000000..47b1c86b21
--- /dev/null
+++ b/datahubreader/src/main/resources/plugin.json
@@ -0,0 +1,6 @@
+{
+    "name": "datahubreader",
+    "class": "com.alibaba.datax.plugin.reader.datahubreader.DatahubReader",
+    "description": "datahub reader",
+    "developer": "alibaba"
+}
\ No newline at end of file
diff --git a/datahubwriter/pom.xml b/datahubwriter/pom.xml
new file mode 100644
index 0000000000..1ee1fe9b0a
--- /dev/null
+++ b/datahubwriter/pom.xml
@@ -0,0 +1,79 @@
+
+
+    
+        datax-all
+        com.alibaba.datax
+        0.0.1-SNAPSHOT
+    
+    4.0.0
+
+    datahubwriter
+
+    0.0.1-SNAPSHOT
+
+    
+        
+            com.alibaba.datax
+            datax-common
+            ${datax-project-version}
+            
+                
+                    slf4j-log4j12
+                    org.slf4j
+                
+            
+        
+        
+            org.slf4j
+            slf4j-api
+        
+        
+            ch.qos.logback
+            logback-classic
+        
+        
+            com.aliyun.datahub
+            aliyun-sdk-datahub
+            2.21.6-public
+        
+        
+            junit
+            junit
+            4.12
+            test
+        
+    
+
+    
+        
+            
+            
+                maven-compiler-plugin
+                
+                    ${jdk-version}
+                    ${jdk-version}
+                    ${project-sourceEncoding}
+                
+            
+            
+            
+                maven-assembly-plugin
+                
+                    
+                        src/main/assembly/package.xml
+                    
+                    datax
+                
+                
+                    
+                        dwzip
+                        package
+                        
+                            single
+                        
+                    
+                
+            
+        
+    
+
diff --git a/datahubwriter/src/main/assembly/package.xml b/datahubwriter/src/main/assembly/package.xml
new file mode 100644
index 0000000000..aaef9f997f
--- /dev/null
+++ b/datahubwriter/src/main/assembly/package.xml
@@ -0,0 +1,34 @@
+
+    
+    
+        dir
+    
+    false
+    
+        
+            src/main/resources
+            
+                plugin.json
+            
+            plugin/writer/datahubwriter
+        
+        
+            target/
+            
+                datahubwriter-0.0.1-SNAPSHOT.jar
+            
+            plugin/writer/datahubwriter
+        
+    
+
+    
+        
+            false
+            plugin/writer/datahubwriter/libs
+            runtime
+        
+    
+
diff --git a/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/DatahubClientHelper.java b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/DatahubClientHelper.java
new file mode 100644
index 0000000000..c25d1210d8
--- /dev/null
+++ b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/DatahubClientHelper.java
@@ -0,0 +1,43 @@
+package com.alibaba.datax.plugin.writer.datahubwriter;
+
+import org.apache.commons.lang3.StringUtils;
+
+import com.alibaba.datax.common.util.Configuration;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.TypeReference;
+import com.aliyun.datahub.client.DatahubClient;
+import com.aliyun.datahub.client.DatahubClientBuilder;
+import com.aliyun.datahub.client.auth.Account;
+import com.aliyun.datahub.client.auth.AliyunAccount;
+import com.aliyun.datahub.client.common.DatahubConfig;
+import com.aliyun.datahub.client.http.HttpConfig;
+
+public class DatahubClientHelper {
+    public static DatahubClient getDatahubClient(Configuration jobConfig) {
+        String accessId = jobConfig.getNecessaryValue(Key.CONFIG_KEY_ACCESS_ID,
+                DatahubWriterErrorCode.MISSING_REQUIRED_VALUE);
+        String accessKey = jobConfig.getNecessaryValue(Key.CONFIG_KEY_ACCESS_KEY,
+                DatahubWriterErrorCode.MISSING_REQUIRED_VALUE);
+        String endpoint = jobConfig.getNecessaryValue(Key.CONFIG_KEY_ENDPOINT,
+                DatahubWriterErrorCode.MISSING_REQUIRED_VALUE);
+        Account account = new AliyunAccount(accessId, accessKey);
+        // 是否开启二进制传输,服务端2.12版本开始支持
+        boolean enableBinary = jobConfig.getBool("enableBinary", false);
+        DatahubConfig datahubConfig = new DatahubConfig(endpoint, account, enableBinary);
+        // HttpConfig可不设置,不设置时采用默认值
+        // 读写数据推荐打开网络传输 LZ4压缩
+        HttpConfig httpConfig = null;
+        String httpConfigStr = jobConfig.getString("httpConfig");
+        if (StringUtils.isNotBlank(httpConfigStr)) {
+            httpConfig = JSON.parseObject(httpConfigStr, new TypeReference() {
+            });
+        }
+
+        DatahubClientBuilder builder = DatahubClientBuilder.newBuilder().setDatahubConfig(datahubConfig);
+        if (null != httpConfig) {
+            builder.setHttpConfig(httpConfig);
+        }
+        DatahubClient datahubClient = builder.build();
+        return datahubClient;
+    }
+}
diff --git a/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/DatahubWriter.java b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/DatahubWriter.java
new file mode 100644
index 0000000000..cd414fc57b
--- /dev/null
+++ b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/DatahubWriter.java
@@ -0,0 +1,355 @@
+package com.alibaba.datax.plugin.writer.datahubwriter;
+
+import com.alibaba.datax.common.element.Column;
+import com.alibaba.datax.common.element.Record;
+import com.alibaba.datax.common.exception.DataXException;
+import com.alibaba.datax.common.plugin.RecordReceiver;
+import com.alibaba.datax.common.spi.Writer;
+import com.alibaba.datax.common.util.Configuration;
+import com.alibaba.datax.common.util.DataXCaseEnvUtil;
+import com.alibaba.datax.common.util.RetryUtil;
+import com.alibaba.fastjson2.JSON;
+import com.aliyun.datahub.client.DatahubClient;
+import com.aliyun.datahub.client.model.FieldType;
+import com.aliyun.datahub.client.model.GetTopicResult;
+import com.aliyun.datahub.client.model.ListShardResult;
+import com.aliyun.datahub.client.model.PutErrorEntry;
+import com.aliyun.datahub.client.model.PutRecordsResult;
+import com.aliyun.datahub.client.model.RecordEntry;
+import com.aliyun.datahub.client.model.RecordSchema;
+import com.aliyun.datahub.client.model.RecordType;
+import com.aliyun.datahub.client.model.ShardEntry;
+import com.aliyun.datahub.client.model.ShardState;
+import com.aliyun.datahub.client.model.TupleRecordData;
+
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Random;
+import java.util.concurrent.Callable;
+
+public class DatahubWriter extends Writer {
+
+    /**
+     * Job 中的方法仅执行一次,Task 中方法会由框架启动多个 Task 线程并行执行。
+     * 

+ * 整个 Writer 执行流程是: + *

+     * Job类init-->prepare-->split
+     *
+     *                          Task类init-->prepare-->startWrite-->post-->destroy
+     *                          Task类init-->prepare-->startWrite-->post-->destroy
+     *
+     *                                                                            Job类post-->destroy
+     * 
+ */ + public static class Job extends Writer.Job { + private static final Logger LOG = LoggerFactory + .getLogger(Job.class); + + private Configuration jobConfig = null; + + @Override + public void init() { + this.jobConfig = super.getPluginJobConf(); + jobConfig.getNecessaryValue(Key.CONFIG_KEY_ENDPOINT, DatahubWriterErrorCode.MISSING_REQUIRED_VALUE); + jobConfig.getNecessaryValue(Key.CONFIG_KEY_ACCESS_ID, DatahubWriterErrorCode.MISSING_REQUIRED_VALUE); + jobConfig.getNecessaryValue(Key.CONFIG_KEY_ACCESS_KEY, DatahubWriterErrorCode.MISSING_REQUIRED_VALUE); + jobConfig.getNecessaryValue(Key.CONFIG_KEY_PROJECT, DatahubWriterErrorCode.MISSING_REQUIRED_VALUE); + jobConfig.getNecessaryValue(Key.CONFIG_KEY_TOPIC, DatahubWriterErrorCode.MISSING_REQUIRED_VALUE); + } + + @Override + public void prepare() { + String project = jobConfig.getNecessaryValue(Key.CONFIG_KEY_PROJECT, + DatahubWriterErrorCode.MISSING_REQUIRED_VALUE); + String topic = jobConfig.getNecessaryValue(Key.CONFIG_KEY_TOPIC, + DatahubWriterErrorCode.MISSING_REQUIRED_VALUE); + RecordType recordType = null; + DatahubClient client = DatahubClientHelper.getDatahubClient(this.jobConfig); + try { + GetTopicResult getTopicResult = client.getTopic(project, topic); + recordType = getTopicResult.getRecordType(); + } catch (Exception e) { + LOG.warn("get topic type error: {}", e.getMessage()); + } + if (null != recordType) { + if (recordType == RecordType.BLOB) { + throw DataXException.asDataXException(DatahubWriterErrorCode.WRITE_DATAHUB_FAIL, + "DatahubWriter only support 'Tuple' RecordType now, but your RecordType is 'BLOB'"); + } + } + } + + @Override + public List split(int mandatoryNumber) { + List configs = new ArrayList(); + for (int i = 0; i < mandatoryNumber; ++i) { + configs.add(jobConfig.clone()); + } + return configs; + } + + @Override + public void post() {} + + @Override + public void destroy() {} + + } + + public static class Task extends Writer.Task { + private static final Logger LOG = LoggerFactory + .getLogger(Task.class); + private static final List FATAL_ERRORS_DEFAULT = Arrays.asList( + "InvalidParameterM", + "MalformedRecord", + "INVALID_SHARDID", + "NoSuchTopic", + "NoSuchShard" + ); + + private Configuration taskConfig; + private DatahubClient client; + private String project; + private String topic; + private List shards; + private int maxCommitSize; + private int maxRetryCount; + private RecordSchema schema; + private long retryInterval; + private Random random; + private List column; + private List columnIndex; + private boolean enableColumnConfig; + private List fatalErrors; + + @Override + public void init() { + this.taskConfig = super.getPluginJobConf(); + project = taskConfig.getNecessaryValue(Key.CONFIG_KEY_PROJECT, DatahubWriterErrorCode.MISSING_REQUIRED_VALUE); + topic = taskConfig.getNecessaryValue(Key.CONFIG_KEY_TOPIC, DatahubWriterErrorCode.MISSING_REQUIRED_VALUE); + maxCommitSize = taskConfig.getInt(Key.CONFIG_KEY_MAX_COMMIT_SIZE, 1024*1024); + maxRetryCount = taskConfig.getInt(Key.CONFIG_KEY_MAX_RETRY_COUNT, 500); + this.retryInterval = taskConfig.getInt(Key.RETRY_INTERVAL, 650); + this.random = new Random(); + this.column = this.taskConfig.getList(Key.CONFIG_KEY_COLUMN, String.class); + // ["*"] + if (null != this.column && 1 == this.column.size()) { + if (StringUtils.equals("*", this.column.get(0))) { + this.column = null; + } + } + this.columnIndex = new ArrayList(); + // 留个开关保平安 + this.enableColumnConfig = this.taskConfig.getBool("enableColumnConfig", true); + this.fatalErrors = this.taskConfig.getList("fatalErrors", Task.FATAL_ERRORS_DEFAULT, String.class); + this.client = DatahubClientHelper.getDatahubClient(this.taskConfig); + } + + @Override + public void prepare() { + final String shardIdConfig = this.taskConfig.getString(Key.CONFIG_KEY_SHARD_ID); + this.shards = new ArrayList(); + try { + RetryUtil.executeWithRetry(new Callable() { + @Override + public Void call() throws Exception { + ListShardResult result = client.listShard(project, topic); + if (StringUtils.isNotBlank(shardIdConfig)) { + shards.add(shardIdConfig); + } else { + for (ShardEntry shard : result.getShards()) { + if (shard.getState() == ShardState.ACTIVE || shard.getState() == ShardState.OPENING) { + shards.add(shard.getShardId()); + } + } + } + schema = client.getTopic(project, topic).getRecordSchema(); + return null; + } + }, DataXCaseEnvUtil.getRetryTimes(5), DataXCaseEnvUtil.getRetryInterval(10000L), DataXCaseEnvUtil.getRetryExponential(false)); + } catch (Exception e) { + throw DataXException.asDataXException(DatahubWriterErrorCode.GET_TOPOIC_INFO_FAIL, + "get topic info failed", e); + } + LOG.info("datahub topic {} shard to write: {}", this.topic, JSON.toJSONString(this.shards)); + LOG.info("datahub topic {} has schema: {}", this.topic, JSON.toJSONString(this.schema)); + + // 根据 schmea 顺序 和用户配置的 column,计算写datahub的顺序关系,以支持列换序 + // 后续统一使用 columnIndex 的顺位关系写 datahub + int totalSize = this.schema.getFields().size(); + if (null != this.column && !this.column.isEmpty() && this.enableColumnConfig) { + for (String eachCol : this.column) { + int indexFound = -1; + for (int i = 0; i < totalSize; i++) { + // warn: 大小写ignore + if (StringUtils.equalsIgnoreCase(eachCol, this.schema.getField(i).getName())) { + indexFound = i; + break; + } + } + if (indexFound >= 0) { + this.columnIndex.add(indexFound); + } else { + throw DataXException.asDataXException(DatahubWriterErrorCode.SCHEMA_NOT_MATCH, + String.format("can not find column %s in datahub topic %s", eachCol, this.topic)); + } + } + } else { + for (int i = 0; i < totalSize; i++) { + this.columnIndex.add(i); + } + } + } + + @Override + public void startWrite(RecordReceiver recordReceiver) { + Record record; + List records = new ArrayList(); + String shardId = null; + if (1 == this.shards.size()) { + shardId = shards.get(0); + } else { + shardId = shards.get(this.random.nextInt(shards.size())); + } + int commitSize = 0; + try { + while ((record = recordReceiver.getFromReader()) != null) { + RecordEntry dhRecord = convertRecord(record, shardId); + if (dhRecord != null) { + records.add(dhRecord); + } + commitSize += record.getByteSize(); + if (commitSize >= maxCommitSize) { + commit(records); + records.clear(); + commitSize = 0; + if (1 == this.shards.size()) { + shardId = shards.get(0); + } else { + shardId = shards.get(this.random.nextInt(shards.size())); + } + } + } + if (commitSize > 0) { + commit(records); + } + } catch (Exception e) { + throw DataXException.asDataXException( + DatahubWriterErrorCode.WRITE_DATAHUB_FAIL, e); + } + } + + @Override + public void post() {} + + @Override + public void destroy() {} + + private void commit(List records) throws InterruptedException { + PutRecordsResult result = client.putRecords(project, topic, records); + if (result.getFailedRecordCount() > 0) { + for (int i = 0; i < maxRetryCount; ++i) { + boolean limitExceededMessagePrinted = false; + for (PutErrorEntry error : result.getPutErrorEntries()) { + // 如果是 LimitExceeded 这样打印日志,不能每行记录打印一次了 + if (StringUtils.equalsIgnoreCase("LimitExceeded", error.getErrorcode())) { + if (!limitExceededMessagePrinted) { + LOG.warn("write record error, request id: {}, error code: {}, error message: {}", + result.getRequestId(), error.getErrorcode(), error.getMessage()); + limitExceededMessagePrinted = true; + } + } else { + LOG.error("write record error, request id: {}, error code: {}, error message: {}", + result.getRequestId(), error.getErrorcode(), error.getMessage()); + } + if (this.fatalErrors.contains(error.getErrorcode())) { + throw DataXException.asDataXException( + DatahubWriterErrorCode.WRITE_DATAHUB_FAIL, + error.getMessage()); + } + } + + if (this.retryInterval >= 0) { + Thread.sleep(this.retryInterval); + } else { + Thread.sleep(new Random().nextInt(700) + 300); + } + + result = client.putRecords(project, topic, result.getFailedRecords()); + if (result.getFailedRecordCount() == 0) { + return; + } + } + throw DataXException.asDataXException( + DatahubWriterErrorCode.WRITE_DATAHUB_FAIL, + "write datahub failed"); + } + } + + private RecordEntry convertRecord(Record dxRecord, String shardId) { + try { + RecordEntry dhRecord = new RecordEntry(); + dhRecord.setShardId(shardId); + TupleRecordData data = new TupleRecordData(this.schema); + for (int i = 0; i < this.columnIndex.size(); ++i) { + int orderInSchema = this.columnIndex.get(i); + FieldType type = this.schema.getField(orderInSchema).getType(); + Column column = dxRecord.getColumn(i); + switch (type) { + case BIGINT: + data.setField(orderInSchema, column.asLong()); + break; + case DOUBLE: + data.setField(orderInSchema, column.asDouble()); + break; + case STRING: + data.setField(orderInSchema, column.asString()); + break; + case BOOLEAN: + data.setField(orderInSchema, column.asBoolean()); + break; + case TIMESTAMP: + if (null == column.asDate()) { + data.setField(orderInSchema, null); + } else { + data.setField(orderInSchema, column.asDate().getTime() * 1000); + } + break; + case DECIMAL: + // warn + data.setField(orderInSchema, column.asBigDecimal()); + break; + case INTEGER: + data.setField(orderInSchema, column.asLong()); + break; + case FLOAT: + data.setField(orderInSchema, column.asDouble()); + break; + case TINYINT: + data.setField(orderInSchema, column.asLong()); + break; + case SMALLINT: + data.setField(orderInSchema, column.asLong()); + break; + default: + throw DataXException.asDataXException( + DatahubWriterErrorCode.SCHEMA_NOT_MATCH, + String.format("does not support type: %s", type)); + } + } + dhRecord.setRecordData(data); + return dhRecord; + } catch (Exception e) { + super.getTaskPluginCollector().collectDirtyRecord(dxRecord, e, "convert recor failed"); + } + return null; + } + } + +} \ No newline at end of file diff --git a/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/DatahubWriterErrorCode.java b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/DatahubWriterErrorCode.java new file mode 100644 index 0000000000..ad03abd199 --- /dev/null +++ b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/DatahubWriterErrorCode.java @@ -0,0 +1,37 @@ +package com.alibaba.datax.plugin.writer.datahubwriter; + +import com.alibaba.datax.common.spi.ErrorCode; +import com.alibaba.datax.common.util.MessageSource; + +public enum DatahubWriterErrorCode implements ErrorCode { + MISSING_REQUIRED_VALUE("DatahubWriter-01", MessageSource.loadResourceBundle(DatahubWriterErrorCode.class).message("errorcode.missing_required_value")), + INVALID_CONFIG_VALUE("DatahubWriter-02", MessageSource.loadResourceBundle(DatahubWriterErrorCode.class).message("errorcode.invalid_config_value")), + GET_TOPOIC_INFO_FAIL("DatahubWriter-03", MessageSource.loadResourceBundle(DatahubWriterErrorCode.class).message("errorcode.get_topic_info_fail")), + WRITE_DATAHUB_FAIL("DatahubWriter-04", MessageSource.loadResourceBundle(DatahubWriterErrorCode.class).message("errorcode.write_datahub_fail")), + SCHEMA_NOT_MATCH("DatahubWriter-05", MessageSource.loadResourceBundle(DatahubWriterErrorCode.class).message("errorcode.schema_not_match")), + ; + + private final String code; + private final String description; + + private DatahubWriterErrorCode(String code, String description) { + this.code = code; + this.description = description; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + return String.format("Code:[%s], Description:[%s]. ", this.code, + this.description); + } +} \ No newline at end of file diff --git a/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/Key.java b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/Key.java new file mode 100644 index 0000000000..5f17923470 --- /dev/null +++ b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/Key.java @@ -0,0 +1,26 @@ +package com.alibaba.datax.plugin.writer.datahubwriter; + +public final class Key { + + /** + * 此处声明插件用到的需要插件使用者提供的配置项 + */ + public static final String CONFIG_KEY_ENDPOINT = "endpoint"; + public static final String CONFIG_KEY_ACCESS_ID = "accessId"; + public static final String CONFIG_KEY_ACCESS_KEY = "accessKey"; + public static final String CONFIG_KEY_PROJECT = "project"; + public static final String CONFIG_KEY_TOPIC = "topic"; + public static final String CONFIG_KEY_WRITE_MODE = "mode"; + public static final String CONFIG_KEY_SHARD_ID = "shardId"; + public static final String CONFIG_KEY_MAX_COMMIT_SIZE = "maxCommitSize"; + public static final String CONFIG_KEY_MAX_RETRY_COUNT = "maxRetryCount"; + + public static final String CONFIG_VALUE_SEQUENCE_MODE = "sequence"; + public static final String CONFIG_VALUE_RANDOM_MODE = "random"; + + public final static String MAX_RETRY_TIME = "maxRetryTime"; + + public final static String RETRY_INTERVAL = "retryInterval"; + + public final static String CONFIG_KEY_COLUMN = "column"; +} diff --git a/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings.properties b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings.properties new file mode 100644 index 0000000000..e85c8ab321 --- /dev/null +++ b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings.properties @@ -0,0 +1,5 @@ +errorcode.missing_required_value=\u60A8\u7F3A\u5931\u4E86\u5FC5\u987B\u586B\u5199\u7684\u53C2\u6570\u503C. +errorcode.invalid_config_value=\u60A8\u7684\u53C2\u6570\u914D\u7F6E\u9519\u8BEF. +errorcode.get_topic_info_fail=\u83B7\u53D6shard\u5217\u8868\u5931\u8D25. +errorcode.write_datahub_fail=\u5199\u6570\u636E\u5931\u8D25. +errorcode.schema_not_match=\u6570\u636E\u683C\u5F0F\u9519\u8BEF. diff --git a/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_en_US.properties b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_en_US.properties new file mode 100644 index 0000000000..31a291e6db --- /dev/null +++ b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_en_US.properties @@ -0,0 +1,5 @@ +errorcode.missing_required_value=\u60A8\u7F3A\u5931\u4E86\u5FC5\u987B\u586B\u5199\u7684\u53C2\u6570\u503C. +errorcode.invalid_config_value=\u60A8\u7684\u53C2\u6570\u914D\u7F6E\u9519\u8BEF. +errorcode.get_topic_info_fail=\u83B7\u53D6shard\u5217\u8868\u5931\u8D25. +errorcode.write_datahub_fail=\u5199\u6570\u636E\u5931\u8D25. +errorcode.schema_not_match=\u6570\u636E\u683C\u5F0F\u9519\u8BEF. \ No newline at end of file diff --git a/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_ja_JP.properties b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..31a291e6db --- /dev/null +++ b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_ja_JP.properties @@ -0,0 +1,5 @@ +errorcode.missing_required_value=\u60A8\u7F3A\u5931\u4E86\u5FC5\u987B\u586B\u5199\u7684\u53C2\u6570\u503C. +errorcode.invalid_config_value=\u60A8\u7684\u53C2\u6570\u914D\u7F6E\u9519\u8BEF. +errorcode.get_topic_info_fail=\u83B7\u53D6shard\u5217\u8868\u5931\u8D25. +errorcode.write_datahub_fail=\u5199\u6570\u636E\u5931\u8D25. +errorcode.schema_not_match=\u6570\u636E\u683C\u5F0F\u9519\u8BEF. \ No newline at end of file diff --git a/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_zh_CN.properties b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..31a291e6db --- /dev/null +++ b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_zh_CN.properties @@ -0,0 +1,5 @@ +errorcode.missing_required_value=\u60A8\u7F3A\u5931\u4E86\u5FC5\u987B\u586B\u5199\u7684\u53C2\u6570\u503C. +errorcode.invalid_config_value=\u60A8\u7684\u53C2\u6570\u914D\u7F6E\u9519\u8BEF. +errorcode.get_topic_info_fail=\u83B7\u53D6shard\u5217\u8868\u5931\u8D25. +errorcode.write_datahub_fail=\u5199\u6570\u636E\u5931\u8D25. +errorcode.schema_not_match=\u6570\u636E\u683C\u5F0F\u9519\u8BEF. \ No newline at end of file diff --git a/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_zh_HK.properties b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..c6a3a0e01d --- /dev/null +++ b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_zh_HK.properties @@ -0,0 +1,9 @@ +errorcode.missing_required_value=\u60A8\u7F3A\u5931\u4E86\u5FC5\u987B\u586B\u5199\u7684\u53C2\u6570\u503C. +errorcode.invalid_config_value=\u60A8\u7684\u53C2\u6570\u914D\u7F6E\u9519\u8BEF. +errorcode.get_topic_info_fail=\u83B7\u53D6shard\u5217\u8868\u5931\u8D25. +errorcode.write_datahub_fail=\u5199\u6570\u636E\u5931\u8D25. +errorcode.schema_not_match=\u6570\u636E\u683C\u5F0F\u9519\u8BEF.errorcode.missing_required_value=您缺失了必須填寫的參數值. +errorcode.invalid_config_value=您的參數配寘錯誤. +errorcode.get_topic_info_fail=獲取shard清單失敗. +errorcode.write_datahub_fail=寫數據失敗. +errorcode.schema_not_match=數據格式錯誤. diff --git a/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_zh_TW.properties b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..c6a3a0e01d --- /dev/null +++ b/datahubwriter/src/main/java/com/alibaba/datax/plugin/writer/datahubwriter/LocalStrings_zh_TW.properties @@ -0,0 +1,9 @@ +errorcode.missing_required_value=\u60A8\u7F3A\u5931\u4E86\u5FC5\u987B\u586B\u5199\u7684\u53C2\u6570\u503C. +errorcode.invalid_config_value=\u60A8\u7684\u53C2\u6570\u914D\u7F6E\u9519\u8BEF. +errorcode.get_topic_info_fail=\u83B7\u53D6shard\u5217\u8868\u5931\u8D25. +errorcode.write_datahub_fail=\u5199\u6570\u636E\u5931\u8D25. +errorcode.schema_not_match=\u6570\u636E\u683C\u5F0F\u9519\u8BEF.errorcode.missing_required_value=您缺失了必須填寫的參數值. +errorcode.invalid_config_value=您的參數配寘錯誤. +errorcode.get_topic_info_fail=獲取shard清單失敗. +errorcode.write_datahub_fail=寫數據失敗. +errorcode.schema_not_match=數據格式錯誤. diff --git a/datahubwriter/src/main/resources/job_config_template.json b/datahubwriter/src/main/resources/job_config_template.json new file mode 100644 index 0000000000..8b0b41aef8 --- /dev/null +++ b/datahubwriter/src/main/resources/job_config_template.json @@ -0,0 +1,14 @@ +{ + "name": "datahubwriter", + "parameter": { + "endpoint":"", + "accessId": "", + "accessKey": "", + "project": "", + "topic": "", + "mode": "random", + "shardId": "", + "maxCommitSize": 524288, + "maxRetryCount": 500 + } +} \ No newline at end of file diff --git a/datahubwriter/src/main/resources/plugin.json b/datahubwriter/src/main/resources/plugin.json new file mode 100644 index 0000000000..91c1729297 --- /dev/null +++ b/datahubwriter/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "datahubwriter", + "class": "com.alibaba.datax.plugin.writer.datahubwriter.DatahubWriter", + "description": "datahub writer", + "developer": "alibaba" +} \ No newline at end of file diff --git a/datax-example/datax-example-core/pom.xml b/datax-example/datax-example-core/pom.xml new file mode 100644 index 0000000000..6a2e9e8ee9 --- /dev/null +++ b/datax-example/datax-example-core/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + com.alibaba.datax + datax-example + 0.0.1-SNAPSHOT + + + datax-example-core + + + 8 + 8 + UTF-8 + + + \ No newline at end of file diff --git a/datax-example/datax-example-core/src/main/java/com/alibaba/datax/example/ExampleContainer.java b/datax-example/datax-example-core/src/main/java/com/alibaba/datax/example/ExampleContainer.java new file mode 100644 index 0000000000..a4229fd1e2 --- /dev/null +++ b/datax-example/datax-example-core/src/main/java/com/alibaba/datax/example/ExampleContainer.java @@ -0,0 +1,26 @@ +package com.alibaba.datax.example; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.core.Engine; +import com.alibaba.datax.example.util.ExampleConfigParser; + +/** + * {@code Date} 2023/8/6 11:22 + * + * @author fuyouj + */ + +public class ExampleContainer { + /** + * example对外暴露的启动入口 + * 使用前最好看下 datax-example/doc/README.MD + * @param jobPath 任务json绝对路径 + */ + public static void start(String jobPath) { + + Configuration configuration = ExampleConfigParser.parse(jobPath); + + Engine engine = new Engine(); + engine.start(configuration); + } +} diff --git a/datax-example/datax-example-core/src/main/java/com/alibaba/datax/example/Main.java b/datax-example/datax-example-core/src/main/java/com/alibaba/datax/example/Main.java new file mode 100644 index 0000000000..56bf9f0b6c --- /dev/null +++ b/datax-example/datax-example-core/src/main/java/com/alibaba/datax/example/Main.java @@ -0,0 +1,23 @@ +package com.alibaba.datax.example; + + +import com.alibaba.datax.example.util.PathUtil; + +/** + * @author fuyouj + */ +public class Main { + + /** + * 1.在example模块pom文件添加你依赖的的调试插件, + * 你可以直接打开本模块的pom文件,参考是如何引入streamreader,streamwriter + * 2. 在此处指定你的job文件 + */ + public static void main(String[] args) { + + String classPathJobPath = "/job/stream2stream.json"; + String absJobPath = PathUtil.getAbsolutePathFromClassPath(classPathJobPath); + ExampleContainer.start(absJobPath); + } + +} diff --git a/datax-example/datax-example-core/src/main/java/com/alibaba/datax/example/util/ExampleConfigParser.java b/datax-example/datax-example-core/src/main/java/com/alibaba/datax/example/util/ExampleConfigParser.java new file mode 100644 index 0000000000..6bbb4a23c9 --- /dev/null +++ b/datax-example/datax-example-core/src/main/java/com/alibaba/datax/example/util/ExampleConfigParser.java @@ -0,0 +1,154 @@ +package com.alibaba.datax.example.util; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.core.util.ConfigParser; +import com.alibaba.datax.core.util.FrameworkErrorCode; +import com.alibaba.datax.core.util.container.CoreConstant; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.file.Paths; +import java.util.*; + +/** + * @author fuyouj + */ +public class ExampleConfigParser { + private static final String CORE_CONF = "/example/conf/core.json"; + + private static final String PLUGIN_DESC_FILE = "plugin.json"; + + /** + * 指定Job配置路径,ConfigParser会解析Job、Plugin、Core全部信息,并以Configuration返回 + * 不同于Core的ConfigParser,这里的core,plugin 不依赖于编译后的datax.home,而是扫描程序编译后的target目录 + */ + public static Configuration parse(final String jobPath) { + + Configuration configuration = ConfigParser.parseJobConfig(jobPath); + configuration.merge(coreConfig(), + false); + + Map pluginTypeMap = new HashMap<>(); + String readerName = configuration.getString(CoreConstant.DATAX_JOB_CONTENT_READER_NAME); + String writerName = configuration.getString(CoreConstant.DATAX_JOB_CONTENT_WRITER_NAME); + pluginTypeMap.put(readerName, "reader"); + pluginTypeMap.put(writerName, "writer"); + Configuration pluginsDescConfig = parsePluginsConfig(pluginTypeMap); + configuration.merge(pluginsDescConfig, false); + return configuration; + } + + private static Configuration parsePluginsConfig(Map pluginTypeMap) { + + Configuration configuration = Configuration.newDefault(); + + //最初打算通过user.dir获取工作目录来扫描插件, + //但是user.dir在不同有一些不确定性,所以废弃了这个选择 + + for (File basePackage : runtimeBasePackages()) { + if (pluginTypeMap.isEmpty()) { + break; + } + scanPluginByPackage(basePackage, configuration, basePackage.listFiles(), pluginTypeMap); + } + if (!pluginTypeMap.isEmpty()) { + String failedPlugin = pluginTypeMap.keySet().toString(); + String message = "\nplugin %s load failed :ry to analyze the reasons from the following aspects.。\n" + + "1: Check if the name of the plugin is spelled correctly, and verify whether DataX supports this plugin\n" + + "2:Verify if the tag has been added under section in the pom file of the relevant plugin.\n" + + " src/main/resources\n" + + " \n" + + " **/*.*\n" + + " \n" + + " true\n" + + " \n [Refer to the streamreader pom file] \n" + + "3: Check that the datax-yourPlugin-example module imported your test plugin"; + message = String.format(message, failedPlugin); + throw DataXException.asDataXException(FrameworkErrorCode.PLUGIN_INIT_ERROR, message); + } + return configuration; + } + + /** + * 通过classLoader获取程序编译的输出目录 + * + * @return File[/datax-example/target/classes,xxReader/target/classes,xxWriter/target/classes] + */ + private static File[] runtimeBasePackages() { + List basePackages = new ArrayList<>(); + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + Enumeration resources = null; + try { + resources = classLoader.getResources(""); + } catch (IOException e) { + throw DataXException.asDataXException(e.getMessage()); + } + + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + File file = new File(resource.getFile()); + if (file.isDirectory()) { + basePackages.add(file); + } + } + + return basePackages.toArray(new File[0]); + } + + /** + * @param packageFile 编译出来的target/classes根目录 便于找到插件时设置插件的URL目录,设置根目录是最保险的方式 + * @param configuration pluginConfig + * @param files 待扫描文件 + * @param needPluginTypeMap 需要的插件 + */ + private static void scanPluginByPackage(File packageFile, + Configuration configuration, + File[] files, + Map needPluginTypeMap) { + if (files == null) { + return; + } + for (File file : files) { + if (file.isFile() && PLUGIN_DESC_FILE.equals(file.getName())) { + Configuration pluginDesc = Configuration.from(file); + String descPluginName = pluginDesc.getString("name", ""); + + if (needPluginTypeMap.containsKey(descPluginName)) { + + String type = needPluginTypeMap.get(descPluginName); + configuration.merge(parseOnePlugin(packageFile.getAbsolutePath(), type, descPluginName, pluginDesc), false); + needPluginTypeMap.remove(descPluginName); + + } + } else { + scanPluginByPackage(packageFile, configuration, file.listFiles(), needPluginTypeMap); + } + } + } + + + private static Configuration parseOnePlugin(String packagePath, + String pluginType, + String pluginName, + Configuration pluginDesc) { + //设置path 兼容jarLoader的加载方式URLClassLoader + pluginDesc.set("path", packagePath); + Configuration pluginConfInJob = Configuration.newDefault(); + pluginConfInJob.set( + String.format("plugin.%s.%s", pluginType, pluginName), + pluginDesc.getInternal()); + return pluginConfInJob; + } + + private static Configuration coreConfig() { + try { + URL resource = ExampleConfigParser.class.getResource(CORE_CONF); + return Configuration.from(Paths.get(resource.toURI()).toFile()); + } catch (Exception ignore) { + throw DataXException.asDataXException("Failed to load the configuration file core.json. " + + "Please check whether /example/conf/core.json exists!"); + } + } +} diff --git a/datax-example/datax-example-core/src/main/java/com/alibaba/datax/example/util/PathUtil.java b/datax-example/datax-example-core/src/main/java/com/alibaba/datax/example/util/PathUtil.java new file mode 100644 index 0000000000..e197fa7363 --- /dev/null +++ b/datax-example/datax-example-core/src/main/java/com/alibaba/datax/example/util/PathUtil.java @@ -0,0 +1,26 @@ +package com.alibaba.datax.example.util; + + +import com.alibaba.datax.common.exception.DataXException; + +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Paths; + +/** + * @author fuyouj + */ +public class PathUtil { + public static String getAbsolutePathFromClassPath(String path) { + URL resource = PathUtil.class.getResource(path); + try { + assert resource != null; + URI uri = resource.toURI(); + return Paths.get(uri).toString(); + } catch (NullPointerException | URISyntaxException e) { + throw DataXException.asDataXException("path error,please check whether the path is correct"); + } + + } +} diff --git a/datax-example/datax-example-core/src/main/resources/example/conf/core.json b/datax-example/datax-example-core/src/main/resources/example/conf/core.json new file mode 100755 index 0000000000..33281ac068 --- /dev/null +++ b/datax-example/datax-example-core/src/main/resources/example/conf/core.json @@ -0,0 +1,60 @@ +{ + "entry": { + "jvm": "-Xms1G -Xmx1G", + "environment": {} + }, + "common": { + "column": { + "datetimeFormat": "yyyy-MM-dd HH:mm:ss", + "timeFormat": "HH:mm:ss", + "dateFormat": "yyyy-MM-dd", + "extraFormats":["yyyyMMdd"], + "timeZone": "GMT+8", + "encoding": "utf-8" + } + }, + "core": { + "dataXServer": { + "address": "http://localhost:7001/api", + "timeout": 10000, + "reportDataxLog": false, + "reportPerfLog": false + }, + "transport": { + "channel": { + "class": "com.alibaba.datax.core.transport.channel.memory.MemoryChannel", + "speed": { + "byte": -1, + "record": -1 + }, + "flowControlInterval": 20, + "capacity": 512, + "byteCapacity": 67108864 + }, + "exchanger": { + "class": "com.alibaba.datax.core.plugin.BufferedRecordExchanger", + "bufferSize": 32 + } + }, + "container": { + "job": { + "reportInterval": 10000 + }, + "taskGroup": { + "channel": 5 + }, + "trace": { + "enable": "false" + } + + }, + "statistics": { + "collector": { + "plugin": { + "taskClass": "com.alibaba.datax.core.statistics.plugin.task.StdoutPluginCollector", + "maxDirtyNumber": 10 + } + } + } + } +} diff --git a/datax-example/datax-example-core/src/test/java/com/alibaba/datax/example/util/PathUtilTest.java b/datax-example/datax-example-core/src/test/java/com/alibaba/datax/example/util/PathUtilTest.java new file mode 100644 index 0000000000..8985b54caa --- /dev/null +++ b/datax-example/datax-example-core/src/test/java/com/alibaba/datax/example/util/PathUtilTest.java @@ -0,0 +1,19 @@ +package com.alibaba.datax.example.util; + +import org.junit.Assert; +import org.junit.Test; + +/** + * {@code Author} FuYouJ + * {@code Date} 2023/8/19 21:38 + */ + +public class PathUtilTest { + + @Test + public void testParseClassPathFile() { + String path = "/pathTest.json"; + String absolutePathFromClassPath = PathUtil.getAbsolutePathFromClassPath(path); + Assert.assertNotNull(absolutePathFromClassPath); + } +} diff --git a/datax-example/datax-example-core/src/test/resources/pathTest.json b/datax-example/datax-example-core/src/test/resources/pathTest.json new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/datax-example/datax-example-core/src/test/resources/pathTest.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/datax-example/datax-example-neo4j/pom.xml b/datax-example/datax-example-neo4j/pom.xml new file mode 100644 index 0000000000..303b14a8d2 --- /dev/null +++ b/datax-example/datax-example-neo4j/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + com.alibaba.datax + datax-example + 0.0.1-SNAPSHOT + + + datax-example-neo4j + + + 8 + 8 + UTF-8 + 1.17.6 + 4.4.9 + + + + com.alibaba.datax + datax-example-core + 0.0.1-SNAPSHOT + + + org.testcontainers + testcontainers + ${test.container.version} + + + com.alibaba.datax + neo4jwriter + 0.0.1-SNAPSHOT + + + com.alibaba.datax + datax-example-streamreader + 0.0.1-SNAPSHOT + + + \ No newline at end of file diff --git a/datax-example/datax-example-neo4j/src/test/java/com/alibaba/datax/example/neo4j/StreamReader2Neo4jWriterTest.java b/datax-example/datax-example-neo4j/src/test/java/com/alibaba/datax/example/neo4j/StreamReader2Neo4jWriterTest.java new file mode 100644 index 0000000000..9cf0125314 --- /dev/null +++ b/datax-example/datax-example-neo4j/src/test/java/com/alibaba/datax/example/neo4j/StreamReader2Neo4jWriterTest.java @@ -0,0 +1,138 @@ +package com.alibaba.datax.example.neo4j; + +import com.alibaba.datax.example.ExampleContainer; +import com.alibaba.datax.example.util.PathUtil; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.neo4j.driver.*; +import org.neo4j.driver.types.Node; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.lifecycle.Startables; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.DockerLoggerFactory; + +import java.net.URI; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +/** + * {@code Author} FuYouJ + * {@code Date} 2023/8/19 21:48 + */ + +public class StreamReader2Neo4jWriterTest { + private static final Logger LOGGER = LoggerFactory.getLogger(StreamReader2Neo4jWriterTest.class); + private static final String CONTAINER_IMAGE = "neo4j:5.9.0"; + + private static final String CONTAINER_HOST = "neo4j-host"; + private static final int HTTP_PORT = 7474; + private static final int BOLT_PORT = 7687; + private static final String CONTAINER_NEO4J_USERNAME = "neo4j"; + private static final String CONTAINER_NEO4J_PASSWORD = "Test@12343"; + private static final URI CONTAINER_URI = URI.create("neo4j://localhost:" + BOLT_PORT); + + protected static final Network NETWORK = Network.newNetwork(); + + private GenericContainer container; + protected Driver neo4jDriver; + protected Session neo4jSession; + private static final int CHANNEL = 5; + private static final int READER_NUM = 10; + + @Before + public void init() { + DockerImageName imageName = DockerImageName.parse(CONTAINER_IMAGE); + container = + new GenericContainer<>(imageName) + .withNetwork(NETWORK) + .withNetworkAliases(CONTAINER_HOST) + .withExposedPorts(HTTP_PORT, BOLT_PORT) + .withEnv( + "NEO4J_AUTH", + CONTAINER_NEO4J_USERNAME + "/" + CONTAINER_NEO4J_PASSWORD) + .withEnv("apoc.export.file.enabled", "true") + .withEnv("apoc.import.file.enabled", "true") + .withEnv("apoc.import.file.use_neo4j_config", "true") + .withEnv("NEO4J_PLUGINS", "[\"apoc\"]") + .withLogConsumer( + new Slf4jLogConsumer( + DockerLoggerFactory.getLogger(CONTAINER_IMAGE))); + container.setPortBindings( + Arrays.asList( + String.format("%s:%s", HTTP_PORT, HTTP_PORT), + String.format("%s:%s", BOLT_PORT, BOLT_PORT))); + Startables.deepStart(Stream.of(container)).join(); + LOGGER.info("container started"); + Awaitility.given() + .ignoreExceptions() + .await() + .atMost(30, TimeUnit.SECONDS) + .untilAsserted(this::initConnection); + } + + //在neo4jWriter模块使用Example测试整个job,方便发现整个流程的代码问题 + @Test + public void streamReader2Neo4j() { + + deleteHistoryIfExist(); + + String path = "/streamreader2neo4j.json"; + String jobPath = PathUtil.getAbsolutePathFromClassPath(path); + + ExampleContainer.start(jobPath); + + //根据channel和reader的mock数据,校验结果集是否符合预期 + verifyWriteResult(); + } + + private void deleteHistoryIfExist() { + String query = "match (n:StreamReader) return n limit 1"; + String delete = "match (n:StreamReader) delete n"; + if (neo4jSession.run(query).hasNext()) { + neo4jSession.run(delete); + } + } + + private void verifyWriteResult() { + int total = CHANNEL * READER_NUM; + String query = "match (n:StreamReader) return n"; + Result run = neo4jSession.run(query); + int count = 0; + while (run.hasNext()) { + Record record = run.next(); + Node node = record.get("n").asNode(); + if (node.hasLabel("StreamReader")) { + count++; + } + } + Assert.assertEquals(count, total); + } + @After + public void destroy() { + if (neo4jSession != null) { + neo4jSession.close(); + } + if (neo4jDriver != null) { + neo4jDriver.close(); + } + if (container != null) { + container.close(); + } + } + + private void initConnection() { + neo4jDriver = + GraphDatabase.driver( + CONTAINER_URI, + AuthTokens.basic(CONTAINER_NEO4J_USERNAME, CONTAINER_NEO4J_PASSWORD)); + neo4jSession = neo4jDriver.session(SessionConfig.forDatabase("neo4j")); + } +} diff --git a/datax-example/datax-example-neo4j/src/test/resources/streamreader2neo4j.json b/datax-example/datax-example-neo4j/src/test/resources/streamreader2neo4j.json new file mode 100644 index 0000000000..3d543ce366 --- /dev/null +++ b/datax-example/datax-example-neo4j/src/test/resources/streamreader2neo4j.json @@ -0,0 +1,51 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "sliceRecordCount": 10, + "column": [ + { + "type": "string", + "value": "StreamReader" + }, + { + "type": "string", + "value": "1997" + } + ] + } + }, + "writer": { + "name": "neo4jWriter", + "parameter": { + "uri": "bolt://localhost:7687", + "username":"neo4j", + "password":"Test@12343", + "database":"neo4j", + "cypher": "unwind $batch as row CALL apoc.cypher.doIt( 'create (n:`' + row.Label + '`{id:$id})' ,{id: row.id} ) YIELD value RETURN 1 ", + "batchDataVariableName": "batch", + "batchSize": "3", + "properties": [ + { + "name": "Label", + "type": "string" + }, + { + "name": "id", + "type": "STRING" + } + ] + } + } + } + ], + "setting": { + "speed": { + "channel": 5 + } + } + } +} \ No newline at end of file diff --git a/datax-example/datax-example-streamreader/pom.xml b/datax-example/datax-example-streamreader/pom.xml new file mode 100644 index 0000000000..ea70de1079 --- /dev/null +++ b/datax-example/datax-example-streamreader/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + + com.alibaba.datax + datax-example + 0.0.1-SNAPSHOT + + + datax-example-streamreader + + + 8 + 8 + UTF-8 + + + + com.alibaba.datax + datax-example-core + 0.0.1-SNAPSHOT + + + com.alibaba.datax + streamreader + 0.0.1-SNAPSHOT + + + com.alibaba.datax + streamwriter + 0.0.1-SNAPSHOT + + + + \ No newline at end of file diff --git a/datax-example/datax-example-streamreader/src/test/java/com/alibaba/datax/example/streamreader/StreamReader2StreamWriterTest.java b/datax-example/datax-example-streamreader/src/test/java/com/alibaba/datax/example/streamreader/StreamReader2StreamWriterTest.java new file mode 100644 index 0000000000..71d083d06a --- /dev/null +++ b/datax-example/datax-example-streamreader/src/test/java/com/alibaba/datax/example/streamreader/StreamReader2StreamWriterTest.java @@ -0,0 +1,19 @@ +package com.alibaba.datax.example.streamreader; + +import com.alibaba.datax.example.ExampleContainer; +import com.alibaba.datax.example.util.PathUtil; +import org.junit.Test; + +/** + * {@code Author} FuYouJ + * {@code Date} 2023/8/14 20:16 + */ + +public class StreamReader2StreamWriterTest { + @Test + public void testStreamReader2StreamWriter() { + String path = "/stream2stream.json"; + String jobPath = PathUtil.getAbsolutePathFromClassPath(path); + ExampleContainer.start(jobPath); + } +} diff --git a/datax-example/datax-example-streamreader/src/test/resources/stream2stream.json b/datax-example/datax-example-streamreader/src/test/resources/stream2stream.json new file mode 100644 index 0000000000..b2a5739571 --- /dev/null +++ b/datax-example/datax-example-streamreader/src/test/resources/stream2stream.json @@ -0,0 +1,36 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "sliceRecordCount": 10, + "column": [ + { + "type": "long", + "value": "10" + }, + { + "type": "string", + "value": "hello,你好,世界-DataX" + } + ] + } + }, + "writer": { + "name": "streamwriter", + "parameter": { + "encoding": "UTF-8", + "print": true + } + } + } + ], + "setting": { + "speed": { + "channel": 5 + } + } + } +} \ No newline at end of file diff --git a/datax-example/doc/README.md b/datax-example/doc/README.md new file mode 100644 index 0000000000..15f77e871c --- /dev/null +++ b/datax-example/doc/README.md @@ -0,0 +1,107 @@ +## [DataX-Example]调试datax插件的模块 + +### 为什么要开发这个模块 + +一般使用DataX启动数据同步任务是从datax.py 脚本开始,获取程序datax包目录设置到系统变量datax.home里,此后系统核心插件的加载,配置初始化均依赖于变量datax.home,这带来了一些麻烦,以一次本地 DeBug streamreader 插件为例。 + +- maven 打包 datax 生成 datax 目录 +- 在 IDE 中 设置系统环境变量 datax.home,或者在Engine启动类中硬编码设置datax.home。 +- 修改插件 streamreader 代码 +- 再次 maven 打包,使JarLoader 能够加载到最新的 streamreader 代码。 +- 调试代码 + +在以上步骤中,打包完全不必要且最耗时,等待打包也最煎熬。 + +所以我编写一个新的模块(datax-example),此模块特用于本地调试和复现 BUG。如果模块顺利编写完成,那么以上流程将被简化至两步。 + +- 修改插件 streamreader 代码。 +- 调试代码 + +img + +### 目录结构 +该目录结构演示了如何使用datax-example-core编写测试用例,和校验代码流程。 +img + +### 实现原理 + +- 不修改原有的ConfigParer,使用新的ExampleConfigParser,仅用于example模块。他不依赖datax.home,而是依赖ide编译后的target目录 +- 将ide的target目录作为每个插件的目录类加载目录。 + +![img](img/img02.png) + +### 如何使用 +1.修改插件的pom文件,做如下改动。以streamreader为例。
+改动前 +```xml + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + +``` +改动后 +```xml + + + + + src/main/resources + + **/*.* + + true + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + +``` +#### 在测试模块模块使用 +参考datax-example/datax-example-streamreader的StreamReader2StreamWriterTest.java +```java +public class StreamReader2StreamWriterTest { + @Test + public void testStreamReader2StreamWriter() { + String path = "/stream2stream.json"; + String jobPath = PathUtil.getAbsolutePathFromClassPath(path); + ExampleContainer.start(jobPath); + } +} + +``` +参考datax-example/datax-example-neo4j的StreamReader2Neo4jWriterTest +```java +public class StreamReader2Neo4jWriterTest{ +@Test + public void streamReader2Neo4j() { + + deleteHistoryIfExist(); + + String path = "/streamreader2neo4j.json"; + String jobPath = PathUtil.getAbsolutePathFromClassPath(path); + + ExampleContainer.start(jobPath); + + //根据channel和reader的mock数据,校验结果集是否符合预期 + verifyWriteResult(); + } +} +``` \ No newline at end of file diff --git a/datax-example/doc/img/img01.png b/datax-example/doc/img/img01.png new file mode 100644 index 0000000000..d0431c1af2 Binary files /dev/null and b/datax-example/doc/img/img01.png differ diff --git a/datax-example/doc/img/img02.png b/datax-example/doc/img/img02.png new file mode 100644 index 0000000000..eec860d423 Binary files /dev/null and b/datax-example/doc/img/img02.png differ diff --git a/datax-example/doc/img/img03.png b/datax-example/doc/img/img03.png new file mode 100644 index 0000000000..731f81bde0 Binary files /dev/null and b/datax-example/doc/img/img03.png differ diff --git a/datax-example/pom.xml b/datax-example/pom.xml new file mode 100644 index 0000000000..9c4c92001b --- /dev/null +++ b/datax-example/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + com.alibaba.datax + datax-all + 0.0.1-SNAPSHOT + + + datax-example + pom + + datax-example-core + datax-example-streamreader + datax-example-neo4j + + + + 8 + 8 + UTF-8 + 4.13.2 + + + + com.alibaba.datax + datax-common + 0.0.1-SNAPSHOT + + + com.alibaba.datax + datax-core + 0.0.1-SNAPSHOT + + + junit + junit + ${junit4.version} + test + + + + + + + src/main/resources + + **/*.* + + true + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + + \ No newline at end of file diff --git a/dataxPluginDev.md b/dataxPluginDev.md index 4483f2708c..8c7241bf5a 100644 --- a/dataxPluginDev.md +++ b/dataxPluginDev.md @@ -447,6 +447,9 @@ DataX的内部类型在实现上会选用不同的java类型: 3. 用户在插件中在`reader`/`writer`配置的`name`字段指定插件名字。框架根据插件的类型(`reader`/`writer`)和插件名称去插件的路径下扫描所有的jar,加入`classpath`。 4. 根据插件配置中定义的入口类,框架通过反射实例化对应的`Job`和`Task`对象。 +### 编写测试用例 +1. 在datax-example工程下新建新的插件测试模块,调用`ExampleContainer.start(jobPath)`方法来检测你的代码逻辑是否正确。[datax-example使用](https://github.com/alibaba/DataX/blob/master/datax-example/doc/README.md) + ## 三、Last but not Least diff --git a/dorisreader/doc/dorisreader.md b/dorisreader/doc/dorisreader.md new file mode 100644 index 0000000000..c249c17834 --- /dev/null +++ b/dorisreader/doc/dorisreader.md @@ -0,0 +1,224 @@ +# DorisReader 插件文档 + +___ + +## 1 快速介绍 + +DorisReader插件实现了从Doris读取数据。在底层实现上,DorisReader通过JDBC连接远程Doris数据库,并执行相应的sql语句将数据从doris库中SELECT出来。 + +## 2 实现原理 + +简而言之,DorisReader通过JDBC连接器连接到远程的Doris数据库,并根据用户配置的信息生成查询SELECT +SQL语句,然后发送到远程Doris数据库,并将该SQL执行返回结果使用DataX自定义的数据类型拼装为抽象的数据集,并传递给下游Writer处理。 + +对于用户配置Table、Column、Where的信息,DorisReader将其拼接为SQL语句发送到Doris数据库;对于用户配置querySql信息,DorisReader直接将其发送到Doris数据库。 + +## 3 功能说明 + +### 3.1 配置样例 + +* 配置一个从Doris数据库同步抽取数据到本地的作业: + +``` +{ + "job": { + "setting": { + "speed": { + "channel": 3 + }, + "errorLimit": { + "record": 0, + "percentage": 0.02 + } + }, + "content": [ + { + "reader": { + "name": "dorisreader", + "parameter": { + "username": "root", + "password": "root", + "column": [ + "id", + "name" + ], + "splitPk": "db_id", + "connection": [ + { + "table": [ + "table" + ], + "jdbcUrl": [ + "jdbc:Doris://127.0.0.1:9030/database" + ] + } + ] + } + }, + "writer": { + "name": "streamwriter", + "parameter": { + "print":true + } + } + } + ] + } +} + +``` + +* 配置一个自定义SQL的数据库同步任务到本地内容的作业: + +``` +{ + "job": { + "setting": { + "speed": { + "channel":1 + } + }, + "content": [ + { + "reader": { + "name": "dorisreader", + "parameter": { + "username": "root", + "password": "root", + "connection": [ + { + "querySql": [ + "select db_id,on_line_flag from db_info where db_id < 10;", + "select db_id,on_line_flag from db_info where db_id >= 10;" + + ], + "jdbcUrl": [ + "jdbc:Doris://127.0.0.1:9030/database" + ] + } + ] + } + }, + "writer": { + "name": "streamwriter", + "parameter": { + "print": false, + "encoding": "UTF-8" + } + } + } + ] + } +} +``` + +### 3.2 参数说明 + +* **jdbcUrl** + + * + 描述:描述的是到对端数据库的JDBC连接信息,使用JSON的数组描述,并支持一个库填写多个连接地址。之所以使用JSON数组描述连接信息,是因为阿里集团内部支持多个IP探测,如果配置了多个,DorisReader可以依次探测ip的可连接性,直到选择一个合法的IP。如果全部连接失败,DorisReader报错。 + 注意,jdbcUrl必须包含在connection配置单元中。对于阿里集团外部使用情况,JSON数组填写一个JDBC连接即可。 + + * 必选:是
+ + * 默认值:无
+ +* **username** + + * 描述:数据源的用户名
+ + * 必选:是
+ + * 默认值:无
+ +* **password** + + * 描述:数据源指定用户名的密码
+ + * 必选:是
+ + * 默认值:无
+ +* **table** + + * + 描述:所选取的需要同步的表。使用JSON的数组描述,因此支持多张表同时抽取。当配置为多张表时,用户自己需保证多张表是同一schema结构,DorisReader不予检查表是否同一逻辑表。注意,table必须包含在connection配置单元中。
+ + * 必选:是
+ + * 默认值:无
+ +* **column** + + * 描述:所配置的表中需要同步的列名集合,使用JSON的数组描述字段信息。用户使用\*代表默认使用所有列配置,例如['\*']。 + + 支持列裁剪,即列可以挑选部分列进行导出。 + + 支持列换序,即列可以不按照表schema信息进行导出。 + + 支持常量配置,用户需要按照Doris SQL语法格式: + ["id", "\`table\`", "1", "'bazhen.csy'", "null", "to_char(a + 1)", "2.3" , "true"] + id为普通列名,\`table\`为包含保留字的列名,1为整形数字常量,'bazhen.csy'为字符串常量,null为空指针,to_char(a + 1)为表达式,2.3为浮点数,true为布尔值。 + + * 必选:是
+ + * 默认值:无
+ +* **splitPk** + + * 描述:DorisReader进行数据抽取时,如果指定splitPk,表示用户希望使用splitPk代表的字段进行数据分片,DataX因此会启动并发任务进行数据同步,这样可以大大提供数据同步的效能。 + + 推荐splitPk用户使用表主键,因为表主键通常情况下比较均匀,因此切分出来的分片也不容易出现数据热点。 + + 目前splitPk仅支持整形数据切分,`不支持浮点、字符串、日期等其他类型`。如果用户指定其他非支持类型,DorisReader将报错! + + 如果splitPk不填写,包括不提供splitPk或者splitPk值为空,DataX视作使用单通道同步该表数据。 + + * 必选:否
+ + * 默认值:空
+ +* **where** + + * 描述:筛选条件,DorisReader根据指定的column、table、where条件拼接SQL,并根据这个SQL进行数据抽取。在实际业务场景中,往往会选择当天的数据进行同步,可以将where条件指定为gmt_create > + $bizdate 。注意:不可以将where条件指定为limit 10,limit不是SQL的合法where子句。
+ + where条件可以有效地进行业务增量同步。如果不填写where语句,包括不提供where的key或者value,DataX均视作同步全量数据。 + + * 必选:否
+ + * 默认值:无
+ +* **querySql** + + * + 描述:在有些业务场景下,where这一配置项不足以描述所筛选的条件,用户可以通过该配置型来自定义筛选SQL。当用户配置了这一项之后,DataX系统就会忽略table,column这些配置型,直接使用这个配置项的内容对数据进行筛选,例如需要进行多表join后同步数据,使用select + a,b from table_a join table_b on table_a.id = table_b.id
+ + `当用户配置querySql时,DorisReader直接忽略table、column、where条件的配置`,querySql优先级大于table、column、where选项。 + + * 必选:否
+ + * 默认值:无
+ +### 3.3 类型转换 + +目前DorisReader支持大部分Doris类型,但也存在部分个别类型没有支持的情况,请注意检查你的类型。 + +下面列出DorisReaderr针对Doris类型转换列表: + +| DataX 内部类型| doris 数据类型 | +| -------- |-------------------------------------------------------| +| Long | int, tinyint, smallint, int, bigint,Largint | +| Double | float, double, decimal | +| String | varchar, char, text, string, map, json, array, struct | +| Date | date, datetime | +| Boolean | Boolean | + +请注意: + +* `tinyint(1) DataX视作为整形`。 + + + diff --git a/dorisreader/pom.xml b/dorisreader/pom.xml new file mode 100755 index 0000000000..15a025b6c4 --- /dev/null +++ b/dorisreader/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + com.alibaba.datax + datax-all + 0.0.1-SNAPSHOT + + dorisreader + dorisreader + jar + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + mysql + mysql-connector-java + ${mysql.driver.version} + + + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + diff --git a/dorisreader/src/main/assembly/package.xml b/dorisreader/src/main/assembly/package.xml new file mode 100755 index 0000000000..724613f9b9 --- /dev/null +++ b/dorisreader/src/main/assembly/package.xml @@ -0,0 +1,35 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/reader/dorisreader + + + target/ + + dorisreader-0.0.1-SNAPSHOT.jar + + plugin/reader/dorisreader + + + + + + false + plugin/reader/dorisreader/libs + runtime + + + diff --git a/dorisreader/src/main/java/com/alibaba/datax/plugin/reader/dorisreader/DorisReader.java b/dorisreader/src/main/java/com/alibaba/datax/plugin/reader/dorisreader/DorisReader.java new file mode 100755 index 0000000000..56a44316c9 --- /dev/null +++ b/dorisreader/src/main/java/com/alibaba/datax/plugin/reader/dorisreader/DorisReader.java @@ -0,0 +1,94 @@ +package com.alibaba.datax.plugin.reader.dorisreader; + +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.spi.Reader; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.CommonRdbmsReader; +import com.alibaba.datax.plugin.rdbms.reader.Constant; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public class DorisReader extends Reader { + + private static final DataBaseType DATABASE_TYPE = DataBaseType.Doris; + + public static class Job extends Reader.Job { + private static final Logger LOG = LoggerFactory + .getLogger(Job.class); + + private Configuration originalConfig = null; + private CommonRdbmsReader.Job commonRdbmsReaderJob; + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + + Integer fetchSize = this.originalConfig.getInt(Constant.FETCH_SIZE,Integer.MIN_VALUE); + this.originalConfig.set(Constant.FETCH_SIZE, fetchSize); + + this.commonRdbmsReaderJob = new CommonRdbmsReader.Job(DATABASE_TYPE); + this.commonRdbmsReaderJob.init(this.originalConfig); + } + + @Override + public void preCheck(){ + init(); + this.commonRdbmsReaderJob.preCheck(this.originalConfig,DATABASE_TYPE); + + } + + @Override + public List split(int adviceNumber) { + return this.commonRdbmsReaderJob.split(this.originalConfig, adviceNumber); + + } + + @Override + public void post() { + this.commonRdbmsReaderJob.post(this.originalConfig); + } + + @Override + public void destroy() { + this.commonRdbmsReaderJob.destroy(this.originalConfig); + } + + } + + public static class Task extends Reader.Task { + + private Configuration readerSliceConfig; + private CommonRdbmsReader.Task commonRdbmsReaderTask; + + @Override + public void init() { + this.readerSliceConfig = super.getPluginJobConf(); + this.commonRdbmsReaderTask = new CommonRdbmsReader.Task(DATABASE_TYPE,super.getTaskGroupId(), super.getTaskId()); + this.commonRdbmsReaderTask.init(this.readerSliceConfig); + + } + + @Override + public void startRead(RecordSender recordSender) { + int fetchSize = this.readerSliceConfig.getInt(Constant.FETCH_SIZE); + + this.commonRdbmsReaderTask.startRead(this.readerSliceConfig, recordSender, + super.getTaskPluginCollector(), fetchSize); + } + + @Override + public void post() { + this.commonRdbmsReaderTask.post(this.readerSliceConfig); + } + + @Override + public void destroy() { + this.commonRdbmsReaderTask.destroy(this.readerSliceConfig); + } + + } + +} diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESWriterErrorCode.java b/dorisreader/src/main/java/com/alibaba/datax/plugin/reader/dorisreader/DorisReaderErrorCode.java old mode 100644 new mode 100755 similarity index 50% rename from elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESWriterErrorCode.java rename to dorisreader/src/main/java/com/alibaba/datax/plugin/reader/dorisreader/DorisReaderErrorCode.java index 59dcbd0ae1..f9a8c4495b --- a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESWriterErrorCode.java +++ b/dorisreader/src/main/java/com/alibaba/datax/plugin/reader/dorisreader/DorisReaderErrorCode.java @@ -1,20 +1,14 @@ -package com.alibaba.datax.plugin.writer.elasticsearchwriter; +package com.alibaba.datax.plugin.reader.dorisreader; import com.alibaba.datax.common.spi.ErrorCode; -public enum ESWriterErrorCode implements ErrorCode { - BAD_CONFIG_VALUE("ESWriter-00", "您配置的值不合法."), - ES_INDEX_DELETE("ESWriter-01", "删除index错误."), - ES_INDEX_CREATE("ESWriter-02", "创建index错误."), - ES_MAPPINGS("ESWriter-03", "mappings错误."), - ES_INDEX_INSERT("ESWriter-04", "插入数据错误."), - ES_ALIAS_MODIFY("ESWriter-05", "别名修改错误."), +public enum DorisReaderErrorCode implements ErrorCode { ; private final String code; private final String description; - ESWriterErrorCode(String code, String description) { + private DorisReaderErrorCode(String code, String description) { this.code = code; this.description = description; } @@ -34,4 +28,4 @@ public String toString() { return String.format("Code:[%s], Description:[%s]. ", this.code, this.description); } -} \ No newline at end of file +} diff --git a/dorisreader/src/main/resources/plugin.json b/dorisreader/src/main/resources/plugin.json new file mode 100755 index 0000000000..981d1af897 --- /dev/null +++ b/dorisreader/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "dorisreader", + "class": "com.alibaba.datax.plugin.reader.dorisreader.DorisReader", + "description": "useScene: prod. mechanism: Jdbc connection using the database, execute select sql, retrieve data from the ResultSet. warn: The more you know about the database, the less problems you encounter.", + "developer": "alibaba" +} \ No newline at end of file diff --git a/dorisreader/src/main/resources/plugin_job_template.json b/dorisreader/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..2e3d9fa831 --- /dev/null +++ b/dorisreader/src/main/resources/plugin_job_template.json @@ -0,0 +1,15 @@ +{ + "name": "dorisreader", + "parameter": { + "username": "", + "password": "", + "column": [], + "connection": [ + { + "jdbcUrl": [], + "table": [] + } + ], + "where": "" + } +} \ No newline at end of file diff --git a/doriswriter/doc/doriswriter.md b/doriswriter/doc/doriswriter.md new file mode 100644 index 0000000000..2070113b4d --- /dev/null +++ b/doriswriter/doc/doriswriter.md @@ -0,0 +1,179 @@ +# DorisWriter 插件文档 + +## 1 快速介绍 +DorisWriter支持将大批量数据写入Doris中。 + +## 2 实现原理 +DorisWriter 通过Doris原生支持Stream load方式导入数据, DorisWriter会将`reader`读取的数据进行缓存在内存中,拼接成Json文本,然后批量导入至Doris。 + +## 3 功能说明 + +### 3.1 配置样例 + +这里是一份从Stream读取数据后导入至Doris的配置文件。 + +``` +{ + "job": { + "content": [ + { + "reader": { + "name": "mysqlreader", + "parameter": { + "column": ["emp_no", "birth_date", "first_name","last_name","gender","hire_date"], + "connection": [ + { + "jdbcUrl": ["jdbc:mysql://localhost:3306/demo"], + "table": ["employees_1"] + } + ], + "username": "root", + "password": "xxxxx", + "where": "" + } + }, + "writer": { + "name": "doriswriter", + "parameter": { + "loadUrl": ["172.16.0.13:8030"], + "column": ["emp_no", "birth_date", "first_name","last_name","gender","hire_date"], + "username": "root", + "password": "xxxxxx", + "postSql": ["select count(1) from all_employees_info"], + "preSql": [], + "flushInterval":30000, + "connection": [ + { + "jdbcUrl": "jdbc:mysql://172.16.0.13:9030/demo", + "selectedDatabase": "demo", + "table": ["all_employees_info"] + } + ], + "loadProps": { + "format": "json", + "strip_outer_array": true + } + } + } + } + ], + "setting": { + "speed": { + "channel": "1" + } + } + } +} +``` + +### 3.2 参数说明 + +* **jdbcUrl** + + - 描述:Doris 的 JDBC 连接串,用户执行 preSql 或 postSQL。 + - 必选:是 + - 默认值:无 + +* **loadUrl** + + - 描述:作为 Stream Load 的连接目标。格式为 "ip:port"。其中 IP 是 FE 节点 IP,port 是 FE 节点的 http_port。可以填写多个,多个之间使用英文状态的分号隔开:`;`,doriswriter 将以轮询的方式访问。 + - 必选:是 + - 默认值:无 + +* **username** + + - 描述:访问Doris数据库的用户名 + - 必选:是 + - 默认值:无 + +* **password** + + - 描述:访问Doris数据库的密码 + - 必选:否 + - 默认值:空 + +* **connection.selectedDatabase** + - 描述:需要写入的Doris数据库名称。 + - 必选:是 + - 默认值:无 + +* **connection.table** + - 描述:需要写入的Doris表名称。 + - 必选:是 + - 默认值:无 + +* **column** + + - 描述:目的表**需要写入数据**的字段,这些字段将作为生成的 Json 数据的字段名。字段之间用英文逗号分隔。例如: "column": ["id","name","age"]。 + - 必选:是 + - 默认值:否 + +* **preSql** + + - 描述:写入数据到目的表前,会先执行这里的标准语句。 + - 必选:否 + - 默认值:无 + +* **postSql** + + - 描述:写入数据到目的表后,会执行这里的标准语句。 + - 必选:否 + - 默认值:无 + + +* **maxBatchRows** + + - 描述:每批次导入数据的最大行数。和 **batchSize** 共同控制每批次的导入数量。每批次数据达到两个阈值之一,即开始导入这一批次的数据。 + - 必选:否 + - 默认值:500000 + +* **batchSize** + + - 描述:每批次导入数据的最大数据量。和 **maxBatchRows** 共同控制每批次的导入数量。每批次数据达到两个阈值之一,即开始导入这一批次的数据。 + - 必选:否 + - 默认值:104857600 + +* **maxRetries** + + - 描述:每批次导入数据失败后的重试次数。 + - 必选:否 + - 默认值:0 + +* **labelPrefix** + + - 描述:每批次导入任务的 label 前缀。最终的 label 将有 `labelPrefix + UUID` 组成全局唯一的 label,确保数据不会重复导入 + - 必选:否 + - 默认值:`datax_doris_writer_` + +* **loadProps** + + - 描述:StreamLoad 的请求参数,详情参照StreamLoad介绍页面。[Stream load - Apache Doris](https://doris.apache.org/zh-CN/docs/data-operate/import/import-way/stream-load-manual) + + 这里包括导入的数据格式:format等,导入数据格式默认我们使用csv,支持JSON,具体可以参照下面类型转换部分,也可以参照上面Stream load 官方信息 + + - 必选:否 + + - 默认值:无 + +### 类型转换 + +默认传入的数据均会被转为字符串,并以`\t`作为列分隔符,`\n`作为行分隔符,组成`csv`文件进行StreamLoad导入操作。 + +默认是csv格式导入,如需更改列分隔符, 则正确配置 `loadProps` 即可: + +```json +"loadProps": { + "column_separator": "\\x01", + "line_delimiter": "\\x02" +} +``` + +如需更改导入格式为`json`, 则正确配置 `loadProps` 即可: +```json +"loadProps": { + "format": "json", + "strip_outer_array": true +} +``` + +更多信息请参照 Doris 官网:[Stream load - Apache Doris](https://doris.apache.org/zh-CN/docs/data-operate/import/import-way/stream-load-manual) diff --git a/doriswriter/doc/mysql2doris.json b/doriswriter/doc/mysql2doris.json new file mode 100644 index 0000000000..5810d6db67 --- /dev/null +++ b/doriswriter/doc/mysql2doris.json @@ -0,0 +1,48 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "mysqlreader", + "parameter": { + "column": ["k1", "k2", "k3"], + "connection": [ + { + "jdbcUrl": ["jdbc:mysql://192.168.10.10:3306/db1"], + "table": ["t1"] + } + ], + "username": "root", + "password": "", + "where": "" + } + }, + "writer": { + "name": "doriswriter", + "parameter": { + "loadUrl": ["192.168.1.1:8030"], + "loadProps": {}, + "database": "db1", + "column": ["k1", "k2", "k3"], + "username": "root", + "password": "", + "postSql": [], + "preSql": [], + "connection": [ + { + "jdbcUrl":"jdbc:mysql://192.168.1.1:9030/", + "table":["xxx"], + "selectedDatabase":"xxxx" + } + ] + } + } + } + ], + "setting": { + "speed": { + "channel": "1" + } + } + } +} diff --git a/doriswriter/pom.xml b/doriswriter/pom.xml new file mode 100644 index 0000000000..aa1e6ff080 --- /dev/null +++ b/doriswriter/pom.xml @@ -0,0 +1,99 @@ + + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + doriswriter + doriswriter + jar + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + mysql + mysql-connector-java + ${mysql.driver.version} + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + diff --git a/doriswriter/src/main/assembly/package.xml b/doriswriter/src/main/assembly/package.xml new file mode 100644 index 0000000000..715963328e --- /dev/null +++ b/doriswriter/src/main/assembly/package.xml @@ -0,0 +1,52 @@ + + + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/writer/doriswriter + + + target/ + + doriswriter-0.0.1-SNAPSHOT.jar + + plugin/writer/doriswriter + + + + + false + plugin/writer/doriswriter/libs + runtime + + + diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DelimiterParser.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DelimiterParser.java new file mode 100644 index 0000000000..e84bd7dd85 --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DelimiterParser.java @@ -0,0 +1,54 @@ +package com.alibaba.datax.plugin.writer.doriswriter; + +import com.google.common.base.Strings; + +import java.io.StringWriter; + +public class DelimiterParser { + + private static final String HEX_STRING = "0123456789ABCDEF"; + + public static String parse(String sp, String dSp) throws RuntimeException { + if ( Strings.isNullOrEmpty(sp)) { + return dSp; + } + if (!sp.toUpperCase().startsWith("\\X")) { + return sp; + } + String hexStr = sp.substring(2); + // check hex str + if (hexStr.isEmpty()) { + throw new RuntimeException("Failed to parse delimiter: `Hex str is empty`"); + } + if (hexStr.length() % 2 != 0) { + throw new RuntimeException("Failed to parse delimiter: `Hex str length error`"); + } + for (char hexChar : hexStr.toUpperCase().toCharArray()) { + if (HEX_STRING.indexOf(hexChar) == -1) { + throw new RuntimeException("Failed to parse delimiter: `Hex str format error`"); + } + } + // transform to separator + StringWriter writer = new StringWriter(); + for (byte b : hexStrToBytes(hexStr)) { + writer.append((char) b); + } + return writer.toString(); + } + + private static byte[] hexStrToBytes(String hexStr) { + String upperHexStr = hexStr.toUpperCase(); + int length = upperHexStr.length() / 2; + char[] hexChars = upperHexStr.toCharArray(); + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) { + int pos = i * 2; + bytes[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1])); + } + return bytes; + } + + private static byte charToByte(char c) { + return (byte) HEX_STRING.indexOf(c); + } +} diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisBaseCodec.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisBaseCodec.java new file mode 100644 index 0000000000..ee7ded5606 --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisBaseCodec.java @@ -0,0 +1,23 @@ +package com.alibaba.datax.plugin.writer.doriswriter; + +import com.alibaba.datax.common.element.Column; + +public class DorisBaseCodec { + protected String convertionField( Column col) { + if (null == col.getRawData() || Column.Type.NULL == col.getType()) { + return null; + } + if ( Column.Type.BOOL == col.getType()) { + return String.valueOf(col.asLong()); + } + if ( Column.Type.BYTES == col.getType()) { + byte[] bts = (byte[])col.getRawData(); + long value = 0; + for (int i = 0; i < bts.length; i++) { + value += (bts[bts.length - i - 1] & 0xffL) << (8 * i); + } + return String.valueOf(value); + } + return col.asString(); + } +} diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisCodec.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisCodec.java new file mode 100644 index 0000000000..a2437a1cbb --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisCodec.java @@ -0,0 +1,10 @@ +package com.alibaba.datax.plugin.writer.doriswriter; + +import com.alibaba.datax.common.element.Record; + +import java.io.Serializable; + +public interface DorisCodec extends Serializable { + + String codec( Record row); +} diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisCodecFactory.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisCodecFactory.java new file mode 100644 index 0000000000..22c4b4099b --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisCodecFactory.java @@ -0,0 +1,19 @@ +package com.alibaba.datax.plugin.writer.doriswriter; + +import java.util.Map; + +public class DorisCodecFactory { + public DorisCodecFactory (){ + + } + public static DorisCodec createCodec( Keys writerOptions) { + if ( Keys.StreamLoadFormat.CSV.equals(writerOptions.getStreamLoadFormat())) { + Map props = writerOptions.getLoadProps(); + return new DorisCsvCodec (null == props || !props.containsKey("column_separator") ? null : String.valueOf(props.get("column_separator"))); + } + if ( Keys.StreamLoadFormat.JSON.equals(writerOptions.getStreamLoadFormat())) { + return new DorisJsonCodec (writerOptions.getColumns()); + } + throw new RuntimeException("Failed to create row serializer, unsupported `format` from stream load properties."); + } +} diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisCsvCodec.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisCsvCodec.java new file mode 100644 index 0000000000..518aa3043f --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisCsvCodec.java @@ -0,0 +1,27 @@ +package com.alibaba.datax.plugin.writer.doriswriter; + +import com.alibaba.datax.common.element.Record; + +public class DorisCsvCodec extends DorisBaseCodec implements DorisCodec { + + private static final long serialVersionUID = 1L; + + private final String columnSeparator; + + public DorisCsvCodec ( String sp) { + this.columnSeparator = DelimiterParser.parse(sp, "\t"); + } + + @Override + public String codec( Record row) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < row.getColumnNumber(); i++) { + String value = convertionField(row.getColumn(i)); + sb.append(null == value ? "\\N" : value); + if (i < row.getColumnNumber() - 1) { + sb.append(columnSeparator); + } + } + return sb.toString(); + } +} diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisJsonCodec.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisJsonCodec.java new file mode 100644 index 0000000000..68abd9eb40 --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisJsonCodec.java @@ -0,0 +1,33 @@ +package com.alibaba.datax.plugin.writer.doriswriter; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.fastjson2.JSON; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DorisJsonCodec extends DorisBaseCodec implements DorisCodec { + + private static final long serialVersionUID = 1L; + + private final List fieldNames; + + public DorisJsonCodec ( List fieldNames) { + this.fieldNames = fieldNames; + } + + @Override + public String codec( Record row) { + if (null == fieldNames) { + return ""; + } + Map rowMap = new HashMap<> (fieldNames.size()); + int idx = 0; + for (String fieldName : fieldNames) { + rowMap.put(fieldName, convertionField(row.getColumn(idx))); + idx++; + } + return JSON.toJSONString(rowMap); + } +} diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisStreamLoadObserver.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisStreamLoadObserver.java new file mode 100644 index 0000000000..e1f6e0eed6 --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisStreamLoadObserver.java @@ -0,0 +1,235 @@ +package com.alibaba.datax.plugin.writer.doriswriter; + +import com.alibaba.fastjson2.JSON; +import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHeaders; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultRedirectStrategy; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +public class DorisStreamLoadObserver { + private static final Logger LOG = LoggerFactory.getLogger(DorisStreamLoadObserver.class); + + private Keys options; + + private long pos; + private static final String RESULT_FAILED = "Fail"; + private static final String RESULT_LABEL_EXISTED = "Label Already Exists"; + private static final String LAEBL_STATE_VISIBLE = "VISIBLE"; + private static final String LAEBL_STATE_COMMITTED = "COMMITTED"; + private static final String RESULT_LABEL_PREPARE = "PREPARE"; + private static final String RESULT_LABEL_ABORTED = "ABORTED"; + private static final String RESULT_LABEL_UNKNOWN = "UNKNOWN"; + + + public DorisStreamLoadObserver ( Keys options){ + this.options = options; + } + + public void streamLoad(WriterTuple data) throws Exception { + String host = getLoadHost(); + if(host == null){ + throw new IOException ("load_url cannot be empty, or the host cannot connect.Please check your configuration."); + } + String loadUrl = new StringBuilder(host) + .append("/api/") + .append(options.getDatabase()) + .append("/") + .append(options.getTable()) + .append("/_stream_load") + .toString(); + LOG.info("Start to join batch data: rows[{}] bytes[{}] label[{}].", data.getRows().size(), data.getBytes(), data.getLabel()); + Map loadResult = put(loadUrl, data.getLabel(), addRows(data.getRows(), data.getBytes().intValue())); + LOG.info("StreamLoad response :{}",JSON.toJSONString(loadResult)); + final String keyStatus = "Status"; + if (null == loadResult || !loadResult.containsKey(keyStatus)) { + throw new IOException("Unable to flush data to Doris: unknown result status."); + } + LOG.debug("StreamLoad response:{}",JSON.toJSONString(loadResult)); + if (RESULT_FAILED.equals(loadResult.get(keyStatus))) { + throw new IOException( + new StringBuilder("Failed to flush data to Doris.\n").append(JSON.toJSONString(loadResult)).toString() + ); + } else if (RESULT_LABEL_EXISTED.equals(loadResult.get(keyStatus))) { + LOG.debug("StreamLoad response:{}",JSON.toJSONString(loadResult)); + checkStreamLoadState(host, data.getLabel()); + } + } + + private void checkStreamLoadState(String host, String label) throws IOException { + int idx = 0; + while(true) { + try { + TimeUnit.SECONDS.sleep(Math.min(++idx, 5)); + } catch (InterruptedException ex) { + break; + } + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpGet httpGet = new HttpGet(new StringBuilder(host).append("/api/").append(options.getDatabase()).append("/get_load_state?label=").append(label).toString()); + httpGet.setHeader("Authorization", getBasicAuthHeader(options.getUsername(), options.getPassword())); + httpGet.setHeader("Connection", "close"); + + try (CloseableHttpResponse resp = httpclient.execute(httpGet)) { + HttpEntity respEntity = getHttpEntity(resp); + if (respEntity == null) { + throw new IOException(String.format("Failed to flush data to Doris, Error " + + "could not get the final state of label[%s].\n", label), null); + } + Map result = (Map)JSON.parse(EntityUtils.toString(respEntity)); + String labelState = (String)result.get("data"); + if (null == labelState) { + throw new IOException(String.format("Failed to flush data to Doris, Error " + + "could not get the final state of label[%s]. response[%s]\n", label, EntityUtils.toString(respEntity)), null); + } + LOG.info(String.format("Checking label[%s] state[%s]\n", label, labelState)); + switch(labelState) { + case LAEBL_STATE_VISIBLE: + case LAEBL_STATE_COMMITTED: + return; + case RESULT_LABEL_PREPARE: + continue; + case RESULT_LABEL_ABORTED: + throw new DorisWriterExcetion (String.format("Failed to flush data to Doris, Error " + + "label[%s] state[%s]\n", label, labelState), null, true); + case RESULT_LABEL_UNKNOWN: + default: + throw new IOException(String.format("Failed to flush data to Doris, Error " + + "label[%s] state[%s]\n", label, labelState), null); + } + } + } + } + } + + private byte[] addRows(List rows, int totalBytes) { + if (Keys.StreamLoadFormat.CSV.equals(options.getStreamLoadFormat())) { + Map props = (options.getLoadProps() == null ? new HashMap<> () : options.getLoadProps()); + byte[] lineDelimiter = DelimiterParser.parse((String)props.get("line_delimiter"), "\n").getBytes(StandardCharsets.UTF_8); + ByteBuffer bos = ByteBuffer.allocate(totalBytes + rows.size() * lineDelimiter.length); + for (byte[] row : rows) { + bos.put(row); + bos.put(lineDelimiter); + } + return bos.array(); + } + + if (Keys.StreamLoadFormat.JSON.equals(options.getStreamLoadFormat())) { + ByteBuffer bos = ByteBuffer.allocate(totalBytes + (rows.isEmpty() ? 2 : rows.size() + 1)); + bos.put("[".getBytes(StandardCharsets.UTF_8)); + byte[] jsonDelimiter = ",".getBytes(StandardCharsets.UTF_8); + boolean isFirstElement = true; + for (byte[] row : rows) { + if (!isFirstElement) { + bos.put(jsonDelimiter); + } + bos.put(row); + isFirstElement = false; + } + bos.put("]".getBytes(StandardCharsets.UTF_8)); + return bos.array(); + } + throw new RuntimeException("Failed to join rows data, unsupported `format` from stream load properties:"); + } + private Map put(String loadUrl, String label, byte[] data) throws IOException { + LOG.info(String.format("Executing stream load to: '%s', size: '%s'", loadUrl, data.length)); + final HttpClientBuilder httpClientBuilder = HttpClients.custom() + .setRedirectStrategy(new DefaultRedirectStrategy () { + @Override + protected boolean isRedirectable(String method) { + return true; + } + }); + try ( CloseableHttpClient httpclient = httpClientBuilder.build()) { + HttpPut httpPut = new HttpPut(loadUrl); + httpPut.removeHeaders(HttpHeaders.CONTENT_LENGTH); + httpPut.removeHeaders(HttpHeaders.TRANSFER_ENCODING); + List cols = options.getColumns(); + if (null != cols && !cols.isEmpty() && Keys.StreamLoadFormat.CSV.equals(options.getStreamLoadFormat())) { + httpPut.setHeader("columns", String.join(",", cols.stream().map(f -> String.format("`%s`", f)).collect(Collectors.toList()))); + } + if (null != options.getLoadProps()) { + for (Map.Entry entry : options.getLoadProps().entrySet()) { + httpPut.setHeader(entry.getKey(), String.valueOf(entry.getValue())); + } + } + httpPut.setHeader("Expect", "100-continue"); + httpPut.setHeader("label", label); + httpPut.setHeader("two_phase_commit", "false"); + httpPut.setHeader("Authorization", getBasicAuthHeader(options.getUsername(), options.getPassword())); + httpPut.setEntity(new ByteArrayEntity(data)); + httpPut.setConfig(RequestConfig.custom().setRedirectsEnabled(true).build()); + try ( CloseableHttpResponse resp = httpclient.execute(httpPut)) { + HttpEntity respEntity = getHttpEntity(resp); + if (respEntity == null) + return null; + return (Map)JSON.parse(EntityUtils.toString(respEntity)); + } + } + } + + private String getBasicAuthHeader(String username, String password) { + String auth = username + ":" + password; + byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.UTF_8)); + return new StringBuilder("Basic ").append(new String(encodedAuth)).toString(); + } + + private HttpEntity getHttpEntity(CloseableHttpResponse resp) { + int code = resp.getStatusLine().getStatusCode(); + if (200 != code) { + LOG.warn("Request failed with code:{}", code); + return null; + } + HttpEntity respEntity = resp.getEntity(); + if (null == respEntity) { + LOG.warn("Request failed with empty response."); + return null; + } + return respEntity; + } + + private String getLoadHost() { + List hostList = options.getLoadUrlList(); + Collections.shuffle(hostList); + String host = new StringBuilder("http://").append(hostList.get((0))).toString(); + if (checkConnection(host)){ + return host; + } + return null; + } + + private boolean checkConnection(String host) { + try { + URL url = new URL(host); + HttpURLConnection co = (HttpURLConnection) url.openConnection(); + co.setConnectTimeout(5000); + co.connect(); + co.disconnect(); + return true; + } catch (Exception e1) { + e1.printStackTrace(); + return false; + } + } +} diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisUtil.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisUtil.java new file mode 100644 index 0000000000..5f5a6f3443 --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisUtil.java @@ -0,0 +1,105 @@ +package com.alibaba.datax.plugin.writer.doriswriter; + +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.util.RdbmsException; +import com.alibaba.datax.plugin.rdbms.writer.Constant; +import com.alibaba.druid.sql.parser.ParserException; +import com.google.common.base.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * jdbc util + */ +public class DorisUtil { + private static final Logger LOG = LoggerFactory.getLogger(DorisUtil.class); + + private DorisUtil() {} + + public static List getDorisTableColumns( Connection conn, String databaseName, String tableName) { + String currentSql = String.format("SELECT COLUMN_NAME FROM `information_schema`.`COLUMNS` WHERE `TABLE_SCHEMA` = '%s' AND `TABLE_NAME` = '%s' ORDER BY `ORDINAL_POSITION` ASC;", databaseName, tableName); + List columns = new ArrayList<> (); + ResultSet rs = null; + try { + rs = DBUtil.query(conn, currentSql); + while (DBUtil.asyncResultSetNext(rs)) { + String colName = rs.getString("COLUMN_NAME"); + columns.add(colName); + } + return columns; + } catch (Exception e) { + throw RdbmsException.asQueryException(DataBaseType.MySql, e, currentSql, null, null); + } finally { + DBUtil.closeDBResources(rs, null, null); + } + } + + public static List renderPreOrPostSqls(List preOrPostSqls, String tableName) { + if (null == preOrPostSqls) { + return Collections.emptyList(); + } + List renderedSqls = new ArrayList<>(); + for (String sql : preOrPostSqls) { + if (! Strings.isNullOrEmpty(sql)) { + renderedSqls.add(sql.replace(Constant.TABLE_NAME_PLACEHOLDER, tableName)); + } + } + return renderedSqls; + } + + public static void executeSqls(Connection conn, List sqls) { + Statement stmt = null; + String currentSql = null; + try { + stmt = conn.createStatement(); + for (String sql : sqls) { + currentSql = sql; + DBUtil.executeSqlWithoutResultSet(stmt, sql); + } + } catch (Exception e) { + throw RdbmsException.asQueryException(DataBaseType.MySql, e, currentSql, null, null); + } finally { + DBUtil.closeDBResources(null, stmt, null); + } + } + + public static void preCheckPrePareSQL( Keys options) { + String table = options.getTable(); + List preSqls = options.getPreSqlList(); + List renderedPreSqls = DorisUtil.renderPreOrPostSqls(preSqls, table); + if (null != renderedPreSqls && !renderedPreSqls.isEmpty()) { + LOG.info("Begin to preCheck preSqls:[{}].", String.join(";", renderedPreSqls)); + for (String sql : renderedPreSqls) { + try { + DBUtil.sqlValid(sql, DataBaseType.MySql); + } catch ( ParserException e) { + throw RdbmsException.asPreSQLParserException(DataBaseType.MySql,e,sql); + } + } + } + } + + public static void preCheckPostSQL( Keys options) { + String table = options.getTable(); + List postSqls = options.getPostSqlList(); + List renderedPostSqls = DorisUtil.renderPreOrPostSqls(postSqls, table); + if (null != renderedPostSqls && !renderedPostSqls.isEmpty()) { + LOG.info("Begin to preCheck postSqls:[{}].", String.join(";", renderedPostSqls)); + for(String sql : renderedPostSqls) { + try { + DBUtil.sqlValid(sql, DataBaseType.MySql); + } catch (ParserException e){ + throw RdbmsException.asPostSQLParserException(DataBaseType.MySql,e,sql); + } + } + } + } +} diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisWriter.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisWriter.java new file mode 100644 index 0000000000..b44d544068 --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisWriter.java @@ -0,0 +1,164 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package com.alibaba.datax.plugin.writer.doriswriter; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.List; + +/** + * doris data writer + */ +public class DorisWriter extends Writer { + + public static class Job extends Writer.Job { + + private static final Logger LOG = LoggerFactory.getLogger(Job.class); + private Configuration originalConfig = null; + private Keys options; + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + options = new Keys (super.getPluginJobConf()); + options.doPretreatment(); + } + + @Override + public void preCheck(){ + this.init(); + DorisUtil.preCheckPrePareSQL(options); + DorisUtil.preCheckPostSQL(options); + } + + @Override + public void prepare() { + String username = options.getUsername(); + String password = options.getPassword(); + String jdbcUrl = options.getJdbcUrl(); + List renderedPreSqls = DorisUtil.renderPreOrPostSqls(options.getPreSqlList(), options.getTable()); + if (null != renderedPreSqls && !renderedPreSqls.isEmpty()) { + Connection conn = DBUtil.getConnection(DataBaseType.MySql, jdbcUrl, username, password); + LOG.info("Begin to execute preSqls:[{}]. context info:{}.", String.join(";", renderedPreSqls), jdbcUrl); + DorisUtil.executeSqls(conn, renderedPreSqls); + DBUtil.closeDBResources(null, null, conn); + } + } + + @Override + public List split(int mandatoryNumber) { + List configurations = new ArrayList<>(mandatoryNumber); + for (int i = 0; i < mandatoryNumber; i++) { + configurations.add(originalConfig); + } + return configurations; + } + + @Override + public void post() { + String username = options.getUsername(); + String password = options.getPassword(); + String jdbcUrl = options.getJdbcUrl(); + List renderedPostSqls = DorisUtil.renderPreOrPostSqls(options.getPostSqlList(), options.getTable()); + if (null != renderedPostSqls && !renderedPostSqls.isEmpty()) { + Connection conn = DBUtil.getConnection(DataBaseType.MySql, jdbcUrl, username, password); + LOG.info("Start to execute preSqls:[{}]. context info:{}.", String.join(";", renderedPostSqls), jdbcUrl); + DorisUtil.executeSqls(conn, renderedPostSqls); + DBUtil.closeDBResources(null, null, conn); + } + } + + @Override + public void destroy() { + } + + } + + public static class Task extends Writer.Task { + private DorisWriterManager writerManager; + private Keys options; + private DorisCodec rowCodec; + + @Override + public void init() { + options = new Keys (super.getPluginJobConf()); + if (options.isWildcardColumn()) { + Connection conn = DBUtil.getConnection(DataBaseType.MySql, options.getJdbcUrl(), options.getUsername(), options.getPassword()); + List columns = DorisUtil.getDorisTableColumns(conn, options.getDatabase(), options.getTable()); + options.setInfoCchemaColumns(columns); + } + writerManager = new DorisWriterManager(options); + rowCodec = DorisCodecFactory.createCodec(options); + } + + @Override + public void prepare() { + } + + public void startWrite(RecordReceiver recordReceiver) { + try { + Record record; + while ((record = recordReceiver.getFromReader()) != null) { + if (record.getColumnNumber() != options.getColumns().size()) { + throw DataXException + .asDataXException( + DBUtilErrorCode.CONF_ERROR, + String.format( + "There is an error in the column configuration information. " + + "This is because you have configured a task where the number of fields to be read from the source:%s " + + "is not equal to the number of fields to be written to the destination table:%s. " + + "Please check your configuration and make changes.", + record.getColumnNumber(), + options.getColumns().size())); + } + writerManager.writeRecord(rowCodec.codec(record)); + } + } catch (Exception e) { + throw DataXException.asDataXException(DBUtilErrorCode.WRITE_DATA_ERROR, e); + } + } + + @Override + public void post() { + try { + writerManager.close(); + } catch (Exception e) { + throw DataXException.asDataXException(DBUtilErrorCode.WRITE_DATA_ERROR, e); + } + } + + @Override + public void destroy() {} + + @Override + public boolean supportFailOver(){ + return false; + } + } +} diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisWriterExcetion.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisWriterExcetion.java new file mode 100644 index 0000000000..7797d79fdd --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisWriterExcetion.java @@ -0,0 +1,29 @@ +package com.alibaba.datax.plugin.writer.doriswriter; + +import java.io.IOException; +import java.util.Map; + +public class DorisWriterExcetion extends IOException { + + private final Map response; + private boolean reCreateLabel; + + public DorisWriterExcetion ( String message, Map response) { + super(message); + this.response = response; + } + + public DorisWriterExcetion ( String message, Map response, boolean reCreateLabel) { + super(message); + this.response = response; + this.reCreateLabel = reCreateLabel; + } + + public Map getFailedResponse() { + return response; + } + + public boolean needReCreateLabel() { + return reCreateLabel; + } +} diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisWriterManager.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisWriterManager.java new file mode 100644 index 0000000000..f0ba6b5283 --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/DorisWriterManager.java @@ -0,0 +1,192 @@ +package com.alibaba.datax.plugin.writer.doriswriter; + +import com.google.common.base.Strings; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class DorisWriterManager { + + private static final Logger LOG = LoggerFactory.getLogger(DorisWriterManager.class); + + private final DorisStreamLoadObserver visitor; + private final Keys options; + private final List buffer = new ArrayList<> (); + private int batchCount = 0; + private long batchSize = 0; + private volatile boolean closed = false; + private volatile Exception flushException; + private final LinkedBlockingDeque< WriterTuple > flushQueue; + private ScheduledExecutorService scheduler; + private ScheduledFuture scheduledFuture; + + public DorisWriterManager( Keys options) { + this.options = options; + this.visitor = new DorisStreamLoadObserver (options); + flushQueue = new LinkedBlockingDeque<>(options.getFlushQueueLength()); + this.startScheduler(); + this.startAsyncFlushing(); + } + + public void startScheduler() { + stopScheduler(); + this.scheduler = Executors.newScheduledThreadPool(1, new BasicThreadFactory.Builder().namingPattern("Doris-interval-flush").daemon(true).build()); + this.scheduledFuture = this.scheduler.schedule(() -> { + synchronized (DorisWriterManager.this) { + if (!closed) { + try { + String label = createBatchLabel(); + LOG.info(String.format("Doris interval Sinking triggered: label[%s].", label)); + if (batchCount == 0) { + startScheduler(); + } + flush(label, false); + } catch (Exception e) { + flushException = e; + } + } + } + }, options.getFlushInterval(), TimeUnit.MILLISECONDS); + } + + public void stopScheduler() { + if (this.scheduledFuture != null) { + scheduledFuture.cancel(false); + this.scheduler.shutdown(); + } + } + + public final synchronized void writeRecord(String record) throws IOException { + checkFlushException(); + try { + byte[] bts = record.getBytes(StandardCharsets.UTF_8); + buffer.add(bts); + batchCount++; + batchSize += bts.length; + if (batchCount >= options.getBatchRows() || batchSize >= options.getBatchSize()) { + String label = createBatchLabel(); + LOG.debug(String.format("Doris buffer Sinking triggered: rows[%d] label[%s].", batchCount, label)); + flush(label, false); + } + } catch (Exception e) { + throw new IOException("Writing records to Doris failed.", e); + } + } + + public synchronized void flush(String label, boolean waitUtilDone) throws Exception { + checkFlushException(); + if (batchCount == 0) { + if (waitUtilDone) { + waitAsyncFlushingDone(); + } + return; + } + flushQueue.put(new WriterTuple (label, batchSize, new ArrayList<>(buffer))); + if (waitUtilDone) { + // wait the last flush + waitAsyncFlushingDone(); + } + buffer.clear(); + batchCount = 0; + batchSize = 0; + } + + public synchronized void close() { + if (!closed) { + closed = true; + try { + String label = createBatchLabel(); + if (batchCount > 0) LOG.debug(String.format("Doris Sink is about to close: label[%s].", label)); + flush(label, true); + } catch (Exception e) { + throw new RuntimeException("Writing records to Doris failed.", e); + } + } + checkFlushException(); + } + + public String createBatchLabel() { + StringBuilder sb = new StringBuilder(); + if (! Strings.isNullOrEmpty(options.getLabelPrefix())) { + sb.append(options.getLabelPrefix()); + } + return sb.append(UUID.randomUUID().toString()) + .toString(); + } + + private void startAsyncFlushing() { + // start flush thread + Thread flushThread = new Thread(new Runnable(){ + public void run() { + while(true) { + try { + asyncFlush(); + } catch (Exception e) { + flushException = e; + } + } + } + }); + flushThread.setDaemon(true); + flushThread.start(); + } + + private void waitAsyncFlushingDone() throws InterruptedException { + // wait previous flushings + for (int i = 0; i <= options.getFlushQueueLength(); i++) { + flushQueue.put(new WriterTuple ("", 0l, null)); + } + checkFlushException(); + } + + private void asyncFlush() throws Exception { + WriterTuple flushData = flushQueue.take(); + if (Strings.isNullOrEmpty(flushData.getLabel())) { + return; + } + stopScheduler(); + LOG.debug(String.format("Async stream load: rows[%d] bytes[%d] label[%s].", flushData.getRows().size(), flushData.getBytes(), flushData.getLabel())); + for (int i = 0; i <= options.getMaxRetries(); i++) { + try { + // flush to Doris with stream load + visitor.streamLoad(flushData); + LOG.info(String.format("Async stream load finished: label[%s].", flushData.getLabel())); + startScheduler(); + break; + } catch (Exception e) { + LOG.warn("Failed to flush batch data to Doris, retry times = {}", i, e); + if (i >= options.getMaxRetries()) { + throw new IOException(e); + } + if (e instanceof DorisWriterExcetion && (( DorisWriterExcetion )e).needReCreateLabel()) { + String newLabel = createBatchLabel(); + LOG.warn(String.format("Batch label changed from [%s] to [%s]", flushData.getLabel(), newLabel)); + flushData.setLabel(newLabel); + } + try { + Thread.sleep(1000l * Math.min(i + 1, 10)); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IOException("Unable to flush, interrupted while doing another attempt", e); + } + } + } + } + + private void checkFlushException() { + if (flushException != null) { + throw new RuntimeException("Writing records to Doris failed.", flushException); + } + } +} diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/Keys.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/Keys.java new file mode 100644 index 0000000000..e460e76b83 --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/Keys.java @@ -0,0 +1,177 @@ +package com.alibaba.datax.plugin.writer.doriswriter; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class Keys implements Serializable { + + private static final long serialVersionUID = 1l; + private static final int MAX_RETRIES = 3; + private static final int BATCH_ROWS = 500000; + private static final long DEFAULT_FLUSH_INTERVAL = 30000; + + private static final String LOAD_PROPS_FORMAT = "format"; + public enum StreamLoadFormat { + CSV, JSON; + } + + private static final String USERNAME = "username"; + private static final String PASSWORD = "password"; + private static final String DATABASE = "connection[0].selectedDatabase"; + private static final String TABLE = "connection[0].table[0]"; + private static final String COLUMN = "column"; + private static final String PRE_SQL = "preSql"; + private static final String POST_SQL = "postSql"; + private static final String JDBC_URL = "connection[0].jdbcUrl"; + private static final String LABEL_PREFIX = "labelPrefix"; + private static final String MAX_BATCH_ROWS = "maxBatchRows"; + private static final String MAX_BATCH_SIZE = "batchSize"; + private static final String FLUSH_INTERVAL = "flushInterval"; + private static final String LOAD_URL = "loadUrl"; + private static final String FLUSH_QUEUE_LENGTH = "flushQueueLength"; + private static final String LOAD_PROPS = "loadProps"; + + private static final String DEFAULT_LABEL_PREFIX = "datax_doris_writer_"; + + private static final long DEFAULT_MAX_BATCH_SIZE = 90 * 1024 * 1024; //default 90M + + private final Configuration options; + + private List infoSchemaColumns; + private List userSetColumns; + private boolean isWildcardColumn; + + public Keys ( Configuration options) { + this.options = options; + this.userSetColumns = options.getList(COLUMN, String.class).stream().map(str -> str.replace("`", "")).collect(Collectors.toList()); + if (1 == options.getList(COLUMN, String.class).size() && "*".trim().equals(options.getList(COLUMN, String.class).get(0))) { + this.isWildcardColumn = true; + } + } + + public void doPretreatment() { + validateRequired(); + validateStreamLoadUrl(); + } + + public String getJdbcUrl() { + return options.getString(JDBC_URL); + } + + public String getDatabase() { + return options.getString(DATABASE); + } + + public String getTable() { + return options.getString(TABLE); + } + + public String getUsername() { + return options.getString(USERNAME); + } + + public String getPassword() { + return options.getString(PASSWORD); + } + + public String getLabelPrefix() { + String label = options.getString(LABEL_PREFIX); + return null == label ? DEFAULT_LABEL_PREFIX : label; + } + + public List getLoadUrlList() { + return options.getList(LOAD_URL, String.class); + } + + public List getColumns() { + if (isWildcardColumn) { + return this.infoSchemaColumns; + } + return this.userSetColumns; + } + + public boolean isWildcardColumn() { + return this.isWildcardColumn; + } + + public void setInfoCchemaColumns(List cols) { + this.infoSchemaColumns = cols; + } + + public List getPreSqlList() { + return options.getList(PRE_SQL, String.class); + } + + public List getPostSqlList() { + return options.getList(POST_SQL, String.class); + } + + public Map getLoadProps() { + return options.getMap(LOAD_PROPS); + } + + public int getMaxRetries() { + return MAX_RETRIES; + } + + public int getBatchRows() { + Integer rows = options.getInt(MAX_BATCH_ROWS); + return null == rows ? BATCH_ROWS : rows; + } + + public long getBatchSize() { + Long size = options.getLong(MAX_BATCH_SIZE); + return null == size ? DEFAULT_MAX_BATCH_SIZE : size; + } + + public long getFlushInterval() { + Long interval = options.getLong(FLUSH_INTERVAL); + return null == interval ? DEFAULT_FLUSH_INTERVAL : interval; + } + + public int getFlushQueueLength() { + Integer len = options.getInt(FLUSH_QUEUE_LENGTH); + return null == len ? 1 : len; + } + + public StreamLoadFormat getStreamLoadFormat() { + Map loadProps = getLoadProps(); + if (null == loadProps) { + return StreamLoadFormat.CSV; + } + if (loadProps.containsKey(LOAD_PROPS_FORMAT) + && StreamLoadFormat.JSON.name().equalsIgnoreCase(String.valueOf(loadProps.get(LOAD_PROPS_FORMAT)))) { + return StreamLoadFormat.JSON; + } + return StreamLoadFormat.CSV; + } + + private void validateStreamLoadUrl() { + List urlList = getLoadUrlList(); + for (String host : urlList) { + if (host.split(":").length < 2) { + throw DataXException.asDataXException(DBUtilErrorCode.CONF_ERROR, + "The format of loadUrl is not correct, please enter:[`fe_ip:fe_http_ip;fe_ip:fe_http_ip`]."); + } + } + } + + private void validateRequired() { + final String[] requiredOptionKeys = new String[]{ + USERNAME, + DATABASE, + TABLE, + COLUMN, + LOAD_URL + }; + for (String optionKey : requiredOptionKeys) { + options.getNecessaryValue(optionKey, DBUtilErrorCode.REQUIRED_VALUE); + } + } +} diff --git a/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/WriterTuple.java b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/WriterTuple.java new file mode 100644 index 0000000000..32e0b341b8 --- /dev/null +++ b/doriswriter/src/main/java/com/alibaba/datax/plugin/writer/doriswriter/WriterTuple.java @@ -0,0 +1,20 @@ +package com.alibaba.datax.plugin.writer.doriswriter; + +import java.util.List; + +public class WriterTuple { + private String label; + private Long bytes; + private List rows; + + public WriterTuple ( String label, Long bytes, List rows){ + this.label = label; + this.rows = rows; + this.bytes = bytes; + } + + public String getLabel() { return label; } + public void setLabel(String label) { this.label = label; } + public Long getBytes() { return bytes; } + public List getRows() { return rows; } +} diff --git a/doriswriter/src/main/resources/plugin.json b/doriswriter/src/main/resources/plugin.json new file mode 100644 index 0000000000..69dc31a26c --- /dev/null +++ b/doriswriter/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "doriswriter", + "class": "com.alibaba.datax.plugin.writer.doriswriter.DorisWriter", + "description": "apache doris writer plugin", + "developer": "apche doris" +} diff --git a/doriswriter/src/main/resources/plugin_job_template.json b/doriswriter/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..0187e53965 --- /dev/null +++ b/doriswriter/src/main/resources/plugin_job_template.json @@ -0,0 +1,20 @@ +{ + "name": "doriswriter", + "parameter": { + "username": "", + "password": "", + "column": [], + "preSql": [], + "postSql": [], + "beLoadUrl": [], + "loadUrl": [], + "loadProps": {}, + "connection": [ + { + "jdbcUrl": "", + "selectedDatabase": "", + "table": [] + } + ] + } +} \ No newline at end of file diff --git a/drdsreader/doc/drdsreader.md b/drdsreader/doc/drdsreader.md index 25df920029..c54e6bd149 100644 --- a/drdsreader/doc/drdsreader.md +++ b/drdsreader/doc/drdsreader.md @@ -50,7 +50,7 @@ DRDS的插件目前DataX只适配了Mysql引擎的场景,DRDS对于DataX而言 // 数据库连接密码 "password": "root", "column": [ - "id","name" + "id","name" ], "connection": [ { diff --git a/drdsreader/pom.xml b/drdsreader/pom.xml index 71c7108dd8..e38884ab96 100755 --- a/drdsreader/pom.xml +++ b/drdsreader/pom.xml @@ -42,7 +42,7 @@ mysql mysql-connector-java - 5.1.34 + ${mysql.driver.version} diff --git a/drdswriter/pom.xml b/drdswriter/pom.xml index 6a361760ab..35a7f28d17 100755 --- a/drdswriter/pom.xml +++ b/drdswriter/pom.xml @@ -44,7 +44,7 @@ mysql mysql-connector-java - 5.1.34 + ${mysql.driver.version} diff --git a/elasticsearchwriter/doc/elasticsearchwriter.md b/elasticsearchwriter/doc/elasticsearchwriter.md index 9a22f13c22..3a3315edc3 100644 --- a/elasticsearchwriter/doc/elasticsearchwriter.md +++ b/elasticsearchwriter/doc/elasticsearchwriter.md @@ -167,79 +167,4 @@ * dynamic * 描述: 不使用datax的mappings,使用es自己的自动mappings * 必选: 否 - * 默认值: false - - - -## 4 性能报告 - -### 4.1 环境准备 - -* 总数据量 1kw条数据, 每条0.1kb -* 1个shard, 0个replica -* 不加id,这样默认是append_only模式,不检查版本,插入速度会有20%左右的提升 - -#### 4.1.1 输入数据类型(streamreader) - -``` -{"value": "1.1.1.1", "type": "string"}, -{"value": 19890604.0, "type": "double"}, -{"value": 19890604, "type": "long"}, -{"value": 19890604, "type": "long"}, -{"value": "hello world", "type": "string"}, -{"value": "hello world", "type": "string"}, -{"value": "41.12,-71.34", "type": "string"}, -{"value": "2017-05-25", "type": "string"}, -``` - -#### 4.1.2 输出数据类型(eswriter) - -``` -{ "name": "col_ip","type": "ip" }, -{ "name": "col_double","type": "double" }, -{ "name": "col_long","type": "long" }, -{ "name": "col_integer","type": "integer" }, -{ "name": "col_keyword", "type": "keyword" }, -{ "name": "col_text", "type": "text"}, -{ "name": "col_geo_point", "type": "geo_point" }, -{ "name": "col_date", "type": "date"} -``` - -#### 4.1.2 机器参数 - -1. cpu: 32 Intel(R) Xeon(R) CPU E5-2650 v2 @ 2.60GHz -2. mem: 128G -3. net: 千兆双网卡 - -#### 4.1.3 DataX jvm 参数 - --Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError - -### 4.2 测试报告 - -| 通道数| 批量提交行数| DataX速度(Rec/s)|DataX流量(MB/s)| -|--------|--------| --------|--------| -| 4| 256| 11013| 0.828| -| 4| 1024| 19417| 1.43| -| 4| 4096| 23923| 1.76| -| 4| 8172| 24449| 1.80| -| 8| 256| 21459| 1.58| -| 8| 1024| 37037| 2.72| -| 8| 4096| 45454| 3.34| -| 8| 8172| 45871| 3.37| -| 16| 1024| 67567| 4.96| -| 16| 4096| 78125| 5.74| -| 16| 8172| 77519| 5.69| -| 32| 1024| 94339| 6.93| -| 32| 4096| 96153| 7.06| -| 64| 1024| 91743| 6.74| - -### 4.3 测试总结 - -* 最好的结果是32通道,每次传4096,如果单条数据很大, 请适当减少批量数,防止oom -* 当然这个很容易水平扩展,而且es也是分布式的,多设置几个shard也可以水平扩展 - -## 5 约束限制 - -* 如果导入id,这样数据导入失败也会重试,重新导入也仅仅是覆盖,保证数据一致性 -* 如果不导入id,就是append_only模式,elasticsearch自动生成id,速度会提升20%左右,但数据无法修复,适合日志型数据(对数据精度要求不高的) \ No newline at end of file + * 默认值: false \ No newline at end of file diff --git a/elasticsearchwriter/pom.xml b/elasticsearchwriter/pom.xml index 2a246805e0..8699c6e599 100644 --- a/elasticsearchwriter/pom.xml +++ b/elasticsearchwriter/pom.xml @@ -35,12 +35,12 @@ io.searchbox jest-common - 2.4.0 + 6.3.1 io.searchbox jest - 2.4.0 + 6.3.1 joda-time @@ -50,7 +50,7 @@ junit junit - 4.11 + 4.13.1 test diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESClient.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESClient.java deleted file mode 100644 index 34bb7e5420..0000000000 --- a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESClient.java +++ /dev/null @@ -1,236 +0,0 @@ -package com.alibaba.datax.plugin.writer.elasticsearchwriter; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import io.searchbox.action.Action; -import io.searchbox.client.JestClient; -import io.searchbox.client.JestClientFactory; -import io.searchbox.client.JestResult; -import io.searchbox.client.config.HttpClientConfig; -import io.searchbox.client.config.HttpClientConfig.Builder; -import io.searchbox.core.Bulk; -import io.searchbox.indices.CreateIndex; -import io.searchbox.indices.DeleteIndex; -import io.searchbox.indices.IndicesExists; -import io.searchbox.indices.aliases.*; -import io.searchbox.indices.mapping.PutMapping; -import org.apache.http.HttpHost; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -/** - * Created by xiongfeng.bxf on 17/2/8. - */ -public class ESClient { - private static final Logger log = LoggerFactory.getLogger(ESClient.class); - - private JestClient jestClient; - - public JestClient getClient() { - return jestClient; - } - - public void createClient(String endpoint, - String user, - String passwd, - boolean multiThread, - int readTimeout, - boolean compression, - boolean discovery) { - - JestClientFactory factory = new JestClientFactory(); - Builder httpClientConfig = new HttpClientConfig - .Builder(endpoint) - .setPreemptiveAuth(new HttpHost(endpoint)) - .multiThreaded(multiThread) - .connTimeout(30000) - .readTimeout(readTimeout) - .maxTotalConnection(200) - .requestCompressionEnabled(compression) - .discoveryEnabled(discovery) - .discoveryFrequency(5l, TimeUnit.MINUTES); - - if (!("".equals(user) || "".equals(passwd))) { - httpClientConfig.defaultCredentials(user, passwd); - } - - factory.setHttpClientConfig(httpClientConfig.build()); - - jestClient = factory.getObject(); - } - - public boolean indicesExists(String indexName) throws Exception { - boolean isIndicesExists = false; - JestResult rst = jestClient.execute(new IndicesExists.Builder(indexName).build()); - if (rst.isSucceeded()) { - isIndicesExists = true; - } else { - switch (rst.getResponseCode()) { - case 404: - isIndicesExists = false; - break; - case 401: - // 无权访问 - default: - log.warn(rst.getErrorMessage()); - break; - } - } - return isIndicesExists; - } - - public boolean deleteIndex(String indexName) throws Exception { - log.info("delete index " + indexName); - if (indicesExists(indexName)) { - JestResult rst = execute(new DeleteIndex.Builder(indexName).build()); - if (!rst.isSucceeded()) { - return false; - } - } else { - log.info("index cannot found, skip delete " + indexName); - } - return true; - } - - public boolean createIndex(String indexName, String typeName, - Object mappings, String settings, boolean dynamic) throws Exception { - JestResult rst = null; - if (!indicesExists(indexName)) { - log.info("create index " + indexName); - rst = jestClient.execute( - new CreateIndex.Builder(indexName) - .settings(settings) - .setParameter("master_timeout", "5m") - .build() - ); - //index_already_exists_exception - if (!rst.isSucceeded()) { - if (getStatus(rst) == 400) { - log.info(String.format("index [%s] already exists", indexName)); - return true; - } else { - log.error(rst.getErrorMessage()); - return false; - } - } else { - log.info(String.format("create [%s] index success", indexName)); - } - } - - int idx = 0; - while (idx < 5) { - if (indicesExists(indexName)) { - break; - } - Thread.sleep(2000); - idx ++; - } - if (idx >= 5) { - return false; - } - - if (dynamic) { - log.info("ignore mappings"); - return true; - } - log.info("create mappings for " + indexName + " " + mappings); - rst = jestClient.execute(new PutMapping.Builder(indexName, typeName, mappings) - .setParameter("master_timeout", "5m").build()); - if (!rst.isSucceeded()) { - if (getStatus(rst) == 400) { - log.info(String.format("index [%s] mappings already exists", indexName)); - } else { - log.error(rst.getErrorMessage()); - return false; - } - } else { - log.info(String.format("index [%s] put mappings success", indexName)); - } - return true; - } - - public JestResult execute(Action clientRequest) throws Exception { - JestResult rst = null; - rst = jestClient.execute(clientRequest); - if (!rst.isSucceeded()) { - //log.warn(rst.getErrorMessage()); - } - return rst; - } - - public Integer getStatus(JestResult rst) { - JsonObject jsonObject = rst.getJsonObject(); - if (jsonObject.has("status")) { - return jsonObject.get("status").getAsInt(); - } - return 600; - } - - public boolean isBulkResult(JestResult rst) { - JsonObject jsonObject = rst.getJsonObject(); - return jsonObject.has("items"); - } - - - public boolean alias(String indexname, String aliasname, boolean needClean) throws IOException { - GetAliases getAliases = new GetAliases.Builder().addIndex(aliasname).build(); - AliasMapping addAliasMapping = new AddAliasMapping.Builder(indexname, aliasname).build(); - JestResult rst = jestClient.execute(getAliases); - log.info(rst.getJsonString()); - List list = new ArrayList(); - if (rst.isSucceeded()) { - JsonParser jp = new JsonParser(); - JsonObject jo = (JsonObject)jp.parse(rst.getJsonString()); - for(Map.Entry entry : jo.entrySet()){ - String tindex = entry.getKey(); - if (indexname.equals(tindex)) { - continue; - } - AliasMapping m = new RemoveAliasMapping.Builder(tindex, aliasname).build(); - String s = new Gson().toJson(m.getData()); - log.info(s); - if (needClean) { - list.add(m); - } - } - } - - ModifyAliases modifyAliases = new ModifyAliases.Builder(addAliasMapping).addAlias(list).setParameter("master_timeout", "5m").build(); - rst = jestClient.execute(modifyAliases); - if (!rst.isSucceeded()) { - log.error(rst.getErrorMessage()); - return false; - } - return true; - } - - public JestResult bulkInsert(Bulk.Builder bulk, int trySize) throws Exception { - // es_rejected_execution_exception - // illegal_argument_exception - // cluster_block_exception - JestResult rst = null; - rst = jestClient.execute(bulk.build()); - if (!rst.isSucceeded()) { - log.warn(rst.getErrorMessage()); - } - return rst; - } - - /** - * 关闭JestClient客户端 - * - */ - public void closeJestClient() { - if (jestClient != null) { - jestClient.shutdownClient(); - } - } -} diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESColumn.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESColumn.java deleted file mode 100644 index 8990d77c21..0000000000 --- a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESColumn.java +++ /dev/null @@ -1,65 +0,0 @@ -package com.alibaba.datax.plugin.writer.elasticsearchwriter; - -/** - * Created by xiongfeng.bxf on 17/3/2. - */ -public class ESColumn { - - private String name;//: "appkey", - - private String type;//": "TEXT", - - private String timezone; - - private String format; - - private Boolean array; - - public void setName(String name) { - this.name = name; - } - - public void setType(String type) { - this.type = type; - } - - public void setTimeZone(String timezone) { - this.timezone = timezone; - } - - public void setFormat(String format) { - this.format = format; - } - - public String getName() { - return name; - } - - public String getType() { - return type; - } - - public String getTimezone() { - return timezone; - } - - public String getFormat() { - return format; - } - - public void setTimezone(String timezone) { - this.timezone = timezone; - } - - public Boolean isArray() { - return array; - } - - public void setArray(Boolean array) { - this.array = array; - } - - public Boolean getArray() { - return array; - } -} diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESWriter.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESWriter.java deleted file mode 100644 index eb0e9a8137..0000000000 --- a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESWriter.java +++ /dev/null @@ -1,460 +0,0 @@ -package com.alibaba.datax.plugin.writer.elasticsearchwriter; - -import com.alibaba.datax.common.element.Column; -import com.alibaba.datax.common.element.Record; -import com.alibaba.datax.common.exception.DataXException; -import com.alibaba.datax.common.plugin.RecordReceiver; -import com.alibaba.datax.common.spi.Writer; -import com.alibaba.datax.common.util.Configuration; -import com.alibaba.datax.common.util.RetryUtil; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; -import com.alibaba.fastjson.TypeReference; -import io.searchbox.client.JestResult; -import io.searchbox.core.Bulk; -import io.searchbox.core.BulkResult; -import io.searchbox.core.Index; -import org.joda.time.DateTime; -import org.joda.time.DateTimeZone; -import org.joda.time.format.DateTimeFormat; -import org.joda.time.format.DateTimeFormatter; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.URLEncoder; -import java.util.*; -import java.util.concurrent.Callable; - -public class ESWriter extends Writer { - private final static String WRITE_COLUMNS = "write_columns"; - - public static class Job extends Writer.Job { - private static final Logger log = LoggerFactory.getLogger(Job.class); - - private Configuration conf = null; - - @Override - public void init() { - this.conf = super.getPluginJobConf(); - } - - @Override - public void prepare() { - /** - * 注意:此方法仅执行一次。 - * 最佳实践:如果 Job 中有需要进行数据同步之前的处理,可以在此处完成,如果没有必要则可以直接去掉。 - */ - ESClient esClient = new ESClient(); - esClient.createClient(Key.getEndpoint(conf), - Key.getAccessID(conf), - Key.getAccessKey(conf), - false, - 300000, - false, - false); - - String indexName = Key.getIndexName(conf); - String typeName = Key.getTypeName(conf); - boolean dynamic = Key.getDynamic(conf); - String mappings = genMappings(typeName); - String settings = JSONObject.toJSONString( - Key.getSettings(conf) - ); - log.info(String.format("index:[%s], type:[%s], mappings:[%s]", indexName, typeName, mappings)); - - try { - boolean isIndicesExists = esClient.indicesExists(indexName); - if (Key.isCleanup(this.conf) && isIndicesExists) { - esClient.deleteIndex(indexName); - } - // 强制创建,内部自动忽略已存在的情况 - if (!esClient.createIndex(indexName, typeName, mappings, settings, dynamic)) { - throw new IOException("create index or mapping failed"); - } - } catch (Exception ex) { - throw DataXException.asDataXException(ESWriterErrorCode.ES_MAPPINGS, ex.toString()); - } - esClient.closeJestClient(); - } - - private String genMappings(String typeName) { - String mappings = null; - Map propMap = new HashMap(); - List columnList = new ArrayList(); - - List column = conf.getList("column"); - if (column != null) { - for (Object col : column) { - JSONObject jo = JSONObject.parseObject(col.toString()); - String colName = jo.getString("name"); - String colTypeStr = jo.getString("type"); - if (colTypeStr == null) { - throw DataXException.asDataXException(ESWriterErrorCode.BAD_CONFIG_VALUE, col.toString() + " column must have type"); - } - ESFieldType colType = ESFieldType.getESFieldType(colTypeStr); - if (colType == null) { - throw DataXException.asDataXException(ESWriterErrorCode.BAD_CONFIG_VALUE, col.toString() + " unsupported type"); - } - - ESColumn columnItem = new ESColumn(); - - if (colName.equals(Key.PRIMARY_KEY_COLUMN_NAME)) { - // 兼容已有版本 - colType = ESFieldType.ID; - colTypeStr = "id"; - } - - columnItem.setName(colName); - columnItem.setType(colTypeStr); - - if (colType == ESFieldType.ID) { - columnList.add(columnItem); - // 如果是id,则properties为空 - continue; - } - - Boolean array = jo.getBoolean("array"); - if (array != null) { - columnItem.setArray(array); - } - Map field = new HashMap(); - field.put("type", colTypeStr); - //https://www.elastic.co/guide/en/elasticsearch/reference/5.2/breaking_50_mapping_changes.html#_literal_index_literal_property - // https://www.elastic.co/guide/en/elasticsearch/guide/2.x/_deep_dive_on_doc_values.html#_disabling_doc_values - field.put("doc_values", jo.getBoolean("doc_values")); - field.put("ignore_above", jo.getInteger("ignore_above")); - field.put("index", jo.getBoolean("index")); - - switch (colType) { - case STRING: - // 兼容string类型,ES5之前版本 - break; - case KEYWORD: - // https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-search-speed.html#_warm_up_global_ordinals - field.put("eager_global_ordinals", jo.getBoolean("eager_global_ordinals")); - case TEXT: - field.put("analyzer", jo.getString("analyzer")); - // 优化disk使用,也同步会提高index性能 - // https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-disk-usage.html - field.put("norms", jo.getBoolean("norms")); - field.put("index_options", jo.getBoolean("index_options")); - break; - case DATE: - columnItem.setTimeZone(jo.getString("timezone")); - columnItem.setFormat(jo.getString("format")); - // 后面时间会处理为带时区的标准时间,所以不需要给ES指定格式 - /* - if (jo.getString("format") != null) { - field.put("format", jo.getString("format")); - } else { - //field.put("format", "strict_date_optional_time||epoch_millis||yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"); - } - */ - break; - case GEO_SHAPE: - field.put("tree", jo.getString("tree")); - field.put("precision", jo.getString("precision")); - default: - break; - } - propMap.put(colName, field); - columnList.add(columnItem); - } - } - - conf.set(WRITE_COLUMNS, JSON.toJSONString(columnList)); - - log.info(JSON.toJSONString(columnList)); - - Map rootMappings = new HashMap(); - Map typeMappings = new HashMap(); - typeMappings.put("properties", propMap); - rootMappings.put(typeName, typeMappings); - - mappings = JSON.toJSONString(rootMappings); - - if (mappings == null || "".equals(mappings)) { - throw DataXException.asDataXException(ESWriterErrorCode.BAD_CONFIG_VALUE, "must have mappings"); - } - - return mappings; - } - - @Override - public List split(int mandatoryNumber) { - List configurations = new ArrayList(mandatoryNumber); - for (int i = 0; i < mandatoryNumber; i++) { - configurations.add(conf); - } - return configurations; - } - - @Override - public void post() { - ESClient esClient = new ESClient(); - esClient.createClient(Key.getEndpoint(conf), - Key.getAccessID(conf), - Key.getAccessKey(conf), - false, - 300000, - false, - false); - String alias = Key.getAlias(conf); - if (!"".equals(alias)) { - log.info(String.format("alias [%s] to [%s]", alias, Key.getIndexName(conf))); - try { - esClient.alias(Key.getIndexName(conf), alias, Key.isNeedCleanAlias(conf)); - } catch (IOException e) { - throw DataXException.asDataXException(ESWriterErrorCode.ES_ALIAS_MODIFY, e); - } - } - } - - @Override - public void destroy() { - - } - } - - public static class Task extends Writer.Task { - - private static final Logger log = LoggerFactory.getLogger(Job.class); - - private Configuration conf; - - - ESClient esClient = null; - private List typeList; - private List columnList; - - private int trySize; - private int batchSize; - private String index; - private String type; - private String splitter; - - @Override - public void init() { - this.conf = super.getPluginJobConf(); - index = Key.getIndexName(conf); - type = Key.getTypeName(conf); - - trySize = Key.getTrySize(conf); - batchSize = Key.getBatchSize(conf); - splitter = Key.getSplitter(conf); - columnList = JSON.parseObject(this.conf.getString(WRITE_COLUMNS), new TypeReference>() { - }); - - typeList = new ArrayList(); - - for (ESColumn col : columnList) { - typeList.add(ESFieldType.getESFieldType(col.getType())); - } - - esClient = new ESClient(); - } - - @Override - public void prepare() { - esClient.createClient(Key.getEndpoint(conf), - Key.getAccessID(conf), - Key.getAccessKey(conf), - Key.isMultiThread(conf), - Key.getTimeout(conf), - Key.isCompression(conf), - Key.isDiscovery(conf)); - } - - @Override - public void startWrite(RecordReceiver recordReceiver) { - List writerBuffer = new ArrayList(this.batchSize); - Record record = null; - long total = 0; - while ((record = recordReceiver.getFromReader()) != null) { - writerBuffer.add(record); - if (writerBuffer.size() >= this.batchSize) { - total += doBatchInsert(writerBuffer); - writerBuffer.clear(); - } - } - - if (!writerBuffer.isEmpty()) { - total += doBatchInsert(writerBuffer); - writerBuffer.clear(); - } - - String msg = String.format("task end, write size :%d", total); - getTaskPluginCollector().collectMessage("writesize", String.valueOf(total)); - log.info(msg); - esClient.closeJestClient(); - } - - private String getDateStr(ESColumn esColumn, Column column) { - DateTime date = null; - DateTimeZone dtz = DateTimeZone.getDefault(); - if (esColumn.getTimezone() != null) { - // 所有时区参考 http://www.joda.org/joda-time/timezones.html - dtz = DateTimeZone.forID(esColumn.getTimezone()); - } - if (column.getType() != Column.Type.DATE && esColumn.getFormat() != null) { - DateTimeFormatter formatter = DateTimeFormat.forPattern(esColumn.getFormat()); - date = formatter.withZone(dtz).parseDateTime(column.asString()); - return date.toString(); - } else if (column.getType() == Column.Type.DATE) { - date = new DateTime(column.asLong(), dtz); - return date.toString(); - } else { - return column.asString(); - } - } - - private long doBatchInsert(final List writerBuffer) { - Map data = null; - final Bulk.Builder bulkaction = new Bulk.Builder().defaultIndex(this.index).defaultType(this.type); - for (Record record : writerBuffer) { - data = new HashMap(); - String id = null; - for (int i = 0; i < record.getColumnNumber(); i++) { - Column column = record.getColumn(i); - String columnName = columnList.get(i).getName(); - ESFieldType columnType = typeList.get(i); - //如果是数组类型,那它传入的必是字符串类型 - if (columnList.get(i).isArray() != null && columnList.get(i).isArray()) { - String[] dataList = column.asString().split(splitter); - if (!columnType.equals(ESFieldType.DATE)) { - data.put(columnName, dataList); - } else { - for (int pos = 0; pos < dataList.length; pos++) { - dataList[pos] = getDateStr(columnList.get(i), column); - } - data.put(columnName, dataList); - } - } else { - switch (columnType) { - case ID: - if (id != null) { - id += record.getColumn(i).asString(); - } else { - id = record.getColumn(i).asString(); - } - break; - case DATE: - try { - String dateStr = getDateStr(columnList.get(i), column); - data.put(columnName, dateStr); - } catch (Exception e) { - getTaskPluginCollector().collectDirtyRecord(record, String.format("时间类型解析失败 [%s:%s] exception: %s", columnName, column.toString(), e.toString())); - } - break; - case KEYWORD: - case STRING: - case TEXT: - case IP: - case GEO_POINT: - data.put(columnName, column.asString()); - break; - case BOOLEAN: - data.put(columnName, column.asBoolean()); - break; - case BYTE: - case BINARY: - data.put(columnName, column.asBytes()); - break; - case LONG: - data.put(columnName, column.asLong()); - break; - case INTEGER: - data.put(columnName, column.asBigInteger()); - break; - case SHORT: - data.put(columnName, column.asBigInteger()); - break; - case FLOAT: - case DOUBLE: - data.put(columnName, column.asDouble()); - break; - case NESTED: - case OBJECT: - case GEO_SHAPE: - data.put(columnName, JSON.parse(column.asString())); - break; - default: - getTaskPluginCollector().collectDirtyRecord(record, "类型错误:不支持的类型:" + columnType + " " + columnName); - } - } - } - - if (id == null) { - //id = UUID.randomUUID().toString(); - bulkaction.addAction(new Index.Builder(data).build()); - } else { - bulkaction.addAction(new Index.Builder(data).id(id).build()); - } - } - - try { - return RetryUtil.executeWithRetry(new Callable() { - @Override - public Integer call() throws Exception { - JestResult jestResult = esClient.bulkInsert(bulkaction, 1); - if (jestResult.isSucceeded()) { - return writerBuffer.size(); - } - - String msg = String.format("response code: [%d] error :[%s]", jestResult.getResponseCode(), jestResult.getErrorMessage()); - log.warn(msg); - if (esClient.isBulkResult(jestResult)) { - BulkResult brst = (BulkResult) jestResult; - List failedItems = brst.getFailedItems(); - for (BulkResult.BulkResultItem item : failedItems) { - if (item.status != 400) { - // 400 BAD_REQUEST 如果非数据异常,请求异常,则不允许忽略 - throw DataXException.asDataXException(ESWriterErrorCode.ES_INDEX_INSERT, String.format("status:[%d], error: %s", item.status, item.error)); - } else { - // 如果用户选择不忽略解析错误,则抛异常,默认为忽略 - if (!Key.isIgnoreParseError(conf)) { - throw DataXException.asDataXException(ESWriterErrorCode.ES_INDEX_INSERT, String.format("status:[%d], error: %s, config not ignoreParseError so throw this error", item.status, item.error)); - } - } - } - - List items = brst.getItems(); - for (int idx = 0; idx < items.size(); ++idx) { - BulkResult.BulkResultItem item = items.get(idx); - if (item.error != null && !"".equals(item.error)) { - getTaskPluginCollector().collectDirtyRecord(writerBuffer.get(idx), String.format("status:[%d], error: %s", item.status, item.error)); - } - } - return writerBuffer.size() - brst.getFailedItems().size(); - } else { - Integer status = esClient.getStatus(jestResult); - switch (status) { - case 429: //TOO_MANY_REQUESTS - log.warn("server response too many requests, so auto reduce speed"); - break; - } - throw DataXException.asDataXException(ESWriterErrorCode.ES_INDEX_INSERT, jestResult.getErrorMessage()); - } - } - }, trySize, 60000L, true); - } catch (Exception e) { - if (Key.isIgnoreWriteError(this.conf)) { - log.warn(String.format("重试[%d]次写入失败,忽略该错误,继续写入!", trySize)); - } else { - throw DataXException.asDataXException(ESWriterErrorCode.ES_INDEX_INSERT, e); - } - } - return 0; - } - - @Override - public void post() { - } - - @Override - public void destroy() { - esClient.closeJestClient(); - } - } -} diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchClient.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchClient.java new file mode 100644 index 0000000000..08486e1f03 --- /dev/null +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchClient.java @@ -0,0 +1,314 @@ +package com.alibaba.datax.plugin.writer.elasticsearchwriter; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.elasticsearchwriter.jest.ClusterInfo; +import com.alibaba.datax.plugin.writer.elasticsearchwriter.jest.ClusterInfoResult; +import com.alibaba.datax.plugin.writer.elasticsearchwriter.jest.PutMapping7; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import io.searchbox.action.Action; +import io.searchbox.client.JestClient; +import io.searchbox.client.JestClientFactory; +import io.searchbox.client.JestResult; +import io.searchbox.client.config.HttpClientConfig; +import io.searchbox.client.config.HttpClientConfig.Builder; +import io.searchbox.core.Bulk; +import io.searchbox.indices.CreateIndex; +import io.searchbox.indices.DeleteIndex; +import io.searchbox.indices.IndicesExists; +import io.searchbox.indices.aliases.*; +import io.searchbox.indices.mapping.GetMapping; +import io.searchbox.indices.mapping.PutMapping; + +import io.searchbox.indices.settings.GetSettings; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * Created by xiongfeng.bxf on 17/2/8. + */ +public class ElasticSearchClient { + private static final Logger LOGGER = LoggerFactory.getLogger(ElasticSearchClient.class); + + private JestClient jestClient; + private Configuration conf; + + public JestClient getClient() { + return jestClient; + } + + public ElasticSearchClient(Configuration conf) { + this.conf = conf; + String endpoint = Key.getEndpoint(conf); + //es是支持集群写入的 + String[] endpoints = endpoint.split(","); + String user = Key.getUsername(conf); + String passwd = Key.getPassword(conf); + boolean multiThread = Key.isMultiThread(conf); + int readTimeout = Key.getTimeout(conf); + boolean compression = Key.isCompression(conf); + boolean discovery = Key.isDiscovery(conf); + String discoveryFilter = Key.getDiscoveryFilter(conf); + int totalConnection = this.conf.getInt("maxTotalConnection", 200); + JestClientFactory factory = new JestClientFactory(); + Builder httpClientConfig = new HttpClientConfig + .Builder(Arrays.asList(endpoints)) +// .setPreemptiveAuth(new HttpHost(endpoint)) + .multiThreaded(multiThread) + .connTimeout(readTimeout) + .readTimeout(readTimeout) + .maxTotalConnection(totalConnection) + .requestCompressionEnabled(compression) + .discoveryEnabled(discovery) + .discoveryFrequency(5L, TimeUnit.MINUTES) + .discoveryFilter(discoveryFilter); + if (!(StringUtils.isBlank(user) || StringUtils.isBlank(passwd))) { + // 匿名登录 + httpClientConfig.defaultCredentials(user, passwd); + } + factory.setHttpClientConfig(httpClientConfig.build()); + this.jestClient = factory.getObject(); + } + + public boolean indicesExists(String indexName) throws Exception { + boolean isIndicesExists = false; + JestResult rst = execute(new IndicesExists.Builder(indexName).build()); + if (rst.isSucceeded()) { + isIndicesExists = true; + } else { + LOGGER.warn("IndicesExists got ResponseCode: {} ErrorMessage: {}", rst.getResponseCode(), rst.getErrorMessage()); + switch (rst.getResponseCode()) { + case 404: + isIndicesExists = false; + break; + case 401: + // 无权访问 + default: + LOGGER.warn(rst.getErrorMessage()); + break; + } + } + return isIndicesExists; + } + + public boolean deleteIndex(String indexName) throws Exception { + LOGGER.info("delete index {}", indexName); + if (indicesExists(indexName)) { + JestResult rst = execute(new DeleteIndex.Builder(indexName).build()); + if (!rst.isSucceeded()) { + LOGGER.warn("DeleteIndex got ResponseCode: {}, ErrorMessage: {}", rst.getResponseCode(), rst.getErrorMessage()); + return false; + } else { + LOGGER.info("delete index {} success", indexName); + } + } else { + LOGGER.info("index cannot found, skip delete index {}", indexName); + } + return true; + } + + public boolean isGreaterOrEqualThan7() throws Exception { + try { + ClusterInfoResult result = execute(new ClusterInfo.Builder().build()); + LOGGER.info("ClusterInfoResult: {}", result.getJsonString()); + return result.isGreaterOrEqualThan7(); + }catch(Exception e) { + LOGGER.warn(e.getMessage()); + return false; + } + } + + /** + * 获取索引的settings + * @param indexName 索引名 + * @return 设置 + */ + public String getIndexSettings(String indexName) { + GetSettings.Builder builder = new GetSettings.Builder(); + builder.addIndex(indexName); + GetSettings getSettings = builder.build(); + try { + LOGGER.info("begin GetSettings for index: {}", indexName); + JestResult result = this.execute(getSettings); + return result.getJsonString(); + } catch (Exception e) { + String message = "GetSettings for index error: " + e.getMessage(); + LOGGER.warn(message, e); + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.ES_GET_SETTINGS, e.getMessage(), e); + } + } + + public boolean createIndexIfNotExists(String indexName, String typeName, + Object mappings, String settings, + boolean dynamic, boolean isGreaterOrEqualThan7) throws Exception { + JestResult rst; + if (!indicesExists(indexName)) { + LOGGER.info("create index {}", indexName); + rst = execute( + new CreateIndex.Builder(indexName) + .settings(settings) + .setParameter("master_timeout", Key.getMasterTimeout(this.conf)) + .build() + ); + //index_already_exists_exception + if (!rst.isSucceeded()) { + LOGGER.warn("CreateIndex got ResponseCode: {}, ErrorMessage: {}", rst.getResponseCode(), rst.getErrorMessage()); + if (getStatus(rst) == 400) { + LOGGER.info(String.format("index {} already exists", indexName)); + return true; + } else { + return false; + } + } else { + LOGGER.info("create {} index success", indexName); + } + } + + if (dynamic) { + LOGGER.info("dynamic is true, ignore mappings"); + return true; + } + LOGGER.info("create mappings for {} {}", indexName, mappings); + //如果大于7.x,mapping的PUT请求URI中不能带type,并且mapping设置中不能带有嵌套结构 + if (isGreaterOrEqualThan7) { + rst = execute(new PutMapping7.Builder(indexName, mappings). + setParameter("master_timeout", Key.getMasterTimeout(this.conf)).build()); + } else { + rst = execute(new PutMapping.Builder(indexName, typeName, mappings) + .setParameter("master_timeout", Key.getMasterTimeout(this.conf)).build()); + } + if (!rst.isSucceeded()) { + LOGGER.error("PutMapping got ResponseCode: {}, ErrorMessage: {}", rst.getResponseCode(), rst.getErrorMessage()); + return false; + } else { + LOGGER.info("index {} put mappings success", indexName); + } + return true; + } + + public T execute(Action clientRequest) throws IOException { + T rst = jestClient.execute(clientRequest); + if (!rst.isSucceeded()) { + LOGGER.warn(rst.getJsonString()); + } + return rst; + } + + public Integer getStatus(JestResult rst) { + JsonObject jsonObject = rst.getJsonObject(); + if (jsonObject.has("status")) { + return jsonObject.get("status").getAsInt(); + } + return 600; + } + + public boolean isBulkResult(JestResult rst) { + JsonObject jsonObject = rst.getJsonObject(); + return jsonObject.has("items"); + } + + + public boolean alias(String indexname, String aliasname, boolean needClean) throws IOException { + GetAliases getAliases = new GetAliases.Builder().addIndex(aliasname).build(); + AliasMapping addAliasMapping = new AddAliasMapping.Builder(indexname, aliasname).build(); + JestResult rst = null; + List list = new ArrayList(); + if (needClean) { + rst = execute(getAliases); + if (rst.isSucceeded()) { + JsonParser jp = new JsonParser(); + JsonObject jo = (JsonObject) jp.parse(rst.getJsonString()); + for (Map.Entry entry : jo.entrySet()) { + String tindex = entry.getKey(); + if (indexname.equals(tindex)) { + continue; + } + AliasMapping m = new RemoveAliasMapping.Builder(tindex, aliasname).build(); + String s = new Gson().toJson(m.getData()); + LOGGER.info(s); + list.add(m); + } + } + } + + ModifyAliases modifyAliases = new ModifyAliases.Builder(addAliasMapping).addAlias(list).setParameter("master_timeout", Key.getMasterTimeout(this.conf)).build(); + rst = execute(modifyAliases); + if (!rst.isSucceeded()) { + LOGGER.error(rst.getErrorMessage()); + throw new IOException(rst.getErrorMessage()); + } + return true; + } + + /** + * 获取index的mapping + */ + public String getIndexMapping(String indexName) { + GetMapping.Builder builder = new GetMapping.Builder(); + builder.addIndex(indexName); + GetMapping getMapping = builder.build(); + try { + LOGGER.info("begin GetMapping for index: {}", indexName); + JestResult result = this.execute(getMapping); + return result.getJsonString(); + } catch (Exception e) { + String message = "GetMapping for index error: " + e.getMessage(); + LOGGER.warn(message, e); + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.ES_MAPPINGS, e.getMessage(), e); + } + } + + public String getMappingForIndexType(String indexName, String typeName) { + String indexMapping = this.getIndexMapping(indexName); + JSONObject indexMappingInJson = JSON.parseObject(indexMapping); + List paths = Arrays.asList(indexName, "mappings"); + JSONObject properties = JsonPathUtil.getJsonObject(paths, indexMappingInJson); + JSONObject propertiesParent = properties; + if (StringUtils.isNotBlank(typeName) && properties.containsKey(typeName)) { + propertiesParent = (JSONObject) properties.get(typeName); + } + JSONObject mapping = (JSONObject) propertiesParent.get("properties"); + return JSON.toJSONString(mapping); + } + + public JestResult bulkInsert(Bulk.Builder bulk) throws Exception { + // es_rejected_execution_exception + // illegal_argument_exception + // cluster_block_exception + JestResult rst = null; + rst = execute(bulk.build()); + if (!rst.isSucceeded()) { + LOGGER.warn(rst.getErrorMessage()); + } + return rst; + } + + /** + * 关闭JestClient客户端 + * + */ + public void closeJestClient() { + if (jestClient != null) { + try { + // jestClient.shutdownClient(); + jestClient.close(); + } catch (IOException e) { + LOGGER.warn("ignore error: ", e.getMessage()); + } + + } + } +} diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchColumn.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchColumn.java new file mode 100644 index 0000000000..a27b15b211 --- /dev/null +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchColumn.java @@ -0,0 +1,126 @@ +package com.alibaba.datax.plugin.writer.elasticsearchwriter; + +import java.util.List; + +/** + * Created by xiongfeng.bxf on 17/3/2. + */ +public class ElasticSearchColumn { + + private String name;//: "appkey", + + private String type;//": "TEXT", + + private String timezone; + + /** + * 源头数据格式化处理,datax做的事情 + */ + private String format; + + /** + * 目标端格式化,es原生支持的格式 + */ + private String dstFormat; + + private boolean array; + + /** + * 是否使用目标端(ES原生)数组类型 + * + * 默认是false + */ + private boolean dstArray = false; + + private boolean jsonArray; + + private boolean origin; + + private List combineFields; + + private String combineFieldsValueSeparator = "-"; + + public String getCombineFieldsValueSeparator() { + return combineFieldsValueSeparator; + } + + public void setCombineFieldsValueSeparator(String combineFieldsValueSeparator) { + this.combineFieldsValueSeparator = combineFieldsValueSeparator; + } + + public List getCombineFields() { + return combineFields; + } + + public void setCombineFields(List combineFields) { + this.combineFields = combineFields; + } + + public void setName(String name) { + this.name = name; + } + + public void setType(String type) { + this.type = type; + } + + public void setTimeZone(String timezone) { + this.timezone = timezone; + } + + public void setFormat(String format) { + this.format = format; + } + + public String getName() { + return name; + } + + public String getType() { + return type; + } + + public boolean isOrigin() { return origin; } + + public void setOrigin(boolean origin) { this.origin = origin; } + + public String getTimezone() { + return timezone; + } + + public String getFormat() { + return format; + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + } + + public boolean isArray() { + return array; + } + + public void setArray(boolean array) { + this.array = array; + } + + public boolean isJsonArray() {return jsonArray;} + + public void setJsonArray(boolean jsonArray) {this.jsonArray = jsonArray;} + + public String getDstFormat() { + return dstFormat; + } + + public void setDstFormat(String dstFormat) { + this.dstFormat = dstFormat; + } + + public boolean isDstArray() { + return dstArray; + } + + public void setDstArray(boolean dstArray) { + this.dstArray = dstArray; + } +} diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESFieldType.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchFieldType.java similarity index 73% rename from elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESFieldType.java rename to elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchFieldType.java index 14b096891a..22c3ee6b53 100644 --- a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ESFieldType.java +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchFieldType.java @@ -3,8 +3,11 @@ /** * Created by xiongfeng.bxf on 17/3/1. */ -public enum ESFieldType { +public enum ElasticSearchFieldType { ID, + PARENT, + ROUTING, + VERSION, STRING, TEXT, KEYWORD, @@ -24,20 +27,18 @@ public enum ESFieldType { DATE_RANGE, GEO_POINT, GEO_SHAPE, - IP, + IP_RANGE, COMPLETION, TOKEN_COUNT, - - ARRAY, OBJECT, NESTED; - public static ESFieldType getESFieldType(String type) { + public static ElasticSearchFieldType getESFieldType(String type) { if (type == null) { return null; } - for (ESFieldType f : ESFieldType.values()) { + for (ElasticSearchFieldType f : ElasticSearchFieldType.values()) { if (f.name().compareTo(type.toUpperCase()) == 0) { return f; } diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchWriter.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchWriter.java new file mode 100644 index 0000000000..2c8ed2d05e --- /dev/null +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchWriter.java @@ -0,0 +1,1117 @@ +package com.alibaba.datax.plugin.writer.elasticsearchwriter; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.DataXCaseEnvUtil; +import com.alibaba.datax.common.util.RetryUtil; +import com.alibaba.datax.plugin.writer.elasticsearchwriter.Key.ActionType; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.TypeReference; +import com.alibaba.fastjson2.JSONWriter; +import com.google.common.base.Joiner; +import io.searchbox.client.JestResult; +import io.searchbox.core.*; +import io.searchbox.params.Parameters; +import org.apache.commons.lang3.StringUtils; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.Callable; + +public class ElasticSearchWriter extends Writer { + private final static String WRITE_COLUMNS = "write_columns"; + + public static class Job extends Writer.Job { + private static final Logger LOGGER = LoggerFactory.getLogger(Job.class); + + private Configuration conf = null; + int retryTimes = 3; + long sleepTimeInMilliSecond = 10000L; + + private String settingsCache; + + private void setSettings(String settings) { + this.settingsCache = JsonUtil.mergeJsonStr(settings, this.settingsCache); + } + + @Override + public void init() { + this.conf = super.getPluginJobConf(); + //LOGGER.info("conf:{}", conf); + this.retryTimes = this.conf.getInt("retryTimes", 3); + this.sleepTimeInMilliSecond = this.conf.getLong("sleepTimeInMilliSecond", 10000L); + } + + public List getIncludeSettings() { + return this.conf.getList("includeSettingKeys", Arrays.asList("number_of_shards", "number_of_replicas"), String.class); + } + + /** + * 从es中获取的原始settings转为需要的settings + * @param originSettings 原始settings + * @return settings + */ + private String convertSettings(String originSettings) { + if(StringUtils.isBlank(originSettings)) { + return null; + } + JSONObject jsonObject = JSON.parseObject(originSettings); + for(String key : jsonObject.keySet()) { + JSONObject settingsObj = jsonObject.getJSONObject(key); + if(settingsObj != null) { + JSONObject indexObj = settingsObj.getJSONObject("settings"); + JSONObject settings = indexObj.getJSONObject("index"); + JSONObject filterSettings = new JSONObject(); + if(settings != null) { + List includeSettings = getIncludeSettings(); + if(includeSettings != null && includeSettings.size() > 0) { + for(String includeSetting : includeSettings) { + Object fieldValue = settings.get(includeSetting); + if(fieldValue != null) { + filterSettings.put(includeSetting, fieldValue); + } + } + return filterSettings.toJSONString(); + } + } + } + } + return null; + } + + @Override + public void prepare() { + /** + * 注意:此方法仅执行一次。 + * 最佳实践:如果 Job 中有需要进行数据同步之前的处理,可以在此处完成,如果没有必要则可以直接去掉。 + * 对于7.x之后的es版本,取消了index设置type的逻辑,因此在prepare阶段,加入了判断是否为7.x及以上版本 + * 如果是7.x及以上版本,需要对于index的type做不同的处理 + * 详见 : https://www.elastic.co/guide/en/elasticsearch/reference/6.8/removal-of-types.html + */ + final ElasticSearchClient esClient = new ElasticSearchClient(this.conf); + final String indexName = Key.getIndexName(conf); + ActionType actionType = Key.getActionType(conf); + final String typeName = Key.getTypeName(conf); + final boolean dynamic = Key.getDynamic(conf); + final String dstDynamic = Key.getDstDynamic(conf); + final String newSettings = JSONObject.toJSONString(Key.getSettings(conf)); + LOGGER.info("conf settings:{}, settingsCache:{}", newSettings, this.settingsCache); + final Integer esVersion = Key.getESVersion(conf); + boolean hasId = this.hasID(); + this.conf.set("hasId", hasId); + if (ActionType.UPDATE.equals(actionType) && !hasId && !hasPrimaryKeyInfo()) { + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.UPDATE_WITH_ID, "Update mode must specify column type with id or primaryKeyInfo config"); + } + + try { + RetryUtil.executeWithRetry(() -> { + boolean isGreaterOrEqualThan7 = esClient.isGreaterOrEqualThan7(); + if (esVersion != null && esVersion >= 7) { + isGreaterOrEqualThan7 = true; + } + String mappings = genMappings(dstDynamic, typeName, isGreaterOrEqualThan7); + conf.set("isGreaterOrEqualThan7", isGreaterOrEqualThan7); + + + LOGGER.info(String.format("index:[%s], type:[%s], mappings:[%s]", indexName, typeName, mappings)); + boolean isIndicesExists = esClient.indicesExists(indexName); + if (isIndicesExists) { + try { + // 将原有的mapping打印出来,便于排查问题 + String oldMappings = esClient.getMappingForIndexType(indexName, typeName); + LOGGER.info("the mappings for old index is: {}", oldMappings); + } catch (Exception e) { + LOGGER.warn("warn message: {}", e.getMessage()); + } + } + + if (Key.isTruncate(conf) && isIndicesExists) { + // 备份老的索引中的settings到缓存 + try { + String oldOriginSettings = esClient.getIndexSettings(indexName); + if (StringUtils.isNotBlank(oldOriginSettings)) { + String includeSettings = convertSettings(oldOriginSettings); + LOGGER.info("merge1 settings:{}, settingsCache:{}, includeSettings:{}", + oldOriginSettings, + this.settingsCache, includeSettings); + this.setSettings(includeSettings); + } + } catch (Exception e) { + LOGGER.warn("get old settings fail, indexName:{}", indexName); + } + esClient.deleteIndex(indexName); + } + + // 更新缓存中的settings + this.setSettings(newSettings); + LOGGER.info("merge2 settings:{}, settingsCache:{}", newSettings, this.settingsCache); + // 强制创建,内部自动忽略已存在的情况 + if (!esClient.createIndexIfNotExists(indexName, typeName, mappings, this.settingsCache, dynamic, + isGreaterOrEqualThan7)) { + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.ES_MAPPINGS, ""); + } + + return true; + }, DataXCaseEnvUtil.getRetryTimes(this.retryTimes), DataXCaseEnvUtil.getRetryInterval(this.sleepTimeInMilliSecond), DataXCaseEnvUtil.getRetryExponential(false)); + } catch (Exception ex) { + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.ES_MAPPINGS, ex.getMessage(), ex); + } finally { + try { + esClient.closeJestClient(); + } catch (Exception e) { + LOGGER.warn("ignore close jest client error: {}", e.getMessage()); + } + } + } + + private boolean hasID() { + List column = conf.getList("column"); + if (column != null) { + for (Object col : column) { + JSONObject jo = JSONObject.parseObject(col.toString()); + String colTypeStr = jo.getString("type"); + ElasticSearchFieldType colType = ElasticSearchFieldType.getESFieldType(colTypeStr); + if (ElasticSearchFieldType.ID.equals(colType)) { + return true; + } + } + } + return false; + } + + private boolean hasPrimaryKeyInfo() { + PrimaryKeyInfo primaryKeyInfo = Key.getPrimaryKeyInfo(this.conf); + if (null != primaryKeyInfo && null != primaryKeyInfo.getColumn() && !primaryKeyInfo.getColumn().isEmpty()) { + return true; + } else { + return false; + } + } + + + private String genMappings(String dstDynamic, String typeName, boolean isGreaterOrEqualThan7) { + String mappings; + Map propMap = new HashMap(); + List columnList = new ArrayList(); + ElasticSearchColumn combineItem = null; + + List column = conf.getList("column"); + if (column != null) { + for (Object col : column) { + JSONObject jo = JSONObject.parseObject(col.toString()); + String colName = jo.getString("name"); + String colTypeStr = jo.getString("type"); + if (colTypeStr == null) { + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.BAD_CONFIG_VALUE, col.toString() + " column must have type"); + } + ElasticSearchFieldType colType = ElasticSearchFieldType.getESFieldType(colTypeStr); + if (colType == null) { + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.BAD_CONFIG_VALUE, col.toString() + " unsupported type"); + } + + ElasticSearchColumn columnItem = new ElasticSearchColumn(); + + if (Key.PRIMARY_KEY_COLUMN_NAME.equals(colName)) { + // 兼容已有版本 + colType = ElasticSearchFieldType.ID; + colTypeStr = "id"; + } + + columnItem.setName(colName); + columnItem.setType(colTypeStr); + + JSONArray combineFields = jo.getJSONArray("combineFields"); + if (combineFields != null && !combineFields.isEmpty() && ElasticSearchFieldType.ID.equals(ElasticSearchFieldType.getESFieldType(colTypeStr))) { + List fields = new ArrayList(); + for (Object item : combineFields) { + fields.add((String) item); + } + columnItem.setCombineFields(fields); + combineItem = columnItem; + } + + String combineFieldsValueSeparator = jo.getString("combineFieldsValueSeparator"); + if (StringUtils.isNotBlank(combineFieldsValueSeparator)) { + columnItem.setCombineFieldsValueSeparator(combineFieldsValueSeparator); + } + + // 如果是id,version,routing,不需要创建mapping + if (colType == ElasticSearchFieldType.ID || colType == ElasticSearchFieldType.VERSION || colType == ElasticSearchFieldType.ROUTING) { + columnList.add(columnItem); + continue; + } + + // 如果是组合id中的字段,不需要创建mapping + // 所以组合id的定义必须要在columns最前面 + if (combineItem != null && combineItem.getCombineFields().contains(colName)) { + columnList.add(columnItem); + continue; + } + columnItem.setDstArray(false); + Boolean array = jo.getBoolean("array"); + if (array != null) { + columnItem.setArray(array); + Boolean dstArray = jo.getBoolean("dstArray"); + if(dstArray!=null) { + columnItem.setDstArray(dstArray); + } + } else { + columnItem.setArray(false); + } + Boolean jsonArray = jo.getBoolean("json_array"); + if (jsonArray != null) { + columnItem.setJsonArray(jsonArray); + } else { + columnItem.setJsonArray(false); + } + Map field = new HashMap(); + field.put("type", colTypeStr); + //https://www.elastic.co/guide/en/elasticsearch/reference/5.2/breaking_50_mapping_changes.html#_literal_index_literal_property + // https://www.elastic.co/guide/en/elasticsearch/guide/2.x/_deep_dive_on_doc_values.html#_disabling_doc_values + field.put("doc_values", jo.getBoolean("doc_values")); + field.put("ignore_above", jo.getInteger("ignore_above")); + field.put("index", jo.getBoolean("index")); + switch (colType) { + case STRING: + // 兼容string类型,ES5之前版本 + break; + case KEYWORD: + // https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-search-speed.html#_warm_up_global_ordinals + field.put("eager_global_ordinals", jo.getBoolean("eager_global_ordinals")); + break; + case TEXT: + field.put("analyzer", jo.getString("analyzer")); + // 优化disk使用,也同步会提高index性能 + // https://www.elastic.co/guide/en/elasticsearch/reference/current/tune-for-disk-usage.html + field.put("norms", jo.getBoolean("norms")); + field.put("index_options", jo.getBoolean("index_options")); + if(jo.getString("fields") != null) { + field.put("fields", jo.getJSONObject("fields")); + } + break; + case DATE: + if (Boolean.TRUE.equals(jo.getBoolean("origin"))) { + if (jo.getString("format") != null) { + field.put("format", jo.getString("format")); + } + // es原生format覆盖原先来的format + if (jo.getString("dstFormat") != null) { + field.put("format", jo.getString("dstFormat")); + } + if(jo.getBoolean("origin") != null) { + columnItem.setOrigin(jo.getBoolean("origin")); + } + } else { + columnItem.setTimeZone(jo.getString("timezone")); + columnItem.setFormat(jo.getString("format")); + } + break; + case GEO_SHAPE: + field.put("tree", jo.getString("tree")); + field.put("precision", jo.getString("precision")); + break; + case OBJECT: + case NESTED: + if (jo.getString("dynamic") != null) { + field.put("dynamic", jo.getString("dynamic")); + } + break; + default: + break; + } + if (jo.containsKey("other_params")) { + field.putAll(jo.getJSONObject("other_params")); + } + propMap.put(colName, field); + columnList.add(columnItem); + } + } + + long version = System.currentTimeMillis(); + LOGGER.info("unified version: {}", version); + conf.set("version", version); + conf.set(WRITE_COLUMNS, JSON.toJSONString(columnList)); + + LOGGER.info(JSON.toJSONString(columnList)); + + Map rootMappings = new HashMap(); + Map typeMappings = new HashMap(); + typeMappings.put("properties", propMap); + rootMappings.put(typeName, typeMappings); + + // 7.x以后版本取消了index中关于type的指定,所以mapping的格式只能支持 + // { + // "properties" : { + // "abc" : { + // "type" : "text" + // } + // } + // } + // properties 外不能再嵌套typeName + + if(StringUtils.isNotBlank(dstDynamic)) { + typeMappings.put("dynamic", dstDynamic); + } + if (isGreaterOrEqualThan7) { + mappings = JSON.toJSONString(typeMappings); + } else { + mappings = JSON.toJSONString(rootMappings); + } + if (StringUtils.isBlank(mappings)) { + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.BAD_CONFIG_VALUE, "must have mappings"); + } + + return mappings; + } + + @Override + public List split(int mandatoryNumber) { + List configurations = new ArrayList(mandatoryNumber); + for (int i = 0; i < mandatoryNumber; i++) { + configurations.add(this.conf.clone()); + } + return configurations; + } + + @Override + public void post() { + ElasticSearchClient esClient = new ElasticSearchClient(this.conf); + String alias = Key.getAlias(conf); + if (!"".equals(alias)) { + LOGGER.info(String.format("alias [%s] to [%s]", alias, Key.getIndexName(conf))); + try { + esClient.alias(Key.getIndexName(conf), alias, Key.isNeedCleanAlias(conf)); + } catch (IOException e) { + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.ES_ALIAS_MODIFY, e); + } + } + } + + @Override + public void destroy() { + + } + } + + public static class Task extends Writer.Task { + + private static final Logger LOGGER = LoggerFactory.getLogger(Job.class); + + private Configuration conf; + + + ElasticSearchClient esClient = null; + private List typeList; + private List columnList; + private List> deleteByConditions; + + private int trySize; + private long tryInterval; + private int batchSize; + private String index; + private String type; + private String splitter; + private ActionType actionType; + private ElasticSearchColumn combinedIdColumn; + private Map colNameToIndexMap; + private Map urlParams; + private boolean columnSizeChecked = false; + private boolean enableRedundantColumn = false; + private boolean enableWriteNull = true; + int retryTimes = 3; + long sleepTimeInMilliSecond = 10000L; + boolean isGreaterOrEqualThan7 = false; + private String fieldDelimiter; + private boolean hasId; + private PrimaryKeyInfo primaryKeyInfo; + private boolean hasPrimaryKeyInfo = false; + private List esPartitionColumn; + private boolean hasEsPartitionColumn = false; + + @Override + public void init() { + this.conf = super.getPluginJobConf(); + this.index = Key.getIndexName(conf); + this.type = Key.getTypeName(conf); + this.trySize = Key.getTrySize(conf); + this.tryInterval = Key.getTryInterval(conf); + this.batchSize = Key.getBatchSize(conf); + this.splitter = Key.getSplitter(conf); + this.actionType = Key.getActionType(conf); + this.urlParams = Key.getUrlParams(conf); + this.enableWriteNull = Key.isEnableNullUpdate(conf); + this.retryTimes = this.conf.getInt("retryTimes", 3); + this.sleepTimeInMilliSecond = this.conf.getLong("sleepTimeInMilliSecond", 10000L); + this.isGreaterOrEqualThan7 = this.conf.getBool("isGreaterOrEqualThan7", false); + this.parseDeleteCondition(conf); + this.columnList = JSON.parseObject(this.conf.getString(WRITE_COLUMNS), new TypeReference>() { + }); + LOGGER.info("columnList: {}", JSON.toJSONString(columnList)); + this.hasId = this.conf.getBool("hasId", false); + if (hasId) { + LOGGER.info("Task has id column, will use it to set _id property"); + } else { + LOGGER.info("Task will use elasticsearch auto generated _id property"); + } + this.fieldDelimiter = Key.getFieldDelimiter(this.conf); + this.enableRedundantColumn = this.conf.getBool("enableRedundantColumn", false); + this.typeList = new ArrayList(); + for (ElasticSearchColumn esColumn : this.columnList) { + this.typeList.add(ElasticSearchFieldType.getESFieldType(esColumn.getType())); + if (esColumn.getCombineFields() != null && esColumn.getCombineFields().size() > 0 + && ElasticSearchFieldType.getESFieldType(esColumn.getType()).equals(ElasticSearchFieldType.ID)) { + combinedIdColumn = esColumn; + } + } + this.primaryKeyInfo = Key.getPrimaryKeyInfo(this.conf); + this.esPartitionColumn = Key.getEsPartitionColumn(this.conf); + this.colNameToIndexMap = new HashMap(5); + this.handleMetaKeys(); + this.esClient = new ElasticSearchClient(this.conf); + } + + private void handleMetaKeys() { + if (null != this.primaryKeyInfo && null != this.primaryKeyInfo.getColumn() + && !this.primaryKeyInfo.getColumn().isEmpty()) { + this.hasPrimaryKeyInfo = true; + if (null == this.primaryKeyInfo.getFieldDelimiter()) { + if (null != this.fieldDelimiter) { + this.primaryKeyInfo.setFieldDelimiter(this.fieldDelimiter); + } else { + this.primaryKeyInfo.setFieldDelimiter(""); + } + } + + for (String eachPk : this.primaryKeyInfo.getColumn()) { + boolean foundKeyInColumn = false; + for (int i = 0; i < columnList.size(); i++) { + if (StringUtils.equals(eachPk, columnList.get(i).getName())) { + this.colNameToIndexMap.put(eachPk, i); + foundKeyInColumn = true; + break; + } + } + if (!foundKeyInColumn) { + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.RECORD_FIELD_NOT_FOUND, + "primaryKeyInfo has column not exists in column"); + } + } + } + + if (null != this.esPartitionColumn && !this.esPartitionColumn.isEmpty()) { + this.hasEsPartitionColumn = true; + for (PartitionColumn eachPartitionCol : this.esPartitionColumn) { + boolean foundKeyInColumn = false; + for (int i = 0; i < columnList.size(); i++) { + if (StringUtils.equals(eachPartitionCol.getName(), columnList.get(i).getName())) { + this.colNameToIndexMap.put(eachPartitionCol.getName(), i); + foundKeyInColumn = true; + break; + } + } + if (!foundKeyInColumn) { + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.RECORD_FIELD_NOT_FOUND, + "esPartitionColumn has column not exists in column"); + } + } + } + } + + private void parseDeleteCondition(Configuration conf) { + List> list = new ArrayList>(); + String config = Key.getDeleteBy(conf); + if (config != null) { + JSONArray array = JSON.parseArray(config); + for (Object obj : array) { + list.add((Map) obj); + } + deleteByConditions = list; + } + } + + + @Override + public void prepare() { + } + + /** + * 示例:{ + * "deleteBy" : [ + * {"product_status" : [-1,-2], "sub_status" : -3}, + * {"product_status" : -3} + * ] + * } + * + * 表示以下两类数据删除: + * 1. product_status为-1或-2并且sub_status为-3 + * 2. product_status为-3 + * + * 注意[{}]返回true + * @param record + * @return + */ + private boolean isDeleteRecord(Record record) { + if (deleteByConditions == null) { + return false; + } + + Map kv = new HashMap(); + for (int i = 0; i < record.getColumnNumber(); i++) { + Column column = record.getColumn(i); + String columnName = columnList.get(i).getName(); + kv.put(columnName, column.asString()); + } + + for (Map delCondition : deleteByConditions) { + if (meetAllCondition(kv, delCondition)) { + return true; + } + } + + return false; + } + + private boolean meetAllCondition(Map kv, Map delCondition) { + for (Map.Entry oneCondition : delCondition.entrySet()) { + if (!checkOneCondition(kv, oneCondition)) { + return false; + } + } + return true; + } + + private boolean checkOneCondition(Map kv, Map.Entry entry) { + Object value = kv.get(entry.getKey()); + if (entry.getValue() instanceof List) { + for (Object obj : (List) entry.getValue()) { + if (obj.toString().equals(value)) { + return true; + } + } + } else { + if (value != null && value.equals(entry.getValue().toString())) { + return true; + } + } + return false; + } + + @Override + public void startWrite(RecordReceiver recordReceiver) { + List writerBuffer = new ArrayList(this.batchSize); + Record record = null; + while ((record = recordReceiver.getFromReader()) != null) { + if (!columnSizeChecked) { + boolean isInvalid = true; + if (enableRedundantColumn) { + isInvalid = this.columnList.size() > record.getColumnNumber(); + } else { + isInvalid = this.columnList.size() != record.getColumnNumber(); + } + if (isInvalid) { + String message = String.format( + "column number not equal error, reader column size is %s, but the writer column size is %s", + record.getColumnNumber(), this.columnList.size()); + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.BAD_CONFIG_VALUE, message); + } + columnSizeChecked = true; + } + writerBuffer.add(record); + if (writerBuffer.size() >= this.batchSize) { + this.doBatchInsert(writerBuffer); + writerBuffer.clear(); + } + } + + if (!writerBuffer.isEmpty()) { + this.doBatchInsert(writerBuffer); + writerBuffer.clear(); + } + } + + private String getDateStr(ElasticSearchColumn esColumn, Column column) { + // 如果保持原样,就直接返回 + if (esColumn.isOrigin()) { + return column.asString(); + } + DateTime date = null; + DateTimeZone dtz = DateTimeZone.getDefault(); + if (esColumn.getTimezone() != null) { + // 所有时区参考 http://www.joda.org/joda-time/timezones.html + // TODO:创建一次多处复用 + dtz = DateTimeZone.forID(esColumn.getTimezone()); + } + if (column.getType() != Column.Type.DATE && esColumn.getFormat() != null) { + // TODO:创建一次多处复用 + DateTimeFormatter formatter = DateTimeFormat.forPattern(esColumn.getFormat()); + date = formatter.withZone(dtz).parseDateTime(column.asString()); + return date.toString(); + } else if (column.getType() == Column.Type.DATE) { + if (null == column.getRawData()) { + return null; + } else { + date = new DateTime(column.asLong(), dtz); + return date.toString(); + } + } else { + return column.asString(); + } + } + + private void doBatchInsert(final List writerBuffer) { + Map data = null; + Bulk.Builder bulkactionTmp = null; + int totalNumber = writerBuffer.size(); + int dirtyDataNumber = 0; + if (this.isGreaterOrEqualThan7) { + bulkactionTmp = new Bulk.Builder().defaultIndex(this.index); + } else { + bulkactionTmp = new Bulk.Builder().defaultIndex(this.index).defaultType(this.type); + } + final Bulk.Builder bulkaction = bulkactionTmp; + // 增加url的参数 + for (Map.Entry entry : urlParams.entrySet()) { + bulkaction.setParameter(entry.getKey(), entry.getValue()); + } + for (Record record : writerBuffer) { + data = new HashMap(); + String id = null; + String parent = null; + String routing = null; + String version = null; + String columnName = null; + Column column = null; + try { + for (int i = 0; i < record.getColumnNumber(); i++) { + column = record.getColumn(i); + columnName = columnList.get(i).getName(); + // 如果组合id不等于null,需要把相关的字段全部忽略 + if (combinedIdColumn != null) { + if (combinedIdColumn.getCombineFields().contains(columnName)) { + continue; + } + } + //如果是json数组,当成对象类型处理 + ElasticSearchFieldType columnType = columnList.get(i).isJsonArray() ? ElasticSearchFieldType.NESTED : typeList.get(i); + + Boolean dstArray = columnList.get(i).isDstArray(); + + //如果是数组类型,那它传入的是字符串类型,也有可能是null + if (columnList.get(i).isArray() && null != column.asString()) { + String[] dataList = column.asString().split(splitter); + if (!columnType.equals(ElasticSearchFieldType.DATE)) { + if (dstArray) { + try { + // 根据客户配置的类型,转换成相应的类型 + switch (columnType) { + case BYTE: + case KEYWORD: + case TEXT: + data.put(columnName, dataList); + break; + case SHORT: + case INTEGER: + if (StringUtils.isBlank(column.asString().trim())) { + data.put(columnName, null); + } else { + Integer[] intDataList = new Integer[dataList.length]; + for (int j = 0; j < dataList.length; j++) { + dataList[j] = dataList[j].trim(); + if (StringUtils.isNotBlank(dataList[j])) { + intDataList[j] = Integer.valueOf(dataList[j]); + } + } + data.put(columnName, intDataList); + } + break; + case LONG: + if (StringUtils.isBlank(column.asString().trim())) { + data.put(columnName, null); + } else { + Long[] longDataList = new Long[dataList.length]; + for (int j = 0; j < dataList.length; j++) { + dataList[j] = dataList[j].trim(); + if (StringUtils.isNotBlank(dataList[j])) { + longDataList[j] = Long.valueOf(dataList[j]); + } + } + data.put(columnName, longDataList); + } + break; + case FLOAT: + case DOUBLE: + if (StringUtils.isBlank(column.asString().trim())) { + data.put(columnName, null); + } else { + Double[] doubleDataList = new Double[dataList.length]; + for (int j = 0; j < dataList.length; j++) { + dataList[j] = dataList[j].trim(); + if (StringUtils.isNotBlank(dataList[j])) { + doubleDataList[j] = Double.valueOf(dataList[j]); + } + } + data.put(columnName, doubleDataList); + } + break; + default: + data.put(columnName, dataList); + break; + } + } catch (Exception e) { + LOGGER.info("脏数据,记录:{}", record.toString()); + continue; + } + } else { + data.put(columnName, dataList); + } + } else { + data.put(columnName, dataList); + } + } else { + // LOGGER.info("columnType: {} integer: {}", columnType, column.asString()); + switch (columnType) { + case ID: + if (id != null) { + id += record.getColumn(i).asString(); + } else { + id = record.getColumn(i).asString(); + } + break; + case PARENT: + if (parent != null) { + parent += record.getColumn(i).asString(); + } else { + parent = record.getColumn(i).asString(); + } + break; + case ROUTING: + if (routing != null) { + routing += record.getColumn(i).asString(); + } else { + routing = record.getColumn(i).asString(); + } + break; + + case VERSION: + if (version != null) { + version += record.getColumn(i).asString(); + } else { + version = record.getColumn(i).asString(); + } + break; + case DATE: + String dateStr = getDateStr(columnList.get(i), column); + data.put(columnName, dateStr); + break; + case KEYWORD: + case STRING: + case TEXT: + case IP: + case GEO_POINT: + case IP_RANGE: + data.put(columnName, column.asString()); + break; + case BOOLEAN: + data.put(columnName, column.asBoolean()); + break; + case BYTE: + case BINARY: + // json序列化不支持byte类型,es支持的binary类型,必须传入base64的格式 + data.put(columnName, column.asString()); + break; + case LONG: + data.put(columnName, column.asLong()); + break; + case INTEGER: + data.put(columnName, column.asLong()); + break; + case SHORT: + data.put(columnName, column.asLong()); + break; + case FLOAT: + case DOUBLE: + data.put(columnName, column.asDouble()); + break; + case GEO_SHAPE: + case DATE_RANGE: + case INTEGER_RANGE: + case FLOAT_RANGE: + case LONG_RANGE: + case DOUBLE_RANGE: + if (null == column.asString()) { + data.put(columnName, column.asString()); + } else { + data.put(columnName, JSON.parse(column.asString())); + } + break; + case NESTED: + case OBJECT: + if (null == column.asString()) { + data.put(columnName, column.asString()); + } else { + // 转json格式 + data.put(columnName, JSON.parse(column.asString())); + } + break; + default: + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.BAD_CONFIG_VALUE, String.format( + "Type error: unsupported type %s for column %s", columnType, columnName)); + } + } + } + + + if (this.hasPrimaryKeyInfo) { + List idData = new ArrayList(); + for (String eachCol : this.primaryKeyInfo.getColumn()) { + Column recordColumn = record.getColumn(this.colNameToIndexMap.get(eachCol)); + idData.add(recordColumn.asString()); + } + id = StringUtils.join(idData, this.primaryKeyInfo.getFieldDelimiter()); + } + if (this.hasEsPartitionColumn) { + List idData = new ArrayList(); + for (PartitionColumn eachCol : this.esPartitionColumn) { + Column recordColumn = record.getColumn(this.colNameToIndexMap.get(eachCol.getName())); + idData.add(recordColumn.asString()); + } + routing = StringUtils.join(idData, ""); + } + } catch (Exception e) { + // 脏数据 + super.getTaskPluginCollector().collectDirtyRecord(record, + String.format("parse error for column: %s errorMessage: %s", columnName, e.getMessage())); + dirtyDataNumber++; + // 处理下一个record + continue; + } + + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("id: {} routing: {} data: {}", id, routing, JSON.toJSONString(data)); + } + + + if (isDeleteRecord(record)) { + Delete.Builder builder = new Delete.Builder(id); + bulkaction.addAction(builder.build()); + } else { + // 使用用户自定义组合唯一键 + if (combinedIdColumn != null) { + try { + id = processIDCombineFields(record, combinedIdColumn); + // LOGGER.debug("id: {}", id); + } catch (Exception e) { + // 脏数据 + super.getTaskPluginCollector().collectDirtyRecord(record, + String.format("parse error for column: %s errorMessage: %s", columnName, e.getMessage())); + // 处理下一个record + dirtyDataNumber++; + continue; + } + } + switch (actionType) { + case INDEX: + // 先进行json序列化,jest client的gson序列化会把等号按照html序列化 + Index.Builder builder = null; + if (this.enableWriteNull) { + builder = new Index.Builder( + JSONObject.toJSONString(data, JSONWriter.Feature.WriteMapNullValue, + JSONWriter.Feature.WriteEnumUsingToString)); + } else { + builder = new Index.Builder(JSONObject.toJSONString(data)); + } + if (id != null) { + builder.id(id); + } + if (parent != null) { + builder.setParameter(Parameters.PARENT, parent); + } + if (routing != null) { + builder.setParameter(Parameters.ROUTING, routing); + } + if (version != null) { + builder.setParameter(Parameters.VERSION, version); + builder.setParameter(Parameters.VERSION_TYPE, "external"); + } + bulkaction.addAction(builder.build()); + break; + case UPDATE: + // doc: https://www.cnblogs.com/crystaltu/articles/6992935.html + // doc: https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html + Map updateDoc = new HashMap(); + updateDoc.put("doc", data); + updateDoc.put("doc_as_upsert", true); + Update.Builder update = null; + if (this.enableWriteNull) { + // write: {a:"1",b:null} + update = new Update.Builder( + JSONObject.toJSONString(updateDoc, JSONWriter.Feature.WriteMapNullValue, + JSONWriter.Feature.WriteEnumUsingToString)); + // 在DEFAULT_GENERATE_FEATURE基础上,只增加了SerializerFeature.WRITE_MAP_NULL_FEATURES + } else { + // write: {"a":"1"} + update = new Update.Builder(JSONObject.toJSONString(updateDoc)); + } + if (id != null) { + update.id(id); + } + if (parent != null) { + update.setParameter(Parameters.PARENT, parent); + } + if (routing != null) { + update.setParameter(Parameters.ROUTING, routing); + } + // version type [EXTERNAL] is not supported by the update API + if (version != null) { + update.setParameter(Parameters.VERSION, version); + } + bulkaction.addAction(update.build()); + break; + default: + break; + } + } + } + + if (dirtyDataNumber >= totalNumber) { + // all batch is dirty data + LOGGER.warn("all this batch is dirty data, dirtyDataNumber: {} totalDataNumber: {}", dirtyDataNumber, + totalNumber); + return; + } + + BulkResult bulkResult = null; + try { + bulkResult = RetryUtil.executeWithRetry(new Callable() { + @Override + public BulkResult call() throws Exception { + JestResult jestResult = esClient.bulkInsert(bulkaction); + if (jestResult.isSucceeded()) { + return null; + } + String msg = String.format("response code: [%d] error :[%s]", jestResult.getResponseCode(), + jestResult.getErrorMessage()); + LOGGER.warn(msg); + if (esClient.isBulkResult(jestResult)) { + BulkResult brst = (BulkResult) jestResult; + List failedItems = brst.getFailedItems(); + for (BulkResult.BulkResultItem item : failedItems) { + if (item.status != 400) { + // 400 BAD_REQUEST 如果非数据异常,请求异常,则不允许忽略 + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.ES_INDEX_INSERT, + String.format("status:[%d], error: %s", item.status, item.error)); + } else { + // 如果用户选择不忽略解析错误,则抛异常,默认为忽略 + if (!Key.isIgnoreParseError(conf)) { + throw new NoReRunException(ElasticSearchWriterErrorCode.ES_INDEX_INSERT, + String.format( + "status:[%d], error: %s, config not ignoreParseError so throw this error", + item.status, item.error)); + } + } + } + return brst; + } else { + Integer status = esClient.getStatus(jestResult); + switch (status) { + case 429: // TOO_MANY_REQUESTS + LOGGER.warn("server response too many requests, so auto reduce speed"); + break; + default: + break; + } + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.ES_INDEX_INSERT, + jestResult.getErrorMessage()); + } + } + }, this.trySize, this.tryInterval, false, Arrays.asList(DataXException.class)); + } catch (Exception e) { + if (Key.isIgnoreWriteError(this.conf)) { + LOGGER.warn(String.format("Retry [%d] write failed, ignore the error, continue to write!", trySize)); + } else { + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.ES_INDEX_INSERT, e.getMessage(), e); + } + } + + if (null != bulkResult) { + List items = bulkResult.getItems(); + for (int idx = 0; idx < items.size(); ++idx) { + BulkResult.BulkResultItem item = items.get(idx); + if (item.error != null && !"".equals(item.error)) { + super.getTaskPluginCollector().collectDirtyRecord(writerBuffer.get(idx), + String.format("status:[%d], error: %s", item.status, item.error)); + } + } + } + } + + private int getRecordColumnIndex(Record record, String columnName) { + if (colNameToIndexMap.containsKey(columnName)) { + return colNameToIndexMap.get(columnName); + } + + List columns = new ArrayList(); + int index = -1; + for (int i=0; i 1) { + throw DataXException.asDataXException( + ElasticSearchWriterErrorCode.RECORD_FIELD_NOT_FOUND, + "record has multiple columns found by name: " + columnName); + } + + colNameToIndexMap.put(columnName, index); + return index; + } + + private String processIDCombineFields(Record record, ElasticSearchColumn esColumn) { + List values = new ArrayList(esColumn.getCombineFields().size()); + for (String field : esColumn.getCombineFields()) { + int colIndex = getRecordColumnIndex(record, field); + Column col = record.getColumnNumber() <= colIndex ? null : record.getColumn(colIndex); + if (col == null) { + throw DataXException.asDataXException(ElasticSearchWriterErrorCode.RECORD_FIELD_NOT_FOUND, field); + } + values.add(col.asString()); + } + return Joiner.on(esColumn.getCombineFieldsValueSeparator()).join(values); + } + + @Override + public void post() { + } + + @Override + public void destroy() { + try { + this.esClient.closeJestClient(); + } catch (Exception e) { + LOGGER.warn("ignore close jest client error: {}", e.getMessage()); + } + } + + } +} diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchWriterErrorCode.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchWriterErrorCode.java new file mode 100644 index 0000000000..c9b02532e3 --- /dev/null +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/ElasticSearchWriterErrorCode.java @@ -0,0 +1,41 @@ +package com.alibaba.datax.plugin.writer.elasticsearchwriter; + +import com.alibaba.datax.common.spi.ErrorCode; + +public enum ElasticSearchWriterErrorCode implements ErrorCode { + BAD_CONFIG_VALUE("ESWriter-00", "The value you configured is not valid."), + ES_INDEX_DELETE("ESWriter-01", "Delete index error."), + ES_INDEX_CREATE("ESWriter-02", "Index creation error."), + ES_MAPPINGS("ESWriter-03", "The mappings error."), + ES_INDEX_INSERT("ESWriter-04", "Insert data error."), + ES_ALIAS_MODIFY("ESWriter-05", "Alias modification error."), + JSON_PARSE("ESWrite-06", "Json format parsing error"), + UPDATE_WITH_ID("ESWrite-07", "Update mode must specify column type with id"), + RECORD_FIELD_NOT_FOUND("ESWrite-08", "Field does not exist in the original table"), + ES_GET_SETTINGS("ESWriter-09", "get settings failed"); + ; + + private final String code; + private final String description; + + ElasticSearchWriterErrorCode(String code, String description) { + this.code = code; + this.description = description; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + return String.format("Code:[%s], Description:[%s]. ", this.code, + this.description); + } +} diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/JsonPathUtil.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/JsonPathUtil.java new file mode 100644 index 0000000000..e7619e7c39 --- /dev/null +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/JsonPathUtil.java @@ -0,0 +1,28 @@ +package com.alibaba.datax.plugin.writer.elasticsearchwriter; + +import java.util.List; + +import com.alibaba.fastjson2.JSONObject; + +public class JsonPathUtil { + + public static JSONObject getJsonObject(List paths, JSONObject data) { + if (null == paths || paths.isEmpty()) { + return data; + } + + if (null == data) { + return null; + } + + JSONObject dataTmp = data; + for (String each : paths) { + if (null != dataTmp) { + dataTmp = dataTmp.getJSONObject(each); + } else { + return null; + } + } + return dataTmp; + } +} \ No newline at end of file diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/JsonUtil.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/JsonUtil.java new file mode 100644 index 0000000000..ad6c01be76 --- /dev/null +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/JsonUtil.java @@ -0,0 +1,54 @@ +package com.alibaba.datax.plugin.writer.elasticsearchwriter; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONException; +import com.alibaba.fastjson2.JSONObject; + +/** + * @author bozu + * @date 2021/01/06 + */ +public class JsonUtil { + + /** + * 合并两个json + * @param source 源json + * @param target 目标json + * @return 合并后的json + * @throws JSONException + */ + public static String mergeJsonStr(String source, String target) throws JSONException { + if(source == null) { + return target; + } + if(target == null) { + return source; + } + return JSON.toJSONString(deepMerge(JSON.parseObject(source), JSON.parseObject(target))); + } + + /** + * 深度合并两个json对象,将source的值,merge到target中 + * @param source 源json + * @param target 目标json + * @return 合并后的json + * @throws JSONException + */ + private static JSONObject deepMerge(JSONObject source, JSONObject target) throws JSONException { + for (String key: source.keySet()) { + Object value = source.get(key); + if (target.containsKey(key)) { + // existing value for "key" - recursively deep merge: + if (value instanceof JSONObject) { + JSONObject valueJson = (JSONObject)value; + deepMerge(valueJson, target.getJSONObject(key)); + } else { + target.put(key, value); + } + } else { + target.put(key, value); + } + } + return target; + } +} diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/Key.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/Key.java index 0f2d3f5c20..fcaac935a8 100644 --- a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/Key.java +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/Key.java @@ -1,9 +1,13 @@ package com.alibaba.datax.plugin.writer.elasticsearchwriter; import com.alibaba.datax.common.util.Configuration; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; + import org.apache.commons.lang3.StringUtils; import java.util.HashMap; +import java.util.List; import java.util.Map; public final class Key { @@ -37,31 +41,35 @@ public static ActionType getActionType(Configuration conf) { public static String getEndpoint(Configuration conf) { - return conf.getNecessaryValue("endpoint", ESWriterErrorCode.BAD_CONFIG_VALUE); + return conf.getNecessaryValue("endpoint", ElasticSearchWriterErrorCode.BAD_CONFIG_VALUE); } - public static String getAccessID(Configuration conf) { - return conf.getString("accessId", ""); + public static String getUsername(Configuration conf) { + return conf.getString("username", conf.getString("accessId")); } - public static String getAccessKey(Configuration conf) { - return conf.getString("accessKey", ""); + public static String getPassword(Configuration conf) { + return conf.getString("password", conf.getString("accessKey")); } public static int getBatchSize(Configuration conf) { - return conf.getInt("batchSize", 1000); + return conf.getInt("batchSize", 1024); } public static int getTrySize(Configuration conf) { return conf.getInt("trySize", 30); } + public static long getTryInterval(Configuration conf) { + return conf.getLong("tryInterval", 60000L); + } + public static int getTimeout(Configuration conf) { return conf.getInt("timeout", 600000); } - public static boolean isCleanup(Configuration conf) { - return conf.getBool("cleanup", false); + public static boolean isTruncate(Configuration conf) { + return conf.getBool("truncate", conf.getBool("cleanup", false)); } public static boolean isDiscovery(Configuration conf) { @@ -69,7 +77,7 @@ public static boolean isDiscovery(Configuration conf) { } public static boolean isCompression(Configuration conf) { - return conf.getBool("compression", true); + return conf.getBool("compress", conf.getBool("compression", true)); } public static boolean isMultiThread(Configuration conf) { @@ -77,9 +85,17 @@ public static boolean isMultiThread(Configuration conf) { } public static String getIndexName(Configuration conf) { - return conf.getNecessaryValue("index", ESWriterErrorCode.BAD_CONFIG_VALUE); + return conf.getNecessaryValue("index", ElasticSearchWriterErrorCode.BAD_CONFIG_VALUE); } + public static String getDeleteBy(Configuration conf) { + return conf.getString("deleteBy"); + } + + + /** + * TODO: 在7.0开始,一个索引只能建一个Type为_doc + * */ public static String getTypeName(Configuration conf) { String indexType = conf.getString("indexType"); if(StringUtils.isBlank(indexType)){ @@ -128,4 +144,58 @@ public static String getSplitter(Configuration conf) { public static boolean getDynamic(Configuration conf) { return conf.getBool("dynamic", false); } + + public static String getDstDynamic(Configuration conf) { + return conf.getString("dstDynamic"); + } + + public static String getDiscoveryFilter(Configuration conf){ + return conf.getString("discoveryFilter","_all"); + } + + public static Boolean getVersioning(Configuration conf) { + return conf.getBool("versioning", false); + } + + public static Long getUnifiedVersion(Configuration conf) { + return conf.getLong("version", System.currentTimeMillis()); + } + + public static Map getUrlParams(Configuration conf) { + return conf.getMap("urlParams", new HashMap()); + } + + public static Integer getESVersion(Configuration conf) { + return conf.getInt("esVersion"); + } + + public static String getMasterTimeout(Configuration conf) { + return conf.getString("masterTimeout", "5m"); + } + + public static boolean isEnableNullUpdate(Configuration conf) { + return conf.getBool("enableWriteNull", true); + } + + public static String getFieldDelimiter(Configuration conf) { + return conf.getString("fieldDelimiter", ""); + } + + public static PrimaryKeyInfo getPrimaryKeyInfo(Configuration conf) { + String primaryKeyInfoString = conf.getString("primaryKeyInfo"); + if (StringUtils.isNotBlank(primaryKeyInfoString)) { + return JSON.parseObject(primaryKeyInfoString, new TypeReference() {}); + } else { + return null; + } + } + + public static List getEsPartitionColumn(Configuration conf) { + String esPartitionColumnString = conf.getString("esPartitionColumn"); + if (StringUtils.isNotBlank(esPartitionColumnString)) { + return JSON.parseObject(esPartitionColumnString, new TypeReference>() {}); + } else { + return null; + } + } } diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/NoReRunException.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/NoReRunException.java new file mode 100644 index 0000000000..52064e58d7 --- /dev/null +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/NoReRunException.java @@ -0,0 +1,16 @@ +package com.alibaba.datax.plugin.writer.elasticsearchwriter; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.spi.ErrorCode; + +public class NoReRunException extends DataXException { + public NoReRunException(String errorMessage) { + super(errorMessage); + } + + public NoReRunException(ErrorCode errorCode, String errorMessage) { + super(errorCode, errorMessage); + } + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/PartitionColumn.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/PartitionColumn.java new file mode 100644 index 0000000000..b99829b2cc --- /dev/null +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/PartitionColumn.java @@ -0,0 +1,42 @@ +package com.alibaba.datax.plugin.writer.elasticsearchwriter; + +public class PartitionColumn { + private String name; + // like: DATA + private String metaType; + private String comment; + // like: VARCHAR + private String type; + + public String getName() { + return name; + } + + public String getMetaType() { + return metaType; + } + + public String getComment() { + return comment; + } + + public String getType() { + return type; + } + + public void setName(String name) { + this.name = name; + } + + public void setMetaType(String metaType) { + this.metaType = metaType; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public void setType(String type) { + this.type = type; + } +} \ No newline at end of file diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/PrimaryKeyInfo.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/PrimaryKeyInfo.java new file mode 100644 index 0000000000..b5821f515e --- /dev/null +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/PrimaryKeyInfo.java @@ -0,0 +1,47 @@ +package com.alibaba.datax.plugin.writer.elasticsearchwriter; + +import java.util.List; + +public class PrimaryKeyInfo { + + /** + * 主键类型:PrimaryKeyTypeEnum + * + * pk: 单个(业务)主键 specific: 联合主键 + */ + private String type; + + /** + * 用户定义的联合主键的连接符号 + */ + private String fieldDelimiter; + + /** + * 主键的列的名称 + */ + private List column; + + public String getType() { + return type; + } + + public String getFieldDelimiter() { + return fieldDelimiter; + } + + public List getColumn() { + return column; + } + + public void setType(String type) { + this.type = type; + } + + public void setFieldDelimiter(String fieldDelimiter) { + this.fieldDelimiter = fieldDelimiter; + } + + public void setColumn(List column) { + this.column = column; + } +} \ No newline at end of file diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/jest/ClusterInfo.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/jest/ClusterInfo.java new file mode 100644 index 0000000000..173bc9e264 --- /dev/null +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/jest/ClusterInfo.java @@ -0,0 +1,35 @@ +package com.alibaba.datax.plugin.writer.elasticsearchwriter.jest; + +import com.google.gson.Gson; +import io.searchbox.action.AbstractAction; +import io.searchbox.client.config.ElasticsearchVersion; + +public class ClusterInfo extends AbstractAction { + @Override + protected String buildURI(ElasticsearchVersion elasticsearchVersion) { + return ""; + } + + @Override + public String getRestMethodName() { + return "GET"; + } + + @Override + public ClusterInfoResult createNewElasticSearchResult(String responseBody, int statusCode, String reasonPhrase, Gson gson) { + return createNewElasticSearchResult(new ClusterInfoResult(gson), responseBody, statusCode, reasonPhrase, gson); + } + + public static class Builder extends AbstractAction.Builder { + + public Builder() { + setHeader("accept", "application/json"); + setHeader("content-type", "application/json"); + } + + @Override + public ClusterInfo build() { + return new ClusterInfo(); + } + } +} diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/jest/ClusterInfoResult.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/jest/ClusterInfoResult.java new file mode 100644 index 0000000000..b4f49a37c0 --- /dev/null +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/jest/ClusterInfoResult.java @@ -0,0 +1,49 @@ +package com.alibaba.datax.plugin.writer.elasticsearchwriter.jest; + +import com.google.gson.Gson; +import io.searchbox.client.JestResult; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ClusterInfoResult extends JestResult { + + private static final Pattern FIRST_NUMBER = Pattern.compile("\\d"); + + private static final int SEVEN = 7; + + public ClusterInfoResult(Gson gson) { + super(gson); + } + + public ClusterInfoResult(JestResult source) { + super(source); + } + + /** + * 判断es集群的部署版本是否大于7.x + * 大于7.x的es对于Index的type有较大改动,需要做额外判定 + * 对于7.x与6.x版本的es都做过测试,返回符合预期;5.x以下版本直接try-catch后返回false,向下兼容 + * @return + */ + public Boolean isGreaterOrEqualThan7() throws Exception { + // 如果是没有权限,直接返回false,兼容老版本 + if (responseCode == 403) { + return false; + } + if (!isSucceeded) { + throw new Exception(getJsonString()); + } + try { + String version = jsonObject.getAsJsonObject("version").get("number").toString(); + Matcher matcher = FIRST_NUMBER.matcher(version); + matcher.find(); + String number = matcher.group(); + Integer versionNum = Integer.valueOf(number); + return versionNum >= SEVEN; + } catch (Exception e) { + //5.x 以下版本不做兼容测试,如果返回json格式解析失败,有可能是以下版本,所以认为不大于7.x + return false; + } + } +} diff --git a/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/jest/PutMapping7.java b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/jest/PutMapping7.java new file mode 100644 index 0000000000..c9f1d6be84 --- /dev/null +++ b/elasticsearchwriter/src/main/java/com/alibaba/datax/plugin/writer/elasticsearchwriter/jest/PutMapping7.java @@ -0,0 +1,39 @@ +package com.alibaba.datax.plugin.writer.elasticsearchwriter.jest; + +import io.searchbox.action.GenericResultAbstractAction; +import io.searchbox.client.config.ElasticsearchVersion; + +public class PutMapping7 extends GenericResultAbstractAction { + protected PutMapping7(PutMapping7.Builder builder) { + super(builder); + + this.indexName = builder.index; + this.payload = builder.source; + } + + @Override + protected String buildURI(ElasticsearchVersion elasticsearchVersion) { + return super.buildURI(elasticsearchVersion) + "/_mapping"; + } + + @Override + public String getRestMethodName() { + return "PUT"; + } + + public static class Builder extends GenericResultAbstractAction.Builder { + private String index; + private Object source; + + public Builder(String index, Object source) { + this.index = index; + this.source = source; + } + + @Override + public PutMapping7 build() { + return new PutMapping7(this); + } + } + +} diff --git a/elasticsearchwriter/src/main/resources/plugin.json b/elasticsearchwriter/src/main/resources/plugin.json index b6e6384bce..b39f1222d0 100644 --- a/elasticsearchwriter/src/main/resources/plugin.json +++ b/elasticsearchwriter/src/main/resources/plugin.json @@ -1,6 +1,6 @@ { "name": "elasticsearchwriter", - "class": "com.alibaba.datax.plugin.writer.elasticsearchwriter.ESWriter", + "class": "com.alibaba.datax.plugin.writer.elasticsearchwriter.ElasticSearchWriter", "description": "适用于: 生产环境. 原理: TODO", "developer": "alibaba" } \ No newline at end of file diff --git a/ftpreader/pom.xml b/ftpreader/pom.xml index 7778d49158..57bf889d72 100755 --- a/ftpreader/pom.xml +++ b/ftpreader/pom.xml @@ -45,7 +45,7 @@ com.jcraft jsch - 0.1.51 + 0.1.54 commons-net @@ -89,4 +89,4 @@ - + diff --git a/ftpreader/src/main/java/com/alibaba/datax/plugin/reader/ftpreader/SftpHelper.java b/ftpreader/src/main/java/com/alibaba/datax/plugin/reader/ftpreader/SftpHelper.java index d25b040c41..6e42e10c02 100644 --- a/ftpreader/src/main/java/com/alibaba/datax/plugin/reader/ftpreader/SftpHelper.java +++ b/ftpreader/src/main/java/com/alibaba/datax/plugin/reader/ftpreader/SftpHelper.java @@ -64,6 +64,8 @@ public void loginFtpServer(String host, String username, String password, int po String message = String.format("请确认连接ftp服务器端口是否正确,错误的端口: [%s] ", port); LOG.error(message); throw DataXException.asDataXException(FtpReaderErrorCode.FAIL_LOGIN, message, e); + }else{ + throw DataXException.asDataXException(FtpReaderErrorCode.COMMAND_FTP_IO_EXCEPTION, "", e); } }else { if("Auth fail".equals(e.getMessage())){ diff --git a/ftpwriter/doc/ftpwriter.md b/ftpwriter/doc/ftpwriter.md index 6b1b2687f1..a38a1052e2 100644 --- a/ftpwriter/doc/ftpwriter.md +++ b/ftpwriter/doc/ftpwriter.md @@ -24,7 +24,7 @@ FtpWriter实现了从DataX协议转为FTP文件功能,FTP文件本身是无结 我们不能做到: -1. 单个文件不能支持并发写入。 +1. 单个文件并发写入。 ## 3 功能说明 diff --git a/ftpwriter/pom.xml b/ftpwriter/pom.xml index 69ec4a0712..bf7ce83de4 100644 --- a/ftpwriter/pom.xml +++ b/ftpwriter/pom.xml @@ -45,7 +45,7 @@ com.jcraft jsch - 0.1.51 + 0.1.54 commons-net diff --git a/ftpwriter/src/main/java/com/alibaba/datax/plugin/writer/ftpwriter/util/SftpHelperImpl.java b/ftpwriter/src/main/java/com/alibaba/datax/plugin/writer/ftpwriter/util/SftpHelperImpl.java index e6d786298e..e748f12c97 100644 --- a/ftpwriter/src/main/java/com/alibaba/datax/plugin/writer/ftpwriter/util/SftpHelperImpl.java +++ b/ftpwriter/src/main/java/com/alibaba/datax/plugin/writer/ftpwriter/util/SftpHelperImpl.java @@ -14,8 +14,8 @@ import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.plugin.writer.ftpwriter.FtpWriterErrorCode; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONWriter; import com.jcraft.jsch.ChannelSftp; import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSchException; @@ -251,7 +251,7 @@ public Set getAllFilesInDir(String dir, String prefixFileName) { @SuppressWarnings("rawtypes") Vector allFiles = this.channelSftp.ls(dir); LOG.debug(String.format("ls: %s", JSON.toJSONString(allFiles, - SerializerFeature.UseSingleQuotes))); + JSONWriter.Feature.UseSingleQuotes))); for (int i = 0; i < allFiles.size(); i++) { LsEntry le = (LsEntry) allFiles.get(i); String strName = le.getFilename(); diff --git a/ftpwriter/src/main/java/com/alibaba/datax/plugin/writer/ftpwriter/util/StandardFtpHelperImpl.java b/ftpwriter/src/main/java/com/alibaba/datax/plugin/writer/ftpwriter/util/StandardFtpHelperImpl.java index 8999b0a85a..d5b9a74671 100644 --- a/ftpwriter/src/main/java/com/alibaba/datax/plugin/writer/ftpwriter/util/StandardFtpHelperImpl.java +++ b/ftpwriter/src/main/java/com/alibaba/datax/plugin/writer/ftpwriter/util/StandardFtpHelperImpl.java @@ -18,8 +18,8 @@ import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.plugin.writer.ftpwriter.FtpWriterErrorCode; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.serializer.SerializerFeature; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONWriter; public class StandardFtpHelperImpl implements IFtpHelper { private static final Logger LOG = LoggerFactory @@ -244,7 +244,7 @@ public Set getAllFilesInDir(String dir, String prefixFileName) { FTPFile[] fs = this.ftpClient.listFiles(dir); // LOG.debug(JSON.toJSONString(this.ftpClient.listNames(dir))); LOG.debug(String.format("ls: %s", - JSON.toJSONString(fs, SerializerFeature.UseSingleQuotes))); + JSON.toJSONString(fs, JSONWriter.Feature.UseSingleQuotes))); for (FTPFile ff : fs) { String strName = ff.getName(); if (strName.startsWith(prefixFileName)) { diff --git a/gaussdbreader/doc/gaussdbreader.md b/gaussdbreader/doc/gaussdbreader.md new file mode 100644 index 0000000000..5caa4d5976 --- /dev/null +++ b/gaussdbreader/doc/gaussdbreader.md @@ -0,0 +1,297 @@ + +# GaussDbReader 插件文档 + + +___ + + +## 1 快速介绍 + +GaussDbReader插件实现了从GaussDB读取数据。在底层实现上,GaussDbReader通过JDBC连接远程GaussDB数据库,并执行相应的sql语句将数据从GaussDB库中SELECT出来。 + +## 2 实现原理 + +简而言之,GaussDbReader通过JDBC连接器连接到远程的GaussDB数据库,并根据用户配置的信息生成查询SELECT SQL语句并发送到远程GaussDB数据库,并将该SQL执行返回结果使用DataX自定义的数据类型拼装为抽象的数据集,并传递给下游Writer处理。 + +对于用户配置Table、Column、Where的信息,GaussDbReader将其拼接为SQL语句发送到GaussDB数据库;对于用户配置querySql信息,GaussDbReader直接将其发送到GaussDB数据库。 + + +## 3 功能说明 + +### 3.1 配置样例 + +* 配置一个从GaussDB数据库同步抽取数据到本地的作业: + +``` +{ + "job": { + "setting": { + "speed": { + //设置传输速度,单位为byte/s,DataX运行会尽可能达到该速度但是不超过它. + "byte": 1048576 + }, + //出错限制 + "errorLimit": { + //出错的record条数上限,当大于该值即报错。 + "record": 0, + //出错的record百分比上限 1.0表示100%,0.02表示2% + "percentage": 0.02 + } + }, + "content": [ + { + "reader": { + "name": "gaussdbreader", + "parameter": { + // 数据库连接用户名 + "username": "xx", + // 数据库连接密码 + "password": "xx", + "column": [ + "id","name" + ], + //切分主键 + "splitPk": "id", + "connection": [ + { + "table": [ + "table" + ], + "jdbcUrl": [ + "jdbc:opengauss://host:port/database" + ] + } + ] + } + }, + "writer": { + //writer类型 + "name": "streamwriter", + //是否打印内容 + "parameter": { + "print":true, + } + } + } + ] + } +} + +``` + +* 配置一个自定义SQL的数据库同步任务到本地内容的作业: + +```json +{ + "job": { + "setting": { + "speed": 1048576 + }, + "content": [ + { + "reader": { + "name": "gaussdbreader", + "parameter": { + "username": "xx", + "password": "xx", + "where": "", + "connection": [ + { + "querySql": [ + "select db_id,on_line_flag from db_info where db_id < 10;" + ], + "jdbcUrl": [ + "jdbc:opengauss://host:port/database", "jdbc:opengauss://host:port/database" + ] + } + ] + } + }, + "writer": { + "name": "streamwriter", + "parameter": { + "print": false, + "encoding": "UTF-8" + } + } + } + ] + } +} +``` + + +### 3.2 参数说明 + +* **jdbcUrl** + + * 描述:描述的是到对端数据库的JDBC连接信息,使用JSON的数组描述,并支持一个库填写多个连接地址。之所以使用JSON数组描述连接信息,是因为阿里集团内部支持多个IP探测,如果配置了多个,GaussDbReader可以依次探测ip的可连接性,直到选择一个合法的IP。如果全部连接失败,GaussDbReader报错。 注意,jdbcUrl必须包含在connection配置单元中。对于阿里集团外部使用情况,JSON数组填写一个JDBC连接即可。 + + jdbcUrl按照GaussDB官方规范,并可以填写连接附件控制信息。具体请参看[GaussDB官方文档](https://docs.opengauss.org/zh/docs/3.1.0/docs/Developerguide/java-sql-Connection.html)。 + + * 必选:是
+ + * 默认值:无
+ +* **username** + + * 描述:数据源的用户名
+ + * 必选:是
+ + * 默认值:无
+ +* **password** + + * 描述:数据源指定用户名的密码
+ + * 必选:是
+ + * 默认值:无
+ +* **table** + + * 描述:所选取的需要同步的表。使用JSON的数组描述,因此支持多张表同时抽取。当配置为多张表时,用户自己需保证多张表是同一schema结构,GaussDbReader不予检查表是否同一逻辑表。注意,table必须包含在connection配置单元中。
+ + * 必选:是
+ + * 默认值:无
+ +* **column** + + * 描述:所配置的表中需要同步的列名集合,使用JSON的数组描述字段信息。用户使用\*代表默认使用所有列配置,例如['\*']。 + + 支持列裁剪,即列可以挑选部分列进行导出。 + + 支持列换序,即列可以不按照表schema信息进行导出。 + + 支持常量配置,用户需要按照GaussDB语法格式: + ["id", "'hello'::varchar", "true", "2.5::real", "power(2,3)"] + id为普通列名,'hello'::varchar为字符串常量,true为布尔值,2.5为浮点数, power(2,3)为函数。 + + **column必须用户显示指定同步的列集合,不允许为空!** + + * 必选:是
+ + * 默认值:无
+ +* **splitPk** + + * 描述:GaussDbReader进行数据抽取时,如果指定splitPk,表示用户希望使用splitPk代表的字段进行数据分片,DataX因此会启动并发任务进行数据同步,这样可以大大提高数据同步的效能。 + + 推荐splitPk用户使用表主键,因为表主键通常情况下比较均匀,因此切分出来的分片也不容易出现数据热点。 + + 目前splitPk仅支持整形数据切分,`不支持浮点、字符串型、日期等其他类型`。如果用户指定其他非支持类型,GaussDbReader将报错! + + splitPk设置为空,底层将视作用户不允许对单表进行切分,因此使用单通道进行抽取。 + + * 必选:否
+ + * 默认值:空
+ +* **where** + + * 描述:筛选条件,GaussDbReader根据指定的column、table、where条件拼接SQL,并根据这个SQL进行数据抽取。在实际业务场景中,往往会选择当天的数据进行同步,可以将where条件指定为gmt_create > $bizdate 。注意:不可以将where条件指定为limit 10,limit不是SQL的合法where子句。
+ + where条件可以有效地进行业务增量同步。 where条件不配置或者为空,视作全表同步数据。 + + * 必选:否
+ + * 默认值:无
+ +* **querySql** + + * 描述:在有些业务场景下,where这一配置项不足以描述所筛选的条件,用户可以通过该配置型来自定义筛选SQL。当用户配置了这一项之后,DataX系统就会忽略table,column这些配置型,直接使用这个配置项的内容对数据进行筛选,例如需要进行多表join后同步数据,使用select a,b from table_a join table_b on table_a.id = table_b.id
+ + `当用户配置querySql时,GaussDbReader直接忽略table、column、where条件的配置`。 + + * 必选:否
+ + * 默认值:无
+ +* **fetchSize** + + * 描述:该配置项定义了插件和数据库服务器端每次批量数据获取条数,该值决定了DataX和服务器端的网络交互次数,能够较大的提升数据抽取性能。
+ + `注意,该值过大(>2048)可能造成DataX进程OOM。`。 + + * 必选:否
+ + * 默认值:1024
+ + +### 3.3 类型转换 + +目前GaussDbReader支持大部分GaussDB类型,但也存在部分个别类型没有支持的情况,请注意检查你的类型。 + +下面列出GaussDbReader针对GaussDB类型转换列表: + + +| DataX 内部类型| GaussDB 数据类型 | +| -------- | ----- | +| Long |bigint, bigserial, integer, smallint, serial | +| Double |double precision, money, numeric, real | +| String |varchar, char, text, bit, inet| +| Date |date, time, timestamp | +| Boolean |bool| +| Bytes |bytea| + +请注意: + +* `除上述罗列字段类型外,其他类型均不支持; money,inet,bit需用户使用a_inet::varchar类似的语法转换`。 + +## 4 性能报告 + +### 4.1 环境准备 + +#### 4.1.1 数据特征 +建表语句: + +create table pref_test( + id serial, + a_bigint bigint, + a_bit bit(10), + a_boolean boolean, + a_char character(5), + a_date date, + a_double double precision, + a_integer integer, + a_money money, + a_num numeric(10,2), + a_real real, + a_smallint smallint, + a_text text, + a_time time, + a_timestamp timestamp +) + +#### 4.1.2 机器参数 + +* 执行DataX的机器参数为: + 1. cpu: 16核 Intel(R) Xeon(R) CPU E5620 @ 2.40GHz + 2. mem: MemTotal: 24676836kB MemFree: 6365080kB + 3. net: 百兆双网卡 + +* GaussDB数据库机器参数为: + D12 24逻辑核 192G内存 12*480G SSD 阵列 + + +### 4.2 测试报告 + +#### 4.2.1 单表测试报告 + + +| 通道数 | 是否按照主键切分 | DataX速度(Rec/s) | DataX流量(MB/s) | DataX机器运行负载 | +|--------|--------| --------|--------|--------| +|1| 否 | 10211 | 0.63 | 0.2 | +|1| 是 | 10211 | 0.63 | 0.2 | +|4| 否 | 10211 | 0.63 | 0.2 | +|4| 是 | 40000 | 2.48 | 0.5 | +|8| 否 | 10211 | 0.63 | 0.2 | +|8| 是 | 78048 | 4.84 | 0.8 | + + +说明: + +1. 这里的单表,主键类型为 serial,数据分布均匀。 +2. 对单表如果没有按照主键切分,那么配置通道个数不会提升速度,效果与1个通道一样。 diff --git a/gaussdbreader/pom.xml b/gaussdbreader/pom.xml new file mode 100644 index 0000000000..ad2e0ba058 --- /dev/null +++ b/gaussdbreader/pom.xml @@ -0,0 +1,86 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + + gaussdbreader + gaussdbreader + jar + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + + org.slf4j + slf4j-api + + + + ch.qos.logback + logback-classic + + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + + org.opengauss + opengauss-jdbc + 3.0.0 + + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + + \ No newline at end of file diff --git a/gaussdbreader/src/main/assembly/package.xml b/gaussdbreader/src/main/assembly/package.xml new file mode 100755 index 0000000000..65601e4505 --- /dev/null +++ b/gaussdbreader/src/main/assembly/package.xml @@ -0,0 +1,35 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/reader/gaussdbreader + + + target/ + + gaussdbreader-0.0.1-SNAPSHOT.jar + + plugin/reader/gaussdbreader + + + + + + false + plugin/reader/gaussdbreader/libs + runtime + + + diff --git a/gaussdbreader/src/main/java/com/alibaba/datax/plugin/reader/gaussdbreader/Constant.java b/gaussdbreader/src/main/java/com/alibaba/datax/plugin/reader/gaussdbreader/Constant.java new file mode 100644 index 0000000000..33cdd3091e --- /dev/null +++ b/gaussdbreader/src/main/java/com/alibaba/datax/plugin/reader/gaussdbreader/Constant.java @@ -0,0 +1,7 @@ +package com.alibaba.datax.plugin.reader.gaussdbreader; + +public class Constant { + + public static final int DEFAULT_FETCH_SIZE = 1000; + +} diff --git a/gaussdbreader/src/main/java/com/alibaba/datax/plugin/reader/gaussdbreader/GaussDbReader.java b/gaussdbreader/src/main/java/com/alibaba/datax/plugin/reader/gaussdbreader/GaussDbReader.java new file mode 100644 index 0000000000..ca158ab720 --- /dev/null +++ b/gaussdbreader/src/main/java/com/alibaba/datax/plugin/reader/gaussdbreader/GaussDbReader.java @@ -0,0 +1,86 @@ +package com.alibaba.datax.plugin.reader.gaussdbreader; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.spi.Reader; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.CommonRdbmsReader; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; + +import java.util.List; + +public class GaussDbReader extends Reader { + + private static final DataBaseType DATABASE_TYPE = DataBaseType.GaussDB; + + public static class Job extends Reader.Job { + + private Configuration originalConfig; + private CommonRdbmsReader.Job commonRdbmsReaderMaster; + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + int fetchSize = this.originalConfig.getInt(com.alibaba.datax.plugin.rdbms.reader.Constant.FETCH_SIZE, + Constant.DEFAULT_FETCH_SIZE); + if (fetchSize < 1) { + throw DataXException.asDataXException(DBUtilErrorCode.REQUIRED_VALUE, + String.format("您配置的fetchSize有误,根据DataX的设计,fetchSize : [%d] 设置值不能小于 1.", fetchSize)); + } + this.originalConfig.set(com.alibaba.datax.plugin.rdbms.reader.Constant.FETCH_SIZE, fetchSize); + + this.commonRdbmsReaderMaster = new CommonRdbmsReader.Job(DATABASE_TYPE); + this.commonRdbmsReaderMaster.init(this.originalConfig); + } + + @Override + public List split(int adviceNumber) { + return this.commonRdbmsReaderMaster.split(this.originalConfig, adviceNumber); + } + + @Override + public void post() { + this.commonRdbmsReaderMaster.post(this.originalConfig); + } + + @Override + public void destroy() { + this.commonRdbmsReaderMaster.destroy(this.originalConfig); + } + + } + + public static class Task extends Reader.Task { + + private Configuration readerSliceConfig; + private CommonRdbmsReader.Task commonRdbmsReaderSlave; + + @Override + public void init() { + this.readerSliceConfig = super.getPluginJobConf(); + this.commonRdbmsReaderSlave = new CommonRdbmsReader.Task(DATABASE_TYPE,super.getTaskGroupId(), super.getTaskId()); + this.commonRdbmsReaderSlave.init(this.readerSliceConfig); + } + + @Override + public void startRead(RecordSender recordSender) { + int fetchSize = this.readerSliceConfig.getInt(com.alibaba.datax.plugin.rdbms.reader.Constant.FETCH_SIZE); + + this.commonRdbmsReaderSlave.startRead(this.readerSliceConfig, recordSender, + super.getTaskPluginCollector(), fetchSize); + } + + @Override + public void post() { + this.commonRdbmsReaderSlave.post(this.readerSliceConfig); + } + + @Override + public void destroy() { + this.commonRdbmsReaderSlave.destroy(this.readerSliceConfig); + } + + } + +} diff --git a/gaussdbreader/src/main/resources/plugin.json b/gaussdbreader/src/main/resources/plugin.json new file mode 100755 index 0000000000..7d4ac8de7b --- /dev/null +++ b/gaussdbreader/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "gaussdbreader", + "class": "com.alibaba.datax.plugin.reader.gaussdbreader.GaussDbReader", + "description": "useScene: prod. mechanism: Jdbc connection using the database, execute select sql, retrieve data from the ResultSet. warn: The more you know about the database, the less problems you encounter.", + "developer": "alibaba" +} \ No newline at end of file diff --git a/gaussdbreader/src/main/resources/plugin_job_template.json b/gaussdbreader/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..e39220eb43 --- /dev/null +++ b/gaussdbreader/src/main/resources/plugin_job_template.json @@ -0,0 +1,13 @@ +{ + "name": "gaussdbreader", + "parameter": { + "username": "", + "password": "", + "connection": [ + { + "table": [], + "jdbcUrl": [] + } + ] + } +} \ No newline at end of file diff --git a/gaussdbwriter/doc/gaussdbwriter.md b/gaussdbwriter/doc/gaussdbwriter.md new file mode 100644 index 0000000000..e65b74d30d --- /dev/null +++ b/gaussdbwriter/doc/gaussdbwriter.md @@ -0,0 +1,267 @@ +# DataX GaussDbWriter + + +--- + + +## 1 快速介绍 + +GaussDbWriter插件实现了写入数据到 GaussDB主库目的表的功能。在底层实现上,GaussDbWriter通过JDBC连接远程 GaussDB 数据库,并执行相应的 insert into ... sql 语句将数据写入 GaussDB,内部会分批次提交入库。 + +GaussDbWriter面向ETL开发工程师,他们使用GaussDbWriter从数仓导入数据到GaussDB。同时 GaussDbWriter亦可以作为数据迁移工具为DBA等用户提供服务。 + + +## 2 实现原理 + +GaussDbWriter通过 DataX 框架获取 Reader 生成的协议数据,根据你配置生成相应的SQL插入语句 + + +* `insert into...`(当主键/唯一性索引冲突时会写不进去冲突的行) + +
+ + 注意: + 1. 目的表所在数据库必须是主库才能写入数据;整个任务至少需具备 insert into...的权限,是否需要其他权限,取决于你任务配置中在 preSql 和 postSql 中指定的语句。 + 2. GaussDbWriter和MysqlWriter不同,不支持配置writeMode参数。 + + +## 3 功能说明 + +### 3.1 配置样例 + +* 这里使用一份从内存产生到 GaussDbWriter导入的数据。 + +```json +{ + "job": { + "setting": { + "speed": { + "channel": 1 + } + }, + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "column" : [ + { + "value": "DataX", + "type": "string" + }, + { + "value": 19880808, + "type": "long" + }, + { + "value": "1988-08-08 08:08:08", + "type": "date" + }, + { + "value": true, + "type": "bool" + }, + { + "value": "test", + "type": "bytes" + } + ], + "sliceRecordCount": 1000 + } + }, + "writer": { + "name": "gaussdbwriter", + "parameter": { + "username": "xx", + "password": "xx", + "column": [ + "id", + "name" + ], + "preSql": [ + "delete from test" + ], + "connection": [ + { + "jdbcUrl": "jdbc:opengauss://127.0.0.1:3002/datax", + "table": [ + "test" + ] + } + ] + } + } + } + ] + } +} + +``` + + +### 3.2 参数说明 + +* **jdbcUrl** + + * 描述:目的数据库的 JDBC 连接信息 ,jdbcUrl必须包含在connection配置单元中。 + + 注意:1、在一个数据库上只能配置一个值。 + 2、jdbcUrl按照GaussDB官方规范,并可以填写连接附加参数信息。具体请参看GaussDB官方文档或者咨询对应 DBA。 + + +* 必选:是
+ +* 默认值:无
+ +* **username** + + * 描述:目的数据库的用户名
+ + * 必选:是
+ + * 默认值:无
+ +* **password** + + * 描述:目的数据库的密码
+ + * 必选:是
+ + * 默认值:无
+ +* **table** + + * 描述:目的表的表名称。支持写入一个或者多个表。当配置为多张表时,必须确保所有表结构保持一致。 + + 注意:table 和 jdbcUrl 必须包含在 connection 配置单元中 + + * 必选:是
+ + * 默认值:无
+ +* **column** + + * 描述:目的表需要写入数据的字段,字段之间用英文逗号分隔。例如: "column": ["id","name","age"]。如果要依次写入全部列,使用\*表示, 例如: "column": ["\*"] + + 注意:1、我们强烈不推荐你这样配置,因为当你目的表字段个数、类型等有改动时,你的任务可能运行不正确或者失败 + 2、此处 column 不能配置任何常量值 + + * 必选:是
+ + * 默认值:否
+ +* **preSql** + + * 描述:写入数据到目的表前,会先执行这里的标准语句。如果 Sql 中有你需要操作到的表名称,请使用 `@table` 表示,这样在实际执行 Sql 语句时,会对变量按照实际表名称进行替换。比如你的任务是要写入到目的端的100个同构分表(表名称为:datax_00,datax01, ... datax_98,datax_99),并且你希望导入数据前,先对表中数据进行删除操作,那么你可以这样配置:`"preSql":["delete from @table"]`,效果是:在执行到每个表写入数据前,会先执行对应的 delete from 对应表名称
+ + * 必选:否
+ + * 默认值:无
+ +* **postSql** + + * 描述:写入数据到目的表后,会执行这里的标准语句。(原理同 preSql )
+ + * 必选:否
+ + * 默认值:无
+ +* **batchSize** + + * 描述:一次性批量提交的记录数大小,该值可以极大减少DataX与GaussDB的网络交互次数,并提升整体吞吐量。但是该值设置过大可能会造成DataX运行进程OOM情况。
+ + * 必选:否
+ + * 默认值:1024
+ +### 3.3 类型转换 + +目前 GaussDbWriter支持大部分 GaussDB类型,但也存在部分没有支持的情况,请注意检查你的类型。 + +下面列出 GaussDbWriter针对 GaussDB类型转换列表: + +| DataX 内部类型| GaussDB 数据类型 | +| -------- | ----- | +| Long |bigint, bigserial, integer, smallint, serial | +| Double |double precision, money, numeric, real | +| String |varchar, char, text, bit| +| Date |date, time, timestamp | +| Boolean |bool| +| Bytes |bytea| + +## 4 性能报告 + +### 4.1 环境准备 + +#### 4.1.1 数据特征 +建表语句: + +create table pref_test( +id serial, +a_bigint bigint, +a_bit bit(10), +a_boolean boolean, +a_char character(5), +a_date date, +a_double double precision, +a_integer integer, +a_money money, +a_num numeric(10,2), +a_real real, +a_smallint smallint, +a_text text, +a_time time, +a_timestamp timestamp +) + +#### 4.1.2 机器参数 + +* 执行DataX的机器参数为: + 1. cpu: 16核 Intel(R) Xeon(R) CPU E5620 @ 2.40GHz + 2. mem: MemTotal: 24676836kB MemFree: 6365080kB + 3. net: 百兆双网卡 + +* GaussDB数据库机器参数为: + D12 24逻辑核 192G内存 12*480G SSD 阵列 + + +### 4.2 测试报告 + +#### 4.2.1 单表测试报告 + +| 通道数| 批量提交batchSize | DataX速度(Rec/s)| DataX流量(M/s) | DataX机器运行负载 +|--------|--------| --------|--------|--------|--------| +|1| 128 | 9259 | 0.55 | 0.3 +|1| 512 | 10869 | 0.653 | 0.3 +|1| 2048 | 9803 | 0.589 | 0.8 +|4| 128 | 30303 | 1.82 | 1 +|4| 512 | 36363 | 2.18 | 1 +|4| 2048 | 36363 | 2.18 | 1 +|8| 128 | 57142 | 3.43 | 2 +|8| 512 | 66666 | 4.01 | 1.5 +|8| 2048 | 66666 | 4.01 | 1.1 +|16| 128 | 88888 | 5.34 | 1.8 +|16| 2048 | 94117 | 5.65 | 2.5 +|32| 512 | 76190 | 4.58 | 3 + +#### 4.2.2 性能测试小结 +1. `channel数对性能影响很大` +2. `通常不建议写入数据库时,通道个数 > 32` + + +## FAQ + +*** + +**Q: GaussDbWriter 执行 postSql 语句报错,那么数据导入到目标数据库了吗?** + +A: DataX 导入过程存在三块逻辑,pre 操作、导入操作、post 操作,其中任意一环报错,DataX 作业报错。由于 DataX 不能保证在同一个事务完成上述几个操作,因此有可能数据已经落入到目标端。 + +*** + +**Q: 按照上述说法,那么有部分脏数据导入数据库,如果影响到线上数据库怎么办?** + +A: 目前有两种解法,第一种配置 pre 语句,该 sql 可以清理当天导入数据, DataX 每次导入时候可以把上次清理干净并导入完整数据。 +第二种,向临时表导入数据,完成后再 rename 到线上表。 + +*** diff --git a/gaussdbwriter/pom.xml b/gaussdbwriter/pom.xml new file mode 100644 index 0000000000..9da02effad --- /dev/null +++ b/gaussdbwriter/pom.xml @@ -0,0 +1,86 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + + gaussdbwriter + gaussdbwriter + jar + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + + org.slf4j + slf4j-api + + + + ch.qos.logback + logback-classic + + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + + org.opengauss + opengauss-jdbc + 3.0.0 + + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + + \ No newline at end of file diff --git a/gaussdbwriter/src/main/assembly/package.xml b/gaussdbwriter/src/main/assembly/package.xml new file mode 100755 index 0000000000..7167c89de8 --- /dev/null +++ b/gaussdbwriter/src/main/assembly/package.xml @@ -0,0 +1,35 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/writer/gaussdbwriter + + + target/ + + gaussdbwriter-0.0.1-SNAPSHOT.jar + + plugin/writer/gaussdbwriter + + + + + + false + plugin/writer/gaussdbwriter/libs + runtime + + + diff --git a/gaussdbwriter/src/main/java/com/alibaba/datax/plugin/writer/gaussdbwriter/GaussDbWriter.java b/gaussdbwriter/src/main/java/com/alibaba/datax/plugin/writer/gaussdbwriter/GaussDbWriter.java new file mode 100644 index 0000000000..751defca89 --- /dev/null +++ b/gaussdbwriter/src/main/java/com/alibaba/datax/plugin/writer/gaussdbwriter/GaussDbWriter.java @@ -0,0 +1,103 @@ +package com.alibaba.datax.plugin.writer.gaussdbwriter; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter; +import com.alibaba.datax.plugin.rdbms.writer.Key; + +import java.util.List; + +public class GaussDbWriter extends Writer { + + private static final DataBaseType DATABASE_TYPE = DataBaseType.GaussDB; + + public static class Job extends Writer.Job { + private Configuration originalConfig = null; + private CommonRdbmsWriter.Job commonRdbmsWriterMaster; + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + + // warn:not like mysql, GaussDB only support insert mode, don't use + String writeMode = this.originalConfig.getString(Key.WRITE_MODE); + if (null != writeMode) { + throw DataXException.asDataXException(DBUtilErrorCode.CONF_ERROR, + String.format("写入模式(writeMode)配置有误. 因为GaussDB不支持配置参数项 writeMode: %s, GaussDB仅使用insert sql 插入数据. 请检查您的配置并作出修改.", writeMode)); + } + + this.commonRdbmsWriterMaster = new CommonRdbmsWriter.Job(DATABASE_TYPE); + this.commonRdbmsWriterMaster.init(this.originalConfig); + } + + @Override + public void prepare() { + this.commonRdbmsWriterMaster.prepare(this.originalConfig); + } + + @Override + public List split(int mandatoryNumber) { + return this.commonRdbmsWriterMaster.split(this.originalConfig, mandatoryNumber); + } + + @Override + public void post() { + this.commonRdbmsWriterMaster.post(this.originalConfig); + } + + @Override + public void destroy() { + this.commonRdbmsWriterMaster.destroy(this.originalConfig); + } + + } + + public static class Task extends Writer.Task { + private Configuration writerSliceConfig; + private CommonRdbmsWriter.Task commonRdbmsWriterSlave; + + @Override + public void init() { + this.writerSliceConfig = super.getPluginJobConf(); + this.commonRdbmsWriterSlave = new CommonRdbmsWriter.Task(DATABASE_TYPE){ + @Override + public String calcValueHolder(String columnType){ + if("serial".equalsIgnoreCase(columnType)){ + return "?::int"; + }else if("bigserial".equalsIgnoreCase(columnType)){ + return "?::int8"; + }else if("bit".equalsIgnoreCase(columnType)){ + return "?::bit varying"; + } + return "?::" + columnType; + } + }; + this.commonRdbmsWriterSlave.init(this.writerSliceConfig); + } + + @Override + public void prepare() { + this.commonRdbmsWriterSlave.prepare(this.writerSliceConfig); + } + + public void startWrite(RecordReceiver recordReceiver) { + this.commonRdbmsWriterSlave.startWrite(recordReceiver, this.writerSliceConfig, super.getTaskPluginCollector()); + } + + @Override + public void post() { + this.commonRdbmsWriterSlave.post(this.writerSliceConfig); + } + + @Override + public void destroy() { + this.commonRdbmsWriterSlave.destroy(this.writerSliceConfig); + } + + } + +} diff --git a/gaussdbwriter/src/main/resources/plugin.json b/gaussdbwriter/src/main/resources/plugin.json new file mode 100755 index 0000000000..2f52a16793 --- /dev/null +++ b/gaussdbwriter/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "gaussdbwriter", + "class": "com.alibaba.datax.plugin.writer.gaussdbwriter.GaussDbWriter", + "description": "useScene: prod. mechanism: Jdbc connection using the database, execute insert sql. warn: The more you know about the database, the less problems you encounter.", + "developer": "alibaba" +} \ No newline at end of file diff --git a/gaussdbwriter/src/main/resources/plugin_job_template.json b/gaussdbwriter/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..539fa46f74 --- /dev/null +++ b/gaussdbwriter/src/main/resources/plugin_job_template.json @@ -0,0 +1,16 @@ +{ + "name": "gaussdbwriter", + "parameter": { + "username": "", + "password": "", + "column": [], + "connection": [ + { + "jdbcUrl": "", + "table": [] + } + ], + "preSql": [], + "postSql": [] + } +} \ No newline at end of file diff --git a/gdbwriter/src/main/java/com/alibaba/datax/plugin/writer/gdbwriter/mapping/DefaultGdbMapper.java b/gdbwriter/src/main/java/com/alibaba/datax/plugin/writer/gdbwriter/mapping/DefaultGdbMapper.java index 73a94cf5b9..2c0158795a 100644 --- a/gdbwriter/src/main/java/com/alibaba/datax/plugin/writer/gdbwriter/mapping/DefaultGdbMapper.java +++ b/gdbwriter/src/main/java/com/alibaba/datax/plugin/writer/gdbwriter/mapping/DefaultGdbMapper.java @@ -19,8 +19,8 @@ import com.alibaba.datax.plugin.writer.gdbwriter.model.GdbEdge; import com.alibaba.datax.plugin.writer.gdbwriter.model.GdbElement; import com.alibaba.datax.plugin.writer.gdbwriter.model.GdbVertex; -import com.alibaba.fastjson.JSONArray; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; import lombok.extern.slf4j.Slf4j; diff --git a/gdbwriter/src/main/java/com/alibaba/datax/plugin/writer/gdbwriter/util/ConfigHelper.java b/gdbwriter/src/main/java/com/alibaba/datax/plugin/writer/gdbwriter/util/ConfigHelper.java index 178b5e7c8a..644f889854 100644 --- a/gdbwriter/src/main/java/com/alibaba/datax/plugin/writer/gdbwriter/util/ConfigHelper.java +++ b/gdbwriter/src/main/java/com/alibaba/datax/plugin/writer/gdbwriter/util/ConfigHelper.java @@ -12,8 +12,8 @@ import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.util.Configuration; import com.alibaba.datax.plugin.writer.gdbwriter.GdbWriterErrorCode; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; /** * @author jerrywang diff --git a/hbase094xreader/src/main/java/com/alibaba/datax/plugin/reader/hbase094xreader/Hbase094xHelper.java b/hbase094xreader/src/main/java/com/alibaba/datax/plugin/reader/hbase094xreader/Hbase094xHelper.java index c3e2a21224..b9f16b1752 100644 --- a/hbase094xreader/src/main/java/com/alibaba/datax/plugin/reader/hbase094xreader/Hbase094xHelper.java +++ b/hbase094xreader/src/main/java/com/alibaba/datax/plugin/reader/hbase094xreader/Hbase094xHelper.java @@ -2,8 +2,8 @@ import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.TypeReference; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.hadoop.fs.Path; diff --git a/hbase094xwriter/src/main/java/com/alibaba/datax/plugin/writer/hbase094xwriter/Hbase094xHelper.java b/hbase094xwriter/src/main/java/com/alibaba/datax/plugin/writer/hbase094xwriter/Hbase094xHelper.java index f671d31d52..00b128f38d 100644 --- a/hbase094xwriter/src/main/java/com/alibaba/datax/plugin/writer/hbase094xwriter/Hbase094xHelper.java +++ b/hbase094xwriter/src/main/java/com/alibaba/datax/plugin/writer/hbase094xwriter/Hbase094xHelper.java @@ -2,8 +2,8 @@ import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.TypeReference; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.hadoop.fs.Path; diff --git a/hbase11xreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xreader/Hbase11xHelper.java b/hbase11xreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xreader/Hbase11xHelper.java index 643072a92e..82ad712235 100644 --- a/hbase11xreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xreader/Hbase11xHelper.java +++ b/hbase11xreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xreader/Hbase11xHelper.java @@ -2,8 +2,8 @@ import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.TypeReference; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.hadoop.hbase.HBaseConfiguration; diff --git a/hbase11xsqlreader/doc/hbase11xsqlreader.md b/hbase11xsqlreader/doc/hbase11xsqlreader.md index 03261a1f06..9f70077f4e 100644 --- a/hbase11xsqlreader/doc/hbase11xsqlreader.md +++ b/hbase11xsqlreader/doc/hbase11xsqlreader.md @@ -60,12 +60,16 @@ hbase11xsqlreader插件实现了从Phoenix(HBase SQL)读取数据。在底层实 //填写连接Phoenix的hbase集群zk地址 "hbaseConfig": { "hbase.zookeeper.quorum": "hb-proxy-xxx-002.hbase.rds.aliyuncs.com,hb-proxy-xxx-001.hbase.rds.aliyuncs.com,hb-proxy-xxx-003.hbase.rds.aliyuncs.com" - }, + }, + //填写要读取的phoenix的命名空间 + "schema": "TAG", //填写要读取的phoenix的表名 "table": "US_POPULATION", //填写要读取的列名,不填读取所有列 "column": [ - ] + ], + //查询条件 + "where": "id=" } }, "writer": { @@ -92,11 +96,18 @@ hbase11xsqlreader插件实现了从Phoenix(HBase SQL)读取数据。在底层实 * 必选:是
+ * 默认值:无
+* **schema** + + * 描述:编写Phoenix中的namespace,该值设置为'' + + * 必选:是
+ * 默认值:无
* **table** - * 描述:编写Phoenix中的表名,如果有namespace,该值设置为'namespace.tablename' + * 描述:编写Phoenix中的表名,该值设置为'tablename' * 必选:是
@@ -109,7 +120,13 @@ hbase11xsqlreader插件实现了从Phoenix(HBase SQL)读取数据。在底层实 * 必选:是
* 默认值:无
+* **where** + + * 描述:填写需要从phoenix表中读取条件判断。 + + * 可选:是
+ * 默认值:无
### 3.3 类型转换 @@ -172,11 +189,14 @@ hbase11xsqlreader插件实现了从Phoenix(HBase SQL)读取数据。在底层实 "hbaseConfig": { "hbase.zookeeper.quorum": "hb-proxy-xxx-002.hbase.rds.aliyuncs.com,hb-proxy-xxx-001.hbase.rds.aliyuncs.com,hb-proxy-xxx-003.hbase.rds.aliyuncs.com" }, + "schema": "TAG", //填写要读取的phoenix的表名 "table": "US_POPULATION", //填写要读取的列名,不填读取所有列 "column": [ - ] + ], + //查询条件 + "where": "id=" } }, "writer": { @@ -204,7 +224,13 @@ hbase11xsqlreader插件实现了从Phoenix(HBase SQL)读取数据。在底层实 * 必选:是
* 默认值:无
- +* **schema** + + * 描述:编写Phoenix中的namespace,该值设置为'' + + * 必选:是
+ + * 默认值:无
* **table** * 描述:编写Phoenix中的表名,如果有namespace,该值设置为'namespace.tablename' @@ -220,7 +246,13 @@ hbase11xsqlreader插件实现了从Phoenix(HBase SQL)读取数据。在底层实 * 必选:是
* 默认值:无
+ * **where** + * 描述:填写需要从phoenix表中读取条件判断。 + + * 可选:是
+ + * 默认值:无
### 3.3 类型转换 diff --git a/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/HbaseSQLHelper.java b/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/HbaseSQLHelper.java index 5309d1d99a..cf4304eeca 100644 --- a/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/HbaseSQLHelper.java +++ b/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/HbaseSQLHelper.java @@ -2,8 +2,8 @@ import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.TypeReference; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; import org.apache.hadoop.hbase.HConstants; import org.apache.hadoop.hbase.util.Pair; import org.apache.hadoop.mapreduce.InputSplit; @@ -26,9 +26,7 @@ import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; public class HbaseSQLHelper { @@ -50,11 +48,15 @@ public static org.apache.hadoop.conf.Configuration generatePhoenixConf(HbaseSQLR String zkUrl = readerConfig.getZkUrl(); PhoenixConfigurationUtil.setInputClass(conf, PhoenixRecordWritable.class); - PhoenixConfigurationUtil.setInputTableName(conf, table); + + PhoenixConfigurationUtil.setInputTableName(conf, readerConfig.getSchema()+"."+table); if (!columns.isEmpty()) { PhoenixConfigurationUtil.setSelectColumnNames(conf, columns.toArray(new String[columns.size()])); } + if(Objects.nonNull(readerConfig.getWhere())){ + PhoenixConfigurationUtil.setInputTableConditions(conf,readerConfig.getWhere()); + } PhoenixEmbeddedDriver.ConnectionInfo info = null; try { info = PhoenixEmbeddedDriver.ConnectionInfo.create(zkUrl); @@ -67,15 +69,19 @@ public static org.apache.hadoop.conf.Configuration generatePhoenixConf(HbaseSQLR conf.setInt(HConstants.ZOOKEEPER_CLIENT_PORT, info.getPort()); if (info.getRootNode() != null) conf.set(HConstants.ZOOKEEPER_ZNODE_PARENT, info.getRootNode()); + conf.set(Key.NAME_SPACE_MAPPING_ENABLED,"true"); + conf.set(Key.SYSTEM_TABLES_TO_NAMESPACE,"true"); return conf; } - public static List getPColumnNames(String connectionString, String tableName) throws SQLException { - Connection con = - DriverManager.getConnection(connectionString); + public static List getPColumnNames(String connectionString, String tableName,String schema) throws SQLException { + Properties pro = new Properties(); + pro.put(Key.NAME_SPACE_MAPPING_ENABLED, true); + pro.put(Key.SYSTEM_TABLES_TO_NAMESPACE, true); + Connection con = DriverManager.getConnection(connectionString,pro); PhoenixConnection phoenixConnection = con.unwrap(PhoenixConnection.class); MetaDataClient metaDataClient = new MetaDataClient(phoenixConnection); - PTable table = metaDataClient.updateCache("", tableName).getTable(); + PTable table = metaDataClient.updateCache(schema, tableName).getTable(); List columnNames = new ArrayList(); for (PColumn pColumn : table.getColumns()) { if (!pColumn.getName().getString().equals(SaltingUtil.SALTING_COLUMN_NAME)) diff --git a/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/HbaseSQLReaderConfig.java b/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/HbaseSQLReaderConfig.java index ab06f6e1a8..37060986f3 100644 --- a/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/HbaseSQLReaderConfig.java +++ b/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/HbaseSQLReaderConfig.java @@ -9,6 +9,7 @@ import java.sql.SQLException; import java.util.List; +import java.util.StringJoiner; public class HbaseSQLReaderConfig { private final static Logger LOG = LoggerFactory.getLogger(HbaseSQLReaderConfig.class); @@ -27,6 +28,9 @@ public String getZkUrl() { private String tableName; private List columns; // 目的表的所有列的列名,包括主键和非主键,不包括时间列 + private String where;//条件 + + private String schema;// /** * @return 获取原始的datax配置 */ @@ -96,22 +100,27 @@ private static void parseClusterConfig(HbaseSQLReaderConfig cfg, Configuration d } String zkQuorum = zkCfg.getFirst(); String znode = zkCfg.getSecond(); + if (zkQuorum == null || zkQuorum.isEmpty()) { throw DataXException.asDataXException( HbaseSQLReaderErrorCode.ILLEGAL_VALUE, "HBase的hbase.zookeeper.quorum配置不能为空" ); } // 生成sql使用的连接字符串, 格式: jdbc:hbase:zk_quorum:2181:/znode_parent - cfg.connectionString = "jdbc:phoenix:" + zkQuorum; - cfg.zkUrl = zkQuorum + ":2181"; + StringBuilder connectionString=new StringBuilder("jdbc:phoenix:"); + connectionString.append(zkQuorum); + cfg.connectionString = connectionString.toString(); + StringBuilder zkUrl =new StringBuilder(zkQuorum); + cfg.zkUrl = zkUrl.append(":2181").toString(); if (!znode.isEmpty()) { - cfg.connectionString += cfg.connectionString + ":" + znode; - cfg.zkUrl += cfg.zkUrl + ":" + znode; + cfg.connectionString = connectionString.append(":").append(znode).toString(); + cfg.zkUrl=zkUrl.append(":").append(znode).toString(); } } private static void parseTableConfig(HbaseSQLReaderConfig cfg, Configuration dataxCfg) { // 解析并检查表名 cfg.tableName = dataxCfg.getString(Key.TABLE); + cfg.schema = dataxCfg.getString(Key.SCHEMA); if (cfg.tableName == null || cfg.tableName.isEmpty()) { throw DataXException.asDataXException( HbaseSQLReaderErrorCode.ILLEGAL_VALUE, "HBase的tableName配置不能为空,请检查并修改配置." ); @@ -124,13 +133,14 @@ private static void parseTableConfig(HbaseSQLReaderConfig cfg, Configuration dat HbaseSQLReaderErrorCode.ILLEGAL_VALUE, "您配置的tableName含有非法字符{0},请检查您的配置."); } else if (cfg.columns.isEmpty()) { try { - cfg.columns = HbaseSQLHelper.getPColumnNames(cfg.connectionString, cfg.tableName); + cfg.columns = HbaseSQLHelper.getPColumnNames(cfg.connectionString, cfg.tableName,cfg.schema); dataxCfg.set(Key.COLUMN, cfg.columns); } catch (SQLException e) { throw DataXException.asDataXException( HbaseSQLReaderErrorCode.GET_PHOENIX_COLUMN_ERROR, "HBase的columns配置不能为空,请添加目标表的列名配置." + e.getMessage(), e); } } + cfg.where=dataxCfg.getString(Key.WHERE); } @Override @@ -151,6 +161,8 @@ public String toString() { ret.append(","); } ret.setLength(ret.length() - 1); + ret.append("[where=]").append(getWhere()); + ret.append("[schema=]").append(getSchema()); ret.append("\n"); return ret.toString(); @@ -161,4 +173,20 @@ public String toString() { */ private HbaseSQLReaderConfig() { } + + public String getWhere() { + return where; + } + + public void setWhere(String where) { + this.where = where; + } + + public String getSchema() { + return schema; + } + + public void setSchema(String schema) { + this.schema = schema; + } } diff --git a/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/HbaseSQLReaderTask.java b/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/HbaseSQLReaderTask.java index 1ca22c6fb9..461649d1e9 100644 --- a/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/HbaseSQLReaderTask.java +++ b/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/HbaseSQLReaderTask.java @@ -19,10 +19,8 @@ import java.io.IOException; import java.math.BigDecimal; import java.sql.*; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.sql.Date; +import java.util.*; /** * Created by admin on 1/3/18. @@ -42,11 +40,14 @@ public HbaseSQLReaderTask(Configuration config) { } private void getPColumns() throws SQLException { + Properties pro = new Properties(); + pro.put(Key.NAME_SPACE_MAPPING_ENABLED, true); + pro.put(Key.SYSTEM_TABLES_TO_NAMESPACE, true); Connection con = - DriverManager.getConnection(this.readerConfig.getConnectionString()); + DriverManager.getConnection(this.readerConfig.getConnectionString(),pro); PhoenixConnection phoenixConnection = con.unwrap(PhoenixConnection.class); MetaDataClient metaDataClient = new MetaDataClient(phoenixConnection); - PTable table = metaDataClient.updateCache("", this.readerConfig.getTableName()).getTable(); + PTable table = metaDataClient.updateCache(this.readerConfig.getSchema(), this.readerConfig.getTableName()).getTable(); List columnNames = this.readerConfig.getColumns(); for (PColumn pColumn : table.getColumns()) { if (columnNames.contains(pColumn.getName().getString())) { diff --git a/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/Key.java b/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/Key.java index 7987d6c8b2..f8453add8f 100644 --- a/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/Key.java +++ b/hbase11xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase11xsqlreader/Key.java @@ -24,5 +24,18 @@ public final class Key { * 【必选】列配置 */ public final static String COLUMN = "column"; + /** + * + */ + public static final String WHERE = "where"; + + /** + * 【可选】Phoenix表所属schema,默认为空 + */ + public static final String SCHEMA = "schema"; + + public static final String NAME_SPACE_MAPPING_ENABLED = "phoenix.schema.isNamespaceMappingEnabled"; + + public static final String SYSTEM_TABLES_TO_NAMESPACE = "phoenix.schema.mapSystemTablesToNamespace"; } diff --git a/hbase11xsqlreader/src/main/resources/plugin.json b/hbase11xsqlreader/src/main/resources/plugin.json index e245ca2742..162f571288 100644 --- a/hbase11xsqlreader/src/main/resources/plugin.json +++ b/hbase11xsqlreader/src/main/resources/plugin.json @@ -2,6 +2,6 @@ "name": "hbase11xsqlreader", "class": "com.alibaba.datax.plugin.reader.hbase11xsqlreader.HbaseSQLReader", "description": "useScene: prod. mechanism: Scan to read data.", - "developer": "liwei.li, bug reported to : liwei.li@alibaba-inc.com" + "developer": "alibaba" } diff --git a/hbase11xsqlwriter/src/main/java/com/alibaba/datax/plugin/writer/hbase11xsqlwriter/HbaseSQLHelper.java b/hbase11xsqlwriter/src/main/java/com/alibaba/datax/plugin/writer/hbase11xsqlwriter/HbaseSQLHelper.java index 41e57d4efe..d1b23fdf63 100644 --- a/hbase11xsqlwriter/src/main/java/com/alibaba/datax/plugin/writer/hbase11xsqlwriter/HbaseSQLHelper.java +++ b/hbase11xsqlwriter/src/main/java/com/alibaba/datax/plugin/writer/hbase11xsqlwriter/HbaseSQLHelper.java @@ -2,8 +2,8 @@ import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.TypeReference; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; import org.apache.hadoop.hbase.TableName; import org.apache.hadoop.hbase.client.Admin; import org.apache.hadoop.hbase.util.Pair; diff --git a/hbase11xwriter/doc/hbase11xwriter.md b/hbase11xwriter/doc/hbase11xwriter.md index ff20abe998..969f2e4748 100644 --- a/hbase11xwriter/doc/hbase11xwriter.md +++ b/hbase11xwriter/doc/hbase11xwriter.md @@ -203,19 +203,20 @@ HbaseWriter 插件实现了从向Hbase中写取数据。在底层实现上,Hba * 描述:要写入的hbase字段。index:指定该列对应reader端column的索引,从0开始;name:指定hbase表中的列,必须为 列族:列名 的格式;type:指定写入数据类型,用于转换HBase byte[]。配置格式如下: ``` -"column": [ - { - "index":1, - "name": "cf1:q1", - "type": "string" - }, - { - "index":2, - "name": "cf1:q2", - "type": "string" - } - ] - + + "column": [ + { + "index":1, + "name": "cf1:q1", + "type": "string" + }, + { + "index":2, + "name": "cf1:q2", + "type": "string" + } + ] + ``` * 必选:是
@@ -227,17 +228,17 @@ HbaseWriter 插件实现了从向Hbase中写取数据。在底层实现上,Hba * 描述:要写入的hbase的rowkey列。index:指定该列对应reader端column的索引,从0开始,若为常量index为-1;type:指定写入数据类型,用于转换HBase byte[];value:配置常量,常作为多个字段的拼接符。hbasewriter会将rowkeyColumn中所有列按照配置顺序进行拼接作为写入hbase的rowkey,不能全为常量。配置格式如下: ``` -"rowkeyColumn": [ - { - "index":0, - "type":"string" - }, - { - "index":-1, - "type":"string", - "value":"_" - } - ] + "rowkeyColumn": [ + { + "index":0, + "type":"string" + }, + { + "index":-1, + "type":"string", + "value":"_" + } + ] ``` @@ -250,19 +251,19 @@ HbaseWriter 插件实现了从向Hbase中写取数据。在底层实现上,Hba * 描述:指定写入hbase的时间戳。支持:当前时间、指定时间列,指定时间,三者选一。若不配置表示用当前时间。index:指定对应reader端column的索引,从0开始,需保证能转换为long,若是Date类型,会尝试用yyyy-MM-dd HH:mm:ss和yyyy-MM-dd HH:mm:ss SSS去解析;若为指定时间index为-1;value:指定时间的值,long值。配置格式如下: ``` -"versionColumn":{ - "index":1 -} + "versionColumn":{ + "index":1 + } ``` 或者 ``` -"versionColumn":{ - "index":-1, - "value":123456789 -} + "versionColumn":{ + "index":-1, + "value":123456789 + } ``` diff --git a/hbase11xwriter/src/main/java/com/alibaba/datax/plugin/writer/hbase11xwriter/Hbase11xHelper.java b/hbase11xwriter/src/main/java/com/alibaba/datax/plugin/writer/hbase11xwriter/Hbase11xHelper.java index 94b13b60c9..2889b647fb 100644 --- a/hbase11xwriter/src/main/java/com/alibaba/datax/plugin/writer/hbase11xwriter/Hbase11xHelper.java +++ b/hbase11xwriter/src/main/java/com/alibaba/datax/plugin/writer/hbase11xwriter/Hbase11xHelper.java @@ -2,8 +2,8 @@ import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.TypeReference; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.hadoop.hbase.HBaseConfiguration; diff --git a/hbase20xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase20xsqlreader/HBase20SQLReaderHelper.java b/hbase20xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase20xsqlreader/HBase20SQLReaderHelper.java index 0edc993f81..11bbf734a4 100644 --- a/hbase20xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase20xsqlreader/HBase20SQLReaderHelper.java +++ b/hbase20xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase20xsqlreader/HBase20SQLReaderHelper.java @@ -175,7 +175,7 @@ public List doSplit(int adviceNumber) { if (querySql == null || querySql.isEmpty()) { // 如果splitPoints为空,则根据splitKey自动切分,不过这种切分方式无法保证数据均分,且只支持整形和字符型列 if (splitPoints == null || splitPoints.isEmpty()) { - LOG.info("Split accoring min and max value of splitColumn..."); + LOG.info("Split according min and max value of splitColumn..."); Pair minMaxPK = getPkRange(configuration); if (null == minMaxPK) { throw DataXException.asDataXException(HBase20xSQLReaderErrorCode.ILLEGAL_SPLIT_PK, @@ -208,7 +208,7 @@ public List doSplit(int adviceNumber) { } } else { - LOG.info("Split accoring splitPoints..."); + LOG.info("Split according splitPoints..."); // 根据指定splitPoints进行切分 rangeList = buildSplitRange(); } diff --git a/hbase20xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase20xsqlreader/HBase20xSQLReaderTask.java b/hbase20xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase20xsqlreader/HBase20xSQLReaderTask.java index 866cef38bc..3ebc704cd5 100644 --- a/hbase20xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase20xsqlreader/HBase20xSQLReaderTask.java +++ b/hbase20xsqlreader/src/main/java/com/alibaba/datax/plugin/reader/hbase20xsqlreader/HBase20xSQLReaderTask.java @@ -86,16 +86,18 @@ private Column convertPhoenixValueToDataxColumn(int sqlType, Object value) { column = new LongColumn((Integer) value); break; case Types.TINYINT: - column = new LongColumn(((Byte) value).longValue()); + Byte aByte = (Byte) value; + column = new LongColumn(null == aByte ? null : aByte.longValue()); break; case Types.SMALLINT: - column = new LongColumn(((Short) value).longValue()); + Short aShort = (Short) value; + column = new LongColumn(null == aShort ? null : aShort.longValue()); break; case Types.BIGINT: column = new LongColumn((Long) value); break; case Types.FLOAT: - column = new DoubleColumn((Float.valueOf(value.toString()))); + column = new DoubleColumn(null == value ? null : (Float.valueOf(value.toString()))); break; case Types.DECIMAL: column = new DoubleColumn((BigDecimal)value); diff --git a/hbase20xsqlreader/src/main/resources/plugin.json b/hbase20xsqlreader/src/main/resources/plugin.json index 45856411e0..4a7b4edfd2 100644 --- a/hbase20xsqlreader/src/main/resources/plugin.json +++ b/hbase20xsqlreader/src/main/resources/plugin.json @@ -2,6 +2,6 @@ "name": "hbase20xsqlreader", "class": "com.alibaba.datax.plugin.reader.hbase20xsqlreader.HBase20xSQLReader", "description": "useScene: prod. mechanism: read data from phoenix through queryserver.", - "developer": "bake" + "developer": "alibaba" } diff --git a/hbase20xsqlwriter/src/main/resources/plugin.json b/hbase20xsqlwriter/src/main/resources/plugin.json index 91b7069f4a..93d3002a54 100755 --- a/hbase20xsqlwriter/src/main/resources/plugin.json +++ b/hbase20xsqlwriter/src/main/resources/plugin.json @@ -2,6 +2,6 @@ "name": "hbase20xsqlwriter", "class": "com.alibaba.datax.plugin.writer.hbase20xsqlwriter.HBase20xSQLWriter", "description": "useScene: prod. mechanism: use hbase sql UPSERT to put data, index tables will be updated too.", - "developer": "bake" + "developer": "alibaba" } diff --git a/hdfsreader/doc/hdfsreader.md b/hdfsreader/doc/hdfsreader.md index cd83c530e7..ca9a021fd6 100644 --- a/hdfsreader/doc/hdfsreader.md +++ b/hdfsreader/doc/hdfsreader.md @@ -166,20 +166,20 @@ HdfsReader实现了从Hadoop分布式文件系统Hdfs中读取文件数据并转 默认情况下,用户可以全部按照String类型读取数据,配置如下: ```json - "column": ["*"] + "column": ["*"] ``` 用户可以指定Column字段信息,配置如下: ```json -{ - "type": "long", - "index": 0 //从本地文件文本第一列获取int字段 -}, -{ - "type": "string", - "value": "alibaba" //HdfsReader内部生成alibaba的字符串字段作为当前字段 -} + { + "type": "long", + "index": 0 //从本地文件文本第一列获取int字段 + }, + { + "type": "string", + "value": "alibaba" //HdfsReader内部生成alibaba的字符串字段作为当前字段 + } ``` 对于用户指定Column信息,type必须填写,index/value必须选择其一。 diff --git a/hdfsreader/pom.xml b/hdfsreader/pom.xml index 5d07dc256b..f90e71eeb1 100644 --- a/hdfsreader/pom.xml +++ b/hdfsreader/pom.xml @@ -1,5 +1,6 @@ - + datax-all com.alibaba.datax @@ -12,10 +13,21 @@ 0.0.1-SNAPSHOT jar - 1.1.1 + 3.1.3 2.7.1 + + org.apache.logging.log4j + log4j-api + 2.17.1 + + + + org.apache.logging.log4j + log4j-core + 2.17.1 + com.alibaba.datax datax-common @@ -51,6 +63,11 @@ hadoop-yarn-common ${hadoop.version} + + com.aliyun.oss + hadoop-aliyun + 2.7.2 + org.apache.hadoop hadoop-mapreduce-client-core @@ -95,6 +112,42 @@ ${datax-project-version} + + org.apache.parquet + parquet-column + 1.12.0 + + + org.apache.parquet + parquet-avro + 1.12.0 + + + org.apache.parquet + parquet-common + 1.12.0 + + + org.apache.parquet + parquet-format + 2.10.0 + + + org.apache.parquet + parquet-jackson + 1.12.0 + + + org.apache.parquet + parquet-encoding + 1.12.0 + + + org.apache.parquet + parquet-hadoop + 1.12.0 + + diff --git a/hdfsreader/src/main/assembly/package.xml b/hdfsreader/src/main/assembly/package.xml index 3f1393b764..a5f28e5c60 100644 --- a/hdfsreader/src/main/assembly/package.xml +++ b/hdfsreader/src/main/assembly/package.xml @@ -37,6 +37,28 @@ + + + + + + + + + + src/main/libs + + *.* + + plugin/reader/ossreader/libs + + + src/main/libs + + *.* + + plugin/reader/hivereader/libs + diff --git a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/Constant.java b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/Constant.java index 6bfb9bf7e5..061c55a0ef 100644 --- a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/Constant.java +++ b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/Constant.java @@ -10,4 +10,5 @@ public class Constant { public static final String CSV = "CSV"; public static final String SEQ = "SEQ"; public static final String RC = "RC"; + public static final String PARQUET = "PARQUET"; } diff --git a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/DFSUtil.java b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/DFSUtil.java index c39d38473f..720f8bf6bf 100644 --- a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/DFSUtil.java +++ b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/DFSUtil.java @@ -8,13 +8,17 @@ import com.alibaba.datax.plugin.unstructuredstorage.reader.ColumnEntry; import com.alibaba.datax.plugin.unstructuredstorage.reader.UnstructuredStorageReaderErrorCode; import com.alibaba.datax.plugin.unstructuredstorage.reader.UnstructuredStorageReaderUtil; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FileStatus; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; +import org.apache.hadoop.hive.common.type.HiveDecimal; import org.apache.hadoop.hive.ql.io.RCFile; import org.apache.hadoop.hive.ql.io.RCFileRecordReader; import org.apache.hadoop.hive.ql.io.orc.OrcFile; @@ -29,14 +33,30 @@ import org.apache.hadoop.mapred.*; import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.util.ReflectionUtils; +import org.apache.parquet.example.data.Group; +import org.apache.parquet.hadoop.ParquetReader; +import org.apache.parquet.hadoop.example.GroupReadSupport; +import org.apache.parquet.hadoop.util.HadoopInputFile; +import org.apache.parquet.io.api.Binary; +import org.apache.parquet.schema.MessageType; +import org.apache.parquet.schema.MessageTypeParser; +import org.apache.parquet.schema.PrimitiveType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.sql.Timestamp; import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** * Created by mingya.wmy on 2015/8/12. @@ -56,6 +76,10 @@ public class DFSUtil { public static final String HDFS_DEFAULTFS_KEY = "fs.defaultFS"; public static final String HADOOP_SECURITY_AUTHENTICATION_KEY = "hadoop.security.authentication"; + private Boolean skipEmptyOrcFile = false; + + private Integer orcFileEmptySize = null; + public DFSUtil(Configuration taskConfig) { hadoopConf = new org.apache.hadoop.conf.Configuration(); @@ -79,6 +103,7 @@ public DFSUtil(Configuration taskConfig) { this.hadoopConf.set(HADOOP_SECURITY_AUTHENTICATION_KEY, "kerberos"); } this.kerberosAuthentication(this.kerberosPrincipal, this.kerberosKeytabFilePath); + this.skipEmptyOrcFile = taskConfig.getBool(Key.SKIP_EMPTY_ORCFILE, false); LOG.info(String.format("hadoopConfig details:%s", JSON.toJSONString(this.hadoopConf))); } @@ -102,10 +127,11 @@ private void kerberosAuthentication(String kerberosPrincipal, String kerberosKey * @param srcPaths 路径列表 * @param specifiedFileType 指定文件类型 */ - public HashSet getAllFiles(List srcPaths, String specifiedFileType) { + public HashSet getAllFiles(List srcPaths, String specifiedFileType, Boolean skipEmptyOrcFile, Integer orcFileEmptySize) { this.specifiedFileType = specifiedFileType; - + this.skipEmptyOrcFile = skipEmptyOrcFile; + this.orcFileEmptySize = orcFileEmptySize; if (!srcPaths.isEmpty()) { for (String eachPath : srcPaths) { LOG.info(String.format("get HDFS all files in path = [%s]", eachPath)); @@ -127,9 +153,13 @@ public HashSet getHDFSAllFiles(String hdfsPath) { FileStatus stats[] = hdfs.globStatus(path); for (FileStatus f : stats) { if (f.isFile()) { - if (f.getLen() == 0) { + long fileLength = f.getLen(); + if (fileLength == 0) { String message = String.format("文件[%s]长度为0,将会跳过不作处理!", hdfsPath); LOG.warn(message); + } else if (BooleanUtils.isTrue(this.skipEmptyOrcFile) && this.orcFileEmptySize != null && fileLength <= this.orcFileEmptySize) { + String message = String.format("The orc file [%s] is empty, file size: %s, DataX will skip it !", f.getPath().toString(), fileLength); + LOG.warn(message); } else { addSourceFileByType(f.getPath().toString()); } @@ -167,7 +197,16 @@ private HashSet getHDFSAllFilesNORegex(String path, FileSystem hdfs) thr LOG.info(String.format("[%s] 是目录, 递归获取该目录下的文件", f.getPath().toString())); getHDFSAllFilesNORegex(f.getPath().toString(), hdfs); } else if (f.isFile()) { - + long fileLength = f.getLen(); + if (fileLength == 0) { + String message = String.format("The file [%s] is empty, DataX will skip it !", f.getPath().toString()); + LOG.warn(message); + continue; + } else if (BooleanUtils.isTrue(this.skipEmptyOrcFile) && this.orcFileEmptySize != null && fileLength <= this.orcFileEmptySize) { + String message = String.format("The orc file [%s] is empty, file size: %s, DataX will skip it !", f.getPath().toString(), fileLength); + LOG.warn(message); + continue; + } addSourceFileByType(f.getPath().toString()); } else { String message = String.format("该路径[%s]文件类型既不是目录也不是文件,插件自动忽略。", @@ -331,26 +370,45 @@ public void orcFileStartRead(String sourceOrcFilePath, Configuration readerSlice //If the network disconnected, will retry 45 times, each time the retry interval for 20 seconds //Each file as a split //TODO multy threads - InputSplit[] splits = in.getSplits(conf, 1); - - RecordReader reader = in.getRecordReader(splits[0], conf, Reporter.NULL); - Object key = reader.createKey(); - Object value = reader.createValue(); - // 获取列信息 - List fields = inspector.getAllStructFieldRefs(); - - List recordFields; - while (reader.next(key, value)) { - recordFields = new ArrayList(); - - for (int i = 0; i <= columnIndexMax; i++) { - Object field = inspector.getStructFieldData(value, fields.get(i)); - recordFields.add(field); + // OrcInputFormat getSplits params numSplits not used, splits size = block numbers + InputSplit[] splits; + try { + splits = in.getSplits(conf, 1); + } catch (Exception splitException) { + if (Boolean.TRUE.equals(this.skipEmptyOrcFile)) { + boolean isOrcFileEmptyException = checkIsOrcEmptyFileExecption(splitException); + if (isOrcFileEmptyException) { + LOG.info("skipEmptyOrcFile: true, \"{}\" is an empty orc file, skip it!", sourceOrcFilePath); + return; + } + } + throw splitException; + } + for (InputSplit split : splits) { + { + RecordReader reader = in.getRecordReader(split, conf, Reporter.NULL); + Object key = reader.createKey(); + Object value = reader.createValue(); + // 获取列信息 + List fields = inspector.getAllStructFieldRefs(); + + List recordFields; + while (reader.next(key, value)) { + recordFields = new ArrayList(); + + for (int i = 0; i <= columnIndexMax; i++) { + Object field = inspector.getStructFieldData(value, fields.get(i)); + recordFields.add(field); + } + List hivePartitionColumnEntrys = UnstructuredStorageReaderUtil.getListColumnEntry(readerSliceConfig, com.alibaba.datax.plugin.unstructuredstorage.reader.Key.HIVE_PARTION_COLUMN); + ArrayList hivePartitionColumns = new ArrayList<>(); + hivePartitionColumns = UnstructuredStorageReaderUtil.getHivePartitionColumns(sourceOrcFilePath, hivePartitionColumnEntrys); + transportOneRecord(column, recordFields, recordSender, + taskPluginCollector, isReadAllColumns, nullFormat,hivePartitionColumns); + } + reader.close(); } - transportOneRecord(column, recordFields, recordSender, - taskPluginCollector, isReadAllColumns, nullFormat); } - reader.close(); } catch (Exception e) { String message = String.format("从orcfile文件路径[%s]中读取数据发生异常,请联系系统管理员。" , sourceOrcFilePath); @@ -363,8 +421,20 @@ public void orcFileStartRead(String sourceOrcFilePath, Configuration readerSlice } } + private boolean checkIsOrcEmptyFileExecption(Exception e) { + if (e == null) { + return false; + } + + String fullStackTrace = ExceptionUtils.getStackTrace(e); + if (fullStackTrace.contains("org.apache.orc.impl.ReaderImpl.getRawDataSizeOfColumn") && fullStackTrace.contains("Caused by: java.lang.IndexOutOfBoundsException: Index: 1, Size: 1")) { + return true; + } + return false; + } + private Record transportOneRecord(List columnConfigs, List recordFields - , RecordSender recordSender, TaskPluginCollector taskPluginCollector, boolean isReadAllColumns, String nullFormat) { + , RecordSender recordSender, TaskPluginCollector taskPluginCollector, boolean isReadAllColumns, String nullFormat, ArrayList hiveParitionColumns) { Record record = recordSender.createRecord(); Column columnGenerated; try { @@ -551,8 +621,9 @@ public boolean checkHdfsFileType(String filepath, String specifiedFileType) { } else if (StringUtils.equalsIgnoreCase(specifiedFileType, Constant.SEQ)) { return isSequenceFile(filepath, in); + } else if (StringUtils.equalsIgnoreCase(specifiedFileType, Constant.PARQUET)) { + return true; } - } catch (Exception e) { String message = String.format("检查文件[%s]类型失败,目前支持ORC,SEQUENCE,RCFile,TEXT,CSV五种格式的文件," + "请检查您文件类型和文件是否正确。", filepath); @@ -689,4 +760,332 @@ private boolean isSequenceFile(String filepath, FSDataInputStream in) { return false; } + public void parquetFileStartRead(String sourceParquetFilePath, Configuration readerSliceConfig, RecordSender recordSender, TaskPluginCollector taskPluginCollector) { + String schemaString = readerSliceConfig.getString(Key.PARQUET_SCHEMA); + if (StringUtils.isNotBlank(schemaString)) { + LOG.info("You config parquet schema, use it {}", schemaString); + } else { + schemaString = getParquetSchema(sourceParquetFilePath, hadoopConf); + LOG.info("Parquet schema parsed from: {} , schema is {}", sourceParquetFilePath, schemaString); + if (StringUtils.isBlank(schemaString)) { + throw DataXException.asDataXException("ParquetSchema is required, please check your config"); + } + } + MessageType parquetSchema = null; + List parquetTypes = null; + Map parquetMetaMap = null; + int fieldCount = 0; + try { + parquetSchema = MessageTypeParser.parseMessageType(schemaString); + fieldCount = parquetSchema.getFieldCount(); + parquetTypes = parquetSchema.getFields(); + parquetMetaMap = ParquetMessageHelper.parseParquetTypes(parquetTypes); + } catch (Exception e) { + String message = String.format("Error parsing to MessageType via Schema string [%s]", schemaString); + LOG.error(message); + throw DataXException.asDataXException(HdfsReaderErrorCode.PARSE_MESSAGE_TYPE_FROM_SCHEMA_ERROR, e); + } + List column = UnstructuredStorageReaderUtil.getListColumnEntry(readerSliceConfig, com.alibaba.datax.plugin.unstructuredstorage.reader.Key.COLUMN); + String nullFormat = readerSliceConfig.getString(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.NULL_FORMAT); + boolean isUtcTimestamp = readerSliceConfig.getBool(Key.PARQUET_UTC_TIMESTAMP, false); + boolean isReadAllColumns = (column == null || column.size() == 0) ? true : false; + LOG.info("ReadingAllColums: " + isReadAllColumns); + + /** + * 支持 hive 表中间加列场景 + * + * 开关默认 false,在 hive表存在中间加列的场景打开,需要根据 name排序 + * 不默认打开的原因 + * 1、存量hdfs任务,只根据 index获取字段,无name字段配置 + * 2、中间加列场景比较少 + * 3、存量任务可能存在列错位的问题,不能随意纠正 + */ + boolean supportAddMiddleColumn = readerSliceConfig.getBool(Key.SUPPORT_ADD_MIDDLE_COLUMN, false); + + boolean printNullValueException = readerSliceConfig.getBool("printNullValueException", false); + List ignoreIndex = readerSliceConfig.getList("ignoreIndex", new ArrayList(), Integer.class); + + JobConf conf = new JobConf(hadoopConf); + ParquetReader reader = null; + try { + Path parquetFilePath = new Path(sourceParquetFilePath); + GroupReadSupport readSupport = new GroupReadSupport(); + readSupport.init(conf, null, parquetSchema); + // 这里初始化parquetReader的时候,会getFileSystem,如果是HA集群,期间会根据hadoopConfig中区加载failover类,这里初始化builder带上conf + ParquetReader.Builder parquetReaderBuilder = ParquetReader.builder(readSupport, parquetFilePath); + parquetReaderBuilder.withConf(hadoopConf); + reader = parquetReaderBuilder.build(); + Group g = null; + + // 从文件名中解析分区信息 + List hivePartitionColumnEntrys = UnstructuredStorageReaderUtil.getListColumnEntry(readerSliceConfig, com.alibaba.datax.plugin.unstructuredstorage.reader.Key.HIVE_PARTION_COLUMN); + ArrayList hivePartitionColumns = new ArrayList<>(); + hivePartitionColumns = UnstructuredStorageReaderUtil.getHivePartitionColumns(sourceParquetFilePath, hivePartitionColumnEntrys); + List schemaFieldList = null; + Map colNameIndexMap = null; + Map indexMap = null; + if (supportAddMiddleColumn) { + boolean nonName = column.stream().anyMatch(columnEntry -> StringUtils.isEmpty(columnEntry.getName())); + if (nonName) { + throw new DataXException("You configured column item without name, please correct it"); + } + List parquetFileFields = getParquetFileFields(parquetFilePath, hadoopConf); + schemaFieldList = parquetFileFields.stream().map(org.apache.parquet.schema.Type::getName).collect(Collectors.toList()); + colNameIndexMap = new ConcurrentHashMap<>(); + Map finalColNameIndexMap = colNameIndexMap; + column.forEach(columnEntry -> finalColNameIndexMap.put(columnEntry.getIndex(), columnEntry.getName())); + Iterator> iterator = finalColNameIndexMap.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry next = iterator.next(); + if (!schemaFieldList.contains(next.getValue())) { + finalColNameIndexMap.remove((next.getKey())); + } + } + LOG.info("SupportAddMiddleColumn is true, fields from parquet file is {}, " + + "colNameIndexMap is {}", JSON.toJSONString(schemaFieldList), JSON.toJSONString(colNameIndexMap)); + fieldCount = column.size(); + indexMap = new HashMap<>(); + for (int j = 0; j < fieldCount; j++) { + if (colNameIndexMap.containsKey(j)) { + int index = findIndex(schemaFieldList, findEleInMap(colNameIndexMap, j)); + indexMap.put(j, index); + } + } + } + while ((g = reader.read()) != null) { + List formattedRecord = new ArrayList(fieldCount); + try { + for (int j = 0; j < fieldCount; j++) { + Object data = null; + try { + if (null != ignoreIndex && !ignoreIndex.isEmpty() && ignoreIndex.contains(j)) { + data = null; + } else { + if (supportAddMiddleColumn) { + if (!colNameIndexMap.containsKey(j)) { + formattedRecord.add(null); + continue; + } else { + data = DFSUtil.this.readFields(g, parquetTypes.get(indexMap.get(j)), indexMap.get(j), parquetMetaMap, isUtcTimestamp); + } + } else { + data = DFSUtil.this.readFields(g, parquetTypes.get(j), j, parquetMetaMap, isUtcTimestamp); + } + } + } catch (RuntimeException e) { + if (printNullValueException) { + LOG.warn(e.getMessage()); + } + } + formattedRecord.add(data); + } + transportOneRecord(column, formattedRecord, recordSender, taskPluginCollector, isReadAllColumns, nullFormat, hivePartitionColumns); + } catch (Exception e) { + throw DataXException.asDataXException(HdfsReaderErrorCode.READ_PARQUET_ERROR, e); + } + } + } catch (Exception e) { + throw DataXException.asDataXException(HdfsReaderErrorCode.READ_PARQUET_ERROR, e); + } finally { + org.apache.commons.io.IOUtils.closeQuietly(reader); + } + } + + private String findEleInMap(Map map, Integer key) { + Iterator> iterator = map.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry next = iterator.next(); + if (key.equals(next.getKey())) { + return next.getValue(); + } + } + return null; + } + + private int findIndex(List schemaFieldList, String colName) { + for (int i = 0; i < schemaFieldList.size(); i++) { + if (schemaFieldList.get(i).equals(colName)) { + return i; + } + } + return -1; + } + + private List getParquetFileFields(Path filePath, org.apache.hadoop.conf.Configuration configuration) { + try (org.apache.parquet.hadoop.ParquetFileReader reader = org.apache.parquet.hadoop.ParquetFileReader.open(HadoopInputFile.fromPath(filePath, configuration))) { + org.apache.parquet.schema.MessageType schema = reader.getFooter().getFileMetaData().getSchema(); + List fields = schema.getFields(); + return fields; + } catch (IOException e) { + LOG.error("Fetch parquet field error", e); + throw new DataXException(String.format("Fetch parquet field error, msg is %s", e.getMessage())); + } + } + + private String getParquetSchema(String sourceParquetFilePath, org.apache.hadoop.conf.Configuration hadoopConf) { + GroupReadSupport readSupport = new GroupReadSupport(); + ParquetReader.Builder parquetReaderBuilder = ParquetReader.builder(readSupport, new Path(sourceParquetFilePath)); + ParquetReader reader = null; + try { + parquetReaderBuilder.withConf(hadoopConf); + reader = parquetReaderBuilder.build(); + Group g = null; + if ((g = reader.read()) != null) { + return g.getType().toString(); + } + } catch (Throwable e) { + LOG.error("Inner error, getParquetSchema failed, message is {}", e.getMessage()); + } finally { + org.apache.commons.io.IOUtils.closeQuietly(reader); + } + return null; + } + + /** + * parquet 相关 + */ + private static final int JULIAN_EPOCH_OFFSET_DAYS = 2440588; + private static final long MILLIS_IN_DAY = TimeUnit.DAYS.toMillis(1); + private static final long NANOS_PER_MILLISECOND = TimeUnit.MILLISECONDS.toNanos(1); + + private long julianDayToMillis(int julianDay) { + return (julianDay - JULIAN_EPOCH_OFFSET_DAYS) * MILLIS_IN_DAY; + } + + private org.apache.parquet.schema.OriginalType getOriginalType(org.apache.parquet.schema.Type type, Map parquetMetaMap) { + ParquetMeta meta = parquetMetaMap.get(type.getName()); + return meta.getOriginalType(); + } + + private org.apache.parquet.schema.PrimitiveType asPrimitiveType(org.apache.parquet.schema.Type type, Map parquetMetaMap) { + ParquetMeta meta = parquetMetaMap.get(type.getName()); + return meta.getPrimitiveType(); + } + + private Object readFields(Group g, org.apache.parquet.schema.Type type, int index, Map parquetMetaMap, boolean isUtcTimestamp) { + if (this.getOriginalType(type, parquetMetaMap) == org.apache.parquet.schema.OriginalType.MAP) { + Group groupData = g.getGroup(index, 0); + List parquetTypes = groupData.getType().getFields(); + JSONObject data = new JSONObject(); + for (int i = 0; i < parquetTypes.size(); i++) { + int j = groupData.getFieldRepetitionCount(i); + // map key value 的对数 + for (int k = 0; k < j; k++) { + Group groupDataK = groupData.getGroup(0, k); + List parquetTypesK = groupDataK.getType().getFields(); + if (2 != parquetTypesK.size()) { + // warn: 不是key value成对出现 + throw new RuntimeException(String.format("bad parquet map type: %s", groupData.getValueToString(index, 0))); + } + Object subDataKey = this.readFields(groupDataK, parquetTypesK.get(0), 0, parquetMetaMap, isUtcTimestamp); + Object subDataValue = this.readFields(groupDataK, parquetTypesK.get(1), 1, parquetMetaMap, isUtcTimestamp); + if (StringUtils.equalsIgnoreCase("key", parquetTypesK.get(0).getName())) { + ((JSONObject) data).put(subDataKey.toString(), subDataValue); + } else { + ((JSONObject) data).put(subDataValue.toString(), subDataKey); + } + } + } + return data; + } else if (this.getOriginalType(type, parquetMetaMap) == org.apache.parquet.schema.OriginalType.MAP_KEY_VALUE) { + Group groupData = g.getGroup(index, 0); + List parquetTypes = groupData.getType().getFields(); + JSONObject data = new JSONObject(); + for (int i = 0; i < parquetTypes.size(); i++) { + int j = groupData.getFieldRepetitionCount(i); + // map key value 的对数 + for (int k = 0; k < j; k++) { + Group groupDataK = groupData.getGroup(0, k); + List parquetTypesK = groupDataK.getType().getFields(); + if (2 != parquetTypesK.size()) { + // warn: 不是key value成对出现 + throw new RuntimeException(String.format("bad parquet map type: %s", groupData.getValueToString(index, 0))); + } + Object subDataKey = this.readFields(groupDataK, parquetTypesK.get(0), 0, parquetMetaMap, isUtcTimestamp); + Object subDataValue = this.readFields(groupDataK, parquetTypesK.get(1), 1, parquetMetaMap, isUtcTimestamp); + if (StringUtils.equalsIgnoreCase("key", parquetTypesK.get(0).getName())) { + ((JSONObject) data).put(subDataKey.toString(), subDataValue); + } else { + ((JSONObject) data).put(subDataValue.toString(), subDataKey); + } + } + } + return data; + } else if (this.getOriginalType(type, parquetMetaMap) == org.apache.parquet.schema.OriginalType.LIST) { + Group groupData = g.getGroup(index, 0); + List parquetTypes = groupData.getType().getFields(); + JSONArray data = new JSONArray(); + for (int i = 0; i < parquetTypes.size(); i++) { + Object subData = this.readFields(groupData, parquetTypes.get(i), i, parquetMetaMap, isUtcTimestamp); + data.add(subData); + } + return data; + } else if (this.getOriginalType(type, parquetMetaMap) == org.apache.parquet.schema.OriginalType.DECIMAL) { + Binary binaryDate = g.getBinary(index, 0); + if (null == binaryDate) { + return null; + } else { + org.apache.hadoop.hive.serde2.io.HiveDecimalWritable decimalWritable = new org.apache.hadoop.hive.serde2.io.HiveDecimalWritable(binaryDate.getBytes(), this.asPrimitiveType(type, parquetMetaMap).getDecimalMetadata().getScale()); + // g.getType().getFields().get(1).asPrimitiveType().getDecimalMetadata().getScale() + HiveDecimal hiveDecimal = decimalWritable.getHiveDecimal(); + if (null == hiveDecimal) { + return null; + } else { + return hiveDecimal.bigDecimalValue(); + } + // return decimalWritable.doubleValue(); + } + } else if (this.getOriginalType(type, parquetMetaMap) == org.apache.parquet.schema.OriginalType.DATE) { + return java.sql.Date.valueOf(LocalDate.ofEpochDay(g.getInteger(index, 0))); + } else if (this.getOriginalType(type, parquetMetaMap) == org.apache.parquet.schema.OriginalType.UTF8) { + return g.getValueToString(index, 0); + } else { + if (type.isPrimitive()) { + PrimitiveType.PrimitiveTypeName primitiveTypeName = this.asPrimitiveType(type, parquetMetaMap).getPrimitiveTypeName(); + if (PrimitiveType.PrimitiveTypeName.BINARY == primitiveTypeName) { + return g.getValueToString(index, 0); + } else if (PrimitiveType.PrimitiveTypeName.BOOLEAN == primitiveTypeName) { + return g.getValueToString(index, 0); + } else if (PrimitiveType.PrimitiveTypeName.DOUBLE == primitiveTypeName) { + return g.getValueToString(index, 0); + } else if (PrimitiveType.PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY == primitiveTypeName) { + return g.getValueToString(index, 0); + } else if (PrimitiveType.PrimitiveTypeName.FLOAT == primitiveTypeName) { + return g.getValueToString(index, 0); + } else if (PrimitiveType.PrimitiveTypeName.INT32 == primitiveTypeName) { + return g.getValueToString(index, 0); + } else if (PrimitiveType.PrimitiveTypeName.INT64 == primitiveTypeName) { + return g.getValueToString(index, 0); + } else if (PrimitiveType.PrimitiveTypeName.INT96 == primitiveTypeName) { + Binary dataInt96 = g.getInt96(index, 0); + if (null == dataInt96) { + return null; + } else { + ByteBuffer buf = dataInt96.toByteBuffer(); + buf.order(ByteOrder.LITTLE_ENDIAN); + long timeOfDayNanos = buf.getLong(); + int julianDay = buf.getInt(); + if (isUtcTimestamp) { + // UTC + LocalDate localDate = LocalDate.ofEpochDay(julianDay - JULIAN_EPOCH_OFFSET_DAYS); + LocalTime localTime = LocalTime.ofNanoOfDay(timeOfDayNanos); + return Timestamp.valueOf(LocalDateTime.of(localDate, localTime)); + } else { + // local time + long mills = julianDayToMillis(julianDay) + (timeOfDayNanos / NANOS_PER_MILLISECOND); + Timestamp timestamp = new Timestamp(mills); + timestamp.setNanos((int) (timeOfDayNanos % TimeUnit.SECONDS.toNanos(1))); + return timestamp; + } + } + } else { + return g.getValueToString(index, 0); + } + } else { + return g.getValueToString(index, 0); + } + } + } + + } diff --git a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/HdfsPathFilter.java b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/HdfsPathFilter.java new file mode 100644 index 0000000000..88dd1fa773 --- /dev/null +++ b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/HdfsPathFilter.java @@ -0,0 +1,21 @@ +package com.alibaba.datax.plugin.reader.hdfsreader; + +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.PathFilter; + +/** + * Created by wmy on 16/11/29. + */ +public class HdfsPathFilter implements PathFilter { + + private String regex = null; + + public HdfsPathFilter(String regex) { + this.regex = regex; + } + + @Override + public boolean accept(Path path) { + return regex != null ? path.getName().matches(regex) : true; + } +} diff --git a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/HdfsReader.java b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/HdfsReader.java index c953ef162e..08c630fc28 100644 --- a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/HdfsReader.java +++ b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/HdfsReader.java @@ -41,6 +41,8 @@ public static class Job extends Reader.Job { private String specifiedFileType = null; private DFSUtil dfsUtil = null; private List path = null; + private boolean skipEmptyOrcFile = false; + private Integer orcFileEmptySize = null; @Override public void init() { @@ -81,9 +83,10 @@ public void validate(){ !specifiedFileType.equalsIgnoreCase(Constant.TEXT) && !specifiedFileType.equalsIgnoreCase(Constant.CSV) && !specifiedFileType.equalsIgnoreCase(Constant.SEQ) && - !specifiedFileType.equalsIgnoreCase(Constant.RC)){ - String message = "HdfsReader插件目前支持ORC, TEXT, CSV, SEQUENCE, RC五种格式的文件," + - "请将fileType选项的值配置为ORC, TEXT, CSV, SEQUENCE 或者 RC"; + !specifiedFileType.equalsIgnoreCase(Constant.RC) && + !specifiedFileType.equalsIgnoreCase(Constant.PARQUET)){ + String message = "HdfsReader插件目前支持ORC, TEXT, CSV, SEQUENCE, RC, PARQUET 六种格式的文件," + + "请将fileType选项的值配置为ORC, TEXT, CSV, SEQUENCE,RC 和 PARQUET"; throw DataXException.asDataXException(HdfsReaderErrorCode.FILE_TYPE_ERROR, message); } @@ -115,6 +118,16 @@ public void validate(){ UnstructuredStorageReaderUtil.validateCompress(this.readerOriginConfig); UnstructuredStorageReaderUtil.validateCsvReaderConfig(this.readerOriginConfig); } + if (this.specifiedFileType.equalsIgnoreCase(Constant.ORC)) { + skipEmptyOrcFile = this.readerOriginConfig.getBool(Key.SKIP_EMPTY_ORCFILE, false); + orcFileEmptySize = this.readerOriginConfig.getInt(Key.ORCFILE_EMPTYSIZE); + //将orcFileEmptySize必填项检查去掉,仅需要配置skipEmptyOrcFile即可,考虑历史任务兼容性(For中华保险),保留orcFileEmptySize参数配置 + //if (skipEmptyOrcFile && orcFileEmptySize == null) { + // throw new IllegalArgumentException("When \"skipEmptyOrcFile\" is configured, " + // + "parameter \"orcFileEmptySize\" cannot be null."); + //} + } + LOG.info("skipEmptyOrcFile: {}, orcFileEmptySize: {}", skipEmptyOrcFile, orcFileEmptySize); } @@ -166,7 +179,7 @@ private void validateColumns(){ @Override public void prepare() { LOG.info("prepare(), start to getAllFiles..."); - this.sourceFiles = dfsUtil.getAllFiles(path, specifiedFileType); + this.sourceFiles = dfsUtil.getAllFiles(path, specifiedFileType,skipEmptyOrcFile, orcFileEmptySize); LOG.info(String.format("您即将读取的文件数为: [%s], 列表为: [%s]", this.sourceFiles.size(), StringUtils.join(this.sourceFiles, ","))); @@ -273,7 +286,9 @@ public void startRead(RecordSender recordSender) { }else if(specifiedFileType.equalsIgnoreCase(Constant.RC)){ dfsUtil.rcFileStartRead(sourceFile, this.taskConfig, recordSender, this.getTaskPluginCollector()); - }else { + } else if (specifiedFileType.equalsIgnoreCase(Constant.PARQUET)) { + dfsUtil.parquetFileStartRead(sourceFile, this.taskConfig, recordSender, this.getTaskPluginCollector()); + } else { String message = "HdfsReader插件目前支持ORC, TEXT, CSV, SEQUENCE, RC五种格式的文件," + "请将fileType选项的值配置为ORC, TEXT, CSV, SEQUENCE 或者 RC"; diff --git a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/HdfsReaderErrorCode.java b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/HdfsReaderErrorCode.java index 8dd3f37095..f2caa1a81e 100644 --- a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/HdfsReaderErrorCode.java +++ b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/HdfsReaderErrorCode.java @@ -19,7 +19,12 @@ public enum HdfsReaderErrorCode implements ErrorCode { FILE_TYPE_UNSUPPORT("HdfsReader-12", "文件类型目前不支持"), KERBEROS_LOGIN_ERROR("HdfsReader-13", "KERBEROS认证失败"), READ_SEQUENCEFILE_ERROR("HdfsReader-14", "读取SequenceFile文件出错"), - READ_RCFILE_ERROR("HdfsReader-15", "读取RCFile文件出错"),; + READ_RCFILE_ERROR("HdfsReader-15", "读取RCFile文件出错"), + INIT_RCFILE_SERDE_ERROR("HdfsReader-16", "Deserialize RCFile, initialization failed!"), + PARSE_MESSAGE_TYPE_FROM_SCHEMA_ERROR("HdfsReader-17", "Error parsing ParquetSchema"), + INVALID_PARQUET_SCHEMA("HdfsReader-18", "ParquetSchema is invalid"), + READ_PARQUET_ERROR("HdfsReader-19", "Error reading Parquet file"), + CONNECT_HDFS_IO_ERROR("HdfsReader-20", "I/O exception in establishing connection with HDFS"); private final String code; private final String description; diff --git a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/Key.java b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/Key.java index 7b985a8832..7f9b3a0ab3 100644 --- a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/Key.java +++ b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/Key.java @@ -7,9 +7,60 @@ public final class Key { */ public final static String PATH = "path"; public final static String DEFAULT_FS = "defaultFS"; + public final static String HIVE_VERSION = "hiveVersion"; public static final String FILETYPE = "fileType"; public static final String HADOOP_CONFIG = "hadoopConfig"; public static final String HAVE_KERBEROS = "haveKerberos"; public static final String KERBEROS_KEYTAB_FILE_PATH = "kerberosKeytabFilePath"; + public static final String KERBEROS_CONF_FILE_PATH = "kerberosConfFilePath"; public static final String KERBEROS_PRINCIPAL = "kerberosPrincipal"; + public static final String PATH_FILTER = "pathFilter"; + public static final String PARQUET_SCHEMA = "parquetSchema"; + /** + * hive 3.x 或 cdh高版本,使用UTC时区存储时间戳,如果发现时区偏移,该配置项要配置成 true + */ + public static final String PARQUET_UTC_TIMESTAMP = "parquetUtcTimestamp"; + public static final String SUCCESS_ON_NO_FILE = "successOnNoFile"; + public static final String PROTECTION = "protection"; + + /** + * 用于显示地指定hdfs客户端的用户名 + */ + public static final String HDFS_USERNAME = "hdfsUsername"; + + /** + * ORC FILE空文件大小 + */ + public static final String ORCFILE_EMPTYSIZE = "orcFileEmptySize"; + + /** + * 是否跳过空的OrcFile + */ + public static final String SKIP_EMPTY_ORCFILE = "skipEmptyOrcFile"; + + /** + * 是否跳过 orc meta 信息 + */ + + public static final String SKIP_ORC_META = "skipOrcMetaInfo"; + /** + * 过滤_或者.开头的文件 + */ + public static final String REGEX_PATTERN = "^.*[/][^._].*"; + + public static final String FILTER_TAG_FILE = "filterTagFile"; + + // high level params refs https://github.com/aliyun/alibabacloud-jindodata/blob/master/docs/user/4.x/4.4.0/oss/configuration/jindosdk_configuration_list.md + // + public static final String FS_OSS_DOWNLOAD_QUEUE_SIZE = "ossDownloadQueueSize"; + + // + public static final String FS_OSS_DOWNLOAD_THREAD_CONCURRENCY = "ossDownloadThreadConcurrency"; + + public static final String FS_OSS_READ_READAHEAD_BUFFER_COUNT = "ossDownloadBufferCount"; + + public static final String FILE_SYSTEM_TYPE = "fileSystemType"; + public static final String CDH_3_X_HIVE_VERSION = "3.1.3-cdh"; + + public static final String SUPPORT_ADD_MIDDLE_COLUMN = "supportAddMiddleColumn"; } diff --git a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/ParquetMessageHelper.java b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/ParquetMessageHelper.java new file mode 100644 index 0000000000..e5838d6eff --- /dev/null +++ b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/ParquetMessageHelper.java @@ -0,0 +1,33 @@ +package com.alibaba.datax.plugin.reader.hdfsreader; + +import org.apache.parquet.schema.OriginalType; +import org.apache.parquet.schema.PrimitiveType; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author jitongchen + * @date 2023/9/7 10:20 AM + */ +public class ParquetMessageHelper { + public static Map parseParquetTypes(List parquetTypes) { + int fieldCount = parquetTypes.size(); + Map parquetMetaMap = new HashMap(); + for (int i = 0; i < fieldCount; i++) { + org.apache.parquet.schema.Type type = parquetTypes.get(i); + String name = type.getName(); + ParquetMeta parquetMeta = new ParquetMeta(); + parquetMeta.setName(name); + OriginalType originalType = type.getOriginalType(); + parquetMeta.setOriginalType(originalType); + if (type.isPrimitive()) { + PrimitiveType primitiveType = type.asPrimitiveType(); + parquetMeta.setPrimitiveType(primitiveType); + } + parquetMetaMap.put(name, parquetMeta); + } + return parquetMetaMap; + } +} diff --git a/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/ParquetMeta.java b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/ParquetMeta.java new file mode 100644 index 0000000000..6f99e9b599 --- /dev/null +++ b/hdfsreader/src/main/java/com/alibaba/datax/plugin/reader/hdfsreader/ParquetMeta.java @@ -0,0 +1,38 @@ +package com.alibaba.datax.plugin.reader.hdfsreader; + +import org.apache.parquet.schema.OriginalType; +import org.apache.parquet.schema.PrimitiveType; + +/** + * @author jitongchen + * @date 2023/9/7 10:20 AM + */ +public class ParquetMeta { + private String name; + private OriginalType originalType; + private PrimitiveType primitiveType; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public OriginalType getOriginalType() { + return originalType; + } + + public void setOriginalType(OriginalType originalType) { + this.originalType = originalType; + } + + public PrimitiveType getPrimitiveType() { + return primitiveType; + } + + public void setPrimitiveType(PrimitiveType primitiveType) { + this.primitiveType = primitiveType; + } +} \ No newline at end of file diff --git a/hdfswriter/doc/hdfswriter.md b/hdfswriter/doc/hdfswriter.md index 028a544e63..1259b253a4 100644 --- a/hdfswriter/doc/hdfswriter.md +++ b/hdfswriter/doc/hdfswriter.md @@ -231,6 +231,7 @@ HdfsWriter提供向HDFS文件系统指定路径中写入TEXTFile文件和ORCFile * append,写入前不做任何处理,DataX hdfswriter直接使用filename写入,并保证文件名不冲突。 * nonConflict,如果目录下有fileName前缀的文件,直接报错。 + * truncate,如果目录下有fileName前缀的文件,先删除后写入。 * 必选:是
diff --git a/hdfswriter/pom.xml b/hdfswriter/pom.xml index 15b3780a60..5d82520faa 100644 --- a/hdfswriter/pom.xml +++ b/hdfswriter/pom.xml @@ -8,17 +8,33 @@ 0.0.1-SNAPSHOT 4.0.0 - hdfswriter hdfswriter HdfsWriter提供了写入HDFS功能。 jar - 1.1.1 + 3.1.3 2.7.1 + + + com.twitter + parquet-hadoop-bundle + 1.6.0 + + + org.apache.logging.log4j + log4j-api + 2.17.1 + + + + org.apache.logging.log4j + log4j-core + 2.17.1 + com.alibaba.datax datax-common @@ -30,6 +46,11 @@ + + com.aliyun.oss + hadoop-aliyun + 2.7.2 + org.slf4j slf4j-api @@ -132,4 +153,4 @@ - \ No newline at end of file + diff --git a/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/HdfsHelper.java b/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/HdfsHelper.java index c8bfa50b6c..e2900f7c32 100644 --- a/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/HdfsHelper.java +++ b/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/HdfsHelper.java @@ -6,10 +6,13 @@ import com.alibaba.datax.common.plugin.RecordReceiver; import com.alibaba.datax.common.plugin.TaskPluginCollector; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.datax.plugin.unstructuredstorage.util.ColumnTypeUtil; +import com.alibaba.datax.plugin.unstructuredstorage.util.HdfsUtil; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; import com.google.common.collect.Lists; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.tuple.MutablePair; import org.apache.hadoop.fs.*; import org.apache.hadoop.hive.ql.io.orc.OrcOutputFormat; @@ -24,7 +27,11 @@ import org.apache.hadoop.security.UserGroupInformation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import parquet.hadoop.metadata.CompressionCodecName; +import parquet.schema.*; + import java.io.IOException; +import java.sql.Timestamp; import java.text.SimpleDateFormat; import java.util.*; @@ -434,7 +441,7 @@ public List getColumnTypeInspectors(List column objectInspector = ObjectInspectorFactory.getReflectionObjectInspector(Double.class, ObjectInspectorFactory.ObjectInspectorOptions.JAVA); break; case TIMESTAMP: - objectInspector = ObjectInspectorFactory.getReflectionObjectInspector(java.sql.Timestamp.class, ObjectInspectorFactory.ObjectInspectorOptions.JAVA); + objectInspector = ObjectInspectorFactory.getReflectionObjectInspector(org.apache.hadoop.hive.common.type.Timestamp.class, ObjectInspectorFactory.ObjectInspectorOptions.JAVA); break; case DATE: objectInspector = ObjectInspectorFactory.getReflectionObjectInspector(java.sql.Date.class, ObjectInspectorFactory.ObjectInspectorOptions.JAVA); @@ -527,7 +534,13 @@ public static MutablePair, Boolean> transportOneRecord( recordList.add(new java.sql.Date(column.asDate().getTime())); break; case TIMESTAMP: - recordList.add(new java.sql.Timestamp(column.asDate().getTime())); + Date date = column.asDate(); + if (date == null) { + recordList.add(null); + } else { + Timestamp ts = new Timestamp(date.getTime()); + recordList.add(org.apache.hadoop.hive.common.type.Timestamp.ofEpochMilli(ts.getTime(), ts.getNanos())); + } break; default: throw DataXException @@ -556,4 +569,131 @@ public static MutablePair, Boolean> transportOneRecord( transportResult.setLeft(recordList); return transportResult; } + + + public static String generateParquetSchemaFromColumnAndType(List columns) { + Map decimalColInfo = new HashMap<>(16); + ColumnTypeUtil.DecimalInfo PARQUET_DEFAULT_DECIMAL_INFO = new ColumnTypeUtil.DecimalInfo(10, 2); + Types.MessageTypeBuilder typeBuilder = Types.buildMessage(); + for (Configuration column : columns) { + String name = column.getString("name"); + String colType = column.getString("type"); + Validate.notNull(name, "column.name can't be null"); + Validate.notNull(colType, "column.type can't be null"); + switch (colType.toLowerCase()) { + case "tinyint": + case "smallint": + case "int": + typeBuilder.optional(PrimitiveType.PrimitiveTypeName.INT32).named(name); + break; + case "bigint": + case "long": + typeBuilder.optional(PrimitiveType.PrimitiveTypeName.INT64).named(name); + break; + case "float": + typeBuilder.optional(PrimitiveType.PrimitiveTypeName.FLOAT).named(name); + break; + case "double": + typeBuilder.optional(PrimitiveType.PrimitiveTypeName.DOUBLE).named(name); + break; + case "binary": + typeBuilder.optional(PrimitiveType.PrimitiveTypeName.BINARY).named(name); + break; + case "char": + case "varchar": + case "string": + typeBuilder.optional(PrimitiveType.PrimitiveTypeName.BINARY).as(OriginalType.UTF8).named(name); + break; + case "boolean": + typeBuilder.optional(PrimitiveType.PrimitiveTypeName.BOOLEAN).named(name); + break; + case "timestamp": + typeBuilder.optional(PrimitiveType.PrimitiveTypeName.INT96).named(name); + break; + case "date": + typeBuilder.optional(PrimitiveType.PrimitiveTypeName.INT32).as(OriginalType.DATE).named(name); + break; + default: + if (ColumnTypeUtil.isDecimalType(colType)) { + ColumnTypeUtil.DecimalInfo decimalInfo = ColumnTypeUtil.getDecimalInfo(colType, PARQUET_DEFAULT_DECIMAL_INFO); + typeBuilder.optional(PrimitiveType.PrimitiveTypeName.FIXED_LEN_BYTE_ARRAY) + .as(OriginalType.DECIMAL) + .precision(decimalInfo.getPrecision()) + .scale(decimalInfo.getScale()) + .length(HdfsUtil.computeMinBytesForPrecision(decimalInfo.getPrecision())) + .named(name); + + decimalColInfo.put(name, decimalInfo); + } else { + typeBuilder.optional(PrimitiveType.PrimitiveTypeName.BINARY).named(name); + } + break; + } + } + return typeBuilder.named("m").toString(); + } + + public void parquetFileStartWrite(RecordReceiver lineReceiver, Configuration config, String fileName, TaskPluginCollector taskPluginCollector, Configuration taskConfig) { + MessageType messageType = null; + ParquetFileProccessor proccessor = null; + Path outputPath = new Path(fileName); + String schema = config.getString(Key.PARQUET_SCHEMA, null); + if (schema == null) { + List columns = config.getListConfiguration(Key.COLUMN); + if (columns == null || columns.isEmpty()) { + throw DataXException.asDataXException("parquetSchema or column can't be blank!"); + } + schema = HdfsHelper.generateParquetSchemaFromColumnAndType(columns); + } + try { + messageType = MessageTypeParser.parseMessageType(schema); + } catch (Exception e) { + String message = String.format("Error parsing the Schema string [%s] into MessageType", schema); + LOG.error(message); + throw DataXException.asDataXException(HdfsWriterErrorCode.PARSE_MESSAGE_TYPE_FROM_SCHEMA_ERROR, e); + } + + // determine the compression codec + String compress = config.getString(Key.COMPRESS, null); + // be compatible with the old NONE + if ("NONE".equalsIgnoreCase(compress)) { + compress = "UNCOMPRESSED"; + } + CompressionCodecName compressionCodecName = CompressionCodecName.fromConf(compress); + LOG.info("The compression codec used for parquet writing is: {}", compressionCodecName, compress); + try { + proccessor = new ParquetFileProccessor(outputPath, messageType, compressionCodecName, false, taskConfig, taskPluginCollector, hadoopConf); + } catch (Exception e) { + String message = String.format("Initializing ParquetFileProccessor based on Schema[%s] failed.", schema); + LOG.error(message); + throw DataXException.asDataXException(HdfsWriterErrorCode.INIT_PROCCESSOR_FAILURE, e); + } + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmm"); + String attempt = "attempt_" + dateFormat.format(new Date()) + "_0001_m_000000_0"; + conf.set(JobContext.TASK_ATTEMPT_ID, attempt); + FileOutputFormat outFormat = new TextOutputFormat(); + outFormat.setOutputPath(conf, outputPath); + outFormat.setWorkOutputPath(conf, outputPath); + try { + Record record = null; + while ((record = lineReceiver.getFromReader()) != null) { + proccessor.write(record); + } + } catch (Exception e) { + String message = String.format("An exception occurred while writing the file file [%s]", fileName); + LOG.error(message); + Path path = new Path(fileName); + deleteDir(path.getParent()); + throw DataXException.asDataXException(HdfsWriterErrorCode.Write_FILE_IO_ERROR, e); + } finally { + if (proccessor != null) { + try { + proccessor.close(); + } catch (IOException e) { + LOG.error(e.getMessage(), e); + } + } + } + } + } diff --git a/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/HdfsWriter.java b/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/HdfsWriter.java index 853613a225..7535687cec 100644 --- a/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/HdfsWriter.java +++ b/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/HdfsWriter.java @@ -9,9 +9,11 @@ import org.apache.commons.io.Charsets; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; import org.apache.hadoop.fs.Path; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import parquet.schema.MessageTypeParser; import java.util.*; @@ -51,8 +53,8 @@ private void validateParameter() { this.defaultFS = this.writerSliceConfig.getNecessaryValue(Key.DEFAULT_FS, HdfsWriterErrorCode.REQUIRED_VALUE); //fileType check this.fileType = this.writerSliceConfig.getNecessaryValue(Key.FILE_TYPE, HdfsWriterErrorCode.REQUIRED_VALUE); - if( !fileType.equalsIgnoreCase("ORC") && !fileType.equalsIgnoreCase("TEXT")){ - String message = "HdfsWriter插件目前只支持ORC和TEXT两种格式的文件,请将filetype选项的值配置为ORC或者TEXT"; + if (!fileType.equalsIgnoreCase("ORC") && !fileType.equalsIgnoreCase("TEXT") && !fileType.equalsIgnoreCase("PARQUET")) { + String message = "HdfsWriter插件目前只支持ORC、TEXT、PARQUET三种格式的文件,请将filetype选项的值配置为ORC、TEXT或PARQUET"; throw DataXException.asDataXException(HdfsWriterErrorCode.ILLEGAL_VALUE, message); } //path @@ -226,6 +228,12 @@ public List split(int mandatoryNumber) { String endFullFileName = null; fileSuffix = UUID.randomUUID().toString().replace('-', '_'); + if (fileType.equalsIgnoreCase("PARQUET")) { + if (StringUtils.isNotBlank(this.compress)) { + fileSuffix += "." + this.compress.toLowerCase(); + } + fileSuffix += ".parquet"; + } fullFileName = String.format("%s%s%s__%s", defaultFS, storePath, filePrefix, fileSuffix); endFullFileName = String.format("%s%s%s__%s", defaultFS, endStorePath, filePrefix, fileSuffix); @@ -323,8 +331,55 @@ private String buildTmpFilePath(String userPath) { } return tmpFilePath; } + public void unitizeParquetConfig(Configuration writerSliceConfig) { + String parquetSchema = writerSliceConfig.getString(Key.PARQUET_SCHEMA); + if (StringUtils.isNotBlank(parquetSchema)) { + LOG.info("parquetSchema has config. use parquetSchema:\n{}", parquetSchema); + return; + } + + List columns = writerSliceConfig.getListConfiguration(Key.COLUMN); + if (columns == null || columns.isEmpty()) { + throw DataXException.asDataXException("parquetSchema or column can't be blank!"); + } + + parquetSchema = generateParquetSchemaFromColumn(columns); + // 为了兼容历史逻辑,对之前的逻辑做保留,但是如果配置的时候报错,则走新逻辑 + try { + MessageTypeParser.parseMessageType(parquetSchema); + } catch (Throwable e) { + LOG.warn("The generated parquetSchema {} is illegal, try to generate parquetSchema in another way", parquetSchema); + parquetSchema = HdfsHelper.generateParquetSchemaFromColumnAndType(columns); + LOG.info("The last generated parquet schema is {}", parquetSchema); + } + writerSliceConfig.set(Key.PARQUET_SCHEMA, parquetSchema); + LOG.info("dataxParquetMode use default fields."); + writerSliceConfig.set(Key.DATAX_PARQUET_MODE, "fields"); + } + + private String generateParquetSchemaFromColumn(List columns) { + StringBuffer parquetSchemaStringBuffer = new StringBuffer(); + parquetSchemaStringBuffer.append("message m {"); + for (Configuration column: columns) { + String name = column.getString("name"); + Validate.notNull(name, "column.name can't be null"); + + String type = column.getString("type"); + Validate.notNull(type, "column.type can't be null"); + + String parquetColumn = String.format("optional %s %s;", type, name); + parquetSchemaStringBuffer.append(parquetColumn); + } + parquetSchemaStringBuffer.append("}"); + String parquetSchema = parquetSchemaStringBuffer.toString(); + LOG.info("generate parquetSchema:\n{}", parquetSchema); + return parquetSchema; + } + } + + public static class Task extends Writer.Task { private static final Logger LOG = LoggerFactory.getLogger(Task.class); @@ -366,6 +421,9 @@ public void startWrite(RecordReceiver lineReceiver) { //写ORC FILE hdfsHelper.orcFileStartWrite(lineReceiver,this.writerSliceConfig, this.fileName, this.getTaskPluginCollector()); + } else if (fileType.equalsIgnoreCase("PARQUET")) { + //写PARQUET FILE + hdfsHelper.parquetFileStartWrite(lineReceiver, this.writerSliceConfig, this.fileName, this.getTaskPluginCollector(), this.writerSliceConfig); } LOG.info("end do write"); diff --git a/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/HdfsWriterErrorCode.java b/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/HdfsWriterErrorCode.java index a9e1cb30e6..8a729f9787 100644 --- a/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/HdfsWriterErrorCode.java +++ b/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/HdfsWriterErrorCode.java @@ -16,7 +16,11 @@ public enum HdfsWriterErrorCode implements ErrorCode { CONNECT_HDFS_IO_ERROR("HdfsWriter-06", "与HDFS建立连接时出现IO异常."), COLUMN_REQUIRED_VALUE("HdfsWriter-07", "您column配置中缺失了必须填写的参数值."), HDFS_RENAME_FILE_ERROR("HdfsWriter-08", "将文件移动到配置路径失败."), - KERBEROS_LOGIN_ERROR("HdfsWriter-09", "KERBEROS认证失败"); + KERBEROS_LOGIN_ERROR("HdfsWriter-09", "KERBEROS认证失败"), + PARSE_MESSAGE_TYPE_FROM_SCHEMA_ERROR("HdfsWriter-10", "Parse parquet schema error"), + + INIT_PROCCESSOR_FAILURE("HdfsWriter-11", "Init processor failed"); + private final String code; private final String description; diff --git a/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/Key.java b/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/Key.java index f1f6309689..05f4cd0a4f 100644 --- a/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/Key.java +++ b/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/Key.java @@ -33,4 +33,45 @@ public class Key { public static final String KERBEROS_PRINCIPAL = "kerberosPrincipal"; // hadoop config public static final String HADOOP_CONFIG = "hadoopConfig"; + + // useOldRawDataTransf + public final static String PARQUET_FILE_USE_RAW_DATA_TRANSF = "useRawDataTransf"; + + public final static String DATAX_PARQUET_MODE = "dataxParquetMode"; + + // hdfs username 默认值 admin + public final static String HDFS_USERNAME = "hdfsUsername"; + + public static final String PROTECTION = "protection"; + + public static final String PARQUET_SCHEMA = "parquetSchema"; + public static final String PARQUET_MERGE_RESULT = "parquetMergeResult"; + + /** + * hive 3.x 或 cdh高版本,使用UTC时区存储时间戳,如果发现时区偏移,该配置项要配置成 true + */ + public static final String PARQUET_UTC_TIMESTAMP = "parquetUtcTimestamp"; + + // Kerberos + public static final String KERBEROS_CONF_FILE_PATH = "kerberosConfFilePath"; + + // PanguFS + public final static String PANGU_FS_CONFIG = "panguFSConfig"; + public final static String PANGU_FS_CONFIG_NUWA_CLUSTER = "nuwaCluster"; + public final static String PANGU_FS_CONFIG_NUWA_SERVERS = "nuwaServers"; + public final static String PANGU_FS_CONFIG_NUWA_PROXIES = "nuwaProxies"; + public final static String PANGU_FS_CONFIG_CAPABILITY = "capability"; + + + public static final String FS_OSS_UPLOAD_THREAD_CONCURRENCY = "ossUploadConcurrency"; + // + public static final String FS_OSS_UPLOAD_QUEUE_SIZE = "ossUploadQueueSize"; + // + public static final String FS_OSS_UPLOAD_MAX_PENDING_TASKS_PER_STREAM = "ossUploadMaxPendingTasksPerStream"; + + public static final String FS_OSS_BLOCKLET_SIZE_MB = "ossBlockSize"; + + public static final String FILE_SYSTEM_TYPE = "fileSystemType"; + public static final String ENABLE_COLUMN_EXCHANGE = "enableColumnExchange"; + public static final String SUPPORT_HIVE_DATETIME = "supportHiveDateTime"; } diff --git a/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/ParquetFileProccessor.java b/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/ParquetFileProccessor.java new file mode 100644 index 0000000000..90d0f6e5b6 --- /dev/null +++ b/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/ParquetFileProccessor.java @@ -0,0 +1,30 @@ +package com.alibaba.datax.plugin.writer.hdfswriter; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; +import org.apache.hadoop.fs.Path; +import parquet.hadoop.ParquetWriter; +import parquet.hadoop.metadata.CompressionCodecName; +import parquet.schema.MessageType; + +import java.io.IOException; + +/** + * @author jitongchen + * @date 2023/9/7 9:41 AM + */ +public class ParquetFileProccessor extends ParquetWriter { + + public ParquetFileProccessor(Path file, MessageType schema, boolean enableDictionary, Configuration taskConfig, TaskPluginCollector taskPluginCollector, org.apache.hadoop.conf.Configuration configuration) throws IOException { + this(file, schema, CompressionCodecName.UNCOMPRESSED, enableDictionary, taskConfig, taskPluginCollector, configuration); + } + + public ParquetFileProccessor(Path file, MessageType schema, CompressionCodecName codecName, boolean enableDictionary, Configuration taskConfig, TaskPluginCollector taskPluginCollector) throws IOException { + super(file, new ParquetFileSupport(schema, taskConfig, taskPluginCollector), codecName, DEFAULT_BLOCK_SIZE, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE, enableDictionary, false, DEFAULT_WRITER_VERSION); + } + + public ParquetFileProccessor(Path file, MessageType schema, CompressionCodecName codecName, boolean enableDictionary, Configuration taskConfig, TaskPluginCollector taskPluginCollector, org.apache.hadoop.conf.Configuration configuration) throws IOException { + super(file, new ParquetFileSupport(schema, taskConfig, taskPluginCollector), codecName, DEFAULT_BLOCK_SIZE, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE, enableDictionary, false, DEFAULT_WRITER_VERSION, configuration); + } +} diff --git a/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/ParquetFileSupport.java b/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/ParquetFileSupport.java new file mode 100644 index 0000000000..410d52318d --- /dev/null +++ b/hdfswriter/src/main/java/com/alibaba/datax/plugin/writer/hdfswriter/ParquetFileSupport.java @@ -0,0 +1,642 @@ +package com.alibaba.datax.plugin.writer.hdfswriter; + +import com.alibaba.datax.common.element.*; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.LimitLogger; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import parquet.column.ColumnDescriptor; +import parquet.hadoop.api.WriteSupport; +import parquet.io.api.Binary; +import parquet.io.api.RecordConsumer; +import parquet.schema.*; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.sql.Timestamp; +import java.text.SimpleDateFormat; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.temporal.ChronoField; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * @author jitongchen + * @date 2023/9/7 9:41 AM + */ +public class ParquetFileSupport extends WriteSupport { + public static final Logger LOGGER = LoggerFactory.getLogger(ParquetFileSupport.class); + private MessageType schema; + private List columns; + private RecordConsumer recordConsumer; + private boolean useRawDataTransf = true; + private boolean printStackTrace = true; + + // 不通类型的nullFormat + private String nullFormat; + + private String dateFormat; + private boolean isUtcTimestamp; + private SimpleDateFormat dateParse; + private Binary binaryForNull; + private TaskPluginCollector taskPluginCollector; + private String dataxParquetMode; + + public ParquetFileSupport(MessageType schema, com.alibaba.datax.common.util.Configuration taskConfig, TaskPluginCollector taskPluginCollector) { + this.schema = schema; + this.columns = schema.getColumns(); + this.useRawDataTransf = taskConfig.getBool(Key.PARQUET_FILE_USE_RAW_DATA_TRANSF, true); + + // 不通类型的nullFormat + this.nullFormat = taskConfig.getString(Key.NULL_FORMAT, Constant.DEFAULT_NULL_FORMAT); + this.binaryForNull = Binary.fromString(this.nullFormat); + + this.dateFormat = taskConfig.getString(Key.DATE_FORMAT, null); + if (StringUtils.isNotBlank(this.dateFormat)) { + this.dateParse = new SimpleDateFormat(dateFormat); + } + + this.isUtcTimestamp = taskConfig.getBool(Key.PARQUET_UTC_TIMESTAMP, false); + + this.taskPluginCollector = taskPluginCollector; + if (taskConfig.getKeys().contains("dataxParquetMode")) { + this.dataxParquetMode = taskConfig.getString("dataxParquetMode"); + } else { + // 默认值是columns + this.dataxParquetMode = "columns"; + } + } + + @Override + public WriteContext init(Configuration configuration) { + return new WriteContext(schema, new HashMap()); + } + + @Override + public void prepareForWrite(RecordConsumer recordConsumer) { + this.recordConsumer = recordConsumer; + } + + @Override + public void write(Record values) { + if (dataxParquetMode.equalsIgnoreCase("fields")) { + writeBaseOnFields(values); + return; + } + + // NOTE: 下面的实现其实是不对的,只是看代码注释貌似有用户已经在用 + // 所以暂时不动下面的逻辑。 + // 默认走的就是下面的这条代码路径 + if (values != null && columns != null && values.getColumnNumber() == columns.size()) { + recordConsumer.startMessage(); + for (int i = 0; i < columns.size(); i++) { + Column value = values.getColumn(i); + ColumnDescriptor columnDescriptor = columns.get(i); + Type type = this.schema.getFields().get(i); + if (value != null) { + try { + if (this.useRawDataTransf) { + if (value.getRawData() == null) { + continue; + } + recordConsumer.startField(columnDescriptor.getPath()[0], i); + // 原来使用Column->RawData的方法其实是错误的类型转换策略,会将DataX的数据内部表示形象序列化出去 + // 但是 Parquet 已经有用户使用了,故暂时只是配置项切换 + String rawData = value.getRawData().toString(); + switch (columnDescriptor.getType()) { + case BOOLEAN: + recordConsumer.addBoolean(Boolean.parseBoolean(rawData)); + break; + case FLOAT: + recordConsumer.addFloat(Float.parseFloat(rawData)); + break; + case DOUBLE: + recordConsumer.addDouble(Double.parseDouble(rawData)); + break; + case INT32: + OriginalType originalType = type.getOriginalType(); + if (originalType != null && StringUtils.equalsIgnoreCase("DATE", originalType.name())) { + int realVal = (int) (new java.sql.Date(Long.parseLong(rawData)).toLocalDate().toEpochDay()); + recordConsumer.addInteger(realVal); + } else { + recordConsumer.addInteger(Integer.parseInt(rawData)); + } + break; + case INT64: + recordConsumer.addLong(Long.valueOf(rawData)); + break; + case INT96: + recordConsumer.addBinary(timestampColToBinary(value)); + break; + case BINARY: + recordConsumer.addBinary(Binary.fromString(rawData)); + break; + case FIXED_LEN_BYTE_ARRAY: + PrimitiveType primitiveType = type.asPrimitiveType(); + if (primitiveType.getDecimalMetadata() != null) { + // decimal + recordConsumer.addBinary(decimalToBinary(value, primitiveType.getDecimalMetadata().getPrecision(), primitiveType.getDecimalMetadata().getScale())); + break; + } + /* fall through */ + default: + recordConsumer.addBinary(Binary.fromString(rawData)); + break; + } + + recordConsumer.endField(columnDescriptor.getPath()[0], i); + } else { + boolean isNull = null == value.getRawData(); + + if (!isNull) { + recordConsumer.startField(columnDescriptor.getPath()[0], i); + + // no skip: empty fields are illegal, the field should be ommited completely instead + switch (columnDescriptor.getType()) { + case BOOLEAN: + recordConsumer.addBoolean(value.asBoolean()); + break; + case FLOAT: + recordConsumer.addFloat(value.asDouble().floatValue()); + break; + case DOUBLE: + recordConsumer.addDouble(value.asDouble()); + break; + case INT32: + OriginalType originalType = type.getOriginalType(); + if (originalType != null && StringUtils.equalsIgnoreCase("DATE", originalType.name())) { + int realVal = (int) (new java.sql.Date(value.asLong()).toLocalDate().toEpochDay()); + recordConsumer.addInteger(realVal); + } else { + recordConsumer.addInteger(value.asLong().intValue()); + } + break; + case INT64: + recordConsumer.addLong(value.asLong()); + break; + case INT96: + recordConsumer.addBinary(timestampColToBinary(value)); + break; + case BINARY: + String valueAsString2Write = null; + if (Column.Type.DATE == value.getType() && null != this.dateParse) { + valueAsString2Write = dateParse.format(value.asDate()); + } else { + valueAsString2Write = value.asString(); + } + recordConsumer.addBinary(Binary.fromString(valueAsString2Write)); + break; + case FIXED_LEN_BYTE_ARRAY: + PrimitiveType primitiveType = type.asPrimitiveType(); + if (primitiveType.getDecimalMetadata() != null) { + // decimal + recordConsumer.addBinary(decimalToBinary(value, primitiveType.getDecimalMetadata().getPrecision(), primitiveType.getDecimalMetadata().getScale())); + break; + } + /* fall through */ + default: + recordConsumer.addBinary(Binary.fromString(value.asString())); + break; + } + recordConsumer.endField(columnDescriptor.getPath()[0], i); + } + } + } catch (Exception e) { + if (printStackTrace) { + printStackTrace = false; + LOGGER.warn("write to parquet error: {}", e.getMessage(), e); + } + // dirty data + if (null != this.taskPluginCollector) { + // job post 里面的merge taskPluginCollector 为null + this.taskPluginCollector.collectDirtyRecord(values, e, e.getMessage()); + } + } + } else { + recordConsumer.addBinary(this.binaryForNull); + } + } + recordConsumer.endMessage(); + } + } + + private Binary decimalToBinary(Column value, int precision, int scale) { + BigDecimal bigDecimal = value.asBigDecimal(); + bigDecimal = bigDecimal.setScale(scale, RoundingMode.HALF_UP); + byte[] decimalBytes = bigDecimal.unscaledValue().toByteArray(); + + int precToBytes = ParquetHiveSerDe.PRECISION_TO_BYTE_COUNT[precision - 1]; + if (precToBytes == decimalBytes.length) { + // No padding needed. + return Binary.fromByteArray(decimalBytes); + } + + byte[] tgt = new byte[precToBytes]; + + // padding -1 for negative number + if (bigDecimal.compareTo(new BigDecimal("0")) < 0) { + Arrays.fill(tgt, 0, precToBytes - decimalBytes.length, (byte) -1); + } + + System.arraycopy(decimalBytes, 0, tgt, precToBytes - decimalBytes.length, decimalBytes.length); + return Binary.fromByteArray(tgt); + } + + private static final int JULIAN_EPOCH_OFFSET_DAYS = 2_440_588; + private static final long MILLIS_IN_DAY = TimeUnit.DAYS.toMillis(1); + private static final long MILLS_PER_SECOND = TimeUnit.SECONDS.toMillis(1); + private static final long NANOS_PER_DAY = TimeUnit.DAYS.toNanos(1); + private static final long NANOS_PER_SECOND = TimeUnit.SECONDS.toNanos(1); + private static final ZoneOffset defaultOffset = OffsetDateTime.now().getOffset(); + + /** + * int 96 is timestamp in parquet + * + * @param valueColumn + * @return + */ + private Binary timestampColToBinary(Column valueColumn) { + if (valueColumn.getRawData() == null) { + return Binary.EMPTY; + } + long mills; + long nanos = 0; + if (valueColumn instanceof DateColumn) { + DateColumn dateColumn = (DateColumn) valueColumn; + mills = dateColumn.asLong(); + nanos = dateColumn.getNanos(); + } else { + mills = valueColumn.asLong(); + } + int julianDay; + long nanosOfDay; + if (isUtcTimestamp) { + // utc ignore current timezone (task should set timezone same as hive/hdfs) + long seconds = mills >= 0 ? mills / MILLS_PER_SECOND : (mills / MILLS_PER_SECOND - 1); + LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(seconds, (int) nanos, defaultOffset); + julianDay = (int) (localDateTime.getLong(ChronoField.EPOCH_DAY) + JULIAN_EPOCH_OFFSET_DAYS); + nanosOfDay = localDateTime.getLong(ChronoField.NANO_OF_DAY); + } else { + // local date + julianDay = (int) ((mills / MILLIS_IN_DAY) + JULIAN_EPOCH_OFFSET_DAYS); + if (mills >= 0) { + nanosOfDay = ((mills % MILLIS_IN_DAY) / MILLS_PER_SECOND) * NANOS_PER_SECOND + nanos; + } else { + julianDay--; + nanosOfDay = (((mills % MILLIS_IN_DAY) / MILLS_PER_SECOND) - 1) * NANOS_PER_SECOND + nanos; + nanosOfDay += NANOS_PER_DAY; + } + } + + ByteBuffer buf = ByteBuffer.allocate(12); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.putLong(nanosOfDay); + buf.putInt(julianDay); + buf.flip(); + return Binary.fromByteBuffer(buf); + } + + private void writeBaseOnFields(Record values) { + //LOGGER.info("Writing parquet data using fields mode(The correct mode.)"); + List types = this.schema.getFields(); + + if (values != null && types != null && values.getColumnNumber() == types.size()) { + recordConsumer.startMessage(); + writeFields(types, values); + recordConsumer.endMessage(); + } + } + + private void writeFields(List types, Record values) { + for (int i = 0; i < types.size(); i++) { + Type type = types.get(i); + Column value = values.getColumn(i); + if (value != null) { + try { + if (type.isPrimitive()) { + writePrimitiveType(type, value, i); + } else { + writeGroupType(type, (JSON) JSON.parse(value.asString()), i); + } + } catch (Exception e) { + if (printStackTrace) { + printStackTrace = false; + LOGGER.warn("write to parquet error: {}", e.getMessage(), e); + } + // dirty data + if (null != this.taskPluginCollector) { + // job post 里面的merge taskPluginCollector 为null + this.taskPluginCollector.collectDirtyRecord(values, e, e.getMessage()); + } + } + } + } + } + + private void writeFields(List types, JSONObject values) { + for (int i = 0; i < types.size(); i++) { + Type type = types.get(i); + Object value = values.get(type.getName()); + + if (value != null) { + try { + if (type.isPrimitive()) { + writePrimitiveType(type, value, i); + } else { + writeGroupType(type, (JSON) value, i); + } + } catch (Exception e) { + if (printStackTrace) { + printStackTrace = false; + LOGGER.warn("write to parquet error: {}", e.getMessage(), e); + } + } + } else { + recordConsumer.addBinary(this.binaryForNull); + } + } + } + + private void writeGroupType(Type type, JSON value, int index) { + GroupType groupType = type.asGroupType(); + OriginalType originalType = groupType.getOriginalType(); + if (originalType != null) { + switch (originalType) { + case MAP: + writeMap(groupType, value, index); + break; + case LIST: + writeList(groupType, value, index); + break; + default: + break; + } + } else { + // struct + writeStruct(groupType, value, index); + } + } + + private void writeMap(GroupType groupType, JSON value, int index) { + if (value == null) { + return; + } + + JSONObject json = (JSONObject) value; + + if (json.isEmpty()) { + return; + } + + recordConsumer.startField(groupType.getName(), index); + + recordConsumer.startGroup(); + + // map + // key_value start + recordConsumer.startField("key_value", 0); + recordConsumer.startGroup(); + + List keyValueFields = groupType.getFields().get(0).asGroupType().getFields(); + Type keyType = keyValueFields.get(0); + Type valueType = keyValueFields.get(1); + for (String key : json.keySet()) { + // key + writePrimitiveType(keyType, key, 0); + + // value + if (valueType.isPrimitive()) { + writePrimitiveType(valueType, json.get(key), 1); + } else { + writeGroupType(valueType, (JSON) json.get(key), 1); + } + } + + recordConsumer.endGroup(); + recordConsumer.endField("key_value", 0); + // key_value end + + recordConsumer.endGroup(); + recordConsumer.endField(groupType.getName(), index); + } + + private void writeList(GroupType groupType, JSON value, int index) { + if (value == null) { + return; + } + + JSONArray json = (JSONArray) value; + + if (json.isEmpty()) { + return; + } + + recordConsumer.startField(groupType.getName(), index); + // list + recordConsumer.startGroup(); + + // list start + recordConsumer.startField("list", 0); + recordConsumer.startGroup(); + + Type elementType = groupType.getFields().get(0).asGroupType().getFields().get(0); + + if (elementType.isPrimitive()) { + for (Object elementValue : json) { + writePrimitiveType(elementType, elementValue, 0); + } + } else { + for (Object elementValue : json) { + writeGroupType(elementType, (JSON) elementValue, 0); + } + } + + recordConsumer.endGroup(); + recordConsumer.endField("list", 0); + // list end + recordConsumer.endGroup(); + + recordConsumer.endField(groupType.getName(), index); + } + + private void writeStruct(GroupType groupType, JSON value, int index) { + if (value == null) { + return; + } + JSONObject json = (JSONObject) value; + if (json.isEmpty()) { + return; + } + + recordConsumer.startField(groupType.getName(), index); + // struct start + recordConsumer.startGroup(); + + writeFields(groupType.getFields(), json); + recordConsumer.endGroup(); + // struct end + recordConsumer.endField(groupType.getName(), index); + } + + private void writePrimitiveType(Type type, Object value, int index) { + if (value == null) { + return; + } + + recordConsumer.startField(type.getName(), index); + PrimitiveType primitiveType = type.asPrimitiveType(); + + switch (primitiveType.getPrimitiveTypeName()) { + case BOOLEAN: + recordConsumer.addBoolean((Boolean) value); + break; + case FLOAT: + if (value instanceof Float) { + recordConsumer.addFloat(((Float) value).floatValue()); + } else if (value instanceof Double) { + recordConsumer.addFloat(((Double) value).floatValue()); + } else if (value instanceof Long) { + recordConsumer.addFloat(((Long) value).floatValue()); + } else if (value instanceof Integer) { + recordConsumer.addFloat(((Integer) value).floatValue()); + } + break; + case DOUBLE: + if (value instanceof Float) { + recordConsumer.addDouble(((Float) value).doubleValue()); + } else if (value instanceof Double) { + recordConsumer.addDouble(((Double) value).doubleValue()); + } else if (value instanceof Long) { + recordConsumer.addDouble(((Long) value).doubleValue()); + } else if (value instanceof Integer) { + recordConsumer.addDouble(((Integer) value).doubleValue()); + } + break; + case INT32: + if (value instanceof Integer) { + recordConsumer.addInteger((Integer) value); + } else if (value instanceof Long) { + recordConsumer.addInteger(((Long) value).intValue()); + } else { + // 之前代码写的有问题,导致这里丢列了没抛异常,先收集,后续看看有没有任务命中在决定怎么改 + LimitLogger.limit("dirtyDataHiveWriterParquet", TimeUnit.MINUTES.toMillis(1), () -> LOGGER.warn("dirtyDataHiveWriterParquet {}", String.format("Invalid value: %s(clazz: %s) for field: %s", value, value.getClass(), type.getName()))); + } + break; + case INT64: + if (value instanceof Integer) { + recordConsumer.addLong(((Integer) value).longValue()); + } else if (value instanceof Long) { + recordConsumer.addInteger(((Long) value).intValue()); + } else { + // 之前代码写的有问题,导致这里丢列了没抛异常,先收集,后续看看有没有任务命中在决定怎么改 + LimitLogger.limit("dirtyDataHiveWriterParquet", TimeUnit.MINUTES.toMillis(1), () -> LOGGER.warn("dirtyDataHiveWriterParquet {}", String.format("Invalid value: %s(clazz: %s) for field: %s", value, value.getClass(), type.getName()))); + } + break; + case INT96: + if (value instanceof Integer) { + recordConsumer.addBinary(timestampColToBinary(new LongColumn((Integer) value))); + } else if (value instanceof Long) { + recordConsumer.addBinary(timestampColToBinary(new LongColumn((Long) value))); + } else if (value instanceof Timestamp) { + recordConsumer.addBinary(timestampColToBinary(new DateColumn((Timestamp) value))); + } else if (value instanceof Date) { + recordConsumer.addBinary(timestampColToBinary(new DateColumn((Date) value))); + } else { + recordConsumer.addBinary(timestampColToBinary(new StringColumn(value.toString()))); + } + break; + case FIXED_LEN_BYTE_ARRAY: + if (primitiveType.getDecimalMetadata() != null) { + // decimal + Column column; + if (value instanceof Integer) { + column = new LongColumn((Integer) value); + } else if (value instanceof Long) { + column = new LongColumn((Long) value); + } else if (value instanceof Double) { + column = new DoubleColumn((Double) value); + } else if (value instanceof BigDecimal) { + column = new DoubleColumn((BigDecimal) value); + } else { + column = new StringColumn(value.toString()); + } + recordConsumer.addBinary(decimalToBinary(column, primitiveType.getDecimalMetadata().getPrecision(), primitiveType.getDecimalMetadata().getScale())); + break; + } + /* fall through */ + case BINARY: + default: + recordConsumer.addBinary(Binary.fromString((String) value)); + break; + } + recordConsumer.endField(type.getName(), index); + } + + private void writePrimitiveType(Type type, Column value, int index) { + if (value == null || value.getRawData() == null) { + return; + } + + recordConsumer.startField(type.getName(), index); + PrimitiveType primitiveType = type.asPrimitiveType(); + switch (primitiveType.getPrimitiveTypeName()) { + case BOOLEAN: + recordConsumer.addBoolean(value.asBoolean()); + break; + case FLOAT: + recordConsumer.addFloat(value.asDouble().floatValue()); + break; + case DOUBLE: + recordConsumer.addDouble(value.asDouble()); + break; + case INT32: + OriginalType originalType = type.getOriginalType(); + if (OriginalType.DATE.equals(originalType)) { + int realVal = (int) (new java.sql.Date(value.asLong()).toLocalDate().toEpochDay()); + recordConsumer.addInteger(realVal); + } else { + recordConsumer.addInteger(value.asLong().intValue()); + } + break; + case INT64: + recordConsumer.addLong(value.asLong()); + break; + case INT96: + recordConsumer.addBinary(timestampColToBinary(value)); + break; + case BINARY: + String valueAsString2Write = null; + if (Column.Type.DATE == value.getType() && null != this.dateParse) { + valueAsString2Write = dateParse.format(value.asDate()); + } else { + valueAsString2Write = value.asString(); + } + recordConsumer.addBinary(Binary.fromString(valueAsString2Write)); + break; + case FIXED_LEN_BYTE_ARRAY: + if (primitiveType.getDecimalMetadata() != null) { + // decimal + recordConsumer.addBinary(decimalToBinary(value, primitiveType.getDecimalMetadata().getPrecision(), primitiveType.getDecimalMetadata().getScale())); + break; + } + /* fall through */ + default: + recordConsumer.addBinary(Binary.fromString(value.asString())); + break; + } + recordConsumer.endField(type.getName(), index); + } +} diff --git a/hologresjdbcwriter/doc/hologresjdbcwriter.md b/hologresjdbcwriter/doc/hologresjdbcwriter.md new file mode 100644 index 0000000000..8b163017be --- /dev/null +++ b/hologresjdbcwriter/doc/hologresjdbcwriter.md @@ -0,0 +1,204 @@ +# DataX HologresJdbcWriter + + +--- + + +## 1 快速介绍 + +HologresJdbcWriter 插件实现了写入数据到 Hologres目的表的功能。在底层实现上,HologresJdbcWriter通过JDBC连接远程 Hologres 数据库,并执行相应的 insert into ... on conflict sql 语句将数据写入 Hologres,内部会分批次提交入库。 + +
+ +* HologresJdbcWriter 只支持单表同步 + +## 2 实现原理 + +HologresJdbcWriter 通过 DataX 框架获取 Reader 生成的协议数据,根据你配置生成相应的SQL插入语句 + +* `insert into... on conflict ` + + +## 3 功能说明 + +### 3.1 配置样例 + +* 这里使用一份从内存产生到 HologresJdbcWriter导入的数据。 + +```json +{ + "job": { + "setting": { + "speed": { + "channel": 1 + } + }, + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "column" : [ + { + "value": "DataX", + "type": "string" + }, + { + "value": 19880808, + "type": "long" + }, + { + "value": "1988-08-08 08:08:08", + "type": "date" + }, + { + "value": true, + "type": "bool" + }, + { + "value": "test", + "type": "bytes" + } + ], + "sliceRecordCount": 1000 + } + }, + "writer": { + "name": "hologresjdbcwriter", + "parameter": { + "username": "xx", + "password": "xx", + "column": [ + "id", + "name" + ], + "preSql": [ + "delete from test" + ], + "connection": [ + { + "jdbcUrl": "jdbc:postgresql://127.0.0.1:3002/datax", + "table": [ + "test" + ] + } + ], + "writeMode" : "REPLACE", + "client" : { + "writeThreadSize" : 3 + } + } + } + } + ] + } +} + +``` + + +### 3.2 参数说明 + +* **jdbcUrl** + + * 描述:目的数据库的 JDBC 连接信息 ,jdbcUrl必须包含在connection配置单元中。 + + 注意:1、在一个数据库上只能配置一个值。 + 2、jdbcUrl按照PostgreSQL官方规范,并可以填写连接附加参数信息。具体请参看PostgreSQL官方文档或者咨询对应 DBA。 + + + * 必选:是
+ + * 默认值:无
+ +* **username** + + * 描述:目的数据库的用户名
+ + * 必选:是
+ + * 默认值:无
+ +* **password** + + * 描述:目的数据库的密码
+ + * 必选:是
+ + * 默认值:无
+ +* **table** + + * 描述:目的表的表名称。只支持写入一个表。 + + 注意:table 和 jdbcUrl 必须包含在 connection 配置单元中 + + * 必选:是
+ + * 默认值:无
+ +* **column** + + * 描述:目的表需要写入数据的字段,字段之间用英文逗号分隔。例如: "column": ["id","name","age"]。如果要依次写入全部列,使用\*表示, 例如: "column": ["\*"] + + 注意:1、我们强烈不推荐你这样配置,因为当你目的表字段个数、类型等有改动时,你的任务可能运行不正确或者失败 + 2、此处 column 不能配置任何常量值 + + * 必选:是
+ + * 默认值:否
+ +* **preSql** + + * 描述:写入数据到目的表前,会先执行这里的标准语句。如果 Sql 中有你需要操作到的表名称,请使用 `@table` 表示,这样在实际执行 Sql 语句时,会对变量按照实际表名称进行替换。
+ + * 必选:否
+ + * 默认值:无
+ +* **postSql** + + * 描述:写入数据到目的表后,会执行这里的标准语句。(原理同 preSql )
+ + * 必选:否
+ + * 默认值:无
+ +* **batchSize** + + * 描述:一次性批量提交的记录数大小,该值可以极大减少DataX与HologresJdbcWriter的网络交互次数,并提升整体吞吐量。但是该值设置过大可能会造成DataX运行进程OOM情况。
+ + * 必选:否
+ + * 默认值:512
+ +* **writeMode** + + * 描述:当写入hologres有主键表时,控制主键冲突后的策略。REPLACE表示冲突后hologres表的所有字段都被覆盖(未在writer中配置的字段将填充null);UPDATE表示冲突后hologres表writer配置的字段将被覆盖;IGNORE表示冲突后丢弃新数据,不覆盖。
+ + * 必选:否
+ + * 默认值:REPLACE
+ +* **client.writeThreadSize** + + * 描述:写入hologres的连接池大小,多个连接将并行写入数据。
+ + * 必选:否
+ + * 默认值:1
+ +### 3.3 类型转换 + +目前 HologresJdbcWriter支持大部分 Hologres类型,但也存在部分没有支持的情况,请注意检查你的类型。 + +下面列出 HologresJdbcWriter针对 Hologres类型转换列表: + +| DataX 内部类型| Hologres 数据类型 | +| -------- | ----- | +| Long |bigint, integer, smallint | +| Double |double precision, money, numeric, real | +| String |varchar, char, text, bit| +| Date |date, time, timestamp | +| Boolean |bool| +| Bytes |bytea| diff --git a/hologresjdbcwriter/pom.xml b/hologresjdbcwriter/pom.xml new file mode 100644 index 0000000000..a908dfed86 --- /dev/null +++ b/hologresjdbcwriter/pom.xml @@ -0,0 +1,90 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + + hologresjdbcwriter + hologresjdbcwriter + jar + writer data into hologres using jdbc + + + 1.8 + + + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + + org.slf4j + slf4j-api + + + + ch.qos.logback + logback-classic + + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + + com.alibaba.hologres + holo-client + 2.1.0 + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + \ No newline at end of file diff --git a/hologresjdbcwriter/src/main/assembly/package.xml b/hologresjdbcwriter/src/main/assembly/package.xml new file mode 100755 index 0000000000..db8100e16f --- /dev/null +++ b/hologresjdbcwriter/src/main/assembly/package.xml @@ -0,0 +1,35 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/writer/hologresjdbcwriter + + + target/ + + hologresjdbcwriter-0.0.1-SNAPSHOT.jar + + plugin/writer/hologresjdbcwriter + + + + + + false + plugin/writer/hologresjdbcwriter/libs + runtime + + + diff --git a/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/BaseWriter.java b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/BaseWriter.java new file mode 100644 index 0000000000..03349e3768 --- /dev/null +++ b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/BaseWriter.java @@ -0,0 +1,528 @@ +package com.alibaba.datax.plugin.writer.hologresjdbcwriter; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.DateColumn; +import com.alibaba.datax.common.element.LongColumn; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.RetryUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.writer.hologresjdbcwriter.util.ConfLoader; +import com.alibaba.datax.plugin.writer.hologresjdbcwriter.util.OriginalConfPretreatmentUtil; +import com.alibaba.datax.plugin.writer.hologresjdbcwriter.util.WriterUtil; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.hologres.client.HoloClient; +import com.alibaba.hologres.client.HoloConfig; +import com.alibaba.hologres.client.Put; +import com.alibaba.hologres.client.exception.HoloClientWithDetailsException; +import com.alibaba.hologres.client.model.TableSchema; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Time; +import java.sql.Timestamp; +import java.sql.Types; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class BaseWriter { + + protected static final Set ignoreConfList; + + static { + ignoreConfList = new HashSet<>(); + ignoreConfList.add("jdbcUrl"); + ignoreConfList.add("username"); + ignoreConfList.add("password"); + ignoreConfList.add("writeMode"); + } + + enum WriteMode { + IGNORE, + UPDATE, + REPLACE + } + + private static WriteMode getWriteMode(String text) { + text = text.toUpperCase(); + switch (text) { + case "IGNORE": + return WriteMode.IGNORE; + case "UPDATE": + return WriteMode.UPDATE; + case "REPLACE": + return WriteMode.REPLACE; + default: + throw DataXException.asDataXException(DBUtilErrorCode.ILLEGAL_VALUE, "writeMode只支持IGNORE,UPDATE,REPLACE,无法识别 " + text); + } + } + + public static class Job { + private DataBaseType dataBaseType; + + private static final Logger LOG = LoggerFactory + .getLogger(BaseWriter.Job.class); + + public Job(DataBaseType dataBaseType) { + this.dataBaseType = dataBaseType; + OriginalConfPretreatmentUtil.DATABASE_TYPE = this.dataBaseType; + } + + public void init(Configuration originalConfig) { + OriginalConfPretreatmentUtil.doPretreatment(originalConfig, this.dataBaseType); + checkConf(originalConfig); + LOG.debug("After job init(), originalConfig now is:[\n{}\n]", + originalConfig.toJSON()); + } + + private void checkConf(Configuration originalConfig) { + getWriteMode(originalConfig.getString(Key.WRITE_MODE, "REPLACE")); + List userConfiguredColumns = originalConfig.getList(Key.COLUMN, String.class); + List conns = originalConfig.getList(Constant.CONN_MARK, + JSONObject.class); + if (conns.size() > 1) { + throw DataXException.asDataXException(DBUtilErrorCode.ILLEGAL_VALUE, "只支持单表同步"); + } + int tableNumber = originalConfig.getInt(Constant.TABLE_NUMBER_MARK); + if (tableNumber > 1) { + throw DataXException.asDataXException(DBUtilErrorCode.ILLEGAL_VALUE, "只支持单表同步"); + } + JSONObject connConf = conns.get(0); + String jdbcUrl = connConf.getString(Key.JDBC_URL); + String username = originalConfig.getString(Key.USERNAME); + String password = originalConfig.getString(Key.PASSWORD); + + String table = connConf.getJSONArray(Key.TABLE).getString(0); + + Map clientConf = originalConfig.getMap("client"); + + HoloConfig config = new HoloConfig(); + config.setJdbcUrl(jdbcUrl); + config.setUsername(username); + config.setPassword(password); + if (clientConf != null) { + try { + config = ConfLoader.load(clientConf, config, ignoreConfList); + } catch (Exception e) { + throw DataXException + .asDataXException( + DBUtilErrorCode.CONF_ERROR, + "配置解析失败."); + } + } + + try (HoloClient client = new HoloClient(config)) { + TableSchema schema = client.getTableSchema(table); + LOG.info("table {} column info:", schema.getTableNameObj().getFullName()); + for (com.alibaba.hologres.client.model.Column column : schema.getColumnSchema()) { + LOG.info("name:{},type:{},typeName:{},nullable:{},defaultValue:{}", column.getName(), column.getType(), column.getTypeName(), column.getAllowNull(), column.getDefaultValue()); + } + for (String userColumn : userConfiguredColumns) { + if (schema.getColumnIndex(userColumn) == null) { + throw DataXException.asDataXException(DBUtilErrorCode.CONF_ERROR, "配置的列 " + userColumn + " 不存在"); + } + } + } catch (Exception e) { + throw DataXException.asDataXException(DBUtilErrorCode.CONN_DB_ERROR, "获取表schema失败", e); + } + + } + + // 一般来说,是需要推迟到 task 中进行pre 的执行(单表情况例外) + public void prepare(Configuration originalConfig) { + + try { + String username = originalConfig.getString(Key.USERNAME); + String password = originalConfig.getString(Key.PASSWORD); + + List conns = originalConfig.getList(Constant.CONN_MARK, + Object.class); + Configuration connConf = Configuration.from(conns.get(0) + .toString()); + + String jdbcUrl = connConf.getString(Key.JDBC_URL); + originalConfig.set(Key.JDBC_URL, jdbcUrl); + + String table = connConf.getList(Key.TABLE, String.class).get(0); + originalConfig.set(Key.TABLE, table); + + List preSqls = originalConfig.getList(Key.PRE_SQL, + String.class); + List renderedPreSqls = WriterUtil.renderPreOrPostSqls( + preSqls, table); + + originalConfig.remove(Constant.CONN_MARK); + if (null != renderedPreSqls && !renderedPreSqls.isEmpty()) { + // 说明有 preSql 配置,则此处删除掉 + originalConfig.remove(Key.PRE_SQL); + String tempJdbcUrl = jdbcUrl.replace("jdbc:postgresql://", "jdbc:hologres://"); + try (Connection conn = DriverManager.getConnection( + tempJdbcUrl, username, password)) { + LOG.info("Begin to execute preSqls:[{}]. context info:{}.", + StringUtils.join(renderedPreSqls, ";"), tempJdbcUrl); + + WriterUtil.executeSqls(conn, renderedPreSqls, tempJdbcUrl, dataBaseType); + } + } + LOG.debug("After job prepare(), originalConfig now is:[\n{}\n]", + originalConfig.toJSON()); + } catch (SQLException e) { + throw DataXException.asDataXException(DBUtilErrorCode.SQL_EXECUTE_FAIL, e); + } + } + + public List split(Configuration originalConfig, + int mandatoryNumber) { + return WriterUtil.doSplit(originalConfig, mandatoryNumber); + } + + // 一般来说,是需要推迟到 task 中进行post 的执行(单表情况例外) + public void post(Configuration originalConfig) { + + try { + String username = originalConfig.getString(Key.USERNAME); + String password = originalConfig.getString(Key.PASSWORD); + + String jdbcUrl = originalConfig.getString(Key.JDBC_URL); + + String table = originalConfig.getString(Key.TABLE); + + List postSqls = originalConfig.getList(Key.POST_SQL, + String.class); + List renderedPostSqls = WriterUtil.renderPreOrPostSqls( + postSqls, table); + + if (null != renderedPostSqls && !renderedPostSqls.isEmpty()) { + // 说明有 postSql 配置,则此处删除掉 + originalConfig.remove(Key.POST_SQL); + String tempJdbcUrl = jdbcUrl.replace("jdbc:postgresql://", "jdbc:hologres://"); + try (Connection conn = DriverManager.getConnection( + tempJdbcUrl, username, password)) { + LOG.info( + "Begin to execute postSqls:[{}]. context info:{}.", + StringUtils.join(renderedPostSqls, ";"), tempJdbcUrl); + WriterUtil.executeSqls(conn, renderedPostSqls, tempJdbcUrl, dataBaseType); + } + } + } catch (SQLException e) { + throw DataXException.asDataXException(DBUtilErrorCode.SQL_EXECUTE_FAIL, e); + } + } + + public void destroy(Configuration originalConfig) { + } + + } + + public static class Task { + protected static final Logger LOG = LoggerFactory + .getLogger(BaseWriter.Task.class); + + protected DataBaseType dataBaseType; + + protected String username; + protected String password; + protected String jdbcUrl; + protected String table; + protected List columns; + protected int batchSize; + protected int batchByteSize; + protected int columnNumber = 0; + protected TaskPluginCollector taskPluginCollector; + + // 作为日志显示信息时,需要附带的通用信息。比如信息所对应的数据库连接等信息,针对哪个表做的操作 + protected static String BASIC_MESSAGE; + + protected WriteMode writeMode; + protected String arrayDelimiter; + protected boolean emptyAsNull; + + protected HoloConfig config; + + public Task(DataBaseType dataBaseType) { + this.dataBaseType = dataBaseType; + } + + public void init(Configuration writerSliceConfig) { + this.username = writerSliceConfig.getString(Key.USERNAME); + this.password = writerSliceConfig.getString(Key.PASSWORD); + this.jdbcUrl = writerSliceConfig.getString(Key.JDBC_URL); + this.table = writerSliceConfig.getString(Key.TABLE); + + this.columns = writerSliceConfig.getList(Key.COLUMN, String.class); + this.columnNumber = this.columns.size(); + + this.arrayDelimiter = writerSliceConfig.getString(Key.Array_Delimiter); + + this.batchSize = writerSliceConfig.getInt(Key.BATCH_SIZE, Constant.DEFAULT_BATCH_SIZE); + this.batchByteSize = writerSliceConfig.getInt(Key.BATCH_BYTE_SIZE, Constant.DEFAULT_BATCH_BYTE_SIZE); + + writeMode = getWriteMode(writerSliceConfig.getString(Key.WRITE_MODE, "REPLACE")); + emptyAsNull = writerSliceConfig.getBool(Key.EMPTY_AS_NULL, true); + + Map clientConf = writerSliceConfig.getMap("client"); + + config = new HoloConfig(); + config.setJdbcUrl(this.jdbcUrl); + config.setUsername(username); + config.setPassword(password); + config.setWriteMode(writeMode == WriteMode.IGNORE ? com.alibaba.hologres.client.model.WriteMode.INSERT_OR_IGNORE : (writeMode == WriteMode.UPDATE ? com.alibaba.hologres.client.model.WriteMode.INSERT_OR_UPDATE : com.alibaba.hologres.client.model.WriteMode.INSERT_OR_REPLACE)); + config.setWriteBatchSize(this.batchSize); + config.setWriteBatchTotalByteSize(this.batchByteSize); + config.setMetaCacheTTL(3600000L); + config.setEnableDefaultForNotNullColumn(false); + config.setRetryCount(5); + config.setAppName("datax"); + + if (clientConf != null) { + try { + config = ConfLoader.load(clientConf, config, ignoreConfList); + } catch (Exception e) { + throw DataXException + .asDataXException( + DBUtilErrorCode.CONF_ERROR, + "配置解析失败."); + } + } + + BASIC_MESSAGE = String.format("jdbcUrl:[%s], table:[%s]", + this.jdbcUrl, this.table); + } + + public void prepare(Configuration writerSliceConfig) { + + } + + public void startWriteWithConnection(RecordReceiver recordReceiver, TaskPluginCollector taskPluginCollector) { + this.taskPluginCollector = taskPluginCollector; + + try (HoloClient client = new HoloClient(config)) { + Record record; + TableSchema schema = RetryUtil.executeWithRetry(() -> client.getTableSchema(this.table), 3, 5000L, true); + while ((record = recordReceiver.getFromReader()) != null) { + if (record.getColumnNumber() != this.columnNumber) { + // 源头读取字段列数与目的表字段写入列数不相等,直接报错 + throw DataXException + .asDataXException( + DBUtilErrorCode.CONF_ERROR, + String.format( + "列配置信息有错误. 因为您配置的任务中,源头读取字段数:%s 与 目的表要写入的字段数:%s 不相等. 请检查您的配置并作出修改.", + record.getColumnNumber(), + this.columnNumber)); + } + Put put = convertToPut(record, schema); + if (null != put) { + try { + client.put(put); + } catch (HoloClientWithDetailsException detail) { + handleDirtyData(detail); + } + } + } + try { + client.flush(); + } catch (HoloClientWithDetailsException detail) { + handleDirtyData(detail); + } + } catch (Exception e) { + throw DataXException.asDataXException( + DBUtilErrorCode.WRITE_DATA_ERROR, e); + } + } + + private void handleDirtyData(HoloClientWithDetailsException detail) { + for (int i = 0; i < detail.size(); ++i) { + com.alibaba.hologres.client.model.Record failRecord = detail.getFailRecord(i); + if (failRecord.getAttachmentList() != null) { + for (Object obj : failRecord.getAttachmentList()) { + taskPluginCollector.collectDirtyRecord((Record) obj, detail.getException(i)); + } + } + } + } + + public void startWrite(RecordReceiver recordReceiver, + TaskPluginCollector taskPluginCollector) { + startWriteWithConnection(recordReceiver, taskPluginCollector); + } + + public void post(Configuration writerSliceConfig) { + + } + + public void destroy(Configuration writerSliceConfig) { + } + + // 直接使用了两个类变量:columnNumber,resultSetMetaData + protected Put convertToPut(Record record, TableSchema schema) { + try { + Put put = new Put(schema); + put.getRecord().addAttachment(record); + for (int i = 0; i < this.columnNumber; i++) { + fillColumn(put, schema, schema.getColumnIndex(this.columns.get(i)), record.getColumn(i)); + } + return put; + } catch (Exception e) { + taskPluginCollector.collectDirtyRecord(record, e); + return null; + } + + } + + protected void fillColumn(Put data, TableSchema schema, int index, Column column) throws SQLException { + com.alibaba.hologres.client.model.Column holoColumn = schema.getColumn(index); + switch (holoColumn.getType()) { + case Types.CHAR: + case Types.NCHAR: + case Types.CLOB: + case Types.NCLOB: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.NVARCHAR: + case Types.LONGNVARCHAR: + String value = column.asString(); + if (emptyAsNull && value != null && value.length() == 0) { + data.setObject(index, null); + } else { + data.setObject(index, value); + } + break; + + case Types.SMALLINT: + if (column.getByteSize() > 0) { + data.setObject(index, column.asBigInteger().shortValue()); + } else if (emptyAsNull) { + data.setObject(index, null); + } + break; + case Types.INTEGER: + if (column.getByteSize() > 0) { + data.setObject(index, column.asBigInteger().intValue()); + } else if (emptyAsNull) { + data.setObject(index, null); + } + break; + case Types.BIGINT: + if (column.getByteSize() > 0) { + data.setObject(index, column.asBigInteger().longValue()); + } else if (emptyAsNull) { + data.setObject(index, null); + } + break; + case Types.NUMERIC: + case Types.DECIMAL: + if (column.getByteSize() > 0) { + data.setObject(index, column.asBigDecimal()); + } else if (emptyAsNull) { + data.setObject(index, null); + } + break; + case Types.FLOAT: + case Types.REAL: + if (column.getByteSize() > 0) { + data.setObject(index, column.asBigDecimal().floatValue()); + } else if (emptyAsNull) { + data.setObject(index, null); + } + break; + case Types.DOUBLE: + if (column.getByteSize() > 0) { + data.setObject(index, column.asDouble()); + } else if (emptyAsNull) { + data.setObject(index, null); + } + break; + case Types.TIME: + if (column.getByteSize() > 0) { + if (column instanceof LongColumn || column instanceof DateColumn) { + data.setObject(index, new Time(column.asLong())); + } else { + data.setObject(index, column.asString()); + } + } else if (emptyAsNull) { + data.setObject(index, null); + } + break; + case Types.DATE: + if (column.getByteSize() > 0) { + if (column instanceof LongColumn || column instanceof DateColumn) { + data.setObject(index, column.asLong()); + } else { + data.setObject(index, column.asString()); + } + } else if (emptyAsNull) { + data.setObject(index, null); + } + break; + case Types.TIMESTAMP: + if (column.getByteSize() > 0) { + if (column instanceof LongColumn || column instanceof DateColumn) { + data.setObject(index, new Timestamp(column.asLong())); + } else { + data.setObject(index, column.asString()); + } + } else if (emptyAsNull) { + data.setObject(index, null); + } + break; + + case Types.BINARY: + case Types.VARBINARY: + case Types.BLOB: + case Types.LONGVARBINARY: + String byteValue = column.asString(); + if (null != byteValue) { + data.setObject(index, column + .asBytes()); + } + break; + case Types.BOOLEAN: + case Types.BIT: + if (column.getByteSize() == 0) { + break; + } + try { + Boolean boolValue = column.asBoolean(); + data.setObject(index, boolValue); + } catch (Exception e) { + data.setObject(index, !"0".equals(column.asString())); + } + break; + case Types.ARRAY: + String arrayString = column.asString(); + Object arrayObject = null; + if (null == arrayString || (emptyAsNull && "".equals(arrayString))) { + data.setObject(index, null); + break; + } else if (arrayDelimiter != null && arrayDelimiter.length() > 0) { + arrayObject = arrayString.split(this.arrayDelimiter); + } else { + arrayObject = JSONArray.parseArray(arrayString); + } + data.setObject(index, arrayObject); + break; + default: + throw DataXException + .asDataXException( + DBUtilErrorCode.UNSUPPORTED_TYPE, + String.format( + "您的配置文件中的列配置信息有误. 因为DataX 不支持数据库写入这种字段类型. 字段名:[%s], 字段类型:[%d], 字段Java类型:[%s]. 请修改表中该字段的类型或者不同步该字段.", + holoColumn.getName(), + holoColumn.getType(), + holoColumn.getTypeName())); + } + } + } +} diff --git a/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/Constant.java b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/Constant.java new file mode 100755 index 0000000000..eb51d0267a --- /dev/null +++ b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/Constant.java @@ -0,0 +1,15 @@ +package com.alibaba.datax.plugin.writer.hologresjdbcwriter; + +/** + * 用于插件解析用户配置时,需要进行标识(MARK)的常量的声明. + */ +public final class Constant { + public static final int DEFAULT_BATCH_SIZE = 512; + + public static final int DEFAULT_BATCH_BYTE_SIZE = 50 * 1024 * 1024; + + public static String CONN_MARK = "connection"; + + public static String TABLE_NUMBER_MARK = "tableNumber"; + +} diff --git a/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/HologresJdbcWriter.java b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/HologresJdbcWriter.java new file mode 100755 index 0000000000..811a2e11b3 --- /dev/null +++ b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/HologresJdbcWriter.java @@ -0,0 +1,78 @@ +package com.alibaba.datax.plugin.writer.hologresjdbcwriter; + +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; + +import java.util.List; + +public class HologresJdbcWriter extends Writer { + private static final DataBaseType DATABASE_TYPE = DataBaseType.PostgreSQL; + + public static class Job extends Writer.Job { + private Configuration originalConfig = null; + private BaseWriter.Job baseWriterMaster; + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + this.baseWriterMaster = new BaseWriter.Job(DATABASE_TYPE); + this.baseWriterMaster.init(this.originalConfig); + } + + @Override + public void prepare() { + this.baseWriterMaster.prepare(this.originalConfig); + } + + @Override + public List split(int mandatoryNumber) { + return this.baseWriterMaster.split(this.originalConfig, mandatoryNumber); + } + + @Override + public void post() { + this.baseWriterMaster.post(this.originalConfig); + } + + @Override + public void destroy() { + this.baseWriterMaster.destroy(this.originalConfig); + } + + } + + public static class Task extends Writer.Task { + private Configuration writerSliceConfig; + private BaseWriter.Task baseWriterSlave; + + @Override + public void init() { + this.writerSliceConfig = super.getPluginJobConf(); + this.baseWriterSlave = new BaseWriter.Task(DATABASE_TYPE); + this.baseWriterSlave.init(this.writerSliceConfig); + } + + @Override + public void prepare() { + this.baseWriterSlave.prepare(this.writerSliceConfig); + } + + public void startWrite(RecordReceiver recordReceiver) { + this.baseWriterSlave.startWrite(recordReceiver, super.getTaskPluginCollector()); + } + + @Override + public void post() { + this.baseWriterSlave.post(this.writerSliceConfig); + } + + @Override + public void destroy() { + this.baseWriterSlave.destroy(this.writerSliceConfig); + } + + } + +} diff --git a/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/Key.java b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/Key.java new file mode 100755 index 0000000000..3bd5d1e2c5 --- /dev/null +++ b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/Key.java @@ -0,0 +1,31 @@ +package com.alibaba.datax.plugin.writer.hologresjdbcwriter; + +public final class Key { + public final static String JDBC_URL = "jdbcUrl"; + + public final static String USERNAME = "username"; + + public final static String PASSWORD = "password"; + + public final static String TABLE = "table"; + + public final static String COLUMN = "column"; + + public final static String Array_Delimiter = "arrayDelimiter"; + + public final static String WRITE_MODE = "writeMode"; + + public final static String PRE_SQL = "preSql"; + + public final static String POST_SQL = "postSql"; + + //默认值:256 + public final static String BATCH_SIZE = "batchSize"; + + //默认值:50m + public final static String BATCH_BYTE_SIZE = "batchByteSize"; + + public final static String EMPTY_AS_NULL = "emptyAsNull"; + + +} \ No newline at end of file diff --git a/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/util/ConfLoader.java b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/util/ConfLoader.java new file mode 100644 index 0000000000..48d7584e98 --- /dev/null +++ b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/util/ConfLoader.java @@ -0,0 +1,59 @@ +package com.alibaba.datax.plugin.writer.hologresjdbcwriter.util; + +import com.alibaba.hologres.client.model.WriteMode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.Set; + +public class ConfLoader { + public static Logger LOG = LoggerFactory.getLogger(ConfLoader.class); + + static public T load(Map props, T config, Set ignoreList) throws Exception { + Field[] fields = config.getClass().getDeclaredFields(); + for (Map.Entry entry : props.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue().toString(); + if (ignoreList.contains(key)) { + LOG.info("Config Skip {}", key); + continue; + } + boolean match = false; + for (Field field : fields) { + if (field.getName().equals(key)) { + match = true; + field.setAccessible(true); + Class type = field.getType(); + if (type.equals(String.class)) { + field.set(config, value); + } else if (type.equals(int.class)) { + field.set(config, Integer.parseInt(value)); + } else if (type.equals(long.class)) { + field.set(config, Long.parseLong(value)); + } else if (type.equals(boolean.class)) { + field.set(config, Boolean.parseBoolean(value)); + } else if (WriteMode.class.equals(type)) { + field.set(config, WriteMode.valueOf(value)); + } else { + throw new Exception("invalid type " + type + " for param " + key); + } + if ("password".equals(key)) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < value.length(); ++i) { + sb.append("*"); + } + LOG.info("Config {}={}", key, sb.toString()); + } else { + LOG.info("Config {}={}", key, value); + } + } + } + if (!match) { + throw new Exception("param " + key + " not found in HoloConfig"); + } + } + return config; + } +} diff --git a/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/util/OriginalConfPretreatmentUtil.java b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/util/OriginalConfPretreatmentUtil.java new file mode 100755 index 0000000000..70176b912d --- /dev/null +++ b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/util/OriginalConfPretreatmentUtil.java @@ -0,0 +1,82 @@ +package com.alibaba.datax.plugin.writer.hologresjdbcwriter.util; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.util.TableExpandUtil; +import com.alibaba.datax.plugin.writer.hologresjdbcwriter.Constant; +import com.alibaba.datax.plugin.writer.hologresjdbcwriter.Key; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public final class OriginalConfPretreatmentUtil { + private static final Logger LOG = LoggerFactory + .getLogger(OriginalConfPretreatmentUtil.class); + + public static DataBaseType DATABASE_TYPE; + + public static void doPretreatment(Configuration originalConfig, DataBaseType dataBaseType) { + // 检查 username/password 配置(必填) + originalConfig.getNecessaryValue(Key.USERNAME, DBUtilErrorCode.REQUIRED_VALUE); + originalConfig.getNecessaryValue(Key.PASSWORD, DBUtilErrorCode.REQUIRED_VALUE); + + doCheckBatchSize(originalConfig); + simplifyConf(originalConfig); + } + + public static void doCheckBatchSize(Configuration originalConfig) { + // 检查batchSize 配置(选填,如果未填写,则设置为默认值) + int batchSize = originalConfig.getInt(Key.BATCH_SIZE, Constant.DEFAULT_BATCH_SIZE); + if (batchSize < 1) { + throw DataXException.asDataXException(DBUtilErrorCode.ILLEGAL_VALUE, String.format( + "您的batchSize配置有误. 您所配置的写入数据库表的 batchSize:%s 不能小于1. 推荐配置范围为:[256-1024] (保持128的倍数), 该值越大, 内存溢出可能性越大. 请检查您的配置并作出修改.", + batchSize)); + } + + originalConfig.set(Key.BATCH_SIZE, batchSize); + } + + public static void simplifyConf(Configuration originalConfig) { + List connections = originalConfig.getList(Constant.CONN_MARK, + Object.class); + + int tableNum = 0; + + for (int i = 0, len = connections.size(); i < len; i++) { + Configuration connConf = Configuration.from(connections.get(i).toString()); + + String jdbcUrl = connConf.getString(Key.JDBC_URL); + if (StringUtils.isBlank(jdbcUrl)) { + throw DataXException.asDataXException(DBUtilErrorCode.REQUIRED_VALUE, "您未配置的写入数据库表的 jdbcUrl."); + } + + List tables = connConf.getList(Key.TABLE, String.class); + + if (null == tables || tables.isEmpty()) { + throw DataXException.asDataXException(DBUtilErrorCode.REQUIRED_VALUE, + "您未配置写入数据库表的表名称. 根据配置DataX找不到您配置的表. 请检查您的配置并作出修改."); + } + + // 对每一个connection 上配置的table 项进行解析 + List expandedTables = TableExpandUtil + .expandTableConf(DATABASE_TYPE, tables); + + if (null == expandedTables || expandedTables.isEmpty()) { + throw DataXException.asDataXException(DBUtilErrorCode.CONF_ERROR, + "您配置的写入数据库表名称错误. DataX找不到您配置的表,请检查您的配置并作出修改."); + } + + tableNum += expandedTables.size(); + + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, + i, Key.TABLE), expandedTables); + } + + originalConfig.set(Constant.TABLE_NUMBER_MARK, tableNum); + } + +} diff --git a/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/util/WriterUtil.java b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/util/WriterUtil.java new file mode 100755 index 0000000000..d35194e833 --- /dev/null +++ b/hologresjdbcwriter/src/main/java/com/alibaba/datax/plugin/writer/hologresjdbcwriter/util/WriterUtil.java @@ -0,0 +1,111 @@ +package com.alibaba.datax.plugin.writer.hologresjdbcwriter.util; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.util.RdbmsException; +import com.alibaba.datax.plugin.rdbms.writer.Constant; +import com.alibaba.datax.plugin.rdbms.writer.Key; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public final class WriterUtil { + private static final Logger LOG = LoggerFactory.getLogger(WriterUtil.class); + + //TODO 切分报错 + public static List doSplit(Configuration simplifiedConf, + int adviceNumber) { + + List splitResultConfigs = new ArrayList(); + + int tableNumber = simplifiedConf.getInt(Constant.TABLE_NUMBER_MARK); + + //处理单表的情况 + if (tableNumber == 1) { + //由于在之前的 master prepare 中已经把 table,jdbcUrl 提取出来,所以这里处理十分简单 + for (int j = 0; j < adviceNumber; j++) { + splitResultConfigs.add(simplifiedConf.clone()); + } + + return splitResultConfigs; + } + + if (tableNumber != adviceNumber) { + throw DataXException.asDataXException(DBUtilErrorCode.CONF_ERROR, + String.format("您的配置文件中的列配置信息有误. 您要写入的目的端的表个数是:%s , 但是根据系统建议需要切分的份数是:%s. 请检查您的配置并作出修改.", + tableNumber, adviceNumber)); + } + + String jdbcUrl; + List preSqls = simplifiedConf.getList(Key.PRE_SQL, String.class); + List postSqls = simplifiedConf.getList(Key.POST_SQL, String.class); + + List conns = simplifiedConf.getList(Constant.CONN_MARK, + Object.class); + + for (Object conn : conns) { + Configuration sliceConfig = simplifiedConf.clone(); + + Configuration connConf = Configuration.from(conn.toString()); + jdbcUrl = connConf.getString(Key.JDBC_URL); + sliceConfig.set(Key.JDBC_URL, jdbcUrl); + + sliceConfig.remove(Constant.CONN_MARK); + + List tables = connConf.getList(Key.TABLE, String.class); + + for (String table : tables) { + Configuration tempSlice = sliceConfig.clone(); + tempSlice.set(Key.TABLE, table); + tempSlice.set(Key.PRE_SQL, renderPreOrPostSqls(preSqls, table)); + tempSlice.set(Key.POST_SQL, renderPreOrPostSqls(postSqls, table)); + + splitResultConfigs.add(tempSlice); + } + + } + + return splitResultConfigs; + } + + public static List renderPreOrPostSqls(List preOrPostSqls, String tableName) { + if (null == preOrPostSqls) { + return Collections.emptyList(); + } + + List renderedSqls = new ArrayList(); + for (String sql : preOrPostSqls) { + //preSql为空时,不加入执行队列 + if (StringUtils.isNotBlank(sql)) { + renderedSqls.add(sql.replace(Constant.TABLE_NAME_PLACEHOLDER, tableName)); + } + } + + return renderedSqls; + } + + public static void executeSqls(Connection conn, List sqls, String basicMessage,DataBaseType dataBaseType) { + Statement stmt = null; + String currentSql = null; + try { + stmt = conn.createStatement(); + for (String sql : sqls) { + currentSql = sql; + DBUtil.executeSqlWithoutResultSet(stmt, sql); + } + } catch (Exception e) { + throw RdbmsException.asQueryException(dataBaseType,e,currentSql,null,null); + } finally { + DBUtil.closeDBResources(null, stmt, null); + } + } +} diff --git a/hologresjdbcwriter/src/main/resources/plugin.json b/hologresjdbcwriter/src/main/resources/plugin.json new file mode 100644 index 0000000000..a9f939965e --- /dev/null +++ b/hologresjdbcwriter/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "hologresjdbcwriter", + "class": "com.alibaba.datax.plugin.writer.hologresjdbcwriter.HologresJdbcWriter", + "description": "", + "developer": "alibaba" +} \ No newline at end of file diff --git a/hologresjdbcwriter/src/main/resources/plugin_job_template.json b/hologresjdbcwriter/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..f509ccc06b --- /dev/null +++ b/hologresjdbcwriter/src/main/resources/plugin_job_template.json @@ -0,0 +1,11 @@ +{ + "name": "hologresjdbcwriter", + "parameter": { + "url": "", + "username": "", + "password": "", + "database": "", + "table": "", + "partition": "" + } +} diff --git a/images/datax.logo.png b/images/datax.logo.png new file mode 100644 index 0000000000..d5b20350e8 Binary files /dev/null and b/images/datax.logo.png differ diff --git a/introduction.md b/introduction.md index b27607c7ee..d08ad98d77 100644 --- a/introduction.md +++ b/introduction.md @@ -36,6 +36,7 @@ DataX本身作为离线数据同步框架,采用Framework + plugin架构构建 | ------------ | ---------- | :-------: | :-------: |:-------: | | RDBMS 关系型数据库 | MySQL | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/mysqlreader/doc/mysqlreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/mysqlwriter/doc/mysqlwriter.md)| |             | Oracle     |     √     |     √     |[读](https://github.com/alibaba/DataX/blob/master/oraclereader/doc/oraclereader.md) 、[写](https://github.com/alibaba/DataX/blob/master/oraclewriter/doc/oraclewriter.md)| +|             | OceanBase  |     √     |     √     |[读](https://open.oceanbase.com/docs/community/oceanbase-database/V3.1.0/use-datax-to-full-migration-data-to-oceanbase) 、[写](https://open.oceanbase.com/docs/community/oceanbase-database/V3.1.0/use-datax-to-full-migration-data-to-oceanbase)| | | SQLServer | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/sqlserverreader/doc/sqlserverreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/sqlserverwriter/doc/sqlserverwriter.md)| | | PostgreSQL | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/postgresqlreader/doc/postgresqlreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/postgresqlwriter/doc/postgresqlwriter.md)| | | DRDS | √ | √ |[读](https://github.com/alibaba/DataX/blob/master/drdsreader/doc/drdsreader.md) 、[写](https://github.com/alibaba/DataX/blob/master/drdswriter/doc/drdswriter.md)| diff --git a/kuduwriter/pom.xml b/kuduwriter/pom.xml index 04b5ef535d..5d78be4cb7 100644 --- a/kuduwriter/pom.xml +++ b/kuduwriter/pom.xml @@ -30,7 +30,7 @@ junit junit - 4.13 + 4.13.1 test diff --git a/kuduwriter/src/main/java/com/q1/datax/plugin/writer/kudu11xwriter/Kudu11xHelper.java b/kuduwriter/src/main/java/com/q1/datax/plugin/writer/kudu11xwriter/Kudu11xHelper.java index cf1b0f8ff5..558693ff07 100644 --- a/kuduwriter/src/main/java/com/q1/datax/plugin/writer/kudu11xwriter/Kudu11xHelper.java +++ b/kuduwriter/src/main/java/com/q1/datax/plugin/writer/kudu11xwriter/Kudu11xHelper.java @@ -3,7 +3,7 @@ import com.alibaba.datax.common.element.Column; import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.Validate; import org.apache.kudu.ColumnSchema; diff --git a/kuduwriter/src/main/java/com/q1/datax/plugin/writer/kudu11xwriter/KuduWriterTask.java b/kuduwriter/src/main/java/com/q1/datax/plugin/writer/kudu11xwriter/KuduWriterTask.java index bff3509fbd..df87284240 100644 --- a/kuduwriter/src/main/java/com/q1/datax/plugin/writer/kudu11xwriter/KuduWriterTask.java +++ b/kuduwriter/src/main/java/com/q1/datax/plugin/writer/kudu11xwriter/KuduWriterTask.java @@ -134,7 +134,7 @@ public void startWriter(RecordReceiver lineReceiver, TaskPluginCollector taskPlu break; case BOOLEAN: synchronized (lock) { - row.addBoolean(name, Boolean.getBoolean(rawData)); + row.addBoolean(name, Boolean.parseBoolean(rawData)); } break; case STRING: diff --git a/license.txt b/license.txt index 00b845b43b..2f293c0fe7 100644 --- a/license.txt +++ b/license.txt @@ -1,4 +1,4 @@ -Copyright 1999-2017 Alibaba Group Holding Ltd. +Copyright 1999-2022 Alibaba Group Holding Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/loghubreader/pom.xml b/loghubreader/pom.xml new file mode 100644 index 0000000000..b2f52f3da1 --- /dev/null +++ b/loghubreader/pom.xml @@ -0,0 +1,73 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + + loghubreader + + 0.0.1-SNAPSHOT + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + com.aliyun.openservices + aliyun-log + 0.6.22 + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + diff --git a/loghubreader/src/main/assembly/package.xml b/loghubreader/src/main/assembly/package.xml new file mode 100644 index 0000000000..e1d8d73912 --- /dev/null +++ b/loghubreader/src/main/assembly/package.xml @@ -0,0 +1,34 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + + plugin/reader/loghubreader + + + target/ + + loghubreader-0.0.1-SNAPSHOT.jar + + plugin/reader/loghubreader + + + + + + false + plugin/reader/loghubreader/libs + runtime + + + diff --git a/loghubreader/src/main/java/com/alibaba/datax/plugin/reader/loghubreader/Constant.java b/loghubreader/src/main/java/com/alibaba/datax/plugin/reader/loghubreader/Constant.java new file mode 100644 index 0000000000..fd9e88dc28 --- /dev/null +++ b/loghubreader/src/main/java/com/alibaba/datax/plugin/reader/loghubreader/Constant.java @@ -0,0 +1,26 @@ +package com.alibaba.datax.plugin.reader.loghubreader; + +public class Constant { + + public static String DATETIME_FORMAT = "yyyyMMddHHmmss"; + public static String DATE_FORMAT = "yyyyMMdd"; + + static String META_COL_SOURCE = "__source__"; + static String META_COL_TOPIC = "__topic__"; + static String META_COL_CATEGORY = "__category__"; + static String META_COL_MACHINEUUID = "__machineUUID__"; + static String META_COL_HOSTNAME = "__hostname__"; + static String META_COL_PATH = "__path__"; + static String META_COL_LOGTIME = "__logtime__"; + public static String META_COL_RECEIVE_TIME = "__receive_time__"; + + /** + * 除用户手动配置的列之外,其余数据列作为一个 json 读取到一列 + */ + static String COL_EXTRACT_OTHERS = "C__extract_others__"; + + /** + * 将所有元数据列作为一个 json 读取到一列 + */ + static String COL_EXTRACT_ALL_META = "C__extract_all_meta__"; +} diff --git a/loghubreader/src/main/java/com/alibaba/datax/plugin/reader/loghubreader/Key.java b/loghubreader/src/main/java/com/alibaba/datax/plugin/reader/loghubreader/Key.java new file mode 100644 index 0000000000..9067cc68fc --- /dev/null +++ b/loghubreader/src/main/java/com/alibaba/datax/plugin/reader/loghubreader/Key.java @@ -0,0 +1,38 @@ +package com.alibaba.datax.plugin.reader.loghubreader; + +public final class Key { + + /** + * 此处声明插件用到的需要插件使用者提供的配置项 + */ + public static final String ENDPOINT = "endpoint"; + + public static final String ACCESSKEYID = "accessId"; + + public static final String ACCESSKEYSECRET = "accessKey"; + + public static final String PROJECT = "project"; + + public static final String LOGSTORE = "logstore"; + + public static final String TOPIC = "topic"; + + public static final String COLUMN = "column"; + + public static final String BATCHSIZE = "batchSize"; + + public static final String BEGINTIMESTAMPMILLIS = "beginTimestampMillis"; + + public static final String ENDTIMESTAMPMILLIS = "endTimestampMillis"; + + public static final String BEGINDATETIME = "beginDateTime"; + + public static final String ENDDATETIME = "endDateTime"; + + public static final String TIMEFORMAT = "timeformat"; + + public static final String SOURCE = "source"; + + public static final String SHARD = "shard"; + +} diff --git a/loghubreader/src/main/java/com/alibaba/datax/plugin/reader/loghubreader/LogHubReader.java b/loghubreader/src/main/java/com/alibaba/datax/plugin/reader/loghubreader/LogHubReader.java new file mode 100644 index 0000000000..c52ef62d81 --- /dev/null +++ b/loghubreader/src/main/java/com/alibaba/datax/plugin/reader/loghubreader/LogHubReader.java @@ -0,0 +1,482 @@ +package com.alibaba.datax.plugin.reader.loghubreader; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.element.StringColumn; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.spi.Reader; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.DataXCaseEnvUtil; +import com.alibaba.datax.common.util.RetryUtil; +import com.alibaba.fastjson2.JSONObject; +import com.aliyun.openservices.log.Client; +import com.aliyun.openservices.log.common.Consts.CursorMode; +import com.aliyun.openservices.log.common.*; +import com.aliyun.openservices.log.exception.LogException; +import com.aliyun.openservices.log.response.BatchGetLogResponse; +import com.aliyun.openservices.log.response.GetCursorResponse; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.concurrent.Callable; + +public class LogHubReader extends Reader { + public static class Job extends Reader.Job { + + private static final Logger LOG = LoggerFactory.getLogger(Job.class); + + private Client client; + private Configuration originalConfig; + + private Long beginTimestampMillis; + private Long endTimestampMillis; + + @Override + public void init() { + LOG.info("loghub reader job init begin ..."); + this.originalConfig = super.getPluginJobConf(); + validateParameter(originalConfig); + + String endPoint = this.originalConfig.getString(Key.ENDPOINT); + String accessKeyId = this.originalConfig.getString(Key.ACCESSKEYID); + String accessKeySecret = this.originalConfig.getString(Key.ACCESSKEYSECRET); + + client = new Client(endPoint, accessKeyId, accessKeySecret); + LOG.info("loghub reader job init end."); + } + + private void validateParameter(Configuration conf){ + conf.getNecessaryValue(Key.ENDPOINT,LogHubReaderErrorCode.REQUIRE_VALUE); + conf.getNecessaryValue(Key.ACCESSKEYID,LogHubReaderErrorCode.REQUIRE_VALUE); + conf.getNecessaryValue(Key.ACCESSKEYSECRET,LogHubReaderErrorCode.REQUIRE_VALUE); + conf.getNecessaryValue(Key.PROJECT,LogHubReaderErrorCode.REQUIRE_VALUE); + conf.getNecessaryValue(Key.LOGSTORE,LogHubReaderErrorCode.REQUIRE_VALUE); + conf.getNecessaryValue(Key.COLUMN,LogHubReaderErrorCode.REQUIRE_VALUE); + + int batchSize = this.originalConfig.getInt(Key.BATCHSIZE); + if (batchSize > 1000) { + throw DataXException.asDataXException(LogHubReaderErrorCode.BAD_CONFIG_VALUE, + "Invalid batchSize[" + batchSize + "] value (0,1000]!"); + } + + beginTimestampMillis = this.originalConfig.getLong(Key.BEGINTIMESTAMPMILLIS); + String beginDateTime = this.originalConfig.getString(Key.BEGINDATETIME); + + if (beginDateTime != null) { + try { + beginTimestampMillis = getUnixTimeFromDateTime(beginDateTime); + } catch (ParseException e) { + throw DataXException.asDataXException(LogHubReaderErrorCode.BAD_CONFIG_VALUE, + "Invalid beginDateTime[" + beginDateTime + "], format [yyyyMMddHHmmss or yyyyMMdd]!"); + } + } + + if (beginTimestampMillis != null && beginTimestampMillis <= 0) { + throw DataXException.asDataXException(LogHubReaderErrorCode.BAD_CONFIG_VALUE, + "Invalid beginTimestampMillis[" + beginTimestampMillis + "]!"); + } + + endTimestampMillis = this.originalConfig.getLong(Key.ENDTIMESTAMPMILLIS); + String endDateTime = this.originalConfig.getString(Key.ENDDATETIME); + + if (endDateTime != null) { + try { + endTimestampMillis = getUnixTimeFromDateTime(endDateTime); + } catch (ParseException e) { + throw DataXException.asDataXException(LogHubReaderErrorCode.BAD_CONFIG_VALUE, + "Invalid beginDateTime[" + endDateTime + "], format [yyyyMMddHHmmss or yyyyMMdd]!"); + } + } + + if (endTimestampMillis != null && endTimestampMillis <= 0) { + throw DataXException.asDataXException(LogHubReaderErrorCode.BAD_CONFIG_VALUE, + "Invalid endTimestampMillis[" + endTimestampMillis + "]!"); + } + + if (beginTimestampMillis != null && endTimestampMillis != null + && endTimestampMillis <= beginTimestampMillis) { + throw DataXException.asDataXException(LogHubReaderErrorCode.BAD_CONFIG_VALUE, + "endTimestampMillis[" + endTimestampMillis + "] must bigger than beginTimestampMillis[" + beginTimestampMillis + "]!"); + } + } + + private long getUnixTimeFromDateTime(String dateTime) throws ParseException { + try { + String format = Constant.DATETIME_FORMAT; + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format); + return simpleDateFormat.parse(dateTime).getTime() / 1000; + } catch (ParseException ignored) { + throw DataXException.asDataXException(LogHubReaderErrorCode.BAD_CONFIG_VALUE, + "Invalid DateTime[" + dateTime + "]!"); + } + } + + @Override + public void prepare() { + } + + @Override + public List split(int adviceNumber) { + LOG.info("split() begin..."); + + List readerSplitConfigs = new ArrayList(); + + final String project = this.originalConfig.getString(Key.PROJECT); + final String logstore = this.originalConfig.getString(Key.LOGSTORE); + + List logStore = null; + try { + logStore = RetryUtil.executeWithRetry(new Callable>() { + @Override + public List call() throws Exception { + return client.ListShard(project, logstore).GetShards(); + } + }, DataXCaseEnvUtil.getRetryTimes(7), DataXCaseEnvUtil.getRetryInterval(1000L), DataXCaseEnvUtil.getRetryExponential(true)); + } catch (Exception e) { + throw DataXException.asDataXException(LogHubReaderErrorCode.BAD_CONFIG_VALUE, + "get LogStore[" + logstore + "] error, please check ! detail error messsage: " + e.toString()); + } + + if (logStore == null) { + throw DataXException.asDataXException(LogHubReaderErrorCode.BAD_CONFIG_VALUE, + "LogStore[" + logstore + "] isn't exists, please check !"); + } + + int splitNumber = logStore.size(); + if (0 == splitNumber) { + throw DataXException.asDataXException(LogHubReaderErrorCode.EMPTY_LOGSTORE_VALUE, + "LogStore[" + logstore + "] has 0 shard, please check !"); + } + + Collections.shuffle(logStore); + for (int i = 0; i < logStore.size(); i++) { + if (beginTimestampMillis != null && endTimestampMillis != null) { + try { + String beginCursor = getCursorWithRetry(client, project, logstore, logStore.get(i).GetShardId(), beginTimestampMillis).GetCursor(); + String endCursor = getCursorWithRetry(client, project, logstore, logStore.get(i).GetShardId(), endTimestampMillis).GetCursor(); + if (beginCursor.equals(endCursor)) { + if ((i == logStore.size() - 1) && (readerSplitConfigs.size() == 0)) { + + } else { + LOG.info("skip empty shard[" + logStore.get(i) + "]!"); + continue; + } + } + } catch (Exception e) { + LOG.error("Check Shard[" + logStore.get(i) + "] Error, please check !" + e.toString()); + throw DataXException.asDataXException(LogHubReaderErrorCode.LOG_HUB_ERROR, e); + } + } + Configuration splitedConfig = this.originalConfig.clone(); + splitedConfig.set(Key.SHARD, logStore.get(i).GetShardId()); + readerSplitConfigs.add(splitedConfig); + } + + if (splitNumber < adviceNumber) { + // LOG.info(MESSAGE_SOURCE.message("hdfsreader.12", + // splitNumber, adviceNumber, splitNumber, splitNumber)); + } + LOG.info("split() ok and end..."); + + return readerSplitConfigs; + } + + @Override + public void post() { + } + + @Override + public void destroy() { + } + + private GetCursorResponse getCursorWithRetry(final Client client, final String project, final String logstore, final int shard, final long fromTime) throws Exception { + return + RetryUtil.executeWithRetry(new Callable() { + @Override + public GetCursorResponse call() throws Exception { + LOG.info("loghug get cursor with project: {} logstore: {} shard: {} time: {}", project, logstore, shard, fromTime); + return client.GetCursor(project, logstore, shard, fromTime); + } + }, 7, 1000L, true); + } + + } + + public static class Task extends Reader.Task { + + private static final Logger LOG = LoggerFactory.getLogger(Task.class); + + private Configuration taskConfig; + private Client client; + private String endPoint; + private String accessKeyId; + private String accessKeySecret; + private String project; + private String logstore; + private long beginTimestampMillis; + private long endTimestampMillis; + private int batchSize; + private int shard; + private List columns; + + @Override + public void init() { + this.taskConfig = super.getPluginJobConf(); + + endPoint = this.taskConfig.getString(Key.ENDPOINT); + accessKeyId = this.taskConfig.getString(Key.ACCESSKEYID); + accessKeySecret = this.taskConfig.getString(Key.ACCESSKEYSECRET); + project = this.taskConfig.getString(Key.PROJECT); + logstore = this.taskConfig.getString(Key.LOGSTORE); + batchSize = this.taskConfig.getInt(Key.BATCHSIZE, 128); + + this.beginTimestampMillis = this.taskConfig.getLong(Key.BEGINTIMESTAMPMILLIS, -1); + String beginDateTime = this.taskConfig.getString(Key.BEGINDATETIME); + + if (beginDateTime != null) { + try { + beginTimestampMillis = getUnixTimeFromDateTime(beginDateTime); + } catch (ParseException e) { + } + } + + this.endTimestampMillis = this.taskConfig.getLong(Key.ENDTIMESTAMPMILLIS, -1); + String endDateTime = this.taskConfig.getString(Key.ENDDATETIME); + + if (endDateTime != null) { + try { + endTimestampMillis = getUnixTimeFromDateTime(endDateTime); + } catch (ParseException e) { + } + } + + columns = this.taskConfig.getList(Key.COLUMN, String.class); + + shard = this.taskConfig.getInt(Key.SHARD); + + client = new Client(endPoint, accessKeyId, accessKeySecret); + LOG.info("init loghub reader task finished.project:{} logstore:{} batchSize:{}", project, logstore, batchSize); + } + + @Override + public void prepare() { + } + + private long getUnixTimeFromDateTime(String dateTime) throws ParseException { + try { + String format = Constant.DATETIME_FORMAT; + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format); + return simpleDateFormat.parse(dateTime).getTime() / 1000; + } catch (ParseException ignored) { + } + String format = Constant.DATE_FORMAT; + SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format); + return simpleDateFormat.parse(dateTime).getTime() / 1000; + } + + private GetCursorResponse getCursorWithRetry(final Client client, final String project, final String logstore, final int shard, final long fromTime) throws Exception { + return + RetryUtil.executeWithRetry(new Callable() { + @Override + public GetCursorResponse call() throws Exception { + LOG.info("loghug get cursor with project: {} logstore: {} shard: {} time: {}", project, logstore, shard, fromTime); + return client.GetCursor(project, logstore, shard, fromTime); + } + }, 7, 1000L, true); + } + + private GetCursorResponse getCursorWithRetry(final Client client, final String project, final String logstore, final int shard, final CursorMode mode) throws Exception { + return + RetryUtil.executeWithRetry(new Callable() { + @Override + public GetCursorResponse call() throws Exception { + LOG.info("loghug get cursor with project: {} logstore: {} shard: {} mode: {}", project, logstore, shard, mode); + return client.GetCursor(project, logstore, shard, mode); + } + }, 7, 1000L, true); + } + + private BatchGetLogResponse batchGetLogWithRetry(final Client client, final String project, final String logstore, final int shard, final int batchSize, + final String curCursor, final String endCursor) throws Exception { + return + RetryUtil.executeWithRetry(new Callable() { + @Override + public BatchGetLogResponse call() throws Exception { + return client.BatchGetLog(project, logstore, shard, batchSize, curCursor, endCursor); + } + }, 7, 1000L, true); + } + + @Override + public void startRead(RecordSender recordSender) { + LOG.info("read start"); + + try { + GetCursorResponse cursorRes; + if (this.beginTimestampMillis != -1) { + cursorRes = getCursorWithRetry(client, project, logstore, this.shard, beginTimestampMillis); + } else { + cursorRes = getCursorWithRetry(client, project, logstore, this.shard, CursorMode.BEGIN); + } + String beginCursor = cursorRes.GetCursor(); + + LOG.info("the begin cursor, loghub requestId: {} cursor: {}", cursorRes.GetRequestId(), cursorRes.GetCursor()); + + if (this.endTimestampMillis != -1) { + cursorRes = getCursorWithRetry(client, project, logstore, this.shard, endTimestampMillis); + } else { + cursorRes = getCursorWithRetry(client, project, logstore, this.shard, CursorMode.END); + } + String endCursor = cursorRes.GetCursor(); + LOG.info("the end cursor, loghub requestId: {} cursor: {}", cursorRes.GetRequestId(), cursorRes.GetCursor()); + + if (StringUtils.equals(beginCursor, endCursor)) { + LOG.info("beginCursor:{} equals endCursor:{}, end directly!", beginCursor, endCursor); + return; + } + + String currentCursor = null; + String nextCursor = beginCursor; + + HashMap metaMap = new HashMap(); + HashMap dataMap = new HashMap(); + JSONObject allMetaJson = new JSONObject(); + while (!StringUtils.equals(currentCursor, nextCursor)) { + currentCursor = nextCursor; + BatchGetLogResponse logDataRes = batchGetLogWithRetry(client, project, logstore, this.shard, this.batchSize, currentCursor, endCursor); + + List logGroups = logDataRes.GetLogGroups(); + + for(LogGroupData logGroup: logGroups) { + metaMap.clear(); + allMetaJson.clear(); + FastLogGroup flg = logGroup.GetFastLogGroup(); + + metaMap.put("C_Category", flg.getCategory()); + metaMap.put(Constant.META_COL_CATEGORY, flg.getCategory()); + allMetaJson.put(Constant.META_COL_CATEGORY, flg.getCategory()); + + metaMap.put("C_Source", flg.getSource()); + metaMap.put(Constant.META_COL_SOURCE, flg.getSource()); + allMetaJson.put(Constant.META_COL_SOURCE, flg.getSource()); + + metaMap.put("C_Topic", flg.getTopic()); + metaMap.put(Constant.META_COL_TOPIC, flg.getTopic()); + allMetaJson.put(Constant.META_COL_TOPIC, flg.getTopic()); + + metaMap.put("C_MachineUUID", flg.getMachineUUID()); + metaMap.put(Constant.META_COL_MACHINEUUID, flg.getMachineUUID()); + allMetaJson.put(Constant.META_COL_MACHINEUUID, flg.getMachineUUID()); + + for (int tagIdx = 0; tagIdx < flg.getLogTagsCount(); ++tagIdx) { + FastLogTag logtag = flg.getLogTags(tagIdx); + String tagKey = logtag.getKey(); + String tagValue = logtag.getValue(); + if (tagKey.equals(Constant.META_COL_HOSTNAME)) { + metaMap.put("C_HostName", logtag.getValue()); + } else if (tagKey.equals(Constant.META_COL_PATH)) { + metaMap.put("C_Path", logtag.getValue()); + } + metaMap.put(tagKey, tagValue); + allMetaJson.put(tagKey, tagValue); + } + + for (int lIdx = 0; lIdx < flg.getLogsCount(); ++lIdx) { + dataMap.clear(); + FastLog log = flg.getLogs(lIdx); + + String logTime = String.valueOf(log.getTime()); + metaMap.put("C_LogTime", logTime); + metaMap.put(Constant.META_COL_LOGTIME, logTime); + allMetaJson.put(Constant.META_COL_LOGTIME, logTime); + + for (int cIdx = 0; cIdx < log.getContentsCount(); ++cIdx) { + FastLogContent content = log.getContents(cIdx); + dataMap.put(content.getKey(), content.getValue()); + } + + Record record = recordSender.createRecord(); + + JSONObject extractOthers = new JSONObject(); + if(columns.contains(Constant.COL_EXTRACT_OTHERS)){ + List keyList = Arrays.asList(dataMap.keySet().toArray(new String[dataMap.keySet().size()])); + for (String otherKey:keyList) { + if (!columns.contains(otherKey)){ + extractOthers.put(otherKey,dataMap.get(otherKey)); + } + } + } + if (null != this.columns && 1 == this.columns.size()) { + String columnsInStr = columns.get(0).toString(); + if ("\"*\"".equals(columnsInStr) || "*".equals(columnsInStr)) { + List keyList = Arrays.asList(dataMap.keySet().toArray(new String[dataMap.keySet().size()])); + Collections.sort(keyList); + + for (String key : keyList) { + record.addColumn(new StringColumn(key + ":" + dataMap.get(key))); + } + } else { + if (dataMap.containsKey(columnsInStr)) { + record.addColumn(new StringColumn(dataMap.get(columnsInStr))); + } else if (metaMap.containsKey(columnsInStr)) { + record.addColumn(new StringColumn(metaMap.get(columnsInStr))); + } else if (Constant.COL_EXTRACT_OTHERS.equals(columnsInStr)){ + record.addColumn(new StringColumn(extractOthers.toJSONString())); + } else if (Constant.COL_EXTRACT_ALL_META.equals(columnsInStr)) { + record.addColumn(new StringColumn(allMetaJson.toJSONString())); + } + } + } else { + for (String col : this.columns) { + if (dataMap.containsKey(col)) { + record.addColumn(new StringColumn(dataMap.get(col))); + } else if (metaMap.containsKey(col)) { + record.addColumn(new StringColumn(metaMap.get(col))); + } else if (col != null && col.startsWith("'") && col.endsWith("'")){ + String constant = col.substring(1, col.length()-1); + record.addColumn(new StringColumn(constant)); + }else if (Constant.COL_EXTRACT_OTHERS.equals(col)){ + record.addColumn(new StringColumn(extractOthers.toJSONString())); + } else if (Constant.COL_EXTRACT_ALL_META.equals(col)) { + record.addColumn(new StringColumn(allMetaJson.toJSONString())); + } else { + record.addColumn(new StringColumn(null)); + } + } + } + + recordSender.sendToWriter(record); + } + } + + nextCursor = logDataRes.GetNextCursor(); + } + } catch (LogException e) { + if (e.GetErrorCode().equals("LogStoreNotExist")) { + LOG.info("logStore[" + logstore +"] Not Exits! detail error messsage: " + e.toString()); + } else { + LOG.error("read LogStore[" + logstore + "] error, please check ! detail error messsage: " + e.toString()); + throw DataXException.asDataXException(LogHubReaderErrorCode.LOG_HUB_ERROR, e); + } + + } catch (Exception e) { + LOG.error("read LogStore[" + logstore + "] error, please check ! detail error messsage: " + e.toString()); + throw DataXException.asDataXException(LogHubReaderErrorCode.LOG_HUB_ERROR, e); + } + + LOG.info("end read loghub shard..."); + } + + @Override + public void post() { + } + + @Override + public void destroy() { + } + } +} diff --git a/loghubreader/src/main/java/com/alibaba/datax/plugin/reader/loghubreader/LogHubReaderErrorCode.java b/loghubreader/src/main/java/com/alibaba/datax/plugin/reader/loghubreader/LogHubReaderErrorCode.java new file mode 100644 index 0000000000..d9ee4c8ee1 --- /dev/null +++ b/loghubreader/src/main/java/com/alibaba/datax/plugin/reader/loghubreader/LogHubReaderErrorCode.java @@ -0,0 +1,34 @@ +package com.alibaba.datax.plugin.reader.loghubreader; + +import com.alibaba.datax.common.spi.ErrorCode; + +public enum LogHubReaderErrorCode implements ErrorCode { + BAD_CONFIG_VALUE("LogHuReader-00", "The value you configured is invalid."), + LOG_HUB_ERROR("LogHubReader-01","LogHub access encounter exception"), + REQUIRE_VALUE("LogHubReader-02","Missing parameters"), + EMPTY_LOGSTORE_VALUE("LogHubReader-03","There is no shard in this LogStore"); + + private final String code; + private final String description; + + private LogHubReaderErrorCode(String code, String description) { + this.code = code; + this.description = description; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + return String.format("Code:[%s], Description:[%s]. ", this.code, + this.description); + } +} diff --git a/loghubreader/src/main/resources/plugin.json b/loghubreader/src/main/resources/plugin.json new file mode 100644 index 0000000000..31403dd63d --- /dev/null +++ b/loghubreader/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "loghubreader", + "class": "com.alibaba.datax.plugin.reader.loghubreader.LogHubReader", + "description": "适用于: 从SLS LogHub中读取数据", + "developer": "alibaba" +} \ No newline at end of file diff --git a/loghubreader/src/main/resources/plugin_job_template.json b/loghubreader/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..4d536eb9fb --- /dev/null +++ b/loghubreader/src/main/resources/plugin_job_template.json @@ -0,0 +1,12 @@ +{ + "name": "loghubreader", + "parameter": { + "endpoint": "", + "accessId": "", + "accessKey": "", + "project": "", + "logstore": "", + "batchSize":1024, + "column": [] + } +} \ No newline at end of file diff --git a/loghubwriter/pom.xml b/loghubwriter/pom.xml new file mode 100644 index 0000000000..d43b7286d8 --- /dev/null +++ b/loghubwriter/pom.xml @@ -0,0 +1,73 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + + loghubwriter + + 0.0.1-SNAPSHOT + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + com.aliyun.openservices + aliyun-log + 0.6.12 + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + diff --git a/loghubwriter/src/main/assembly/package.xml b/loghubwriter/src/main/assembly/package.xml new file mode 100644 index 0000000000..44d25a48a3 --- /dev/null +++ b/loghubwriter/src/main/assembly/package.xml @@ -0,0 +1,34 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + + plugin/writer/loghubwriter + + + target/ + + loghubwriter-0.0.1-SNAPSHOT.jar + + plugin/writer/loghubwriter + + + + + + false + plugin/writer/loghubwriter/libs + runtime + + + diff --git a/loghubwriter/src/main/java/com/alibaba/datax/plugin/writer/loghubwriter/Key.java b/loghubwriter/src/main/java/com/alibaba/datax/plugin/writer/loghubwriter/Key.java new file mode 100644 index 0000000000..bdfe3fa537 --- /dev/null +++ b/loghubwriter/src/main/java/com/alibaba/datax/plugin/writer/loghubwriter/Key.java @@ -0,0 +1,35 @@ +package com.alibaba.datax.plugin.writer.loghubwriter; + +/** + * 配置关键字 + * @author + */ +public final class Key { + + /** + * 此处声明插件用到的需要插件使用者提供的配置项 + */ + public static final String ENDPOINT = "endpoint"; + + public static final String ACCESS_KEY_ID = "accessId"; + + public static final String ACCESS_KEY_SECRET = "accessKey"; + + public static final String PROJECT = "project"; + + public static final String LOG_STORE = "logstore"; + + public static final String TOPIC = "topic"; + + public static final String COLUMN = "column"; + + public static final String BATCH_SIZE = "batchSize"; + + public static final String TIME = "time"; + + public static final String TIME_FORMAT = "timeformat"; + + public static final String SOURCE = "source"; + + public static final String HASH_BY_KEY = "hashKey"; +} diff --git a/loghubwriter/src/main/java/com/alibaba/datax/plugin/writer/loghubwriter/LogHubWriter.java b/loghubwriter/src/main/java/com/alibaba/datax/plugin/writer/loghubwriter/LogHubWriter.java new file mode 100644 index 0000000000..bf60d08c0f --- /dev/null +++ b/loghubwriter/src/main/java/com/alibaba/datax/plugin/writer/loghubwriter/LogHubWriter.java @@ -0,0 +1,315 @@ +package com.alibaba.datax.plugin.writer.loghubwriter; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.RetryUtil; +import com.alibaba.datax.common.util.StrUtil; +import com.aliyun.openservices.log.Client; +import com.aliyun.openservices.log.common.LogItem; +import com.aliyun.openservices.log.common.Shard; +import com.aliyun.openservices.log.exception.LogException; +import com.aliyun.openservices.log.request.ListShardRequest; +import com.aliyun.openservices.log.request.PutLogsRequest; +import com.aliyun.openservices.log.response.ListShardResponse; +import com.aliyun.openservices.log.response.PutLogsResponse; + +import org.apache.commons.codec.digest.Md5Crypt; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import sun.security.provider.MD5; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; + +/** + * SLS 写插件 + * @author + */ +public class LogHubWriter extends Writer { + + public static class Job extends Writer.Job { + private static final Logger LOG = LoggerFactory.getLogger(Job.class); + + private Configuration jobConfig = null; + + @Override + public void init() { + info(LOG, "loghub writer job init begin ..."); + this.jobConfig = super.getPluginJobConf(); + validateParameter(jobConfig); + info(LOG, "loghub writer job init end."); + } + + private void validateParameter(Configuration conf){ + conf.getNecessaryValue(Key.ENDPOINT,LogHubWriterErrorCode.REQUIRE_VALUE); + conf.getNecessaryValue(Key.ACCESS_KEY_ID,LogHubWriterErrorCode.REQUIRE_VALUE); + conf.getNecessaryValue(Key.ACCESS_KEY_SECRET,LogHubWriterErrorCode.REQUIRE_VALUE); + conf.getNecessaryValue(Key.PROJECT,LogHubWriterErrorCode.REQUIRE_VALUE); + conf.getNecessaryValue(Key.LOG_STORE,LogHubWriterErrorCode.REQUIRE_VALUE); + conf.getNecessaryValue(Key.COLUMN,LogHubWriterErrorCode.REQUIRE_VALUE); + } + + @Override + public List split(int mandatoryNumber) { + info(LOG, "split begin..."); + List configurationList = new ArrayList(); + for (int i = 0; i < mandatoryNumber; i++) { + configurationList.add(this.jobConfig.clone()); + } + info(LOG, "split end..."); + return configurationList; + } + + @Override + public void post() { + } + + @Override + public void destroy() { + } + } + + public static class Task extends Writer.Task { + private static final Logger LOG = LoggerFactory.getLogger(Task.class); + private Configuration taskConfig; + private com.aliyun.openservices.log.Client logHubClient; + private String logStore; + private String topic; + private String project; + private List columnList; + private int batchSize; + private String timeCol; + private String timeFormat; + private String source; + private boolean isHashKey; + private List shards; + public void init() { + this.taskConfig = super.getPluginJobConf(); + String endpoint = taskConfig.getString(Key.ENDPOINT); + String accessKeyId = taskConfig.getString(Key.ACCESS_KEY_ID); + String accessKeySecret = taskConfig.getString(Key.ACCESS_KEY_SECRET); + project = taskConfig.getString(Key.PROJECT); + logStore = taskConfig.getString(Key.LOG_STORE); + topic = taskConfig.getString(Key.TOPIC,""); + columnList = taskConfig.getList(Key.COLUMN,String.class); + batchSize = taskConfig.getInt(Key.BATCH_SIZE,1024); + timeCol = taskConfig.getString(Key.TIME,""); + timeFormat = taskConfig.getString(Key.TIME_FORMAT,""); + source = taskConfig.getString(Key.SOURCE,""); + isHashKey = taskConfig.getBool(Key.HASH_BY_KEY,false); + logHubClient = new Client(endpoint, accessKeyId, accessKeySecret); + if (isHashKey) { + listShard(); + info(LOG, "init loghub writer with hash key mode."); + } + if (LOG.isInfoEnabled()) { + LOG.info("init loghub writer task finished.project:{} logstore:{} topic:{} batchSize:{}",project,logStore,topic,batchSize); + } + } + + /** + * 获取通道的分片信息 + */ + private void listShard() { + try { + ListShardResponse response = logHubClient.ListShard(new ListShardRequest(project,logStore)); + shards = response.GetShards(); + if (LOG.isInfoEnabled()) { + LOG.info("Get shard count:{}", shards.size()); + } + } catch (LogException e) { + info(LOG, "Get shard failed!"); + throw new RuntimeException("Get shard failed!", e); + } + } + + @Override + public void prepare() { + } + + private int getTime(String v) { + try { + if ("bigint".equalsIgnoreCase(timeFormat)) { + return Integer.valueOf(v); + } + + DateFormat sdf = new SimpleDateFormat(timeFormat); + Date date = sdf.parse(v); + return (int)(date.getTime()/1000); + } catch (Exception e) { + LOG.warn("Format time failed!", e); + } + return (int)(((new Date())).getTime()/1000); + } + + @Override + public void startWrite(RecordReceiver recordReceiver) { + info(LOG, "start to write....................."); + // 按照shared做hash处理 + if (isHashKey) { + processDataWithHashKey(recordReceiver); + } else { + processDataWithoutHashKey(recordReceiver); + } + info(LOG, "finish to write........."); + } + + private void processDataWithHashKey(RecordReceiver receiver) { + Record record; + Map> logMap = new HashMap>(shards.size()); + int count = 0; + try { + while ((record = receiver.getFromReader()) != null) { + LogItem logItem = new LogItem(); + if (record.getColumnNumber() != columnList.size()) { + this.getTaskPluginCollector().collectDirtyRecord(record, "column not match"); + } + + String id = ""; + for (int i = 0; i < record.getColumnNumber(); i++) { + String colName = columnList.get(i); + String colValue = record.getColumn(i).asString(); + if (colName.endsWith("_id")) { + id = colValue; + } + + logItem.PushBack(colName, colValue); + if (colName.equals(timeCol)) { + logItem.SetTime(getTime(colValue)); + } + } + + String hashKey = getShardHashKey(StrUtil.getMd5(id), shards); + if (!logMap.containsKey(hashKey)) { + info(LOG, "Hash key:" + hashKey); + logMap.put(hashKey, new ArrayList()); + } + logMap.get(hashKey).add(logItem); + + if (logMap.get(hashKey).size() % batchSize == 0) { + PutLogsRequest request = new PutLogsRequest(project, logStore, topic, source, logMap.get(hashKey), hashKey); + PutLogsResponse response = putLog(request); + count += logMap.get(hashKey).size(); + if (LOG.isDebugEnabled()) { + LOG.debug("record count:{}, request id:{}", logMap.get(hashKey).size(), response.GetRequestId()); + } + logMap.get(hashKey).clear(); + } + } + + for (Map.Entry> entry : logMap.entrySet()) { + if (!entry.getValue().isEmpty()) { + // 将剩余的数据发送 + PutLogsRequest request = new PutLogsRequest(project, logStore, topic, source, entry.getValue(), entry.getKey()); + PutLogsResponse response = putLog(request); + count += entry.getValue().size(); + if (LOG.isDebugEnabled()) { + LOG.debug("record count:{}, request id:{}", entry.getValue().size(), response.GetRequestId()); + } + entry.getValue().clear(); + } + } + LOG.info("{} records have been sent", count); + } catch (LogException ex) { + throw DataXException.asDataXException(LogHubWriterErrorCode.LOG_HUB_ERROR, ex.getMessage(), ex); + } catch (Exception e) { + throw DataXException.asDataXException(LogHubWriterErrorCode.LOG_HUB_ERROR, e.getMessage(), e); + } + } + + private void processDataWithoutHashKey(RecordReceiver receiver) { + Record record; + ArrayList logGroup = new ArrayList(); + int count = 0; + try { + while ((record = receiver.getFromReader()) != null) { + LogItem logItem = new LogItem(); + if(record.getColumnNumber() != columnList.size()){ + this.getTaskPluginCollector().collectDirtyRecord(record,"column not match"); + } + for (int i = 0; i < record.getColumnNumber(); i++) { + String colName = columnList.get(i); + String colValue = record.getColumn(i).asString(); + logItem.PushBack(colName, colValue); + if(colName.equals(timeCol)){ + logItem.SetTime(getTime(colValue)); + } + } + + logGroup.add(logItem); + count++; + if (count % batchSize == 0) { + PutLogsRequest request = new PutLogsRequest(project, logStore, topic, source, logGroup); + PutLogsResponse response = putLog(request); + logGroup.clear(); + if (LOG.isDebugEnabled()) { + LOG.debug("record count:{}, request id:{}", count, response.GetRequestId()); + } + } + } + if (!logGroup.isEmpty()) { + //将剩余的数据发送 + PutLogsRequest request = new PutLogsRequest(project, logStore, topic, source, logGroup); + PutLogsResponse response = putLog(request); + logGroup.clear(); + if (LOG.isDebugEnabled()) { + LOG.debug("record count:{}, request id:{}", count, response.GetRequestId()); + } + } + LOG.info("{} records have been sent", count); + } catch (LogException ex) { + throw DataXException.asDataXException(LogHubWriterErrorCode.LOG_HUB_ERROR, ex.getMessage(), ex); + } catch (Exception e) { + throw DataXException.asDataXException(LogHubWriterErrorCode.LOG_HUB_ERROR, e.getMessage(), e); + } + } + + private PutLogsResponse putLog(final PutLogsRequest request) throws Exception{ + final Client client = this.logHubClient; + + return RetryUtil.executeWithRetry(new Callable() { + public PutLogsResponse call() throws LogException{ + return client.PutLogs(request); + } + }, 3, 1000L, false); + } + + private String getShardHashKey(String hashKey, List shards) { + for (Shard shard : shards) { + if (hashKey.compareTo(shard.getExclusiveEndKey()) < 0 && hashKey.compareTo(shard.getInclusiveBeginKey()) >= 0) { + return shard.getInclusiveBeginKey(); + } + } + return shards.get(0).getInclusiveBeginKey(); + } + + @Override + public void post() { + } + + @Override + public void destroy() { + } + } + + /** + * 日志打印控制 + * + * @param logger + * @param message + */ + public static void info(Logger logger, String message) { + if (logger.isInfoEnabled()) { + logger.info(message); + } + } +} diff --git a/loghubwriter/src/main/java/com/alibaba/datax/plugin/writer/loghubwriter/LogHubWriterErrorCode.java b/loghubwriter/src/main/java/com/alibaba/datax/plugin/writer/loghubwriter/LogHubWriterErrorCode.java new file mode 100644 index 0000000000..98c5e16f88 --- /dev/null +++ b/loghubwriter/src/main/java/com/alibaba/datax/plugin/writer/loghubwriter/LogHubWriterErrorCode.java @@ -0,0 +1,33 @@ +package com.alibaba.datax.plugin.writer.loghubwriter; + +import com.alibaba.datax.common.spi.ErrorCode; + +public enum LogHubWriterErrorCode implements ErrorCode { + BAD_CONFIG_VALUE("LogHubWriter-00", "The value you configured is invalid."), + LOG_HUB_ERROR("LogHubWriter-01","LogHub access encounter exception"), + REQUIRE_VALUE("LogHubWriter-02","Missing parameters"); + + private final String code; + private final String description; + + private LogHubWriterErrorCode(String code, String description) { + this.code = code; + this.description = description; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + return String.format("Code:[%s], Description:[%s]. ", this.code, + this.description); + } +} \ No newline at end of file diff --git a/loghubwriter/src/main/resources/plugin.json b/loghubwriter/src/main/resources/plugin.json new file mode 100644 index 0000000000..2a913b1437 --- /dev/null +++ b/loghubwriter/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "loghubwriter", + "class": "com.alibaba.datax.plugin.writer.loghubwriter.LogHubWriter", + "description": "适用于: 将数据导入到SLS LogHub中", + "developer": "alibaba" +} \ No newline at end of file diff --git a/loghubwriter/src/main/resources/plugin_job_template.json b/loghubwriter/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..ac0d3b2a04 --- /dev/null +++ b/loghubwriter/src/main/resources/plugin_job_template.json @@ -0,0 +1,13 @@ +{ + "name": "loghubwriter", + "parameter": { + "endpoint": "", + "accessId": "", + "accessKey": "", + "project": "", + "logstore": "", + "topic": "", + "batchSize":1024, + "column": [] + } +} \ No newline at end of file diff --git a/milvuswriter/doc/milvuswriter.md b/milvuswriter/doc/milvuswriter.md new file mode 100644 index 0000000000..3788c62ac6 --- /dev/null +++ b/milvuswriter/doc/milvuswriter.md @@ -0,0 +1,273 @@ +# DataX milvuswriter + + +--- + + +## 1 快速介绍 + +milvuswriter 插件实现了写入数据到 milvus集合的功能; 面向ETL开发工程师,使用 milvuswriter 从数仓导入数据到 milvus, 同时 milvuswriter 亦可以作为数据迁移工具为DBA等用户提供服务。 + + +## 2 实现原理 + +milvuswriter 通过 DataX 框架获取 Reader 生成的协议数据,通过 `upsert/insert `方式写入数据到milvus, 并通过batchSize累积的方式进行数据提交。 +
+ + 注意:upsert写入方式(推荐): 在非autid表场景下根据主键更新 Collection 中的某个 Entity;autid表场景下会将 Entity 中的主键替换为自动生成的主键并插入数据。 + insert写入方式: 多用于autid表插入数据milvus自动生成主键, 非autoid表下使用insert会导致数据重复。 + + +## 3 功能说明 + +### 3.1 配置样例 + +* 这里提供一份从内存产生数据导入到 milvus的配置样例。 + +```json +{ + "job": { + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "column" : [ + { + "value": 1, + "type": "long" + }, + { + "value": "[1.1,1.2,1.3]", + "type": "string" + }, + { + "value": 100, + "type": "long" + }, + { + "value": 200, + "type": "long" + }, + { + "value": 300, + "type": "long" + }, + { + "value": 3.14159, + "type": "double" + }, + { + "value": 3.1415926, + "type": "double" + }, + { + "value": "testvarcharvalue", + "type": "string" + }, + { + "value": true, + "type": "bool" + }, + { + "value": "[1.123,1.2456,1.3789]", + "type": "string" + }, + { + "value": "[2.123,2.2456,2.3789]", + "type": "string" + }, + { + "value": "12345678", + "type": "string" + }, + { + "value": "{\"a\":1,\"b\":2,\"c\":3}", + "type": "string" + }, + { + "value": "[1,2,3,4]", + "type": "string" + } + ], + "sliceRecordCount": 1 + } + }, + "writer": { + "parameter": { + "schemaCreateMode": "createIfNotExist", + "connectTimeoutMs": 60000, + "writeMode": "upsert", + "collection": "demo01", + "type": "milvus", + "token": "xxxxxxx", + "endpoint": "https://xxxxxxxx.com:443", + "batchSize": 1024, + "column": [ + { + "name": "id", + "type": "Int64", + "primaryKey": "true" + }, + { + "name": "floatvector", + "type": "FloatVector", + "dimension": "3" + }, + { + "name": "int8col", + "type": "Int8" + }, + { + "name": "int16col", + "type": "Int16" + }, + { + "name": "int32col", + "type": "Int32" + }, + { + "name": "floatcol", + "type": "Float" + }, + { + "name": "doublecol", + "type": "Double" + }, + { + "name": "varcharcol", + "type": "VarChar" + }, + { + "name": "boolcol", + "type": "Bool" + }, + { + "name": "bfloat16vectorcol", + "type": "BFloat16Vector", + "dimension": "3" + }, + { + "name": "float16vectorcol", + "type": "Float16Vector", + "dimension": "3" + }, + { + "name": "binaryvectorcol", + "type": "BinaryVector", + "dimension": "64" + }, + { + "name": "jsoncol", + "type": "JSON" + }, + { + "name": "intarraycol", + "maxCapacity": "8", + "type": "Array", + "elementType": "Int32" + } + ] + }, + "name": "milvuswriter" + } + } + ], + "setting": { + "errorLimit": { + "record": "0" + }, + "speed": { + "concurrent": 2, + "channel": 2 + } + } + } +} + +``` + + +### 3.2 参数说明 + +* **endpoint** + * 描述:milvus数据库的连接信息,包含地址和端口,例如https://xxxxxxxx.com:443 + + 注意:1、在一个数据库上只能配置一个 endpoint 值 + 2、一个milvus 写入任务仅能配置一个 endpoint + * 必选:是
+ * 默认值:无
+* *schemaCreateMode* + * 描述: 集合创建的模式,同步时milvus集合不存在的处理方式, 根据配置的column属性进行创建 + * 取值 + * createIfNotExist: 如果集合不存在,则创建集合,如果集合存在,则不执行任何操作 + * ignore: 如果集合不存在,任务异常报错,如果集合存在,则不执行任何操作 + * recreate: 如果集合不存在,则创建集合,如果集合存在,则删除集合重建集合 + * 必选:否
+ * 默认值:createIfNotExist
+* **connectTimeoutMs** + * 描述:与milvus交互是客户端的连接超时时间,单位毫秒
+ * 必选:否
+ * 默认值:10000
+* **token** + * 描述:milvus实例认证的token秘钥,与username认证方式二选一配置
+ * 必选:否
+ * 默认值:无
+* **username** + * 描述:目的milvus数据库的用户名, 与token二选一配置
+ * 必选:否
+ * 默认值:无
+* **password** + * 描述:目的milvus数据库的密码
+ * 必选:否
+ * 默认值:无
+* *writeMode* + * 描述: 写入milvus集合的写入方式 + * 取值 + * upsert(推荐): 在非autid表场景下根据主键更新 Collection 中的某个 Entity;autid表场景下会将 Entity 中的主键替换为自动生成的主键并插入数据。 + * insert: 多用于autid表插入数据milvus自动生成主键, 非autoid表下使用insert会导致数据重复。 + * 必选:是
+ * 默认值:upsert
+* **collection** + * 描述:目的集合名称。 只能配置一个milvus的集合名称。 + * 必选:是
+ * 默认值:无
+* **batchSize** + * 描述:一次性批量提交的记录数大小,该值可以极大减少DataX与milvus的网络交互次数,并提升整体吞吐量。但是该值设置过大可能会造成DataX运行进程OOM情况。
+ * 必选:否
+ * 默认值:1024
+ +* **column** + * 描述:目的集合需要写入数据的字段,字段内容用json格式描述,字段之间用英文逗号分隔。字段属性必填name、type, 其他属性在需要schemaCreateMode创建集合按需填入,例如: + + "column": [ + { + "name": "id", + "type": "Int64", + "primaryKey": "true" + }, + { + "name": "floatvector", + "type": "FloatVector", + "dimension": "3" + }] + * 必选:是
+ * 默认值:否
+### 3.3 支持同步milvus字段类型 + Bool, + Int8, + Int16, + Int32, + Int64, + Float, + Double, + String, + VarChar, + Array, + JSON, + BinaryVector, + FloatVector, + Float16Vector, + BFloat16Vector, + SparseFloatVector + diff --git a/milvuswriter/pom.xml b/milvuswriter/pom.xml new file mode 100644 index 0000000000..16c00560de --- /dev/null +++ b/milvuswriter/pom.xml @@ -0,0 +1,125 @@ + + + 4.0.0 + + com.alibaba.datax + datax-all + 0.0.1-SNAPSHOT + + + milvuswriter + + + UTF-8 + official + 1.8 + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.49 + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.projectlombok + lombok + 1.18.30 + + + guava + com.google.guava + 32.0.1-jre + + + io.milvus + milvus-sdk-java + 2.5.2 + + + org.mockito + mockito-core + 3.3.3 + test + + + junit + junit + 4.11 + test + + + org.jetbrains.kotlin + kotlin-stdlib + 2.0.0 + + + org.powermock + powermock-module-junit4 + 2.0.9 + test + + + org.powermock + powermock-api-mockito2 + 2.0.9 + test + + + + + + + + src/main/resources + + **/*.* + + true + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + + \ No newline at end of file diff --git a/milvuswriter/src/main/assembly/package.xml b/milvuswriter/src/main/assembly/package.xml new file mode 100644 index 0000000000..62357b4ae5 --- /dev/null +++ b/milvuswriter/src/main/assembly/package.xml @@ -0,0 +1,36 @@ + + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/writer/milvuswriter + + + target/ + + milvuswriter-0.0.1-SNAPSHOT.jar + + plugin/writer/milvuswriter + + + + + + false + plugin/writer/milvuswriter/libs + runtime + + + diff --git a/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/KeyConstant.java b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/KeyConstant.java new file mode 100644 index 0000000000..28f1ff13e5 --- /dev/null +++ b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/KeyConstant.java @@ -0,0 +1,17 @@ +package com.alibaba.datax.plugin.writer.milvuswriter; + +public class KeyConstant { + public static final String USERNAME = "username"; + public static final String PASSWORD = "password"; + public static final String ENDPOINT = "endpoint"; + public static final String TOKEN = "token"; + public static final String DATABASE = "database"; + public static final String COLLECTION = "collection"; + public static final String BATCH_SIZE = "batchSize"; + public static final String COLUMN = "column"; + public static final String SCHAME_CREATE_MODE = "schemaCreateMode"; + public static final String WRITE_MODE = "writeMode"; + public static final String PARTITION = "partition"; + public static final String CONNECT_TIMEOUT_MS = "connectTimeoutMs"; + public static final String ENABLE_DYNAMIC_SCHEMA = "enableDynamicSchema"; +} \ No newline at end of file diff --git a/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusBufferWriter.java b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusBufferWriter.java new file mode 100644 index 0000000000..b78728e44e --- /dev/null +++ b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusBufferWriter.java @@ -0,0 +1,166 @@ +package com.alibaba.datax.plugin.writer.milvuswriter; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.plugin.writer.milvuswriter.enums.WriteModeEnum; +import com.alibaba.fastjson2.JSONArray; +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.milvus.v2.common.DataType; +import io.milvus.v2.service.vector.request.data.BFloat16Vec; +import io.milvus.v2.service.vector.request.data.Float16Vec; +import lombok.extern.slf4j.Slf4j; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.TreeMap; +import java.util.stream.Collectors; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; + +@Slf4j +public class MilvusBufferWriter { + + private final MilvusClient milvusClient; + private final String collection; + private final Integer batchSize; + private List dataCache; + private List milvusColumnMeta; + private WriteModeEnum writeMode; + private String partition; + + public MilvusBufferWriter(MilvusClient milvusClient, Configuration writerSliceConfig) { + this.milvusClient = milvusClient; + this.collection = writerSliceConfig.getString(KeyConstant.COLLECTION); + this.batchSize = writerSliceConfig.getInt(KeyConstant.BATCH_SIZE, 100); + this.dataCache = new ArrayList<>(batchSize); + this.milvusColumnMeta = JSON.parseObject(writerSliceConfig.getString(KeyConstant.COLUMN), new TypeReference>() { + }); + this.writeMode = WriteModeEnum.getEnum(writerSliceConfig.getString(KeyConstant.WRITE_MODE)); + this.partition = writerSliceConfig.getString(KeyConstant.PARTITION); + } + + public void add(Record record, TaskPluginCollector taskPluginCollector) { + try { + JsonObject data = this.convertByType(milvusColumnMeta, record); + dataCache.add(data); + } catch (Exception e) { + taskPluginCollector.collectDirtyRecord(record, String.format("parse record error errorMessage: %s", e.getMessage())); + } + } + + public Boolean needCommit() { + return dataCache.size() >= batchSize; + } + + public void commit() { + if (dataCache.isEmpty()) { + log.info("dataCache is empty, skip commit"); + return; + } + if (writeMode == WriteModeEnum.INSERT) { + milvusClient.insert(collection, partition, dataCache); + } else { + milvusClient.upsert(collection, partition, dataCache); + } + dataCache = new ArrayList<>(batchSize); + } + + public int getDataCacheSize() { + return dataCache.size(); + } + + private JsonObject convertByType(List milvusColumnMeta, Record record) { + JsonObject data = new JsonObject(); + Gson gson = new Gson(); + for (int i = 0; i < record.getColumnNumber(); i++) { + MilvusColumn milvusColumn = milvusColumnMeta.get(i); + DataType fieldType = milvusColumn.getMilvusTypeEnum(); + String fieldName = milvusColumn.getName(); + Column column = record.getColumn(i); + try { + Object field = convertToMilvusField(fieldType, column, milvusColumn); + data.add(fieldName, gson.toJsonTree(field)); + } catch (Exception e) { + log.error("parse error for column: {} errorMessage: {}", fieldName, e.getMessage(), e); + throw e; + } + } + return data; + } + + //值需要跟这里匹配:io.milvus.param.ParamUtils#checkFieldData(io.milvus.param.collection.FieldType, java.util.List, boolean) + private Object convertToMilvusField(DataType type, Column column, MilvusColumn milvusColumn) { + if (column.getRawData() == null) { + return null; + } + switch (type) { + case Int8: + case Int16: + case Int32: + case Int64: + return column.asLong(); + case Float: + case Double: + return column.asDouble(); + case String: + case VarChar: + return column.asString(); + case Bool: + return column.asBoolean(); + case BFloat16Vector: + JSONArray bFloat16ArrayJson = JSON.parseArray(column.asString()); + List bfloat16Vector = new ArrayList<>(); + for (int i = 0; i < bFloat16ArrayJson.size(); i++) { + Float value = Float.parseFloat(bFloat16ArrayJson.getString(i)); + bfloat16Vector.add(value); + } + BFloat16Vec bFloat16Vec = new BFloat16Vec(bfloat16Vector); + ByteBuffer byteBuffer = (ByteBuffer) bFloat16Vec.getData(); + return byteBuffer.array(); + case Float16Vector: + JSONArray float16ArrayJson = JSON.parseArray(column.asString()); + List float16Vector = new ArrayList<>(); + for (int i = 0; i < float16ArrayJson.size(); i++) { + Float floatValue = Float.parseFloat(float16ArrayJson.getString(i)); + float16Vector.add(floatValue); + } + Float16Vec float16Vec = new Float16Vec(float16Vector); + ByteBuffer data = (ByteBuffer) float16Vec.getData(); + return data.array(); + case BinaryVector: + return column.asBytes(); + case FloatVector: + JSONArray arrayJson = JSON.parseArray(column.asString()); + return arrayJson.stream().map(item -> Float.parseFloat(String.valueOf(item))).collect(Collectors.toList()); + case SparseFloatVector: + //[3:0.5, 24:0.8, 76:0.2] + try { + JSONArray sparseFloatArray = JSON.parseArray(column.asString()); + TreeMap mapValue = new TreeMap<>(); + for (int i = 0; i < sparseFloatArray.size(); i++) { + String value = sparseFloatArray.getString(i); + String[] split = value.split(":"); + Long key = Long.parseLong(split[0]); + Float val = Float.parseFloat(split[1]); + mapValue.put(key, val); + } + return mapValue; + } catch (Exception e) { + log.error("parse column[{}] SparseFloatVector value error, value should like [3:0.5, 24:0.8, 76:0.2], but get:{}", milvusColumn.getName(), column.asString()); + throw e; + } + case JSON: + return column.asString(); + case Array: + JSONArray parseArray = JSON.parseArray(column.asString()); + return parseArray.stream().map(item -> String.valueOf(item)).collect(Collectors.toList()); + default: + throw new RuntimeException(String.format("Unsupported data type[%s]", type)); + } + } +} \ No newline at end of file diff --git a/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusClient.java b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusClient.java new file mode 100644 index 0000000000..1bf4743b01 --- /dev/null +++ b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusClient.java @@ -0,0 +1,95 @@ +package com.alibaba.datax.plugin.writer.milvuswriter; + +import java.util.List; + +import com.alibaba.datax.common.util.Configuration; + +import com.google.gson.JsonObject; +import io.milvus.v2.client.ConnectConfig; +import io.milvus.v2.client.MilvusClientV2; +import io.milvus.v2.service.collection.request.CreateCollectionReq; +import io.milvus.v2.service.collection.request.DropCollectionReq; +import io.milvus.v2.service.collection.request.HasCollectionReq; +import io.milvus.v2.service.partition.request.CreatePartitionReq; +import io.milvus.v2.service.partition.request.HasPartitionReq; +import io.milvus.v2.service.vector.request.InsertReq; +import io.milvus.v2.service.vector.request.UpsertReq; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +/** + * @author ziming(子茗) + * @date 12/27/24 + * @description + */ +@Slf4j +public class MilvusClient { + private MilvusClientV2 milvusClientV2; + + public MilvusClient(Configuration conf) { + // connect to milvus + ConnectConfig connectConfig = ConnectConfig.builder().uri(conf.getString(KeyConstant.ENDPOINT)).build(); + String token = null; + if (conf.getString(KeyConstant.TOKEN) != null) { + token = conf.getString(KeyConstant.TOKEN); + } else { + token = conf.getString(KeyConstant.USERNAME) + ":" + conf.getString(KeyConstant.PASSWORD); + } + connectConfig.setToken(token); + String database = conf.getString(KeyConstant.DATABASE); + if (StringUtils.isNotBlank(database)) { + log.info("use database {}", database); + connectConfig.setDbName(conf.getString(KeyConstant.DATABASE)); + } + Integer connectTimeOut = conf.getInt(KeyConstant.CONNECT_TIMEOUT_MS); + if (connectTimeOut != null) { + connectConfig.setConnectTimeoutMs(connectTimeOut); + } + this.milvusClientV2 = new MilvusClientV2(connectConfig); + } + + public void upsert(String collection, String partition, List data) { + UpsertReq upsertReq = UpsertReq.builder().collectionName(collection).data(data).build(); + if (StringUtils.isNotEmpty(partition)) { + upsertReq.setPartitionName(partition); + } + milvusClientV2.upsert(upsertReq); + } + + public void insert(String collection, String partition, List data) { + InsertReq insertReq = InsertReq.builder().collectionName(collection).data(data).build(); + if (StringUtils.isNotEmpty(partition)) { + insertReq.setPartitionName(partition); + } + milvusClientV2.insert(insertReq); + } + + public Boolean hasCollection(String collection) { + HasCollectionReq build = HasCollectionReq.builder().collectionName(collection).build(); + return milvusClientV2.hasCollection(build); + } + + public void createCollection(String collection, CreateCollectionReq.CollectionSchema schema) { + CreateCollectionReq createCollectionReq = CreateCollectionReq.builder().collectionName(collection).collectionSchema(schema).build(); + milvusClientV2.createCollection(createCollectionReq); + } + + public void dropCollection(String collection) { + DropCollectionReq request = DropCollectionReq.builder().collectionName(collection).build(); + milvusClientV2.dropCollection(request); + } + public Boolean hasPartition(String collection, String partition) { + HasPartitionReq hasPartitionReq = HasPartitionReq.builder().collectionName(collection).partitionName(partition).build(); + return milvusClientV2.hasPartition(hasPartitionReq); + } + + public void createPartition(String collectionName, String partitionName) { + CreatePartitionReq createPartitionReq = CreatePartitionReq.builder().collectionName(collectionName).partitionName(partitionName).build(); + milvusClientV2.createPartition(createPartitionReq); + } + + public void close() { + log.info("Closing Milvus client"); + milvusClientV2.close(); + } +} diff --git a/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusColumn.java b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusColumn.java new file mode 100644 index 0000000000..06070248a9 --- /dev/null +++ b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusColumn.java @@ -0,0 +1,112 @@ +package com.alibaba.datax.plugin.writer.milvuswriter; + +import io.milvus.v2.common.DataType; + +import java.util.Arrays; + +/** + * @author ziming(子茗) + * @date 12/27/24 + * @description + */ +public class MilvusColumn { + private String name; + private String type; + private DataType milvusTypeEnum; + private Boolean isPrimaryKey; + private Integer dimension; + private Boolean isPartitionKey; + private Integer maxLength; + private Boolean isAutoId; + private Integer maxCapacity; + private String elementType; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + for (DataType item : DataType.values()) { + if (item.name().equalsIgnoreCase(type)) { + this.milvusTypeEnum = item; + break; + } + } + if (this.milvusTypeEnum == null) { + throw new RuntimeException("Unsupported type: " + type + " supported types: " + Arrays.toString(DataType.values())); + } + } + + public Integer getDimension() { + return dimension; + } + + public void setDimension(Integer dimension) { + this.dimension = dimension; + } + + public Integer getMaxLength() { + return maxLength; + } + + public void setMaxLength(Integer maxLength) { + this.maxLength = maxLength; + } + + public Boolean getPrimaryKey() { + return isPrimaryKey; + } + + public Boolean getPartitionKey() { + return isPartitionKey; + } + + public void setPartitionKey(Boolean partitionKey) { + isPartitionKey = partitionKey; + } + + public void setPrimaryKey(Boolean primaryKey) { + isPrimaryKey = primaryKey; + } + + public Boolean getAutoId() { + return isAutoId; + } + + public void setAutoId(Boolean autoId) { + isAutoId = autoId; + } + + public Integer getMaxCapacity() { + return maxCapacity; + } + + public void setMaxCapacity(Integer maxCapacity) { + this.maxCapacity = maxCapacity; + } + + public String getElementType() { + return elementType; + } + + public void setElementType(String elementType) { + this.elementType = elementType; + } + + public DataType getMilvusTypeEnum() { + return milvusTypeEnum; + } + + public void setMilvusTypeEnum(DataType milvusTypeEnum) { + this.milvusTypeEnum = milvusTypeEnum; + } +} diff --git a/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusCreateCollection.java b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusCreateCollection.java new file mode 100644 index 0000000000..84e296f4d6 --- /dev/null +++ b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusCreateCollection.java @@ -0,0 +1,102 @@ +package com.alibaba.datax.plugin.writer.milvuswriter; + +import java.util.List; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.milvuswriter.enums.SchemaCreateModeEnum; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; + +import io.milvus.v2.common.DataType; +import io.milvus.v2.service.collection.request.AddFieldReq; +import io.milvus.v2.service.collection.request.CreateCollectionReq; +import lombok.extern.slf4j.Slf4j; + +import static io.milvus.v2.common.DataType.valueOf; + +/** + * @author ziming(子茗) + * @date 12/27/24 + * @description + */ +@Slf4j +public class MilvusCreateCollection { + + private Configuration config; + + MilvusCreateCollection(Configuration originalConfig) { + this.config = originalConfig; + } + + public void createCollectionByMode(MilvusClient milvusClient) { + String collection = this.config.getString(KeyConstant.COLLECTION); + SchemaCreateModeEnum schemaCreateMode = SchemaCreateModeEnum.getEnum(this.config.getString(KeyConstant.SCHAME_CREATE_MODE)); + List milvusColumnMeta = JSON.parseObject(config.getString(KeyConstant.COLUMN), new TypeReference>() { + }); + Boolean hasCollection = milvusClient.hasCollection(collection); + if (schemaCreateMode == SchemaCreateModeEnum.CREATEIFNOTEXIT) { + // create collection + if (hasCollection) { + log.info("collection[{}] already exists, continue create", collection); + } else { + log.info("creating collection[{}]", collection); + CreateCollectionReq.CollectionSchema collectionSchema = prepareCollectionSchema(milvusColumnMeta); + milvusClient.createCollection(collection, collectionSchema); + } + } else if (schemaCreateMode == SchemaCreateModeEnum.RECREATE) { + if (hasCollection) { + log.info("collection already exist, try to drop"); + milvusClient.dropCollection(collection); + } + log.info("creating collection[{}]", collection); + CreateCollectionReq.CollectionSchema collectionSchema = prepareCollectionSchema(milvusColumnMeta); + milvusClient.createCollection(collection, collectionSchema); + } else if (schemaCreateMode == SchemaCreateModeEnum.IGNORE && !hasCollection) { + log.error("Collection not exist, throw exception"); + throw new RuntimeException("Collection not exist"); + } + } + + private CreateCollectionReq.CollectionSchema prepareCollectionSchema(List milvusColumnMeta) { + CreateCollectionReq.CollectionSchema collectionSchema = CreateCollectionReq.CollectionSchema.builder().build(); + for (int i = 0; i < milvusColumnMeta.size(); i++) { + MilvusColumn milvusColumn = milvusColumnMeta.get(i); + AddFieldReq addFieldReq = AddFieldReq.builder() + .fieldName(milvusColumn.getName()) + .dataType(valueOf(milvusColumn.getType())) + .build(); + if (milvusColumn.getPrimaryKey() != null) { + addFieldReq.setIsPrimaryKey(milvusColumn.getPrimaryKey()); + } + if (milvusColumn.getDimension() != null) { + addFieldReq.setDimension(milvusColumn.getDimension()); + } + if (milvusColumn.getPartitionKey() != null) { + addFieldReq.setIsPartitionKey(milvusColumn.getPartitionKey()); + } + if (milvusColumn.getMaxLength() != null) { + addFieldReq.setMaxLength(milvusColumn.getMaxLength()); + } + if (milvusColumn.getAutoId() != null) { + addFieldReq.setAutoID(milvusColumn.getAutoId()); + } + if (milvusColumn.getMaxCapacity() != null) { + addFieldReq.setMaxCapacity(milvusColumn.getMaxCapacity()); + } + if (milvusColumn.getElementType() != null) { + addFieldReq.setElementType(DataType.valueOf(milvusColumn.getElementType())); + } + try { + collectionSchema.addField(addFieldReq); + } catch (Exception e) { + log.error("add filed[{}] error", milvusColumn.getName()); + throw e; + } + } + Boolean enableDynamic = config.getBool(KeyConstant.ENABLE_DYNAMIC_SCHEMA); + if (enableDynamic != null) { + collectionSchema.setEnableDynamicField(enableDynamic); + } + return collectionSchema; + } +} diff --git a/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusWriter.java b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusWriter.java new file mode 100644 index 0000000000..764a9d943e --- /dev/null +++ b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusWriter.java @@ -0,0 +1,110 @@ +package com.alibaba.datax.plugin.writer.milvuswriter; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; + +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +public class MilvusWriter extends Writer { + public static class Job extends Writer.Job { + private Configuration originalConfig = null; + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + originalConfig.getNecessaryValue(KeyConstant.ENDPOINT, MilvusWriterErrorCode.REQUIRED_VALUE); + originalConfig.getNecessaryValue(KeyConstant.COLUMN, MilvusWriterErrorCode.REQUIRED_VALUE); + originalConfig.getNecessaryValue(KeyConstant.COLLECTION, MilvusWriterErrorCode.REQUIRED_VALUE); + } + + @Override + public void prepare() { + //collection create process + MilvusClient milvusClient = new MilvusClient(originalConfig); + try { + MilvusCreateCollection milvusCreateCollection = new MilvusCreateCollection(originalConfig); + milvusCreateCollection.createCollectionByMode(milvusClient); + String collection = originalConfig.getString(KeyConstant.COLLECTION); + String partition = originalConfig.getString(KeyConstant.PARTITION); + if (partition != null && !milvusClient.hasPartition(collection, partition)) { + log.info("collection[{}] not contain partition[{}],try to create partition", collection, partition); + milvusClient.createPartition(collection, partition); + } + } catch (Exception e) { + throw DataXException.asDataXException(MilvusWriterErrorCode.MILVUS_COLLECTION, e.getMessage(), e); + } finally { + milvusClient.close(); + } + } + + /** + * 切分任务。
+ * + * @param mandatoryNumber 为了做到Reader、Writer任务数对等,这里要求Writer插件必须按照源端的切分数进行切分。否则框架报错! + */ + @Override + public List split(int mandatoryNumber) { + List configList = new ArrayList<>(); + for (int i = 0; i < mandatoryNumber; i++) { + configList.add(this.originalConfig.clone()); + } + return configList; + } + + @Override + public void destroy() { + + } + } + + public static class Task extends Writer.Task { + + private MilvusBufferWriter milvusBufferWriter; + MilvusClient milvusClient; + + @Override + public void init() { + log.info("Initializing Milvus writer"); + // get configuration + Configuration writerSliceConfig = this.getPluginJobConf(); + this.milvusClient = new MilvusClient(writerSliceConfig); + this.milvusBufferWriter = new MilvusBufferWriter(this.milvusClient, writerSliceConfig); + log.info("Milvus writer initialized"); + } + + @Override + public void startWrite(RecordReceiver lineReceiver) { + Record record = null; + while ((record = lineReceiver.getFromReader()) != null) { + milvusBufferWriter.add(record, this.getTaskPluginCollector()); + if (milvusBufferWriter.needCommit()) { + log.info("begin committing data size[{}]", milvusBufferWriter.getDataCacheSize()); + milvusBufferWriter.commit(); + } + } + if (milvusBufferWriter.getDataCacheSize() > 0) { + log.info("begin committing data size[{}]", milvusBufferWriter.getDataCacheSize()); + milvusBufferWriter.commit(); + } + } + + @Override + public void prepare() { + super.prepare(); + } + + @Override + public void destroy() { + if (this.milvusClient != null) { + this.milvusClient.close(); + } + } + } +} \ No newline at end of file diff --git a/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusWriterErrorCode.java b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusWriterErrorCode.java new file mode 100644 index 0000000000..7264160fd9 --- /dev/null +++ b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/MilvusWriterErrorCode.java @@ -0,0 +1,35 @@ +package com.alibaba.datax.plugin.writer.milvuswriter; + +import com.alibaba.datax.common.spi.ErrorCode; + +/** + * @author ziming(子茗) + * @date 12/27/24 + * @description + */ +public enum MilvusWriterErrorCode implements ErrorCode { + MILVUS_COLLECTION("MilvusWriter-01", "collection process error"), + REQUIRED_VALUE("MilvusWriter-02", "miss required parameter"); + private final String code; + private final String description; + + MilvusWriterErrorCode(String code, String description) { + this.code = code; + this.description = description; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + return String.format("Code:[%s], Description:[%s]. ", this.code, this.description); + } +} diff --git a/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/enums/SchemaCreateModeEnum.java b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/enums/SchemaCreateModeEnum.java new file mode 100644 index 0000000000..b8c88bf1b3 --- /dev/null +++ b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/enums/SchemaCreateModeEnum.java @@ -0,0 +1,34 @@ +package com.alibaba.datax.plugin.writer.milvuswriter.enums; + +import lombok.extern.slf4j.Slf4j; + +/** + * @author ziming(子茗) + * @date 12/27/24 + * @description + */ +@Slf4j +public enum SchemaCreateModeEnum { + CREATEIFNOTEXIT("createIfNotExist"), + IGNORE("ignore"), + RECREATE("recreate"); + String type; + + SchemaCreateModeEnum(String type) { + this.type = type; + } + + public String getType() { + return type; + } + + public static SchemaCreateModeEnum getEnum(String name) { + for (SchemaCreateModeEnum value : SchemaCreateModeEnum.values()) { + if (value.getType().equalsIgnoreCase(name)) { + return value; + } + } + log.info("use default CREATEIFNOTEXIT schame create mode"); + return CREATEIFNOTEXIT; + } +} \ No newline at end of file diff --git a/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/enums/WriteModeEnum.java b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/enums/WriteModeEnum.java new file mode 100644 index 0000000000..0098dbaddd --- /dev/null +++ b/milvuswriter/src/main/java/com/alibaba/datax/plugin/writer/milvuswriter/enums/WriteModeEnum.java @@ -0,0 +1,28 @@ +package com.alibaba.datax.plugin.writer.milvuswriter.enums; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public enum WriteModeEnum { + INSERT("insert"), + UPSERT("upsert"); + String mode; + + public String getMode() { + return mode; + } + + WriteModeEnum(String mode) { + this.mode = mode; + } + + public static WriteModeEnum getEnum(String mode) { + for (WriteModeEnum writeModeEnum : WriteModeEnum.values()) { + if (writeModeEnum.getMode().equalsIgnoreCase(mode)) { + return writeModeEnum; + } + } + log.info("use default write mode upsert"); + return UPSERT; + } +} diff --git a/milvuswriter/src/main/resources/plugin.json b/milvuswriter/src/main/resources/plugin.json new file mode 100644 index 0000000000..8b91230934 --- /dev/null +++ b/milvuswriter/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "milvuswriter", + "class": "com.alibaba.datax.plugin.writer.milvuswriter.MilvusWriter", + "description": "useScene: prod. mechanism: via milvusclient connect milvus write data concurrent.", + "developer": "nianliuu" +} diff --git a/milvuswriter/src/main/resources/plugin_job_template.json b/milvuswriter/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..33bd941a52 --- /dev/null +++ b/milvuswriter/src/main/resources/plugin_job_template.json @@ -0,0 +1,12 @@ +{ + "name": "milvuswriter", + "parameter": { + "endpoint": "", + "username": "", + "password": "", + "database": "", + "collection": "", + "column": [], + "enableDynamicSchema": "" + } +} \ No newline at end of file diff --git a/mongodbreader/doc/mongodbreader.md b/mongodbreader/doc/mongodbreader.md index 6d1e6a9991..297e598cd6 100644 --- a/mongodbreader/doc/mongodbreader.md +++ b/mongodbreader/doc/mongodbreader.md @@ -8,7 +8,7 @@ MongoDBReader 插件利用 MongoDB 的java客户端MongoClient进行MongoDB的 MongoDBReader通过Datax框架从MongoDB并行的读取数据,通过主控的JOB程序按照指定的规则对MongoDB中的数据进行分片,并行读取,然后将MongoDB支持的类型通过逐一判断转换成Datax支持的类型。 #### 3 功能说明 -* 该示例从ODPS读一份数据到MongoDB。 +* 该示例从MongoDB读一份数据到ODPS。 { "job": { @@ -114,8 +114,7 @@ MongoDBReader通过Datax框架从MongoDB并行的读取数据,通过主控的J "accessKey": "********************", "truncate": true, "odpsServer": "xxx/api", - "tunnelServer": "xxx", - "accountType": "aliyun" + "tunnelServer": "xxx" } } } @@ -127,6 +126,7 @@ MongoDBReader通过Datax框架从MongoDB并行的读取数据,通过主控的J * address: MongoDB的数据地址信息,因为MonogDB可能是个集群,则ip端口信息需要以Json数组的形式给出。【必填】 * userName:MongoDB的用户名。【选填】 * userPassword: MongoDB的密码。【选填】 +* authDb: MongoDB认证数据库【选填】 * collectionName: MonogoDB的集合名。【必填】 * column:MongoDB的文档列名。【必填】 * name:Column的名字。【必填】 @@ -147,4 +147,4 @@ MongoDBReader通过Datax框架从MongoDB并行的读取数据,通过主控的J #### 6 性能报告 -#### 7 测试报告 \ No newline at end of file +#### 7 测试报告 diff --git a/mongodbreader/src/main/java/com/alibaba/datax/plugin/reader/mongodbreader/MongoDBReader.java b/mongodbreader/src/main/java/com/alibaba/datax/plugin/reader/mongodbreader/MongoDBReader.java index ba7f07f43e..4d129a5af7 100644 --- a/mongodbreader/src/main/java/com/alibaba/datax/plugin/reader/mongodbreader/MongoDBReader.java +++ b/mongodbreader/src/main/java/com/alibaba/datax/plugin/reader/mongodbreader/MongoDBReader.java @@ -18,9 +18,9 @@ import com.alibaba.datax.common.util.Configuration; import com.alibaba.datax.plugin.reader.mongodbreader.util.CollectionSplitUtil; import com.alibaba.datax.plugin.reader.mongodbreader.util.MongoUtil; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONArray; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; import com.google.common.base.Joiner; import com.google.common.base.Strings; diff --git a/mongodbwriter/doc/mongodbwriter.md b/mongodbwriter/doc/mongodbwriter.md index e30008dbd8..93f502903e 100644 --- a/mongodbwriter/doc/mongodbwriter.md +++ b/mongodbwriter/doc/mongodbwriter.md @@ -116,10 +116,10 @@ MongoDBWriter通过Datax框架获取Reader生成的数据,然后将Datax支持 "type": "int" } ], - "upsertInfo": { - "isUpsert": "true", - "upsertKey": "unique_id" - } + "writeMode": { + "isReplace": "true", + "replaceKey": "unique_id" + } } } } @@ -135,11 +135,11 @@ MongoDBWriter通过Datax框架获取Reader生成的数据,然后将Datax支持 * collectionName: MonogoDB的集合名。【必填】 * column:MongoDB的文档列名。【必填】 * name:Column的名字。【必填】 -* type:Column的类型。【选填】 +* type:Column的类型。【必填】 * splitter:特殊分隔符,当且仅当要处理的字符串要用分隔符分隔为字符数组时,才使用这个参数,通过这个参数指定的分隔符,将字符串分隔存储到MongoDB的数组中。【选填】 -* upsertInfo:指定了传输数据时更新的信息。【选填】 -* isUpsert:当设置为true时,表示针对相同的upsertKey做更新操作。【选填】 -* upsertKey:upsertKey指定了没行记录的业务主键。用来做更新时使用。【选填】 +* writeMode:指定了传输数据时更新的信息。【选填】 +* isReplace:当设置为true时,表示针对相同的replaceKey做更新操作。【选填】 +* replaceKey:replaceKey指定了每行记录的业务主键。用来做更新时使用。【选填】 #### 5 类型转换 @@ -154,4 +154,4 @@ MongoDBWriter通过Datax框架获取Reader生成的数据,然后将Datax支持 #### 6 性能报告 -#### 7 测试报告 \ No newline at end of file +#### 7 测试报告 diff --git a/mongodbwriter/src/main/java/com/alibaba/datax/plugin/writer/mongodbwriter/MongoDBWriter.java b/mongodbwriter/src/main/java/com/alibaba/datax/plugin/writer/mongodbwriter/MongoDBWriter.java index 66c75078d3..76f35a40f0 100644 --- a/mongodbwriter/src/main/java/com/alibaba/datax/plugin/writer/mongodbwriter/MongoDBWriter.java +++ b/mongodbwriter/src/main/java/com/alibaba/datax/plugin/writer/mongodbwriter/MongoDBWriter.java @@ -7,9 +7,9 @@ import com.alibaba.datax.common.util.Configuration; import com.alibaba.datax.plugin.rdbms.writer.Key; import com.alibaba.datax.plugin.writer.mongodbwriter.util.MongoUtil; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONArray; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; import com.google.common.base.Strings; import com.mongodb.*; import com.mongodb.client.MongoCollection; diff --git a/mysqlreader/doc/mysqlreader.md b/mysqlreader/doc/mysqlreader.md index 3ae52afbf2..bae4bce0f6 100644 --- a/mysqlreader/doc/mysqlreader.md +++ b/mysqlreader/doc/mysqlreader.md @@ -165,7 +165,7 @@ MysqlReader插件实现了从Mysql读取数据。在底层实现上,MysqlReade 支持常量配置,用户需要按照Mysql SQL语法格式: ["id", "\`table\`", "1", "'bazhen.csy'", "null", "to_char(a + 1)", "2.3" , "true"] - id为普通列名,\`table\`为包含保留在的列名,1为整形数字常量,'bazhen.csy'为字符串常量,null为空指针,to_char(a + 1)为表达式,2.3为浮点数,true为布尔值。 + id为普通列名,\`table\`为包含保留字的列名,1为整形数字常量,'bazhen.csy'为字符串常量,null为空指针,to_char(a + 1)为表达式,2.3为浮点数,true为布尔值。 * 必选:是
diff --git a/mysqlreader/pom.xml b/mysqlreader/pom.xml index 08183272e7..621326ae27 100755 --- a/mysqlreader/pom.xml +++ b/mysqlreader/pom.xml @@ -40,7 +40,7 @@ mysql mysql-connector-java - 5.1.34 + ${mysql.driver.version} diff --git a/mysqlwriter/doc/mysqlwriter.md b/mysqlwriter/doc/mysqlwriter.md index 5368775cc1..294846c2e6 100644 --- a/mysqlwriter/doc/mysqlwriter.md +++ b/mysqlwriter/doc/mysqlwriter.md @@ -6,7 +6,7 @@ ## 1 快速介绍 -MysqlWriter 插件实现了写入数据到 Mysql 主库的目的表的功能。在底层实现上, MysqlWriter 通过 JDBC 连接远程 Mysql 数据库,并执行相应的 insert into ... 或者 ( replace into ...) 的 sql 语句将数据写入 Mysql,内部会分批次提交入库,需要数据库本身采用 innodb 引擎。 +MysqlWriter 插件实现了写入数据到 Mysql 主库的目的表的功能。在底层实现上, MysqlWriter 通过 JDBC 连接远程 Mysql 数据库,并执行相应的 insert into ... 或者 ( replace into ...) 的 sql 语句将数据写入 Mysql,内部会分批次提交入库,需要数据库本身采用 InnoDB 引擎。 MysqlWriter 面向ETL开发工程师,他们使用 MysqlWriter 从数仓导入数据到 Mysql。同时 MysqlWriter 亦可以作为数据迁移工具为DBA等用户提供服务。 diff --git a/mysqlwriter/pom.xml b/mysqlwriter/pom.xml index 1161802205..1c3891f5d5 100755 --- a/mysqlwriter/pom.xml +++ b/mysqlwriter/pom.xml @@ -40,7 +40,7 @@ mysql mysql-connector-java - 5.1.34 + ${mysql.driver.version} diff --git a/neo4jwriter/doc/neo4jwriter.md b/neo4jwriter/doc/neo4jwriter.md new file mode 100644 index 0000000000..0c6e356cfb --- /dev/null +++ b/neo4jwriter/doc/neo4jwriter.md @@ -0,0 +1,193 @@ +# DataX neo4jWriter 插件文档 + +## 功能简介 + +本目前市面上的neo4j 批量导入主要有Cypher Create,Load CSV,第三方或者官方提供的Batch Import。Load CSV支持节点10W级别一下,Batch Import 需要对数据库进行停机。要想实现不停机的数据写入,Cypher是最好的方式。 + +## 支持版本 + +支持Neo4j 4 和Neo4j 5,如果是Neo4j 3,需要自行将驱动降低至相对应的版本进行编译。 + +## 实现原理 + +将datax的数据转换成了neo4j驱动能识别的对象,利用 unwind 语法进行批量插入。 + +## 如何配置 + +### 配置项介绍 + +| 配置 | 说明 | 是否必须 | 默认值 | 示例 | +|:-------------------------------|--------------------| -------- | ------ | ---------------------------------------------------- | +| database | 数据库名字 | 是 | - | neo4j | +| uri | 数据库访问链接 | 是 | - | bolt://localhost:7687 | +| username | 访问用户名 | 是 | - | neo4j | +| password | 访问密码 | 是 | - | neo4j | +| bearerToken | 权限相关 | 否 | - | - | +| kerberosTicket | 权限相关 | 否 | - | - | +| cypher | 同步语句 | 是 | - | unwind $batch as row create(p) set p.name = row.name | +| batchDataVariableName | unwind 携带的数据变量名 | | | batch | +| properties | 定义neo4j中数据的属性名字和类型 | 是 | - | 见后续案例 | +| batchSize | 一批写入数据量 | 否 | 1000 | | +| maxTransactionRetryTimeSeconds | 事务运行最长时间 | 否 | 30秒 | 30 | +| maxConnectionTimeoutSeconds | 驱动最长链接时间 | 否 | 30秒 | 30 | +| retryTimes | 发生错误的重试次数 | 否 | 3次 | 3 | +| retrySleepMills | 重试失败后的等待时间 | 否 | 3秒 | 3 | + +### 支持的数据类型 +> 配置时均忽略大小写 +``` +BOOLEAN, +STRING, +LONG, +SHORT, +INTEGER, +DOUBLE, +FLOAT, +LOCAL_DATE, +LOCAL_TIME, +LOCAL_DATE_TIME, +LIST, +//map类型支持 . 属性表达式取值 +MAP, +CHAR_ARRAY, +BYTE_ARRAY, +BOOLEAN_ARRAY, +STRING_ARRAY, +LONG_ARRAY, +INT_ARRAY, +SHORT_ARRAY, +DOUBLE_ARRAY, +FLOAT_ARRAY, +Object_ARRAY +``` + +### 写节点 + +这里提供了一个写节点包含很多类型属性的例子。你可以在我的测试方法中运行。 + +```json +"writer": { + "name": "neo4jWriter", + "parameter": { + "uri": "neo4j://localhost:7687", + "username": "neo4j", + "password": "Test@12343", + "database": "neo4j", + "cypher": "unwind $batch as row create(p:Person) set p.pbool = row.pbool,p.pstring = row.pstring,p.plong = row.plong,p.pshort = row.pshort,p.pdouble=row.pdouble,p.pstringarr=row.pstringarr,p.plocaldate=row.plocaldate", + "batchDataVariableName": "batch", + "batchSize": "33", + "properties": [ + { + "name": "pbool", + "type": "BOOLEAN" + }, + { + "name": "pstring", + "type": "STRING" + }, + { + "name": "plong", + "type": "LONG" + }, + { + "name": "pshort", + "type": "SHORT" + }, + { + "name": "pdouble", + "type": "DOUBLE" + }, + { + "name": "pstringarr", + "type": "STRING_ARRAY", + "split": "," + }, + { + "name": "plocaldate", + "type": "LOCAL_DATE", + "dateFormat": "yyyy-MM-dd" + } + ] + } + } +``` + +### 写关系 + +```json +"writer": { + "name": "neo4jWriter", + "parameter": { + "uri": "neo4j://localhost:7687", + "username": "neo4j", + "password": "Test@12343", + "database": "neo4j", + "cypher": "unwind $batch as row match(p1:Person) where p1.id = row.startNodeId match(p2:Person) where p2.id = row.endNodeId create (p1)-[:LINK]->(p2)", + "batchDataVariableName": "batch", + "batch_size": "33", + "properties": [ + { + "name": "startNodeId", + "type": "STRING" + }, + { + "name": "endNodeId", + "type": "STRING" + } + ] + } + } +``` + +### 节点/关系类型动态写 + +> 需要使用AOPC函数拓展,如果你的数据库没有,请安装APOC函数拓展 + +```json + "writer": { + "name": "neo4jWriter", + "parameter": { + "uri": "bolt://localhost:7687", + "username": "yourUserName", + "password": "yourPassword", + "database": "yourDataBase", + "cypher": "unwind $batch as row CALL apoc.cypher.doIt( 'create (n:`' + row.Label + '`{id:$id})' ,{id: row.id} ) YIELD value RETURN 1 ", + "batchDataVariableName": "batch", + "batch_size": "1", + "properties": [ + { + "name": "Label", + "type": "STRING" + }, + { + "name": "id", + "type": "STRING" + } + ] + } + } +``` + +## 注意事项 + +* properties定义的顺序需要与reader端顺序一一对应。 +* 灵活使用map类型,可以免去很多数据加工的烦恼。在cypher中,可以根据 . 属性访问符号一直取值。比如 unwind $batch as row create (p) set p.name = row.prop.name,set p.age = row.prop.age,在这个例子中,prop是map类型,包含name和age两个属性。 +* 如果提示事务超时,建议调大事务运行时间或者调小batchSize +* 如果用于更新场景,遇到死锁问题影响写入,建议二开源码加入死锁异常检测,并进行重试。 + +## 性能报告 + +**JVM参数** + +16G G1垃圾收集器 8核心 + +**Neo4j数据库配置** + +32核心,256G + +**datax 配置** + +* Channel 20 batchsize = 1000 +* 任务平均流量:15.23MB/s +* 记录写入速度:44440 rec/s +* 读出记录总数:2222013 diff --git a/neo4jwriter/pom.xml b/neo4jwriter/pom.xml new file mode 100644 index 0000000000..2ff0f55021 --- /dev/null +++ b/neo4jwriter/pom.xml @@ -0,0 +1,100 @@ + + + + com.alibaba.datax + datax-all + 0.0.1-SNAPSHOT + + 4.0.0 + + neo4jwriter + neo4jwriter + jar + + + 8 + 8 + UTF-8 + 4.4.9 + 4.13.2 + 1.17.6 + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.neo4j.driver + neo4j-java-driver + ${neo4j-java-driver.version} + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + + org.testcontainers + testcontainers + ${test.container.version} + + + + junit + junit + ${junit4.version} + test + + + + + + + src/main/resources + + **/*.* + + true + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + diff --git a/neo4jwriter/src/main/assembly/package.xml b/neo4jwriter/src/main/assembly/package.xml new file mode 100644 index 0000000000..3acbe6740d --- /dev/null +++ b/neo4jwriter/src/main/assembly/package.xml @@ -0,0 +1,35 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/writer/neo4jwriter + + + target/ + + neo4jwriter-0.0.1-SNAPSHOT.jar + + plugin/writer/neo4jwriter + + + + + + false + plugin/writer/neo4jwriter/libs + runtime + + + diff --git a/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/Neo4jClient.java b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/Neo4jClient.java new file mode 100644 index 0000000000..4451bbdf2d --- /dev/null +++ b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/Neo4jClient.java @@ -0,0 +1,256 @@ +package com.alibaba.datax.plugin.writer.neo4jwriter; + + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.RetryUtil; +import com.alibaba.datax.plugin.writer.neo4jwriter.adapter.DateAdapter; +import com.alibaba.datax.plugin.writer.neo4jwriter.adapter.ValueAdapter; +import com.alibaba.datax.plugin.writer.neo4jwriter.config.Neo4jProperty; +import com.alibaba.datax.plugin.writer.neo4jwriter.exception.Neo4jErrorCode; +import com.alibaba.fastjson2.JSON; +import org.apache.commons.lang3.StringUtils; +import org.neo4j.driver.*; +import org.neo4j.driver.exceptions.Neo4jException; +import org.neo4j.driver.internal.value.MapValue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.concurrent.TimeUnit; + +import static com.alibaba.datax.plugin.writer.neo4jwriter.config.ConfigConstants.*; +import static com.alibaba.datax.plugin.writer.neo4jwriter.exception.Neo4jErrorCode.DATABASE_ERROR; + +public class Neo4jClient { + private static final Logger LOGGER = LoggerFactory.getLogger(Neo4jClient.class); + private Driver driver; + + private WriteConfig writeConfig; + private RetryConfig retryConfig; + private TaskPluginCollector taskPluginCollector; + + private Session session; + + private List writerBuffer; + + + public Neo4jClient(Driver driver, + WriteConfig writeConfig, + RetryConfig retryConfig, + TaskPluginCollector taskPluginCollector) { + this.driver = driver; + this.writeConfig = writeConfig; + this.retryConfig = retryConfig; + this.taskPluginCollector = taskPluginCollector; + this.writerBuffer = new ArrayList<>(writeConfig.batchSize); + } + + public void init() { + String database = writeConfig.database; + //neo4j 3.x 没有数据库 + if (null != database && !"".equals(database)) { + this.session = driver.session(SessionConfig.forDatabase(database)); + } else { + this.session = driver.session(); + } + } + + public static Neo4jClient build(Configuration config, TaskPluginCollector taskPluginCollector) { + + Driver driver = buildNeo4jDriver(config); + String cypher = checkCypher(config); + String database = config.getString(DATABASE.getKey()); + String batchVariableName = config.getString(BATCH_DATA_VARIABLE_NAME.getKey(), + BATCH_DATA_VARIABLE_NAME.getDefaultValue()); + List neo4jProperties = JSON.parseArray(config.getString(NEO4J_PROPERTIES.getKey()), Neo4jProperty.class); + int batchSize = config.getInt(BATCH_SIZE.getKey(), BATCH_SIZE.getDefaultValue()); + int retryTimes = config.getInt(RETRY_TIMES.getKey(), RETRY_TIMES.getDefaultValue()); + + return new Neo4jClient(driver, + new WriteConfig(cypher, database, batchVariableName, neo4jProperties, batchSize), + new RetryConfig(retryTimes, config.getLong(RETRY_SLEEP_MILLS.getKey(), RETRY_SLEEP_MILLS.getDefaultValue())), + taskPluginCollector + ); + } + + private static String checkCypher(Configuration config) { + String cypher = config.getString(CYPHER.getKey()); + if (StringUtils.isBlank(cypher)) { + throw DataXException.asDataXException(Neo4jErrorCode.CONFIG_INVALID, "cypher must not null or empty"); + } + return cypher; + } + + private static Driver buildNeo4jDriver(Configuration config) { + + Config.ConfigBuilder configBuilder = Config.builder().withMaxConnectionPoolSize(1); + String uri = checkUriConfig(config); + + //connection timeout + //连接超时时间 + Long maxConnTime = config.getLong(MAX_CONNECTION_TIMEOUT_SECONDS.getKey(), MAX_TRANSACTION_RETRY_TIME.getDefaultValue()); + configBuilder + .withConnectionAcquisitionTimeout( + maxConnTime * 2, TimeUnit.SECONDS) + .withConnectionTimeout(maxConnTime, TimeUnit.SECONDS); + + + //transaction timeout + //事务运行超时时间 + Long txRetryTime = config.getLong(MAX_TRANSACTION_RETRY_TIME.getKey(), MAX_TRANSACTION_RETRY_TIME.getDefaultValue()); + configBuilder.withMaxTransactionRetryTime(txRetryTime, TimeUnit.SECONDS); + String username = config.getString(USERNAME.getKey()); + String password = config.getString(PASSWORD.getKey()); + String bearerToken = config.getString(BEARER_TOKEN.getKey()); + String kerberosTicket = config.getString(KERBEROS_TICKET.getKey()); + + if (StringUtils.isNotBlank(username) && StringUtils.isNotBlank(password)) { + + return GraphDatabase.driver(uri, AuthTokens.basic(username, password), configBuilder.build()); + + } else if (StringUtils.isNotBlank(bearerToken)) { + + return GraphDatabase.driver(uri, AuthTokens.bearer(bearerToken), configBuilder.build()); + + } else if (StringUtils.isNotBlank(kerberosTicket)) { + + return GraphDatabase.driver(uri, AuthTokens.kerberos(kerberosTicket), configBuilder.build()); + + } + + throw DataXException.asDataXException(Neo4jErrorCode.CONFIG_INVALID, "Invalid Auth config."); + } + + private static String checkUriConfig(Configuration config) { + String uri = config.getString(URI.getKey()); + if (null == uri || uri.length() == 0) { + throw DataXException.asDataXException(Neo4jErrorCode.CONFIG_INVALID, "Invalid uri configuration"); + } + return uri; + } + + public void destroy() { + tryFlushBuffer(); + if (driver != null) { + driver.close(); + } + if (session != null) { + session.close(); + } + DateAdapter.destroy(); + } + + private void tryFlushBuffer() { + if (!writerBuffer.isEmpty()) { + doWrite(writerBuffer); + writerBuffer.clear(); + } + } + + private void tryBatchWrite() { + if (!writerBuffer.isEmpty() && writerBuffer.size() >= writeConfig.batchSize) { + doWrite(writerBuffer); + writerBuffer.clear(); + } + } + + private void doWrite(List values) { + Value batchValues = Values.parameters(this.writeConfig.batchVariableName, values); + Query query = new Query(this.writeConfig.cypher, batchValues); +// LOGGER.debug("query:{}", query.text()); +// LOGGER.debug("batch:{}", toUnwindStr(values)); + try { + RetryUtil.executeWithRetry(() -> { + session.writeTransaction(tx -> tx.run(query)); + return null; + }, this.retryConfig.retryTimes, retryConfig.retrySleepMills, true, + Collections.singletonList(Neo4jException.class)); + } catch (Exception e) { + LOGGER.error("an exception occurred while writing to the database,message:{}", e.getMessage()); + throw DataXException.asDataXException(DATABASE_ERROR, e.getMessage()); + } + + + } + + private String toUnwindStr(List values) { + StringJoiner joiner = new StringJoiner(","); + for (MapValue value : values) { + joiner.add(value.toString()); + } + return "[" + joiner + "]"; + } + + public void tryWrite(Record record) { + MapValue neo4jValue = checkAndConvert(record); + writerBuffer.add(neo4jValue); + tryBatchWrite(); + } + + private MapValue checkAndConvert(Record record) { + int sourceColNum = record.getColumnNumber(); + List neo4jProperties = writeConfig.neo4jProperties; + if (neo4jProperties == null || neo4jProperties.size() != sourceColNum) { + throw new DataXException(Neo4jErrorCode.CONFIG_INVALID, "the read and write columns do not match!"); + } + Map data = new HashMap<>(sourceColNum * 4 / 3); + for (int i = 0; i < sourceColNum; i++) { + Column column = record.getColumn(i); + Neo4jProperty neo4jProperty = neo4jProperties.get(i); + try { + + Value value = ValueAdapter.column2Value(column, neo4jProperty); + data.put(neo4jProperty.getName(), value); + } catch (Exception e) { + LOGGER.info("dirty record:{},message :{}", column, e.getMessage()); + this.taskPluginCollector.collectDirtyRecord(record, e.getMessage()); + } + } + return new MapValue(data); + } + + public List getNeo4jFields() { + return this.writeConfig.neo4jProperties; + } + + + static class RetryConfig { + int retryTimes; + long retrySleepMills; + + RetryConfig(int retryTimes, long retrySleepMills) { + this.retryTimes = retryTimes; + this.retrySleepMills = retrySleepMills; + } + } + + static class WriteConfig { + String cypher; + + String database; + + String batchVariableName; + + List neo4jProperties; + + int batchSize; + + public WriteConfig(String cypher, + String database, + String batchVariableName, + List neo4jProperties, + int batchSize) { + this.cypher = cypher; + this.database = database; + this.batchVariableName = batchVariableName; + this.neo4jProperties = neo4jProperties; + this.batchSize = batchSize; + } + + + } +} diff --git a/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/Neo4jWriter.java b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/Neo4jWriter.java new file mode 100644 index 0000000000..6a589c1d03 --- /dev/null +++ b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/Neo4jWriter.java @@ -0,0 +1,64 @@ +package com.alibaba.datax.plugin.writer.neo4jwriter; + +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.element.Record; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class Neo4jWriter extends Writer { + public static class Job extends Writer.Job { + private static final Logger LOGGER = LoggerFactory.getLogger(Job.class); + + private Configuration jobConf = null; + @Override + public void init() { + LOGGER.info("Neo4jWriter Job init success"); + this.jobConf = getPluginJobConf(); + } + + @Override + public void destroy() { + LOGGER.info("Neo4jWriter Job destroyed"); + } + + @Override + public List split(int mandatoryNumber) { + List configurations = new ArrayList(mandatoryNumber); + for (int i = 0; i < mandatoryNumber; i++) { + configurations.add(this.jobConf.clone()); + } + return configurations; + } + } + + public static class Task extends Writer.Task { + private static final Logger TASK_LOGGER = LoggerFactory.getLogger(Task.class); + private Neo4jClient neo4jClient; + @Override + public void init() { + Configuration taskConf = super.getPluginJobConf(); + this.neo4jClient = Neo4jClient.build(taskConf,getTaskPluginCollector()); + this.neo4jClient.init(); + TASK_LOGGER.info("neo4j writer task init success."); + } + + @Override + public void destroy() { + this.neo4jClient.destroy(); + TASK_LOGGER.info("neo4j writer task destroyed."); + } + + @Override + public void startWrite(RecordReceiver receiver) { + Record record; + while ((record = receiver.getFromReader()) != null){ + this.neo4jClient.tryWrite(record); + } + } + } +} diff --git a/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/adapter/DateAdapter.java b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/adapter/DateAdapter.java new file mode 100644 index 0000000000..51b214bd07 --- /dev/null +++ b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/adapter/DateAdapter.java @@ -0,0 +1,70 @@ +package com.alibaba.datax.plugin.writer.neo4jwriter.adapter; + + +import com.alibaba.datax.plugin.writer.neo4jwriter.config.Neo4jProperty; +import org.testcontainers.shaded.com.google.common.base.Supplier; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; + +/** + * @author fuyouj + */ +public class DateAdapter { + private static final ThreadLocal LOCAL_DATE_FORMATTER_MAP = new ThreadLocal<>(); + private static final ThreadLocal LOCAL_TIME_FORMATTER_MAP = new ThreadLocal<>(); + private static final ThreadLocal LOCAL_DATE_TIME_FORMATTER_MAP = new ThreadLocal<>(); + private static final String DEFAULT_LOCAL_DATE_FORMATTER = "yyyy-MM-dd"; + private static final String DEFAULT_LOCAL_TIME_FORMATTER = "HH:mm:ss"; + private static final String DEFAULT_LOCAL_DATE_TIME_FORMATTER = "yyyy-MM-dd HH:mm:ss"; + + + public static LocalDate localDate(String text, Neo4jProperty neo4jProperty) { + if (LOCAL_DATE_FORMATTER_MAP.get() != null) { + return LocalDate.parse(text, LOCAL_DATE_FORMATTER_MAP.get()); + } + + String format = getOrDefault(neo4jProperty::getDateFormat, DEFAULT_LOCAL_DATE_FORMATTER); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format); + LOCAL_DATE_FORMATTER_MAP.set(dateTimeFormatter); + return LocalDate.parse(text, dateTimeFormatter); + } + + public static String getOrDefault(Supplier dateFormat, String defaultFormat) { + String format = dateFormat.get(); + if (null == format || "".equals(format)) { + return defaultFormat; + } else { + return format; + } + } + + public static void destroy() { + LOCAL_DATE_FORMATTER_MAP.remove(); + LOCAL_TIME_FORMATTER_MAP.remove(); + LOCAL_DATE_TIME_FORMATTER_MAP.remove(); + } + + public static LocalTime localTime(String text, Neo4jProperty neo4JProperty) { + if (LOCAL_TIME_FORMATTER_MAP.get() != null) { + return LocalTime.parse(text, LOCAL_TIME_FORMATTER_MAP.get()); + } + + String format = getOrDefault(neo4JProperty::getDateFormat, DEFAULT_LOCAL_TIME_FORMATTER); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format); + LOCAL_TIME_FORMATTER_MAP.set(dateTimeFormatter); + return LocalTime.parse(text, dateTimeFormatter); + } + + public static LocalDateTime localDateTime(String text, Neo4jProperty neo4JProperty) { + if (LOCAL_DATE_TIME_FORMATTER_MAP.get() != null){ + return LocalDateTime.parse(text,LOCAL_DATE_TIME_FORMATTER_MAP.get()); + } + String format = getOrDefault(neo4JProperty::getDateFormat, DEFAULT_LOCAL_DATE_TIME_FORMATTER); + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format); + LOCAL_DATE_TIME_FORMATTER_MAP.set(dateTimeFormatter); + return LocalDateTime.parse(text, dateTimeFormatter); + } +} diff --git a/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/adapter/ValueAdapter.java b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/adapter/ValueAdapter.java new file mode 100644 index 0000000000..d0f4044d79 --- /dev/null +++ b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/adapter/ValueAdapter.java @@ -0,0 +1,95 @@ +package com.alibaba.datax.plugin.writer.neo4jwriter.adapter; + + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.plugin.writer.neo4jwriter.config.Neo4jProperty; +import com.alibaba.datax.plugin.writer.neo4jwriter.element.PropertyType; +import com.alibaba.fastjson2.JSON; +import org.neo4j.driver.Value; +import org.neo4j.driver.Values; +import org.neo4j.driver.internal.value.NullValue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +/** + * @author fuyouj + */ +public class ValueAdapter { + + + public static Value column2Value(final Column column, final Neo4jProperty neo4JProperty) { + + String typeStr = neo4JProperty.getType(); + PropertyType type = PropertyType.fromStrIgnoreCase(typeStr); + if (column.asString() == null) { + return NullValue.NULL; + } + + switch (type) { + case NULL: + return NullValue.NULL; + case MAP: + return Values.value(JSON.parseObject(column.asString(), Map.class)); + case BOOLEAN: + return Values.value(column.asBoolean()); + case STRING: + return Values.value(column.asString()); + case INTEGER: + case LONG: + return Values.value(column.asLong()); + case SHORT: + return Values.value(Short.valueOf(column.asString())); + case FLOAT: + case DOUBLE: + return Values.value(column.asDouble()); + case BYTE_ARRAY: + return Values.value(parseArrayType(neo4JProperty, column.asString(), Byte::valueOf)); + case CHAR_ARRAY: + return Values.value(parseArrayType(neo4JProperty, column.asString(), (s) -> s.charAt(0))); + case BOOLEAN_ARRAY: + return Values.value(parseArrayType(neo4JProperty, column.asString(), Boolean::valueOf)); + case STRING_ARRAY: + case Object_ARRAY: + case LIST: + return Values.value(parseArrayType(neo4JProperty, column.asString(), Function.identity())); + case LONG_ARRAY: + return Values.value(parseArrayType(neo4JProperty, column.asString(), Long::valueOf)); + case INT_ARRAY: + return Values.value(parseArrayType(neo4JProperty, column.asString(), Integer::valueOf)); + case SHORT_ARRAY: + return Values.value(parseArrayType(neo4JProperty, column.asString(), Short::valueOf)); + case DOUBLE_ARRAY: + case FLOAT_ARRAY: + return Values.value(parseArrayType(neo4JProperty, column.asString(), Double::valueOf)); + case LOCAL_DATE: + return Values.value(DateAdapter.localDate(column.asString(), neo4JProperty)); + case LOCAL_TIME: + return Values.value(DateAdapter.localTime(column.asString(), neo4JProperty)); + case LOCAL_DATE_TIME: + return Values.value(DateAdapter.localDateTime(column.asString(), neo4JProperty)); + default: + return Values.value(column.getRawData()); + + } + } + + + private static List parseArrayType(final Neo4jProperty neo4JProperty, + final String strValue, + final Function convertFunc) { + if (null == strValue || "".equals(strValue)) { + return Collections.emptyList(); + } + String split = neo4JProperty.getSplitOrDefault(); + String[] strArr = strValue.split(split); + List ans = new ArrayList<>(); + for (String s : strArr) { + ans.add(convertFunc.apply(s)); + } + return ans; + } +} diff --git a/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/config/ConfigConstants.java b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/config/ConfigConstants.java new file mode 100644 index 0000000000..eed3588e2d --- /dev/null +++ b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/config/ConfigConstants.java @@ -0,0 +1,116 @@ +package com.alibaba.datax.plugin.writer.neo4jwriter.config; + + +import java.util.List; + +/** + * @author fuyouj + */ +public final class ConfigConstants { + + public static final Long DEFAULT_MAX_TRANSACTION_RETRY_SECONDS = 30L; + + public static final Long DEFAULT_MAX_CONNECTION_SECONDS = 30L; + + + + public static final Option RETRY_TIMES = + Option.builder() + .key("retryTimes") + .defaultValue(3) + .desc("The number of overwrites when an error occurs") + .build(); + + public static final Option RETRY_SLEEP_MILLS = + Option.builder() + .key("retrySleepMills") + .defaultValue(3000L) + .build(); + + /** + * cluster mode please reference + * how to connect cluster mode + */ + public static final Option URI = + Option.builder() + .key("uri") + .noDefaultValue() + .desc("uir of neo4j database") + .build(); + + public static final Option USERNAME = + Option.builder() + .key("username") + .noDefaultValue() + .desc("username for accessing the neo4j database") + .build(); + + public static final Option PASSWORD = + Option.builder() + .key("password") + .noDefaultValue() + .desc("password for accessing the neo4j database") + .build(); + + public static final Option BEARER_TOKEN = + Option.builder() + .key("bearerToken") + .noDefaultValue() + .desc("base64 encoded bearer token of the Neo4j. for Auth.") + .build(); + + public static final Option KERBEROS_TICKET = + Option.builder() + .key("kerberosTicket") + .noDefaultValue() + .desc("base64 encoded kerberos ticket of the Neo4j. for Auth.") + .build(); + + public static final Option DATABASE = + Option.builder() + .key("database") + .noDefaultValue() + .desc("database name.") + .build(); + + public static final Option CYPHER = + Option.builder() + .key("cypher") + .noDefaultValue() + .desc("cypher query.") + .build(); + + public static final Option MAX_TRANSACTION_RETRY_TIME = + Option.builder() + .key("maxTransactionRetryTimeSeconds") + .defaultValue(DEFAULT_MAX_TRANSACTION_RETRY_SECONDS) + .desc("maximum transaction retry time(seconds). transaction fail if exceeded.") + .build(); + public static final Option MAX_CONNECTION_TIMEOUT_SECONDS = + Option.builder() + .key("maxConnectionTimeoutSeconds") + .defaultValue(DEFAULT_MAX_CONNECTION_SECONDS) + .desc("The maximum amount of time to wait for a TCP connection to be established (seconds).") + .build(); + + public static final Option BATCH_DATA_VARIABLE_NAME = + Option.builder() + .key("batchDataVariableName") + .defaultValue("batch") + .desc("in a cypher statement, a variable name that represents a batch of data") + .build(); + + public static final Option> NEO4J_PROPERTIES = + Option.>builder() + .key("properties") + .noDefaultValue() + .desc("neo4j node or relation`s props") + .build(); + + public static final Option BATCH_SIZE = + Option.builder(). + key("batchSize") + .defaultValue(1000) + .desc("max batch size") + .build(); +} diff --git a/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/config/Neo4jProperty.java b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/config/Neo4jProperty.java new file mode 100644 index 0000000000..5c5867b3b0 --- /dev/null +++ b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/config/Neo4jProperty.java @@ -0,0 +1,82 @@ +package com.alibaba.datax.plugin.writer.neo4jwriter.config; + +/** + * 由于dataX并不能传输数据的元数据,所以只能在writer端定义每列数据的名字 + * datax does not support data metadata, + * only the name of each column of data can be defined on neo4j writer + * + * @author fuyouj + */ +public class Neo4jProperty { + public static final String DEFAULT_SPLIT = ","; + + /** + * name of neo4j field + */ + private String name; + + /** + * neo4j type + * reference by org.neo4j.driver.Values + */ + private String type; + + /** + * for date + */ + private String dateFormat; + + /** + * for array type + */ + private String split; + + public Neo4jProperty() { + } + + public Neo4jProperty(String name, String type, String format, String split) { + this.name = name; + this.type = type; + this.dateFormat = format; + this.split = split; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDateFormat() { + return dateFormat; + } + + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } + + public String getSplit() { + return getSplitOrDefault(); + } + + public String getSplitOrDefault() { + if (split == null || "".equals(split)) { + return DEFAULT_SPLIT; + } + return split; + } + + public void setSplit(String split) { + this.split = split; + } +} diff --git a/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/config/Option.java b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/config/Option.java new file mode 100644 index 0000000000..f22bd2054e --- /dev/null +++ b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/config/Option.java @@ -0,0 +1,65 @@ +package com.alibaba.datax.plugin.writer.neo4jwriter.config; + + +public class Option { + + public static class Builder { + private String key; + private String desc; + + private T defaultValue; + + public Builder key(String key) { + this.key = key; + return this; + } + + public Builder desc(String desc) { + this.desc = desc; + return this; + } + + public Builder defaultValue(T defaultValue) { + this.defaultValue = defaultValue; + return this; + } + + public Builder noDefaultValue() { + return this; + } + + public Option build() { + return new Option<>(this.key, this.desc, this.defaultValue); + } + } + + private final String key; + private final String desc; + + private final T defaultValue; + + public Option(String key, String desc, T defaultValue) { + this.key = key; + this.desc = desc; + this.defaultValue = defaultValue; + } + + public static Builder builder(){ + return new Builder<>(); + } + + public String getKey() { + return key; + } + + public String getDesc() { + return desc; + } + + public T getDefaultValue() { + if (defaultValue == null){ + throw new IllegalStateException(key + ":defaultValue is null"); + } + return defaultValue; + } +} diff --git a/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/element/PropertyType.java b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/element/PropertyType.java new file mode 100644 index 0000000000..b3446de730 --- /dev/null +++ b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/element/PropertyType.java @@ -0,0 +1,40 @@ +package com.alibaba.datax.plugin.writer.neo4jwriter.element; + +import java.util.Arrays; + +/** + * @see org.neo4j.driver.Values + * @author fuyouj + */ +public enum PropertyType { + NULL, + BOOLEAN, + STRING, + LONG, + SHORT, + INTEGER, + DOUBLE, + FLOAT, + LOCAL_DATE, + LOCAL_TIME, + LOCAL_DATE_TIME, + LIST, + MAP, + CHAR_ARRAY, + BYTE_ARRAY, + BOOLEAN_ARRAY, + STRING_ARRAY, + LONG_ARRAY, + INT_ARRAY, + SHORT_ARRAY, + DOUBLE_ARRAY, + FLOAT_ARRAY, + Object_ARRAY; + + public static PropertyType fromStrIgnoreCase(String typeStr) { + return Arrays.stream(PropertyType.values()) + .filter(e -> e.name().equalsIgnoreCase(typeStr)) + .findFirst() + .orElse(PropertyType.STRING); + } +} diff --git a/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/exception/Neo4jErrorCode.java b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/exception/Neo4jErrorCode.java new file mode 100644 index 0000000000..d7df79ffa3 --- /dev/null +++ b/neo4jwriter/src/main/java/com/alibaba/datax/plugin/writer/neo4jwriter/exception/Neo4jErrorCode.java @@ -0,0 +1,37 @@ +package com.alibaba.datax.plugin.writer.neo4jwriter.exception; + +import com.alibaba.datax.common.spi.ErrorCode; + + +public enum Neo4jErrorCode implements ErrorCode { + + /** + * Invalid configuration + * 配置校验异常 + */ + CONFIG_INVALID("NEO4J_ERROR_01","invalid configuration"), + /** + * database error + * 在执行写入到数据库时抛出的异常,可能是权限异常,也可能是连接超时,或者是配置到了从节点。 + * 如果是更新操作,还会有死锁异常。具体原因根据报错信息确定,但是这与dataX无关。 + */ + DATABASE_ERROR("NEO4J_ERROR_02","database error"); + + private final String code; + private final String description; + + @Override + public String getCode() { + return code; + } + + @Override + public String getDescription() { + return description; + } + + Neo4jErrorCode(String code, String description) { + this.code = code; + this.description = description; + } +} diff --git a/neo4jwriter/src/main/resources/plugin.json b/neo4jwriter/src/main/resources/plugin.json new file mode 100644 index 0000000000..3c8878f638 --- /dev/null +++ b/neo4jwriter/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "neo4jWriter", + "class": "com.alibaba.datax.plugin.writer.neo4jwriter.Neo4jWriter", + "description": "dataX neo4j 写插件", + "developer": "付有杰" +} \ No newline at end of file diff --git a/neo4jwriter/src/main/resources/plugin_job_template.json b/neo4jwriter/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..45bf3c88c2 --- /dev/null +++ b/neo4jwriter/src/main/resources/plugin_job_template.json @@ -0,0 +1,42 @@ +{ + "uri": "neo4j://localhost:7687", + "username": "neo4j", + "password": "Test@12343", + "database": "neo4j", + "cypher": "unwind $batch as row create(p:Person) set p.pbool = row.pbool,p.pstring = row.pstring,p.plong = row.plong,p.pshort = row.pshort,p.pdouble=row.pdouble,p.pstringarr=row.pstringarr,p.plocaldate=row.plocaldate", + "batchDataVariableName": "batch", + "batchSize": "33", + "properties": [ + { + "name": "pbool", + //type 忽略大小写 + "type": "BOOLEAN" + }, + { + "name": "pstring", + "type": "STRING" + }, + { + "name": "plong", + "type": "LONG" + }, + { + "name": "pshort", + "type": "SHORT" + }, + { + "name": "pdouble", + "type": "DOUBLE" + }, + { + "name": "pstringarr", + "type": "STRING_ARRAY", + "split": "," + }, + { + "name": "plocaldate", + "type": "LOCAL_DATE", + "dateFormat": "yyyy-MM-dd" + } + ] +} \ No newline at end of file diff --git a/neo4jwriter/src/test/java/com/alibaba/datax/plugin/writer/Neo4jWriterTest.java b/neo4jwriter/src/test/java/com/alibaba/datax/plugin/writer/Neo4jWriterTest.java new file mode 100644 index 0000000000..53c9235e99 --- /dev/null +++ b/neo4jwriter/src/test/java/com/alibaba/datax/plugin/writer/Neo4jWriterTest.java @@ -0,0 +1,257 @@ +package com.alibaba.datax.plugin.writer; + + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.element.StringColumn; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.mock.MockRecord; +import com.alibaba.datax.plugin.writer.mock.MockUtil; +import com.alibaba.datax.plugin.writer.neo4jwriter.Neo4jClient; +import com.alibaba.datax.plugin.writer.neo4jwriter.config.Neo4jProperty; +import com.alibaba.datax.plugin.writer.neo4jwriter.element.PropertyType; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.neo4j.driver.*; +import org.neo4j.driver.types.Node; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.lifecycle.Startables; +import org.testcontainers.shaded.org.awaitility.Awaitility; +import org.testcontainers.utility.DockerImageName; +import org.testcontainers.utility.DockerLoggerFactory; + +import java.io.File; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + + +public class Neo4jWriterTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(Neo4jWriterTest.class); + private static final int MOCK_NUM = 100; + private static final String CONTAINER_IMAGE = "neo4j:5.9.0"; + + private static final String CONTAINER_HOST = "neo4j-host"; + private static final int HTTP_PORT = 7474; + private static final int BOLT_PORT = 7687; + private static final String CONTAINER_NEO4J_USERNAME = "neo4j"; + private static final String CONTAINER_NEO4J_PASSWORD = "Test@12343"; + private static final URI CONTAINER_URI = URI.create("neo4j://localhost:" + BOLT_PORT); + + protected static final Network NETWORK = Network.newNetwork(); + + private GenericContainer container; + private Driver neo4jDriver; + private Session neo4jSession; + + @Before + public void init() { + DockerImageName imageName = DockerImageName.parse(CONTAINER_IMAGE); + container = + new GenericContainer<>(imageName) + .withNetwork(NETWORK) + .withNetworkAliases(CONTAINER_HOST) + .withExposedPorts(HTTP_PORT, BOLT_PORT) + .withEnv( + "NEO4J_AUTH", + CONTAINER_NEO4J_USERNAME + "/" + CONTAINER_NEO4J_PASSWORD) + .withEnv("apoc.export.file.enabled", "true") + .withEnv("apoc.import.file.enabled", "true") + .withEnv("apoc.import.file.use_neo4j_config", "true") + .withEnv("NEO4J_PLUGINS", "[\"apoc\"]") + .withLogConsumer( + new Slf4jLogConsumer( + DockerLoggerFactory.getLogger(CONTAINER_IMAGE))); + container.setPortBindings( + Arrays.asList( + String.format("%s:%s", HTTP_PORT, HTTP_PORT), + String.format("%s:%s", BOLT_PORT, BOLT_PORT))); + Startables.deepStart(Stream.of(container)).join(); + LOGGER.info("container started"); + Awaitility.given() + .ignoreExceptions() + .await() + .atMost(30, TimeUnit.SECONDS) + .untilAsserted(this::initConnection); + } + + @Test + public void testCreateNodeAllTypeField() { + final Result checkExists = neo4jSession.run("MATCH (p:Person) RETURN p limit 1"); + if (checkExists.hasNext()) { + neo4jSession.run("MATCH (p:Person) delete p"); + } + + Configuration configuration = Configuration.from(new File("src/test/resources/allTypeFieldNode.json")); + Neo4jClient neo4jClient = Neo4jClient.build(configuration, null); + + neo4jClient.init(); + for (int i = 0; i < MOCK_NUM; i++) { + neo4jClient.tryWrite(mockAllTypeFieldTestNode(neo4jClient.getNeo4jFields())); + } + neo4jClient.destroy(); + + + Result result = neo4jSession.run("MATCH (p:Person) return p"); + // nodes + assertTrue(result.hasNext()); + int cnt = 0; + while (result.hasNext()) { + org.neo4j.driver.Record record = result.next(); + record.get("p").get("pbool").asBoolean(); + record.get("p").get("pstring").asString(); + record.get("p").get("plong").asLong(); + record.get("p").get("pshort").asInt(); + record.get("p").get("pdouble").asDouble(); + List list = (List) record.get("p").get("pstringarr").asObject(); + record.get("p").get("plocaldate").asLocalDate(); + cnt++; + + } + assertEquals(cnt, MOCK_NUM); + } + + + /** + * 创建关系 必须先有节点 + * 所以先创建节点再模拟关系 + */ + @Test + public void testCreateRelation() { + final Result checkExists = neo4jSession.run("MATCH (p1:Person)-[r:LINK]->(p1:Person) return r limit 1"); + if (checkExists.hasNext()) { + neo4jSession.run("MATCH (p1:Person)-[r:LINK]->(p1:Person) delete r,p1,p2"); + } + + String createNodeCql = "create (p:Person) set p.id = '%s'"; + Configuration configuration = Configuration.from(new File("src/test/resources/relationship.json")); + + Neo4jClient neo4jClient = Neo4jClient.build(configuration, null); + neo4jClient.init(); + //创建节点为后续写关系做准备 + //Create nodes to prepare for subsequent write relationships + for (int i = 0; i < MOCK_NUM; i++) { + neo4jSession.run(String.format(createNodeCql, i + "start")); + neo4jSession.run(String.format(createNodeCql, i + "end")); + Record record = new MockRecord(); + record.addColumn(new StringColumn(i + "start")); + record.addColumn(new StringColumn(i + "end")); + neo4jClient.tryWrite(record); + + } + neo4jClient.destroy(); + + Result result = neo4jSession.run("MATCH (start:Person)-[r:LINK]->(end:Person) return r,start,end"); + // relationships + assertTrue(result.hasNext()); + int cnt = 0; + while (result.hasNext()) { + org.neo4j.driver.Record record = result.next(); + + Node startNode = record.get("start").asNode(); + assertTrue(startNode.hasLabel("Person")); + assertTrue(startNode.asMap().containsKey("id")); + + Node endNode = record.get("end").asNode(); + assertTrue(startNode.hasLabel("Person")); + assertTrue(endNode.asMap().containsKey("id")); + + + String name = record.get("r").type().name(); + assertEquals("RELATIONSHIP", name); + cnt++; + } + assertEquals(cnt, MOCK_NUM); + } + + /** + * neo4j中,Label和关系类型,想动态的写,需要借助于apoc函数 + */ + @Test + public void testUseApocCreateDynamicLabel() { + List dynamicLabel = new ArrayList<>(); + for (int i = 0; i < MOCK_NUM; i++) { + dynamicLabel.add("Label" + i); + } + //删除原有数据 + //remove test data if exist + //这种占位符的方式不支持批量动态写,当然可以使用union拼接,但是性能不好 + String query = "match (p:%s) return p"; + String delete = "match (p:%s) delete p"; + for (String label : dynamicLabel) { + Result result = neo4jSession.run(String.format(query, label)); + if (result.hasNext()) { + neo4jSession.run(String.format(delete, label)); + } + } + + Configuration configuration = Configuration.from(new File("src/test/resources/dynamicLabel.json")); + Neo4jClient neo4jClient = Neo4jClient.build(configuration, null); + + neo4jClient.init(); + for (int i = 0; i < dynamicLabel.size(); i++) { + Record record = new MockRecord(); + record.addColumn(new StringColumn(dynamicLabel.get(i))); + record.addColumn(new StringColumn(String.valueOf(i))); + neo4jClient.tryWrite(record); + } + neo4jClient.destroy(); + + //校验脚本的批量写入是否正确 + int cnt = 0; + for (int i = 0; i < dynamicLabel.size(); i++) { + String label = dynamicLabel.get(i); + Result result = neo4jSession.run(String.format(query, label)); + while (result.hasNext()) { + org.neo4j.driver.Record record = result.next(); + Node node = record.get("p").asNode(); + assertTrue(node.hasLabel(label)); + assertEquals(node.asMap().get("id"), i + ""); + cnt++; + } + } + assertEquals(cnt, MOCK_NUM); + + } + + + private Record mockAllTypeFieldTestNode(List neo4JProperties) { + Record mock = new MockRecord(); + for (Neo4jProperty field : neo4JProperties) { + mock.addColumn(MockUtil.mockColumnByType(PropertyType.fromStrIgnoreCase(field.getType()))); + } + return mock; + } + + @After + public void destroy() { + if (neo4jSession != null) { + neo4jSession.close(); + } + if (neo4jDriver != null) { + neo4jDriver.close(); + } + if (container != null) { + container.close(); + } + } + + private void initConnection() { + neo4jDriver = + GraphDatabase.driver( + CONTAINER_URI, + AuthTokens.basic(CONTAINER_NEO4J_USERNAME, CONTAINER_NEO4J_PASSWORD)); + neo4jSession = neo4jDriver.session(SessionConfig.forDatabase("neo4j")); + } +} diff --git a/neo4jwriter/src/test/java/com/alibaba/datax/plugin/writer/mock/MockRecord.java b/neo4jwriter/src/test/java/com/alibaba/datax/plugin/writer/mock/MockRecord.java new file mode 100644 index 0000000000..77d3f5005c --- /dev/null +++ b/neo4jwriter/src/test/java/com/alibaba/datax/plugin/writer/mock/MockRecord.java @@ -0,0 +1,104 @@ +package com.alibaba.datax.plugin.writer.mock; + + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.fastjson2.JSON; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MockRecord implements Record { + private static final int RECORD_AVERGAE_COLUMN_NUMBER = 16; + + private List columns; + + private int byteSize; + + + private Map meta; + + public MockRecord() { + this.columns = new ArrayList<>(RECORD_AVERGAE_COLUMN_NUMBER); + } + + @Override + public void addColumn(Column column) { + columns.add(column); + incrByteSize(column); + } + + @Override + public Column getColumn(int i) { + if (i < 0 || i >= columns.size()) { + return null; + } + return columns.get(i); + } + + @Override + public void setColumn(int i, final Column column) { + if (i < 0) { + throw new IllegalArgumentException("不能给index小于0的column设置值"); + } + + if (i >= columns.size()) { + expandCapacity(i + 1); + } + + decrByteSize(getColumn(i)); + this.columns.set(i, column); + incrByteSize(getColumn(i)); + } + + @Override + public String toString() { + Map json = new HashMap(); + json.put("size", this.getColumnNumber()); + json.put("data", this.columns); + return JSON.toJSONString(json); + } + + @Override + public int getColumnNumber() { + return this.columns.size(); + } + + @Override + public int getByteSize() { + return byteSize; + } + + public int getMemorySize() { + throw new UnsupportedOperationException(); + } + + @Override + public void setMeta(Map meta) { + + } + + @Override + public Map getMeta() { + return null; + } + + private void decrByteSize(final Column column) { + } + + private void incrByteSize(final Column column) { + } + + private void expandCapacity(int totalSize) { + if (totalSize <= 0) { + return; + } + + int needToExpand = totalSize - columns.size(); + while (needToExpand-- > 0) { + this.columns.add(null); + } + } +} diff --git a/neo4jwriter/src/test/java/com/alibaba/datax/plugin/writer/mock/MockUtil.java b/neo4jwriter/src/test/java/com/alibaba/datax/plugin/writer/mock/MockUtil.java new file mode 100644 index 0000000000..8f05f1e86e --- /dev/null +++ b/neo4jwriter/src/test/java/com/alibaba/datax/plugin/writer/mock/MockUtil.java @@ -0,0 +1,50 @@ +package com.alibaba.datax.plugin.writer.mock; + + +import com.alibaba.datax.common.element.*; +import com.alibaba.datax.plugin.writer.neo4jwriter.element.PropertyType; +import com.alibaba.fastjson2.JSON; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.Random; + +public class MockUtil { + + public static Column mockColumnByType(PropertyType type) { + Random random = new Random(); + switch (type) { + case SHORT: + return new StringColumn("1"); + case BOOLEAN: + return new BoolColumn(random.nextInt() % 2 == 0); + case INTEGER: + case LONG: + return new LongColumn(random.nextInt(Integer.MAX_VALUE)); + case FLOAT: + case DOUBLE: + return new DoubleColumn(random.nextDouble()); + case NULL: + return null; + case BYTE_ARRAY: + return new BytesColumn(new byte[]{(byte) (random.nextInt() % 2)}); + case LOCAL_DATE: + return new StringColumn(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))); + case MAP: + return new StringColumn(JSON.toJSONString(propmap())); + case STRING_ARRAY: + return new StringColumn("[1,1,1,1,1,1,1]"); + default: + return new StringColumn("randomStr" + random.nextInt(Integer.MAX_VALUE)); + } + } + + public static Map propmap() { + Map prop = new HashMap<>(); + prop.put("name", "neo4jWriter"); + prop.put("age", "1"); + return prop; + } +} diff --git a/neo4jwriter/src/test/resources/allTypeFieldNode.json b/neo4jwriter/src/test/resources/allTypeFieldNode.json new file mode 100644 index 0000000000..6d504d7930 --- /dev/null +++ b/neo4jwriter/src/test/resources/allTypeFieldNode.json @@ -0,0 +1,41 @@ +{ + "uri": "neo4j://localhost:7687", + "username":"neo4j", + "password":"Test@12343", + "database":"neo4j", + "cypher": "unwind $batch as row create(p:Person) set p.pbool = row.pbool,p.pstring = row.pstring,p.plong = row.plong,p.pshort = row.pshort,p.pdouble=row.pdouble,p.pstringarr=row.pstringarr,p.plocaldate=row.plocaldate", + "batchDataVariableName": "batch", + "batchSize": "33", + "properties": [ + { + "name": "pbool", + "type": "BOOLEAN" + }, + { + "name": "pstring", + "type": "STRING" + }, + { + "name": "plong", + "type": "LONG" + }, + { + "name": "pshort", + "type": "SHORT" + }, + { + "name": "pdouble", + "type": "DOUBLE" + }, + { + "name": "pstringarr", + "type": "STRING_ARRAY", + "split": "," + }, + { + "name": "plocaldate", + "type": "LOCAL_DATE", + "dateFormat": "yyyy-MM-dd" + } + ] +} \ No newline at end of file diff --git a/neo4jwriter/src/test/resources/dynamicLabel.json b/neo4jwriter/src/test/resources/dynamicLabel.json new file mode 100644 index 0000000000..05ed3e7668 --- /dev/null +++ b/neo4jwriter/src/test/resources/dynamicLabel.json @@ -0,0 +1,19 @@ +{ + "uri": "bolt://localhost:7687", + "username":"neo4j", + "password":"Test@12343", + "database":"neo4j", + "cypher": "unwind $batch as row CALL apoc.cypher.doIt( 'create (n:`' + row.Label + '`{id:$id})' ,{id: row.id} ) YIELD value RETURN 1 ", + "batchDataVariableName": "batch", + "batchSize": "33", + "properties": [ + { + "name": "Label", + "type": "string" + }, + { + "name": "id", + "type": "STRING" + } + ] +} \ No newline at end of file diff --git a/neo4jwriter/src/test/resources/relationship.json b/neo4jwriter/src/test/resources/relationship.json new file mode 100644 index 0000000000..cb9bbdf444 --- /dev/null +++ b/neo4jwriter/src/test/resources/relationship.json @@ -0,0 +1,19 @@ +{ + "uri": "neo4j://localhost:7687", + "username":"neo4j", + "password":"Test@12343", + "database":"neo4j", + "cypher": "unwind $batch as row match(p1:Person) where p1.id = row.startNodeId match(p2:Person) where p2.id = row.endNodeId create (p1)-[:LINK]->(p2)", + "batchDataVariableName": "batch", + "batchSize": "33", + "properties": [ + { + "name": "startNodeId", + "type": "STRING" + }, + { + "name": "endNodeId", + "type": "STRING" + } + ] +} \ No newline at end of file diff --git a/neo4jwriter/src/test/resources/streamreader2neo4j.json b/neo4jwriter/src/test/resources/streamreader2neo4j.json new file mode 100644 index 0000000000..3d543ce366 --- /dev/null +++ b/neo4jwriter/src/test/resources/streamreader2neo4j.json @@ -0,0 +1,51 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "sliceRecordCount": 10, + "column": [ + { + "type": "string", + "value": "StreamReader" + }, + { + "type": "string", + "value": "1997" + } + ] + } + }, + "writer": { + "name": "neo4jWriter", + "parameter": { + "uri": "bolt://localhost:7687", + "username":"neo4j", + "password":"Test@12343", + "database":"neo4j", + "cypher": "unwind $batch as row CALL apoc.cypher.doIt( 'create (n:`' + row.Label + '`{id:$id})' ,{id: row.id} ) YIELD value RETURN 1 ", + "batchDataVariableName": "batch", + "batchSize": "3", + "properties": [ + { + "name": "Label", + "type": "string" + }, + { + "name": "id", + "type": "STRING" + } + ] + } + } + } + ], + "setting": { + "speed": { + "channel": 5 + } + } + } +} \ No newline at end of file diff --git a/obhbasereader/doc/obhbasereader.md b/obhbasereader/doc/obhbasereader.md new file mode 100644 index 0000000000..675f6ce795 --- /dev/null +++ b/obhbasereader/doc/obhbasereader.md @@ -0,0 +1,178 @@ +OceanBase的table api为应用提供了ObHBase的访问接口,因此,OceanBase的table api的reader与HBase Reader的结构和配置方法类似。 +obhbasereader插件支持sql和hbase api两种读取方式,两种方式存在如下区别: + +1. sql方式可以按照分区或者K值进行数据切片,而hbase api方式的数据切片需要用户手动设置。 +2. sql方式会将从obhbase读取的kqtv形式的数据转换为单一横行,而hbase api则不做行列转换,直接以kqtv形式将数据传递给下游。 +3. sql方式需要配置column属性,hbase api则不需要配置,数据均为固定的kqtv四列。 +4. sql方式仅支持获取获得最新或者最旧版本的数据,而hbase api支持获得多版本数据。 +#### 脚本配置 +```json +{ + "job": { + "setting": { + "speed": { + "channel": 3, + "byte": 104857600 + }, + "errorLimit": { + "record": 10 + } + }, + "content": [ + { + "reader": { + "name": "obhbasereader", + "parameter": { + "username": "username", + "password": "password", + "encoding": "utf8", + "column": [ + { + "name": "f1:column1_1", + "type": "string" + }, + { + "name": "f1:column2_2", + "type": "string" + }, + { + "name": "f1:column1_1", + "type": "string" + }, + { + "name": "f1:column2_2", + "type": "string" + } + ], + "range": [ + { + "startRowkey": "aaa", + "endRowkey": "ccc", + "isBinaryRowkey": false + }, + { + "startRowkey": "eee", + "endRowkey": "zzz", + "isBinaryRowkey": false + } + ], + "mode": "normal", + "readByPartition": "true", + "scanCacheSize": "", + "readerHint": "", + "readBatchSize": "1000", + "connection": [ + { + "table": [ + "htable1", + "htable2" + ], + "jdbcUrl": [ + "||_dsc_ob10_dsc_||集群:租户||_dsc_ob10_dsc_||jdbc:mysql://ip:port/dbName1" + ], + "username": "username", + "password": "password" + }, + { + "table": [ + "htable1", + "htable2" + ], + "jdbcUrl": [ + "jdbc:mysql://ip:port/database" + ] + } + ] + } + }, + "writer": { + "name": "txtfilewriter", + "parameter": { + "path": "/Users/xujing/datax/txtfile", + "charset": "UTF-8", + "fieldDelimiter": ",", + "fileName": "hbase", + "nullFormat": "null", + "writeMode": "truncate" + } + } + } + ] + } +} +``` +##### 参数解释 + +- **connection** + - 描述:配置分库分表的jdbcUrl和分表名。如果一个分库中有多个分表可以用逗号隔开,也可以写成表名[起始序号-截止序号] + - 必须:是 + - 默认值:无 +- **jdbcUrl** + - 描述:连接ob使用的jdbc url,支持如下两种格式: + - jdbc:mysql://obproxyIp:obproxyPort/db + - 此格式下username需要写成三段式格式 + - ||_dsc_ob10_dsc_||集群名:租户名||_dsc_ob10_dsc_||jdbc:mysql://obproxyIp:obproxyPort/db + - 此格式下username仅填写用户名本身,无需三段式写法 + + - 必选:是 + - 默认值:无 +- **table** + - 描述:所选取的需要同步的表。使用JSON的数组描述,因此支持多张表同时抽取。当配置为多张表时,用户自己需保证多张表是同一schema结构,obhbasereader不予检查表是否同一逻辑表。注意,table必须包含在connection配置单元中。 + - 必选:是 + - 默认值:无 +- **readByPartition** + - 描述:使用sql方式读取时,配置**仅**按照分区进行切片。 + - 必须:否 + - 默认值:false +- **partitionName** + - 描述:使用sql方式读取时,标识仅读取指定分区名的数据,用户需要保证配置的分区名在表结构中真实存在(要求严格大小写)。 + - 必须:否 + - 默认值:无 +- **readBatchSize** + - 描述:使用sql方式读取时,分页大小。 + - 必须:否 + - 默认值:10w +- **fetchSize** + - 描述:使用sql方式读取时,控制每次读取数据时从结果集中获取的数据行数。 + - 必须:否 + - 默认值:-2147483648 +- **scanCacheSize** + - 描述:使用hbase api读取时,每次rpc从服务器端读取的行数 + - 必须:否 + - 默认值:256 +- **readerHint** + - 描述:obhbasereader使用sql方式读取时使用的hint + - 必须:否 + - 默认值:/*+READ_CONSISTENCY(weak),QUERY_TIMEOUT(86400000000)*/ +- **column** + - 描述:使用sql方式读取数据时,所配置的表中需要同步的列名集合,使用JSON的数组描述字段信息。 + - 支持列裁剪,即列可以挑选部分列进行导出。 +``` +支持列换序,即列可以不按照表schema信息进行导出,同时支持通配符*,在使用之前需仔细核对列信息。 +``` + +- 必选:sql方式读取时必选 + - 默认值:无 +- **range** + - 描述**:**指定hbasereader读取的rowkey范围 + - 必须:否 + - 默认值:无 +- **username** + - 描述:访问OceanBase的用户名 + - 必选:是 + - 默认值:无 +- **mode** + - 描述:读取obhbase的模式,normal 模式,即仅读取一个版本的数据。 + - 必选:是 + - 默认值:normal +- **version** + - 描述:读取obhbase的版本,当前支持oldest、latest模式,分别表示读取最旧和最新的数据。 + - 必须:是 + - 默认值:oldest + +一些注意点: +注:如果配置了**partitionName**,则无需再配置readByPartition,即便配置了也会忽略readByPartition选项,而是仅会读取指定分区的数据。 +注:如果配置了**readByPartition**,任务将仅按照分区切分任务,而不会再按照K值进行切分。如果是非分区表,则整张表会被当作一个任务而不会再切分。 + + + diff --git a/obhbasereader/pom.xml b/obhbasereader/pom.xml new file mode 100755 index 0000000000..7c8ffb1e5c --- /dev/null +++ b/obhbasereader/pom.xml @@ -0,0 +1,153 @@ + + 4.0.0 + + com.alibaba.datax + datax-all + 0.0.1-SNAPSHOT + + + obhbasereader + com.alibaba.datax + obhbasereader + 0.0.1-SNAPSHOT + + + + com.alibaba.datax + datax-core + ${datax-project-version} + provided + + + com.alibaba.datax + oceanbasev10reader + 0.0.1-SNAPSHOT + + + guava + com.google.guava + + + + + org.apache.zookeeper + zookeeper + 3.3.2 + + + log4j + log4j + + + + + commons-collections + commons-collections + 3.2.1 + + + + + + + + + + com.oceanbase + obkv-hbase-client + 0.1.4.2 + + + guava + com.google.guava + + + + + + com.google.guava + guava + ${guava-version} + + + + org.json + json + 20160810 + + + junit + junit + 4.11 + test + + + org.powermock + powermock-module-junit4 + 1.4.10 + test + + + org.powermock + powermock-api-mockito + 1.4.10 + test + + + org.mockito + mockito-core + 1.8.5 + test + + + + + + + + src/main/java + + **/*.properties + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + + diff --git a/obhbasereader/src/main/assembly/package.xml b/obhbasereader/src/main/assembly/package.xml new file mode 100755 index 0000000000..43da622d5c --- /dev/null +++ b/obhbasereader/src/main/assembly/package.xml @@ -0,0 +1,35 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/reader/obhbasereader + + + target/ + + obhbasereader-0.0.1-SNAPSHOT.jar + + plugin/reader/obhbasereader + + + + + + false + plugin/reader/obhbasereader/libs + runtime + + + diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/Constant.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/Constant.java new file mode 100755 index 0000000000..40dd32d282 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/Constant.java @@ -0,0 +1,34 @@ +package com.alibaba.datax.plugin.reader.obhbasereader; + +import ch.qos.logback.classic.Level; + +public final class Constant { + public static final String ROWKEY_FLAG = "rowkey"; + public static final int DEFAULT_SCAN_CACHE = 256; + public static final int DEFAULT_FETCH_SIZE = Integer.MIN_VALUE; + public static final int DEFAULT_READ_BATCH_SIZE = 100000; + // timeout:24 * 3600 = 86400s + public static final String OB_READ_HINT = "/*+READ_CONSISTENCY(weak),QUERY_TIMEOUT(86400000000)*/"; + public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final String DEFAULT_ENCODING = "UTF-8"; + public static final String DEFAULT_TIMEZONE = "UTC"; + public static final boolean DEFAULT_USE_SQLREADER = true; + public static final boolean DEFAULT_USE_ODPMODE = true; + public static final String OB_TABLE_CLIENT_PROPERTY = "logging.path.com.alipay.oceanbase-table-client"; + public static final String OB_TABLE_HBASE_PROPERTY = "logging.path.com.alipay.oceanbase-table-hbase"; + public static final String OB_TABLE_CLIENT_LOG_LEVEL = "logging.level.oceanbase-table-client"; + public static final String OB_TABLE_HBASE_LOG_LEVEL = "logging.level.oceanbase-table-hbase"; + public static final String OB_COM_ALIPAY_TABLE_CLIENT_LOG_LEVEL = "logging.level.com.alipay.oceanbase-table-client"; + public static final String OB_COM_ALIPAY_TABLE_HBASE_LOG_LEVEL = "logging.level.com.alipay.oceanbase-table-hbase"; + public static final String OB_HBASE_LOG_PATH = System.getProperty("datax.home") + "/log/"; + public static final String DEFAULT_OB_TABLE_CLIENT_LOG_LEVEL = Level.OFF.toString(); + public static final String DEFAULT_OB_TABLE_HBASE_LOG_LEVEL = Level.OFF.toString(); + public static final String OBMYSQL_KEYWORDS = + "CUME_DIST,DENSE_RANK,EMPTY,FIRST_VALUE,GROUPING,GROUPS,INTERSECT,JSON_TABLE,LAG,LAST_VALUE,LATERAL,LEAD,NTH_VALUE,NTILE,OF,OVER,PERCENT_RANK,RANK,RECURSIVE,ROW_NUMBER,SYSTEM,WINDOW,ACCESSIBLE,ACCOUNT,ACTION,ADD,AFTER,AGAINST,AGGREGATE,ALGORITHM,ALL,ALTER,ALWAYS,ANALYSE,AND,ANY,AS,ASC,ASCII,ASENSITIVE,AT,AUTO_INCREMENT,AUTOEXTEND_SIZE,AVG,AVG_ROW_LENGTH,BACKUP,BEFORE,BEGIN,BETWEEN,BIGINT,BINARY,BINLOG,BIT,BLOB,BLOCK,BOOL,BOOLEAN,BOTH,BTREE,BY,BYTE,CACHE,CALL,CASCADE,CASCADED,CASE,CATALOG_NAME,CHAIN,CHANGE,CHANGED,CHANNEL,CHAR,CHARACTER,CHARSET,CHECK,CHECKSUM,CIPHER,CLASS_ORIGIN,CLIENT,CLOSE,COALESCE,CODE,COLLATE,COLLATION,COLUMN,COLUMN_FORMAT,COLUMN_NAME,COLUMNS,COMMENT,COMMIT,COMMITTED,COMPACT,COMPLETION,COMPRESSED,COMPRESSION,CONCURRENT,CONDITION,CONNECTION,CONSISTENT,CONSTRAINT,CONSTRAINT_CATALOG,CONSTRAINT_NAME,CONSTRAINT_SCHEMA,CONTAINS,CONTEXT,CONTINUE,CONVERT,CPU,CREATE,CROSS,CUBE,CURRENT,CURRENT_DATE,CURRENT_TIME,CURRENT_TIMESTAMP,CURRENT_USER,CURSOR," + + "CURSOR_NAME,DATA,DATABASE,DATABASES,DATAFILE,DATE,DATETIME,DAY,DAY_HOUR,DAY_MICROSECOND,DAY_MINUTE,DAY_SECOND,DEALLOCATE,DEC,DECIMAL,DECLARE,DEFAULT,DEFAULT_AUTH,DEFINER,DELAY_KEY_WRITE,DELAYED,DELETE,DES_KEY_FILE,DESC,DESCRIBE,DETERMINISTIC,DIAGNOSTICS,DIRECTORY,DISABLE,DISCARD,DISK,DISTINCT,DISTINCTROW,DIV,DO,DOUBLE,DROP,DUAL,DUMPFILE,DUPLICATE,DYNAMIC,EACH,ELSE,ELSEIF,ENABLE,ENCLOSED,ENCRYPTION,END,ENDS,ENGINE,ENGINES,ENUM,ERROR,ERRORS,ESCAPE,ESCAPED,EVENT,EVENTS,EVERY,EXCHANGE,EXECUTE,EXISTS,EXIT,EXPANSION,EXPIRE,EXPLAIN,EXPORT,EXTENDED,EXTENT_SIZE,FAST,FAULTS,FETCH,FIELDS,FILE,FILE_BLOCK_SIZE,FILTER,FIRST,FIXED,FLOAT,FLOAT4,FLOAT8,FLUSH,FOLLOWS,FOR,FORCE,FOREIGN,FORMAT,FOUND,FROM,FULL,FULLTEXT,FUNCTION,GENERAL,GENERATED,GEOMETRY,GEOMETRYCOLLECTION,GET,GET_FORMAT,GLOBAL,GRANT,GRANTS,GROUP,GROUP_REPLICATION,HANDLER,HASH,HAVING,HELP,HIGH_PRIORITY,HOST,HOSTS,HOUR,HOUR_MICROSECOND,HOUR_MINUTE,HOUR_SECOND,IDENTIFIED,IF,IGNORE,IGNORE_SERVER_IDS,IMPORT,IN,INDEX," + + "INDEXES," + "INFILE,INITIAL_SIZE,INNER,INOUT,INSENSITIVE,INSERT,INSERT_METHOD,INSTALL,INSTANCE,INT,INT1,INT2,INT3,INT4,INT8,INTEGER,INTERVAL,INTO,INVOKE,INVOKER,IO,IO_AFTER_GTIDS,IO_BEFORE_GTIDS,IO_THREAD,IPC,IS,ISOLATION,ISSUER,ITERATE,JOIN,JSON,KEY,KEY_BLOCK_SIZE,KEYS,KILL,LANGUAGE,LAST,LEADING,LEAVE,LEAVES,LEFT,LESS,LEVEL,LIKE,LIMIT,LINEAR,LINES,LINESTRING,LIST,LOAD,LOCAL,LOCALTIME,LOCALTIMESTAMP,LOCK,LOCKS,LOGFILE,LOGS,LONG,LONGBLOB,LONGTEXT,LOOP,LOW_PRIORITY,MASTER,MASTER_AUTO_POSITION,MASTER_BIND,MASTER_CONNECT_RETRY,MASTER_DELAY,MASTER_HEARTBEAT_PERIOD,MASTER_HOST,MASTER_LOG_FILE,MASTER_LOG_POS,MASTER_PASSWORD,MASTER_PORT,MASTER_RETRY_COUNT,MASTER_SERVER_ID,MASTER_SSL,MASTER_SSL_CA,MASTER_SSL_CAPATH,MASTER_SSL_CERT,MASTER_SSL_CIPHER,MASTER_SSL_CRL,MASTER_SSL_CRLPATH,MASTER_SSL_KEY,MASTER_SSL_VERIFY_SERVER_CERT,MASTER_TLS_VERSION,MASTER_USER,MATCH,MAX_CONNECTIONS_PER_HOUR,MAX_QUERIES_PER_HOUR,MAX_ROWS,MAX_SIZE,MAX_STATEMENT_TIME,MAX_UPDATES_PER_HOUR," + + "MAX_USER_CONNECTIONS," + + "MAXVALUE,MEDIUM,MEDIUMBLOB,MEDIUMINT,MEDIUMTEXT,MEMORY,MERGE,MESSAGE_TEXT,MICROSECOND,MIDDLEINT,MIGRATE,MIN_ROWS,MINUTE,MINUTE_MICROSECOND,MINUTE_SECOND,MOD,MODE,MODIFIES,MODIFY,MONTH,MULTILINESTRING,MULTIPOINT,MULTIPOLYGON,MUTEX,MYSQL_ERRNO,NAME,NAMES,NATIONAL,NATURAL,NCHAR,NDB,NDBCLUSTER,NEVER,NEW,NEXT,NO,NO_WAIT,NO_WRITE_TO_BINLOG,NODEGROUP,NONBLOCKING,NONE,NOT,NUMBER,NUMERIC,NVARCHAR,OFFSET,OLD_PASSWORD,ON,ONE,ONLY,OPEN,OPTIMIZE,OPTIMIZER_COSTS,OPTION,OPTIONALLY,OPTIONS,OR,ORDER,OUT,OUTER,OUTFILE,OWNER,PACK_KEYS,PAGE,PARSE_GCOL_EXPR,PARSER,PARTIAL,PARTITION,PARTITIONING,PARTITIONS,PASSWORD,PHASE,PLUGIN,PLUGIN_DIR,PLUGINS,POINT,POLYGON,PORT,PRECEDES,PRECISION,PREPARE,PRESERVE,PREV,PRIMARY,PRIVILEGES,PROCEDURE,PROCESSLIST,PROFILE,PROFILES,PROXY,PURGE,QUARTER,QUERY,QUICK,RANGE,READ,READ_ONLY,READ_WRITE,READS,REAL,REBUILD,RECOVER,REDO_BUFFER_SIZE,REDOFILE,REDUNDANT,REFERENCES,REGEXP,RELAY,RELAY_LOG_FILE,RELAY_LOG_POS,RELAY_THREAD,RELAYLOG,RELEASE,RELOAD,REMOVE," + + "RENAME,REORGANIZE,REPAIR,REPEAT,REPEATABLE,REPLACE,REPLICATE_DO_DB,REPLICATE_DO_TABLE,REPLICATE_IGNORE_DB,REPLICATE_IGNORE_TABLE,REPLICATE_REWRITE_DB,REPLICATE_WILD_DO_TABLE,REPLICATE_WILD_IGNORE_TABLE,REPLICATION,REQUIRE,RESET,RESIGNAL,RESTORE,RESTRICT,RESUME,RETURN,RETURNED_SQLSTATE,RETURNS,REVERSE,REVOKE,RIGHT,RLIKE,ROLLBACK,ROLLUP,ROTATE,ROUTINE,ROW,ROW_COUNT,ROW_FORMAT,ROWS,RTREE,SAVEPOINT,SCHEDULE,SCHEMA,SCHEMA_NAME,SCHEMAS,SECOND,SECOND_MICROSECOND,SECURITY,SELECT,SENSITIVE,SEPARATOR,SERIAL,SERIALIZABLE,SERVER,SESSION,SET,SHARE,SHOW,SHUTDOWN,SIGNAL,SIGNED,SIMPLE,SLAVE,SLOW,SMALLINT,SNAPSHOT,SOCKET,SOME,SONAME,SOUNDS,SOURCE,SPATIAL,SPECIFIC,SQL,SQL_AFTER_GTIDS,SQL_AFTER_MTS_GAPS,SQL_BEFORE_GTIDS,SQL_BIG_RESULT,SQL_BUFFER_RESULT,SQL_CACHE,SQL_CALC_FOUND_ROWS,SQL_NO_CACHE,SQL_SMALL_RESULT,SQL_THREAD,SQL_TSI_DAY,SQL_TSI_HOUR,SQL_TSI_MINUTE,SQL_TSI_MONTH,SQL_TSI_QUARTER,SQL_TSI_SECOND,SQL_TSI_WEEK,SQL_TSI_YEAR,SQLEXCEPTION,SQLSTATE,SQLWARNING,SSL,STACKED," + + "START," + "STARTING,STARTS,STATS_AUTO_RECALC,STATS_PERSISTENT,STATS_SAMPLE_PAGES,STATUS,STOP,STORAGE,STORED,STRAIGHT_JOIN,STRING,SUBCLASS_ORIGIN,SUBJECT,SUBPARTITION,SUBPARTITIONS,SUPER,SUSPEND,SWAPS,SWITCHES,TABLE,TABLE_CHECKSUM,TABLE_NAME,TABLES,TABLESPACE,TEMPORARY,TEMPTABLE,TERMINATED,TEXT,THAN,THEN,TIME,TIMESTAMP,TIMESTAMPADD,TIMESTAMPDIFF,TINYBLOB,TINYINT,TINYTEXT,TO,TRAILING,TRANSACTION,TRIGGER,TRIGGERS,TRUNCATE,TYPE,TYPES,UNCOMMITTED,UNDEFINED,UNDO,UNDO_BUFFER_SIZE,UNDOFILE,UNICODE,UNINSTALL,UNION,UNIQUE,UNKNOWN,UNLOCK,UNSIGNED,UNTIL,UPDATE,UPGRADE,USAGE,USE,USE_FRM,USER,USER_RESOURCES,USING,UTC_DATE,UTC_TIME,UTC_TIMESTAMP,VALIDATION,VALUE,VALUES,VARBINARY,VARCHAR,VARCHARACTER,VARIABLES,VARYING,VIEW,VIRTUAL,WAIT,WARNINGS,WEEK,WEIGHT_STRING,WHEN,WHERE,WHILE,WITH,WITHOUT,WORK,WRAPPER,WRITE,X509,XA,XID,XML,XOR,YEAR,YEAR_MONTH,ZEROFILL,FALSE,TRUE"; +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/HTableManager.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/HTableManager.java new file mode 100755 index 0000000000..c36114fbb8 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/HTableManager.java @@ -0,0 +1,19 @@ +package com.alibaba.datax.plugin.reader.obhbasereader; + +import com.alipay.oceanbase.hbase.OHTable; +import org.apache.hadoop.conf.Configuration; + +import java.io.IOException; + +public final class HTableManager { + + public static OHTable createHTable(Configuration config, String tableName) throws IOException { + return new OHTable(config, tableName); + } + + public static void closeHTable(OHTable hTable) throws IOException { + if (hTable != null) { + hTable.close(); + } + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/HbaseColumnCell.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/HbaseColumnCell.java new file mode 100755 index 0000000000..1f794ae0aa --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/HbaseColumnCell.java @@ -0,0 +1,124 @@ +package com.alibaba.datax.plugin.reader.obhbasereader; + +import com.alibaba.datax.common.base.BaseObject; +import com.alibaba.datax.plugin.reader.obhbasereader.enums.ColumnType; +import com.alibaba.datax.plugin.reader.obhbasereader.util.ObHbaseReaderUtil; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.hadoop.hbase.util.Bytes; + +/** + * 描述 hbasereader 插件中,column 配置中的一个单元项实体 + */ +public class HbaseColumnCell extends BaseObject { + private ColumnType columnType; + + // columnName 格式为:列族:列名 + private String columnName; + + private byte[] cf; + private byte[] qualifier; + + //对于常量类型,其常量值放到 columnValue 里 + private String columnValue; + + //当配置了 columnValue 时,isConstant=true(这个成员变量是用于方便使用本类的地方判断是否是常量类型字段) + private boolean isConstant; + + // 只在类型是时间类型时,才会设置该值,无默认值。形式如:yyyy-MM-dd HH:mm:ss + private String dateformat; + + private HbaseColumnCell(Builder builder) { + this.columnType = builder.columnType; + + //columnName 和 columnValue 必须有一个为 null + Validate.isTrue(builder.columnName == null || builder.columnValue == null, "In obhbasereader, column cannot configure both column name and column value. Choose one of them."); + + //columnName 和 columnValue 不能都为 null + Validate.isTrue(builder.columnName != null || builder.columnValue != null, "In obhbasereader, column cannot configure both column name and column value. Choose one of them."); + + if (builder.columnName != null) { + this.isConstant = false; + this.columnName = builder.columnName; + + // 如果 columnName 不是 rowkey,则必须配置为:列族:列名 格式 + if (!ObHbaseReaderUtil.isRowkeyColumn(this.columnName)) { + + String promptInfo = "In obhbasereader, the column configuration format of column should be: 'family:column'. The column you configured is wrong:" + this.columnName; + String[] cfAndQualifier = this.columnName.split(":"); + Validate.isTrue(cfAndQualifier.length == 2 && StringUtils.isNotBlank(cfAndQualifier[0]) && StringUtils.isNotBlank(cfAndQualifier[1]), promptInfo); + + this.cf = Bytes.toBytes(cfAndQualifier[0].trim()); + this.qualifier = Bytes.toBytes(cfAndQualifier[1].trim()); + } + } else { + this.isConstant = true; + this.columnValue = builder.columnValue; + } + + if (builder.dateformat != null) { + this.dateformat = builder.dateformat; + } + } + + public ColumnType getColumnType() { + return columnType; + } + + public String getColumnName() { + return columnName; + } + + public byte[] getCf() { + return cf; + } + + public byte[] getQualifier() { + return qualifier; + } + + public String getDateformat() { + return dateformat; + } + + public String getColumnValue() { + return columnValue; + } + + public boolean isConstant() { + return isConstant; + } + + // 内部 builder 类 + public static class Builder { + private ColumnType columnType; + private String columnName; + private String columnValue; + + private String dateformat; + + public Builder(ColumnType columnType) { + this.columnType = columnType; + } + + public Builder columnName(String columnName) { + this.columnName = columnName; + return this; + } + + public Builder columnValue(String columnValue) { + this.columnValue = columnValue; + return this; + } + + public Builder dateformat(String dateformat) { + this.dateformat = dateformat; + return this; + } + + public HbaseColumnCell build() { + return new HbaseColumnCell(this); + } + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/HbaseReaderErrorCode.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/HbaseReaderErrorCode.java new file mode 100755 index 0000000000..551b19b630 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/HbaseReaderErrorCode.java @@ -0,0 +1,36 @@ +package com.alibaba.datax.plugin.reader.obhbasereader; + +import com.alibaba.datax.common.spi.ErrorCode; + +public enum HbaseReaderErrorCode implements ErrorCode { + REQUIRED_VALUE("ObHbaseReader-00", "Missing required parameters."), + ILLEGAL_VALUE("ObHbaseReader-01", "Illegal configuration."), + PREPAR_READ_ERROR("ObHbaseReader-02", "Preparing to read ObHBase error."), + SPLIT_ERROR("ObHbaseReader-03", "Splitting ObHBase table error."), + INIT_TABLE_ERROR("ObHbaseReader-04", "Initializing ObHBase extraction table error"), + PARSE_COLUMN_ERROR("ObHbaseReader-05", "Parse column failed."), + READ_ERROR("ObHbaseReader-06", "Read ObHBase error."); + + private final String code; + private final String description; + + private HbaseReaderErrorCode(String code, String description) { + this.code = code; + this.description = description; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + return String.format("Code:[%s], Description:[%s]. ", this.code, this.description); + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/Key.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/Key.java new file mode 100755 index 0000000000..6415efd098 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/Key.java @@ -0,0 +1,103 @@ +package com.alibaba.datax.plugin.reader.obhbasereader; + +public final class Key { + + public final static String HBASE_CONFIG = "hbaseConfig"; + + /** + * mode 可以取 normal 或者 multiVersionFixedColumn 或者 multiVersionDynamicColumn 三个值,无默认值。 + *

+ * normal 配合 column(Map 结构的)使用 + *

+ * multiVersionFixedColumn 配合 maxVersion,tetradType, column(List 结构的)使用 + *

+ * multiVersionDynamicColumn 配合 maxVersion,tetradType, columnFamily(List 结构的)使用 + */ + public final static String MODE = "mode"; + + /** + * 配合 mode = multiVersion 时使用,指明需要读取的版本个数。无默认值 + * -1 表示去读全部版本 + * 不能为0,1 + * >1 表示最多读取对应个数的版本数(不能超过 Integer 的最大值) + */ + public final static String MAX_VERSION = "maxVersion"; + + /** + * 多版本情况下,必须配置 四元组的类型(rowkey,column,timestamp,value) + */ + public final static String TETRAD_TYPE = "tetradType"; + + /** + * 默认为 utf8 + */ + public final static String ENCODING = "encoding"; + + public final static String TABLE = "table"; + + public final static String USERNAME = "username"; + + public final static String OB_SYS_USERNAME = "obSysUser"; + + public final static String CONFIG_URL = "obConfigUrl"; + + public final static String ODP_HOST = "odpHost"; + + public final static String ODP_PORT = "odpPort"; + + public final static String DB_NAME = "dbName"; + + public final static String PASSWORD = "password"; + + public final static String OB_SYS_PASSWORD = "obSysPassword"; + + public final static String COLUMN_FAMILY = "columnFamily"; + + public final static String COLUMN = "column"; + + public final static String START_ROWKEY = "startRowkey"; + + public final static String END_ROWKEY = "endRowkey"; + + public final static String IS_BINARY_ROWKEY = "isBinaryRowkey"; + + public final static String SCAN_CACHE = "scanCache"; + + public final static String RS_URL = "rsUrl"; + + public final static String MAX_ACTIVE_CONNECTION = "maxActiveConnection"; + + public final static int DEFAULT_MAX_ACTIVE_CONNECTION = 2000; + + public final static String TIMEOUT = "timeout"; + + public final static long DEFAULT_TIMEOUT = 30; + + public final static String PARTITION_NAME = "partitionName"; + + public final static String JDBC_URL = "jdbcUrl"; + + public final static String TIMEZONE = "timezone"; + + public final static String FETCH_SIZE = "fetchSize"; + + public final static String READ_BATCH_SIZE = "readBatchSize"; + + public final static String SESSION = "session"; + + public final static String READER_HINT = "readerHint"; + + public final static String QUERY_SQL = "querySql"; + + public final static String SAMPLE_PERCENTAGE = "samplePercentage"; + // 是否使用独立密码 + public final static String USE_SPECIAL_SECRET = "useSpecialSecret"; + + public final static String USE_SQL_READER = "useSqlReader"; + + public final static String USE_ODP_MODE = "useOdpMode"; + + public final static String RANGE = "range"; + + public final static String READ_BY_PARTITION = "readByPartition"; +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/LocalStrings.properties b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/LocalStrings.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/LocalStrings_en_US.properties b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/LocalStrings_en_US.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/LocalStrings_ja_JP.properties b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/LocalStrings_zh_CN.properties b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/LocalStrings_zh_HK.properties b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/LocalStrings_zh_TW.properties b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/ObHbaseReader.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/ObHbaseReader.java new file mode 100755 index 0000000000..15472d6eaf --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/ObHbaseReader.java @@ -0,0 +1,445 @@ +package com.alibaba.datax.plugin.reader.obhbasereader; + +import static com.alibaba.datax.plugin.reader.obhbasereader.Constant.DEFAULT_OB_TABLE_CLIENT_LOG_LEVEL; +import static com.alibaba.datax.plugin.reader.obhbasereader.Constant.DEFAULT_OB_TABLE_HBASE_LOG_LEVEL; +import static com.alibaba.datax.plugin.reader.obhbasereader.Constant.DEFAULT_USE_ODPMODE; +import static com.alibaba.datax.plugin.reader.obhbasereader.Constant.OB_COM_ALIPAY_TABLE_CLIENT_LOG_LEVEL; +import static com.alibaba.datax.plugin.reader.obhbasereader.Constant.OB_COM_ALIPAY_TABLE_HBASE_LOG_LEVEL; +import static com.alibaba.datax.plugin.reader.obhbasereader.Constant.OB_HBASE_LOG_PATH; +import static com.alibaba.datax.plugin.reader.obhbasereader.Constant.OB_TABLE_CLIENT_LOG_LEVEL; +import static com.alibaba.datax.plugin.reader.obhbasereader.Constant.OB_TABLE_CLIENT_PROPERTY; +import static com.alibaba.datax.plugin.reader.obhbasereader.Constant.OB_TABLE_HBASE_LOG_LEVEL; +import static com.alibaba.datax.plugin.reader.obhbasereader.Constant.OB_TABLE_HBASE_PROPERTY; +import static org.apache.commons.lang3.StringUtils.EMPTY; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.spi.Reader; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.Constant; +import com.alibaba.datax.plugin.rdbms.reader.util.ObVersion; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.util.TableExpandUtil; +import com.alibaba.datax.plugin.reader.obhbasereader.enums.ModeType; +import com.alibaba.datax.plugin.reader.obhbasereader.ext.ServerConnectInfo; +import com.alibaba.datax.plugin.reader.obhbasereader.task.AbstractHbaseTask; +import com.alibaba.datax.plugin.reader.obhbasereader.task.SQLNormalModeReader; +import com.alibaba.datax.plugin.reader.obhbasereader.task.ScanMultiVersionReader; +import com.alibaba.datax.plugin.reader.obhbasereader.task.ScanNormalModeReader; +import com.alibaba.datax.plugin.reader.obhbasereader.util.HbaseSplitUtil; +import com.alibaba.datax.plugin.reader.obhbasereader.util.ObHbaseReaderUtil; +import com.alibaba.datax.plugin.reader.obhbasereader.util.SqlReaderSplitUtil; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.util.ObReaderUtils; + +import com.google.common.base.Preconditions; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * ObHbaseReader 支持分库分表 + * 仅支持ob3.x及以上版本 + */ +public class ObHbaseReader extends Reader { + + public static class Job extends Reader.Job { + static private final String ACCESS_DENIED_ERROR = "Access denied for user"; + private static Logger LOG = LoggerFactory.getLogger(ObHbaseReader.class); + private Configuration originalConfig; + + @Override + public void init() { + if (System.getProperty(OB_TABLE_CLIENT_PROPERTY) == null) { + LOG.info(OB_TABLE_CLIENT_PROPERTY + " not set"); + System.setProperty(OB_TABLE_CLIENT_PROPERTY, OB_HBASE_LOG_PATH); + } + if (System.getProperty(OB_TABLE_HBASE_PROPERTY) == null) { + LOG.info(OB_TABLE_HBASE_PROPERTY + " not set"); + System.setProperty(OB_TABLE_HBASE_PROPERTY, OB_HBASE_LOG_PATH); + } + if (System.getProperty(OB_TABLE_CLIENT_LOG_LEVEL) == null) { + LOG.info(OB_TABLE_CLIENT_LOG_LEVEL + " not set"); + System.setProperty(OB_TABLE_CLIENT_LOG_LEVEL, DEFAULT_OB_TABLE_CLIENT_LOG_LEVEL); + } + if (System.getProperty(OB_TABLE_HBASE_LOG_LEVEL) == null) { + LOG.info(OB_TABLE_HBASE_LOG_LEVEL + " not set"); + System.setProperty(OB_TABLE_HBASE_LOG_LEVEL, DEFAULT_OB_TABLE_HBASE_LOG_LEVEL); + } + if (System.getProperty(OB_COM_ALIPAY_TABLE_CLIENT_LOG_LEVEL) == null) { + LOG.info(OB_COM_ALIPAY_TABLE_CLIENT_LOG_LEVEL + " not set"); + System.setProperty(OB_COM_ALIPAY_TABLE_CLIENT_LOG_LEVEL, DEFAULT_OB_TABLE_CLIENT_LOG_LEVEL); + } + if (System.getProperty(OB_COM_ALIPAY_TABLE_HBASE_LOG_LEVEL) == null) { + LOG.info(OB_COM_ALIPAY_TABLE_HBASE_LOG_LEVEL + " not set"); + System.setProperty(OB_COM_ALIPAY_TABLE_HBASE_LOG_LEVEL, DEFAULT_OB_TABLE_HBASE_LOG_LEVEL); + } + + LOG.info("{} is set to {}, {} is set to {}", + OB_TABLE_CLIENT_PROPERTY, OB_HBASE_LOG_PATH, OB_TABLE_HBASE_PROPERTY, OB_HBASE_LOG_PATH); + this.originalConfig = super.getPluginJobConf(); + ObHbaseReaderUtil.doPretreatment(originalConfig); + List conns = originalConfig.getList(Constant.CONN_MARK, Object.class); + // 逻辑表配置 + Preconditions.checkArgument(CollectionUtils.isNotEmpty(conns), "connection information is empty."); + dealLogicConnAndTable(conns); + if (LOG.isDebugEnabled()) { + LOG.debug("After init(), now originalConfig is:\n{}\n", this.originalConfig); + } + } + + @Override + public void destroy() { + } + + private void dealLogicConnAndTable(List conns) { + String unifiedUsername = originalConfig.getString(Key.USERNAME); + String unifiedPassword = originalConfig.getString(Key.PASSWORD); + boolean useSqlReader = originalConfig.getBool(Key.USE_SQL_READER, com.alibaba.datax.plugin.reader.obhbasereader.Constant.DEFAULT_USE_SQLREADER); + boolean checkSlave = originalConfig.getBool(com.alibaba.datax.plugin.rdbms.reader.Key.CHECK_SLAVE, false); + Set keywords = Arrays.stream(com.alibaba.datax.plugin.reader.obhbasereader.Constant.OBMYSQL_KEYWORDS.split(",")).collect(Collectors.toSet()); + List preSql = originalConfig.getList(com.alibaba.datax.plugin.rdbms.reader.Key.PRE_SQL, String.class); + + int tableNum = 0; + + for (int i = 0, len = conns.size(); i < len; i++) { + Configuration connConf = Configuration.from(conns.get(i).toString()); + String curUsername = connConf.getString(Key.USERNAME, unifiedUsername); + Preconditions.checkArgument(StringUtils.isNotEmpty(curUsername), "username is empty."); + String curPassword = connConf.getString(Key.PASSWORD, unifiedPassword); + + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, i, Key.USERNAME), curUsername); + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, i, Key.PASSWORD), curPassword); + + List jdbcUrls = connConf.getList(Key.JDBC_URL, new ArrayList<>(), String.class); + String jdbcUrl; + if (useSqlReader) { + // sql模式下,jdbcUrl必须配置,只有使用sql模式的情况才检查地址 + Preconditions.checkArgument(CollectionUtils.isNotEmpty(jdbcUrls), "if using sql mode, jdbcUrl is needed"); + jdbcUrl = DBUtil.chooseJdbcUrlWithoutRetry(DataBaseType.MySql, jdbcUrls, curUsername, curPassword, preSql, checkSlave); + jdbcUrl = DataBaseType.MySql.appendJDBCSuffixForReader(jdbcUrl); + // 回写到connection[i].jdbcUrl + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, i, Key.JDBC_URL), jdbcUrl); + LOG.info("Available jdbcUrl:{}.", jdbcUrl); + } else { + jdbcUrl = jdbcUrls.get(0); + jdbcUrl = StringUtils.isNotBlank(jdbcUrl) ? DataBaseType.MySql.appendJDBCSuffixForReader(jdbcUrl) : EMPTY; + checkAndSetHbaseConnConf(jdbcUrl, curUsername, curPassword, connConf, i); + } + + // table 方式 + // 对每一个connection 上配置的table 项进行解析(已对表名称进行了 ` 处理的) + List tables = connConf.getList(Key.TABLE, String.class); + + List expandedTables = TableExpandUtil.expandTableConf(DataBaseType.MySql, tables); + + if (expandedTables.isEmpty()) { + throw DataXException.asDataXException(DBUtilErrorCode.ILLEGAL_VALUE, "The specified table list is empty."); + } + + for (int ti = 0; ti < expandedTables.size(); ti++) { + String tableName = expandedTables.get(ti); + if (keywords.contains(tableName.toUpperCase())) { + expandedTables.set(ti, "`" + tableName + "`"); + } + } + tableNum += expandedTables.size(); + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, i, Key.TABLE), expandedTables); + } + + if (tableNum == 0) { + // 分库分表读,未匹配到可以抽取的表 + LOG.error("sharding rule result is empty."); + throw DataXException.asDataXException("No tables were matched"); + } + originalConfig.set(Constant.TABLE_NUMBER_MARK, tableNum); + } + + /** + * In public cloud, only odp mode can be used. + * In private cloud, both odp mode and ocp mode can be used. + * + * @param jdbcUrl + * @param curUsername + * @param curPassword + * @param connConf + */ + private void checkAndSetHbaseConnConf(String jdbcUrl, String curUsername, String curPassword, Configuration connConf, int curIndex) { + ServerConnectInfo serverConnectInfo = new ServerConnectInfo(jdbcUrl, curUsername, curPassword); + if (!originalConfig.getBool(Key.USE_ODP_MODE, false)) { + // Normally, only need to query at first time + // In ocp mode, dbName, configUrl, sysUser and sysPass are needed. + String sysUser = connConf.getString(Key.OB_SYS_USERNAME, originalConfig.getString(Key.OB_SYS_USERNAME)); + String sysPass = connConf.getString(Key.OB_SYS_PASSWORD, originalConfig.getString(Key.OB_SYS_PASSWORD)); + serverConnectInfo.setSysUser(sysUser); + serverConnectInfo.setSysPass(sysPass); + String configUrl = connConf.getString(Key.CONFIG_URL, originalConfig.getString(Key.CONFIG_URL)); + if (StringUtils.isBlank(configUrl)) { + configUrl = queryRsUrl(serverConnectInfo); + } + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, curIndex, Key.USERNAME), curUsername); + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, curIndex, Key.OB_SYS_USERNAME), serverConnectInfo.sysUser); + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, curIndex, Key.OB_SYS_PASSWORD), serverConnectInfo.sysPass); + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, curIndex, Key.CONFIG_URL), configUrl); + } else { + // In odp mode, dbName, odp host and odp port are needed. + String odpHost = connConf.getString(Key.ODP_HOST, serverConnectInfo.host); + String odpPort = connConf.getString(Key.ODP_PORT, serverConnectInfo.port); + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, curIndex, Key.ODP_HOST), odpHost); + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, curIndex, Key.ODP_PORT), odpPort); + } + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, curIndex, Key.DB_NAME), serverConnectInfo.databaseName); + } + + private String queryRsUrl(ServerConnectInfo serverInfo) { + Preconditions.checkArgument(checkVersionAfterV3(serverInfo.jdbcUrl, serverInfo.getFullUserName(), serverInfo.password), "ob before 3.x is not supported."); + String configUrl = originalConfig.getString(Key.CONFIG_URL, null); + if (configUrl == null) { + try { + Connection conn = null; + int retry = 0; + final String sysJDBCUrl = serverInfo.jdbcUrl.replace(serverInfo.databaseName, "oceanbase"); + do { + try { + if (retry > 0) { + int sleep = retry > 9 ? 500 : 1 << retry; + try { + TimeUnit.SECONDS.sleep(sleep); + } catch (InterruptedException e) { + } + LOG.warn("retry fetch RsUrl the {} times", retry); + } + conn = DBUtil.getConnection(DataBaseType.OceanBase, sysJDBCUrl, serverInfo.sysUser, serverInfo.sysPass); + String sql = "show parameters like 'obconfig_url'"; + LOG.info("query param: {}", sql); + PreparedStatement stmt = conn.prepareStatement(sql); + ResultSet result = stmt.executeQuery(); + if (result.next()) { + configUrl = result.getString("Value"); + } + if (StringUtils.isNotBlank(configUrl)) { + break; + } + } catch (Exception e) { + ++retry; + LOG.warn("fetch root server list(rsList) error {}", e.getMessage()); + } finally { + DBUtil.closeDBResources(null, conn); + } + } while (retry < 3); + + LOG.info("configure url is: " + configUrl); + originalConfig.set(Key.CONFIG_URL, configUrl); + } catch (Exception e) { + LOG.error("Fail to get configure url: {}", e.getMessage(), e); + throw DataXException.asDataXException(HbaseReaderErrorCode.REQUIRED_VALUE, "未配置obConfigUrl,且无法获取obConfigUrl"); + } + } + return configUrl; + } + + @Override + public void prepare() { + } + + @Override + public void post() { + } + + @Override + public List split(int adviceNumber) { + Map hbaseColumnCells = ObHbaseReaderUtil.parseColumn(originalConfig.getList(Key.COLUMN, Map.class)); + if (hbaseColumnCells.size() == 0) { + LOG.error("no column cells specified."); + throw new RuntimeException("no column cells specified"); + } + String columnFamily = ObHbaseReaderUtil.parseColumnFamily(hbaseColumnCells.values()); + Preconditions.checkArgument(StringUtils.isNotEmpty(columnFamily), "column family is empty."); + List conns = originalConfig.getList(Constant.CONN_MARK, Object.class); + Preconditions.checkArgument(conns != null && !conns.isEmpty(), "connection information is necessary."); + return splitLogicTables(adviceNumber, conns, columnFamily); + } + + private List splitLogicTables(int adviceNumber, List conns, String columnFamily) { + // adviceNumber这里是channel数量大小, 即datax并发task数量 + // eachTableShouldSplittedNumber是单表应该切分的份数 + int eachTableShouldSplittedNumber = (int) Math.ceil(1.0 * adviceNumber / originalConfig.getInt(Constant.TABLE_NUMBER_MARK)); + boolean useSqlReader = originalConfig.getBool(Key.USE_SQL_READER, com.alibaba.datax.plugin.reader.obhbasereader.Constant.DEFAULT_USE_SQLREADER); + boolean odpMode = originalConfig.getBool(Key.USE_ODP_MODE, DEFAULT_USE_ODPMODE); + boolean readByPartition = originalConfig.getBool(Key.READ_BY_PARTITION, false); + List splittedConfigs = new ArrayList<>(); + + for (int i = 0, len = conns.size(); i < len; i++) { + Configuration sliceConfig = originalConfig.clone(); + Configuration connConf = Configuration.from(conns.get(i).toString()); + copyConnConfByMode(useSqlReader, odpMode, sliceConfig, connConf); + // 说明是配置的 table 方式 + // 已在之前进行了扩展和`处理,可以直接使用 + List tables = connConf.getList(Key.TABLE, String.class); + Validate.isTrue(null != tables && !tables.isEmpty(), "error in your configuration for the reading database table."); + int tempEachTableShouldSplittedNumber = eachTableShouldSplittedNumber; + if (tables.size() == 1) { + Integer splitFactor = originalConfig.getInt(com.alibaba.datax.plugin.rdbms.reader.Key.SPLIT_FACTOR, Constant.SPLIT_FACTOR); + tempEachTableShouldSplittedNumber = eachTableShouldSplittedNumber * splitFactor; + } + for (String table : tables) { + Configuration tempSlice; + tempSlice = sliceConfig.clone(); + tempSlice.set(Key.TABLE, table); + splittedConfigs.addAll( + useSqlReader ? SqlReaderSplitUtil.splitSingleTable(tempSlice, table, columnFamily, tempEachTableShouldSplittedNumber, readByPartition) : HbaseSplitUtil.split(tempSlice)); + } + } + return splittedConfigs; + } + + private void copyConnConfByMode(boolean useSqlReader, boolean odpMode, Configuration targetConf, Configuration sourceConnConf) { + String username = sourceConnConf.getNecessaryValue(Key.USERNAME, DBUtilErrorCode.REQUIRED_VALUE); + targetConf.set(Key.USERNAME, username); + String password = sourceConnConf.getNecessaryValue(Key.PASSWORD, DBUtilErrorCode.REQUIRED_VALUE); + targetConf.set(Key.PASSWORD, password); + + if (useSqlReader) { + String jdbcUrl = sourceConnConf.getNecessaryValue(Key.JDBC_URL, DBUtilErrorCode.REQUIRED_VALUE); + targetConf.set(Key.JDBC_URL, jdbcUrl); + } else if (odpMode) { + String dbName = sourceConnConf.getNecessaryValue(Key.DB_NAME, DBUtilErrorCode.REQUIRED_VALUE); + targetConf.set(Key.DB_NAME, dbName); + String odpHost = sourceConnConf.getNecessaryValue(Key.ODP_HOST, DBUtilErrorCode.REQUIRED_VALUE); + targetConf.set(Key.ODP_HOST, odpHost); + String odpPort = sourceConnConf.getNecessaryValue(Key.ODP_PORT, DBUtilErrorCode.REQUIRED_VALUE); + targetConf.set(Key.ODP_PORT, odpPort); + } else { + String dbName = sourceConnConf.getNecessaryValue(Key.DB_NAME, DBUtilErrorCode.REQUIRED_VALUE); + targetConf.set(Key.DB_NAME, dbName); + String sysUser = sourceConnConf.getNecessaryValue(Key.OB_SYS_USERNAME, DBUtilErrorCode.REQUIRED_VALUE); + targetConf.set(Key.OB_SYS_USERNAME, sysUser); + String sysPass = sourceConnConf.getString(Key.OB_SYS_PASSWORD); + targetConf.set(Key.OB_SYS_PASSWORD, sysPass); + } + targetConf.remove(Constant.CONN_MARK); + } + + private boolean checkVersionAfterV3(String jdbcUrl, String username, String password) { + int retryLimit = 3; + int retryCount = 0; + Connection conn = null; + while (retryCount++ <= retryLimit) { + try { + conn = DBUtil.getConnectionWithoutRetry(DataBaseType.MySql, jdbcUrl, username, password); + ObVersion obVersion = ObReaderUtils.getObVersion(conn); + return ObVersion.V3.compareTo(obVersion) <= 0; + } catch (Exception e) { + LOG.error("fail to check ob version, will retry: " + e.getMessage()); + if (e.getMessage().contains(ACCESS_DENIED_ERROR)) { + throw new RuntimeException(e); + } + try { + TimeUnit.SECONDS.sleep(1); + } catch (Exception ex) { + LOG.error("interrupted while waiting for retry."); + } + } finally { + DBUtil.closeDBResources(null, conn); + } + } + return false; + } + } + + public static class Task extends Reader.Task { + private static Logger LOG = LoggerFactory.getLogger(Task.class); + private Configuration taskConfig; + private AbstractHbaseTask hbaseTaskProxy; + + @Override + public void init() { + this.taskConfig = super.getPluginJobConf(); + + String mode = this.taskConfig.getString(Key.MODE); + ModeType modeType = ModeType.getByTypeName(mode); + boolean useSqlReader = this.taskConfig.getBool(Key.USE_SQL_READER, com.alibaba.datax.plugin.reader.obhbasereader.Constant.DEFAULT_USE_SQLREADER); + LOG.info("init reader with mode: " + modeType); + + switch (modeType) { + case Normal: + this.hbaseTaskProxy = useSqlReader ? new SQLNormalModeReader(this.taskConfig) : new ScanNormalModeReader(this.taskConfig); + break; + case MultiVersionFixedColumn: + this.hbaseTaskProxy = new ScanMultiVersionReader(this.taskConfig); + break; + default: + throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, "This type of mode is not supported by hbasereader:" + modeType); + } + } + + @Override + public void destroy() { + if (this.hbaseTaskProxy != null) { + try { + this.hbaseTaskProxy.close(); + } catch (Exception e) { + // + } + } + } + + @Override + public void prepare() { + try { + this.hbaseTaskProxy.prepare(); + } catch (Exception e) { + throw DataXException.asDataXException(HbaseReaderErrorCode.PREPAR_READ_ERROR, e); + } + } + + @Override + public void post() { + super.post(); + } + + @Override + public void startRead(RecordSender recordSender) { + Record record = recordSender.createRecord(); + boolean fetchOK; + int retryTimes = 0; + int maxRetryTimes = 3; + while (true) { + try { + // TODO check exception + fetchOK = this.hbaseTaskProxy.fetchLine(record); + } catch (Exception e) { + LOG.info("fetch record failed. reason: {}.", e.getMessage(), e); + super.getTaskPluginCollector().collectDirtyRecord(record, e); + if (retryTimes++ > maxRetryTimes) { + throw DataXException.asDataXException(HbaseReaderErrorCode.READ_ERROR, "read from obhbase failed", e); + } + record = recordSender.createRecord(); + continue; + } + if (fetchOK) { + recordSender.sendToWriter(record); + record = recordSender.createRecord(); + } else { + break; + } + } + recordSender.flush(); + } + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/enums/ColumnType.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/enums/ColumnType.java new file mode 100755 index 0000000000..ca4d73a73f --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/enums/ColumnType.java @@ -0,0 +1,44 @@ +package com.alibaba.datax.plugin.reader.obhbasereader.enums; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseReaderErrorCode; + +import java.util.Arrays; + +/** + * 只对 normal 模式读取时有用,多版本读取时,不存在列类型的 + */ +public enum ColumnType { + STRING("string"), + BINARY_STRING("binarystring"), + BYTES("bytes"), + BOOLEAN("boolean"), + SHORT("short"), + INT("int"), + LONG("long"), + FLOAT("float"), + DOUBLE("double"), + DATE("date"); + + private String typeName; + + ColumnType(String typeName) { + this.typeName = typeName; + } + + public static ColumnType getByTypeName(String typeName) { + for (ColumnType columnType : values()) { + if (columnType.typeName.equalsIgnoreCase(typeName)) { + return columnType; + } + } + + throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, + String.format("The type %s is not supported by hbasereader, currently supported type is:%s .", typeName, Arrays.asList(values()))); + } + + @Override + public String toString() { + return this.typeName; + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/enums/FetchVersion.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/enums/FetchVersion.java new file mode 100644 index 0000000000..2bf273c8d1 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/enums/FetchVersion.java @@ -0,0 +1,28 @@ +package com.alibaba.datax.plugin.reader.obhbasereader.enums; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseReaderErrorCode; + +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Stream; + +public enum FetchVersion { + + OLDEST("oldest"), LATEST("latest"); + + private final String version; + + FetchVersion(String version) { + this.version = version; + } + + public static FetchVersion getByDesc(String name) { + Optional result = Stream.of(values()).filter(v -> v.version.equalsIgnoreCase(name)) + .findFirst(); + return result.orElseThrow(() -> { + return DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, + String.format("obHBasereader 不支持该类型:%s, 目前支持的类型是:%s", name, Arrays.asList(values()))); + }); + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/enums/ModeType.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/enums/ModeType.java new file mode 100644 index 0000000000..ccaf879632 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/enums/ModeType.java @@ -0,0 +1,30 @@ +package com.alibaba.datax.plugin.reader.obhbasereader.enums; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseReaderErrorCode; + +import java.util.Arrays; + +public enum ModeType { + Normal("normal"), + MultiVersionFixedColumn("multiVersionFixedColumn"), + MultiVersionDynamicColumn("multiVersionDynamicColumn"), + ; + + private String mode; + + ModeType(String mode) { + this.mode = mode.toLowerCase(); + } + + public static ModeType getByTypeName(String modeName) { + for (ModeType modeType : values()) { + if (modeType.mode.equalsIgnoreCase(modeName)) { + return modeType; + } + } + + throw DataXException.asDataXException( + HbaseReaderErrorCode.ILLEGAL_VALUE, String.format("The mode type is not supported by hbasereader:%s, and the currently supported mode type is:%s", modeName, Arrays.asList(values()))); + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/ext/ServerConnectInfo.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/ext/ServerConnectInfo.java new file mode 100644 index 0000000000..7dca6f5324 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/ext/ServerConnectInfo.java @@ -0,0 +1,146 @@ +package com.alibaba.datax.plugin.reader.obhbasereader.ext; + +import com.google.common.base.Preconditions; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import static org.apache.commons.lang3.StringUtils.EMPTY; + +public class ServerConnectInfo { + + public String clusterName; + public String tenantName; + // userName doesn't contain tenantName or clusterName + public String userName; + public String password; + public String databaseName; + public String ipPort; + public String jdbcUrl; + public String host; + public String port; + public boolean publicCloud; + public int rpcPort; + public String sysUser; + public String sysPass; + + /** + * + * @param jdbcUrl format is jdbc:oceanbase//ip:port + * @param username format is cluster:tenant:username or username@tenant#cluster or user@tenant or user + * @param password + */ + public ServerConnectInfo(final String jdbcUrl, final String username, final String password) { + this(jdbcUrl, username, password, null, null); + } + + public ServerConnectInfo(final String jdbcUrl, final String username, final String password, final String sysUser, final String sysPass) { + if (jdbcUrl.startsWith(com.alibaba.datax.plugin.rdbms.writer.Constant.OB10_SPLIT_STRING)) { + String[] ss = jdbcUrl.split(com.alibaba.datax.plugin.rdbms.writer.Constant.OB10_SPLIT_STRING_PATTERN); + Preconditions.checkArgument(ss.length == 3, "jdbc url format is not correct:" + jdbcUrl); + this.userName = username; + this.clusterName = ss[1].trim().split(":")[0]; + this.tenantName = ss[1].trim().split(":")[1]; + this.jdbcUrl = ss[2]; + } else { + this.jdbcUrl = jdbcUrl; + } + this.password = password; + this.sysUser = sysUser; + this.sysPass = sysPass; + parseJdbcUrl(jdbcUrl); + parseFullUserName(username); + } + + private void parseJdbcUrl(final String jdbcUrl) { + Pattern pattern = Pattern.compile("//([\\w\\.\\-]+:\\d+)/([\\w-]+)\\?"); + Matcher matcher = pattern.matcher(jdbcUrl); + if (matcher.find()) { + String ipPort = matcher.group(1); + String dbName = matcher.group(2); + this.ipPort = ipPort; + String[] hostPort = ipPort.split(":"); + this.host = hostPort[0]; + this.port = hostPort[1]; + this.databaseName = dbName; + this.publicCloud = host.endsWith("aliyuncs.com"); + } else { + throw new RuntimeException("Invalid argument:" + jdbcUrl); + } + } + + private void parseFullUserName(final String fullUserName) { + int tenantIndex = fullUserName.indexOf("@"); + int clusterIndex = fullUserName.indexOf("#"); + // 适用于jdbcUrl以||_dsc_ob10_dsc_开头的场景 + if (fullUserName.contains(":") && tenantIndex < 0) { + String[] names = fullUserName.split(":"); + if (names.length != 3) { + throw new RuntimeException("invalid argument: " + fullUserName); + } else { + this.clusterName = names[0]; + this.tenantName = names[1]; + this.userName = names[2]; + } + } else if (tenantIndex < 0) { + // 适用于short jdbcUrl,且username中不含租户名(主要是公有云场景,此场景下不计算分区) + this.userName = fullUserName; + this.clusterName = EMPTY; + this.tenantName = EMPTY; + } else { + // 适用于short jdbcUrl,且username中含租户名 + this.userName = fullUserName.substring(0, tenantIndex); + if (clusterIndex < 0) { + this.clusterName = EMPTY; + this.tenantName = fullUserName.substring(tenantIndex + 1); + } else { + this.clusterName = fullUserName.substring(clusterIndex + 1); + this.tenantName = fullUserName.substring(tenantIndex + 1, clusterIndex); + } + } + } + + @Override + public String toString() { + return "ServerConnectInfo{" + + "clusterName='" + clusterName + '\'' + + ", tenantName='" + tenantName + '\'' + + ", userName='" + userName + '\'' + + ", password='" + password + '\'' + + ", databaseName='" + databaseName + '\'' + + ", ipPort='" + ipPort + '\'' + + ", jdbcUrl='" + jdbcUrl + '\'' + + ", publicCloud=" + publicCloud + + ", rpcPort=" + rpcPort + + '}'; + } + + public String getFullUserName() { + StringBuilder builder = new StringBuilder(); + builder.append(userName); + if (publicCloud || (rpcPort != 0 && EMPTY.equals(clusterName))) { + return builder.toString(); + } + if (!EMPTY.equals(tenantName)) { + builder.append("@").append(tenantName); + } + + if (!EMPTY.equals(clusterName)) { + builder.append("#").append(clusterName); + } + if (EMPTY.equals(this.clusterName) && EMPTY.equals(this.tenantName)) { + return this.userName; + } + return builder.toString(); + } + + public void setRpcPort(int rpcPort) { + this.rpcPort = rpcPort; + } + + public void setSysUser(String sysUser) { + this.sysUser = sysUser; + } + + public void setSysPass(String sysPass) { + this.sysPass = sysPass; + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/AbstractHbaseTask.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/AbstractHbaseTask.java new file mode 100755 index 0000000000..6f43a8ac84 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/AbstractHbaseTask.java @@ -0,0 +1,41 @@ +package com.alibaba.datax.plugin.reader.obhbasereader.task; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.obhbasereader.Constant; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseColumnCell; +import com.alibaba.datax.plugin.reader.obhbasereader.Key; +import com.alibaba.datax.plugin.reader.obhbasereader.enums.ModeType; +import com.alibaba.datax.plugin.reader.obhbasereader.util.ObHbaseReaderUtil; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractHbaseTask { + protected String encoding; + protected String timezone = null; + protected Map hbaseColumnCellMap; + // 常量字段 + protected Map constantMap; + protected ModeType modeType; + + public AbstractHbaseTask() { + } + + public AbstractHbaseTask(Configuration configuration) { + this.timezone = configuration.getString(Key.TIMEZONE, Constant.DEFAULT_TIMEZONE); + this.encoding = configuration.getString(Key.ENCODING, Constant.DEFAULT_ENCODING); + String mode = configuration.getString(Key.MODE, "Normal"); + this.modeType = ModeType.getByTypeName(mode); + this.constantMap = new HashMap<>(); + this.hbaseColumnCellMap = ObHbaseReaderUtil.parseColumn(configuration.getList(Key.COLUMN, Map.class), constantMap, encoding, timezone); + } + + public abstract void prepare() throws Exception; + + public abstract boolean fetchLine(Record record) throws Exception; + + public abstract void close() throws IOException; +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/AbstractScanReader.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/AbstractScanReader.java new file mode 100755 index 0000000000..8d1e8ce364 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/AbstractScanReader.java @@ -0,0 +1,99 @@ +package com.alibaba.datax.plugin.reader.obhbasereader.task; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.obhbasereader.Constant; +import com.alibaba.datax.plugin.reader.obhbasereader.HTableManager; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseColumnCell; +import com.alibaba.datax.plugin.reader.obhbasereader.Key; +import com.alibaba.datax.plugin.reader.obhbasereader.util.ObHbaseReaderUtil; + +import com.alipay.oceanbase.hbase.OHTable; +import org.apache.hadoop.hbase.client.Result; +import org.apache.hadoop.hbase.client.ResultScanner; +import org.apache.hadoop.hbase.client.Scan; +import org.apache.hadoop.hbase.util.Bytes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public abstract class AbstractScanReader extends AbstractHbaseTask { + private static Logger LOG = LoggerFactory.getLogger(AbstractScanReader.class); + protected OHTable ohtable; + protected Result lastResult = null; + protected Scan scan; + protected ResultScanner resultScanner; + protected int maxVersion; + private int scanCache; + private byte[] startKey = null; + private byte[] endKey = null; + + public AbstractScanReader(Configuration configuration) { + super(configuration); + this.maxVersion = configuration.getInt(Key.MAX_VERSION, 1); + this.scanCache = configuration.getInt(Key.SCAN_CACHE, Constant.DEFAULT_SCAN_CACHE); + this.ohtable = ObHbaseReaderUtil.initOHtable(configuration); + this.startKey = ObHbaseReaderUtil.convertInnerStartRowkey(configuration); + this.endKey = ObHbaseReaderUtil.convertInnerEndRowkey(configuration); + LOG.info("The task set startRowkey=[{}], endRowkey=[{}].", Bytes.toStringBinary(this.startKey), Bytes.toStringBinary(this.endKey)); + } + + @Override + public void prepare() throws Exception { + this.scan = new Scan(); + this.scan.setSmall(false); + this.scan.setCacheBlocks(false); + this.scan.setStartRow(startKey); + this.scan.setStopRow(endKey); + LOG.info("The task set startRowkey=[{}], endRowkey=[{}].", Bytes.toStringBinary(this.startKey), Bytes.toStringBinary(this.endKey)); + this.scan.setCaching(this.scanCache); + if (this.maxVersion == -1 || this.maxVersion == Integer.MAX_VALUE) { + this.scan.setMaxVersions(); + } else { + this.scan.setMaxVersions(this.maxVersion); + } + initScanColumns(); + this.resultScanner = this.ohtable.getScanner(this.scan); + } + + @Override + public void close() throws IOException { + if (this.resultScanner != null) { + this.resultScanner.close(); + } + HTableManager.closeHTable(this.ohtable); + } + + protected void initScanColumns() { + boolean isConstant; + boolean isRowkeyColumn; + for (HbaseColumnCell cell : this.hbaseColumnCellMap.values()) { + isConstant = cell.isConstant(); + isRowkeyColumn = ObHbaseReaderUtil.isRowkeyColumn(cell.getColumnName()); + if (!isConstant && !isRowkeyColumn) { + LOG.info("columnFamily: " + new String(cell.getCf()) + ", qualifier: " + new String(cell.getQualifier())); + this.scan.addColumn(cell.getCf(), cell.getQualifier()); + } + } + } + + protected Result getNextHbaseRow() throws Exception { + Result result = null; + try { + result = resultScanner.next(); + } catch (Exception e) { + LOG.error("failed to get result", e); + if (lastResult != null) { + scan.setStartRow(lastResult.getRow()); + } + resultScanner = this.ohtable.getScanner(scan); + result = resultScanner.next(); + if (lastResult != null && Bytes.equals(lastResult.getRow(), result.getRow())) { + result = resultScanner.next(); + } + } + lastResult = result; + // may be null + return result; + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/SQLNormalModeReader.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/SQLNormalModeReader.java new file mode 100755 index 0000000000..327ac971fe --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/SQLNormalModeReader.java @@ -0,0 +1,257 @@ +package com.alibaba.datax.plugin.reader.obhbasereader.task; + +import static com.alibaba.datax.plugin.reader.obhbasereader.Constant.OB_READ_HINT; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.reader.obhbasereader.Constant; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseColumnCell; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseReaderErrorCode; +import com.alibaba.datax.plugin.reader.obhbasereader.Key; +import com.alibaba.datax.plugin.reader.obhbasereader.enums.FetchVersion; +import com.alibaba.datax.plugin.reader.obhbasereader.util.ObHbaseReaderUtil; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.util.ObReaderUtils; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class SQLNormalModeReader extends AbstractHbaseTask { + private final static String QUERY_SQL_TEMPLATE = "select %s K, Q, T, V, hex(K) as `hex` from %s %s"; + private static Logger LOG = LoggerFactory.getLogger(SQLNormalModeReader.class); + private final Map columnMap; + private final Map versionMap; + private final FetchVersion fetchVersion; + private Set columnNames; + private boolean noMoreData = false; + private String querySQL = null; + private Connection conn = null; + private PreparedStatement stmt = null; + private ResultSet rs = null; + private String jdbcUrl = null; + private String columnFamily = null; + private String username = null; + private String password = null; + private int fetchSize = com.alibaba.datax.plugin.reader.obhbasereader.Constant.DEFAULT_FETCH_SIZE; + private long readBatchSize = com.alibaba.datax.plugin.reader.obhbasereader.Constant.DEFAULT_READ_BATCH_SIZE; + private Configuration configuration; + private boolean hasRange = false; + private String[] savepoint = new String[3]; + // only used by unit test + protected boolean reuseConn = false; + + public SQLNormalModeReader(Configuration configuration) { + this.configuration = configuration; + this.hbaseColumnCellMap = ObHbaseReaderUtil.parseColumn(configuration.getList(Key.COLUMN, Map.class)); + if (hbaseColumnCellMap.size() == 0) { + LOG.error("no column cells specified."); + throw new RuntimeException("no column cells specified"); + } + columnFamily = ObHbaseReaderUtil.parseColumnFamily(hbaseColumnCellMap.values()); + this.columnNames = + hbaseColumnCellMap.keySet().stream().map(e -> ObHbaseReaderUtil.isRowkeyColumn(e) ? Constant.ROWKEY_FLAG : e.substring((columnFamily + ":").length())).collect(Collectors.toSet()); + + String partInfo = ""; + String partName = configuration.getString(Key.PARTITION_NAME, null); + if (partName != null) { + partInfo = "partition(" + partName + ")"; + } + + String tableName = configuration.getString(Key.TABLE, null); + String hint = configuration.getString(Key.READER_HINT, OB_READ_HINT); + this.hasRange = !StringUtils.isEmpty(configuration.getString(Key.RANGE, null)); + this.querySQL = String.format(QUERY_SQL_TEMPLATE, hint, tableName + "$" + columnFamily, partInfo); + if (hasRange) { + this.querySQL = querySQL + " where (" + configuration.getString(Key.RANGE) + ")"; + } + this.jdbcUrl = configuration.getString(Key.JDBC_URL, null); + this.username = configuration.getString(Key.USERNAME, null); + this.password = configuration.getString(Key.PASSWORD, null); + this.columnMap = Maps.newHashMap(); + this.versionMap = Maps.newHashMap(); + this.fetchVersion = FetchVersion.getByDesc(configuration.getString("version", FetchVersion.LATEST.name())); + this.timezone = configuration.getString(Key.TIMEZONE, "UTC"); + this.encoding = configuration.getString(Key.ENCODING, Constant.DEFAULT_ENCODING); + this.fetchSize = configuration.getInt(Key.FETCH_SIZE, com.alibaba.datax.plugin.reader.obhbasereader.Constant.DEFAULT_FETCH_SIZE); + this.readBatchSize = configuration.getLong(Key.READ_BATCH_SIZE, com.alibaba.datax.plugin.reader.obhbasereader.Constant.DEFAULT_READ_BATCH_SIZE); + LOG.info("read from jdbcUrl {} with fetchSize {}, readBatchSize {}", jdbcUrl, fetchSize, readBatchSize); + } + + private boolean notFinished(String currentKey) throws SQLException { + boolean updateSuccess = updateResultSet(); + if (updateSuccess) { + String newKey = rs.getString("K"); + return newKey.equals(currentKey); + } else { + noMoreData = true; + Arrays.fill(savepoint, null); + return false; + } + } + + private boolean updateResultSet() throws SQLException { + if (rs != null && rs.next()) { + return true; + } + if (savepoint[0] != null) { + int retryLimit = 10; + int retryCount = 0; + String tempQuery = querySQL + (hasRange ? " and " : " where ") + "(K,Q,T) > (unhex(?),?,?) order by K,Q,T limit " + readBatchSize; + while (retryCount < retryLimit) { + retryCount++; + try { + resetConnection(); + DBUtil.closeDBResources(rs, stmt, null); + stmt = conn.prepareStatement(tempQuery, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + stmt.setFetchSize(fetchSize); + for (int i = 0; i < savepoint.length; i++) { + stmt.setObject(i + 1, savepoint[i]); + } + rs = stmt.executeQuery(); + if (rs.next()) { + LOG.info("execute sql: {}, savepoint:[{}]", tempQuery, Arrays.stream(savepoint).map(e -> "'" + e + "'").collect(Collectors.joining(","))); + return true; + } + // All data in this task are read + break; + } catch (Exception ex) { + LOG.error("failed to query sql, will retry {} times", retryCount, ex); + DBUtil.closeDBResources(rs, stmt, conn); + if (retryCount > retryLimit) { + LOG.error("Sql: [{}] executed failed, savepoint:[{}], reason: {}", tempQuery, Arrays.stream(savepoint).map(e -> "'" + e + "'").collect(Collectors.joining(",")), + ex.getMessage()); + throw new RuntimeException(ex); + } + } + } + } + return false; + } + + @Override + public void prepare() { + int retryLimit = 10; + int retryCount = 0; + while (true) { + retryCount++; + try { + resetConnection(); + String tempQuery = querySQL + " order by K,Q,T limit " + readBatchSize; + stmt = conn.prepareStatement(tempQuery, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + stmt.setFetchSize(fetchSize); + LOG.info("execute sql : {}", tempQuery); + rs = stmt.executeQuery(); + if (!rs.next()) { + noMoreData = true; + } + break; + } catch (Exception e) { + LOG.error("failed to query sql, will retry {} times", retryCount, e); + DBUtil.closeDBResources(rs, stmt, conn); + if (retryCount > retryLimit) { + LOG.error("Sql: [{}] executed failed, reason: {}", querySQL, e.getMessage()); + throw new RuntimeException(e); + } + } + } + } + + @Override + public boolean fetchLine(Record record) throws Exception { + try { + if (noMoreData) { + return false; + } + String currentKey = rs.getString("K"); + savepoint[0] = rs.getString("hex"); + columnMap.put(Constant.ROWKEY_FLAG, currentKey.getBytes()); + do { + String columnName = rs.getString("Q"); + savepoint[1] = columnName; + if (!this.columnNames.contains(columnName)) { + continue; + } + Long version = rs.getLong("T"); + savepoint[2] = String.valueOf(version); + byte[] value = rs.getBytes("V"); + Predicate predicate; + switch (this.fetchVersion) { + case OLDEST: + predicate = v -> v.compareTo(versionMap.getOrDefault(columnName, Long.MIN_VALUE)) > 0; + break; + case LATEST: + predicate = v -> v.compareTo(versionMap.getOrDefault(columnName, Long.MAX_VALUE)) < 0; + break; + default: + throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, "Not support version: " + this.fetchVersion); + } + + if (predicate.test(version)) { + versionMap.put(columnName, version); + columnMap.put(columnName, value); + } + } while (notFinished(currentKey)); + + for (HbaseColumnCell cell : this.hbaseColumnCellMap.values()) { + Column column = null; + if (cell.isConstant()) { + // 对常量字段的处理 + column = this.constantMap.get(cell.getColumnName()); + } else { + String columnName = ObHbaseReaderUtil.isRowkeyColumn(cell.getColumnName()) ? Constant.ROWKEY_FLAG : cell.getColumnName().substring((columnFamily + ":").length()); + byte[] value = null; + if (!columnMap.containsKey(columnName)) { + LOG.debug("{} is not contained in the record with K value={}. consider this record as null record.", columnName, currentKey); + } else { + value = columnMap.get(columnName); + } + column = ObHbaseReaderUtil.buildColumn(value, cell.getColumnType(), encoding, cell.getDateformat(), timezone); + } + record.addColumn(column); + } + } finally { + this.columnMap.clear(); + this.versionMap.clear(); + } + return true; + } + + @Override + public void close() throws IOException { + DBUtil.closeDBResources(rs, stmt, conn); + } + + private void resetConnection() throws SQLException { + if (reuseConn && conn != null && !conn.isClosed()) { + return; + } + // set ob_query_timeout and ob_trx_timeout to a large time in case timeout + int queryTimeoutSeconds = 60 * 60 * 48; + String setQueryTimeout = "set ob_query_timeout=" + (queryTimeoutSeconds * 1000 * 1000L); + String setTrxTimeout = "set ob_trx_timeout=" + ((queryTimeoutSeconds + 5) * 1000 * 1000L); + List newSessionConfig = Lists.newArrayList(setQueryTimeout, setTrxTimeout); + List sessionConfig = configuration.getList(Key.SESSION, new ArrayList<>(), String.class); + newSessionConfig.addAll(sessionConfig); + configuration.set(Key.SESSION, newSessionConfig); + conn = DBUtil.getConnection(DataBaseType.MySql, jdbcUrl, this.username, this.password); + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/ScanMultiVersionReader.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/ScanMultiVersionReader.java new file mode 100755 index 0000000000..872b5f5f58 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/ScanMultiVersionReader.java @@ -0,0 +1,98 @@ +package com.alibaba.datax.plugin.reader.obhbasereader.task; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.LongColumn; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.obhbasereader.Constant; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseColumnCell; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseReaderErrorCode; +import com.alibaba.datax.plugin.reader.obhbasereader.enums.ColumnType; +import com.alibaba.datax.plugin.reader.obhbasereader.util.ObHbaseReaderUtil; + +import org.apache.hadoop.hbase.KeyValue; +import org.apache.hadoop.hbase.client.Result; +import org.apache.hadoop.hbase.util.Bytes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.List; + +public class ScanMultiVersionReader extends AbstractScanReader { + private final static Logger LOG = LoggerFactory.getLogger(ScanMultiVersionReader.class); + private static byte[] COLON_BYTE; + private List kvList = new ArrayList<>(); + private int currentReadPosition = 0; + + // rowKey类型 + private ColumnType rowkeyReadoutType = null; + + public ScanMultiVersionReader(Configuration configuration) { + super(configuration); + HbaseColumnCell rowKey = hbaseColumnCellMap.get(Constant.ROWKEY_FLAG); + if (rowKey != null && rowKey.getColumnType() != null) { + this.rowkeyReadoutType = rowKey.getColumnType(); + } else { + this.rowkeyReadoutType = ColumnType.BYTES; + } + try { + ScanMultiVersionReader.COLON_BYTE = ":".getBytes(encoding); + } catch (UnsupportedEncodingException e) { + throw DataXException.asDataXException(HbaseReaderErrorCode.PREPAR_READ_ERROR, "Failed to get binary of column family and column name colon separator inside the system.", e); + } + } + + private void convertKVToLine(KeyValue keyValue, Record record) throws Exception { + byte[] rawRowkey = keyValue.getRow(); + long timestamp = keyValue.getTimestamp(); + byte[] cfAndQualifierName = Bytes.add(keyValue.getFamily(), ScanMultiVersionReader.COLON_BYTE, keyValue.getQualifier()); + + record.addColumn(convertBytesToAssignType(this.rowkeyReadoutType, rawRowkey)); + + record.addColumn(convertBytesToAssignType(ColumnType.STRING, cfAndQualifierName)); + + // 直接忽略了用户配置的 timestamp 的类型 + record.addColumn(new LongColumn(timestamp)); + + String cfAndQualifierNameStr = Bytes.toString(cfAndQualifierName); + HbaseColumnCell currentCell = hbaseColumnCellMap.get(cfAndQualifierNameStr); + ColumnType valueReadoutType = currentCell != null ? currentCell.getColumnType() : ColumnType.BYTES; + String dateFormat = currentCell != null ? currentCell.getDateformat() : null; + record.addColumn(convertBytesToAssignType(valueReadoutType, keyValue.getValue(), dateFormat)); + } + + private Column convertBytesToAssignType(ColumnType columnType, byte[] byteArray) throws Exception { + return convertBytesToAssignType(columnType, byteArray, null); + } + + private Column convertBytesToAssignType(ColumnType columnType, byte[] byteArray, String dateFormat) throws Exception { + return ObHbaseReaderUtil.buildColumn(byteArray, columnType, encoding, dateFormat, timezone); + } + + @Override + public boolean fetchLine(Record record) throws Exception { + Result result; + if (this.kvList.size() == this.currentReadPosition) { + result = getNextHbaseRow(); + if (result == null) { + return false; + } + this.kvList = result.list(); + if (this.kvList == null) { + return false; + } + this.currentReadPosition = 0; + } + + try { + KeyValue keyValue = this.kvList.get(this.currentReadPosition); + convertKVToLine(keyValue, record); + } finally { + this.currentReadPosition++; + } + return true; + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/ScanNormalModeReader.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/ScanNormalModeReader.java new file mode 100644 index 0000000000..37d173025e --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/task/ScanNormalModeReader.java @@ -0,0 +1,65 @@ +package com.alibaba.datax.plugin.reader.obhbasereader.task; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.element.StringColumn; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseColumnCell; +import com.alibaba.datax.plugin.reader.obhbasereader.enums.ColumnType; +import com.alibaba.datax.plugin.reader.obhbasereader.util.ObHbaseReaderUtil; + +import org.apache.hadoop.hbase.client.Result; +import org.apache.hadoop.hbase.util.Bytes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ScanNormalModeReader extends AbstractScanReader { + private static Logger LOG = LoggerFactory.getLogger(ScanNormalModeReader.class); + + public ScanNormalModeReader(Configuration configuration) { + super(configuration); + this.maxVersion = 1; + } + + @Override + public boolean fetchLine(Record record) throws Exception { + Result result = getNextHbaseRow(); + if (null == result) { + return false; + } + try { + byte[] hbaseColumnValue; + String columnName; + ColumnType columnType; + + byte[] cf; + byte[] qualifier; + + for (HbaseColumnCell cell : this.hbaseColumnCellMap.values()) { + columnType = cell.getColumnType(); + Column column = null; + if (cell.isConstant()) { + // 对常量字段的处理 + column = constantMap.get(cell.getColumnName()); + } else { + // 根据列名称获取值 + columnName = cell.getColumnName(); + if (ObHbaseReaderUtil.isRowkeyColumn(columnName)) { + hbaseColumnValue = result.getRow(); + } else { + cf = cell.getCf(); + qualifier = cell.getQualifier(); + hbaseColumnValue = result.getValue(cf, qualifier); + } + column = ObHbaseReaderUtil.buildColumn(hbaseColumnValue, columnType, super.encoding, cell.getDateformat(), timezone); + } + record.addColumn(column); + } + } catch (Exception e) { + // 注意,这里catch的异常,期望是byte数组转换失败的情况。而实际上,string的byte数组,转成整数类型是不容易报错的。但是转成double类型容易报错。 + record.setColumn(0, new StringColumn(Bytes.toStringBinary(result.getRow()))); + throw e; + } + return true; + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/HbaseSplitUtil.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/HbaseSplitUtil.java new file mode 100755 index 0000000000..2baa227030 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/HbaseSplitUtil.java @@ -0,0 +1,154 @@ +package com.alibaba.datax.plugin.reader.obhbasereader.util; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseReaderErrorCode; +import com.alibaba.datax.plugin.reader.obhbasereader.Key; + +import com.google.common.collect.Lists; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.hbase.HConstants; +import org.apache.hadoop.hbase.util.Bytes; +import org.apache.hadoop.hbase.util.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public final class HbaseSplitUtil { + private final static Logger LOG = LoggerFactory.getLogger(HbaseSplitUtil.class); + + public static List split(Configuration configuration) { + final List ranges = configuration.getListConfiguration(Key.RANGE); + if (CollectionUtils.isEmpty(ranges)) { + return Lists.newArrayList(configuration); + } + + //TODO(yuez) 后续hbase api具备查询region的功能后,这里需要添加查询table region的逻辑,并且取table region和用户指定的range的交集 + List sliceConfs = new ArrayList<>(ranges.size()); + for (Configuration range : ranges) { + byte[] startRowKey = convertUserRowkey(range, true); + byte[] endRowKey = convertUserRowkey(range, false); + if (startRowKey.length != 0 && endRowKey.length != 0 && Bytes.compareTo(startRowKey, endRowKey) > 0) { + throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, "The startRowkey in obhbasereader must not be greater than the endRowkey."); + } + Configuration sliceConf = configuration.clone(); + sliceConf.remove(Key.RANGE); + String startKeyStr = Bytes.toStringBinary(startRowKey); + String endRowKeyStr = Bytes.toStringBinary(endRowKey); + sliceConf.set(Key.START_ROWKEY, startKeyStr); + sliceConf.set(Key.END_ROWKEY, endRowKeyStr); + sliceConfs.add(sliceConf); + } + return sliceConfs; + } + + public static byte[] convertUserRowkey(Configuration configuration, boolean isStart) { + String keyName = isStart ? Key.START_ROWKEY : Key.END_ROWKEY; + String startRowkey = configuration.getString(keyName); + if (StringUtils.isBlank(startRowkey)) { + return HConstants.EMPTY_BYTE_ARRAY; + } else { + boolean isBinaryRowkey = configuration.getBool(Key.IS_BINARY_ROWKEY, false); + return stringToBytes(startRowkey, isBinaryRowkey); + } + } + + private static byte[] stringToBytes(String rowkey, boolean isBinaryRowkey) { + if (isBinaryRowkey) { + return Bytes.toBytesBinary(rowkey); + } else { + return Bytes.toBytes(rowkey); + } + } + + /** + * 后续hbase api具备查询region的功能后才用得到此方法 + * + * @param config + * @param startRowkeyByte + * @param endRowkeyByte + * @param regionRanges + * @return + */ + private static List doSplit(Configuration config, byte[] startRowkeyByte, byte[] endRowkeyByte, Pair regionRanges) { + + List configurations = new ArrayList(); + + for (int i = 0; i < regionRanges.getFirst().length; i++) { + + byte[] regionStartKey = regionRanges.getFirst()[i]; + byte[] regionEndKey = regionRanges.getSecond()[i]; + + // 当前的region为最后一个region + // 如果最后一个region的start Key大于用户指定的userEndKey,则最后一个region,应该不包含在内 + // 注意如果用户指定userEndKey为"",则此判断应该不成立。userEndKey为""表示取得最大的region + if (Bytes.compareTo(regionEndKey, HConstants.EMPTY_BYTE_ARRAY) == 0 && (endRowkeyByte.length != 0 && (Bytes.compareTo(regionStartKey, endRowkeyByte) > 0))) { + continue; + } + + // 如果当前的region不是最后一个region, + // 用户配置的userStartKey大于等于region的endkey,则这个region不应该含在内 + if ((Bytes.compareTo(regionEndKey, HConstants.EMPTY_BYTE_ARRAY) != 0) && (Bytes.compareTo(startRowkeyByte, regionEndKey) >= 0)) { + continue; + } + + // 如果用户配置的userEndKey小于等于 region的startkey,则这个region不应该含在内 + // 注意如果用户指定的userEndKey为"",则次判断应该不成立。userEndKey为""表示取得最大的region + if (endRowkeyByte.length != 0 && (Bytes.compareTo(endRowkeyByte, regionStartKey) <= 0)) { + continue; + } + + String thisStartKey = getStartKey(startRowkeyByte, regionStartKey); + String thisEndKey = getEndKey(endRowkeyByte, regionEndKey); + Configuration p = config.clone(); + p.set(Key.START_ROWKEY, thisStartKey); + p.set(Key.END_ROWKEY, thisEndKey); + LOG.debug("startRowkey:[{}], endRowkey:[{}] .", thisStartKey, thisEndKey); + configurations.add(p); + } + + return configurations; + } + + private static String getEndKey(byte[] endRowkeyByte, byte[] regionEndKey) { + if (endRowkeyByte == null) { // 由于之前处理过,所以传入的userStartKey不可能为null + throw new IllegalArgumentException("userEndKey should not be null!"); + } + + byte[] tempEndRowkeyByte; + + if (endRowkeyByte.length == 0) { + tempEndRowkeyByte = regionEndKey; + } else if (Bytes.compareTo(regionEndKey, HConstants.EMPTY_BYTE_ARRAY) == 0) { + // 为最后一个region + tempEndRowkeyByte = endRowkeyByte; + } else { + if (Bytes.compareTo(endRowkeyByte, regionEndKey) > 0) { + tempEndRowkeyByte = regionEndKey; + } else { + tempEndRowkeyByte = endRowkeyByte; + } + } + + return Bytes.toStringBinary(tempEndRowkeyByte); + } + + private static String getStartKey(byte[] startRowkeyByte, byte[] regionStarKey) { + if (startRowkeyByte == null) { // 由于之前处理过,所以传入的userStartKey不可能为null + throw new IllegalArgumentException("userStartKey should not be null!"); + } + + byte[] tempStartRowkeyByte; + + if (Bytes.compareTo(startRowkeyByte, regionStarKey) < 0) { + tempStartRowkeyByte = regionStarKey; + } else { + tempStartRowkeyByte = startRowkeyByte; + } + + return Bytes.toStringBinary(tempStartRowkeyByte); + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/LocalStrings.properties b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/LocalStrings.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/LocalStrings_en_US.properties b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/LocalStrings_en_US.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/LocalStrings_ja_JP.properties b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/LocalStrings_zh_CN.properties b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/LocalStrings_zh_HK.properties b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/LocalStrings_zh_TW.properties b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/ObHbaseReaderUtil.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/ObHbaseReaderUtil.java new file mode 100755 index 0000000000..4177f1f5d4 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/ObHbaseReaderUtil.java @@ -0,0 +1,293 @@ +package com.alibaba.datax.plugin.reader.obhbasereader.util; + +import static com.alibaba.datax.plugin.reader.obhbasereader.enums.ModeType.MultiVersionFixedColumn; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_DATABASE; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_FULL_USER_NAME; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_ODP_ADDR; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_ODP_MODE; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_ODP_PORT; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_PARAM_URL; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_PASSWORD; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_SYS_PASSWORD; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_SYS_USER_NAME; + +import com.alibaba.datax.common.element.BoolColumn; +import com.alibaba.datax.common.element.BytesColumn; +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.DateColumn; +import com.alibaba.datax.common.element.DoubleColumn; +import com.alibaba.datax.common.element.LongColumn; +import com.alibaba.datax.common.element.StringColumn; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.obhbasereader.Constant; +import com.alibaba.datax.plugin.reader.obhbasereader.HTableManager; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseColumnCell; +import com.alibaba.datax.plugin.reader.obhbasereader.HbaseReaderErrorCode; +import com.alibaba.datax.plugin.reader.obhbasereader.Key; +import com.alibaba.datax.plugin.reader.obhbasereader.enums.ColumnType; +import com.alibaba.datax.plugin.reader.obhbasereader.enums.ModeType; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.TypeReference; + +import com.alipay.oceanbase.hbase.OHTable; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.commons.lang3.time.DateUtils; +import org.apache.hadoop.hbase.HConstants; +import org.apache.hadoop.hbase.util.Bytes; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.Charset; +import java.text.SimpleDateFormat; +import java.util.Collection; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class ObHbaseReaderUtil { + private static Logger LOG = LoggerFactory.getLogger(ObHbaseReaderUtil.class); + + public static void doPretreatment(Configuration originalConfig) { + String mode = ObHbaseReaderUtil.dealMode(originalConfig); + originalConfig.set(Key.MODE, mode); + + String encoding = originalConfig.getString(Key.ENCODING, Constant.DEFAULT_ENCODING); + if (!Charset.isSupported(encoding)) { + throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, String.format("The encoding you configured is not supported by hbasereader:[%s]", encoding)); + } + originalConfig.set(Key.ENCODING, encoding); + + // 此处增强一个检查:isBinaryRowkey 配置不能出现在与 hbaseConfig 等配置平级地位 + Boolean isBinaryRowkey = originalConfig.getBool(Key.IS_BINARY_ROWKEY); + if (isBinaryRowkey != null) { + throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, String.format("%s cannot be configured here. It should be configured in range.", Key.IS_BINARY_ROWKEY)); + } + } + + /** + * 对模式以及与模式进行配对的配置进行检查 + */ + private static String dealMode(Configuration originalConfig) { + String mode = originalConfig.getString(Key.MODE); + ModeType modeType = ModeType.getByTypeName(mode); + List column = originalConfig.getList(Key.COLUMN, Map.class); + if (column == null || column.isEmpty()) { + throw DataXException.asDataXException(HbaseReaderErrorCode.REQUIRED_VALUE, + "You have configured the normal mode to read the data in HBase, so you must configure the column in the form of:column:[{\"name\": \"cf0:column0\",\"type\": \"string\"}," + + "{\"name\": \"cf1:column1\",\"type\": \"long\"}]"); + } + + // 通过 parse 进行 column 格式的进一步检查 + ObHbaseReaderUtil.parseColumn(column); + if (MultiVersionFixedColumn.equals(modeType)) { + Integer maxVersion = originalConfig.getInt(Key.MAX_VERSION); + Validate.notNull(maxVersion, String.format("You have configured thw mode %s to read the data in HBase, so you must configure: maxVersion", mode)); + + boolean isMaxVersionValid = maxVersion == -1 || maxVersion > 1; + Validate.isTrue(isMaxVersionValid, String.format( + "You have configured the mode %s to read the data in HBase, but the configured maxVersion value is wrong. maxVersion specifies that: - 1 is to read all versions, and cannot be " + + "configured as 0 or 1 (because 0 or 1, we think the user wants to read the data in normal mode instead of reading in mode %s, the difference is big). If it is greater " + + "than" + + " 1, it means to read the latest corresponding number of versions.", + mode, mode)); + } + return mode; + } + + /** + * 注意:convertUserStartRowkey 和 convertInnerStartRowkey,前者会受到 isBinaryRowkey 的影响,只用于第一次对用户配置的 String 类型的 rowkey 转为二进制时使用。而后者约定:切分时得到的二进制的 rowkey 回填到配置中时采用 + */ + public static byte[] convertInnerStartRowkey(Configuration configuration) { + String startRowkey = configuration.getString(Key.START_ROWKEY); + if (StringUtils.isBlank(startRowkey)) { + return HConstants.EMPTY_BYTE_ARRAY; + } + + return Bytes.toBytesBinary(startRowkey); + } + + public static byte[] convertInnerEndRowkey(Configuration configuration) { + String endRowkey = configuration.getString(Key.END_ROWKEY); + if (StringUtils.isBlank(endRowkey)) { + return HConstants.EMPTY_BYTE_ARRAY; + } + + return Bytes.toBytesBinary(endRowkey); + } + + private static void setObHBaseConfig(com.alibaba.datax.common.util.Configuration confFile, org.apache.hadoop.conf.Configuration oHbaseConf) { + + boolean odpMode = confFile.getBool(Key.USE_ODP_MODE); + String username = confFile.getString(Key.USERNAME); + String password = confFile.getString(Key.PASSWORD); + String dbName = confFile.getString(Key.DB_NAME); + +// oHbaseConf.set(RS_LIST_ACQUIRE_CONNECT_TIMEOUT.getKey(), "500"); +// oHbaseConf.set(RS_LIST_ACQUIRE_READ_TIMEOUT.getKey(), "5000"); + oHbaseConf.set(HBASE_OCEANBASE_FULL_USER_NAME, username); + oHbaseConf.set(HBASE_OCEANBASE_PASSWORD, password); +// oHbaseConf.set(HBASE_, META_SCANNER_CACHING); + if (odpMode) { + oHbaseConf.setBoolean(HBASE_OCEANBASE_ODP_MODE, true); + oHbaseConf.set(HBASE_OCEANBASE_DATABASE, dbName); + oHbaseConf.set(HBASE_OCEANBASE_ODP_ADDR, confFile.getString(Key.ODP_HOST)); + oHbaseConf.setInt(HBASE_OCEANBASE_ODP_PORT, confFile.getInt(Key.ODP_PORT)); + } else { + String clusterName = null; + final Pattern pattern = Pattern.compile("([\\w]+)@([\\w]+)#([\\w]+)"); + Matcher matcher = pattern.matcher(username); + if (matcher.find()) { + clusterName = matcher.group(3); + } else { + throw new RuntimeException("user name is not in the correct format: user@tenant#cluster"); + } + String configUrl = confFile.getString(Key.CONFIG_URL); + if (!configUrl.contains("ObRegion")) { + if (configUrl.contains("?")) { + configUrl += "&ObRegion=" + clusterName; + } else { + configUrl += "?ObRegion=" + clusterName; + } + } + + if (!configUrl.contains("database")) { + configUrl += "&database=" + dbName; + } + oHbaseConf.set(HBASE_OCEANBASE_PARAM_URL, configUrl); + oHbaseConf.set(HBASE_OCEANBASE_SYS_USER_NAME, confFile.getString(Key.OB_SYS_USERNAME)); + oHbaseConf.set(HBASE_OCEANBASE_SYS_PASSWORD, confFile.getString(Key.OB_SYS_PASSWORD)); + } + + String hbaseConf = confFile.getString(Key.HBASE_CONFIG); + Map map = JSON.parseObject(hbaseConf, new TypeReference>() { + }); + if (MapUtils.isNotEmpty(map)) { + for (Map.Entry entry : map.entrySet()) { + oHbaseConf.set(entry.getKey(), entry.getValue()); + } + } + } + + /** + * 每次都获取一个新的HTable 注意:HTable 本身是线程不安全的 + */ + public static OHTable initOHtable(com.alibaba.datax.common.util.Configuration configuration) { + String tableName = configuration.getString(Key.TABLE); + try { + org.apache.hadoop.conf.Configuration oHbaseConf = new org.apache.hadoop.conf.Configuration(); + setObHBaseConfig(configuration, oHbaseConf); + return HTableManager.createHTable(oHbaseConf, tableName); + } catch (Exception e) { + LOG.error("init ohTable error, reason: {}", e.getMessage(), e); + throw DataXException.asDataXException(HbaseReaderErrorCode.INIT_TABLE_ERROR, e); + } + } + + public static boolean isRowkeyColumn(String columnName) { + return Constant.ROWKEY_FLAG.equalsIgnoreCase(columnName); + } + + public static String parseColumnFamily(Collection hbaseColumnCells) { + for (HbaseColumnCell columnCell : hbaseColumnCells) { + if (ObHbaseReaderUtil.isRowkeyColumn(columnCell.getColumnName())) { + continue; + } + if (columnCell.getColumnName() == null || columnCell.getColumnName().split(":").length != 2) { + LOG.error("column cell format is unknown: {}", columnCell); + throw new RuntimeException("Column cell format is unknown: " + columnCell); + } + return columnCell.getColumnName().split(":")[0]; + } + throw new RuntimeException("parse column family failed."); + } + + /** + * 用于解析列配置 + */ + public static LinkedHashMap parseColumn(List column) { + return parseColumn(column, null, Constant.DEFAULT_ENCODING, Constant.DEFAULT_TIMEZONE); + } + + public static LinkedHashMap parseColumn(List column, Map constantMap, String encoding, String timezone) { + LinkedHashMap hbaseColumnCells = new LinkedHashMap<>(column.size()); + boolean cacheConstantValue = constantMap != null; + HbaseColumnCell oneColumnCell; + try { + for (Map aColumn : column) { + ColumnType type = ColumnType.getByTypeName(aColumn.get("type")); + boolean isRowKey = isRowkeyColumn(aColumn.get("name")); + String columnName = isRowKey ? Constant.ROWKEY_FLAG : aColumn.get("name"); + + String columnValue = aColumn.get("value"); + String dateFormat = aColumn.getOrDefault("format", Constant.DEFAULT_DATE_FORMAT); + Validate.isTrue(StringUtils.isNotBlank(columnName) || StringUtils.isNotBlank(columnValue), + "It is either a combination of type + name + format or a combination of type + value + format. Your configuration is neither of the two. Please check and modify it."); + if (type == ColumnType.DATE) { + if (StringUtils.isBlank(dateFormat)) { + LOG.warn("date format for {} is empty, use default date format 'yyyy-MM-dd HH:mm:ss' instead.", columnName); + } + oneColumnCell = new HbaseColumnCell.Builder(type).columnName(columnName).columnValue(columnValue).dateformat(dateFormat).build(); + } else { + oneColumnCell = new HbaseColumnCell.Builder(type).columnName(columnName).columnValue(columnValue).build(); + } + hbaseColumnCells.put(columnName, oneColumnCell); + if (cacheConstantValue && oneColumnCell.isConstant()) { + constantMap.put(columnName, buildColumn(columnValue, type, encoding, dateFormat, timezone)); + } + } + return hbaseColumnCells; + } catch (Exception e) { + LOG.error("parse column failed, reason:{}", e.getMessage(), e); + throw DataXException.asDataXException(HbaseReaderErrorCode.PARSE_COLUMN_ERROR, e.getMessage()); + } + } + + public static Column buildColumn(String columnValue, ColumnType columnType, String encoding, String dateformat, String timezone) throws Exception { + return buildColumn(columnValue.getBytes(encoding), columnType, encoding, dateformat, timezone); + } + + public static Column buildColumn(byte[] columnValue, ColumnType columnType, String encoding, String dateformat, String timezone) throws Exception { + switch (columnType) { + case BOOLEAN: + return new BoolColumn(columnValue == null ? null : Bytes.toBoolean(columnValue)); + case SHORT: + return new LongColumn(columnValue == null ? null : String.valueOf(Bytes.toShort(columnValue))); + case INT: + return new LongColumn(columnValue == null ? null : Bytes.toInt(columnValue)); + case LONG: + return new LongColumn(columnValue == null ? null : Bytes.toLong(columnValue)); + case BYTES: + return new BytesColumn(columnValue == null ? null : columnValue); + case FLOAT: + return new DoubleColumn(columnValue == null ? null : Bytes.toFloat(columnValue)); + case DOUBLE: + return new DoubleColumn(columnValue == null ? null : Bytes.toDouble(columnValue)); + case STRING: + return new StringColumn(columnValue == null ? null : new String(columnValue, encoding)); + case BINARY_STRING: + return new StringColumn(columnValue == null ? null : Bytes.toStringBinary(columnValue)); + case DATE: + String dateValue = Bytes.toStringBinary(columnValue); + String timestamp = null; + try { + long milliSec = Long.parseLong(dateValue); + Date date = new java.util.Date(milliSec); + SimpleDateFormat sdf = new java.text.SimpleDateFormat(dateformat); + sdf.setTimeZone(java.util.TimeZone.getTimeZone(timezone)); + timestamp = sdf.format(date); + } catch (Exception e) { + // this is already formatted timestamp + timestamp = dateValue; + } + return columnValue == null ? null : new DateColumn(DateUtils.parseDate(timestamp, dateformat)); + default: + throw DataXException.asDataXException(HbaseReaderErrorCode.ILLEGAL_VALUE, "obHbasereader 不支持您配置的列类型:" + columnType); + } + } +} diff --git a/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/SqlReaderSplitUtil.java b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/SqlReaderSplitUtil.java new file mode 100644 index 0000000000..bd589500a4 --- /dev/null +++ b/obhbasereader/src/main/java/com/alibaba/datax/plugin/reader/obhbasereader/util/SqlReaderSplitUtil.java @@ -0,0 +1,190 @@ +package com.alibaba.datax.plugin.reader.obhbasereader.util; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.Constant; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.util.SplitedSlice; +import com.alibaba.datax.plugin.reader.obhbasereader.Key; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.util.ExecutorTemplate; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.util.ObReaderUtils; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.util.PartInfo; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.util.PartitionSplitUtil; +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import org.apache.commons.collections.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SqlReaderSplitUtil { + public static final String SAMPLE_SQL_TEMPLATE = "SELECT `hex` FROM (SELECT `hex`,K , bucket, ROW_NUMBER() OVER (PARTITION BY bucket ORDER BY K) rn FROM(SELECT %s `hex`, K ,NTILE(%s) OVER " + + "(ORDER BY K ) bucket FROM (SELECT hex(K) as `hex`, K FROM %s SAMPLE BLOCK(%s)) a) b) c WHERE rn = 1 GROUP BY K ORDER BY K"; + public static final String MIDDLE_RANGE_TEMPLATE = "((K) > (unhex('%s'))) AND ((K) <= (unhex('%s')))"; + public static final String MIN_MAX_RANGE_TEMPLATE = "((K)<= (unhex('%s'))) or ((K) > (unhex('%s')))"; + private static final Logger LOG = LoggerFactory.getLogger(SqlReaderSplitUtil.class); + + public static List splitSingleTable(Configuration configuration, String tableName, String columnFamily, int eachTableShouldSplittedNumber, boolean readByPartition) { + List partitionList = Lists.newArrayList(); + String tableNameWithCf = tableName + "$" + columnFamily; + PartInfo partInfo = PartitionSplitUtil.getObMySQLPartInfoBySQL(configuration, tableNameWithCf); + if (partInfo.isPartitionTable()) { + partitionList.addAll(partInfo.getPartList()); + } + // read all partitions and split job only by partition + if (readByPartition) { + LOG.info("table: [{}] will read only by partition", tableNameWithCf); + return splitSingleTableByPartition(configuration, partitionList); + } + + if (eachTableShouldSplittedNumber <= 1) { + LOG.info("total enable splitted number of table: [{}] is {}, no need to split", tableNameWithCf, eachTableShouldSplittedNumber); + return Lists.newArrayList(configuration); + } + + // If user specified some partitions to be read, + List userSetPartitions = configuration.getList(Key.PARTITION_NAME, String.class); + if (CollectionUtils.isNotEmpty(userSetPartitions)) { + Set partSet = new HashSet<>(partitionList); + // If partition name does not exist in the table, throw exception directly. Case is sensitive. + userSetPartitions.forEach(e -> Preconditions.checkArgument(partSet.contains(e), "partition %s does not exist in table: %s", e, tableNameWithCf)); + partitionList.clear(); + partitionList.addAll(userSetPartitions); + } + + if (partitionList.isEmpty()) { + LOG.info("table: [{}] is not partitioned, just split table by rowKey.", tableNameWithCf); + List splitConfs = splitSingleTableByRowKey(configuration, tableNameWithCf, eachTableShouldSplittedNumber); + LOG.info("total split count of non-partitioned table :[{}] is {}", tableNameWithCf, splitConfs.size()); + return splitConfs; + } else { + ExecutorTemplate> template = new ExecutorTemplate<>("split-rows-by-rowkey-" + tableNameWithCf + "-", eachTableShouldSplittedNumber); + int splitNumPerPartition = (int) Math.ceil(1.0d * eachTableShouldSplittedNumber / partitionList.size()); + LOG.info("table: [{}] is partitioned, split table by rowKey in parallel. splitNumPerPartition is {}", tableNameWithCf, splitNumPerPartition); + for (String partName : partitionList) { + try { + template.submit(() -> { + Configuration tempConf = configuration.clone(); + tempConf.set(Key.PARTITION_NAME, partName); + return splitSingleTableByRowKey(tempConf, tableNameWithCf, splitNumPerPartition); + }); + } catch (Throwable th) { + LOG.error("submit split task of table: [{}-{}] failed, reason: {}", tableNameWithCf, partName, th.getMessage(), th); + } + } + List splitConfs = template.waitForResult().stream().flatMap(Collection::stream).collect(Collectors.toList()); + LOG.info("total split count of partitioned table :[{}] is {}", tableNameWithCf, splitConfs.size()); + return splitConfs; + } + } + + private static List splitSingleTableByPartition(Configuration configuration, List partList) { + if (partList == null || partList.isEmpty()) { + return Lists.newArrayList(configuration); + } + List confList = new ArrayList<>(); + for (String partName : partList) { + LOG.info("read sub task: reading from partition " + partName); + Configuration conf = configuration.clone(); + conf.set(Key.PARTITION_NAME, partName); + confList.add(conf); + } + return confList; + } + + /** + * @param configuration + * @param tableNameWithCf + * @param eachTableShouldSplittedNumber + * @return + */ + public static List splitSingleTableByRowKey(Configuration configuration, String tableNameWithCf, int eachTableShouldSplittedNumber) { + String jdbcURL = configuration.getString(Key.JDBC_URL); + String username = configuration.getString(Key.USERNAME); + String password = configuration.getString(Key.PASSWORD); + String hint = configuration.getString(Key.READER_HINT, com.alibaba.datax.plugin.reader.obhbasereader.Constant.OB_READ_HINT); + String partInfo = ""; + String partName = configuration.getString(Key.PARTITION_NAME, null); + if (partName != null) { + partInfo = " partition(" + partName + ")"; + } + tableNameWithCf += partInfo; + int fetchSize = configuration.getInt(Constant.FETCH_SIZE, com.alibaba.datax.plugin.reader.obhbasereader.Constant.DEFAULT_FETCH_SIZE); + Double percentage = configuration.getDouble(Key.SAMPLE_PERCENTAGE, 0.1); + List slices = new ArrayList<>(); + List pluginParams = new ArrayList<>(); + // set ob_query_timeout and ob_trx_timeout to a large time in case timeout + int queryTimeoutSeconds = 60 * 60 * 48; + try (Connection conn = DBUtil.getConnection(DataBaseType.MySql, jdbcURL, username, password)) { + String setQueryTimeout = "set ob_query_timeout=" + (queryTimeoutSeconds * 1000 * 1000L); + String setTrxTimeout = "set ob_trx_timeout=" + ((queryTimeoutSeconds + 5) * 1000 * 1000L); + try (Statement stmt = conn.createStatement()) { + stmt.execute(setQueryTimeout); + stmt.execute(setTrxTimeout); + } catch (Exception e) { + LOG.warn("set ob_query_timeout and set ob_trx_timeout failed. reason: {}", e.getMessage(), e); + } + slices = getSplitSqlBySample(conn, tableNameWithCf, fetchSize, percentage, eachTableShouldSplittedNumber, hint); + } catch (Throwable e) { + LOG.warn("query rowkey range failed of table: {}. reason: {}. the table will not be splitted.", tableNameWithCf, e.getMessage(), e); + } + + if (!slices.isEmpty()) { + for (SplitedSlice slice : slices) { + Configuration tempConfig = configuration.clone(); + tempConfig.set(Key.RANGE, slice.getRange()); + pluginParams.add(tempConfig); + } + } else { + Configuration tempConfig = configuration.clone(); + pluginParams.add(tempConfig); + } + return pluginParams; + } + + /** + * 按照采样方法切分,不能直接顺序切分否则可能导致原本属于一行的数据被切分为两行 + * + * @param conn + * @param tableName + * @param fetchSize + * @param percentage + * @param adviceNum + * @param hint + * @return List + * @throws SQLException + */ + private static List getSplitSqlBySample(Connection conn, String tableName, int fetchSize, double percentage, int adviceNum, String hint) throws SQLException { + String splitSql = String.format(SAMPLE_SQL_TEMPLATE, hint, adviceNum, tableName, percentage); + LOG.info("split pk [sql={}] is running... ", splitSql); + List boundList = new ArrayList<>(); + try (ResultSet rs = DBUtil.query(conn, splitSql, fetchSize)) { + while (rs.next()) { + boundList.add(rs.getString(1)); + } + } + if (boundList.size() == 0) { + return new ArrayList<>(); + } + List rangeSql = new ArrayList<>(); + for (int i = 0; i < boundList.size() - 1; i++) { + String range = String.format(MIDDLE_RANGE_TEMPLATE, boundList.get(i), boundList.get(i + 1)); + SplitedSlice slice = new SplitedSlice(boundList.get(i), boundList.get(i + 1), range); + rangeSql.add(slice); + } + String range = String.format(MIN_MAX_RANGE_TEMPLATE, boundList.get(0), boundList.get(boundList.size() - 1)); + SplitedSlice slice = new SplitedSlice(null, null, range); + rangeSql.add(slice); + return rangeSql; + } +} diff --git a/obhbasereader/src/main/resources/plugin.json b/obhbasereader/src/main/resources/plugin.json new file mode 100755 index 0000000000..36d52d69ad --- /dev/null +++ b/obhbasereader/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "obhbasereader", + "class": "com.alibaba.datax.plugin.reader.obhbasereader.ObHbaseReader", + "description": "useScene: prod. mechanism: Scan to read data.", + "developer": "alibaba" +} diff --git a/obhbasereader/src/main/resources/plugin_job_template.json b/obhbasereader/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..e8adb94510 --- /dev/null +++ b/obhbasereader/src/main/resources/plugin_job_template.json @@ -0,0 +1,15 @@ +{ + "name": "obhbasereader", + "parameter": { + "hbaseConfig": {}, + "table": "", + "encoding": "", + "mode": "", + "column": [], + "range": { + "startRowkey": "", + "endRowkey": "" + }, + "isBinaryRowkey": true + } +} \ No newline at end of file diff --git a/obhbasewriter/doc/obhbasewriter.md b/obhbasewriter/doc/obhbasewriter.md new file mode 100644 index 0000000000..8c2a6749cb --- /dev/null +++ b/obhbasewriter/doc/obhbasewriter.md @@ -0,0 +1,209 @@ +OceanBase的table api为应用提供了ObHBase的访问接口,因此,OceanBase table api的reader与HBase writer的结构和配置方法类似。 +1 快速介绍 +obhbaseWriter 插件实现了从向ObHbase中写取数据。在底层实现上,obhbaseWriter 通过 HBase 的 Java 客户端连接远程 HBase 服务,并通过 put 方式写入obHbase。 +1.1支持功能 +1、目前obhbasewriter支持的obHbase版本为OceanBase3.x以及4.x版本。 +2、目前obhbasewriter支持源端多个字段拼接作为ObHbase 表的 rowkey,具体配置参考:rowkeyColumn配置; +3、写入obhbase的时间戳(版本)支持:用当前时间作为版本,指定源端列作为版本,指定一个时间 三种方式作为版本; +#### 脚本配置 +```json +{ + "job": { + "setting": { + "speed": { + "channel": 5 + } + }, + "content": [ + { + "reader": { + "name": "txtfilereader", + "parameter": { + "path": "/normal.txt", + "charset": "UTF-8", + "column": [ + { + "index": 0, + "type": "String" + }, + { + "index": 1, + "type": "string" + }, + { + "index": 2, + "type": "string" + }, + { + "index": 3, + "type": "string" + }, + { + "index": 4, + "type": "string" + }, + { + "index": 5, + "type": "string" + }, + { + "index": 6, + "type": "string" + } + + ], + "fieldDelimiter": "," + } + }, + "writer": { + "name": "obhbasewriter", + "parameter": { + "username": "username", + "password": "password", + "writerThreadCount": "20", + "writeBufferHighMark": "2147483647", + "rpcExecuteTimeout": "30000", + "useOdpMode": "false", + "obSysUser": "root", + "obSysPassword": "", + "column": [ + { + "index": 0, + "name": "family1:c1", + "type": "string" + }, + { + "index": 1, + "name": "family1:c2", + "type": "string" + }, + { + "index": 2, + "name": "family1:c3", + "type": "string" + }, + { + "index": 3, + "name": "family1:c4", + "type": "string" + }, + { + "index": 4, + "name": "family1:c5", + "type": "string" + }, + { + "index": 5, + "name": "family1:c6", + "type": "string" + }, + { + "index": 6, + "name": "family1:c7", + "type": "string" + } + ], + "mode": "normal", + "rowkeyColumn": [ + { + "index": 0, + "type": "string" + }, + { + "index": 3, + "type": "string" + }, + { + "index": 2, + "type": "string" + }, + { + "index": 1, + "type": "string" + } + ], + "table": "htable3", + "batchSize": "200", + "dbName": "database", + "jdbcUrl": "jdbc:mysql://ip:port/database?" + } + } + } + ] + } +} +``` +##### 参数解释 + +- **connection** + +公有云和私有云需要配置的信息不同,具体如下: +公有云: + +- 数据库用户名;(在外层统一配置) +- 用户密码;(在外层统一配置) +- proxy的jdbc地址 +- 数据库名称; + +私有云: + +- 数据库用户名;(在外层统一配置) +- 用户密码;(在外层统一配置) +- proxy的jdbc地址 +- obSysUser:sys租户的用户名; +- obSysPass:sys租户的密码; +- configUrl; + - 描述:可以通过show parameters like 'obConfigUrl' 获得。 + - 必须:是 + - 默认值:无 +- **jdbcUrl** + - 描述:连接ob使用的jdbc url,支持如下两种格式: + - jdbc:mysql://obproxyIp:obproxyPort/db + - 此格式下username需要写成三段式格式 + - ||_dsc_ob10_dsc_||集群名:租户名||_dsc_ob10_dsc_||jdbc:mysql://obproxyIp:obproxyPort/db + - 此格式下username仅填写用户名本身,无需三段式写法 + - 必选:是 + - 默认值:无 +- **table** + - 描述:所选取的需要同步的表。无需增加列族信息。 + - 必选:是 + - 默认值:无 +- **username** + - 描述:访问OceanBase的用户名 + - 必选:是 + - 默认值:无 +- **useOdpMode** + - 描述:是否通过proxy连接。无法提供sys租户帐密时需要设置为true + - 必须:否 + - 默认值:false +- **column** + - 描述:要写入的hbase字段。index:指定该列对应reader端column的索引,从0开始;name:指定hbase表中的列,必须为 列族:列名 的格式;type:指定写入数据类型,用于转换HBase byte[]。配置格式如下: +```json +"column": [ { "index":1, "name": "cf1:q1", "type": "string" }, { "index":2, "name": "cf1:q2", "type": "string" } ] +``` + +- 必选:是 + - 默认值:无 +- **rowkeyColumn** + - 描述:要写入的ObHbase的rowkey列。index:指定该列对应reader端column的索引,从0开始,若为常量index为-1;type:指定写入数据类型,用于转换HBase byte[];value:配置常量,常作为多个字段的拼接符。obhbasewriter会将rowkeyColumn中所有列按照配置顺序进行拼接作为写入hbase的rowkey,不能全为常量。配置格式如下: +```json +"rowkeyColumn": [ { "index":0, "type":"string" }, { "index":-1, "type":"string", "value":"_" } ] +``` + +- 必选:是 + - 默认值:无 +- **versionColumn** + - 描述:指定写入obhbase的时间戳。支持:当前时间、指定时间列,指定时间,三者选一。若不配置表示用当前时间。index:指定对应reader端column的索引,从0开始,需保证能转换为long,若是Date类型,会尝试用yyyy-MM-dd HH:mm:ss和yyyy-MM-dd HH:mm:ss SSS去解析;若为指定时间index为-1;value:指定时间的值,long值。配置格式如下: +```json +"versionColumn":{ "index":1 } +``` +或者 +```json +"versionColumn":{ "index":-1, "value":123456789 } +``` + +- 必选:否 +- 默认值:无 + + + diff --git a/obhbasewriter/pom.xml b/obhbasewriter/pom.xml new file mode 100644 index 0000000000..b6a29f92c2 --- /dev/null +++ b/obhbasewriter/pom.xml @@ -0,0 +1,181 @@ + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + + obhbasewriter + + com.alibaba.datax + 0.0.1-SNAPSHOT + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + guava + com.google.guava + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.springframework + spring-test + 4.0.4.RELEASE + test + + + + + com.google.guava + guava + 33.1.0-jre + + + + + + log4j + log4j + 1.2.16 + + + + org.json + json + 20160810 + + + junit + junit + 4.11 + test + + + org.powermock + powermock-module-junit4 + 1.4.10 + test + + + org.powermock + powermock-api-mockito + 1.4.10 + test + + + org.mockito + mockito-core + 1.8.5 + test + + + + com.oceanbase + obkv-hbase-client + 0.1.4.2 + + + guava + com.google.guava + + + + + + org.apache.hadoop + hadoop-core + 1.0.3 + + + + + + + + src/main/java + + **/*.properties + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + diff --git a/obhbasewriter/src/main/assembly/package.xml b/obhbasewriter/src/main/assembly/package.xml new file mode 100644 index 0000000000..fd05bea3ca --- /dev/null +++ b/obhbasewriter/src/main/assembly/package.xml @@ -0,0 +1,35 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/writer/obhbasewriter + + + target/ + + obhbasewriter-0.0.1-SNAPSHOT.jar + + plugin/writer/obhbasewriter + + + + + + false + plugin/writer/obhbasewriter/libs + runtime + + + diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ColumnType.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ColumnType.java new file mode 100755 index 0000000000..e451071e80 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ColumnType.java @@ -0,0 +1,50 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.MessageSource; +import java.util.Arrays; + + + +import org.apache.commons.lang.StringUtils; + +/** + * 只对 normal 模式读取时有用,多版本读取时,不存在列类型的 + */ +public enum ColumnType { + STRING("string"), + BINARY_STRING("binarystring"), + BYTES("bytes"), + BOOLEAN("boolean"), + SHORT("short"), + INT("int"), + LONG("long"), + FLOAT("float"), + DOUBLE("double"), + DATE("date"), + BINARY("binary"); + + private String typeName; + + ColumnType(String typeName) { + this.typeName = typeName; + } + + public static ColumnType getByTypeName(String typeName) { + if (StringUtils.isBlank(typeName)) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, MessageSource.loadResourceBundle(ColumnType.class).message("columntype.1", typeName, Arrays.asList(values()))); + } + for (ColumnType columnType : values()) { + if (StringUtils.equalsIgnoreCase(columnType.typeName, typeName.trim())) { + return columnType; + } + } + + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, MessageSource.loadResourceBundle(ColumnType.class).message("columntype.1", typeName, Arrays.asList(values()))); + } + + @Override + public String toString() { + return this.typeName; + } +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/Config.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/Config.java new file mode 100644 index 0000000000..2a37d5d5ea --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/Config.java @@ -0,0 +1,42 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter; + +public interface Config { + + String MEMSTORE_THRESHOLD = "memstoreThreshold"; + + double DEFAULT_MEMSTORE_THRESHOLD = 0.9d; + + String MEMSTORE_CHECK_INTERVAL_SECOND = "memstoreCheckIntervalSecond"; + + long DEFAULT_MEMSTORE_CHECK_INTERVAL_SECOND = 30; + + String FAIL_TRY_COUNT = "failTryCount"; + + int DEFAULT_FAIL_TRY_COUNT = 10000; + + String WRITER_THREAD_COUNT = "writerThreadCount"; + + int DEFAULT_WRITER_THREAD_COUNT = 5; + + String CONCURRENT_WRITE = "concurrentWrite"; + + boolean DEFAULT_CONCURRENT_WRITE = true; + + String RS_URL = "rsUrl"; + + String OB_VERSION = "obVersion"; + + String TIMEOUT = "timeout"; + + String PRINT_COST = "printCost"; + + boolean DEFAULT_PRINT_COST = false; + + String COST_BOUND = "costBound"; + + long DEFAULT_COST_BOUND = 20; + + String MAX_ACTIVE_CONNECTION = "maxActiveConnection"; + + int DEFAULT_MAX_ACTIVE_CONNECTION = 2000; +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ConfigKey.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ConfigKey.java new file mode 100755 index 0000000000..bd06524f3a --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ConfigKey.java @@ -0,0 +1,78 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter; + +public final class ConfigKey { + + public final static String HBASE_CONFIG = "hbaseConfig"; + + public final static String TABLE = "table"; + + public final static String DBNAME = "dbName"; + + public final static String OBCONFIG_URL = "obConfigUrl"; + + public final static String JDBC_URL = "jdbcUrl"; + /** + * mode 可以取 normal 或者 multiVersionFixedColumn 或者 multiVersionDynamicColumn 三个值,无默认值。 + *

+ * normal 配合 column(Map 结构的)使用 + *

+ * multiVersion + */ + public final static String MODE = "mode"; + + public final static String ROWKEY_COLUMN = "rowkeyColumn"; + + public final static String VERSION_COLUMN = "versionColumn"; + + /** + * 默认为 utf8 + */ + public final static String ENCODING = "encoding"; + + public final static String COLUMN = "column"; + + public static final String INDEX = "index"; + + public static final String NAME = "name"; + + public static final String TYPE = "type"; + + public static final String VALUE = "value"; + + public static final String FORMAT = "format"; + + /** + * 默认为 EMPTY_BYTES + */ + public static final String NULL_MODE = "nullMode"; + + public static final String TRUNCATE = "truncate"; + + public static final String AUTO_FLUSH = "autoFlush"; + + public static final String WAL_FLAG = "walFlag"; + + public static final String WRITE_BUFFER_SIZE = "writeBufferSize"; + + public static final String MAX_RETRY_COUNT = "maxRetryCount"; + + public static final String USE_ODP_MODE = "useOdpMode"; + + public static final String OB_SYS_USER = "obSysUser"; + + public static final String OB_SYS_PASSWORD = "obSysPassword"; + + public static final String ODP_HOST = "odpHost"; + + public static final String ODP_PORT = "odpPort"; + + public static final String OBHBASE_HTABLE_CLIENT_WRITE_BUFFER = "obhbaseClientWriteBuffer"; + + public static final String OBHBASE_HTABLE_PUT_WRITE_BUFFER_CHECK = "obhbaseHtablePutWriteBufferCheck"; + + public static final String WRITE_BUFFER_LOW_MARK = "writeBufferLowMark"; + + public static final String WRITE_BUFFER_HIGH_MARK = "writeBufferHighMark"; + + public static final String TABLE_CLIENT_RPC_EXECUTE_TIMEOUT = "rpcExecuteTimeout"; +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ConfigValidator.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ConfigValidator.java new file mode 100644 index 0000000000..22224d4b2c --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ConfigValidator.java @@ -0,0 +1,110 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.MessageSource; +import com.alibaba.datax.plugin.rdbms.writer.Key; + +import java.nio.charset.Charset; +import java.util.List; + +/** + * Created by johnxu.xj on Sept 30 2018 + */ +public class ConfigValidator { + private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(ConfigValidator.class); + + public static void validateParameter(com.alibaba.datax.common.util.Configuration originalConfig) { + originalConfig.getNecessaryValue(Key.USERNAME, Hbase094xWriterErrorCode.REQUIRED_VALUE); + originalConfig.getNecessaryValue(Key.PASSWORD, Hbase094xWriterErrorCode.REQUIRED_VALUE); +// originalConfig.getNecessaryValue(ConfigKey.OBCONFIG_URL, Hbase094xWriterErrorCode.REQUIRED_VALUE); + originalConfig.getNecessaryValue(ConfigKey.TABLE, Hbase094xWriterErrorCode.REQUIRED_VALUE); + originalConfig.getNecessaryValue(ConfigKey.DBNAME, Hbase094xWriterErrorCode.REQUIRED_VALUE); + + ConfigValidator.validateMode(originalConfig); + + String encoding = originalConfig.getString(ConfigKey.ENCODING, Constant.DEFAULT_ENCODING); + if (!Charset.isSupported(encoding)) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, MESSAGE_SOURCE.message("hbase094xhelper.9", encoding)); + } + originalConfig.set(ConfigKey.ENCODING, encoding); + } + + public static void validateMode(com.alibaba.datax.common.util.Configuration originalConfig) { + String mode = originalConfig.getNecessaryValue(ConfigKey.MODE, Hbase094xWriterErrorCode.REQUIRED_VALUE); + ModeType modeType = ModeType.getByTypeName(mode); + if (ModeType.Normal.equals(modeType)) { + validateRowkeyColumn(originalConfig); + validateColumn(originalConfig); + validateVersionColumn(originalConfig); + } + + if (originalConfig.getBool(ConfigKey.USE_ODP_MODE)) { + originalConfig.getNecessaryValue(ConfigKey.ODP_HOST, Hbase094xWriterErrorCode.REQUIRED_VALUE); + originalConfig.getNecessaryValue(ConfigKey.ODP_PORT, Hbase094xWriterErrorCode.REQUIRED_VALUE); + } else { + originalConfig.getNecessaryValue(ConfigKey.OBCONFIG_URL, Hbase094xWriterErrorCode.REQUIRED_VALUE); + originalConfig.getNecessaryValue(ConfigKey.OB_SYS_USER, Hbase094xWriterErrorCode.REQUIRED_VALUE); + } + } + + public static void validateColumn(com.alibaba.datax.common.util.Configuration originalConfig) { + List columns = originalConfig.getListConfiguration(ConfigKey.COLUMN); + if (columns == null || columns.isEmpty()) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.REQUIRED_VALUE, MESSAGE_SOURCE.message("hbase094xhelper.11")); + } + for (Configuration aColumn : columns) { + Integer index = aColumn.getInt(ConfigKey.INDEX); + String type = aColumn.getNecessaryValue(ConfigKey.TYPE, Hbase094xWriterErrorCode.REQUIRED_VALUE); + String name = aColumn.getNecessaryValue(ConfigKey.NAME, Hbase094xWriterErrorCode.REQUIRED_VALUE); + ColumnType.getByTypeName(type); + if (name.split(":").length != 2) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, MESSAGE_SOURCE.message("hbase094xhelper.12", name)); + } + if (index == null || index < 0) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, MESSAGE_SOURCE.message("hbase094xhelper.13")); + } + } + } + + public static void validateRowkeyColumn(com.alibaba.datax.common.util.Configuration originalConfig) { + List rowkeyColumn = originalConfig.getListConfiguration(ConfigKey.ROWKEY_COLUMN); + if (rowkeyColumn == null || rowkeyColumn.isEmpty()) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.REQUIRED_VALUE, MESSAGE_SOURCE.message("hbase094xhelper.14")); + } + int rowkeyColumnSize = rowkeyColumn.size(); + //包含{"index":0,"type":"string"} 或者 {"index":-1,"type":"string","value":"_"} + for (Configuration aRowkeyColumn : rowkeyColumn) { + Integer index = aRowkeyColumn.getInt(ConfigKey.INDEX); + String type = aRowkeyColumn.getNecessaryValue(ConfigKey.TYPE, Hbase094xWriterErrorCode.REQUIRED_VALUE); + ColumnType.getByTypeName(type); + if (index == null) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.REQUIRED_VALUE, MESSAGE_SOURCE.message("hbase094xhelper.15")); + } + //不能只有-1列,即rowkey连接串 + if (rowkeyColumnSize == 1 && index == -1) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, MESSAGE_SOURCE.message("hbase094xhelper.16")); + } + if (index == -1) { + aRowkeyColumn.getNecessaryValue(ConfigKey.VALUE, Hbase094xWriterErrorCode.REQUIRED_VALUE); + } + } + } + + public static void validateVersionColumn(com.alibaba.datax.common.util.Configuration originalConfig) { + Configuration versionColumn = originalConfig.getConfiguration(ConfigKey.VERSION_COLUMN); + //为null,表示用当前时间;指定列,需要index + if (versionColumn != null) { + Integer index = versionColumn.getInt(ConfigKey.INDEX); + if (index == null) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.REQUIRED_VALUE, MESSAGE_SOURCE.message("hbase094xhelper.17")); + } + if (index == -1) { + //指定时间,需要index=-1,value + versionColumn.getNecessaryValue(ConfigKey.VALUE, Hbase094xWriterErrorCode.REQUIRED_VALUE); + } else if (index < 0) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, MESSAGE_SOURCE.message("hbase094xhelper.18")); + } + } + } +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/Constant.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/Constant.java new file mode 100755 index 0000000000..910855953d --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/Constant.java @@ -0,0 +1,27 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter; + +import ch.qos.logback.classic.Level; + +public final class Constant { + public static final String DEFAULT_ENCODING = "UTF-8"; + public static final String DEFAULT_DATA_FORMAT = "yyyy-MM-dd HH:mm:ss"; + public static final String DEFAULT_NULL_MODE = "skip"; + public static final long DEFAULT_WRITE_BUFFER_SIZE = 8 * 1024 * 1024; + public static final long DEFAULT_MEMSTORE_CHECK_INTERVAL_SECOND = 30; + public static final double DEFAULT_MEMSTORE_THRESHOLD = 0.9d; + public static final int DEFAULT_FAIL_TRY_COUNT = 10000; + public static final String OB_TABLE_CLIENT_PROPERTY = "logging.path.com.alipay.oceanbase-table-client"; + public static final String OB_TABLE_HBASE_PROPERTY = "logging.path.com.alipay.oceanbase-table-hbase"; + public static final String OB_TABLE_CLIENT_LOG_LEVEL = "logging.level.oceanbase-table-client"; + public static final String OB_TABLE_HBASE_LOG_LEVEL = "logging.level.oceanbase-table-hbase"; + public static final String OB_COM_ALIPAY_TABLE_CLIENT_LOG_LEVEL = "logging.level.com.alipay.oceanbase-table-client"; + public static final String OB_COM_ALIPAY_TABLE_HBASE_LOG_LEVEL = "logging.level.com.alipay.oceanbase-table-hbase"; + public static final String OB_HBASE_LOG_PATH = System.getProperty("datax.home") + "/log/"; + public static final String DEFAULT_OB_TABLE_CLIENT_LOG_LEVEL = Level.OFF.toString(); + public static final String DEFAULT_OB_TABLE_HBASE_LOG_LEVEL = Level.OFF.toString(); + public static final String DEFAULT_NETTY_BUFFER_LOW_WATERMARK = Integer.toString(512 * 1024); + public static final String DEFAULT_NETTY_BUFFER_HIGH_WATERMARK = Integer.toString(1024 * 1024); + public static final String DEFAULT_HBASE_HTABLE_CLIENT_WRITE_BUFFER = "2097152"; + public static final String DEFAULT_HBASE_HTABLE_PUT_WRITE_BUFFER_CHECK = "10"; + public static final String DEFAULT_RPC_EXECUTE_TIMEOUT = "3000"; +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/Hbase094xWriterErrorCode.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/Hbase094xWriterErrorCode.java new file mode 100644 index 0000000000..08529c378c --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/Hbase094xWriterErrorCode.java @@ -0,0 +1,44 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter; + +import com.alibaba.datax.common.spi.ErrorCode; +import com.alibaba.datax.common.util.MessageSource; + +/** + * Created by shf on 16/3/8. + */ +public enum Hbase094xWriterErrorCode implements ErrorCode { + REQUIRED_VALUE("Hbasewriter-00", MessageSource.loadResourceBundle(Hbase094xWriterErrorCode.class).message("errorcode.required_value")), + ILLEGAL_VALUE("Hbasewriter-01", MessageSource.loadResourceBundle(Hbase094xWriterErrorCode.class).message("errorcode.illegal_value")), + GET_HBASE_CONFIG_ERROR("Hbasewriter-02", MessageSource.loadResourceBundle(Hbase094xWriterErrorCode.class).message("errorcode.get_hbase_config_error")), + GET_HBASE_TABLE_ERROR("Hbasewriter-03", MessageSource.loadResourceBundle(Hbase094xWriterErrorCode.class).message("errorcode.get_hbase_table_error")), + CLOSE_HBASE_AMIN_ERROR("Hbasewriter-05", MessageSource.loadResourceBundle(Hbase094xWriterErrorCode.class).message("errorcode.close_hbase_amin_error")), + CLOSE_HBASE_TABLE_ERROR("Hbasewriter-06", MessageSource.loadResourceBundle(Hbase094xWriterErrorCode.class).message("errorcode.close_hbase_table_error")), + PUT_HBASE_ERROR("Hbasewriter-07", MessageSource.loadResourceBundle(Hbase094xWriterErrorCode.class).message("errorcode.put_hbase_error")), + DELETE_HBASE_ERROR("Hbasewriter-08", MessageSource.loadResourceBundle(Hbase094xWriterErrorCode.class).message("errorcode.delete_hbase_error")), + TRUNCATE_HBASE_ERROR("Hbasewriter-09", MessageSource.loadResourceBundle(Hbase094xWriterErrorCode.class).message("errorcode.truncate_hbase_error")), + CONSTRUCT_ROWKEY_ERROR("Hbasewriter-10", MessageSource.loadResourceBundle(Hbase094xWriterErrorCode.class).message("errorcode.construct_rowkey_error")), + CONSTRUCT_VERSION_ERROR("Hbasewriter-11", MessageSource.loadResourceBundle(Hbase094xWriterErrorCode.class).message("errorcode.construct_version_error")), + INIT_ERROR("Hbasewriter-12", MessageSource.loadResourceBundle(Hbase094xWriterErrorCode.class).message("errorcode.init_error")); + private final String code; + private final String description; + + private Hbase094xWriterErrorCode(String code, String description) { + this.code = code; + this.description = description; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + return String.format("Code:[%s], Description:[%s].", this.code, this.description); + } +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/LocalStrings.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/LocalStrings.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/LocalStrings_en_US.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/LocalStrings_en_US.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/LocalStrings_ja_JP.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/LocalStrings_zh_CN.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/LocalStrings_zh_HK.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/LocalStrings_zh_TW.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ModeType.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ModeType.java new file mode 100644 index 0000000000..592a59a88f --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ModeType.java @@ -0,0 +1,30 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter; + +import java.util.Arrays; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.MessageSource; + +public enum ModeType { + Normal("normal"), + MultiVersion("multiVersion"); + + private String mode; + + ModeType(String mode) { + this.mode = mode.toLowerCase(); + } + + public String getMode() { + return mode; + } + + public static ModeType getByTypeName(String modeName) { + for (ModeType modeType : values()) { + if (modeType.mode.equalsIgnoreCase(modeName)) { + return modeType; + } + } + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, MessageSource.loadResourceBundle(ModeType.class).message("modetype.1", modeName, Arrays.asList(values()))); + } +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/NullModeType.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/NullModeType.java new file mode 100644 index 0000000000..6514a1a44a --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/NullModeType.java @@ -0,0 +1,30 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter; + +import java.util.Arrays; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.MessageSource; + +public enum NullModeType { + Skip("skip"), + Empty("empty"); + + private String mode; + + NullModeType(String mode) { + this.mode = mode.toLowerCase(); + } + + public String getMode() { + return mode; + } + + public static NullModeType getByTypeName(String modeName) { + for (NullModeType modeType : values()) { + if (modeType.mode.equalsIgnoreCase(modeName)) { + return modeType; + } + } + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, MessageSource.loadResourceBundle(NullModeType.class).message("nullmodetype.1", modeName, Arrays.asList(values()))); + } +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ObHTableInfo.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ObHTableInfo.java new file mode 100644 index 0000000000..80b15ae9cd --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ObHTableInfo.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021 OceanBase ob-loader-dumper is licensed under Mulan PSL v2. You can use this software according to + * the terms and conditions of the Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. See the Mulan PSL v2 for more + * details. + */ +package com.alibaba.datax.plugin.writer.obhbasewriter; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.Key; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.apache.commons.lang3.tuple.Triple; + +/** + * @author cjyyz + * @date 2023/03/24 + * @since + */ +public class ObHTableInfo { + + /** + * 不带列族的表名,用于构建OHTable + */ + String tableName; + + /** + * 带列族的表名,用于分区计算 + */ + String fullHbaseTableName; + + NullModeType nullModeType; + + String encoding; + + List columns; + + /** + * 记录配置文件中的columns的列族名,字段名,字段类型,避免每次执行插入都解析 + * Triple left : 列族名;middle : 字段名;right:字段类型 + */ + LinkedHashMap> indexColumnInfoMap; + + /** + * 记录配置文件中rowKey的Index,常量值,字段类型,避免每次执行插入都解析 + * Triple left : Index;middle : 常量值;right:字段类型 + */ + List> rowKeyElementList; + + public ObHTableInfo(Configuration configuration) { + this.nullModeType = NullModeType.getByTypeName(configuration.getString(ConfigKey.NULL_MODE, Constant.DEFAULT_NULL_MODE)); + this.encoding = configuration.getString(ConfigKey.ENCODING, Constant.DEFAULT_ENCODING); + this.columns = configuration.getListConfiguration(ConfigKey.COLUMN); + this.indexColumnInfoMap = new LinkedHashMap<>(); + configuration.getListConfiguration(ConfigKey.COLUMN).forEach(e -> { + String[] name = e.getString(ConfigKey.NAME).split(":"); + indexColumnInfoMap.put(e.getInt(ConfigKey.INDEX), Triple.of(name[0], name[1], ColumnType.getByTypeName(e.getString(ConfigKey.TYPE))) + ); + }); + + this.rowKeyElementList = new ArrayList<>(); + configuration.getListConfiguration(ConfigKey.ROWKEY_COLUMN).forEach(e -> { + Integer index = e.getInt(ConfigKey.INDEX); + String constantValue = e.getString(ConfigKey.VALUE); + ColumnType columnType = ColumnType.getByTypeName(e.getString(ConfigKey.TYPE)); + rowKeyElementList.add(Triple.of(index, constantValue, columnType)); + + }); + + this.tableName = configuration.getString(Key.TABLE); + this.fullHbaseTableName = tableName; + if (!fullHbaseTableName.contains("$")) { + String name = columns.get(0).getString(ConfigKey.NAME); + String familyName = name.split(":")[0]; + fullHbaseTableName = fullHbaseTableName + "$" + familyName; + } + } + + public String getTableName() { + return tableName; + } + + public String getFullHbaseTableName() { + return fullHbaseTableName; + } + + public NullModeType getNullModeType() { + return nullModeType; + } + + public String getEncoding() { + return encoding; + } + + public Map> getIndexColumnInfoMap() { + return indexColumnInfoMap; + } + + public List> getRowKeyElementList() { + return rowKeyElementList; + } +} \ No newline at end of file diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ObHbaseWriter.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ObHbaseWriter.java new file mode 100644 index 0000000000..555ce83638 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ObHbaseWriter.java @@ -0,0 +1,267 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter; + +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.DEFAULT_OB_TABLE_CLIENT_LOG_LEVEL; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.DEFAULT_OB_TABLE_HBASE_LOG_LEVEL; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.OB_COM_ALIPAY_TABLE_CLIENT_LOG_LEVEL; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.OB_COM_ALIPAY_TABLE_HBASE_LOG_LEVEL; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.OB_HBASE_LOG_PATH; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.OB_TABLE_CLIENT_LOG_LEVEL; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.OB_TABLE_CLIENT_PROPERTY; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.OB_TABLE_HBASE_LOG_LEVEL; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.OB_TABLE_HBASE_PROPERTY; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.util.ObVersion; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter; +import com.alibaba.datax.plugin.rdbms.writer.Key; +import com.alibaba.datax.plugin.writer.obhbasewriter.ext.ServerConnectInfo; +import com.alibaba.datax.plugin.writer.obhbasewriter.task.ObHBaseWriteTask; +import com.google.common.base.Preconditions; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; + +/** + * + */ +public class ObHbaseWriter extends Writer { + /** + * Job 中的方法仅执行一次,Task 中方法会由框架启动多个 Task 线程并行执行。 + *

+ * 整个 Writer 执行流程是: + * + *

+     * Job类init-->prepare-->split
+     *
+     *                          Task类init-->prepare-->startWrite-->post-->destroy
+     *                          Task类init-->prepare-->startWrite-->post-->destroy
+     *
+     *                                                                            Job类post-->destroy
+     * 
+ */ + public static class Job extends Writer.Job { + private Configuration originalConfig = null; + private static final Logger LOG = LoggerFactory.getLogger(Job.class); + + /** + * 注意:此方法仅执行一次。 最佳实践:通常在这里对用户的配置进行校验:是否缺失必填项?有无错误值?有没有无关配置项?... + * 并给出清晰的报错/警告提示。校验通常建议采用静态工具类进行,以保证本类结构清晰。 + */ + @Override + public void init() { + if (System.getProperty(OB_TABLE_CLIENT_PROPERTY) == null) { + LOG.info(OB_TABLE_CLIENT_PROPERTY + " not set"); + System.setProperty(OB_TABLE_CLIENT_PROPERTY, OB_HBASE_LOG_PATH); + } + if (System.getProperty(OB_TABLE_HBASE_PROPERTY) == null) { + LOG.info(OB_TABLE_HBASE_PROPERTY + " not set"); + System.setProperty(OB_TABLE_HBASE_PROPERTY, OB_HBASE_LOG_PATH); + } + if (System.getProperty(OB_TABLE_CLIENT_LOG_LEVEL) == null) { + LOG.info(OB_TABLE_CLIENT_LOG_LEVEL + " not set"); + System.setProperty(OB_TABLE_CLIENT_LOG_LEVEL, DEFAULT_OB_TABLE_CLIENT_LOG_LEVEL); + } + if (System.getProperty(OB_TABLE_HBASE_LOG_LEVEL) == null) { + LOG.info(OB_TABLE_HBASE_LOG_LEVEL + " not set"); + System.setProperty(OB_TABLE_HBASE_LOG_LEVEL, DEFAULT_OB_TABLE_HBASE_LOG_LEVEL); + } + if (System.getProperty(OB_COM_ALIPAY_TABLE_CLIENT_LOG_LEVEL) == null) { + LOG.info(OB_COM_ALIPAY_TABLE_CLIENT_LOG_LEVEL + " not set"); + System.setProperty(OB_COM_ALIPAY_TABLE_CLIENT_LOG_LEVEL, DEFAULT_OB_TABLE_CLIENT_LOG_LEVEL); + } + if (System.getProperty(OB_COM_ALIPAY_TABLE_HBASE_LOG_LEVEL) == null) { + LOG.info(OB_COM_ALIPAY_TABLE_HBASE_LOG_LEVEL + " not set"); + System.setProperty(OB_COM_ALIPAY_TABLE_HBASE_LOG_LEVEL, DEFAULT_OB_TABLE_HBASE_LOG_LEVEL); + } + + LOG.info("{} is set to {}, {} is set to {}", + OB_TABLE_CLIENT_PROPERTY, OB_HBASE_LOG_PATH, OB_TABLE_HBASE_PROPERTY, OB_HBASE_LOG_PATH); + this.originalConfig = super.getPluginJobConf(); + boolean useOdpMode = originalConfig.getBool(ConfigKey.USE_ODP_MODE, false); + String configUrl = originalConfig.getString(ConfigKey.OBCONFIG_URL, null); + String jdbcUrl = originalConfig.getString(ConfigKey.JDBC_URL, null); + jdbcUrl = DataBaseType.MySql.appendJDBCSuffixForReader(jdbcUrl); + String user = originalConfig.getString(Key.USERNAME, null); + String password = originalConfig.getString(Key.PASSWORD); + ServerConnectInfo serverConnectInfo = new ServerConnectInfo(jdbcUrl, user, password); + if (useOdpMode) { + originalConfig.set(ConfigKey.ODP_HOST, serverConnectInfo.host); + originalConfig.set(ConfigKey.ODP_PORT, serverConnectInfo.port); + } else if (StringUtils.isBlank(configUrl)) { + serverConnectInfo.setSysUser(originalConfig.getString(ConfigKey.OB_SYS_USER)); + serverConnectInfo.setSysPass(originalConfig.getString(ConfigKey.OB_SYS_PASSWORD)); + try { + originalConfig.set(ConfigKey.OBCONFIG_URL, queryRsUrl(serverConnectInfo)); + originalConfig.set(ConfigKey.OB_SYS_USER, serverConnectInfo.sysUser); + originalConfig.set(ConfigKey.OB_SYS_PASSWORD, serverConnectInfo.sysPass); + LOG.info("fetch configUrl success, configUrl is {}", configUrl); + } catch (Exception e) { + LOG.error("fail to get configure url: " + e.getMessage()); + throw DataXException.asDataXException(Hbase094xWriterErrorCode.REQUIRED_VALUE, "Missing obConfigUrl"); + } + } + if (StringUtils.isBlank(originalConfig.getString(ConfigKey.DBNAME))) { + originalConfig.set(ConfigKey.DBNAME, serverConnectInfo.databaseName); + } + ConfigValidator.validateParameter(this.originalConfig); + } + + private String queryRsUrl(ServerConnectInfo serverInfo) { + String configUrl = originalConfig.getString(ConfigKey.OBCONFIG_URL, null); + if (configUrl == null) { + try { + Connection conn = null; + int retry = 0; + final String sysJDBCUrl = serverInfo.jdbcUrl.replace(serverInfo.databaseName, "oceanbase"); + do { + try { + if (retry > 0) { + int sleep = retry > 9 ? 500 : 1 << retry; + try { + TimeUnit.SECONDS.sleep(sleep); + } catch (InterruptedException e) { + } + LOG.warn("retry fetch RsUrl the {} times", retry); + } + conn = DBUtil.getConnection(DataBaseType.OceanBase, sysJDBCUrl, serverInfo.sysUser, serverInfo.sysPass); + String sql = "show parameters like 'obconfig_url'"; + LOG.info("query param: {}", sql); + PreparedStatement stmt = conn.prepareStatement(sql); + ResultSet result = stmt.executeQuery(); + if (result.next()) { + configUrl = result.getString("Value"); + } + if (StringUtils.isNotBlank(configUrl)) { + break; + } + } catch (Exception e) { + ++retry; + LOG.warn("fetch root server list(rsList) error {}", e.getMessage()); + } finally { + DBUtil.closeDBResources(null, conn); + } + } while (retry < 3); + + LOG.info("configure url is: " + configUrl); + originalConfig.set(ConfigKey.OBCONFIG_URL, configUrl); + } catch (Exception e) { + LOG.error("Fail to get configure url: {}", e.getMessage(), e); + throw DataXException.asDataXException(Hbase094xWriterErrorCode.REQUIRED_VALUE, "未配置obConfigUrl,且无法获取obConfigUrl"); + } + } + return configUrl; + } + + /** + * 注意:此方法仅执行一次。 最佳实践:如果 Job 中有需要进行数据同步之前的处理,可以在此处完成,如果没有必要则可以直接去掉。 + */ + // 一般来说,是需要推迟到 task 中进行pre 的执行(单表情况例外) + @Override + public void prepare() { + } + + /** + * 注意:此方法仅执行一次。 最佳实践:通常采用工具静态类完成把 Job 配置切分成多个 Task 配置的工作。 这里的 + * mandatoryNumber 是强制必须切分的份数。 + */ + @Override + public List split(int mandatoryNumber) { + // This function does not need any change. + Configuration simplifiedConf = this.originalConfig; + + List splitResultConfigs = new ArrayList(); + for (int j = 0; j < mandatoryNumber; j++) { + splitResultConfigs.add(simplifiedConf.clone()); + } + return splitResultConfigs; + } + + /** + * 注意:此方法仅执行一次。 最佳实践:如果 Job 中有需要进行数据同步之后的后续处理,可以在此处完成。 + */ + @Override + public void post() { + // No post supported + } + + /** + * 注意:此方法仅执行一次。 最佳实践:通常配合 Job 中的 post() 方法一起完成 Job 的资源释放。 + */ + @Override + public void destroy() { + + } + } + + public static class Task extends Writer.Task { + private Configuration taskConfig; + private CommonRdbmsWriter.Task writerTask; + + /** + * 注意:此方法每个 Task 都会执行一次。 最佳实践:此处通过对 taskConfig 配置的读取,进而初始化一些资源为 + * startWrite()做准备。 + */ + @Override + public void init() { + this.taskConfig = super.getPluginJobConf(); + String mode = this.taskConfig.getString(ConfigKey.MODE); + ModeType modeType = ModeType.getByTypeName(mode); + + switch (modeType) { + case Normal: + try { + this.writerTask = new ObHBaseWriteTask(this.taskConfig); + } catch (Exception e) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.INIT_ERROR, "ObHbase writer init error:" + e.getMessage()); + } + break; + default: + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, "ObHbase not support this mode type:" + modeType); + } + } + + /** + * 注意:此方法每个 Task 都会执行一次。 最佳实践:如果 Task + * 中有需要进行数据同步之前的处理,可以在此处完成,如果没有必要则可以直接去掉。 + */ + @Override + public void prepare() { + this.writerTask.prepare(taskConfig); + } + + /** + * 注意:此方法每个 Task 都会执行一次。 最佳实践:此处适当封装确保简洁清晰完成数据写入工作。 + */ + public void startWrite(RecordReceiver recordReceiver) { + this.writerTask.startWrite(recordReceiver, taskConfig, super.getTaskPluginCollector()); + } + + /** + * 注意:此方法每个 Task 都会执行一次。 最佳实践:如果 Task 中有需要进行数据同步之后的后续处理,可以在此处完成。 + */ + @Override + public void post() { + this.writerTask.post(taskConfig); + } + + /** + * 注意:此方法每个 Task 都会执行一次。 最佳实践:通常配合Task 中的 post() 方法一起完成 Task 的资源释放。 + */ + @Override + public void destroy() { + this.writerTask.destroy(taskConfig); + } + } +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings.properties new file mode 100644 index 0000000000..63a53efab7 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings.properties @@ -0,0 +1 @@ +databasewriterbuffer.1=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684table\u4e0d\u5b58\u5728, \u7b97\u51fa\u7684tableName={0},db={1}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. \ No newline at end of file diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_en_US.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_en_US.properties new file mode 100644 index 0000000000..86b6a8b3b8 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_en_US.properties @@ -0,0 +1 @@ +databasewriterbuffer.1=The [table] calculated based on the rules does not exist. The calculated [tableName]={0}, [db]={1}. Please check the rules you configured. diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_ja_JP.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..63a53efab7 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_ja_JP.properties @@ -0,0 +1 @@ +databasewriterbuffer.1=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684table\u4e0d\u5b58\u5728, \u7b97\u51fa\u7684tableName={0},db={1}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. \ No newline at end of file diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_zh_CN.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..63a53efab7 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_zh_CN.properties @@ -0,0 +1 @@ +databasewriterbuffer.1=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684table\u4e0d\u5b58\u5728, \u7b97\u51fa\u7684tableName={0},db={1}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. \ No newline at end of file diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_zh_HK.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..4940a177b6 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_zh_HK.properties @@ -0,0 +1 @@ +databasewriterbuffer.1=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684table\u4e0d\u5b58\u5728, \u7b97\u51fa\u7684tableName={0},db={1}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219.databasewriterbuffer.1=通過規則計算出來的table不存在, 算出的tableName={0},db={1}, 請檢查您配置的規則. diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_zh_TW.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..4940a177b6 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/LocalStrings_zh_TW.properties @@ -0,0 +1 @@ +databasewriterbuffer.1=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684table\u4e0d\u5b58\u5728, \u7b97\u51fa\u7684tableName={0},db={1}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219.databasewriterbuffer.1=通過規則計算出來的table不存在, 算出的tableName={0},db={1}, 請檢查您配置的規則. diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/ObDataSourceErrorCode.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/ObDataSourceErrorCode.java new file mode 100644 index 0000000000..5e656a6e41 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/ObDataSourceErrorCode.java @@ -0,0 +1,30 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter.ext; + +import com.alibaba.datax.common.spi.ErrorCode; + +public enum ObDataSourceErrorCode implements ErrorCode { + DESC("ObDataSourceError code", "connect error"); + + private final String code; + private final String describe; + + private ObDataSourceErrorCode(String code, String describe) { + this.code = code; + this.describe = describe; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.describe; + } + + @Override + public String toString() { + return String.format("Code:[%s], Describe:[%s]. ", this.code, this.describe); + } +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/ObHbaseTableHolder.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/ObHbaseTableHolder.java new file mode 100644 index 0000000000..7b74b9479c --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/ObHbaseTableHolder.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2021 OceanBase ob-loader-dumper is licensed under Mulan PSL v2. You can use this software according to + * the terms and conditions of the Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. See the Mulan PSL v2 for more + * details. + */ +package com.alibaba.datax.plugin.writer.obhbasewriter.ext; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.plugin.writer.obhbasewriter.Hbase094xWriterErrorCode; +import com.alipay.oceanbase.hbase.OHTable; +import org.apache.hadoop.conf.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author cjyyz + * @date 2023/03/16 + * @since + */ +public class ObHbaseTableHolder { + private static final Logger LOG = LoggerFactory.getLogger(ObHbaseTableHolder.class); + + private Configuration configuration; + + private String hbaseTableName; + + private OHTable ohTable; + + public ObHbaseTableHolder(Configuration configuration, String hbaseTableName) { + this.configuration = configuration; + this.hbaseTableName = hbaseTableName; + } + + public OHTable getOhTable() { + try { + if (ohTable == null) { + ohTable = new OHTable(configuration, hbaseTableName); + } + return ohTable; + } catch (Exception e) { + LOG.error("build obHTable: {} failed. reason: {}", hbaseTableName, e.getMessage()); + throw DataXException.asDataXException(Hbase094xWriterErrorCode.GET_HBASE_TABLE_ERROR, Hbase094xWriterErrorCode.GET_HBASE_TABLE_ERROR.getDescription()); + } + } + + public void destroy() { + try { + if (ohTable != null) { + ohTable.close(); + } + } catch (Exception e) { + LOG.warn("error in closing htable: {}. Reason: {}", hbaseTableName, e.getMessage()); + } + } +} \ No newline at end of file diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/ServerConnectInfo.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/ServerConnectInfo.java new file mode 100644 index 0000000000..80c2d0d9f7 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/ext/ServerConnectInfo.java @@ -0,0 +1,146 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter.ext; + +import com.google.common.base.Preconditions; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import static org.apache.commons.lang3.StringUtils.EMPTY; + +public class ServerConnectInfo { + + public String clusterName; + public String tenantName; + // userName doesn't contain tenantName or clusterName + public String userName; + public String password; + public String databaseName; + public String ipPort; + public String jdbcUrl; + public String host; + public String port; + public boolean publicCloud; + public int rpcPort; + public String sysUser; + public String sysPass; + + /** + * + * @param jdbcUrl format is jdbc:oceanbase//ip:port + * @param username format is cluster:tenant:username or username@tenant#cluster or user@tenant or user + * @param password + */ + public ServerConnectInfo(final String jdbcUrl, final String username, final String password) { + this(jdbcUrl, username, password, null, null); + } + + public ServerConnectInfo(final String jdbcUrl, final String username, final String password, final String sysUser, final String sysPass) { + if (jdbcUrl.startsWith(com.alibaba.datax.plugin.rdbms.writer.Constant.OB10_SPLIT_STRING)) { + String[] ss = jdbcUrl.split(com.alibaba.datax.plugin.rdbms.writer.Constant.OB10_SPLIT_STRING_PATTERN); + Preconditions.checkArgument(ss.length == 3, "jdbc url format is not correct:" + jdbcUrl); + this.userName = username; + this.clusterName = ss[1].trim().split(":")[0]; + this.tenantName = ss[1].trim().split(":")[1]; + this.jdbcUrl = ss[2]; + } else { + this.jdbcUrl = jdbcUrl; + } + this.password = password; + this.sysUser = sysUser; + this.sysPass = sysPass; + parseJdbcUrl(jdbcUrl); + parseFullUserName(username); + } + + private void parseJdbcUrl(final String jdbcUrl) { + Pattern pattern = Pattern.compile("//([\\w\\.\\-]+:\\d+)/([\\w-]+)\\?"); + Matcher matcher = pattern.matcher(jdbcUrl); + if (matcher.find()) { + String ipPort = matcher.group(1); + String dbName = matcher.group(2); + this.ipPort = ipPort; + String[] hostPort = ipPort.split(":"); + this.host = hostPort[0]; + this.port = hostPort[1]; + this.databaseName = dbName; + this.publicCloud = host.endsWith("aliyuncs.com"); + } else { + throw new RuntimeException("Invalid argument:" + jdbcUrl); + } + } + + private void parseFullUserName(final String fullUserName) { + int tenantIndex = fullUserName.indexOf("@"); + int clusterIndex = fullUserName.indexOf("#"); + // 适用于jdbcUrl以||_dsc_ob10_dsc_开头的场景 + if (fullUserName.contains(":") && tenantIndex < 0) { + String[] names = fullUserName.split(":"); + if (names.length != 3) { + throw new RuntimeException("invalid argument: " + fullUserName); + } else { + this.clusterName = names[0]; + this.tenantName = names[1]; + this.userName = names[2]; + } + } else if (tenantIndex < 0) { + // 适用于short jdbcUrl,且username中不含租户名(主要是公有云场景,此场景下不计算分区) + this.userName = fullUserName; + this.clusterName = EMPTY; + this.tenantName = EMPTY; + } else { + // 适用于short jdbcUrl,且username中含租户名 + this.userName = fullUserName.substring(0, tenantIndex); + if (clusterIndex < 0) { + this.clusterName = EMPTY; + this.tenantName = fullUserName.substring(tenantIndex + 1); + } else { + this.clusterName = fullUserName.substring(clusterIndex + 1); + this.tenantName = fullUserName.substring(tenantIndex + 1, clusterIndex); + } + } + } + + @Override + public String toString() { + return "ServerConnectInfo{" + + "clusterName='" + clusterName + '\'' + + ", tenantName='" + tenantName + '\'' + + ", userName='" + userName + '\'' + + ", password='" + password + '\'' + + ", databaseName='" + databaseName + '\'' + + ", ipPort='" + ipPort + '\'' + + ", jdbcUrl='" + jdbcUrl + '\'' + + ", publicCloud=" + publicCloud + + ", rpcPort=" + rpcPort + + '}'; + } + + public String getFullUserName() { + StringBuilder builder = new StringBuilder(); + builder.append(userName); + if (publicCloud || (rpcPort != 0 && EMPTY.equals(clusterName))) { + return builder.toString(); + } + if (!EMPTY.equals(tenantName)) { + builder.append("@").append(tenantName); + } + + if (!EMPTY.equals(clusterName)) { + builder.append("#").append(clusterName); + } + if (EMPTY.equals(this.clusterName) && EMPTY.equals(this.tenantName)) { + return this.userName; + } + return builder.toString(); + } + + public void setRpcPort(int rpcPort) { + this.rpcPort = rpcPort; + } + + public void setSysUser(String sysUser) { + this.sysUser = sysUser; + } + + public void setSysPass(String sysPass) { + this.sysPass = sysPass; + } +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings.properties new file mode 100644 index 0000000000..d41f6151e9 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings.properties @@ -0,0 +1,21 @@ +multitablewritertask.1=\u914d\u7f6e\u7684tableList\u4e3a\u591a\u8868\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u8868\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.2=\u914d\u7f6e\u7684\u591a\u5e93\u4e2d\u7684\u8868\u540d\u6709\u91cd\u590d\u7684\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u5e93\u89c4\u5219\u548c\u5206\u8868\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.3=\u914d\u7f6e\u7684\u6240\u6709\u8868\u540d\u90fd\u76f8\u540c\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u5e93\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.4=\u914d\u7f6e\u7684table\u548cdb\u540d\u79f0\u90fd\u76f8\u540c\uff0c\u6b64\u79cd\u56de\u6d41\u65b9\u5f0f\u4e0d\u652f\u6301 +multitablewritertask.5=\u5217\u914d\u7f6e\u4fe1\u606f\u6709\u9519\u8bef. \u56e0\u4e3a\u60a8\u914d\u7f6e\u7684\u4efb\u52a1\u4e2d\uff0c\u6e90\u5934\u8bfb\u53d6\u5b57\u6bb5\u6570:{0} \u4e0e \u76ee\u7684\u8868\u8981\u5199\u5165\u7684\u5b57\u6bb5\u6570:{1} \u4e0d\u76f8\u7b49. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +multitablewritertask.6=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684tableName\u67e5\u627e\u5bf9\u5e94\u7684db\u4e0d\u5b58\u5728\uff0ctableName={0}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.7=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684db\u548ctable\u4e0d\u5b58\u5728\uff0c\u7b97\u51fa\u7684dbName={0},tableName={1}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.8=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684db\u4e0d\u5b58\u5728\uff0c\u7b97\u51fa\u7684dbName={0}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.9=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684dbName[{0}], \u5b58\u5728\u591a\u5f20\u5206\u8868\uff0c\u8bf7\u914d\u7f6e\u60a8\u7684\u5206\u8868\u89c4\u5219. +multitablewritertask.10=\u9047\u5230OB\u81f4\u547d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 5\u5206\u949f,SQLState:{0},ErrorCode:{1} +multitablewritertask.11=\u9047\u5230OB\u53ef\u6062\u590d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u5206\u949f,SQLState:{0},ErrorCode:{1} +multitablewritertask.12=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u79d2,\u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0},ErrorCode:{1} +multitablewritertask.13=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0},ErrorCode:{1} +multitablewritertask.14=\u5199\u5165\u8868[{0}]\u5931\u8d25,\u4f11\u7720[{1}]\u6beb\u79d2,\u6570\u636e:{2} +multitablewritertask.15=\u5199\u5165\u8868[{0}]\u5b58\u5728\u810f\u6570\u636e,record={1}, \u5199\u5165\u5f02\u5e38\u4e3a: + + +singletablewritertask.1=\u9047\u5230OB\u81f4\u547d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 5\u5206\u949f,SQLState:{0},ErrorCode:{1} +singletablewritertask.2=\u9047\u5230OB\u53ef\u6062\u590d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u5206\u949f,SQLState:{0},ErrorCode:{1} +singletablewritertask.3=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u79d2,\u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0},ErrorCode:{1} +singletablewritertask.4=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0},ErrorCode:{1} \ No newline at end of file diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_en_US.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_en_US.properties new file mode 100644 index 0000000000..010db531ac --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_en_US.properties @@ -0,0 +1,21 @@ +multitablewritertask.1=The configured [tableList] contains multiple tables but no table splitting rules have been configured. Please check your configuration. +multitablewritertask.2=There are repeated table names in the multiple tables you configured, but no database or table splitting rules have been configured. Please check your configuration. +multitablewritertask.3=All configured tables share the same name, but no database splitting rules have been configured. Please check your configuration. +multitablewritertask.4=The configured table and database share the same name. This back-to-source method is not supported. +multitablewritertask.5=Error in column configuration information. In your configured tasks, the number of source fields to be read: {0} and the number of fields to be written to the target table: {1} are not equivalent. Please check your configuration and make corrections. +multitablewritertask.6=The database that corresponds to the [tableName] calculated based on the rules does not exist. The [tableName]={0}. Please check the rules you configured. +multitablewritertask.7=The database and [table] calculated based on the rules do not exist. The calculated [dbName]={0}, and [tableName]={1}. Please check the rules you configured. +multitablewritertask.8=The database calculated based on the rules does not exist. The calculated [dbName]={0}. Please check the rules you configured. +multitablewritertask.9=The [dbName] [{0}] calculated based on the rules contains multiple sub-tables. Please configure your table splitting rules. +multitablewritertask.10=Fatal exception in OB. Roll back this write and hibernate for five minutes. SQLState: {0}. ErrorCode: {1} +multitablewritertask.11=Recoverable exception in OB. Roll back this write and hibernate for one minute. SQLState: {0}. ErrorCode: {1} +multitablewritertask.12=Exception in OB. Roll back this write and hibernate for one second. Write and submit the records one by one. SQLState: {0}. ErrorCode: {1} +multitablewritertask.13=Exception in OB. Roll back this write. Write and submit the records one by one. SQLState: {0}. ErrorCode: {1} +multitablewritertask.14=Failed to write to table: [{0}]. Hibernate for [{1}] milliseconds. Data: {2} +multitablewritertask.15=writing table [{0}] contains dirty data. Record={1}. Writing exception is: + + +singletablewritertask.1=Fatal exception in OB. Roll back this write and hibernate for five minutes. SQLState: {0}. ErrorCode: {1} +singletablewritertask.2=Recoverable exception in OB. Roll back this write and hibernate for one minute. SQLState: {0}. ErrorCode: {1} +singletablewritertask.3=Exception in OB. Roll back this write and hibernate for one second. Write and submit the records one by one. SQLState: {0}. ErrorCode: {1} +singletablewritertask.4=Exception in OB. Roll back this write. Write and submit the records one by one. SQLState: {0}. ErrorCode: {1} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_ja_JP.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..12e3e481f8 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_ja_JP.properties @@ -0,0 +1,21 @@ +multitablewritertask.1=\u914d\u7f6e\u7684tableList\u4e3a\u591a\u8868\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u8868\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.2=\u914d\u7f6e\u7684\u591a\u5e93\u4e2d\u7684\u8868\u540d\u6709\u91cd\u590d\u7684\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u5e93\u89c4\u5219\u548c\u5206\u8868\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.3=\u914d\u7f6e\u7684\u6240\u6709\u8868\u540d\u90fd\u76f8\u540c\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u5e93\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.4=\u914d\u7f6e\u7684table\u548cdb\u540d\u79f0\u90fd\u76f8\u540c\uff0c\u6b64\u79cd\u56de\u6d41\u65b9\u5f0f\u4e0d\u652f\u6301 +multitablewritertask.5=\u5217\u914d\u7f6e\u4fe1\u606f\u6709\u9519\u8bef. \u56e0\u4e3a\u60a8\u914d\u7f6e\u7684\u4efb\u52a1\u4e2d\uff0c\u6e90\u5934\u8bfb\u53d6\u5b57\u6bb5\u6570:{0} \u4e0e \u76ee\u7684\u8868\u8981\u5199\u5165\u7684\u5b57\u6bb5\u6570:{1} \u4e0d\u76f8\u7b49. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +multitablewritertask.6=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684tableName\u67e5\u627e\u5bf9\u5e94\u7684db\u4e0d\u5b58\u5728\uff0ctableName={0}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.7=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684db\u548ctable\u4e0d\u5b58\u5728\uff0c\u7b97\u51fa\u7684dbName={0},tableName={1}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.8=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684db\u4e0d\u5b58\u5728\uff0c\u7b97\u51fa\u7684dbName={0}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.9=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684dbName[{0}], \u5b58\u5728\u591a\u5f20\u5206\u8868\uff0c\u8bf7\u914d\u7f6e\u60a8\u7684\u5206\u8868\u89c4\u5219. +multitablewritertask.10=\u9047\u5230OB\u81f4\u547d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 5\u5206\u949f,SQLState:{0} +multitablewritertask.11=\u9047\u5230OB\u53ef\u6062\u590d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u5206\u949f,SQLState:{0} +multitablewritertask.12=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u79d2,\u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} +multitablewritertask.13=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} +multitablewritertask.14=\u5199\u5165\u8868[{0}]\u5931\u8d25,\u4f11\u7720[{1}]\u6beb\u79d2,\u6570\u636e:{2} +multitablewritertask.15=\u5199\u5165\u8868[{0}]\u5b58\u5728\u810f\u6570\u636e,record={1}, \u5199\u5165\u5f02\u5e38\u4e3a: + + +singletablewritertask.1=\u9047\u5230OB\u81f4\u547d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 5\u5206\u949f,SQLState:{0} +singletablewritertask.2=\u9047\u5230OB\u53ef\u6062\u590d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u5206\u949f,SQLState:{0} +singletablewritertask.3=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u79d2,\u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} +singletablewritertask.4=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} \ No newline at end of file diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_zh_CN.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..12e3e481f8 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_zh_CN.properties @@ -0,0 +1,21 @@ +multitablewritertask.1=\u914d\u7f6e\u7684tableList\u4e3a\u591a\u8868\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u8868\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.2=\u914d\u7f6e\u7684\u591a\u5e93\u4e2d\u7684\u8868\u540d\u6709\u91cd\u590d\u7684\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u5e93\u89c4\u5219\u548c\u5206\u8868\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.3=\u914d\u7f6e\u7684\u6240\u6709\u8868\u540d\u90fd\u76f8\u540c\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u5e93\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.4=\u914d\u7f6e\u7684table\u548cdb\u540d\u79f0\u90fd\u76f8\u540c\uff0c\u6b64\u79cd\u56de\u6d41\u65b9\u5f0f\u4e0d\u652f\u6301 +multitablewritertask.5=\u5217\u914d\u7f6e\u4fe1\u606f\u6709\u9519\u8bef. \u56e0\u4e3a\u60a8\u914d\u7f6e\u7684\u4efb\u52a1\u4e2d\uff0c\u6e90\u5934\u8bfb\u53d6\u5b57\u6bb5\u6570:{0} \u4e0e \u76ee\u7684\u8868\u8981\u5199\u5165\u7684\u5b57\u6bb5\u6570:{1} \u4e0d\u76f8\u7b49. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +multitablewritertask.6=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684tableName\u67e5\u627e\u5bf9\u5e94\u7684db\u4e0d\u5b58\u5728\uff0ctableName={0}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.7=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684db\u548ctable\u4e0d\u5b58\u5728\uff0c\u7b97\u51fa\u7684dbName={0},tableName={1}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.8=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684db\u4e0d\u5b58\u5728\uff0c\u7b97\u51fa\u7684dbName={0}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.9=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684dbName[{0}], \u5b58\u5728\u591a\u5f20\u5206\u8868\uff0c\u8bf7\u914d\u7f6e\u60a8\u7684\u5206\u8868\u89c4\u5219. +multitablewritertask.10=\u9047\u5230OB\u81f4\u547d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 5\u5206\u949f,SQLState:{0} +multitablewritertask.11=\u9047\u5230OB\u53ef\u6062\u590d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u5206\u949f,SQLState:{0} +multitablewritertask.12=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u79d2,\u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} +multitablewritertask.13=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} +multitablewritertask.14=\u5199\u5165\u8868[{0}]\u5931\u8d25,\u4f11\u7720[{1}]\u6beb\u79d2,\u6570\u636e:{2} +multitablewritertask.15=\u5199\u5165\u8868[{0}]\u5b58\u5728\u810f\u6570\u636e,record={1}, \u5199\u5165\u5f02\u5e38\u4e3a: + + +singletablewritertask.1=\u9047\u5230OB\u81f4\u547d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 5\u5206\u949f,SQLState:{0} +singletablewritertask.2=\u9047\u5230OB\u53ef\u6062\u590d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u5206\u949f,SQLState:{0} +singletablewritertask.3=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u79d2,\u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} +singletablewritertask.4=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} \ No newline at end of file diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_zh_HK.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..4c0f8e55bc --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_zh_HK.properties @@ -0,0 +1,41 @@ +multitablewritertask.1=\u914d\u7f6e\u7684tableList\u4e3a\u591a\u8868\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u8868\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.2=\u914d\u7f6e\u7684\u591a\u5e93\u4e2d\u7684\u8868\u540d\u6709\u91cd\u590d\u7684\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u5e93\u89c4\u5219\u548c\u5206\u8868\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.3=\u914d\u7f6e\u7684\u6240\u6709\u8868\u540d\u90fd\u76f8\u540c\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u5e93\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.4=\u914d\u7f6e\u7684table\u548cdb\u540d\u79f0\u90fd\u76f8\u540c\uff0c\u6b64\u79cd\u56de\u6d41\u65b9\u5f0f\u4e0d\u652f\u6301 +multitablewritertask.5=\u5217\u914d\u7f6e\u4fe1\u606f\u6709\u9519\u8bef. \u56e0\u4e3a\u60a8\u914d\u7f6e\u7684\u4efb\u52a1\u4e2d\uff0c\u6e90\u5934\u8bfb\u53d6\u5b57\u6bb5\u6570:{0} \u4e0e \u76ee\u7684\u8868\u8981\u5199\u5165\u7684\u5b57\u6bb5\u6570:{1} \u4e0d\u76f8\u7b49. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +multitablewritertask.6=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684tableName\u67e5\u627e\u5bf9\u5e94\u7684db\u4e0d\u5b58\u5728\uff0ctableName={0}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.7=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684db\u548ctable\u4e0d\u5b58\u5728\uff0c\u7b97\u51fa\u7684dbName={0},tableName={1}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.8=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684db\u4e0d\u5b58\u5728\uff0c\u7b97\u51fa\u7684dbName={0}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.9=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684dbName[{0}], \u5b58\u5728\u591a\u5f20\u5206\u8868\uff0c\u8bf7\u914d\u7f6e\u60a8\u7684\u5206\u8868\u89c4\u5219. +multitablewritertask.10=\u9047\u5230OB\u81f4\u547d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 5\u5206\u949f,SQLState:{0} +multitablewritertask.11=\u9047\u5230OB\u53ef\u6062\u590d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u5206\u949f,SQLState:{0} +multitablewritertask.12=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u79d2,\u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} +multitablewritertask.13=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} +multitablewritertask.14=\u5199\u5165\u8868[{0}]\u5931\u8d25,\u4f11\u7720[{1}]\u6beb\u79d2,\u6570\u636e:{2} +multitablewritertask.15=\u5199\u5165\u8868[{0}]\u5b58\u5728\u810f\u6570\u636e,record={1}, \u5199\u5165\u5f02\u5e38\u4e3a: + + +singletablewritertask.1=\u9047\u5230OB\u81f4\u547d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 5\u5206\u949f,SQLState:{0} +singletablewritertask.2=\u9047\u5230OB\u53ef\u6062\u590d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u5206\u949f,SQLState:{0} +singletablewritertask.3=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u79d2,\u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} +singletablewritertask.4=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0}multitablewritertask.1=配置的tableList為多表,但未配置分表規則,請檢查您的配置 +multitablewritertask.2=配置的多庫中的表名有重複的,但未配置分庫規則和分表規則,請檢查您的配置 +multitablewritertask.3=配置的所有表名都相同,但未配置分庫規則,請檢查您的配置 +multitablewritertask.4=配置的table和db名稱都相同,此種回流方式不支援 +multitablewritertask.5=列配置資訊有錯誤. 因為您配置的任務中,源頭讀取欄位數:{0}與 目的表要寫入的欄位數:{1}不相等. 請檢查您的配置並作出修改. +multitablewritertask.6=通過規則計算出來的tableName查找對應的db不存在,tableName={0}, 請檢查您配置的規則. +multitablewritertask.7=通過規則計算出來的db和table不存在,算出的dbName={0},tableName={1}, 請檢查您配置的規則. +multitablewritertask.8=通過規則計算出來的db不存在,算出的dbName={0}, 請檢查您配置的規則. +multitablewritertask.9=通過規則計算出來的dbName[{0}], 存在多張分表,請配置您的分表規則. +multitablewritertask.10=遇到OB致命異常,回滾此次寫入, 休眠 5分鐘,SQLState:{0} +multitablewritertask.11=遇到OB可恢復異常,回滾此次寫入, 休眠 1分鐘,SQLState:{0} +multitablewritertask.12=遇到OB異常,回滾此次寫入, 休眠 1秒,採用逐條寫入提交,SQLState:{0} +multitablewritertask.13=遇到OB異常,回滾此次寫入, 採用逐條寫入提交,SQLState:{0} +multitablewritertask.14=寫入表[{0}]失敗,休眠[{1}]毫秒,數據:{2} +multitablewritertask.15=寫入表[{0}]存在髒數據,record={1}, 寫入異常為: + + +singletablewritertask.1=遇到OB致命異常,回滾此次寫入, 休眠 5分鐘,SQLState:{0} +singletablewritertask.2=遇到OB可恢復異常,回滾此次寫入, 休眠 1分鐘,SQLState:{0} +singletablewritertask.3=遇到OB異常,回滾此次寫入, 休眠 1秒,採用逐條寫入提交,SQLState:{0} +singletablewritertask.4=遇到OB異常,回滾此次寫入, 採用逐條寫入提交,SQLState:{0} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_zh_TW.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..4c0f8e55bc --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/LocalStrings_zh_TW.properties @@ -0,0 +1,41 @@ +multitablewritertask.1=\u914d\u7f6e\u7684tableList\u4e3a\u591a\u8868\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u8868\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.2=\u914d\u7f6e\u7684\u591a\u5e93\u4e2d\u7684\u8868\u540d\u6709\u91cd\u590d\u7684\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u5e93\u89c4\u5219\u548c\u5206\u8868\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.3=\u914d\u7f6e\u7684\u6240\u6709\u8868\u540d\u90fd\u76f8\u540c\uff0c\u4f46\u672a\u914d\u7f6e\u5206\u5e93\u89c4\u5219\uff0c\u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e +multitablewritertask.4=\u914d\u7f6e\u7684table\u548cdb\u540d\u79f0\u90fd\u76f8\u540c\uff0c\u6b64\u79cd\u56de\u6d41\u65b9\u5f0f\u4e0d\u652f\u6301 +multitablewritertask.5=\u5217\u914d\u7f6e\u4fe1\u606f\u6709\u9519\u8bef. \u56e0\u4e3a\u60a8\u914d\u7f6e\u7684\u4efb\u52a1\u4e2d\uff0c\u6e90\u5934\u8bfb\u53d6\u5b57\u6bb5\u6570:{0} \u4e0e \u76ee\u7684\u8868\u8981\u5199\u5165\u7684\u5b57\u6bb5\u6570:{1} \u4e0d\u76f8\u7b49. \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4f5c\u51fa\u4fee\u6539. +multitablewritertask.6=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684tableName\u67e5\u627e\u5bf9\u5e94\u7684db\u4e0d\u5b58\u5728\uff0ctableName={0}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.7=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684db\u548ctable\u4e0d\u5b58\u5728\uff0c\u7b97\u51fa\u7684dbName={0},tableName={1}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.8=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684db\u4e0d\u5b58\u5728\uff0c\u7b97\u51fa\u7684dbName={0}, \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684\u89c4\u5219. +multitablewritertask.9=\u901a\u8fc7\u89c4\u5219\u8ba1\u7b97\u51fa\u6765\u7684dbName[{0}], \u5b58\u5728\u591a\u5f20\u5206\u8868\uff0c\u8bf7\u914d\u7f6e\u60a8\u7684\u5206\u8868\u89c4\u5219. +multitablewritertask.10=\u9047\u5230OB\u81f4\u547d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 5\u5206\u949f,SQLState:{0} +multitablewritertask.11=\u9047\u5230OB\u53ef\u6062\u590d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u5206\u949f,SQLState:{0} +multitablewritertask.12=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u79d2,\u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} +multitablewritertask.13=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} +multitablewritertask.14=\u5199\u5165\u8868[{0}]\u5931\u8d25,\u4f11\u7720[{1}]\u6beb\u79d2,\u6570\u636e:{2} +multitablewritertask.15=\u5199\u5165\u8868[{0}]\u5b58\u5728\u810f\u6570\u636e,record={1}, \u5199\u5165\u5f02\u5e38\u4e3a: + + +singletablewritertask.1=\u9047\u5230OB\u81f4\u547d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 5\u5206\u949f,SQLState:{0} +singletablewritertask.2=\u9047\u5230OB\u53ef\u6062\u590d\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u5206\u949f,SQLState:{0} +singletablewritertask.3=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u4f11\u7720 1\u79d2,\u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0} +singletablewritertask.4=\u9047\u5230OB\u5f02\u5e38,\u56de\u6eda\u6b64\u6b21\u5199\u5165, \u91c7\u7528\u9010\u6761\u5199\u5165\u63d0\u4ea4,SQLState:{0}multitablewritertask.1=配置的tableList為多表,但未配置分表規則,請檢查您的配置 +multitablewritertask.2=配置的多庫中的表名有重複的,但未配置分庫規則和分表規則,請檢查您的配置 +multitablewritertask.3=配置的所有表名都相同,但未配置分庫規則,請檢查您的配置 +multitablewritertask.4=配置的table和db名稱都相同,此種回流方式不支援 +multitablewritertask.5=列配置資訊有錯誤. 因為您配置的任務中,源頭讀取欄位數:{0}與 目的表要寫入的欄位數:{1}不相等. 請檢查您的配置並作出修改. +multitablewritertask.6=通過規則計算出來的tableName查找對應的db不存在,tableName={0}, 請檢查您配置的規則. +multitablewritertask.7=通過規則計算出來的db和table不存在,算出的dbName={0},tableName={1}, 請檢查您配置的規則. +multitablewritertask.8=通過規則計算出來的db不存在,算出的dbName={0}, 請檢查您配置的規則. +multitablewritertask.9=通過規則計算出來的dbName[{0}], 存在多張分表,請配置您的分表規則. +multitablewritertask.10=遇到OB致命異常,回滾此次寫入, 休眠 5分鐘,SQLState:{0} +multitablewritertask.11=遇到OB可恢復異常,回滾此次寫入, 休眠 1分鐘,SQLState:{0} +multitablewritertask.12=遇到OB異常,回滾此次寫入, 休眠 1秒,採用逐條寫入提交,SQLState:{0} +multitablewritertask.13=遇到OB異常,回滾此次寫入, 採用逐條寫入提交,SQLState:{0} +multitablewritertask.14=寫入表[{0}]失敗,休眠[{1}]毫秒,數據:{2} +multitablewritertask.15=寫入表[{0}]存在髒數據,record={1}, 寫入異常為: + + +singletablewritertask.1=遇到OB致命異常,回滾此次寫入, 休眠 5分鐘,SQLState:{0} +singletablewritertask.2=遇到OB可恢復異常,回滾此次寫入, 休眠 1分鐘,SQLState:{0} +singletablewritertask.3=遇到OB異常,回滾此次寫入, 休眠 1秒,採用逐條寫入提交,SQLState:{0} +singletablewritertask.4=遇到OB異常,回滾此次寫入, 採用逐條寫入提交,SQLState:{0} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/MultiVersionWriteTask.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/MultiVersionWriteTask.java new file mode 100644 index 0000000000..4e400b060c --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/MultiVersionWriteTask.java @@ -0,0 +1,12 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter.task; + +import com.alibaba.datax.common.util.Configuration; + +/** + * TODO(yuez)升级hbase api之后再补充暂时用不到 + */ +public class MultiVersionWriteTask extends ObHBaseWriteTask{ + public MultiVersionWriteTask(Configuration configuration) throws Exception { + super(configuration); + } +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/NormalWriteTask.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/NormalWriteTask.java new file mode 100644 index 0000000000..3113c022ce --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/NormalWriteTask.java @@ -0,0 +1,12 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter.task; + +import com.alibaba.datax.common.util.Configuration; + +/** + * TODO(yuez) 升级hbase api之后再补充暂时用不到 + */ +public class NormalWriteTask extends ObHBaseWriteTask{ + public NormalWriteTask(Configuration configuration) throws Exception { + super(configuration); + } +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/ObHBaseWriteTask.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/ObHBaseWriteTask.java new file mode 100644 index 0000000000..d424f6eac2 --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/ObHBaseWriteTask.java @@ -0,0 +1,317 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter.task; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.MessageSource; +import com.alibaba.datax.plugin.rdbms.reader.Key; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter; +import com.alibaba.datax.plugin.writer.obhbasewriter.Config; +import com.alibaba.datax.plugin.writer.obhbasewriter.ConfigKey; +import com.alibaba.datax.plugin.writer.obhbasewriter.Constant; +import com.alibaba.datax.plugin.writer.obhbasewriter.NullModeType; +import com.alibaba.datax.plugin.writer.obhbasewriter.ObHTableInfo; +import com.alibaba.datax.plugin.writer.obhbasewriter.ext.ServerConnectInfo; +import com.google.common.collect.Lists; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ObHBaseWriteTask extends CommonRdbmsWriter.Task { + private final static MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(ObHBaseWriteTask.class); + private final static Logger LOG = LoggerFactory.getLogger(ObHBaseWriteTask.class); + + public NullModeType nullMode = null; + private int maxRetryCount; + + public List columns; + public List rowkeyColumn; + public Configuration versionColumn; + + public String hbaseTableName; + public String encoding; + public Boolean walFlag; + + String configUrl; + String dbName; + String ip; + String port; + + String fullUserName; + boolean usdOdpMode; + String sysUsername; + String sysPassword; + private ObHTableInfo obHTableInfo; + + private ConcurrentTableWriter concurrentWriter; + private boolean allTaskInQueue = false; + private long startTime = 0; + private String threadName = Thread.currentThread().getName(); + + private Lock lock = new ReentrantLock(); + private Condition condition = lock.newCondition(); + + public ObHBaseWriteTask(Configuration configuration) { + super(DataBaseType.MySql); + init(configuration); + } + + @Override + public void init(com.alibaba.datax.common.util.Configuration configuration) { + this.obHTableInfo = new ObHTableInfo(configuration); + this.hbaseTableName = configuration.getString(ConfigKey.TABLE); + this.columns = configuration.getListConfiguration(ConfigKey.COLUMN); + this.rowkeyColumn = configuration.getListConfiguration(ConfigKey.ROWKEY_COLUMN); + this.versionColumn = configuration.getConfiguration(ConfigKey.VERSION_COLUMN); + this.encoding = configuration.getString(ConfigKey.ENCODING, Constant.DEFAULT_ENCODING); + this.nullMode = NullModeType.getByTypeName(configuration.getString(ConfigKey.NULL_MODE, Constant.DEFAULT_NULL_MODE)); +// this.memstoreThreshold = configuration.getDouble(Config.MEMSTORE_THRESHOLD, Config.DEFAULT_MEMSTORE_THRESHOLD); + this.walFlag = configuration.getBool(ConfigKey.WAL_FLAG, true); + this.maxRetryCount = configuration.getInt(ConfigKey.MAX_RETRY_COUNT, 3); + + // default 1000 rows are committed together + this.batchSize = com.alibaba.datax.plugin.rdbms.writer.Constant.DEFAULT_BATCH_SIZE; + this.batchByteSize = com.alibaba.datax.plugin.rdbms.writer.Constant.DEFAULT_BATCH_BYTE_SIZE; + + this.configUrl = configuration.getString(ConfigKey.OBCONFIG_URL); + this.jdbcUrl = configuration.getString(ConfigKey.JDBC_URL); + this.username = configuration.getString(Key.USERNAME); + this.password = configuration.getString(Key.PASSWORD); + this.dbName = configuration.getString(Key.DBNAME); + this.usdOdpMode = configuration.getBool(ConfigKey.USE_ODP_MODE); + + ServerConnectInfo connectInfo = new ServerConnectInfo(jdbcUrl, username, password); + String clusterName = connectInfo.clusterName; + this.fullUserName = connectInfo.getFullUserName(); + final String[] ipPort = connectInfo.ipPort.split(":"); + if (usdOdpMode) { + this.ip = ipPort[0]; + this.port = ipPort[1]; + } else { + this.sysUsername = configuration.getString(ConfigKey.OB_SYS_USER); + this.sysPassword = configuration.getString(ConfigKey.OB_SYS_PASSWORD); + connectInfo.setSysUser(sysUsername); + connectInfo.setSysPass(sysPassword); + if (!configUrl.contains("ObRegion")) { + if (configUrl.contains("?")) { + configUrl += "&ObRegion=" + clusterName; + } else { + configUrl += "?ObRegion=" + clusterName; + } + } + if (!configUrl.contains("database")) { + configUrl += "&database=" + dbName; + } + } + if (null == concurrentWriter) { + concurrentWriter = new ConcurrentTableWriter(configuration, connectInfo); + allTaskInQueue = false; + } + } + + @Override + public void prepare(Configuration configuration) { + concurrentWriter.start(); + } + + @Override + public void startWrite(RecordReceiver recordReceiver, Configuration configuration, TaskPluginCollector taskPluginCollector) { + this.taskPluginCollector = taskPluginCollector; + int recordCount = 0; + int bufferBytes = 0; + List records = new ArrayList<>(); + try { + Record record; + while ((record = recordReceiver.getFromReader()) != null) { + recordCount++; + bufferBytes += record.getMemorySize(); + records.add(record); + // 按照指定的批大小进行批量写入 + if (records.size() >= batchSize || bufferBytes >= batchByteSize) { + concurrentWriter.addBatchRecords(Lists.newArrayList(records)); + records.clear(); + bufferBytes = 0; + } + } + + if (!records.isEmpty()) { + concurrentWriter.addBatchRecords(records); + } + } catch (Throwable e) { + LOG.warn("startWrite error unexpected ", e); + throw DataXException.asDataXException(DBUtilErrorCode.WRITE_DATA_ERROR, e); + } + LOG.info(recordCount + " rows received."); + waitTaskFinish(); + } + + public void waitTaskFinish() { + this.allTaskInQueue = true; + LOG.info("ConcurrentTableWriter has put all task in queue, queueSize = {}, total = {}, finished = {}", + concurrentWriter.getTaskQueueSize(), + concurrentWriter.getTotalTaskCount(), + concurrentWriter.getFinishTaskCount()); + + lock.lock(); + try { + while (!concurrentWriter.checkFinish()) { + condition.await(50, TimeUnit.MILLISECONDS); + // print statistic + LOG.debug("Statistic total task {}, finished {}, queue Size {}", + concurrentWriter.getTotalTaskCount(), + concurrentWriter.getFinishTaskCount(), + concurrentWriter.getTaskQueueSize()); + concurrentWriter.printStatistics(); + } + } catch (InterruptedException e) { + LOG.warn("Concurrent table writer wait task finish interrupt"); + } finally { + lock.unlock(); + } + LOG.debug("wait all InsertTask finished ..."); + } + + public boolean isFinished() { + return allTaskInQueue && concurrentWriter.checkFinish(); + } + + public void singalTaskFinish() { + lock.lock(); + try { + condition.signal(); + } finally { + lock.unlock(); + } + } + + public void collectDirtyRecord(Record record, Throwable throwable) { + this.taskPluginCollector.collectDirtyRecord(record, throwable); + } + + @Override + public void post(Configuration configuration) { + + } + + @Override + public void destroy(Configuration configuration) { + if (concurrentWriter != null) { + concurrentWriter.destory(); + } + super.destroy(configuration); + } + + public class ConcurrentTableWriter { + private BlockingQueue> queue; + private List putTasks; + private Configuration config; + private AtomicLong totalTaskCount; + private AtomicLong finishTaskCount; + private ServerConnectInfo connectInfo; + private ExecutorService executorService; + private final int threadCount; + + public ConcurrentTableWriter(Configuration config, ServerConnectInfo connectInfo) { + this.threadCount = config.getInt(Config.WRITER_THREAD_COUNT, Config.DEFAULT_WRITER_THREAD_COUNT); + this.queue = new LinkedBlockingQueue>(threadCount << 1); + this.putTasks = new ArrayList(threadCount); + this.config = config; + this.totalTaskCount = new AtomicLong(0); + this.finishTaskCount = new AtomicLong(0); + this.executorService = Executors.newFixedThreadPool(threadCount); + this.connectInfo = connectInfo; + } + + public long getTotalTaskCount() { + return totalTaskCount.get(); + } + + public long getFinishTaskCount() { + return finishTaskCount.get(); + } + + public int getTaskQueueSize() { + return queue.size(); + } + + public void increFinishCount() { + finishTaskCount.incrementAndGet(); + } + + // should check after put all the task in the queue + public boolean checkFinish() { + long finishCount = finishTaskCount.get(); + long totalCount = totalTaskCount.get(); + return finishCount == totalCount; + } + + public synchronized void start() { + for (int i = 0; i < threadCount; ++i) { + LOG.info("start {} insert task.", (i + 1)); + PutTask putTask = new PutTask(threadName, queue, config, connectInfo, obHTableInfo, ObHBaseWriteTask.this); + putTask.setWriter(this); + putTasks.add(putTask); + } + for (PutTask task : putTasks) { + executorService.execute(task); + } + } + + public void printStatistics() { + long insertTotalCost = 0; + long insertTotalCount = 0; + for (PutTask task : putTasks) { + insertTotalCost += task.getTotalCost(); + insertTotalCount += task.getPutCount(); + } + long avgCost = 0; + if (insertTotalCount != 0) { + avgCost = insertTotalCost / insertTotalCount; + } + ObHBaseWriteTask.LOG.debug("Put {} times, totalCost {} ms, average {} ms", + insertTotalCount, insertTotalCost, avgCost); + } + + public void addBatchRecords(final List records) throws InterruptedException { + boolean isSucc = false; + while (!isSucc) { + isSucc = queue.offer(records, 5, TimeUnit.MILLISECONDS); + } + totalTaskCount.incrementAndGet(); + } + + public synchronized void destory() { + if (putTasks != null) { + for (PutTask task : putTasks) { + task.setStop(); + task.destroy(); + } + } + destroyExecutor(); + } + + private void destroyExecutor() { + if (executorService != null && !executorService.isShutdown()) { + executorService.shutdown(); + try { + executorService.awaitTermination(0L, TimeUnit.SECONDS); + } catch (InterruptedException var2) { + } + } + } + } +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/PutTask.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/PutTask.java new file mode 100644 index 0000000000..768772c0ab --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/task/PutTask.java @@ -0,0 +1,325 @@ +package com.alibaba.datax.plugin.writer.obhbasewriter.task; + +import com.alibaba.datax.common.element.DoubleColumn; +import com.alibaba.datax.common.element.LongColumn; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.MessageSource; +import com.alibaba.datax.plugin.writer.obhbasewriter.ColumnType; +import com.alibaba.datax.plugin.writer.obhbasewriter.Config; +import com.alibaba.datax.plugin.writer.obhbasewriter.ConfigKey; +import com.alibaba.datax.plugin.writer.obhbasewriter.Hbase094xWriterErrorCode; +import com.alibaba.datax.plugin.writer.obhbasewriter.ObHTableInfo; +import com.alibaba.datax.plugin.writer.obhbasewriter.ext.ObHbaseTableHolder; +import com.alibaba.datax.plugin.writer.obhbasewriter.ext.ServerConnectInfo; +import com.alipay.oceanbase.hbase.constants.OHConstants; +import com.alipay.oceanbase.rpc.property.Property; + +import com.google.common.base.Stopwatch; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Queue; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.tuple.Triple; +import org.apache.hadoop.hbase.client.HTableInterface; +import org.apache.hadoop.hbase.client.Put; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.alibaba.datax.plugin.writer.obhbasewriter.ConfigKey.OBHBASE_HTABLE_CLIENT_WRITE_BUFFER; +import static com.alibaba.datax.plugin.writer.obhbasewriter.ConfigKey.OBHBASE_HTABLE_PUT_WRITE_BUFFER_CHECK; +import static com.alibaba.datax.plugin.writer.obhbasewriter.ConfigKey.TABLE_CLIENT_RPC_EXECUTE_TIMEOUT; +import static com.alibaba.datax.plugin.writer.obhbasewriter.ConfigKey.WRITE_BUFFER_HIGH_MARK; +import static com.alibaba.datax.plugin.writer.obhbasewriter.ConfigKey.WRITE_BUFFER_LOW_MARK; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.DEFAULT_HBASE_HTABLE_CLIENT_WRITE_BUFFER; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.DEFAULT_HBASE_HTABLE_PUT_WRITE_BUFFER_CHECK; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.DEFAULT_NETTY_BUFFER_HIGH_WATERMARK; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.DEFAULT_NETTY_BUFFER_LOW_WATERMARK; +import static com.alibaba.datax.plugin.writer.obhbasewriter.Constant.DEFAULT_RPC_EXECUTE_TIMEOUT; +import static com.alibaba.datax.plugin.writer.obhbasewriter.util.ObHbaseWriterUtils.getColumnByte; +import static com.alibaba.datax.plugin.writer.obhbasewriter.util.ObHbaseWriterUtils.getRowkey; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_HTABLE_CLIENT_WRITE_BUFFER; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_HTABLE_PUT_WRITE_BUFFER_CHECK; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_DATABASE; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_FULL_USER_NAME; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_PARAM_URL; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_PASSWORD; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_SYS_USER_NAME; +import static com.alipay.oceanbase.hbase.constants.OHConstants.HBASE_OCEANBASE_SYS_PASSWORD; + +public class PutTask implements Runnable { + + private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(PutTask.class); + + private static final Logger LOG = LoggerFactory.getLogger(PutTask.class); + + private ObHBaseWriteTask writerTask; + private ObHBaseWriteTask.ConcurrentTableWriter writer; + + private long totalCost = 0; + private long putCount = 0; + private boolean isStop; + + private ObHTableInfo obHTableInfo; + private final Configuration versionColumn; + // 失败重试次数 + private final int failTryCount; + + private String parentThreadName; + private Queue> queue; + private Configuration config; + private ServerConnectInfo connInfo; + + private ObHbaseTableHolder tableHolder; + + private final SimpleDateFormat df_second = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + private final SimpleDateFormat df_ms = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS"); + + public PutTask(String parentThreadName, Queue> recordsQueue, Configuration config, ServerConnectInfo connectInfo, ObHTableInfo obHTableInfo, ObHBaseWriteTask writerTask) { + this.parentThreadName = parentThreadName; + this.queue = recordsQueue; + this.config = config; + this.connInfo = connectInfo; + this.obHTableInfo = obHTableInfo; + this.writerTask = writerTask; + this.versionColumn = config.getConfiguration(ConfigKey.VERSION_COLUMN); + this.failTryCount = config.getInt(Config.FAIL_TRY_COUNT, Config.DEFAULT_FAIL_TRY_COUNT); + this.isStop = false; + initTableHolder(); + } + + private void initTableHolder() { + try { + org.apache.hadoop.conf.Configuration c = new org.apache.hadoop.conf.Configuration(); + c.set(HBASE_OCEANBASE_FULL_USER_NAME, writerTask.fullUserName); + c.set(HBASE_OCEANBASE_PASSWORD, this.connInfo.password); + c.set(HBASE_OCEANBASE_DATABASE, writerTask.dbName); + // obkv-table-client is needed the code below + if (writerTask.usdOdpMode) { + c.setBoolean(OHConstants.HBASE_OCEANBASE_ODP_MODE, true); + c.set(OHConstants.HBASE_OCEANBASE_ODP_ADDR, connInfo.host); + c.set(OHConstants.HBASE_OCEANBASE_ODP_PORT, connInfo.port); + LOG.info("sysUser and sysPassword is empty, build HTABLE in odp mode."); + } else { + c.set(HBASE_OCEANBASE_PARAM_URL, writerTask.configUrl); + c.set(HBASE_OCEANBASE_SYS_USER_NAME, this.connInfo.sysUser); + c.set(HBASE_OCEANBASE_SYS_PASSWORD, this.connInfo.sysPass); + LOG.info("sysUser and sysPassword is not empty, build HTABLE in sys mode."); + } + c.set(HBASE_HTABLE_PUT_WRITE_BUFFER_CHECK, config.getString(OBHBASE_HTABLE_PUT_WRITE_BUFFER_CHECK, DEFAULT_HBASE_HTABLE_PUT_WRITE_BUFFER_CHECK)); + c.set(HBASE_HTABLE_CLIENT_WRITE_BUFFER, config.getString(OBHBASE_HTABLE_CLIENT_WRITE_BUFFER, DEFAULT_HBASE_HTABLE_CLIENT_WRITE_BUFFER)); + + c.set(Property.RS_LIST_ACQUIRE_CONNECT_TIMEOUT.getKey(), "500"); + c.set(Property.RS_LIST_ACQUIRE_READ_TIMEOUT.getKey(), "5000"); + c.set(Property.RPC_EXECUTE_TIMEOUT.getKey(), config.getString(TABLE_CLIENT_RPC_EXECUTE_TIMEOUT, DEFAULT_RPC_EXECUTE_TIMEOUT)); + c.set(Property.NETTY_BUFFER_LOW_WATERMARK.getKey(), config.getString(WRITE_BUFFER_LOW_MARK, DEFAULT_NETTY_BUFFER_LOW_WATERMARK)); + c.set(Property.NETTY_BUFFER_HIGH_WATERMARK.getKey(), config.getString(WRITE_BUFFER_HIGH_MARK, DEFAULT_NETTY_BUFFER_HIGH_WATERMARK)); + this.tableHolder = new ObHbaseTableHolder(c, obHTableInfo.getTableName()); + } catch (Exception e) { + LOG.error("init table holder failed, reason: {}", e.getMessage()); + throw new IllegalStateException(e); + } + + } + + private void batchWrite(final List buffer) { + HTableInterface ohTable = null; + Stopwatch stopwatch = Stopwatch.createStarted(); + try { + ohTable = this.tableHolder.getOhTable(); + List puts = buildBatchPutList(buffer); + ohTable.put(puts); + } catch (Exception e) { + if (Objects.isNull(ohTable)) { + LOG.error("build obHTable: {} failed. reason: {}", obHTableInfo.getTableName(), e.getMessage()); + throw DataXException.asDataXException(Hbase094xWriterErrorCode.GET_HBASE_TABLE_ERROR, Hbase094xWriterErrorCode.GET_HBASE_TABLE_ERROR.getDescription()); + } + // + LOG.error("hbase batch error: " + e); + // 出错了之后对该出错的batch逐条重试 + for (Record record : buffer) { + writeOneRecord(ohTable, record); + } + } finally { + this.writer.increFinishCount(); + putCount++; + totalCost += stopwatch.elapsed(TimeUnit.MILLISECONDS); + try { + if (!Objects.isNull(ohTable)) { + ohTable.close(); + } + } catch (Exception e) { + LOG.warn("error in closing htable: {}. Reason: {}", obHTableInfo.getFullHbaseTableName(), e.getMessage()); + } + } + } + + private void writeOneRecord(HTableInterface ohTable, Record record) { + int retryCount = 0; + while (retryCount < this.failTryCount) { + try { + byte[] rowkey = getRowkey(record, obHTableInfo); + Put put = new Put(rowkey); // row key + boolean hasValidValue = buildPut(put, record); + + if (hasValidValue) { + ohTable.put(put); + } + break; + } catch (Exception e) { + retryCount++; + LOG.error("error in writing: " + e.getMessage() + ", retry count: " + retryCount); + if (retryCount == this.failTryCount) { + LOG.warn("ERROR : record {}", record); + this.writerTask.collectDirtyRecord(record, e); + } + } + } + } + + private List buildBatchPutList(List buffer) { + List puts = new ArrayList<>(); + for (Record record : buffer) { + byte[] rowkey = getRowkey(record, obHTableInfo); + Put put = new org.apache.hadoop.hbase.client.Put(rowkey); // row key + boolean hasValidValue = buildPut(put, record); + if (hasValidValue) { + puts.add(put); + } + } + return puts; + } + + private boolean buildPut(Put put, Record record) { + boolean hasValidValue = false; + long timestamp = buildTimestamp(record); + for (Map.Entry> columnInfo : obHTableInfo.getIndexColumnInfoMap().entrySet()) { + Integer index = columnInfo.getKey(); + if (index >= record.getColumnNumber()) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, + MESSAGE_SOURCE.message("normaltask.2", record.getColumnNumber(), index)); + } + ColumnType columnType = columnInfo.getValue().getRight(); + String familyName = columnInfo.getValue().getLeft(); + String columnName = columnInfo.getValue().getMiddle(); + + byte[] value = getColumnByte(columnType, record.getColumn(index), obHTableInfo); + if (value != null) { + hasValidValue = true; + if (timestamp == -1) { + put.add(familyName.getBytes(), // family + columnName.getBytes(), // Q + value); // V + } else { + put.add(familyName.getBytes(), // family + columnName.getBytes(), // Q + timestamp, // timestamp/version + value); // V + } + } + } + + return hasValidValue; + } + + private long buildTimestamp(Record record) { + if (versionColumn == null) { + return -1; + } + + int index = versionColumn.getInt(ConfigKey.INDEX); + long timestamp; + if (index == -1) { + // user specified the constant as timestamp + timestamp = versionColumn.getLong(ConfigKey.VALUE); + if (timestamp < 0) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.CONSTRUCT_VERSION_ERROR, + MESSAGE_SOURCE.message("normaltask.4")); + } + } else { + // 指定列作为版本,long/doubleColumn直接record.aslong, 其它类型尝试用yyyy-MM-dd HH:mm:ss, + // yyyy-MM-dd HH:mm:ss SSS去format + if (index >= record.getColumnNumber()) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.CONSTRUCT_VERSION_ERROR, + MESSAGE_SOURCE.message("normaltask.5", record.getColumnNumber(), index)); + } + + if (record.getColumn(index).getRawData() == null) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.CONSTRUCT_VERSION_ERROR, + MESSAGE_SOURCE.message("normaltask.6")); + } + + if (record.getColumn(index) instanceof LongColumn || record.getColumn(index) instanceof DoubleColumn) { + timestamp = record.getColumn(index).asLong(); + } else { + Date date; + try { + date = df_ms.parse(record.getColumn(index).asString()); + } catch (ParseException e) { + try { + date = df_second.parse(record.getColumn(index).asString()); + } catch (ParseException e1) { + LOG.info(MESSAGE_SOURCE.message("normaltask.7", index)); + throw DataXException.asDataXException(Hbase094xWriterErrorCode.CONSTRUCT_VERSION_ERROR, e1); + } + } + timestamp = date.getTime(); + } + } + + return timestamp; + } + + public void setStop() {isStop = true;} + + public long getTotalCost() {return totalCost;} + + public long getPutCount() {return putCount;} + + public void destroy() { + tableHolder.destroy(); + } + + void setWriterTask(ObHBaseWriteTask writerTask) { + this.writerTask = writerTask; + } + + void setWriter(ObHBaseWriteTask.ConcurrentTableWriter writer) { + this.writer = writer; + } + + @Override + public void run() { + String currentThreadName = String.format("%s-putTask-%d", parentThreadName, Thread.currentThread().getId()); + Thread.currentThread().setName(currentThreadName); + LOG.debug("Task {} start to execute...", currentThreadName); + int sleepTimes = 0; + while (!isStop) { + try { + List records = queue.poll(); + if (null != records) { + batchWrite(records); + } else if (writerTask.isFinished()) { + writerTask.singalTaskFinish(); + LOG.debug("not more task, thread exist ..."); + break; + } else { + TimeUnit.MILLISECONDS.sleep(5); + sleepTimes++; + } + } catch (InterruptedException e) { + LOG.debug("TableWriter is interrupt"); + } catch (Exception e) { + LOG.warn("ERROR UNEXPECTED {}", e); + } + } + LOG.debug("Thread exist..."); + LOG.debug("sleep {} times, total sleep time: {}", sleepTimes, sleepTimes * 5); + } +} diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/LocalStrings.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/LocalStrings.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/LocalStrings_en_US.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/LocalStrings_en_US.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/LocalStrings_ja_JP.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/LocalStrings_zh_CN.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/LocalStrings_zh_HK.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/LocalStrings_zh_TW.properties b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/ObHbaseWriterUtils.java b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/ObHbaseWriterUtils.java new file mode 100644 index 0000000000..9ccd3ed84b --- /dev/null +++ b/obhbasewriter/src/main/java/com/alibaba/datax/plugin/writer/obhbasewriter/util/ObHbaseWriterUtils.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2021 OceanBase ob-loader-dumper is licensed under Mulan PSL v2. You can use this software according to + * the terms and conditions of the Mulan PSL v2. You may obtain a copy of Mulan PSL v2 at: + * + * http://license.coscl.org.cn/MulanPSL2 + * + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING + * BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. See the Mulan PSL v2 for more + * details. + */ +package com.alibaba.datax.plugin.writer.obhbasewriter.util; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.MessageSource; +import com.alibaba.datax.plugin.writer.obhbasewriter.ColumnType; +import com.alibaba.datax.plugin.writer.obhbasewriter.Hbase094xWriterErrorCode; +import com.alibaba.datax.plugin.writer.obhbasewriter.ObHTableInfo; +import com.alibaba.datax.plugin.writer.obhbasewriter.task.PutTask; +import java.nio.charset.Charset; +import org.apache.commons.lang3.tuple.Triple; +import org.apache.hadoop.hbase.HConstants; +import org.apache.hadoop.hbase.util.Bytes; + +/** + * @author cjyyz + * @date 2023/03/23 + * @since + */ +public class ObHbaseWriterUtils { + + private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(PutTask.class); + + public static byte[] getRowkey(Record record, ObHTableInfo obHTableInfo) { + byte[] rowkeyBuffer = {}; + for (Triple rowKeyElement : obHTableInfo.getRowKeyElementList()) { + Integer index = rowKeyElement.getLeft(); + ColumnType columnType = rowKeyElement.getRight(); + if (index == -1) { + String value = rowKeyElement.getMiddle(); + rowkeyBuffer = Bytes.add(rowkeyBuffer, getValueByte(columnType, value, obHTableInfo.getEncoding())); + } else { + if (index >= record.getColumnNumber()) { + throw DataXException.asDataXException(Hbase094xWriterErrorCode.CONSTRUCT_ROWKEY_ERROR, MESSAGE_SOURCE.message("normaltask.3", record.getColumnNumber(), index)); + } + byte[] value = getColumnByte(columnType, record.getColumn(index), obHTableInfo); + rowkeyBuffer = Bytes.add(rowkeyBuffer, value); + } + } + + return rowkeyBuffer; + } + + public static byte[] getColumnByte(ColumnType columnType, Column column, ObHTableInfo obHTableInfo) { + byte[] bytes; + if (column.getRawData() != null && !(columnType == ColumnType.STRING && column.asString().equals("null"))) { + switch (columnType) { + case INT: + bytes = Bytes.toBytes(column.asLong().intValue()); + break; + case LONG: + bytes = Bytes.toBytes(column.asLong()); + break; + case DOUBLE: + bytes = Bytes.toBytes(column.asDouble()); + break; + case FLOAT: + bytes = Bytes.toBytes(column.asDouble().floatValue()); + break; + case SHORT: + bytes = Bytes.toBytes(column.asLong().shortValue()); + break; + case BOOLEAN: + bytes = Bytes.toBytes(column.asBoolean()); + break; + case STRING: + bytes = getValueByte(columnType, column.asString(), obHTableInfo.getEncoding()); + break; + case BINARY: + bytes = Bytes.toBytesBinary(column.asString()); + break; + default: + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, MESSAGE_SOURCE.message("hbaseabstracttask.2", columnType)); + } + } else { + switch (obHTableInfo.getNullModeType()) { + case Skip: + bytes = null; + break; + case Empty: + bytes = HConstants.EMPTY_BYTE_ARRAY; + break; + default: + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, MESSAGE_SOURCE.message("hbaseabstracttask.3")); + } + } + return bytes; + } + + /** + * @param columnType + * @param value + * @return byte[] + */ + private static byte[] getValueByte(ColumnType columnType, String value, String encoding) { + byte[] bytes; + if (value != null) { + switch (columnType) { + case INT: + bytes = Bytes.toBytes(Integer.parseInt(value)); + break; + case LONG: + bytes = Bytes.toBytes(Long.parseLong(value)); + break; + case DOUBLE: + bytes = Bytes.toBytes(Double.parseDouble(value)); + break; + case FLOAT: + bytes = Bytes.toBytes(Float.parseFloat(value)); + break; + case SHORT: + bytes = Bytes.toBytes(Short.parseShort(value)); + break; + case BOOLEAN: + bytes = Bytes.toBytes(Boolean.parseBoolean(value)); + break; + case STRING: + bytes = value.getBytes(Charset.forName(encoding)); + break; + default: + throw DataXException.asDataXException(Hbase094xWriterErrorCode.ILLEGAL_VALUE, MESSAGE_SOURCE.message("hbaseabstracttask.4", columnType)); + } + } else { + bytes = HConstants.EMPTY_BYTE_ARRAY; + } + return bytes; + } +} \ No newline at end of file diff --git a/obhbasewriter/src/main/resources/plugin.json b/obhbasewriter/src/main/resources/plugin.json new file mode 100644 index 0000000000..6ea96196e4 --- /dev/null +++ b/obhbasewriter/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "obhbasewriter", + "class": "com.alibaba.datax.plugin.writer.obhbasewriter.ObHbaseWriter", + "description": "适用于: 生产环境. 原理: TODO", + "developer": "alibaba" +} diff --git a/oceanbasev10reader/doc/oceanbasev10reader.md b/oceanbasev10reader/doc/oceanbasev10reader.md new file mode 100644 index 0000000000..3d1672b5c2 --- /dev/null +++ b/oceanbasev10reader/doc/oceanbasev10reader.md @@ -0,0 +1,244 @@ +## 1 快速介绍 +OceanbaseV10Reader插件实现了从Oceanbase V1.0读取数据。在底层实现上,该读取插件通过java client(jdbc)连接远程Oceanbase 1.0数据库,并执行相应的sql语句将数据从库中SELECT出来。 + +注意,oceanbasev10reader只适用于ob1.0及以后版本的reader。 + +## 2 实现原理 +简而言之,Oceanbasev10Reader通过java client连接器连接到远程的Oceanbase数据库,并根据用户配置的信息生成查询SELECT SQL语句,然后发送到远程Oceanbase v1.0及更高版本数据库,并将该SQL执行返回结果使用DataX自定义的数据类型拼装为抽象的数据集,并传递给下游Writer处理。
对于用户配置Table、Column、Where的信息,OceanbaseV10Reader将其拼接为SQL语句发送到Oceanbase 数据库;对于用户配置querySql信息,Oceanbasev10Reader直接将其发送到Oceanbase数据库。 +## 3 功能说明 +### 3.1 配置样例 + +- 配置一个从Oceanbase数据库同步抽取数据到本地的作业: +``` +{ + "job": { + "setting": { + "speed": { + //设置传输速度,单位为byte/s,DataX运行会尽可能达到该速度但是不超过它. + "byte": 1048576 + } + //出错限制 + "errorLimit": { + //出错的record条数上限,当大于该值即报错。 + "record": 0, + //出错的record百分比上限 1.0表示100%,0.02表示2% + "percentage": 0.02 + } + }, + "content": [ + { + "reader": { + "name": "oceanbasev10reader", + "parameter": { + "where": "", + "timeout": 5, + "readBatchSize": 50000, + "column": [ + "id","name" + ], + "connection": [ + { + "jdbcUrl": ["||_dsc_ob10_dsc_||集群名:租户名||_dsc_ob10_dsc_||jdbc:mysql://obproxyIp:obproxyPort/dbName"], + "table": [ + "table" + ] + } + ] + } + }, + "writer": { + //writer类型 + "name": "streamwriter", + //是否打印内容 + "parameter": { + "print":true, + } + } + } + ] + } +} +``` +``` +{ + "job": { + "setting": { + "speed": { + "channel": 3 + }, + "errorLimit": { + "record": 0 + } + }, + "content": [ + { + "reader": { + "name": "oceanbasev10reader", + "parameter": { + "where": "", + "timeout": 5, + "fetchSize": 500, + "column": [ + "id", + "name" + ], + "splitPk": "pk", + "connection": [ + { + "jdbcUrl": ["||_dsc_ob10_dsc_||集群名:租户名||_dsc_ob10_dsc_||jdbc:mysql://obproxyIp:obproxyPort/dbName"], + "table": [ + "table" + ] + } + ], + "username":"xxx", + "password":"xxx" + } + }, + "writer": { + "name": "streamwriter", + "parameter": { + "print": true + } + } + } + ] + } +} +``` + +- 配置一个自定义SQL的数据库同步任务到本地内容的作业: +``` +{ + "job": { + "setting": { + "channel": 3 + }, + "content": [ + { + "reader": { + "name": "oceanbasev10reader", + "parameter": { + "timeout": 5, + "fetchSize": 500, + "splitPk": "pk", + "connection": [ + { + "jdbcUrl": ["||_dsc_ob10_dsc_||集群名:租户名||_dsc_ob10_dsc_||jdbc:mysql://obproxyIp:obproxyPort/dbName"], + "querySql": [ + "select db_id,on_line_flag from db_info where db_id < 10;" + ] + } + ], + "username":"xxx", + "password":"xxx" + } + }, + "writer": { + "name": "streamwriter", + "parameter": { + "print": false, + "encoding": "UTF-8" + } + } + } + ] + } +} +``` +### 3.2 参数说明 + +- **jdbcUrl** + - 描述:连接ob使用的jdbc url,支持两种格式: + - ||_dsc_ob10_dsc_||集群名:租户名||_dsc_ob10_dsc_||jdbc:mysql://obproxyIp:obproxyPort/db + - 此格式下username仅填写用户名本身,无需三段式写法 + - jdbc:mysql://ip:port/db + - 此格式下username需要三段式写法 + - 必选:是 + - 默认值:无 +- **table** + - 描述:所选取的需要同步的表。使用JSON的数组描述,因此支持多张表同时抽取。当配置为多张表时,用户自己需保证多张表是同一schema结构,OceanbaseReader不予检查表是否同一逻辑表。注意,table必须包含在connection配置单元中。 + - 必选:是 + - 默认值:无 +- **column** + - 描述:所配置的表中需要同步的列名集合,使用JSON的数组描述字段信息。 +- 支持列裁剪,即列可以挑选部分列进行导出。 +``` +支持列换序,即列可以不按照表schema信息进行导出,同时支持通配符*,在使用之前需仔细核对列信息。 +``` + +- 必选:是 +- 默认值:无 +- **where** + - 描述:筛选条件,OceanbaseReader根据指定的column、table、where条件拼接SQL,并根据这个SQL进行数据抽取。在实际业务场景中,往往会选择当天的数据进行同步,可以将where条件指定为gmt_create > $bizdate 。这里gmt_create不可以是索引字段,也不可以是联合索引的第一个字段

where条件可以有效地进行业务增量同步。如果不填写where语句,包括不提供where的key或者value,DataX均视作同步全量数据 + - 必选:否 + - 默认值:无 +- **splitPk** + - 描述:OBReader进行数据抽取时,如果指定splitPk,表示用户希望使用splitPk代表的字段进行数据分片,DataX因此会启动并发任务进行数据同步,这样可以大大提供数据同步的效能。 + - 推荐splitPk用户使用表主键,因为表主键通常情况下比较均匀,因此切分出来的分片也不容易出现数据热点。 + - 目前splitPk仅支持int数据切分,`不支持其他类型`。如果用户指定其他非支持类型将报错。
splitPk如果不填写,将视作用户不对单表进行切分,OBReader使用单通道同步全量数据。 + - 必选:否 + - 默认值:空 +- **querySql** + - 描述:在有些业务场景下,where这一配置项不足以描述所筛选的条件,用户可以通过该配置型来自定义筛选SQL。当用户配置了这一项之后,DataX系统就会忽略table,column这些配置型,直接使用这个配置项的内容对数据进行筛选 +- `当用户配置querySql时,OceanbaseReader直接忽略table、column、where条件的配置`,querySql优先级大于table、column、where选项。 + - 必选:否 + - 默认值:无 +- **timeout** + - 描述:sql执行的超时时间 单位分钟 + - 必选:否 + - 默认值:5 +- **username** + - 描述:访问oceanbase的用户名 + - 必选:是 + - 默认值:无 +- ** password** + - 描述:访问oceanbase的密码 + - 必选:是 + - 默认值:无 +- **readByPartition** + - 描述:对分区表是否按照分区切分任务 + - 必选:否 + - 默认值:fasle +- **readBatchSize** + - 描述:一次读取的行数,如果遇到内存不足的情况,可将该值调小 + - 必选:否 + - 默认值:10000 +### 3.3 类 +### 3.3 类型转换 +下面列出OceanbaseReader针对Oceanbase类型转换列表: + +| DataX 内部类型 | Oceanbase 数据类型 | +| --- | --- | +| Long | int | +| Double | numeric | +| String | varchar | +| Date | timestamp | +| Boolean | bool | + +## 4性能测试 +### 4.1 测试报告 +影响速度的主要原因在于channel数量,channel值受限于分表的数量或者单个表的数据分片数量
单表导出时查看分片数量的办法,idb执行`select/*+query_timeout(150000000)*/ s.tablet_count from __all_table t,__table_stat s where t.table_id = s.table_id and t.table_name = '表名'` + +| 通道数 | DataX速度(Rec/s) | DataX流量(MB/s) | +| --- | --- | --- | +| 1 | 15001 | 4.7 | +| 2 | 28169 | 11.66 | +| 3 | 37076 | 14.77 | +| 4 | 55862 | 17.60 | +| 5 | 70860 | 22.31 | + +## 5常见问题 +### +4.1 oracle模式下报错Invalid fatch size +``` +Caused by: java.sql.SQLSyntaxErrorException: (conn=2498) invalid fetch size. in Oracle mode, extendOracleResultSetClass is ineffective if useOraclePrepareExecute is set to true or usePieceData is set to true + at com.oceanbase.jdbc.internal.util.exceptions.ExceptionFactory.createException(ExceptionFactory.java:110) + at com.oceanbase.jdbc.internal.util.exceptions.ExceptionFactory.create(ExceptionFactory.java:211) + at com.oceanbase.jdbc.OceanBaseStatement.setFetchSize(OceanBaseStatement.java:1599) + at com.alibaba.datax.plugin.reader.oceanbasev10reader.ext.ReaderTask.doRead(ReaderTask.java:270) + ... 5 more +``` +该错误常发生更换了高版本的oceanbase-client.jar驱动,高版本的驱动未来提高效率,增加了oracle预处理语句行为。这个机制和setFetchSize冲突。 +#### 解决方案 +在jdbcUrl中配置extendOracleResultSetClass=true可解决这个冲突。 \ No newline at end of file diff --git a/oceanbasev10reader/pom.xml b/oceanbasev10reader/pom.xml new file mode 100644 index 0000000000..66965320fd --- /dev/null +++ b/oceanbasev10reader/pom.xml @@ -0,0 +1,97 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + oceanbasev10reader + com.alibaba.datax + 0.0.1-SNAPSHOT + jar + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + mysql + mysql-connector-java + 8.0.28 + + + log4j + log4j + 1.2.16 + + + junit + junit + 4.11 + test + + + + + + + src/main/java + + **/*.properties + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + diff --git a/oceanbasev10reader/src/main/assembly/package.xml b/oceanbasev10reader/src/main/assembly/package.xml new file mode 100644 index 0000000000..c1db32a9ab --- /dev/null +++ b/oceanbasev10reader/src/main/assembly/package.xml @@ -0,0 +1,42 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/reader/oceanbasev10reader + + + target/ + + oceanbasev10reader-0.0.1-SNAPSHOT.jar + + plugin/reader/oceanbasev10reader + + + src/main/libs/ + + *.jar + + plugin/reader/oceanbasev10reader/libs + + + + + + false + plugin/reader/oceanbasev10reader/libs + runtime + + + diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/Config.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/Config.java new file mode 100644 index 0000000000..ca803c4978 --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/Config.java @@ -0,0 +1,16 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader; + +public interface Config { + // queryTimeoutSecond + String QUERY_TIMEOUT_SECOND = "memstoreCheckIntervalSecond"; + + int DEFAULT_QUERY_TIMEOUT_SECOND = 60 * 60 * 48;// 2天 + + // readBatchSize + String READ_BATCH_SIZE = "readBatchSize"; + + int DEFAULT_READ_BATCH_SIZE = 100000;// 10万 + + String RETRY_LIMIT = "retryLimit"; + int DEFAULT_RETRY_LIMIT = 10; +} diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/OceanBaseReader.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/OceanBaseReader.java new file mode 100644 index 0000000000..e92e5025e2 --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/OceanBaseReader.java @@ -0,0 +1,147 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader; + +import java.sql.Connection; +import java.util.List; + +import com.alibaba.datax.plugin.reader.oceanbasev10reader.ext.ObReaderKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.spi.Reader; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.Constant; +import com.alibaba.datax.plugin.rdbms.reader.Key; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.ext.ReaderJob; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.ext.ReaderTask; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.util.ObReaderUtils; + +public class OceanBaseReader extends Reader { + + public static class Job extends Reader.Job { + private Configuration originalConfig = null; + private ReaderJob readerJob; + private static final Logger LOG = LoggerFactory.getLogger(Task.class); + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + + Integer userConfigedFetchSize = this.originalConfig.getInt(Constant.FETCH_SIZE); + if (userConfigedFetchSize != null) { + LOG.warn("The [fetchSize] is not recognized, please use readBatchSize instead."); + } + this.originalConfig.set(Constant.FETCH_SIZE, Integer.MIN_VALUE); + setDatabaseType(originalConfig); + this.readerJob = new ReaderJob(); + this.readerJob.init(this.originalConfig); + } + + @Override + public void prepare() { + //ObReaderUtils.DATABASE_TYPE获取当前数据库的语法模式 + } + + @Override + public void preCheck() { + init(); + this.readerJob.preCheck(this.originalConfig, ObReaderUtils.databaseType); + + } + + @Override + public List split(int adviceNumber) { + String splitPk = originalConfig.getString(Key.SPLIT_PK); + List quotedColumns = originalConfig.getList(Key.COLUMN_LIST, String.class); + if (splitPk != null && splitPk.length() > 0 && quotedColumns != null) { + String escapeChar = ObReaderUtils.isOracleMode(originalConfig.getString(ObReaderKey.OB_COMPATIBILITY_MODE)) + ? "\"" : "`"; + if (!splitPk.startsWith(escapeChar) && !splitPk.endsWith(escapeChar)) { + splitPk = escapeChar + splitPk + escapeChar; + } + for (String column : quotedColumns) { + if (column.equals(splitPk)) { + LOG.info("splitPk is an ob reserved keyword, set to {}", splitPk); + originalConfig.set(Key.SPLIT_PK, splitPk); + } + } + } + return this.readerJob.split(this.originalConfig, adviceNumber); + } + + @Override + public void post() { + this.readerJob.post(this.originalConfig); + } + + @Override + public void destroy() { + this.readerJob.destroy(this.originalConfig); + } + + private void setDatabaseType(Configuration config) { + String username = config.getString(Key.USERNAME); + String password = config.getString(Key.PASSWORD); + List conns = originalConfig.getList(Constant.CONN_MARK, Object.class); + Configuration connConf = Configuration.from(conns.get(0).toString()); + List jdbcUrls = connConf.getList(Key.JDBC_URL, String.class); + String jdbcUrl = jdbcUrls.get(0); + if (jdbcUrl.startsWith(com.alibaba.datax.plugin.rdbms.writer.Constant.OB10_SPLIT_STRING)) { + String[] ss = jdbcUrl.split(com.alibaba.datax.plugin.rdbms.writer.Constant.OB10_SPLIT_STRING_PATTERN); + if (ss.length != 3) { + LOG.warn("unrecognized jdbc url: " + jdbcUrl); + return; + } + username = ss[1].trim() + ":" + username; + jdbcUrl = ss[2]; + } + // Use ob-client to get compatible mode. + try { + String obJdbcUrl = jdbcUrl.replace("jdbc:mysql:", "jdbc:oceanbase:"); + Connection conn = DBUtil.getConnection(DataBaseType.OceanBase, obJdbcUrl, username, password); + String compatibleMode = ObReaderUtils.getCompatibleMode(conn); + config.set(ObReaderKey.OB_COMPATIBILITY_MODE, compatibleMode); + if (ObReaderUtils.isOracleMode(compatibleMode)) { + ObReaderUtils.compatibleMode = ObReaderUtils.OB_COMPATIBLE_MODE_ORACLE; + } + + } catch (Exception e) { + LOG.warn("error in get compatible mode, using mysql as default: " + e.getMessage()); + } + } + } + + public static class Task extends Reader.Task { + private Configuration readerSliceConfig; + private ReaderTask commonRdbmsReaderTask; + private static final Logger LOG = LoggerFactory.getLogger(Task.class); + + @Override + public void init() { + this.readerSliceConfig = super.getPluginJobConf(); + this.commonRdbmsReaderTask = new ReaderTask(super.getTaskGroupId(), super.getTaskId()); + this.commonRdbmsReaderTask.init(this.readerSliceConfig); + + } + + @Override + public void startRead(RecordSender recordSender) { + int fetchSize = this.readerSliceConfig.getInt(Constant.FETCH_SIZE); + this.commonRdbmsReaderTask.startRead(this.readerSliceConfig, recordSender, super.getTaskPluginCollector(), + fetchSize); + } + + @Override + public void post() { + this.commonRdbmsReaderTask.post(this.readerSliceConfig); + } + + @Override + public void destroy() { + this.commonRdbmsReaderTask.destroy(this.readerSliceConfig); + } + } + +} diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/ext/Constant.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/ext/Constant.java new file mode 100644 index 0000000000..57977ca4ac --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/ext/Constant.java @@ -0,0 +1,11 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.ext; + +/** + * @author johnrobbet + */ +public class Constant { + + public static String WEAK_READ_QUERY_SQL_TEMPLATE_WITHOUT_WHERE = "select /*+read_consistency(weak)*/ %s from %s "; + + public static String WEAK_READ_QUERY_SQL_TEMPLATE = "select /*+read_consistency(weak)*/ %s from %s where (%s)"; +} diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/ext/ObReaderKey.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/ext/ObReaderKey.java new file mode 100644 index 0000000000..bc8f452560 --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/ext/ObReaderKey.java @@ -0,0 +1,16 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.ext; + +/** + * @author johnrobbet + */ +public class ObReaderKey { + + public final static String READ_BY_PARTITION = "readByPartition"; + + public final static String PARTITION_NAME = "partitionName"; + + public final static String PARTITION_TYPE = "partitionType"; + + public final static String OB_COMPATIBILITY_MODE = "obCompatibilityMode"; + +} diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/ext/ReaderJob.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/ext/ReaderJob.java new file mode 100644 index 0000000000..020709331a --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/ext/ReaderJob.java @@ -0,0 +1,92 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.ext; + +import java.util.List; + +import com.alibaba.datax.common.constant.CommonConstant; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.CommonRdbmsReader; +import com.alibaba.datax.plugin.rdbms.reader.Key; +import com.alibaba.datax.plugin.rdbms.reader.Constant; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.OceanBaseReader; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.util.ObReaderSplitUtil; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.util.ObReaderUtils; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.util.PartitionSplitUtil; +import com.alibaba.fastjson2.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class ReaderJob extends CommonRdbmsReader.Job { + private Logger LOG = LoggerFactory.getLogger(OceanBaseReader.Task.class); + + public ReaderJob() { + super(ObReaderUtils.databaseType); + } + + @Override + public void init(Configuration originalConfig) { + //将config中的column和table中的关键字进行转义 + List columns = originalConfig.getList(Key.COLUMN, String.class); + ObReaderUtils.escapeDatabaseKeyword(columns); + originalConfig.set(Key.COLUMN, columns); + + List conns = originalConfig.getList(Constant.CONN_MARK, JSONObject.class); + for (int i = 0; i < conns.size(); i++) { + JSONObject conn = conns.get(i); + Configuration connConfig = Configuration.from(conn.toString()); + List tables = connConfig.getList(Key.TABLE, String.class); + + // tables will be null when querySql is configured + if (tables != null) { + ObReaderUtils.escapeDatabaseKeyword(tables); + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, i, Key.TABLE), + tables); + } + } + super.init(originalConfig); + } + + @Override + public List split(Configuration originalConfig, int adviceNumber) { + List list; + // readByPartition is lower priority than splitPk. + // and readByPartition only works in table mode. + if (!isSplitPkValid(originalConfig) && + originalConfig.getBool(Constant.IS_TABLE_MODE) && + originalConfig.getBool(ObReaderKey.READ_BY_PARTITION, false)) { + LOG.info("try to split reader job by partition."); + list = PartitionSplitUtil.splitByPartition(originalConfig); + } else { + LOG.info("try to split reader job by splitPk."); + list = ObReaderSplitUtil.doSplit(originalConfig, adviceNumber); + } + + for (Configuration config : list) { + String jdbcUrl = config.getString(Key.JDBC_URL); + String obRegionName = getObRegionName(jdbcUrl); + config.set(CommonConstant.LOAD_BALANCE_RESOURCE_MARK, obRegionName); + } + + return list; + } + + private boolean isSplitPkValid(Configuration originalConfig) { + String splitPk = originalConfig.getString(Key.SPLIT_PK); + return splitPk != null && splitPk.trim().length() > 0; + } + + private String getObRegionName(String jdbcUrl) { + final String obJdbcDelimiter = com.alibaba.datax.plugin.rdbms.writer.Constant.OB10_SPLIT_STRING; + if (jdbcUrl.startsWith(obJdbcDelimiter)) { + String[] ss = jdbcUrl.split(obJdbcDelimiter); + int elementCount = 2; + if (ss.length >= elementCount) { + String tenant = ss[1].trim(); + String[] sss = tenant.split(":"); + return sss[0]; + } + } + + return null; + } +} diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/ext/ReaderTask.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/ext/ReaderTask.java new file mode 100644 index 0000000000..092710f28c --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/ext/ReaderTask.java @@ -0,0 +1,421 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.ext; + +import com.alibaba.datax.common.element.BoolColumn; +import com.alibaba.datax.common.element.BytesColumn; +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.DateColumn; +import com.alibaba.datax.common.element.DoubleColumn; +import com.alibaba.datax.common.element.LongColumn; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.element.StringColumn; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.statistics.PerfRecord; +import com.alibaba.datax.common.statistics.PerfTrace; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.CommonRdbmsReader; +import com.alibaba.datax.plugin.rdbms.reader.Constant; +import com.alibaba.datax.plugin.rdbms.reader.Key; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.RdbmsException; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.Config; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.util.ObReaderUtils; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.util.TaskContext; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.*; +import java.util.ArrayList; +import java.util.List; + +public class ReaderTask extends CommonRdbmsReader.Task { + private static final Logger LOG = LoggerFactory.getLogger(ReaderTask.class); + private int taskGroupId = -1; + private int taskId = -1; + + private String username; + private String password; + private String jdbcUrl; + private String mandatoryEncoding; + private int queryTimeoutSeconds;// 查询超时 默认48小时 + private int readBatchSize; + private int retryLimit = 0; + private String compatibleMode = ObReaderUtils.OB_COMPATIBLE_MODE_MYSQL; + private static final boolean IS_DEBUG = LOG.isDebugEnabled(); + private boolean reuseConn = false; + + public ReaderTask(int taskGroupId, int taskId) { + super(ObReaderUtils.databaseType, taskGroupId, taskId); + this.taskGroupId = taskGroupId; + this.taskId = taskId; + } + + @Override + public void init(Configuration readerSliceConfig) { + /* for database connection */ + username = readerSliceConfig.getString(Key.USERNAME); + password = readerSliceConfig.getString(Key.PASSWORD); + jdbcUrl = readerSliceConfig.getString(Key.JDBC_URL); + queryTimeoutSeconds = readerSliceConfig.getInt(Config.QUERY_TIMEOUT_SECOND, + Config.DEFAULT_QUERY_TIMEOUT_SECOND); + // ob10的处理 + if (jdbcUrl.startsWith(com.alibaba.datax.plugin.rdbms.writer.Constant.OB10_SPLIT_STRING)) { + String[] ss = jdbcUrl.split(com.alibaba.datax.plugin.rdbms.writer.Constant.OB10_SPLIT_STRING_PATTERN); + if (ss.length == 3) { + LOG.info("this is ob1_0 jdbc url."); + username = ss[1].trim() + ":" + username; + jdbcUrl = ss[2]; + } + } + + jdbcUrl = jdbcUrl.replace("jdbc:mysql:", "jdbc:oceanbase:") + "&socketTimeout=1800000&connectTimeout=60000"; //socketTimeout 半个小时 + if (ObReaderUtils.compatibleMode.equals(ObReaderUtils.OB_COMPATIBLE_MODE_ORACLE)) { + compatibleMode = ObReaderUtils.OB_COMPATIBLE_MODE_ORACLE; + } + LOG.info("this is ob1_0 jdbc url. user=" + username + " :url=" + jdbcUrl); + mandatoryEncoding = readerSliceConfig.getString(Key.MANDATORY_ENCODING, ""); + retryLimit = readerSliceConfig.getInt(Config.RETRY_LIMIT, Config.DEFAULT_RETRY_LIMIT); + LOG.info("retryLimit: " + retryLimit); + } + + private void buildSavePoint(TaskContext context) { + if (!ObReaderUtils.isUserSavePointValid(context)) { + LOG.info("user save point is not valid, set to null."); + context.setUserSavePoint(null); + } + } + + /** + * 如果isTableMode && table有PK + *

+ * 则支持断点续读 (若pk不在原始的columns中,则追加到尾部,但不传给下游) + *

+ * 否则,则使用旧模式 + */ + @Override + public void startRead(Configuration readerSliceConfig, RecordSender recordSender, + TaskPluginCollector taskPluginCollector, int fetchSize) { + String querySql = readerSliceConfig.getString(Key.QUERY_SQL); + String table = readerSliceConfig.getString(Key.TABLE); + PerfTrace.getInstance().addTaskDetails(taskId, table + "," + jdbcUrl); + List columns = readerSliceConfig.getList(Key.COLUMN_LIST, String.class); + String where = readerSliceConfig.getString(Key.WHERE); + boolean weakRead = readerSliceConfig.getBool(Key.WEAK_READ, true); // default true, using weak read + String userSavePoint = readerSliceConfig.getString(Key.SAVE_POINT, null); + reuseConn = readerSliceConfig.getBool(Key.REUSE_CONN, false); + String partitionName = readerSliceConfig.getString(Key.PARTITION_NAME, null); + // 从配置文件中取readBatchSize,若无则用默认值 + readBatchSize = readerSliceConfig.getInt(Config.READ_BATCH_SIZE, Config.DEFAULT_READ_BATCH_SIZE); + // 不能少于1万 + if (readBatchSize < 10000) { + readBatchSize = 10000; + } + TaskContext context = new TaskContext(table, columns, where, fetchSize); + context.setQuerySql(querySql); + context.setWeakRead(weakRead); + context.setCompatibleMode(compatibleMode); + if (partitionName != null) { + context.setPartitionName(partitionName); + } + // Add the user save point into the context + context.setUserSavePoint(userSavePoint); + PerfRecord allPerf = new PerfRecord(taskGroupId, taskId, PerfRecord.PHASE.RESULT_NEXT_ALL); + allPerf.start(); + boolean isTableMode = readerSliceConfig.getBool(Constant.IS_TABLE_MODE); + try { + startRead0(isTableMode, context, recordSender, taskPluginCollector); + } finally { + ObReaderUtils.close(null, null, context.getConn()); + } + allPerf.end(context.getCost()); + // 目前大盘是依赖这个打印,而之前这个Finish read record是包含了sql查询和result next的全部时间 + LOG.info("finished read record by Sql: [{}\n] {}.", context.getQuerySql(), jdbcUrl); + } + + private void startRead0(boolean isTableMode, TaskContext context, RecordSender recordSender, + TaskPluginCollector taskPluginCollector) { + // 不是table模式 直接使用原来的做法 + if (!isTableMode) { + doRead(recordSender, taskPluginCollector, context); + return; + } + // check primary key index + Connection conn = DBUtil.getConnection(ObReaderUtils.databaseType, jdbcUrl, username, password); + ObReaderUtils.initConn4Reader(conn, queryTimeoutSeconds); + context.setConn(conn); + try { + ObReaderUtils.initIndex(conn, context); + ObReaderUtils.matchPkIndexs(conn, context); + } catch (Throwable e) { + LOG.warn("fetch PkIndexs fail,table=" + context.getTable(), e); + } + // 如果不是table 且 pk不存在 则仍然使用原来的做法 + if (context.getPkIndexs() == null) { + doRead(recordSender, taskPluginCollector, context); + return; + } + + // setup the user defined save point + buildSavePoint(context); + + // 从这里开始就是 断点续读功能 + // while(true) { + // 正常读 (需 order by pk asc) + // 如果遇到失败,分两种情况: + // a)已读出记录,则开始走增量读逻辑 + // b)未读出记录,则走正常读逻辑(仍然需要order by pk asc) + // 正常结束 则 break + // } + context.setReadBatchSize(readBatchSize); + String getFirstQuerySql = ObReaderUtils.buildFirstQuerySql(context); + String appendQuerySql = ObReaderUtils.buildAppendQuerySql(conn, context); + LOG.warn("start table scan key : {}", context.getIndexName() == null ? "primary" : context.getIndexName()); + context.setQuerySql(getFirstQuerySql); + boolean firstQuery = true; + // 原来打算firstQuery时 limit 1 减少 + // 后来经过对比发现其实是多余的,因为: + // 1.假如走gmt_modified辅助索引,则直接索引扫描 不需要topN的order by + // 2.假如不走辅助索引,而是pk table scan,则减少排序规模并没有好处,因为下一次仍然要排序 + // 减少这个多余的优化tip 可以让代码更易读 + int retryCount = 0; + while (true) { + try { + boolean finish = doRead(recordSender, taskPluginCollector, context); + if (finish) { + break; + } + } catch (Throwable e) { + if (retryLimit == ++retryCount) { + throw RdbmsException.asQueryException(ObReaderUtils.databaseType, new Exception(e), + context.getQuerySql(), context.getTable(), username); + } + LOG.error("read fail, retry count " + retryCount + ", sleep 60 second, save point:" + + context.getSavePoint() + ", error: " + e.getMessage()); + ObReaderUtils.sleep(60000); // sleep 10s + } + // 假如原来的查询有查出数据,则改成增量查询 + if (firstQuery && context.getPkIndexs() != null && context.getSavePoint() != null) { + context.setQuerySql(appendQuerySql); + firstQuery = false; + } + } + DBUtil.closeDBResources(null, context.getConn()); + } + + private boolean isConnectionAlive(Connection conn) { + if (conn == null) { + return false; + } + Statement stmt = null; + ResultSet rs = null; + String sql = "select 1" + (compatibleMode == ObReaderUtils.OB_COMPATIBLE_MODE_ORACLE ? " from dual" : ""); + try { + stmt = conn.createStatement(); + rs = stmt.executeQuery(sql); + rs.next(); + } catch (Exception ex) { + LOG.info("connection is not alive: " + ex.getMessage()); + return false; + } finally { + DBUtil.closeDBResources(rs, stmt, null); + } + return true; + } + + private boolean doRead(RecordSender recordSender, TaskPluginCollector taskPluginCollector, TaskContext context) { + LOG.info("exe sql: {}", context.getQuerySql()); + Connection conn = context.getConn(); + if (reuseConn && isConnectionAlive(conn)) { + LOG.info("connection is alive, will reuse this connection."); + } else { + LOG.info("Create new connection for reader."); + conn = DBUtil.getConnection(ObReaderUtils.databaseType, jdbcUrl, username, password); + ObReaderUtils.initConn4Reader(conn, queryTimeoutSeconds); + context.setConn(conn); + } + PreparedStatement ps = null; + ResultSet rs = null; + PerfRecord perfRecord = new PerfRecord(taskGroupId, taskId, PerfRecord.PHASE.SQL_QUERY); + perfRecord.start(); + try { + ps = conn.prepareStatement(context.getQuerySql(), + ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + if (context.getPkIndexs() != null && context.getSavePoint() != null) { + Record savePoint = context.getSavePoint(); + List point = ObReaderUtils.buildPoint(savePoint, context.getPkIndexs()); + ObReaderUtils.binding(ps, point); + if (LOG.isWarnEnabled()) { + List pointForLog = new ArrayList(); + for (Column c : point) { + pointForLog.add(c.asString()); + } + LOG.warn("{} save point : {}", context.getTable(), StringUtils.join(pointForLog, ',')); + } + } + // 打开流式接口 + ps.setFetchSize(context.getFetchSize()); + rs = ps.executeQuery(); + ResultSetMetaData metaData = rs.getMetaData(); + int columnNumber = metaData.getColumnCount(); + long lastTime = System.nanoTime(); + int count = 0; + for (; rs.next(); count++) { + context.addCost(System.nanoTime() - lastTime); + Record row = buildRecord(recordSender, rs, metaData, columnNumber, mandatoryEncoding, + taskPluginCollector); + // // 如果第一个record重复了,则不需要发送 + // if (count == 0 && + // ObReaderUtils.isPkEquals(context.getSavePoint(), row, + // context.getPkIndexs())) { + // continue; + // } + // 如果是querySql + if (context.getTransferColumnNumber() == -1 + || row.getColumnNumber() == context.getTransferColumnNumber()) { + recordSender.sendToWriter(row); + } else { + Record newRow = recordSender.createRecord(); + for (int i = 0; i < context.getTransferColumnNumber(); i++) { + newRow.addColumn(row.getColumn(i)); + } + recordSender.sendToWriter(newRow); + } + context.setSavePoint(row); + lastTime = System.nanoTime(); + } + LOG.info("end of sql: {}, " + count + "rows are read.", context.getQuerySql()); + return context.getReadBatchSize() <= 0 || count < readBatchSize; + } catch (Exception e) { + ObReaderUtils.close(null, null, context.getConn()); + context.setConn(null); + LOG.error("reader data fail", e); + throw RdbmsException.asQueryException(ObReaderUtils.databaseType, e, context.getQuerySql(), + context.getTable(), username); + } finally { + perfRecord.end(); + if (reuseConn) { + ObReaderUtils.close(rs, ps, null); + } else { + ObReaderUtils.close(rs, ps, conn); + } + } + } + + //重写方法支持array类型 + protected Record buildRecord(RecordSender recordSender,ResultSet rs, ResultSetMetaData metaData, int columnNumber, String mandatoryEncoding, + TaskPluginCollector taskPluginCollector) { + Record record = recordSender.createRecord(); + + try { + for (int i = 1; i <= columnNumber; i++) { + switch (metaData.getColumnType(i)) { + + case Types.CHAR: + case Types.NCHAR: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.NVARCHAR: + case Types.LONGNVARCHAR: + String rawData; + if(StringUtils.isBlank(mandatoryEncoding)){ + rawData = rs.getString(i); + }else{ + rawData = new String((rs.getBytes(i) == null ? EMPTY_CHAR_ARRAY : + rs.getBytes(i)), mandatoryEncoding); + } + record.addColumn(new StringColumn(rawData)); + break; + + case Types.CLOB: + case Types.NCLOB: + record.addColumn(new StringColumn(rs.getString(i))); + break; + + case Types.SMALLINT: + case Types.TINYINT: + case Types.INTEGER: + case Types.BIGINT: + record.addColumn(new LongColumn(rs.getString(i))); + break; + + case Types.NUMERIC: + case Types.DECIMAL: + case Types.FLOAT: + case Types.REAL: + case Types.DOUBLE: + record.addColumn(new DoubleColumn(rs.getString(i))); + break; + + case Types.TIME: + record.addColumn(new DateColumn(rs.getTime(i))); + break; + + // for mysql bug, see http://bugs.mysql.com/bug.php?id=35115 + case Types.DATE: + if (metaData.getColumnTypeName(i).equalsIgnoreCase("year")) { + record.addColumn(new LongColumn(rs.getInt(i))); + } else { + record.addColumn(new DateColumn(rs.getDate(i))); + } + break; + + case Types.TIMESTAMP: + record.addColumn(new DateColumn(rs.getTimestamp(i))); + break; + + case Types.VARBINARY: + case Types.BLOB: + case Types.LONGVARBINARY: + record.addColumn(new BytesColumn(rs.getBytes(i))); + break; + case Types.BINARY: + String isArray = rs.getString(i); + if (isArray.startsWith("[")&& isArray.endsWith("]")){ + record.addColumn(new StringColumn(rs.getString(i))); + }else { + record.addColumn(new BytesColumn(rs.getBytes(i))); + } + break; + // warn: bit(1) -> Types.BIT 可使用BoolColumn + // warn: bit(>1) -> Types.VARBINARY 可使用BytesColumn + case Types.BOOLEAN: + case Types.BIT: + record.addColumn(new BoolColumn(rs.getBoolean(i))); + break; + + case Types.NULL: + String stringData = null; + if(rs.getObject(i) != null) { + stringData = rs.getObject(i).toString(); + } + record.addColumn(new StringColumn(stringData)); + break; + + default: + throw DataXException + .asDataXException( + DBUtilErrorCode.UNSUPPORTED_TYPE, + String.format( + "您的配置文件中的列配置信息有误. 因为DataX 不支持数据库读取这种字段类型. 字段名:[%s], 字段名称:[%s], 字段Java类型:[%s]. 请尝试使用数据库函数将其转换datax支持的类型 或者不同步该字段 .", + metaData.getColumnName(i), + metaData.getColumnType(i), + metaData.getColumnClassName(i))); + } + } + } catch (Exception e) { + if (IS_DEBUG) { + LOG.debug("read data " + record.toString() + + " occur exception:", e); + } + //TODO 这里识别为脏数据靠谱吗? + taskPluginCollector.collectDirtyRecord(record, e); + if (e instanceof DataXException) { + throw (DataXException) e; + } + } + return record; + } +} diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ExecutorTemplate.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ExecutorTemplate.java new file mode 100644 index 0000000000..a027ed7f35 --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ExecutorTemplate.java @@ -0,0 +1,287 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class ExecutorTemplate { + + /** + * The default thread pool size. Set as the number of available processors by default. + */ + public static int DEFAULT_POOL_SIZE = Runtime.getRuntime().availableProcessors(); + + /** + * Indicate whether the executor closes automatically. + */ + private final boolean autoClose; + + /** + * + */ + private final List> futures; + + /** + * + */ + private final ExecutorService internalExecutor; + + private final ExecutorCompletionService completionService; + + /** + * Set pool size for ExecutorTemplate. + */ + public static void setPoolSize(int size) { + DEFAULT_POOL_SIZE = size; + } + + /** + * Default: 1024 AutoClose: true + * + * @param poolName + */ + public ExecutorTemplate(String poolName) { + this(defaultExecutor(poolName), true); + } + + /** + * Default: 1024 AutoClose: true + * + * @param poolName + */ + public ExecutorTemplate(String poolName, int poolSize) { + this(defaultExecutor(poolName, poolSize), true); + } + + public ExecutorTemplate(String poolName, int poolSize, boolean autoClose) { + this(defaultExecutor(poolName, poolSize), autoClose); + } + + /** + * Default: 1024 + * + * @param poolName + * @param autoClose + */ + public ExecutorTemplate(String poolName, boolean autoClose) { + this(defaultExecutor(poolName), autoClose); + } + + /** + * Default: 1024 AutoClose: true + * + * @param executor + */ + public ExecutorTemplate(ExecutorService executor) { + this(executor, true); + } + + /** + * @param executor + */ + public ExecutorTemplate(ExecutorService executor, boolean autoClose) { + this.autoClose = autoClose; + this.internalExecutor = executor; + this.completionService = new ExecutorCompletionService<>(executor); + this.futures = Collections.synchronizedList(new ArrayList<>()); + } + + /** + * @param poolName + * @return ExecutorService + */ + public static ExecutorService defaultExecutor(String poolName) { + return defaultExecutor(100000, poolName, DEFAULT_POOL_SIZE); + } + + /** + * @param poolName + * @param poolSize + * @return ExecutorService + */ + public static ExecutorService defaultExecutor(String poolName, int poolSize) { + return defaultExecutor(100000, poolName, poolSize); + } + + /** + * @param capacity + * @param poolName + * @return ExecutorService + */ + public static ExecutorService defaultExecutor(int capacity, String poolName, int poolSize) { + return new ThreadPoolExecutor(poolSize, poolSize, 30, TimeUnit.SECONDS, /* */ + new ArrayBlockingQueue<>(capacity), new NamedThreadFactory(poolName)); + } + + /** + * Submit a callable task + * + * @param task + */ + public void submit(Callable task) { + Future f = this.completionService.submit(task); + futures.add(f); + check(f); + } + + /** + * Submit a runnable task + * + * @param task + */ + public void submit(Runnable task) { + Future f = this.completionService.submit(task, null); + futures.add(f); + check(f); + } + + /** + * Wait all the task run finished, and get all the results. + * + * @return List + */ + public List waitForResult() { + try { + int index = 0; + Throwable ex = null; + List result = new ArrayList(); + while (index < futures.size()) { + try { + Future f = this.completionService.take(); + result.add(f.get()); + } catch (Throwable e) { + ex = getRootCause(e); + break; + } + index++; + } + if (ex != null) { + cancelAll(); + throw new RuntimeException(ex); + } else { + return result; + } + } finally { + clearFutures(); + if (autoClose) { + destroyExecutor(); + } + } + } + + /** + * + */ + public void cancelAll() { + for (Future f : futures) { + if (!f.isDone() && !f.isCancelled()) { + f.cancel(false); + } + } + } + + /** + * + */ + public void clearFutures() { + this.futures.clear(); + } + + /** + * + */ + public void destroyExecutor() { + if (internalExecutor != null && !internalExecutor.isShutdown()) { + this.internalExecutor.shutdown(); + try { + this.internalExecutor.awaitTermination(0, TimeUnit.SECONDS); + } catch (InterruptedException e) { + } + } + } + + /** + * Fast check the future + * + * @param f + */ + private void check(Future f) { + if (f != null && f.isDone()) { + try { + f.get(); + } catch (Throwable e) { + cancelAll(); + throw new RuntimeException(e); + } + } + } + + /** + * @param throwable + * @return Throwable + */ + private Throwable getRootCause(Throwable throwable) { + final Throwable holder = throwable; + final List list = new ArrayList<>(); + while (throwable != null && !list.contains(throwable)) { + list.add(throwable); + throwable = throwable.getCause(); + } + return list.size() < 2 ? holder : list.get(list.size() - 1); + } + + /** + * An internal named thread factory + */ + static class NamedThreadFactory implements ThreadFactory { + + /** + * + */ + private final boolean daemon; + + /** + * + */ + private final String name; + + /** + * + */ + private final AtomicInteger seq = new AtomicInteger(0); + + /** + * @param name + */ + public NamedThreadFactory(String name) { + this(name, false); + } + + /** + * @param name + * @param daemon + */ + public NamedThreadFactory(String name, boolean daemon) { + this.name = name; + this.daemon = daemon; + } + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setDaemon(daemon); + t.setPriority(Thread.NORM_PRIORITY); + t.setName((name + seq.incrementAndGet())); + return t; + } + } +} \ No newline at end of file diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ObReaderSplitUtil.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ObReaderSplitUtil.java new file mode 100644 index 0000000000..8721fa0dcf --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ObReaderSplitUtil.java @@ -0,0 +1,92 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.util; + +import com.alibaba.datax.common.constant.CommonConstant; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.Constant; +import com.alibaba.datax.plugin.rdbms.reader.Key; +import com.alibaba.datax.plugin.rdbms.reader.util.HintUtil; +import com.alibaba.datax.plugin.rdbms.reader.util.SingleTableSplitUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +/** + * OceanBase Reader 专用的 Job 切分逻辑,在 splitPk 场景下使用 {@link ObSingleTableSplitUtil}。 + */ +public final class ObReaderSplitUtil { + private static final Logger LOG = LoggerFactory.getLogger(ObReaderSplitUtil.class); + + private ObReaderSplitUtil() { + } + + public static List doSplit(Configuration originalSliceConfig, int adviceNumber) { + boolean isTableMode = originalSliceConfig.getBool(Constant.IS_TABLE_MODE).booleanValue(); + int eachTableShouldSplittedNumber = -1; + if (isTableMode) { + eachTableShouldSplittedNumber = calculateEachTableShouldSplittedNumber( + adviceNumber, originalSliceConfig.getInt(Constant.TABLE_NUMBER_MARK)); + } + + String column = originalSliceConfig.getString(Key.COLUMN); + String where = originalSliceConfig.getString(Key.WHERE, null); + List conns = originalSliceConfig.getList(Constant.CONN_MARK, Object.class); + List splittedConfigs = new ArrayList(); + + for (int i = 0, len = conns.size(); i < len; i++) { + Configuration sliceConfig = originalSliceConfig.clone(); + Configuration connConf = Configuration.from(conns.get(i).toString()); + String jdbcUrl = connConf.getString(Key.JDBC_URL); + sliceConfig.set(Key.JDBC_URL, jdbcUrl); + sliceConfig.set(CommonConstant.LOAD_BALANCE_RESOURCE_MARK, DataBaseType.parseIpFromJdbcUrl(jdbcUrl)); + sliceConfig.remove(Constant.CONN_MARK); + + if (isTableMode) { + List tables = connConf.getList(Key.TABLE, String.class); + Validate.isTrue(null != tables && !tables.isEmpty(), "您读取数据库表配置错误."); + + String splitPk = originalSliceConfig.getString(Key.SPLIT_PK, null); + boolean needSplitTable = eachTableShouldSplittedNumber > 1 + && StringUtils.isNotBlank(splitPk); + if (needSplitTable) { + if (tables.size() == 1) { + Integer splitFactor = originalSliceConfig.getInt(Key.SPLIT_FACTOR, Constant.SPLIT_FACTOR); + eachTableShouldSplittedNumber = eachTableShouldSplittedNumber * splitFactor; + } + for (String table : tables) { + Configuration tempSlice = sliceConfig.clone(); + tempSlice.set(Key.TABLE, table); + splittedConfigs.addAll( + ObSingleTableSplitUtil.splitSingleTable(tempSlice, eachTableShouldSplittedNumber)); + } + } else { + for (String table : tables) { + Configuration tempSlice = sliceConfig.clone(); + tempSlice.set(Key.TABLE, table); + String queryColumn = HintUtil.buildQueryColumn(jdbcUrl, table, column); + tempSlice.set(Key.QUERY_SQL, + SingleTableSplitUtil.buildQuerySql(queryColumn, table, where)); + splittedConfigs.add(tempSlice); + } + } + } else { + List sqls = connConf.getList(Key.QUERY_SQL, String.class); + for (String querySql : sqls) { + Configuration tempSlice = sliceConfig.clone(); + tempSlice.set(Key.QUERY_SQL, querySql); + splittedConfigs.add(tempSlice); + } + } + } + + return splittedConfigs; + } + + private static int calculateEachTableShouldSplittedNumber(int adviceNumber, int tableNumber) { + return (int) Math.ceil(1.0 * adviceNumber / tableNumber); + } +} diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ObReaderUtils.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ObReaderUtils.java new file mode 100644 index 0000000000..9aa06644f3 --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ObReaderUtils.java @@ -0,0 +1,922 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.util; + +import com.alibaba.datax.common.element.*; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.plugin.rdbms.reader.util.ObVersion; +import com.alibaba.datax.plugin.rdbms.reader.util.SingleTableSplitUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.ext.Constant; +import com.alibaba.druid.sql.SQLUtils; +import com.alibaba.druid.sql.ast.SQLExpr; +import com.alibaba.druid.sql.ast.expr.SQLBinaryOpExpr; +import com.alibaba.druid.sql.ast.expr.SQLBinaryOperator; + +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.*; +import java.util.*; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author johnrobbet + */ +public class ObReaderUtils { + private static final Logger LOG = LoggerFactory.getLogger(ObReaderUtils.class); + private static final String MYSQL_KEYWORDS + = "ACCESSIBLE,ACCOUNT,ACTION,ADD,AFTER,AGAINST,AGGREGATE,ALGORITHM,ALL,ALTER,ALWAYS,ANALYSE,AND,ANY,AS,ASC,ASCII,ASENSITIVE,AT,AUTO_INCREMENT,AUTOEXTEND_SIZE,AVG,AVG_ROW_LENGTH,BACKUP,BEFORE,BEGIN,BETWEEN,BIGINT,BINARY,BINLOG,BIT,BLOB,BLOCK,BOOL,BOOLEAN,BOTH,BTREE,BY,BYTE,CACHE,CALL,CASCADE,CASCADED,CASE,CATALOG_NAME,CHAIN,CHANGE,CHANGED,CHANNEL,CHAR,CHARACTER,CHARSET,CHECK,CHECKSUM,CIPHER,CLASS_ORIGIN,CLIENT,CLOSE,COALESCE,CODE,COLLATE,COLLATION,COLUMN,COLUMN_FORMAT,COLUMN_NAME,COLUMNS,COMMENT,COMMIT,COMMITTED,COMPACT,COMPLETION,COMPRESSED,COMPRESSION,CONCURRENT,CONDITION,CONNECTION,CONSISTENT,CONSTRAINT,CONSTRAINT_CATALOG,CONSTRAINT_NAME,CONSTRAINT_SCHEMA,CONTAINS,CONTEXT,CONTINUE,CONVERT,CPU,CREATE,CROSS,CUBE,CURRENT,CURRENT_DATE,CURRENT_TIME,CURRENT_TIMESTAMP,CURRENT_USER,CURSOR,CURSOR_NAME,DATA,DATABASE,DATABASES,DATAFILE,DATE,DATETIME,DAY,DAY_HOUR,DAY_MICROSECOND,DAY_MINUTE,DAY_SECOND,DEALLOCATE,DEC,DECIMAL,DECLARE,DEFAULT,DEFAULT_AUTH,DEFINER,DELAY_KEY_WRITE," + + "DELAYED,DELETE,DES_KEY_FILE,DESC,DESCRIBE,DETERMINISTIC,DIAGNOSTICS,DIRECTORY,DISABLE,DISCARD,DISK,DISTINCT,DISTINCTROW,DIV,DO,DOUBLE,DROP,DUAL,DUMPFILE,DUPLICATE,DYNAMIC,EACH,ELSE,ELSEIF,ENABLE,ENCLOSED,ENCRYPTION,END,ENDS,ENGINE,ENGINES,ENUM,ERROR,ERRORS,ESCAPE,ESCAPED,EVENT,EVENTS,EVERY,EXCHANGE,EXECUTE,EXISTS,EXIT,EXPANSION,EXPIRE,EXPLAIN,EXPORT,EXTENDED,EXTENT_SIZE,FAST,FAULTS,FETCH,FIELDS,FILE,FILE_BLOCK_SIZE,FILTER,FIRST,FIXED,FLOAT,FLOAT4,FLOAT8,FLUSH,FOLLOWS,FOR,FORCE,FOREIGN,FORMAT,FOUND,FROM,FULL,FULLTEXT,FUNCTION,GENERAL,GENERATED,GEOMETRY,GEOMETRYCOLLECTION,GET,GET_FORMAT,GLOBAL,GRANT,GRANTS,GROUP,GROUP_REPLICATION,HANDLER,HASH,HAVING,HELP,HIGH_PRIORITY,HOST,HOSTS,HOUR,HOUR_MICROSECOND,HOUR_MINUTE,HOUR_SECOND,IDENTIFIED,IF,IGNORE,IGNORE_SERVER_IDS,IMPORT,IN,INDEX,INDEXES,INFILE,INITIAL_SIZE,INNER,INOUT,INSENSITIVE,INSERT,INSERT_METHOD,INSTALL,INSTANCE,INT,INT1,INT2,INT3,INT4,INT8,INTEGER,INTERVAL,INTO,INVOKER,IO,IO_AFTER_GTIDS,IO_BEFORE_GTIDS,IO_THREAD," + + "IPC,IS,ISOLATION,ISSUER,ITERATE,JOIN,JSON,KEY,KEY_BLOCK_SIZE,KEYS,KILL,LANGUAGE,LAST,LEADING,LEAVE,LEAVES,LEFT,LESS,LEVEL,LIKE,LIMIT,LINEAR,LINES,LINESTRING,LIST,LOAD,LOCAL,LOCALTIME,LOCALTIMESTAMP,LOCK,LOCKS,LOGFILE,LOGS,LONG,LONGBLOB,LONGTEXT,LOOP,LOW_PRIORITY,MASTER,MASTER_AUTO_POSITION,MASTER_BIND,MASTER_CONNECT_RETRY,MASTER_DELAY,MASTER_HEARTBEAT_PERIOD,MASTER_HOST,MASTER_LOG_FILE,MASTER_LOG_POS,MASTER_PASSWORD,MASTER_PORT,MASTER_RETRY_COUNT,MASTER_SERVER_ID,MASTER_SSL,MASTER_SSL_CA,MASTER_SSL_CAPATH,MASTER_SSL_CERT,MASTER_SSL_CIPHER,MASTER_SSL_CRL,MASTER_SSL_CRLPATH,MASTER_SSL_KEY,MASTER_SSL_VERIFY_SERVER_CERT,MASTER_TLS_VERSION,MASTER_USER,MATCH,MAX_CONNECTIONS_PER_HOUR,MAX_QUERIES_PER_HOUR,MAX_ROWS,MAX_SIZE,MAX_STATEMENT_TIME,MAX_UPDATES_PER_HOUR,MAX_USER_CONNECTIONS,MAXVALUE,MEDIUM,MEDIUMBLOB,MEDIUMINT,MEDIUMTEXT,MEMORY,MERGE,MESSAGE_TEXT,MICROSECOND,MIDDLEINT,MIGRATE,MIN_ROWS,MINUTE,MINUTE_MICROSECOND,MINUTE_SECOND,MOD,MODE,MODIFIES,MODIFY,MONTH," + + "MULTILINESTRING,MULTIPOINT,MULTIPOLYGON,MUTEX,MYSQL_ERRNO,NAME,NAMES,NATIONAL,NATURAL,NCHAR,NDB,NDBCLUSTER,NEVER,NEW,NEXT,NO,NO_WAIT,NO_WRITE_TO_BINLOG,NODEGROUP,NONBLOCKING,NONE,NOT,NULL,NUMBER,NUMERIC,NVARCHAR,OFFSET,OLD_PASSWORD,ON,ONE,ONLY,OPEN,OPTIMIZE,OPTIMIZER_COSTS,OPTION,OPTIONALLY,OPTIONS,OR,ORDER,OUT,OUTER,OUTFILE,OWNER,PACK_KEYS,PAGE,PARSE_GCOL_EXPR,PARSER,PARTIAL,PARTITION,PARTITIONING,PARTITIONS,PASSWORD,PHASE,PLUGIN,PLUGIN_DIR,PLUGINS,POINT,POLYGON,PORT,PRECEDES,PRECISION,PREPARE,PRESERVE,PREV,PRIMARY,PRIVILEGES,PROCEDURE,PROCESSLIST,PROFILE,PROFILES,PROXY,PURGE,QUARTER,QUERY,QUICK,RANGE,READ,READ_ONLY,READ_WRITE,READS,REAL,REBUILD,RECOVER,REDO_BUFFER_SIZE,REDOFILE,REDUNDANT,REFERENCES,REGEXP,RELAY,RELAY_LOG_FILE,RELAY_LOG_POS,RELAY_THREAD,RELAYLOG,RELEASE,RELOAD,REMOVE,RENAME,REORGANIZE,REPAIR,REPEAT,REPEATABLE,REPLACE,REPLICATE_DO_DB,REPLICATE_DO_TABLE,REPLICATE_IGNORE_DB,REPLICATE_IGNORE_TABLE,REPLICATE_REWRITE_DB,REPLICATE_WILD_DO_TABLE," + + "REPLICATE_WILD_IGNORE_TABLE,REPLICATION,REQUIRE,RESET,RESIGNAL,RESTORE,RESTRICT,RESUME,RETURN,RETURNED_SQLSTATE,RETURNS,REVERSE,REVOKE,RIGHT,RLIKE,ROLLBACK,ROLLUP,ROTATE,ROUTINE,ROW,ROW_COUNT,ROW_FORMAT,ROWS,RTREE,SAVEPOINT,SCHEDULE,SCHEMA,SCHEMA_NAME,SCHEMAS,SECOND,SECOND_MICROSECOND,SECURITY,SELECT,SENSITIVE,SEPARATOR,SERIAL,SERIALIZABLE,SERVER,SESSION,SET,SHARE,SHOW,SHUTDOWN,SIGNAL,SIGNED,SIMPLE,SLAVE,SLOW,SMALLINT,SNAPSHOT,SOCKET,SOME,SONAME,SOUNDS,SOURCE,SPATIAL,SPECIFIC,SQL,SQL_AFTER_GTIDS,SQL_AFTER_MTS_GAPS,SQL_BEFORE_GTIDS,SQL_BIG_RESULT,SQL_BUFFER_RESULT,SQL_CACHE,SQL_CALC_FOUND_ROWS,SQL_NO_CACHE,SQL_SMALL_RESULT,SQL_THREAD,SQL_TSI_DAY,SQL_TSI_HOUR,SQL_TSI_MINUTE,SQL_TSI_MONTH,SQL_TSI_QUARTER,SQL_TSI_SECOND,SQL_TSI_WEEK,SQL_TSI_YEAR,SQLEXCEPTION,SQLSTATE,SQLWARNING,SSL,STACKED,START,STARTING,STARTS,STATS_AUTO_RECALC,STATS_PERSISTENT,STATS_SAMPLE_PAGES,STATUS,STOP,STORAGE,STORED,STRAIGHT_JOIN,STRING,SUBCLASS_ORIGIN,SUBJECT,SUBPARTITION,SUBPARTITIONS,SUPER," + + "SUSPEND,SWAPS,SWITCHES,TABLE,TABLE_CHECKSUM,TABLE_NAME,TABLES,TABLESPACE,TEMPORARY,TEMPTABLE,TERMINATED,TEXT,THAN,THEN,TIME,TIMESTAMP,TIMESTAMPADD,TIMESTAMPDIFF,TINYBLOB,TINYINT,TINYTEXT,TO,TRAILING,TRANSACTION,TRIGGER,TRIGGERS,TRUNCATE,TYPE,TYPES,UNCOMMITTED,UNDEFINED,UNDO,UNDO_BUFFER_SIZE,UNDOFILE,UNICODE,UNINSTALL,UNION,UNIQUE,UNKNOWN,UNLOCK,UNSIGNED,UNTIL,UPDATE,UPGRADE,USAGE,USE,USE_FRM,USER,USER_RESOURCES,USING,UTC_DATE,UTC_TIME,UTC_TIMESTAMP,VALIDATION,VALUE,VALUES,VARBINARY,VARCHAR,VARCHARACTER,VARIABLES,VARYING,VIEW,VIRTUAL,WAIT,WARNINGS,WEEK,WEIGHT_STRING,WHEN,WHERE,WHILE,WITH,WITHOUT,WORK,WRAPPER,WRITE,X509,XA,XID,XML,XOR,YEAR,YEAR_MONTH,ZEROFILL,FALSE,TRUE"; + private static final String ORACLE_KEYWORDS = "ACCESS,ADD,ALL,ALTER,AND,ANY,ARRAYLEN,AS,ASC,AUDIT,BETWEEN,BY,CHAR,CHECK,CLUSTER,COLUMN,COMMENT,COMPRESS,CONNECT,CREATE,CURRENT,DATE,DECIMAL,DEFAULT,DELETE,DESC,DISTINCT,DROP,ELSE,EXCLUSIVE,EXISTS,FILE,FLOAT,FOR,FROM,GRANT,GROUP,HAVING,IDENTIFIED,IMMEDIATE,IN,INCREMENT,INDEX,INITIAL,INSERT,INTEGER,INTERSECT,INTO,IS,LEVEL,LIKE,LOCK,LONG,MAXEXTENTS,MINUS,MODE,MODIFY,NOAUDIT,NOCOMPRESS,NOT,NOTFOUND,NOWAIT,NUMBER,OF,OFFLINE,ON,ONLINE,OPTION,OR,ORDER,PCTFREE,PRIOR,PRIVILEGES,PUBLIC,RAW,RENAME,RESOURCE,REVOKE,ROW,ROWID,ROWLABEL,ROWNUM,ROWS,SELECT,SESSION,SET,SHARE,SIZE,SMALLINT,SQLBUF,START,SUCCESSFUL,SYNONYM,TABLE,THEN,TO,TRIGGER,UID,UNION,UNIQUE,UPDATE,USER,VALIDATE,VALUES,VARCHAR,VARCHAR2,VIEW,WHENEVER,WHERE,WITH,KEY,NAME,VALUE,TYPE"; + + private static Set databaseKeywords; + final static public String OB_COMPATIBLE_MODE = "obCompatibilityMode"; + final static public String OB_COMPATIBLE_MODE_ORACLE = "ORACLE"; + final static public String OB_COMPATIBLE_MODE_MYSQL = "MYSQL"; + + public static String compatibleMode = OB_COMPATIBLE_MODE_MYSQL; + + public static final DataBaseType databaseType = DataBaseType.OceanBase; + + private static final String TABLE_SCHEMA_DELIMITER = "."; + + private static final Pattern JDBC_PATTERN = Pattern.compile("jdbc:(oceanbase|mysql)://([\\w\\.-]+:\\d+)/([\\w\\.-]+)"); + + private static Set keywordsFromString2HashSet(final String keywords) { + return new HashSet(Arrays.asList(keywords.split(","))); + } + + public static String escapeDatabaseKeyword(String keyword) { + if (databaseKeywords == null) { + if (isOracleMode(compatibleMode)) { + databaseKeywords = keywordsFromString2HashSet(ORACLE_KEYWORDS); + } else { + databaseKeywords = keywordsFromString2HashSet(MYSQL_KEYWORDS); + } + } + char escapeChar = isOracleMode(compatibleMode) ? '"' : '`'; + if (databaseKeywords.contains(keyword.toUpperCase())) { + keyword = escapeChar + keyword + escapeChar; + } + return keyword; + } + + public static void escapeDatabaseKeyword(List ids) { + if (ids != null && ids.size() > 0) { + for (int i = 0; i < ids.size(); i++) { + ids.set(i, escapeDatabaseKeyword(ids.get(i))); + } + } + } + + public static Boolean isEscapeMode(String keyword) { + if (isOracleMode(compatibleMode)) { + return keyword.startsWith("\"") && keyword.endsWith("\""); + } else { + return keyword.startsWith("`") && keyword.endsWith("`"); + } + } + + public static void initConn4Reader(Connection conn, long queryTimeoutSeconds) { + String setQueryTimeout = "set ob_query_timeout=" + (queryTimeoutSeconds * 1000 * 1000L); + String setTrxTimeout = "set ob_trx_timeout=" + ((queryTimeoutSeconds + 5) * 1000 * 1000L); + Statement stmt = null; + try { + conn.setAutoCommit(true); + stmt = conn.createStatement(); + stmt.execute(setQueryTimeout); + stmt.execute(setTrxTimeout); + LOG.warn("setAutoCommit=true;" + setQueryTimeout + ";" + setTrxTimeout + ";"); + } catch (Throwable e) { + LOG.warn("initConn4Reader fail", e); + } finally { + DBUtil.closeDBResources(stmt, null); + } + } + + public static void sleep(int ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + } + } + + /** + * @param conn + * @param context + */ + public static void matchPkIndexs(Connection conn, TaskContext context) { + String[] pkColumns = getPkColumns(conn, context); + if (ArrayUtils.isEmpty(pkColumns)) { + LOG.warn("table=" + context.getTable() + " has no primary key"); + return; + } + List columns = context.getColumns(); + + // 最后参与排序的索引列 + + context.setPkColumns(pkColumns); + + final String escapeChar = isOracleMode(context.getCompatibleMode()) ? "\"" : "`"; + int[] pkIndexs = new int[pkColumns.length]; + for (int i = 0, n = pkColumns.length; i < n; i++) { + String pkc = pkColumns[i]; + String escapedPkc = String.format("%s%s%s", escapeChar, pkc, escapeChar); + int j = 0; + for (int k = columns.size(); j < k; j++) { + // 如果用户定义的 columns中 带有 ``,也不影响, + // 最多只是在select里多加了几列PK column + if (StringUtils.equalsIgnoreCase(pkc, columns.get(j)) + || StringUtils.equalsIgnoreCase(escapedPkc, columns.get(j))) { + pkIndexs[i] = j; + pkColumns[i] = columns.get(j); + break; + } + } + // 到这里 说明主键列不在columns中,则主动追加到尾部 + if (j == columns.size()) { + columns.add(pkc); + pkIndexs[i] = columns.size() - 1; + } + } + context.setPkIndexs(pkIndexs); + } + + private static String[] getPkColumns(Connection conn, TaskContext context) { + String tableName = context.getTable(); + String sql = "show index from " + tableName + " where Key_name='PRIMARY'"; + if (isOracleMode(context.getCompatibleMode())) { + tableName = tableName.toUpperCase(); + String schema; + if (tableName.contains(TABLE_SCHEMA_DELIMITER)) { + schema = String.format("'%s'", tableName.substring(0, tableName.indexOf("."))); + tableName = tableName.substring(tableName.indexOf(".") + 1); + } else { + schema = "(select sys_context('USERENV','current_schema') from dual)"; + } + //OceanBase oracle模式下需要使用position排序获取正确的联合主键顺序 + sql = String.format( + "SELECT cols.column_name Column_name " + + "FROM all_constraints cons, all_cons_columns cols " + + "WHERE cols.table_name = '%s' AND cons.constraint_type = 'P' " + + "AND cons.constraint_name = cols.constraint_name " + + "AND cons.owner = cols.owner and cons.OWNER = %s " + + "order by cols.position ", + tableName, schema); + } + LOG.info("get primary key by sql: " + sql); + Statement ps = null; + ResultSet rs = null; + List realIndex = new ArrayList(); + realIndex.addAll(context.getSecondaryIndexColumns()); + try { + ps = conn.createStatement(); + rs = ps.executeQuery(sql); + boolean hasPk = false; + while (rs.next()) { + hasPk = true; + String columnName = rs.getString("Column_name"); + columnName = escapeDatabaseKeyword(columnName); + if (!realIndex.contains(columnName)) { + realIndex.add(columnName); + } + } + + if (hasPk) { + String[] pks = new String[realIndex.size()]; + realIndex.toArray(pks); + return pks; + } + } catch (Throwable e) { + LOG.error("show index from table fail :" + sql, e); + } finally { + close(rs, ps, null); + } + + return null; + } + + /** + * 首次查的SQL + * + * @param context + * @return + */ + public static String buildFirstQuerySql(TaskContext context) { + String userSavePoint = context.getUserSavePoint(); + String indexName = context.getIndexName(); + String sql = "select "; + boolean weakRead = context.getWeakRead(); + if (StringUtils.isNotEmpty(indexName)) { + String weakReadHint = weakRead ? "+READ_CONSISTENCY(WEAK)," : "+"; + sql += " /*" + weakReadHint + "index(" + context.getTable() + " " + indexName + ")*/ "; + } else if (weakRead) { + sql += " /*+READ_CONSISTENCY(WEAK)*/ "; + } + sql += StringUtils.join(context.getColumns(), ','); + sql += " from " + context.getTable(); + if (context.getPartitionName() != null) { + sql += String.format(" partition(%s) ", context.getPartitionName()); + } + if (StringUtils.isNotEmpty(context.getWhere())) { + sql += " where " + context.getWhere(); + } + + if (userSavePoint != null && userSavePoint.length() != 0) { + userSavePoint = userSavePoint.replace("=", ">"); + sql += (StringUtils.isNotEmpty(context.getWhere()) ? " and " : " where ") + userSavePoint; + } + + if (context.getPkColumns() != null && context.getPkColumns().length > 0) { + // 有主键 + sql += " order by " + StringUtils.join(context.getPkColumns(), ',') + " asc"; + } + + return sql; + } + + /** + * 增量查的SQL + * + * @param conn + * @param context + * @return sql + */ + public static String buildAppendQuerySql(Connection conn, TaskContext context) { + String indexName = context.getIndexName(); + boolean weakRead = context.getWeakRead(); + String sql = "select "; + if (StringUtils.isNotEmpty(indexName)) { + String weakReadHint = weakRead ? "+READ_CONSISTENCY(WEAK)," : "+"; + sql += " /*" + weakReadHint + "index(" + context.getTable() + " " + indexName + ")*/ "; + } else if (weakRead) { + sql += " /*+READ_CONSISTENCY(WEAK)*/ "; + } + sql += StringUtils.join(context.getColumns(), ',') + " from " + context.getTable(); + + if (context.getPartitionName() != null) { + sql += String.format(" partition(%s) ", context.getPartitionName()); + } + + String[] pkColumns = context.getPkColumns(); + StringBuilder whereClause = new StringBuilder(); + + if (pkColumns != null && pkColumns.length > 0) { + whereClause.append(" ("); + for (int i = 0; i < pkColumns.length; i++) { + if (i == 0) { + whereClause.append(pkColumns[i]).append(" > ?"); + } else { + whereClause.append(" OR ("); + for (int j = 0; j <= i; j++) { + if (j > 0) { + whereClause.append(" AND "); + } + if (j == i) { + whereClause.append(pkColumns[j]).append(" > ? "); + } else { + whereClause.append(pkColumns[j]).append(" = ? "); + } + } + whereClause.append(")"); + } + } + whereClause.append(")"); + + // 如果有额外的 WHERE 条件,则拼接进去 + if (StringUtils.isNotEmpty(context.getWhere())) { + whereClause.insert(0, "(" + context.getWhere() + ") AND "); + } + sql += " where " + whereClause; + // 添加 ORDER BY 子句 + sql += " order by " + StringUtils.join(pkColumns, ",") + " asc"; + } else { + // 无主键 + if (StringUtils.isNotEmpty(context.getWhere())) { + sql += " where " + context.getWhere(); + } + } + + return sql; + } + + /** + * check if the userSavePoint is valid + * + * @param context + * @return true - valid, false - invalid + */ + public static boolean isUserSavePointValid(TaskContext context) { + String userSavePoint = context.getUserSavePoint(); + if (userSavePoint == null || userSavePoint.length() == 0) { + LOG.info("user save point is empty!"); + return false; + } + + LOG.info("validating user save point: " + userSavePoint); + + final String patternString = "(.+)=(.+)"; + Pattern parttern = Pattern.compile(patternString); + Matcher matcher = parttern.matcher(userSavePoint); + if (!matcher.find()) { + LOG.error("user save point format is not correct: " + userSavePoint); + return false; + } + + List columnsInUserSavePoint = getColumnsFromUserSavePoint(userSavePoint); + List valuesInUserSavePoint = getValuesFromUserSavePoint(userSavePoint); + if (columnsInUserSavePoint.size() == 0 || valuesInUserSavePoint.size() == 0 || + columnsInUserSavePoint.size() != valuesInUserSavePoint.size()) { + LOG.error("number of columns and values in user save point are different:" + userSavePoint); + return false; + } + + String where = context.getWhere(); + if (StringUtils.isNotEmpty(where)) { + for (String column : columnsInUserSavePoint) { + if (where.contains(column)) { + LOG.error("column " + column + " is conflict with where: " + where); + return false; + } + } + } + + // Columns in userSavePoint must be the selected index. + String[] pkColumns = context.getPkColumns(); + if (pkColumns.length != columnsInUserSavePoint.size()) { + LOG.error("user save point is not on the selected index."); + return false; + } + + for (String column : columnsInUserSavePoint) { + boolean found = false; + for (String pkCol : pkColumns) { + if (pkCol.equals(column)) { + found = true; + break; + } + } + if (!found) { + LOG.error("column " + column + " is not on the selected index."); + return false; + } + } + + return true; + } + + private static String removeBracket(String str) { + final char leftBracket = '('; + final char rightBracket = ')'; + if (str != null && str.contains(String.valueOf(leftBracket)) && str.contains(String.valueOf(rightBracket)) && + str.indexOf(leftBracket) < str.indexOf(rightBracket)) { + return str.substring(str.indexOf(leftBracket) + 1, str.indexOf(rightBracket)); + } + return str; + } + + private static List getColumnsFromUserSavePoint(String userSavePoint) { + return Arrays.asList(removeBracket(userSavePoint.split("=")[0]).split(",")); + } + + private static List getValuesFromUserSavePoint(String userSavePoint) { + return Arrays.asList(removeBracket(userSavePoint.split("=")[1]).split(",")); + } + + /** + * 先解析成where + *

+ * 再判断是否存在索引 + * + * @param conn + * @param context + * @return + */ + public static void initIndex(Connection conn, TaskContext context) { + if (StringUtils.isEmpty(context.getWhere())) { + return; + } + SQLExpr expr = SQLUtils.toSQLExpr(context.getWhere(), "mysql"); + List allColumnsInTab = getAllColumnFromTab(conn, context.getTable()); + List allColNames = getColNames(allColumnsInTab, expr); + + if (allColNames == null) { + return; + } + + // Remove the duplicated column names + Set colNames = new TreeSet(); + for (String colName : allColNames) { + if (!colNames.contains(colName)) { + colNames.add(colName); + } + } + List indexNames = getIndexName(conn, context.getTable(), colNames, context.getCompatibleMode()); + findBestIndex(conn, indexNames, context.getTable(), context); + } + + private static List getAllColumnFromTab(Connection conn, String tableName) { + String sql = "show columns from " + tableName; + Statement stmt = null; + ResultSet rs = null; + List allColumns = new ArrayList(); + try { + stmt = conn.createStatement(); + rs = stmt.executeQuery(sql); + while (rs.next()) { + allColumns.add(rs.getString("Field").toUpperCase()); + } + } catch (Exception e) { + LOG.warn("fail to get all columns from table " + tableName, e); + } finally { + close(rs, stmt, null); + } + + LOG.info("all columns in tab: " + String.join(",", allColumns)); + return allColumns; + } + + /** + * 找出where条件中的列名,目前仅支持全部为and条件,并且操作符为大于、大约等于、等于、小于、小于等于和不等于的表达式。 + *

+ * test coverage: - c6 = 20180710 OR c4 = 320: no index selected - 20180710 + * = c6: correct index selected - 20180710 = c6 and c4 = 320 or c2 < 100: no + * index selected + * + * @param expr + * @return + */ + private static List getColNames(List allColInTab, SQLExpr expr) { + List colNames = new ArrayList(); + if (expr instanceof SQLBinaryOpExpr) { + SQLBinaryOpExpr exp = (SQLBinaryOpExpr) expr; + if (exp.getOperator() == SQLBinaryOperator.BooleanAnd) { + List leftColumns = getColNames(allColInTab, exp.getLeft()); + List rightColumns = getColNames(allColInTab, exp.getRight()); + if (leftColumns == null || rightColumns == null) { + return null; + } + colNames.addAll(leftColumns); + colNames.addAll(rightColumns); + } else if (exp.getOperator() == SQLBinaryOperator.GreaterThan + || exp.getOperator() == SQLBinaryOperator.GreaterThanOrEqual + || exp.getOperator() == SQLBinaryOperator.Equality + || exp.getOperator() == SQLBinaryOperator.LessThan + || exp.getOperator() == SQLBinaryOperator.LessThanOrEqual + || exp.getOperator() == SQLBinaryOperator.NotEqual) { + // only support simple comparison operators + String left = SQLUtils.toMySqlString(exp.getLeft()).toUpperCase(); + String right = SQLUtils.toMySqlString(exp.getRight()).toUpperCase(); + LOG.debug("left: " + left + ", right: " + right); + if (allColInTab.contains(left)) { + colNames.add(left); + } + + if (allColInTab.contains(right)) { + colNames.add(right); + } + } else { + // unsupported operators + return null; + } + } + + return colNames; + } + + private static Map> getAllIndex(Connection conn, String tableName, String compatibleMode) { + Map> allIndex = new HashMap>(); + String sql = "show index from " + tableName; + if (isOracleMode(compatibleMode)) { + String schema; + tableName = tableName.toUpperCase(); + if (tableName.contains(TABLE_SCHEMA_DELIMITER)) { + schema = String.format("'%s'", tableName.substring(0, tableName.indexOf("."))); + tableName = tableName.substring(tableName.indexOf(".") + 1); + } else { + schema = "(select sys_context('USERENV','current_schema') from dual)"; + } + + sql = String.format( + "SELECT INDEX_NAME Key_name, COLUMN_NAME Column_name " + + "from all_ind_columns " + + "where TABLE_NAME = '%s' and TABLE_OWNER = %s " + + " union all " + + "SELECT DISTINCT " + + "CASE " + + "WHEN cons.CONSTRAINT_TYPE = 'P' THEN 'PRIMARY' " + + "WHEN cons.CONSTRAINT_TYPE = 'U' THEN cons.CONSTRAINT_NAME " + + "ELSE '' " + + "END AS Key_name, " + + "cols.column_name Column_name " + + "FROM all_constraints cons, all_cons_columns cols " + + "WHERE cols.table_name = '%s' AND cons.constraint_type in('P', 'U') " + + "AND cons.constraint_name = cols.constraint_name AND cons.owner = cols.owner " + + "AND cons.owner = %s", + tableName, schema, tableName, schema); + } + + Statement stmt = null; + ResultSet rs = null; + + try { + LOG.info("running sql to get index: " + sql); + stmt = conn.createStatement(); + rs = stmt.executeQuery(sql); + while (rs.next()) { + String keyName = rs.getString("Key_name"); + String colName = rs.getString("Column_name").toUpperCase(); + if (allIndex.containsKey(keyName)) { + allIndex.get(keyName).add(colName); + } else { + List allColumns = new ArrayList(); + allColumns.add(colName); + allIndex.put(keyName, allColumns); + } + } + + // add primary key to all index + if (allIndex.containsKey("PRIMARY")) { + List colsInPrimary = allIndex.get("PRIMARY"); + Iterator>> iterator = allIndex.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry> entry = iterator.next(); + if ("PRIMARY".equals(entry.getKey())) { + continue; + } + + // remove the index which is identical with primary key + List indexColumns = entry.getValue(); + if (colsInPrimary.equals(indexColumns)) { + iterator.remove(); + } else { + // add primary key to the index if the index is not on the column + colsInPrimary.forEach( + c -> { + if (!indexColumns.contains(c)) { + indexColumns.add(c); + } + }); + } + } + } + } catch (Exception e) { + LOG.error("fail to get all keys from table" + sql, e); + } finally { + close(rs, stmt, null); + } + + LOG.info("all index: " + allIndex.toString()); + return allIndex; + } + + /** + * find out the indexes which contains all columns in where conditions + * + * @param conn + * @param table + * @param colNamesInCondition + * @return + */ + private static List getIndexName(Connection conn, String table, + Set colNamesInCondition, String compatibleMode) { + List indexNames = new ArrayList(); + if (colNamesInCondition == null || colNamesInCondition.size() == 0) { + LOG.info("there is no qulified conditions in the where clause, skip index selection."); + return indexNames; + } + + LOG.info("columnNamesInConditions: " + String.join(",", colNamesInCondition)); + + Map> allIndex = getAllIndex(conn, table, compatibleMode); + for (String keyName : allIndex.keySet()) { + boolean indexNotMatch = false; + // If the index does not have all the column in where conditions, it + // can not be chosen + // the selected index must start with the columns in where condition + if (allIndex.get(keyName).size() < colNamesInCondition.size()) { + indexNotMatch = true; + } else { + // the first number columns of this index + int num = colNamesInCondition.size(); + for (String colName : allIndex.get(keyName)) { + if (!colNamesInCondition.contains(colName)) { + indexNotMatch = true; + break; + } + if (--num == 0) { + break; + } + } + } + + if (indexNotMatch) { + continue; + } else { + indexNames.add(keyName); + } + } + + return indexNames; + } + + /** + * 以 column开头的索引,可能有多个,也可能存在多列的情形 + *

+ * 所以,需要选择列数最少的 + * + * @param indexNames + * @param context + */ + private static void findBestIndex(Connection conn, List indexNames, String table, TaskContext context) { + if (indexNames.size() == 0) { + LOG.warn("table has no index."); + return; + } + + Map> allIndexs = new HashMap>(); + String sql = "show index from " + table + " where key_name in (" + buildPlaceHolder(indexNames.size()) + ")"; + if (isOracleMode(context.getCompatibleMode())) { + Map> allIndexInTab = getAllIndex(conn, table, context.getCompatibleMode()); + for (String indexName : indexNames) { + if (allIndexInTab.containsKey(indexName)) { + Map index = new TreeMap(); + List columnList = allIndexInTab.get(indexName); + for (int i = 1; i <= columnList.size(); i++) { + index.put(i, columnList.get(i - 1)); + } + allIndexs.put(indexName, index); + } else { + LOG.error("index does not exist: " + indexName); + } + } + } else { + PreparedStatement ps = null; + ResultSet rs = null; + try { + ps = conn.prepareStatement(sql); + for (int i = 0, n = indexNames.size(); i < n; i++) { + ps.setString(i + 1, indexNames.get(i)); + } + rs = ps.executeQuery(); + while (rs.next()) { + String keyName = rs.getString("Key_name"); + Map index = allIndexs.get(keyName); + if (index == null) { + index = new TreeMap(); + allIndexs.put(keyName, index); + } + int keyInIndex = rs.getInt("Seq_in_index"); + String column = rs.getString("Column_name"); + index.put(keyInIndex, column); + } + } catch (Throwable e) { + LOG.error("show index from table fail :" + sql, e); + } finally { + close(rs, ps, null); + } + } + + LOG.info("possible index:" + allIndexs + ",where:" + context.getWhere()); + + Entry> chooseIndex = null; + int columnCount = Integer.MAX_VALUE; + for (Entry> entry : allIndexs.entrySet()) { + if (entry.getValue().size() < columnCount) { + columnCount = entry.getValue().size(); + chooseIndex = entry; + } + } + + if (chooseIndex != null) { + LOG.info("choose index name:" + chooseIndex.getKey() + ",columns:" + chooseIndex.getValue()); + context.setIndexName(chooseIndex.getKey()); + context.setSecondaryIndexColumns(new ArrayList(chooseIndex.getValue().values())); + } + } + + /** + * 由于ObProxy存在bug,事务超时或事务被杀时,conn的close是没有响应的 + * + * @param rs + * @param stmt + * @param conn + */ + public static void close(final ResultSet rs, final Statement stmt, final Connection conn) { + DBUtil.closeDBResources(rs, stmt, conn); + } + + /** + * 判断是否重复record + * + * @param savePoint + * @param row + * @param pkIndexs + * @return + */ + public static boolean isPkEquals(Record savePoint, Record row, int[] pkIndexs) { + if (savePoint == null || row == null) { + return false; + } + try { + for (int index : pkIndexs) { + Object left = savePoint.getColumn(index).getRawData(); + Object right = row.getColumn(index).getRawData(); + if (!left.equals(right)) { + return false; + } + } + } catch (Throwable e) { + return false; + } + return true; + } + + public static String buildPlaceHolder(int n) { + if (n <= 0) { + return ""; + } + StringBuilder str = new StringBuilder(2 * n); + str.append('?'); + for (int i = 1; i < n; i++) { + str.append(",?"); + } + return str.toString(); + } + + public static void binding(PreparedStatement ps, List list) throws SQLException { + if (list.isEmpty()) { + return; + } + List columns = buildFullParams(list); + + for (int i = 0; i < columns.size(); i++) { + Column c = columns.get(i); + if (c instanceof BoolColumn) { + ps.setLong(i + 1, ((BoolColumn) c).asLong()); + } else if (c instanceof BytesColumn) { + ps.setBytes(i + 1, ((BytesColumn) c).asBytes()); + } else if (c instanceof DateColumn) { + ps.setTimestamp(i + 1, new Timestamp(((DateColumn) c).asDate().getTime())); + } else if (c instanceof DoubleColumn) { + //应该直接使用bigDecimal,asDouble会先转换成bigDecimal再转换成Double会导致精度丢失 + ps.setBigDecimal(i + 1, ((DoubleColumn) c).asBigDecimal()); + } else if (c instanceof LongColumn) { + ps.setLong(i + 1, ((LongColumn) c).asLong()); + } else if (c instanceof StringColumn) { + ps.setString(i + 1, ((StringColumn) c).asString()); + } else { + ps.setObject(i + 1, c.getRawData()); + } + } + } + + //增多检查点,上游的构建行为为A,AB,ABC,ABCD的组合,占位符的数量为n(n+1)/2,n为主键列的数量 + public static List buildFullParams(List savePointColumns) { + if (savePointColumns == null || savePointColumns.isEmpty()) { + return new ArrayList<>(); + } + int n = savePointColumns.size(); + List fullParams = new ArrayList<>(); + for (int i = 0; i < n; i++) { + for (int j = 0; j <= i; j++) { + fullParams.add(savePointColumns.get(j)); + } + } + return fullParams; + } + + public static List buildPoint(Record savePoint, int[] pkIndexs) { + List result = new ArrayList(pkIndexs.length); + for (int i = 0, n = pkIndexs.length; i < n; i++) { + result.add(savePoint.getColumn(pkIndexs[i])); + } + return result; + } + + public static String getCompatibleMode(Connection conn) { + String compatibleMode = OB_COMPATIBLE_MODE_MYSQL; + String getCompatibleModeSql = "SHOW VARIABLES LIKE 'ob_compatibility_mode'"; + Statement stmt = null; + ResultSet rs = null; + try { + stmt = conn.createStatement(); + rs = stmt.executeQuery(getCompatibleModeSql); + if (rs.next()) { + compatibleMode = rs.getString("VALUE"); + } + } catch (Exception e) { + LOG.error("fail to get ob compatible mode, using mysql as default: " + e.getMessage()); + } finally { + DBUtil.closeDBResources(rs, stmt, conn); + } + + LOG.info("ob compatible mode is " + compatibleMode); + return compatibleMode; + } + + public static boolean isOracleMode(String mode) { + return (mode != null && OB_COMPATIBLE_MODE_ORACLE.equalsIgnoreCase(mode)); + } + + public static String getDbNameFromJdbcUrl(String jdbcUrl) { + Matcher matcher = JDBC_PATTERN.matcher(jdbcUrl); + if (matcher.find()) { + return matcher.group(3); + } else { + LOG.error("jdbc url {} is not valid.", jdbcUrl); + } + + return null; + } + + public static String buildQuerySql(boolean weakRead, String column, String table, String where) { + if (weakRead) { + return buildWeakReadQuerySql(column, table, where); + } else { + return SingleTableSplitUtil.buildQuerySql(column, table, where); + } + } + + public static String buildWeakReadQuerySql(String column, String table, String where) { + String querySql; + + if (StringUtils.isBlank(where)) { + querySql = String.format(Constant.WEAK_READ_QUERY_SQL_TEMPLATE_WITHOUT_WHERE, column, table); + } else { + querySql = String.format(Constant.WEAK_READ_QUERY_SQL_TEMPLATE, column, table, where); + } + + return querySql; + } + + /** + * compare two ob versions + * + * @param version1 + * @param version2 + * @return 0 when the two versions are the same + * -1 when version1 is smaller (earlier) than version2 + * 1 when version is bigger (later) than version2 + */ + public static int compareObVersion(String version1, String version2) { + if (version1 == null || version2 == null) { + throw new RuntimeException("can not compare null version"); + } + ObVersion v1 = new ObVersion(version1); + ObVersion v2 = new ObVersion(version2); + return v1.compareTo(v2); + } + + /** + * @param conn + * @param sql + * @return + */ + public static List getResultsFromSql(Connection conn, String sql) { + List list = new ArrayList(); + Statement stmt = null; + ResultSet rs = null; + + LOG.info("executing sql: " + sql); + + try { + stmt = conn.createStatement(); + rs = stmt.executeQuery(sql); + while (rs.next()) { + list.add(rs.getString(1)); + } + } catch (Exception e) { + LOG.error("error when executing sql: " + e.getMessage()); + } finally { + DBUtil.closeDBResources(rs, stmt, null); + } + + return list; + } + + /** + * get obversion, try ob_version first, and then try version if failed + * + * @param conn + * @return + */ + public static ObVersion getObVersion(Connection conn) { + List results = getResultsFromSql(conn, "select ob_version()"); + if (results.size() == 0) { + results = getResultsFromSql(conn, "select version()"); + } + ObVersion obVersion = new ObVersion(results.get(0)); + + LOG.info("obVersion: " + obVersion); + return obVersion; + } +} diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ObSingleTableSplitUtil.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ObSingleTableSplitUtil.java new file mode 100644 index 0000000000..e72812abe0 --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ObSingleTableSplitUtil.java @@ -0,0 +1,249 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.util; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.Constant; +import com.alibaba.datax.plugin.rdbms.reader.Key; +import com.alibaba.datax.plugin.rdbms.reader.util.SingleTableSplitUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.util.RdbmsException; +import com.alibaba.datax.plugin.rdbms.util.RdbmsRangeSplitWrap; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.ext.ObReaderKey; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.Statement; +import java.sql.Types; +import java.util.ArrayList; +import java.util.List; + +/** + * OceanBase Reader 专用的单表切分逻辑,包含字符串 splitPk(Oracle 模式)及 OB V10 min/max 查询优化。 + */ +public final class ObSingleTableSplitUtil { + private static final Logger LOG = LoggerFactory.getLogger(ObSingleTableSplitUtil.class); + + private static final DataBaseType DATABASE_TYPE = ObReaderUtils.databaseType; + + private ObSingleTableSplitUtil() { + } + + public static List splitSingleTable(Configuration configuration, int adviceNum) { + List pluginParams = new ArrayList(); + List rangeList; + String splitPkName = configuration.getString(Key.SPLIT_PK); + String column = configuration.getString(Key.COLUMN); + String table = configuration.getString(Key.TABLE); + String where = configuration.getString(Key.WHERE, null); + boolean hasWhere = StringUtils.isNotBlank(where); + + Pair minMaxPK = getPkRange(configuration); + if (null == minMaxPK) { + throw DataXException.asDataXException(DBUtilErrorCode.ILLEGAL_SPLIT_PK, + "根据切分主键切分表失败. DataX 仅支持切分主键为一个,并且类型为整数或者字符串类型. 请尝试使用其他的切分主键或者联系 DBA 进行处理."); + } + + configuration.set(Key.QUERY_SQL, SingleTableSplitUtil.buildQuerySql(column, table, where)); + if (null == minMaxPK.getLeft() || null == minMaxPK.getRight()) { + pluginParams.add(configuration); + return pluginParams; + } + + boolean isStringType = Constant.PK_TYPE_STRING.equals(configuration.getString(Constant.PK_TYPE)); + boolean isLongType = Constant.PK_TYPE_LONG.equals(configuration.getString(Constant.PK_TYPE)); + + if (isStringType) { + DataBaseType stringSplitDbType = resolveStringSplitDataBaseType(configuration); + if (stringSplitDbType == null) { + pluginParams.add(configuration); + LOG.warn("切分主键 {} 为字符串类型,当前 OceanBase MySQL 模式不支持按字符串切分,将使用单通道读取。", + splitPkName); + return pluginParams; + } + rangeList = RdbmsRangeSplitWrap.splitAndWrap( + String.valueOf(minMaxPK.getLeft()), + String.valueOf(minMaxPK.getRight()), adviceNum, + splitPkName, "'", stringSplitDbType); + } else if (isLongType) { + rangeList = RdbmsRangeSplitWrap.splitAndWrap( + new BigInteger(minMaxPK.getLeft().toString()), + new BigInteger(minMaxPK.getRight().toString()), + adviceNum, splitPkName); + } else { + throw DataXException.asDataXException(DBUtilErrorCode.ILLEGAL_SPLIT_PK, + "您配置的切分主键(splitPk) 类型 DataX 不支持. DataX 仅支持切分主键为一个,并且类型为整数或者字符串类型. 请尝试使用其他的切分主键或者联系 DBA 进行处理."); + } + + String tempQuerySql; + List allQuerySql = new ArrayList(); + + if (null != rangeList && !rangeList.isEmpty()) { + for (String range : rangeList) { + Configuration tempConfig = configuration.clone(); + tempQuerySql = SingleTableSplitUtil.buildQuerySql(column, table, where) + + (hasWhere ? " and " : " where ") + range; + allQuerySql.add(tempQuerySql); + tempConfig.set(Key.QUERY_SQL, tempQuerySql); + tempConfig.set(Key.WHERE, (hasWhere ? ("(" + where + ") and") : "") + range); + pluginParams.add(tempConfig); + } + } else { + Configuration tempConfig = configuration.clone(); + tempQuerySql = SingleTableSplitUtil.buildQuerySql(column, table, where) + + (hasWhere ? " and " : " where ") + + String.format(" %s IS NOT NULL", splitPkName); + allQuerySql.add(tempQuerySql); + tempConfig.set(Key.QUERY_SQL, tempQuerySql); + tempConfig.set(Key.WHERE, (hasWhere ? "(" + where + ") and" : "") + + String.format(" %s IS NOT NULL", splitPkName)); + pluginParams.add(tempConfig); + } + + Configuration tempConfig = configuration.clone(); + tempQuerySql = SingleTableSplitUtil.buildQuerySql(column, table, where) + + (hasWhere ? " and " : " where ") + + String.format(" %s IS NULL", splitPkName); + allQuerySql.add(tempQuerySql); + LOG.info("After split(), allQuerySql=[\n{}\n].", StringUtils.join(allQuerySql, "\n")); + tempConfig.set(Key.QUERY_SQL, tempQuerySql); + tempConfig.set(Key.WHERE, (hasWhere ? "(" + where + ") and" : "") + + String.format(" %s IS NULL", splitPkName)); + pluginParams.add(tempConfig); + + return pluginParams; + } + + private static Pair getPkRange(Configuration configuration) { + int fetchSize = configuration.getInt(Constant.FETCH_SIZE); + String jdbcURL = configuration.getString(Key.JDBC_URL); + String username = configuration.getString(Key.USERNAME); + String password = configuration.getString(Key.PASSWORD); + String table = configuration.getString(Key.TABLE); + + Connection conn = DBUtil.getConnection(DATABASE_TYPE, jdbcURL, username, password); + Pair minMaxPK = checkSplitPk(conn, fetchSize, table, username, configuration); + DBUtil.closeDBResources(null, null, conn); + return minMaxPK; + } + + private static Pair checkSplitPk(Connection conn, int fetchSize, String table, + String username, Configuration configuration) { + LOG.info("Get min/max of split key for OBV10"); + int queryTimeoutSeconds = 60 * 60 * 48; + String setQueryTimeout = "set ob_query_timeout=" + (queryTimeoutSeconds * 1000 * 1000L); + String setTrxTimeout = "set ob_trx_timeout=" + ((queryTimeoutSeconds + 5) * 1000 * 1000L); + Statement stmt = null; + try { + stmt = conn.createStatement(); + stmt.execute(setQueryTimeout); + stmt.execute(setTrxTimeout); + } catch (Exception e) { + LOG.warn("set ob_query_timeout and set ob_trx_timeout failed. reason: {}", e.getMessage(), e); + } finally { + DBUtil.closeDBResources(stmt, null); + } + return new ImmutablePair( + getValueForObV10(conn, "min", configuration), + getValueForObV10(conn, "max", configuration)); + } + + private static Object getValueForObV10(Connection conn, String function, Configuration configuration) { + ResultSet rs = null; + Object value = null; + final int fetchSize = 1; + String username = configuration.getString(Key.USERNAME); + String table = configuration.getString(Key.TABLE); + String sql = genSqlForObV10(configuration, function); + + LOG.info("Running Query [{}]", sql); + + try { + rs = DBUtil.query(conn, sql, fetchSize); + ResultSetMetaData rsMetaData = rs.getMetaData(); + if (isPKTypeValid(rsMetaData)) { + if (isStringType(rsMetaData.getColumnType(1))) { + configuration.set(Constant.PK_TYPE, Constant.PK_TYPE_STRING); + } else if (isLongType(rsMetaData.getColumnType(1))) { + configuration.set(Constant.PK_TYPE, Constant.PK_TYPE_LONG); + } else { + throw DataXException.asDataXException(DBUtilErrorCode.ILLEGAL_SPLIT_PK, + "您配置的DataX切分主键(splitPk)有误. 因为您配置的切分主键(splitPk) 类型 DataX 不支持. DataX 仅支持切分主键为一个,并且类型为整数或者字符串类型. 请尝试使用其他的切分主键或者联系 DBA 进行处理."); + } + while (DBUtil.asyncResultSetNext(rs)) { + value = rs.getString(1); + } + } else { + throw DataXException.asDataXException(DBUtilErrorCode.ILLEGAL_SPLIT_PK, + "您配置的DataX切分主键(splitPk)有误. 因为您配置的切分主键(splitPk) 类型 DataX 不支持. DataX 仅支持切分主键为一个,并且类型为整数或者字符串类型. 请尝试使用其他的切分主键或者联系 DBA 进行处理."); + } + } catch (DataXException e) { + throw e; + } catch (Exception e) { + throw RdbmsException.asQueryException(DATABASE_TYPE, e, sql, table, username); + } finally { + DBUtil.closeDBResources(rs, null, null); + } + return value; + } + + private static String genSqlForObV10(Configuration configuration, String function) { + String primaryKey = configuration.getString(Key.SPLIT_PK).trim(); + String table = configuration.getString(Key.TABLE).trim(); + String where = configuration.getString(Key.WHERE, null); + String pkRangeSQL = String.format("SELECT %s(%s) FROM %s", function, primaryKey, table); + if (StringUtils.isNotBlank(where)) { + pkRangeSQL = String.format("%s WHERE (%s AND %s IS NOT NULL)", pkRangeSQL, where, primaryKey); + } + return pkRangeSQL; + } + + private static DataBaseType resolveStringSplitDataBaseType(Configuration configuration) { + if (isObOracleMode(configuration)) { + return DataBaseType.Oracle; + } + return null; + } + + private static boolean isObOracleMode(Configuration configuration) { + return configuration != null + && configuration.getString(ObReaderKey.OB_COMPATIBILITY_MODE, "") + .equalsIgnoreCase(DataBaseType.Oracle.getTypeName()); + } + + private static boolean isPKTypeValid(ResultSetMetaData rsMetaData) { + try { + int pkType = rsMetaData.getColumnType(1); + int maxType = Types.NULL; + if (rsMetaData.getColumnCount() == 2) { + maxType = rsMetaData.getColumnType(2); + } + boolean isNumberType = isLongType(pkType); + boolean isStringType = isStringType(pkType); + return (maxType == Types.NULL || pkType == maxType) && (isNumberType || isStringType); + } catch (Exception e) { + throw DataXException.asDataXException(DBUtilErrorCode.ILLEGAL_SPLIT_PK, + "DataX获取切分主键(splitPk)字段类型失败. 该错误通常是系统底层异常导致. 请联DBA进行处理."); + } + } + + private static boolean isLongType(int type) { + boolean isValidLongType = type == Types.BIGINT || type == Types.INTEGER + || type == Types.SMALLINT || type == Types.TINYINT || type == Types.NUMERIC; + return isValidLongType; + } + + private static boolean isStringType(int type) { + return type == Types.CHAR || type == Types.NCHAR + || type == Types.VARCHAR || type == Types.LONGVARCHAR + || type == Types.NVARCHAR; + } +} diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/PartInfo.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/PartInfo.java new file mode 100644 index 0000000000..7a9a6f7042 --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/PartInfo.java @@ -0,0 +1,35 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author johnrobbet + */ +public class PartInfo { + + private PartType partType; + + List partList; + + public PartInfo(PartType partType) { + this.partType = partType; + this.partList = new ArrayList(); + } + + public String getPartType () { + return partType.getTypeString(); + } + + public void addPart(List partList) { + this.partList.addAll(partList); + } + + public List getPartList() { + return partList; + } + + public boolean isPartitionTable() { + return partType != PartType.NONPARTITION && partList.size() > 0; + } +} diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/PartType.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/PartType.java new file mode 100644 index 0000000000..05c23d6ff9 --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/PartType.java @@ -0,0 +1,28 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.util; + +/** + * @author johnrobbet + */ + +public enum PartType { + // Non partitioned table + NONPARTITION("NONPARTITION"), + + // Partitioned table + PARTITION("PARTITION"), + + // Subpartitioned table + SUBPARTITION("SUBPARTITION"); + + private String typeString; + + PartType (String typeString) { + this.typeString = typeString; + } + + public String getTypeString() { + return typeString; + } +} + + diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/PartitionSplitUtil.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/PartitionSplitUtil.java new file mode 100644 index 0000000000..ad165d9975 --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/PartitionSplitUtil.java @@ -0,0 +1,192 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.util; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.Constant; +import com.alibaba.datax.plugin.rdbms.reader.Key; +import com.alibaba.datax.plugin.rdbms.reader.util.ObVersion; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.reader.oceanbasev10reader.ext.ObReaderKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.List; + +/** + * @author johnrobbet + */ +public class PartitionSplitUtil { + private static final Logger LOG = LoggerFactory.getLogger(PartitionSplitUtil.class); + + private static final String ORACLE_GET_SUBPART_TEMPLATE = + "select subpartition_name " + + "from dba_tab_subpartitions " + + "where table_name = '%s' and table_owner = '%s'"; + + private static final String ORACLE_GET_PART_TEMPLATE = + "select partition_name " + + "from dba_tab_partitions " + + "where table_name = '%s' and table_owner = '%s'"; + + private static final String MYSQL_GET_PART_TEMPLATE = + "select p.part_name " + + "from oceanbase.__all_part p, oceanbase.%s t, oceanbase.__all_database d " + + "where p.table_id = t.table_id " + + "and d.database_id = t.database_id " + + "and d.database_name = '%s' " + + "and t.table_name = '%s'"; + + private static final String MYSQL_GET_SUBPART_TEMPLATE = + "select p.sub_part_name " + + "from oceanbase.__all_sub_part p, oceanbase.%s t, oceanbase.__all_database d " + + "where p.table_id = t.table_id " + + "and d.database_id = t.database_id " + + "and d.database_name = '%s' " + + "and t.table_name = '%s'"; + + /** + * get partition info from data dictionary in ob oracle mode + * @param config + * @param tableName + * @return + */ + public static PartInfo getObOraclePartInfoBySQL(Configuration config, String tableName) { + PartInfo partInfo; + DataBaseType dbType = ObReaderUtils.databaseType; + String jdbcUrl = config.getString(Key.JDBC_URL); + String username = config.getString(Key.USERNAME); + String password = config.getString(Key.PASSWORD); + String dbname = ObReaderUtils.getDbNameFromJdbcUrl(jdbcUrl).toUpperCase(); + Connection conn = DBUtil.getConnection(dbType, jdbcUrl, username, password); + tableName = tableName.toUpperCase(); + + // check if the table has subpartitions or not + String getSubPartSql = String.format(ORACLE_GET_SUBPART_TEMPLATE, tableName, dbname); + List partList = ObReaderUtils.getResultsFromSql(conn, getSubPartSql); + if (partList != null && partList.size() > 0) { + partInfo = new PartInfo(PartType.SUBPARTITION); + partInfo.addPart(partList); + return partInfo; + } + + String getPartSql = String.format(ORACLE_GET_PART_TEMPLATE, tableName, dbname); + partList = ObReaderUtils.getResultsFromSql(conn, getPartSql); + if (partList != null && partList.size() > 0) { + partInfo = new PartInfo(PartType.PARTITION); + partInfo.addPart(partList); + return partInfo; + } + + // table is not partitioned + partInfo = new PartInfo(PartType.NONPARTITION); + return partInfo; + } + + public static List splitByPartition (Configuration configuration) { + List allSlices = new ArrayList<>(); + List connections = configuration.getList(Constant.CONN_MARK, Object.class); + for (int i = 0, len = connections.size(); i < len; i++) { + Configuration sliceConfig = configuration.clone(); + Configuration connConf = Configuration.from(connections.get(i).toString()); + String jdbcUrl = connConf.getString(Key.JDBC_URL); + sliceConfig.set(Key.JDBC_URL, jdbcUrl); + sliceConfig.remove(Constant.CONN_MARK); + + List tables = connConf.getList(Key.TABLE, String.class); + for (String table : tables) { + Configuration tempSlice = sliceConfig.clone(); + tempSlice.set(Key.TABLE, table); + allSlices.addAll(splitSinglePartitionTable(tempSlice)); + } + } + + return allSlices; + } + + private static List splitSinglePartitionTable(Configuration configuration) { + String table = configuration.getString(Key.TABLE); + String where = configuration.getString(Key.WHERE, null); + String column = configuration.getString(Key.COLUMN); + final boolean weakRead = configuration.getBool(Key.WEAK_READ, true); + + List slices = new ArrayList(); + PartInfo partInfo = getObPartInfoBySQL(configuration, table); + if (partInfo != null && partInfo.isPartitionTable()) { + String partitionType = partInfo.getPartType(); + for (String partitionName : partInfo.getPartList()) { + LOG.info(String.format("add %s %s for table %s", partitionType, partitionName, table)); + Configuration slice = configuration.clone(); + slice.set(ObReaderKey.PARTITION_NAME, partitionName); + slice.set(ObReaderKey.PARTITION_TYPE, partitionType); + slice.set(Key.QUERY_SQL, + ObReaderUtils.buildQuerySql(weakRead, column, + String.format("%s partition(%s)", table, partitionName), where)); + slices.add(slice); + } + } else { + LOG.info("table is not partitioned."); + + Configuration slice = configuration.clone(); + slice.set(Key.QUERY_SQL, ObReaderUtils.buildQuerySql(weakRead, column, table, where)); + slices.add(slice); + } + + return slices; + } + + public static PartInfo getObPartInfoBySQL(Configuration config, String table) { + boolean isOracleMode = config.getString(ObReaderKey.OB_COMPATIBILITY_MODE).equals("ORACLE"); + if (isOracleMode) { + return getObOraclePartInfoBySQL(config, table); + } else { + return getObMySQLPartInfoBySQL(config, table); + } + } + + public static PartInfo getObMySQLPartInfoBySQL(Configuration config, String table) { + PartInfo partInfo = new PartInfo(PartType.NONPARTITION); + List partList; + Connection conn = null; + try { + String jdbcUrl = config.getString(Key.JDBC_URL); + String username = config.getString(Key.USERNAME); + String password = config.getString(Key.PASSWORD); + String dbname = ObReaderUtils.getDbNameFromJdbcUrl(jdbcUrl); + String allTable = "__all_table"; + + conn = DBUtil.getConnection(DataBaseType.OceanBase, jdbcUrl, username, password); + ObVersion obVersion = ObReaderUtils.getObVersion(conn); + if (obVersion.compareTo(ObVersion.V2276) >= 0 && + obVersion.compareTo(ObVersion.V4000) < 0) { + allTable = "__all_table_v2"; + } + + String querySubPart = String.format(MYSQL_GET_SUBPART_TEMPLATE, allTable, dbname, table); + + PartType partType = PartType.SUBPARTITION; + + // try subpartition first + partList = ObReaderUtils.getResultsFromSql(conn, querySubPart); + + // if table is not sub-partitioned, the try partition + if (partList.isEmpty()) { + String queryPart = String.format(MYSQL_GET_PART_TEMPLATE, allTable, dbname, table); + partList = ObReaderUtils.getResultsFromSql(conn, queryPart); + partType = PartType.PARTITION; + } + + if (!partList.isEmpty()) { + partInfo = new PartInfo(partType); + partInfo.addPart(partList); + } + } catch (Exception ex) { + LOG.error("error when get partition list: " + ex.getMessage()); + } finally { + DBUtil.closeDBResources(null, conn); + } + + return partInfo; + } +} diff --git a/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/TaskContext.java b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/TaskContext.java new file mode 100644 index 0000000000..df77848f11 --- /dev/null +++ b/oceanbasev10reader/src/main/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/TaskContext.java @@ -0,0 +1,179 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.util; + +import java.sql.Connection; +import java.util.Collections; +import java.util.List; + +import com.alibaba.datax.common.element.Record; + +public class TaskContext { + private Connection conn; + private final String table; + private String indexName; + // 辅助索引的字段列表 + private List secondaryIndexColumns = Collections.emptyList(); + private String querySql; + private final String where; + private final int fetchSize; + private long readBatchSize = -1; + private boolean weakRead = true; + private String userSavePoint; + private String compatibleMode = ObReaderUtils.OB_COMPATIBLE_MODE_MYSQL; + + public String getPartitionName() { + return partitionName; + } + + public void setPartitionName(String partitionName) { + this.partitionName = partitionName; + } + + private String partitionName; + + // 断点续读的保存点 + private volatile Record savePoint; + + // pk在column中的index,用于绑定变量时从savePoint中读取值 + // 如果这个值为null,则表示 不是断点续读的场景 + private int[] pkIndexs; + + private final List columns; + + private String[] pkColumns; + + private long cost; + + private final int transferColumnNumber; + + public TaskContext(String table, List columns, String where, int fetchSize) { + super(); + this.table = table; + this.columns = columns; + // 针对只有querySql的场景 + this.transferColumnNumber = columns == null ? -1 : columns.size(); + this.where = where; + this.fetchSize = fetchSize; + } + + public Connection getConn() { + return conn; + } + + public void setConn(Connection conn) { + this.conn = conn; + } + + public String getIndexName() { + return indexName; + } + + public void setIndexName(String indexName) { + this.indexName = indexName; + } + + public List getSecondaryIndexColumns() { + return secondaryIndexColumns; + } + + public void setSecondaryIndexColumns(List secondaryIndexColumns) { + this.secondaryIndexColumns = secondaryIndexColumns; + } + + public String getQuerySql() { + if (readBatchSize == -1) { + return querySql; + } else if (ObReaderUtils.isOracleMode(compatibleMode)) { + return String.format("select * from (%s) where rownum <= %d", querySql, readBatchSize); + } else { + return querySql + " limit " + readBatchSize; + } + } + + public void setQuerySql(String querySql) { + this.querySql = querySql; + } + + public String getWhere() { + return where; + } + + public Record getSavePoint() { + return savePoint; + } + + public void setSavePoint(Record savePoint) { + this.savePoint = savePoint; + } + + public int[] getPkIndexs() { + return pkIndexs; + } + + public void setPkIndexs(int[] pkIndexs) { + this.pkIndexs = pkIndexs; + } + + public List getColumns() { + return columns; + } + + public String[] getPkColumns() { + return pkColumns; + } + + public void setPkColumns(String[] pkColumns) { + this.pkColumns = pkColumns; + } + + public String getTable() { + return table; + } + + public int getFetchSize() { + return fetchSize; + } + + public long getCost() { + return cost; + } + + public void addCost(long cost) { + this.cost += cost; + } + + public int getTransferColumnNumber() { + return transferColumnNumber; + } + + public long getReadBatchSize() { + return readBatchSize; + } + + public void setReadBatchSize(long readBatchSize) { + this.readBatchSize = readBatchSize; + } + + public boolean getWeakRead() { + return weakRead; + } + + public void setWeakRead(boolean weakRead) { + this.weakRead = weakRead; + } + + public String getUserSavePoint() { + return userSavePoint; + } + + public void setUserSavePoint(String userSavePoint) { + this.userSavePoint = userSavePoint; + } + + public String getCompatibleMode() { + return compatibleMode; + } + + public void setCompatibleMode(String compatibleMode) { + this.compatibleMode = compatibleMode; + } +} diff --git a/oceanbasev10reader/src/main/libs/oceanbase-client-1.1.10.jar b/oceanbasev10reader/src/main/libs/oceanbase-client-1.1.10.jar new file mode 100644 index 0000000000..38162912f8 Binary files /dev/null and b/oceanbasev10reader/src/main/libs/oceanbase-client-1.1.10.jar differ diff --git a/oceanbasev10reader/src/main/resources/plugin.json b/oceanbasev10reader/src/main/resources/plugin.json new file mode 100644 index 0000000000..66acbd62c0 --- /dev/null +++ b/oceanbasev10reader/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "oceanbasev10reader", + "class": "com.alibaba.datax.plugin.reader.oceanbasev10reader.OceanBaseReader", + "description": "read data from oceanbase with SQL interface", + "developer": "oceanbase" +} \ No newline at end of file diff --git a/oceanbasev10reader/src/test/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ObReaderUtilsTest.java b/oceanbasev10reader/src/test/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ObReaderUtilsTest.java new file mode 100644 index 0000000000..3596659502 --- /dev/null +++ b/oceanbasev10reader/src/test/java/com/alibaba/datax/plugin/reader/oceanbasev10reader/util/ObReaderUtilsTest.java @@ -0,0 +1,24 @@ +package com.alibaba.datax.plugin.reader.oceanbasev10reader.util; + +import org.junit.Test; + +public class ObReaderUtilsTest { + + @Test + public void getDbTest() { + assert ObReaderUtils.getDbNameFromJdbcUrl("jdbc:mysql://127.0.0.1:3306/testdb").equalsIgnoreCase("testdb"); + assert ObReaderUtils.getDbNameFromJdbcUrl("jdbc:oceanbase://127.0.0.1:2883/testdb").equalsIgnoreCase("testdb"); + assert ObReaderUtils.getDbNameFromJdbcUrl("||_dsc_ob10_dsc_||obcluster:mysql||_dsc_ob10_dsc_||jdbc:mysql://127.0.0.1:3306/testdb").equalsIgnoreCase("testdb"); + assert ObReaderUtils.getDbNameFromJdbcUrl("||_dsc_ob10_dsc_||obcluster:oracle||_dsc_ob10_dsc_||jdbc:oceanbase://127.0.0.1:3306/testdb").equalsIgnoreCase("testdb"); + } + + @Test + public void compareObVersionTest() { + assert ObReaderUtils.compareObVersion("2.2.70", "3.2.2") == -1; + assert ObReaderUtils.compareObVersion("2.2.70", "2.2.50") == 1; + assert ObReaderUtils.compareObVersion("2.2.70", "3.1.2") == -1; + assert ObReaderUtils.compareObVersion("3.1.2", "3.1.2") == 0; + assert ObReaderUtils.compareObVersion("3.2.3.0", "3.2.3.0") == 0; + assert ObReaderUtils.compareObVersion("3.2.3.0-CE", "3.2.3.0") == 0; + } +} diff --git a/oceanbasev10writer/doc/oceanbasev10writer.md b/oceanbasev10writer/doc/oceanbasev10writer.md new file mode 100644 index 0000000000..32be11b719 --- /dev/null +++ b/oceanbasev10writer/doc/oceanbasev10writer.md @@ -0,0 +1,363 @@ +## 1 快速介绍 +OceanBaseV10Writer 插件实现了写入数据到 OceanBase V1.0以及更高版本数据库的目的表的功能。在底层实现上, OceanbaseV10Writer 通过 java客户端(底层MySQL JDBC或oceanbase client) 连接obproxy远程 OceanBase 数据库,并执行相应的 insert .. on duplicate key update这条sql 语句将数据写入 OceanBase ,内部会分批次提交入库。 +Oceanbasev10Writer 面向ETL开发工程师,他们使用 Oceanbasev10Writer 从数仓导入数据到 Oceanbase。同时 Oceanbasev10Writer 亦可以作为数据迁移工具为DBA等用户提供服务。 + +注意,oceanbasewriter是ob 0.5的writer,oceanbasev10writer是ob 1.0及以后版本的writer。 + +## 2 实现原理 +Oceanbasev10Writer 通过 DataX 框架获取 Reader 生成的协议数据,生成insert ... on duplicate key update语句,在主键或唯一键冲突时,更新表中的所有字段。目前只有这一种行为,写入模式(只写入不更新)和更新指定字段目前暂未支持。 出于性能考虑,写入采用batch方式批量写,当行数累计到预定阈值时,才发起写入请求。 +插件连接ob使用Mysql/Oceanbase JDBC driver通过obproxy连接ob; + +## 3 功能说明 +### 3.1 配置样例 + +- 这里使用一份从内存产生到 Oceanbase 导入的数据。 +``` +{ + "job": { + "setting": { + "speed": { + "channel": 1 + }, + "errorLimit": { + "record": 1 + } + }, + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "column" : [ + { + "value": "DataX", + "type": "string" + }, + { + "value": 19880808, + "type": "long" + }, + { + "value": "1988-08-08 08:08:08", + "type": "date" + }, + { + "value": true, + "type": "bool" + }, + { + "value": "test", + "type": "bytes" + } + ], + "sliceRecordCount": 1000 + } + }, + "writer": { + "name": "oceanbasev10writer", + "parameter": { + "obWriteMode": "update", + "column": [ + "id", + "name" + ], + "preSql": [ + "delete from test" + ], + "connection": [ + { + "jdbcUrl": "||_dsc_ob10_dsc_||集群名:租户名||_dsc_ob10_dsc_||jdbc:mysql://obproxyIp:obproxyPort/dbName", + "table": [ + "test" + ] + } + ], + "username": "xxx", + "password":"xxx", + "batchSize": 256, + "memstoreThreshold": "0.9" + } + } + } + ] + } +} +``` +- 这里使用一份从内存产生到 Oceanbase 旁路导入的数据。 +``` +{ + "job": { + "setting": { + "speed": { + "channel": 1 + }, + "errorLimit": { + "record": 1 + } + }, + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "column" : [ + { + "value": "DataX", + "type": "string" + }, + { + "value": 19880808, + "type": "long" + }, + { + "value": "1988-08-08 08:08:08", + "type": "date" + }, + { + "value": true, + "type": "bool" + }, + { + "value": "test", + "type": "bytes" + } + ], + "sliceRecordCount": 1000 + } + }, + "writer": { + "name": "oceanbasev10writer", + "parameter": { + "obWriteMode": "update", + "column": [ + "id", + "name" + ], + "preSql": [ + "delete from test" + ], + "connection": [ + { + "jdbcUrl": "||_dsc_ob10_dsc_||集群名:租户名||_dsc_ob10_dsc_||jdbc:mysql://obproxyIp:obproxyPort/dbName", + "table": [ + "test" + ] + } + ], + "username": "xxx", + "password":"xxx", + "batchSize": 256, + "directPath": true, + "rpcPort": 2882, + "parallel": 8, + "heartBeatInterval": 1000, + "heartBeatTimeout": 6000, + "bufferSize": 1048576, + "memstoreThreshold": "0.9" + } + } + } + ] + } +} +``` +### 3.2 参数说明 + +- **jdbcUrl** + - 描述:连接ob使用的jdbc url,支持两种格式: + - ||_dsc_ob10_dsc_||集群名:租户名||_dsc_ob10_dsc_||jdbc:mysql://obproxyIp:obproxyPort/db + - 此格式下username仅填写用户名本身,无需三段式写法 + - jdbc:mysql://ip:port/db + - 此格式下username需要三段式写法 + - 必选:是 + - 默认值:无 +- **table** + - 描述:目的表的表名称。开源版obwriter插件仅支持写入一个表。表名中一般不含库名; + - 必选:是 + - 默认值:无 +- **column** + - 描述:目的表需要写入数据的字段,字段之间用英文逗号分隔。例如: "column": ["id","name","age"]。 +``` +**column配置项必须指定,不能留空!** + 注意:1、我们强烈不推荐你这样配置,因为当你目的表字段个数、类型等有改动时,你的任务可能运行不正确或者失败 + 2、 column 不能配置任何常量值 +``` + +- 必选:是 +- 默认值:否 +- **preSql** + - 描述:写入数据到目的表前,会先执行这里的标准语句。如果 Sql 中有你需要操作到的表名称,请使用 `@table` 表示,这样在实际执行 Sql 语句时,会对变量按照实际表名称进行替换。比如你的任务是要写入到目的端的100个同构分表(表名称为:datax_00,datax01, ... datax_98,datax_99),并且你希望导入数据前,先对表中数据进行删除操作,那么你可以这样配置:`"preSql":["delete from @table"]`,效果是:在执行到每个表写入数据前,会先执行对应的 delete from 对应表名称.只支持delete语句 + - 必选:否 + - 默认值:无 +- **batchSize** + - 描述:一次性批量提交的记录数大小,该值可以极大减少DataX与Oceanbase的网络交互次数,并提升整体吞吐量。但是该值设置过大可能会造成DataX运行进程OOM情况。 + - 必选:否 + - 默认值:1000 +- **memstoreThreshold** + - 描述:OB租户的memstore使用率,当达到这个阀值的时候暂停导入,等释放内存后继续导入. 防止租户内存溢出 + - 必选:否 + - 默认值:0.9 +- **username** + - 描述:访问oceanbase的用户名。注意当jdbcUrl配置为||_dsc_ob10_dsc_||集群名:租户名||_dsc_ob10_dsc_||这样的格式时,此处不配置ob的集群名和租户名。否则需要配置为三段式形式。 + - 必选:是 + - 默认值:无 +- **** password**** + - 描述:访问oceanbase的密码 + - 必选:是 + - 默认值:无 +- writerThreadCount + - 描述:每个通道(channel)中写入使用的线程数 + - 必选:否 + - 默认值:1 +- directPath + - 描述:开启旁路导入 + - 必选:否 + - 默认值:false +- rpcPort + - 描述:oceanbase的rpc端口 + - 必选:否 + - 默认值:无 +- parallel + - 描述:旁路导入的启用线程数 + - 必选:否 + - 默认值:1 +- bufferSize + - 描述:旁路导入的切分数据块大小 + - 必选:否 + - 默认值:1048576 +- heartBeatInterval + - 描述:旁路导入的心跳间隔 + - 必选:否 + - 默认值:1000 +- heartBeatTimeout + - 描述:旁路导入的心跳超时时间 + - 必选:否 + - 默认值:6000 +``` +**开启了旁路导入,即directPath:true时** + 注意:1、此时rpcPort为必填项。 + 2、设置parallel时,parallel和oceanbase的负载有关。 + 3、设置heartBeatTimeout最低不能低于6000,heartBeatTimeout的值最低不能低于1000, + 当heartBeatTimeout和heartBeatTimeout同时设置时,heartBeatTimeout-heartBeatTimeout的差值不能低于4000。 + 4、bufferSize的单位为字节数,默认为1M,即1048576。 +``` + +## 4 常见问题 +### +4.1 连接断开导致写入失败 +Data X写入ob的任务失败,在log中可以发现在写入ob时,连接被断开: +``` +2018-12-14 05:40:48.586 [18705170-3-17-writer] WARN CommonRdbmsWriter$Task - 遇到OB异常,回滚此次写入, 休眠 1秒,采用逐条写入提交,SQLState:S1000 +java.sql.SQLException: Could not retrieve transation read-only status server + at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:964) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:897) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:886) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:860) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:877) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:873) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.ConnectionImpl.isReadOnly(ConnectionImpl.java:3603) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.ConnectionImpl.isReadOnly(ConnectionImpl.java:3572) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.PreparedStatement.executeBatchInternal(PreparedStatement.java:1225) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.StatementImpl.executeBatch(StatementImpl.java:958) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.alibaba.datax.plugin.writer.oceanbasev10writer.task.MultiTableWriterTask.write(MultiTableWriterTask.java:357) [oceanbasev10writer-0.0.1-SNAPSHOT.jar:na] + at com.alibaba.datax.plugin.writer.oceanbasev10writer.task.MultiTableWriterTask.calcRuleAndDoBatchInsert(MultiTableWriterTask.java:338) [oceanbasev10writer-0.0.1-SNAPSHOT.jar:na] + at com.alibaba.datax.plugin.writer.oceanbasev10writer.task.MultiTableWriterTask.startWrite(MultiTableWriterTask.java:227) [oceanbasev10writer-0.0.1-SNAPSHOT.jar:na] + at com.alibaba.datax.plugin.writer.oceanbasev10writer.OceanBaseV10Writer$Task.startWrite(OceanBaseV10Writer.java:360) [oceanbasev10writer-0.0.1-SNAPSHOT.jar:na] + at com.alibaba.datax.core.taskgroup.runner.WriterRunner.run(WriterRunner.java:62) [datax-core-0.0.1-SNAPSHOT.jar:na] + at java.lang.Thread.run(Thread.java:834) [na:1.8.0_112] +Caused by: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure +The last packet successfully received from the server was 5 milliseconds ago. The last packet sent successfully to the server was 4 milliseconds ago. + at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_112] + at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_112] + at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_112] + at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_112] + at com.mysql.jdbc.Util.handleNewInstance(Util.java:425) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:989) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3556) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3456) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3897) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2524) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2677) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2545) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2503) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.StatementImpl.executeQuery(StatementImpl.java:1369) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.ConnectionImpl.isReadOnly(ConnectionImpl.java:3597) ~[mysql-connector-java-5.1.40.jar:5.1.40] + ... 9 common frames omitted +Caused by: java.io.EOFException: Can not read response from server. Expected to read 4 bytes, read 0 bytes before connection was unexpectedly lost. + at com.mysql.jdbc.MysqlIO.readFully(MysqlIO.java:3008) ~[mysql-connector-java-5.1.40.jar:5.1.40] + at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3466) ~[mysql-connector-java-5.1.40.jar:5.1.40] + ... 17 common frames omitted +``` +关键字:could not retrieve transation status from read-only status server, communication link failure +检查运行Data X任务的机器,发现obproxy在任务运行时发生若干次重启: +![](https://cdn.nlark.com/lark/0/2018/png/97504/1544760936504-948a2699-e21b-4970-ad76-25b6ac1cd89d.png#height=156&id=wutJw&originHeight=156&originWidth=507&originalType=binary&ratio=1&rotation=0&showTitle=false&status=done&style=none&title=&width=507) +在第一次obproxy退出的日志里,找到退出原因: +``` +[2018-12-14 05:40:47.611683] ERROR [PROXY] do_monitor_mem (ob_proxy_main.cpp:889) [7262][Y0-7F4480213880] [AL=47391-47390-29] obproxy's memroy is out of limit, will be going to commit suicide(mem_limited=838860800, OTHER_MEMORY_SIZE=73400320, is_out_of_mem_limit=true, cur_pos=9) BACKTRACE:0x49db91 0x47fdc9 0x43b115 0x43ee5d 0xa6e623 0xe401b2 0xe3f497 0x4f674c 0x7f4487ace77d 0x7f44865ed9ad +[2018-12-14 05:40:47.612334] ERROR [PROXY] do_monitor_mem (ob_proxy_main.cpp:891) [7262][Y0-7F4480213880] [AL=47392-47391-651] history memory size, history_mem_size[0]=765460480 BACKTRACE:0x49db91 0x47fdc9 0x48717a 0x43f121 0xa6e623 0xe401b2 0xe3f497 0x4f674c 0x7f4487ace77d 0x7f44865ed9ad +[2018-12-14 05:40:47.612934] ERROR [PROXY] do_monitor_mem (ob_proxy_main.cpp:891) [7262][Y0-7F4480213880] [AL=47393-47392-600] history memory size, history_mem_size[1]=765460480 BACKTRACE:0x49db91 0x47fdc9 0x48717a 0x43f121 0xa6e623 0xe401b2 0xe3f497 0x4f674c 0x7f4487ace77d 0x7f44865ed9ad +[2018-12-14 05:40:47.613530] ERROR [PROXY] do_monitor_mem (ob_proxy_main.cpp:891) [7262][Y0-7F4480213880] [AL=47394-47393-596] history memory size, history_mem_size[2]=765460480 BACKTRACE:0x49db91 0x47fdc9 0x48717a 0x43f121 0xa6e623 0xe401b2 0xe3f497 0x4f674c 0x7f4487ace77d 0x7f44865ed9ad +[2018-12-14 05:40:47.614121] ERROR [PROXY] do_monitor_mem (ob_proxy_main.cpp:891) [7262][Y0-7F4480213880] [AL=47395-47394-591] history memory size, history_mem_size[3]=765460480 BACKTRACE:0x49db91 0x47fdc9 0x48717a 0x43f121 0xa6e623 0xe401b2 0xe3f497 0x4f674c 0x7f4487ace77d 0x7f44865ed9ad +[2018-12-14 05:40:47.614717] ERROR [PROXY] do_monitor_mem (ob_proxy_main.cpp:891) [7262][Y0-7F4480213880] [AL=47396-47395-596] history memory size, history_mem_size[4]=765460480 BACKTRACE:0x49db91 0x47fdc9 0x48717a 0x43f121 0xa6e623 0xe401b2 0xe3f497 0x4f674c 0x7f4487ace77d 0x7f44865ed9ad +[2018-12-14 05:40:47.615307] ERROR [PROXY] do_monitor_mem (ob_proxy_main.cpp:891) [7262][Y0-7F4480213880] [AL=47397-47396-590] history memory size, history_mem_size[5]=765460480 BACKTRACE:0x49db91 0x47fdc9 0x48717a 0x43f121 0xa6e623 0xe401b2 0xe3f497 0x4f674c 0x7f4487ace77d 0x7f44865ed9ad +``` +关键字:obproxy's memroy is out of limit, will be going to commit suicide +可以看到,obproxy由于内存不足退出。 +#### 解决方案 +obproxy在启动时, 可以指定使用内存上限,默认是800M,在某些情况下,比如连接数较多(该失败的任务为写入100张分表,并发数32,因此连接数为3200),可能会导致obproxy内存不够用。要解决该问题,一方面可以调低任务的并发数,另一方面可以调大obproxy的内存限制,比如调整至2G。 + +### 4.2 Session interrupted +在使用ob 1.0 writer往单表里写入数据时,遇到以下错误: + +``` +2019-01-03 19:37:27.197 [0-insertTask-73] WARN InsertTask - Insert fatal error SqlState =HY000, errorCode = 5066, java.sql.SQLException: Session interrupted, server ip:port[11.145.28.93:2881] +``` +关键字:fatal,Session interrupted,server ip:port +在任务执行的log中,还可以发现如下log: + +``` +2019-08-09 11:56:56.758 [2-insertTask-82] ERROR StdoutPluginCollector - +java.sql.SQLException: Session interrupted, server ip:port[11.232.58.16:2881] + at com.alipay.oceanbase.obproxy.connection.ObGroupConnection.checkAndThrowException(ObGroupConnection.java:431) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.statement.ObStatement.doExecute(ObStatement.java:598) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.statement.ObStatement.execute(ObStatement.java:456) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.statement.ObPreparedStatement.execute(ObPreparedStatement.java:148) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter$Task.doOneInsert(CommonRdbmsWriter.java:430) ~[plugin-rdbms-util-0.0.1-SNAPSHOT.jar:na] + at com.alibaba.datax.plugin.writer.oceanbasev10writer.task.InsertTask.doMultiInsert(InsertTask.java:196) [oceanbasev10writer-0.0.1-SNAPSHOT.jar:na] + at com.alibaba.datax.plugin.writer.oceanbasev10writer.task.InsertTask.run(InsertTask.java:85) [oceanbasev10writer-0.0.1-SNAPSHOT.jar:na] + at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1147) [na:1.8.0_112] + at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:622) [na:1.8.0_112] + at java.lang.Thread.run(Thread.java:834) [na:1.8.0_112] +Caused by: com.alipay.oceanbase.obproxy.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: INSERT command denied to user 'dwexp'@'%' for table 'mobile_product_version_info' + at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_112] + at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_112] + at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_112] + at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_112] + at com.alipay.oceanbase.obproxy.mysql.jdbc.Util.handleNewInstance(Util.java:409) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.mysql.jdbc.Util.getInstance(Util.java:384) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.mysql.jdbc.SQLError.createSQLException(SQLError.java:1052) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4403) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:4275) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2706) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2867) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2843) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:2085) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.mysql.jdbc.PreparedStatement.execute(PreparedStatement.java:1310) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:493) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.statement.ObPreparedStatement.executeOnConnection(ObPreparedStatement.java:121) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.statement.ObStatement.doExecuteOnConnection(ObStatement.java:677) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + at com.alipay.oceanbase.obproxy.statement.ObStatement.doExecute(ObStatement.java:558) ~[oceanbase-connector-java-2.0.8.20180730.jar:na] + ... 8 common frames omitted +``` +可以看到,异常是由于没有insert权限(INSERT command denied to user 'dwexp'@'%' for table)引起的。 + +关键字:INSERT command denied to user 'dwexp'@'%' +可以看到这个错误是由于没有写入权限导致的,因此在observer的log、obproxy的log中都没有相关的信息。 +#### 解决方案 +在ob中给相关用户授权之后,任务重试即可成功。 + +参考授权命令为: +```sql +grant select, insert, update on dbName.tableName to dwexp; +grant select on oceanbase.gv$memstore to dwexp; +``` diff --git a/oceanbasev10writer/pom.xml b/oceanbasev10writer/pom.xml new file mode 100644 index 0000000000..d19864012f --- /dev/null +++ b/oceanbasev10writer/pom.xml @@ -0,0 +1,311 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + + oceanbasev10writer + + com.alibaba.datax + 0.0.1-SNAPSHOT + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + guava + com.google.guava + + + com.alibaba + druid + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + org.springframework + spring-test + 4.0.4.RELEASE + test + + + com.alibaba + druid + 1.2.18 + + + + com.alipay.oceanbase + oceanbase-connector-java + 3.2.0 + system + ${basedir}/src/main/libs/oceanbase-connector-java-3.2.0.jar + + + com.alipay.oceanbase + oceanbase-client + + + + + + com.oceanbase + oceanbase-client + 2.4.11 + + + com.google.guava + guava + + + + + + com.oceanbase + shade-ob-partition-calculator + 1.0-SNAPSHOT + system + ${pom.basedir}/src/main/libs/shade-ob-partition-calculator-1.0-SNAPSHOT.jar + + + + com.google.guava + guava + 27.0-jre + + + + log4j + log4j + 1.2.16 + + + org.json + json + 20160810 + + + junit + junit + 4.11 + test + + + com.oceanbase + obkv-table-client + 1.4.0 + + + com.oceanbase + obkv-hbase-client + 2.1.0 + + + com.alibaba + fastjson + + + + org.slf4j + slf4j-api + + + + com.oceanbase + oceanbase-client + + + + com.google.guava + guava + + + commons-lang + commons-lang + + + com.alipay.sofa.common + sofa-common-tools + + + + io.netty + netty-codec-dns + + + io.netty + netty-codec-http + + + io.netty + netty-codec-http2 + + + io.netty + netty-codec-haproxy + + + io.netty + netty-codec-mqtt + + + io.netty + netty-codec-memcache + + + io.netty + netty-codec-redis + + + io.netty + netty-codec-smtp + + + io.netty + netty-codec-socks + + + io.netty + netty-codec-stomp + + + io.netty + netty-codec-xml + + + + io.netty + netty-handler-proxy + + + io.netty + netty-handler-ssl-ocsp + + + + io.netty + netty-resolver-dns + + + io.netty + netty-resolver-dns-classes-macos + + + io.netty + netty-resolver-dns-native-macos + + + + io.netty + netty-transport-rxtx + + + io.netty + netty-transport-udt + + + io.netty + netty-transport-sctp + + + + + com.alipay.sofa.common + sofa-common-tools + 1.3.11 + + + + org.slf4j + slf4j-api + + + + com.google.guava + guava + + + + + com.alibaba + fastjson + 1.2.83 + + + commons-lang + commons-lang + 2.6 + + + mysql + mysql-connector-java + ${mysql.driver.version} + + + + + + + src/main/java + + **/*.properties + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + diff --git a/oceanbasev10writer/src/main/assembly/package.xml b/oceanbasev10writer/src/main/assembly/package.xml new file mode 100644 index 0000000000..559ab5d600 --- /dev/null +++ b/oceanbasev10writer/src/main/assembly/package.xml @@ -0,0 +1,42 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/writer/oceanbasev10writer + + + target/ + + oceanbasev10writer-0.0.1-SNAPSHOT.jar + + plugin/writer/oceanbasev10writer + + + src/main/libs + + *.jar + + plugin/writer/oceanbasev10writer/libs + + + + + + false + plugin/writer/oceanbasev10writer/libs + runtime + + + diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/Config.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/Config.java new file mode 100644 index 0000000000..a90de0a399 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/Config.java @@ -0,0 +1,87 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer; + +public interface Config { + + String MEMSTORE_THRESHOLD = "memstoreThreshold"; + + double DEFAULT_MEMSTORE_THRESHOLD = 0.9d; + + double DEFAULT_SLOW_MEMSTORE_THRESHOLD = 0.75d; + + String MEMSTORE_CHECK_INTERVAL_SECOND = "memstoreCheckIntervalSecond"; + + long DEFAULT_MEMSTORE_CHECK_INTERVAL_SECOND = 30; + + int DEFAULT_BATCH_SIZE = 100; + + int MAX_BATCH_SIZE = 4096; + + String FAIL_TRY_COUNT = "failTryCount"; + + int DEFAULT_FAIL_TRY_COUNT = 10000; + + String WRITER_THREAD_COUNT = "writerThreadCount"; + + int DEFAULT_WRITER_THREAD_COUNT = 1; + + String CONCURRENT_WRITE = "concurrentWrite"; + + boolean DEFAULT_CONCURRENT_WRITE = true; + + String OB_VERSION = "obVersion"; + + String TIMEOUT = "timeout"; + + String PRINT_COST = "printCost"; + + boolean DEFAULT_PRINT_COST = false; + + String COST_BOUND = "costBound"; + + long DEFAULT_COST_BOUND = 20; + + String MAX_ACTIVE_CONNECTION = "maxActiveConnection"; + + int DEFAULT_MAX_ACTIVE_CONNECTION = 2000; + + String WRITER_SUB_TASK_COUNT = "writerSubTaskCount"; + + int DEFAULT_WRITER_SUB_TASK_COUNT = 1; + + int MAX_WRITER_SUB_TASK_COUNT = 4096; + + String OB_WRITE_MODE = "obWriteMode"; + + String OB_COMPATIBLE_MODE = "obCompatibilityMode"; + + String OB_COMPATIBLE_MODE_ORACLE = "ORACLE"; + + String OB_COMPATIBLE_MODE_MYSQL = "MYSQL"; + + String OCJ_GET_CONNECT_TIMEOUT = "ocjGetConnectTimeout"; + + int DEFAULT_OCJ_GET_CONNECT_TIMEOUT = 5000; // 5s + + String OCJ_PROXY_CONNECT_TIMEOUT = "ocjProxyConnectTimeout"; + + int DEFAULT_OCJ_PROXY_CONNECT_TIMEOUT = 5000; // 5s + + String OCJ_CREATE_RESOURCE_TIMEOUT = "ocjCreateResourceTimeout"; + + int DEFAULT_OCJ_CREATE_RESOURCE_TIMEOUT = 60000; // 60s + + String OB_UPDATE_COLUMNS = "obUpdateColumns"; + + String USE_PART_CALCULATOR = "usePartCalculator"; + + boolean DEFAULT_USE_PART_CALCULATOR = false; + + String BLOCKS_COUNT = "blocksCount"; + + String DIRECT_PATH = "directPath"; + + String RPC_PORT = "rpcPort"; + + // 区别于recordLimit,这个参数仅针对某张表。即一张表超过最大错误数不会影响其他表。仅用于旁路导入。 + String MAX_ERRORS = "maxErrors"; +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/OceanBaseV10Writer.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/OceanBaseV10Writer.java new file mode 100644 index 0000000000..06292db512 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/OceanBaseV10Writer.java @@ -0,0 +1,260 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer; + +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter; +import com.alibaba.datax.plugin.rdbms.writer.Constant; +import com.alibaba.datax.plugin.rdbms.writer.Key; +import com.alibaba.datax.plugin.rdbms.writer.util.WriterUtil; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.task.ConcurrentTableWriterTask; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.util.DbUtils; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.util.ObWriterUtils; +import com.alibaba.fastjson2.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.List; + +/** + * 2016-04-07 + *

+ * 专门针对OceanBase1.0的Writer + * + * @author biliang.wbl + * + */ +public class OceanBaseV10Writer extends Writer { + private static DataBaseType DATABASE_TYPE = DataBaseType.OceanBase; + + /** + * Job 中的方法仅执行一次,Task 中方法会由框架启动多个 Task 线程并行执行。 + *

+ * 整个 Writer 执行流程是: + * + *

+	 * Job类init-->prepare-->split
+	 * 
+	 *                          Task类init-->prepare-->startWrite-->post-->destroy
+	 *                          Task类init-->prepare-->startWrite-->post-->destroy
+	 * 
+	 *                                                                            Job类post-->destroy
+	 * 
+ */ + public static class Job extends Writer.Job { + private Configuration originalConfig = null; + private CommonRdbmsWriter.Job commonJob; + private static final Logger LOG = LoggerFactory.getLogger(Job.class); + + /** + * 注意:此方法仅执行一次。 最佳实践:通常在这里对用户的配置进行校验:是否缺失必填项?有无错误值?有没有无关配置项?... + * 并给出清晰的报错/警告提示。校验通常建议采用静态工具类进行,以保证本类结构清晰。 + */ + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + checkCompatibleMode(originalConfig); + //将config中的column和table中的关键字进行转义 + List columns = originalConfig.getList(Key.COLUMN, String.class); + ObWriterUtils.escapeDatabaseKeyword(columns); + originalConfig.set(Key.COLUMN, columns); + + List conns = originalConfig.getList(Constant.CONN_MARK, JSONObject.class); + for (int i = 0; i < conns.size(); i++) { + JSONObject conn = conns.get(i); + Configuration connConfig = Configuration.from(conn.toString()); + List tables = connConfig.getList(Key.TABLE, String.class); + ObWriterUtils.escapeDatabaseKeyword(tables); + originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, i, Key.TABLE), tables); + } + this.commonJob = new CommonRdbmsWriter.Job(DATABASE_TYPE); + this.commonJob.init(this.originalConfig); + } + + /** + * 注意:此方法仅执行一次。 最佳实践:如果 Job 中有需要进行数据同步之前的处理,可以在此处完成,如果没有必要则可以直接去掉。 + */ + // 一般来说,是需要推迟到 task 中进行pre 的执行(单表情况例外) + @Override + public void prepare() { + int tableNumber = originalConfig.getInt(Constant.TABLE_NUMBER_MARK); + if (tableNumber == 1) { + this.commonJob.prepare(this.originalConfig); + final String version = fetchServerVersion(originalConfig); + ObWriterUtils.setObVersion(version); + originalConfig.set(Config.OB_VERSION, version); + } + + String username = originalConfig.getString(Key.USERNAME); + String password = originalConfig.getString(Key.PASSWORD); + + // 获取presql配置,并执行 + List preSqls = originalConfig.getList(Key.PRE_SQL, String.class); + if (preSqls == null || preSqls.size() == 0) { + return; + } + + List conns = originalConfig.getList(Constant.CONN_MARK, Object.class); + for (Object connConfObject : conns) { + Configuration connConf = Configuration.from(connConfObject.toString()); + // 这里的 jdbcUrl 已经 append 了合适后缀参数 + String jdbcUrl = connConf.getString(Key.JDBC_URL); + + List tableList = connConf.getList(Key.TABLE, String.class); + for (String table : tableList) { + List renderedPreSqls = WriterUtil.renderPreOrPostSqls(preSqls, table); + if (null != renderedPreSqls && !renderedPreSqls.isEmpty()) { + Connection conn = DBUtil.getConnection(DATABASE_TYPE, jdbcUrl, username, password); + LOG.info("Begin to execute preSqls:[{}]. context info:{}.", + StringUtils.join(renderedPreSqls, ";"), jdbcUrl); + WriterUtil.executeSqls(conn, renderedPreSqls, jdbcUrl, DATABASE_TYPE); + ObWriterUtils.asyncClose(null, null, conn); + } + } + } + if (LOG.isDebugEnabled()) { + LOG.debug("After job prepare(), originalConfig now is:[\n{}\n]", originalConfig.toJSON()); + } + } + + /** + * 注意:此方法仅执行一次。 最佳实践:通常采用工具静态类完成把 Job 配置切分成多个 Task 配置的工作。 这里的 + * mandatoryNumber 是强制必须切分的份数。 + */ + @Override + public List split(int mandatoryNumber) { + int tableNumber = originalConfig.getInt(Constant.TABLE_NUMBER_MARK); + if (tableNumber == 1) { + return this.commonJob.split(this.originalConfig, mandatoryNumber); + } + Configuration simplifiedConf = this.originalConfig; + + List splitResultConfigs = new ArrayList(); + for (int j = 0; j < mandatoryNumber; j++) { + splitResultConfigs.add(simplifiedConf.clone()); + } + return splitResultConfigs; + } + + /** + * 注意:此方法仅执行一次。 最佳实践:如果 Job 中有需要进行数据同步之后的后续处理,可以在此处完成。 + */ + @Override + public void post() { + int tableNumber = originalConfig.getInt(Constant.TABLE_NUMBER_MARK); + if (tableNumber == 1) { + commonJob.post(this.originalConfig); + return; + } + String username = originalConfig.getString(Key.USERNAME); + String password = originalConfig.getString(Key.PASSWORD); + List conns = originalConfig.getList(Constant.CONN_MARK, Object.class); + List postSqls = originalConfig.getList(Key.POST_SQL, String.class); + if (postSqls == null || postSqls.size() == 0) { + return; + } + + for (Object connConfObject : conns) { + Configuration connConf = Configuration.from(connConfObject.toString()); + String jdbcUrl = connConf.getString(Key.JDBC_URL); + List tableList = connConf.getList(Key.TABLE, String.class); + + for (String table : tableList) { + List renderedPostSqls = WriterUtil.renderPreOrPostSqls(postSqls, table); + if (null != renderedPostSqls && !renderedPostSqls.isEmpty()) { + // 说明有 postSql 配置,则此处删除掉 + Connection conn = DBUtil.getConnection(DATABASE_TYPE, jdbcUrl, username, password); + LOG.info("Begin to execute postSqls:[{}]. context info:{}.", + StringUtils.join(renderedPostSqls, ";"), jdbcUrl); + WriterUtil.executeSqls(conn, renderedPostSqls, jdbcUrl, DATABASE_TYPE); + ObWriterUtils.asyncClose(null, null, conn); + } + } + } + originalConfig.remove(Key.POST_SQL); + } + + /** + * 注意:此方法仅执行一次。 最佳实践:通常配合 Job 中的 post() 方法一起完成 Job 的资源释放。 + */ + @Override + public void destroy() { + this.commonJob.destroy(this.originalConfig); + } + + private String fetchServerVersion(Configuration config) { + final String fetchVersionSql = "show variables like 'version_comment'"; + String versionComment = DbUtils.fetchSingleValueWithRetry(config, fetchVersionSql); + return versionComment.split(" ")[1]; + } + + private void checkCompatibleMode(Configuration configure) { + final String fetchCompatibleModeSql = "SHOW VARIABLES LIKE 'ob_compatibility_mode'"; + String compatibleMode = DbUtils.fetchSingleValueWithRetry(configure, fetchCompatibleModeSql); + ObWriterUtils.setCompatibleMode(compatibleMode); + configure.set(Config.OB_COMPATIBLE_MODE, compatibleMode); + } + } + + public static class Task extends Writer.Task { + private static final Logger LOG = LoggerFactory.getLogger(Task.class); + private Configuration writerSliceConfig; + private CommonRdbmsWriter.Task writerTask; + + /** + * 注意:此方法每个 Task 都会执行一次。 最佳实践:此处通过对 taskConfig 配置的读取,进而初始化一些资源为 + * startWrite()做准备。 + */ + @Override + public void init() { + this.writerSliceConfig = super.getPluginJobConf(); + int tableNumber = writerSliceConfig.getInt(Constant.TABLE_NUMBER_MARK); + if (tableNumber == 1) { + // always use concurrentTableWriter + this.writerTask = new ConcurrentTableWriterTask(DATABASE_TYPE); + } else { + throw new RuntimeException("writing to multi-tables is not supported."); + } + LOG.info("tableNumber:" + tableNumber + ",writerTask Class:" + writerTask.getClass().getName()); + this.writerTask.init(this.writerSliceConfig); + } + + /** + * 注意:此方法每个 Task 都会执行一次。 最佳实践:如果 Task + * 中有需要进行数据同步之前的处理,可以在此处完成,如果没有必要则可以直接去掉。 + */ + @Override + public void prepare() { + this.writerTask.prepare(this.writerSliceConfig); + } + + /** + * 注意:此方法每个 Task 都会执行一次。 最佳实践:此处适当封装确保简洁清晰完成数据写入工作。 + */ + @Override + public void startWrite(RecordReceiver recordReceiver) { + this.writerTask.startWrite(recordReceiver, this.writerSliceConfig, super.getTaskPluginCollector()); + } + + /** + * 注意:此方法每个 Task 都会执行一次。 最佳实践:如果 Task 中有需要进行数据同步之后的后续处理,可以在此处完成。 + */ + @Override + public void post() { + this.writerTask.post(this.writerSliceConfig); + } + + /** + * 注意:此方法每个 Task 都会执行一次。 最佳实践:通常配合Task 中的 post() 方法一起完成 Task 的资源释放。 + */ + @Override + public void destroy() { + this.writerTask.destroy(this.writerSliceConfig); + } + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/common/Table.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/common/Table.java new file mode 100644 index 0000000000..53c7025432 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/common/Table.java @@ -0,0 +1,88 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.common; + +import java.util.Objects; + +public class Table { + private String tableName; + private String dbName; + private Throwable error; + private Status status; + + public Table(String dbName, String tableName) { + this.dbName = dbName; + this.tableName = tableName; + this.status = Status.INITIAL; + } + + public Throwable getError() { + return error; + } + + public void setError(Throwable error) { + this.error = error; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Table table = (Table) o; + return tableName.equals(table.tableName) && dbName.equals(table.dbName); + } + + @Override + public int hashCode() { + return Objects.hash(tableName, dbName); + } + + public enum Status { + /** + * + */ + INITIAL(0), + + /** + * + */ + RUNNING(1), + + /** + * + */ + FAILURE(2), + + /** + * + */ + SUCCESS(3); + + private int code; + + /** + * @param code + */ + private Status(int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + } +} \ No newline at end of file diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/common/TableCache.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/common/TableCache.java new file mode 100644 index 0000000000..c59dca0653 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/common/TableCache.java @@ -0,0 +1,21 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.common; + +import java.util.concurrent.ConcurrentHashMap; + +public class TableCache { + private static final TableCache INSTANCE = new TableCache(); + private final ConcurrentHashMap TABLE_CACHE; + + private TableCache() { + TABLE_CACHE = new ConcurrentHashMap<>(); + } + + public static TableCache getInstance() { + return INSTANCE; + } + + public Table getTable(String dbName, String tableName) { + String fullTableName = String.join("-", dbName, tableName); + return TABLE_CACHE.computeIfAbsent(fullTableName, (k) -> new Table(dbName, tableName)); + } +} \ No newline at end of file diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/AbstractRestrictedConnection.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/AbstractRestrictedConnection.java new file mode 100644 index 0000000000..717e2e36fa --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/AbstractRestrictedConnection.java @@ -0,0 +1,257 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.directPath; + +import java.sql.Array; +import java.sql.Blob; +import java.sql.CallableStatement; +import java.sql.Clob; +import java.sql.DatabaseMetaData; +import java.sql.NClob; +import java.sql.PreparedStatement; +import java.sql.SQLClientInfoException; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Savepoint; +import java.sql.Statement; +import java.sql.Struct; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.Executor; + +public abstract class AbstractRestrictedConnection implements java.sql.Connection { + + @Override + public CallableStatement prepareCall(String sql) throws SQLException { + throw new UnsupportedOperationException("prepareCall(String) is unsupported"); + } + + @Override + public String nativeSQL(String sql) throws SQLException { + throw new UnsupportedOperationException("nativeSQL(String) is unsupported"); + } + + @Override + public void setAutoCommit(boolean autoCommit) throws SQLException { + throw new UnsupportedOperationException("setAutoCommit(boolean) is unsupported"); + } + + @Override + public boolean getAutoCommit() throws SQLException { + throw new UnsupportedOperationException("getAutoCommit is unsupported"); + } + + @Override + public void abort(Executor executor) throws SQLException { + throw new UnsupportedOperationException("abort(Executor) is unsupported"); + } + + @Override + public void setNetworkTimeout(Executor executor, int milliseconds) throws SQLException { + throw new UnsupportedOperationException("setNetworkTimeout(Executor, int) is unsupported"); + } + + @Override + public int getNetworkTimeout() throws SQLException { + throw new UnsupportedOperationException("getNetworkTimeout is unsupported"); + } + + @Override + public DatabaseMetaData getMetaData() throws SQLException { + throw new UnsupportedOperationException("getMetaData is unsupported"); + } + + @Override + public void setReadOnly(boolean readOnly) throws SQLException { + throw new UnsupportedOperationException("setReadOnly(boolean) is unsupported"); + } + + @Override + public boolean isReadOnly() throws SQLException { + throw new UnsupportedOperationException("isReadOnly is unsupported"); + } + + @Override + public void setCatalog(String catalog) throws SQLException { + throw new UnsupportedOperationException("setCatalog(String) is unsupported"); + } + + @Override + public String getCatalog() throws SQLException { + throw new UnsupportedOperationException("getCatalog is unsupported"); + } + + @Override + public void setTransactionIsolation(int level) throws SQLException { + throw new UnsupportedOperationException("setTransactionIsolation(int) is unsupported"); + } + + @Override + public int getTransactionIsolation() throws SQLException { + throw new UnsupportedOperationException("getTransactionIsolation is unsupported"); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + throw new UnsupportedOperationException("getWarnings is unsupported"); + } + + @Override + public void clearWarnings() throws SQLException { + throw new UnsupportedOperationException("clearWarnings is unsupported"); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency) throws SQLException { + throw new UnsupportedOperationException("createStatement(int, int) is unsupported"); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + throw new UnsupportedOperationException("prepareStatement(String, int, int) is unsupported"); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency) throws SQLException { + throw new UnsupportedOperationException("prepareCall(String, int, int) is unsupported"); + } + + @Override + public Map> getTypeMap() throws SQLException { + throw new UnsupportedOperationException("getTypeMap is unsupported"); + } + + @Override + public void setTypeMap(Map> map) throws SQLException { + throw new UnsupportedOperationException("setTypeMap(Map>) is unsupported"); + } + + @Override + public void setHoldability(int holdability) throws SQLException { + throw new UnsupportedOperationException("setHoldability is unsupported"); + } + + @Override + public int getHoldability() throws SQLException { + throw new UnsupportedOperationException("getHoldability is unsupported"); + } + + @Override + public Savepoint setSavepoint() throws SQLException { + throw new UnsupportedOperationException("setSavepoint is unsupported"); + } + + @Override + public Savepoint setSavepoint(String name) throws SQLException { + throw new UnsupportedOperationException("setSavepoint(String) is unsupported"); + } + + @Override + public void rollback(Savepoint savepoint) throws SQLException { + throw new UnsupportedOperationException("rollback(Savepoint) is unsupported"); + } + + @Override + public void releaseSavepoint(Savepoint savepoint) throws SQLException { + throw new UnsupportedOperationException("releaseSavepoint(Savepoint) is unsupported"); + } + + @Override + public Statement createStatement(int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + throw new UnsupportedOperationException("createStatement(int, int, int) is unsupported"); + } + + @Override + public PreparedStatement prepareStatement(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + throw new UnsupportedOperationException("prepareStatement(String, int, int, int) is unsupported"); + } + + @Override + public CallableStatement prepareCall(String sql, int resultSetType, int resultSetConcurrency, int resultSetHoldability) throws SQLException { + throw new UnsupportedOperationException("prepareCall(String, int, int, int) is unsupported"); + } + + @Override + public PreparedStatement prepareStatement(String sql, int autoGeneratedKeys) throws SQLException { + throw new UnsupportedOperationException("prepareStatement(String, int) is unsupported"); + } + + @Override + public PreparedStatement prepareStatement(String sql, int[] columnIndexes) throws SQLException { + throw new UnsupportedOperationException("prepareStatement(String, int[]) is unsupported"); + } + + @Override + public PreparedStatement prepareStatement(String sql, String[] columnNames) throws SQLException { + throw new UnsupportedOperationException("prepareStatement(String, String[]) is unsupported"); + } + + @Override + public Clob createClob() throws SQLException { + throw new UnsupportedOperationException("createClob is unsupported"); + } + + @Override + public Blob createBlob() throws SQLException { + throw new UnsupportedOperationException("createBlob is unsupported"); + } + + @Override + public NClob createNClob() throws SQLException { + throw new UnsupportedOperationException("createNClob is unsupported"); + } + + @Override + public SQLXML createSQLXML() throws SQLException { + throw new UnsupportedOperationException("createSQLXML is unsupported"); + } + + @Override + public boolean isValid(int timeout) throws SQLException { + throw new UnsupportedOperationException("isValid(int) is unsupported"); + } + + @Override + public void setClientInfo(String name, String value) throws SQLClientInfoException { + throw new UnsupportedOperationException("setClientInfo(String, String) is unsupported"); + } + + @Override + public void setClientInfo(Properties properties) throws SQLClientInfoException { + throw new UnsupportedOperationException("setClientInfo(Properties) is unsupported"); + } + + @Override + public String getClientInfo(String name) throws SQLException { + throw new UnsupportedOperationException("getClientInfo(String) is unsupported"); + } + + @Override + public Properties getClientInfo() throws SQLException { + throw new UnsupportedOperationException("getClientInfo is unsupported"); + } + + @Override + public Array createArrayOf(String typeName, Object[] elements) throws SQLException { + throw new UnsupportedOperationException("createArrayOf(String, Object[]) is unsupported"); + } + + @Override + public Struct createStruct(String typeName, Object[] attributes) throws SQLException { + throw new UnsupportedOperationException("createStruct(String, Object[]) is unsupported"); + } + + @Override + public void setSchema(String schema) throws SQLException { + throw new UnsupportedOperationException("setSchema(String) is unsupported"); + } + + @Override + public T unwrap(Class iface) throws SQLException { + throw new UnsupportedOperationException("unwrap(Class) is unsupported"); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + throw new UnsupportedOperationException("isWrapperFor(Class) is unsupported"); + } +} \ No newline at end of file diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/AbstractRestrictedPreparedStatement.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/AbstractRestrictedPreparedStatement.java new file mode 100644 index 0000000000..47b2c98ba1 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/AbstractRestrictedPreparedStatement.java @@ -0,0 +1,663 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.directPath; + +import java.io.InputStream; +import java.io.Reader; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.net.URL; +import java.nio.charset.Charset; +import java.sql.Array; +import java.sql.Blob; +import java.sql.Clob; +import java.sql.Date; +import java.sql.NClob; +import java.sql.ParameterMetaData; +import java.sql.Ref; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.RowId; +import java.sql.SQLException; +import java.sql.SQLWarning; +import java.sql.SQLXML; +import java.sql.Time; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.OffsetTime; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.List; + +import com.alipay.oceanbase.rpc.protocol.payload.impl.ObObj; +import com.alipay.oceanbase.rpc.protocol.payload.impl.ObObjType; +import com.alipay.oceanbase.rpc.util.ObVString; +import org.apache.commons.io.IOUtils; + +public abstract class AbstractRestrictedPreparedStatement implements java.sql.PreparedStatement { + + private boolean closed; + + @Override + public void setNull(int parameterIndex, int sqlType) throws SQLException { + this.setParameter(parameterIndex, createObObj(null)); + } + + @Override + public void setNull(int parameterIndex, int sqlType, String typeName) throws SQLException { + throw new UnsupportedOperationException("setNull(int, int, String) is unsupported"); + } + + @Override + public void setBoolean(int parameterIndex, boolean x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setByte(int parameterIndex, byte x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setShort(int parameterIndex, short x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setInt(int parameterIndex, int x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setLong(int parameterIndex, long x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setFloat(int parameterIndex, float x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setDouble(int parameterIndex, double x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setBigDecimal(int parameterIndex, BigDecimal x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setString(int parameterIndex, String x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setBytes(int parameterIndex, byte[] x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setDate(int parameterIndex, Date x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setDate(int parameterIndex, Date x, Calendar cal) throws SQLException { + throw new UnsupportedOperationException("setDate(int, Date, Calendar) is unsupported"); + } + + @Override + public void setTime(int parameterIndex, Time x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setTime(int parameterIndex, Time x, Calendar cal) throws SQLException { + throw new UnsupportedOperationException("setTime(int, Time, Calendar) is unsupported"); + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setTimestamp(int parameterIndex, Timestamp x, Calendar cal) throws SQLException { + throw new UnsupportedOperationException("setTimestamp(int, Timestamp, Calendar) is unsupported"); + } + + @Override + public void setObject(int parameterIndex, Object x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType) throws SQLException { + throw new UnsupportedOperationException("setObject(int, Object, int) is unsupported"); + } + + @Override + public void setObject(int parameterIndex, Object x, int targetSqlType, int scaleOrLength) throws SQLException { + throw new UnsupportedOperationException("setObject(int, Object, int, int) is unsupported"); + } + + @Override + public void setRef(int parameterIndex, Ref x) throws SQLException { + throw new UnsupportedOperationException("setRef(int, Ref) is unsupported"); + } + + @Override + public void setArray(int parameterIndex, Array x) throws SQLException { + throw new UnsupportedOperationException("setArray(int, Array) is unsupported"); + } + + @Override + public void setSQLXML(int parameterIndex, SQLXML xmlObject) throws SQLException { + throw new UnsupportedOperationException("setSQLXML(int, SQLXML) is unsupported"); + } + + @Override + public void setURL(int parameterIndex, URL x) throws SQLException { + // if (x == null) { + // this.setParameter(parameterIndex, createObObj(x)); + // } else { + // // TODO If need BackslashEscapes and character encoding ? + // this.setParameter(parameterIndex, createObObj(x.toString())); + // } + throw new UnsupportedOperationException("setURL(int, URL) is unsupported"); + } + + @Override + public void setRowId(int parameterIndex, RowId x) throws SQLException { + throw new UnsupportedOperationException("setRowId(int, RowId) is unsupported"); + } + + @Override + public void setNString(int parameterIndex, String value) throws SQLException { + this.setParameter(parameterIndex, createObObj(value)); + } + + @Override + public void setBlob(int parameterIndex, Blob x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setBlob(int parameterIndex, InputStream x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setBlob(int parameterIndex, InputStream x, long length) throws SQLException { + throw new UnsupportedOperationException("setBlob(int, InputStream, length) is unsupported"); + } + + @Override + public void setClob(int parameterIndex, Clob x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setClob(int parameterIndex, Reader x) throws SQLException { + this.setCharacterStream(parameterIndex, x); + } + + @Override + public void setClob(int parameterIndex, Reader x, long length) throws SQLException { + throw new UnsupportedOperationException("setClob(int, Reader, length) is unsupported"); + } + + @Override + public void setNClob(int parameterIndex, NClob x) throws SQLException { + this.setClob(parameterIndex, (Clob) (x)); + } + + @Override + public void setNClob(int parameterIndex, Reader x) throws SQLException { + this.setClob(parameterIndex, x); + } + + @Override + public void setNClob(int parameterIndex, Reader x, long length) throws SQLException { + throw new UnsupportedOperationException("setNClob(int, Reader, length) is unsupported"); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Deprecated + @Override + public void setUnicodeStream(int parameterIndex, InputStream x, int length) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException { + throw new UnsupportedOperationException("setAsciiStream(int, InputStream, length) is unsupported"); + } + + @Override + public void setAsciiStream(int parameterIndex, InputStream x, long length) throws SQLException { + throw new UnsupportedOperationException("setAsciiStream(int, InputStream, length) is unsupported"); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException { + throw new UnsupportedOperationException("setBinaryStream(int, InputStream, length) is unsupported"); + } + + @Override + public void setBinaryStream(int parameterIndex, InputStream x, long length) throws SQLException { + throw new UnsupportedOperationException("setBinaryStream(int, InputStream, length) is unsupported"); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader x, int length) throws SQLException { + throw new UnsupportedOperationException("setCharacterStream(int, InputStream, length) is unsupported"); + } + + @Override + public void setCharacterStream(int parameterIndex, Reader x, long length) throws SQLException { + throw new UnsupportedOperationException("setCharacterStream(int, InputStream, length) is unsupported"); + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader x) throws SQLException { + this.setParameter(parameterIndex, createObObj(x)); + } + + @Override + public void setNCharacterStream(int parameterIndex, Reader x, long length) throws SQLException { + throw new UnsupportedOperationException("setNCharacterStream(int, InputStream, length) is unsupported"); + } + + /** + * @return boolean + */ + protected abstract boolean isOracleMode(); + + /** + * Set parameter to the target position. + * + * @param parameterIndex + * @param obObj + * @throws SQLException + */ + protected abstract void setParameter(int parameterIndex, ObObj obObj) throws SQLException; + + /** + * Close the current prepared statement. + * + * @throws SQLException + */ + @Override + public void close() throws SQLException { + this.closed = true; + } + + /** + * Return whether the current prepared statement is closed? + * + * @return boolean + * @throws SQLException + */ + @Override + public boolean isClosed() throws SQLException { + return this.closed; + } + + /** + * Create a {@link ObObj } array with input values. + * + * @param values Original row value + * @return ObObj[] + */ + public ObObj[] createObObjArray(Object[] values) { + if (values == null) { + return null; + } + ObObj[] array = new ObObj[values.length]; + for (int i = 0; i < values.length; i++) { + array[i] = createObObj(values[i]); + } + return array; + } + + /** + * Create a {@link ObObj } array with input values. + * + * @param values Original row value + * @return ObObj[] + */ + public ObObj[] createObObjArray(List values) { + if (values == null) { + return null; + } + ObObj[] array = new ObObj[values.size()]; + for (int i = 0; i < values.size(); i++) { + array[i] = createObObj(values.get(i)); + } + return array; + } + + /** + * Create a {@link ObObj } instance. + * + * @param value Original column value + * @return ObObj + */ + public ObObj createObObj(Object value) { + try { + // Only used for strongly typed declared variables + Object convertedValue = value == null ? null : convertValue(value); + return new ObObj(ObObjType.defaultObjMeta(convertedValue), convertedValue); + } catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + + /** + * Some values with data type is unsupported by ObObjType#valueOfType. + * We should convert the input value to supported value data type. + * + * @param value + * @return Object + * @throws Exception + */ + public static Object convertValue(Object value) throws Exception { + if (value instanceof BigDecimal) { + return value.toString(); + } else if (value instanceof BigInteger) { + return value.toString(); + } else if (value instanceof Instant) { + return Timestamp.from(((Instant) value)); + } else if (value instanceof LocalDate) { + // Warn: java.sql.Date.valueOf() is deprecated. As local zone is used. + return Date.valueOf(((LocalDate) value)); + } else if (value instanceof LocalTime) { + // Warn: java.sql.Time.valueOf() is deprecated. + Time t = Time.valueOf((LocalTime) value); + return new Timestamp(t.getTime()); + } else if (value instanceof LocalDateTime) { + return Timestamp.valueOf(((LocalDateTime) value)); + } else if (value instanceof OffsetDateTime) { + return Timestamp.from(((OffsetDateTime) value).toInstant()); + } else if (value instanceof Time) { + return new Timestamp(((Time) value).getTime()); + } else if (value instanceof ZonedDateTime) { + // Note: Be care of time zone!!! + return Timestamp.from(((ZonedDateTime) value).toInstant()); + } else if (value instanceof OffsetTime) { + LocalTime lt = ((OffsetTime) value).toLocalTime(); + // Warn: java.sql.Time.valueOf() is deprecated. + return new Timestamp(Time.valueOf(lt).getTime()); + } else if (value instanceof InputStream) { + try (InputStream is = ((InputStream) value)) { + // Note: Be care of character set!!! + return new ObVString(IOUtils.toString(is, Charset.defaultCharset())); + } + } else if (value instanceof Blob) { + Blob b = (Blob) value; + try (InputStream is = b.getBinaryStream()) { + if (is == null) { + return null; + } + // Note: Be care of character set!!! + return new ObVString(IOUtils.toString(is, Charset.defaultCharset())); + } finally { + b.free(); + } + } else if (value instanceof Reader) { + try (Reader r = ((Reader) value)) { + return IOUtils.toString(r); + } + } else if (value instanceof Clob) { + Clob c = (Clob) value; + try (Reader r = c.getCharacterStream()) { + return r == null ? null : IOUtils.toString(r); + } finally { + c.free(); + } + } else { + return value; + } + } + + // *********************************************************************************** // + + @Override + public boolean getMoreResults(int current) throws SQLException { + throw new UnsupportedOperationException("getMoreResults(int) is unsupported"); + } + + @Override + public ResultSet getGeneratedKeys() throws SQLException { + throw new UnsupportedOperationException("getGeneratedKeys is unsupported"); + } + + @Override + public int executeUpdate(String sql, int autoGeneratedKeys) throws SQLException { + throw new UnsupportedOperationException("executeUpdate(String, int) is unsupported"); + } + + @Override + public int executeUpdate(String sql, int[] columnIndexes) throws SQLException { + throw new UnsupportedOperationException("executeUpdate(String, int[]) is unsupported"); + } + + @Override + public int executeUpdate(String sql, String[] columnNames) throws SQLException { + throw new UnsupportedOperationException("executeUpdate(String, String[]) is unsupported"); + } + + @Override + public boolean execute(String sql, int autoGeneratedKeys) throws SQLException { + throw new UnsupportedOperationException("execute(String, int) is unsupported"); + } + + @Override + public boolean execute(String sql, int[] columnIndexes) throws SQLException { + throw new UnsupportedOperationException("execute(String, int[]) is unsupported"); + } + + @Override + public boolean execute(String sql, String[] columnNames) throws SQLException { + throw new UnsupportedOperationException("execute(String, String[]) is unsupported"); + } + + @Override + public int getResultSetHoldability() throws SQLException { + throw new UnsupportedOperationException("getResultSetHoldability is unsupported"); + } + + @Override + public void setPoolable(boolean poolable) throws SQLException { + throw new UnsupportedOperationException("setPoolable(boolean) is unsupported"); + } + + @Override + public boolean isPoolable() throws SQLException { + throw new UnsupportedOperationException("isPoolable is unsupported"); + } + + @Override + public void closeOnCompletion() throws SQLException { + throw new UnsupportedOperationException("closeOnCompletion is unsupported"); + } + + @Override + public boolean isCloseOnCompletion() throws SQLException { + throw new UnsupportedOperationException("isCloseOnCompletion is unsupported"); + } + + @Override + public ResultSet executeQuery(String sql) throws SQLException { + throw new UnsupportedOperationException("executeQuery(String) is unsupported"); + } + + @Override + public int executeUpdate(String sql) throws SQLException { + throw new UnsupportedOperationException("executeUpdate(String) is unsupported"); + } + + @Override + public int getMaxFieldSize() throws SQLException { + throw new UnsupportedOperationException("getMaxFieldSize is unsupported"); + } + + @Override + public void setMaxFieldSize(int max) throws SQLException { + throw new UnsupportedOperationException("setMaxFieldSize(int) is unsupported"); + } + + @Override + public int getMaxRows() throws SQLException { + throw new UnsupportedOperationException("getMaxRows is unsupported"); + } + + @Override + public void setMaxRows(int max) throws SQLException { + throw new UnsupportedOperationException("setMaxRows(int) is unsupported"); + } + + @Override + public void setEscapeProcessing(boolean enable) throws SQLException { + throw new UnsupportedOperationException("setEscapeProcessing(boolean) is unsupported"); + } + + @Override + public int getQueryTimeout() throws SQLException { + throw new UnsupportedOperationException("getQueryTimeout is unsupported"); + } + + @Override + public void setQueryTimeout(int seconds) throws SQLException { + throw new UnsupportedOperationException("setQueryTimeout(int) is unsupported"); + } + + @Override + public void cancel() throws SQLException { + throw new UnsupportedOperationException("cancel is unsupported"); + } + + @Override + public SQLWarning getWarnings() throws SQLException { + throw new UnsupportedOperationException("getWarnings is unsupported"); + } + + @Override + public void clearWarnings() throws SQLException { + throw new UnsupportedOperationException("clearWarnings is unsupported"); + } + + @Override + public void setCursorName(String name) throws SQLException { + throw new UnsupportedOperationException("setCursorName(String) is unsupported"); + } + + @Override + public boolean execute(String sql) throws SQLException { + throw new UnsupportedOperationException("execute(String) is unsupported"); + } + + @Override + public ResultSet getResultSet() throws SQLException { + throw new UnsupportedOperationException("getResultSet is unsupported"); + } + + @Override + public int getUpdateCount() throws SQLException { + throw new UnsupportedOperationException("getUpdateCount is unsupported"); + } + + @Override + public boolean getMoreResults() throws SQLException { + throw new UnsupportedOperationException("getMoreResults is unsupported"); + } + + @Override + public void setFetchDirection(int direction) throws SQLException { + throw new UnsupportedOperationException("setFetchDirection(int) is unsupported"); + } + + @Override + public int getFetchDirection() throws SQLException { + throw new UnsupportedOperationException("getFetchDirection is unsupported"); + } + + @Override + public void setFetchSize(int rows) throws SQLException { + throw new UnsupportedOperationException("setFetchSize(int) is unsupported"); + } + + @Override + public int getFetchSize() throws SQLException { + throw new UnsupportedOperationException("getFetchSize is unsupported"); + } + + @Override + public int getResultSetConcurrency() throws SQLException { + throw new UnsupportedOperationException("getResultSetConcurrency is unsupported"); + } + + @Override + public int getResultSetType() throws SQLException { + throw new UnsupportedOperationException("getResultSetType is unsupported"); + } + + @Override + public void addBatch(String sql) throws SQLException { + throw new UnsupportedOperationException("addBatch(String) is unsupported"); + } + + @Override + public ResultSet executeQuery() throws SQLException { + throw new UnsupportedOperationException("executeQuery is unsupported"); + } + + @Override + public int executeUpdate() throws SQLException { + throw new UnsupportedOperationException("executeUpdate is unsupported"); + } + + @Override + public boolean execute() throws SQLException { + throw new UnsupportedOperationException("execute is unsupported"); + } + + @Override + public ParameterMetaData getParameterMetaData() throws SQLException { + throw new UnsupportedOperationException("getParameterMetaData is unsupported"); + } + + @Override + public ResultSetMetaData getMetaData() throws SQLException { + throw new UnsupportedOperationException("getMetaData is unsupported"); + } + + @Override + public T unwrap(Class iface) throws SQLException { + throw new UnsupportedOperationException("isWrapperFor(Class) is unsupported"); + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + throw new UnsupportedOperationException("isWrapperFor(Class) is unsupported"); + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/DirectLoaderBuilder.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/DirectLoaderBuilder.java new file mode 100644 index 0000000000..22664947b0 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/DirectLoaderBuilder.java @@ -0,0 +1,170 @@ +/* + * Copyright 2024 OceanBase. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.datax.plugin.writer.oceanbasev10writer.directPath; + +import java.io.Serializable; + +import com.alipay.oceanbase.rpc.direct_load.ObDirectLoadConnection; +import com.alipay.oceanbase.rpc.direct_load.ObDirectLoadManager; +import com.alipay.oceanbase.rpc.direct_load.ObDirectLoadStatement; +import com.alipay.oceanbase.rpc.direct_load.exception.ObDirectLoadException; +import com.alipay.oceanbase.rpc.exception.ObTableException; +import com.alipay.oceanbase.rpc.protocol.payload.impl.ObLoadDupActionType; +import org.apache.commons.lang.ObjectUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The builder for {@link ObTableDirectLoad}. + */ +public class DirectLoaderBuilder implements Serializable { + + private static final Logger log = LoggerFactory.getLogger(DirectLoaderBuilder.class); + private String host; + private int port; + + private String user; + private String tenant; + private String password; + + private String schema; + private String table; + + /** + * Server-side parallelism. + */ + private int parallel; + + private long maxErrorCount; + + private ObLoadDupActionType duplicateKeyAction; + + /** + * The overall timeout of the direct load task + */ + private Long timeout; + + private Long heartBeatTimeout; + + private Long heartBeatInterval; + + public DirectLoaderBuilder host(String host) { + this.host = host; + return this; + } + + public DirectLoaderBuilder port(int port) { + this.port = port; + return this; + } + + public DirectLoaderBuilder user(String user) { + //1.4.0的obkv版本只需要用户名称,不能带租户和集群信息 + int indexOf = user.indexOf("@"); + this.user = user; + if (indexOf > 0) { + this.user = user.substring(0, indexOf); + } + return this; + } + + public DirectLoaderBuilder tenant(String tenant) { + this.tenant = tenant; + return this; + } + + public DirectLoaderBuilder password(String password) { + this.password = password; + return this; + } + + public DirectLoaderBuilder schema(String schema) { + this.schema = schema; + return this; + } + + public DirectLoaderBuilder table(String table) { + this.table = table; + return this; + } + + public DirectLoaderBuilder parallel(int parallel) { + this.parallel = parallel; + return this; + } + + public DirectLoaderBuilder maxErrorCount(long maxErrorCount) { + this.maxErrorCount = maxErrorCount; + return this; + } + + public DirectLoaderBuilder duplicateKeyAction(ObLoadDupActionType duplicateKeyAction) { + this.duplicateKeyAction = duplicateKeyAction; + return this; + } + + public DirectLoaderBuilder timeout(long timeout) { + this.timeout = timeout; + return this; + } + + public DirectLoaderBuilder heartBeatTimeout(Long heartBeatTimeout) { + this.heartBeatTimeout = heartBeatTimeout; + return this; + } + + public DirectLoaderBuilder heartBeatInterval(Long heartBeatInterval) { + this.heartBeatInterval = heartBeatInterval; + return this; + } + + public ObTableDirectLoad build() { + try { + ObDirectLoadConnection obDirectLoadConnection = buildConnection(parallel); + ObDirectLoadStatement obDirectLoadStatement = buildStatement(obDirectLoadConnection); + return new ObTableDirectLoad(schema, table, obDirectLoadStatement, obDirectLoadConnection); + } catch (ObDirectLoadException e) { + throw new ObTableException(e.getMessage(), e); + } + } + + private ObDirectLoadConnection buildConnection(int writeThreadNum) throws ObDirectLoadException { + if (heartBeatTimeout == null || heartBeatInterval == null) { + throw new IllegalArgumentException("heartBeatTimeout and heartBeatInterval must not be null"); + } + ObDirectLoadConnection build = ObDirectLoadManager.getConnectionBuilder() + .setServerInfo(host, port) + .setLoginInfo(tenant, user, password, schema) + .setHeartBeatInfo(heartBeatTimeout, heartBeatInterval) + .enableParallelWrite(writeThreadNum) + .build(); + log.info("ObDirectLoadConnection value is:{}", ObjectUtils.toString(build)); + return build; + } + + private ObDirectLoadStatement buildStatement(ObDirectLoadConnection connection) throws ObDirectLoadException { + ObDirectLoadStatement build = connection.getStatementBuilder() + .setTableName(table) + .setParallel(parallel) + .setQueryTimeout(timeout) + .setDupAction(duplicateKeyAction) + .setMaxErrorRowCount(maxErrorCount) + .build(); + log.info("ObDirectLoadStatement value is:{}", ObjectUtils.toString(build)); + return build; + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/DirectPathConnection.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/DirectPathConnection.java new file mode 100644 index 0000000000..ce7ef7e3c2 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/DirectPathConnection.java @@ -0,0 +1,398 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.directPath; + +import java.sql.SQLException; +import java.util.Arrays; + +import com.alibaba.datax.common.util.Configuration; + +import com.alipay.oceanbase.rpc.direct_load.ObDirectLoadBucket; +import com.alipay.oceanbase.rpc.protocol.payload.impl.ObLoadDupActionType; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.google.common.base.Preconditions.checkArgument; + +public class DirectPathConnection extends AbstractRestrictedConnection { + + private static final int OB_DIRECT_PATH_DEFAULT_BLOCKS = 1; + private static final long OB_DIRECT_PATH_HEART_BEAT_TIMEOUT = 60000; + private static final long OB_DIRECT_PATH_HEART_BEAT_INTERVAL = 10000; + private static final int DEFAULT_BUFFERSIZE = 1048576; + private final Configuration configuration; + + private State state; + private int commiters; + + private final int blocks; + private final ObTableDirectLoad load; + private final Object lock = new Object(); + + private static final Logger log = LoggerFactory.getLogger(DirectPathConnection.class); + + /** + * Construct a new instance. + * + * @param load + * @param blocks + */ + private DirectPathConnection(ObTableDirectLoad load, int blocks, Configuration configuration) { + this.configuration = configuration; + this.load = load; + this.blocks = blocks; + } + + /** + * Begin a new {@link DirectPathConnection } + * + * @return DirectPathConnection + * @throws SQLException + */ + public DirectPathConnection begin() throws SQLException { + synchronized (lock) { + if (state == null || state == State.CLOSED) { + try { + this.load.begin(); + this.state = State.BEGIN; + } catch (Exception ex) { + throw new SQLException(ex); + } + } else { + throw new IllegalStateException("Begin transaction failed as connection state is already BEGIN"); + } + } + return this; + } + + /** + * Commit buffered data with MAXIMUM timeout. + * + * @throws SQLException + */ + @Override + public void commit() throws SQLException { + synchronized (lock) { + if (state == State.BEGIN) { + this.commiters++; + if (commiters == blocks) { + try { + this.load.commit(); + state = State.FINISHED; + } catch (Exception ex) { + throw new SQLException(ex); + } + } else if (commiters > blocks) { + throw new IllegalStateException("Your commit have exceed the limit. (" + commiters + ">" + blocks + ")"); + } + } else { + throw new IllegalStateException("Commit transaction failed as connection state is not BEGIN"); + } + } + } + + /** + * Rollback if error occurred. + * + * @throws SQLException + */ + @Override + public void rollback() throws SQLException { + synchronized (lock) { + if (state == State.BEGIN) { + try { + //obkv-table-client-2.1.0的close方法包含回滚逻辑 + this.load.close(); + } catch (Exception ex) { + throw new SQLException(ex); + } + } else { + throw new IllegalStateException("Rollback transaction failed as connection state is not BEGIN"); + } + } + } + + /** + * Close this connection. + */ + @Override + public void close() { + synchronized (lock) { + // Closed only if state is BEGIN + this.load.close(); + this.state = State.CLOSED; + } + } + + /** + * @return DirectPathPreparedStatement + */ + @Override + public DirectPathPreparedStatement createStatement() throws SQLException { + return this.prepareStatement(null); + } + + /** + * A new batch need create a new {@link DirectPathPreparedStatement }. + * The {@link DirectPathPreparedStatement } can not be reuse, otherwise it may cause duplicate records. + * + * @return DirectPathStatement + */ + @Override + public DirectPathPreparedStatement prepareStatement(String sql) throws SQLException { + if (state == State.BEGIN) { + Integer bufferSize = configuration.getInt(DirectPathConstants.BUFFERSIZE, DEFAULT_BUFFERSIZE); + log.info("The current bufferSize size is{}", bufferSize); + return new DirectPathPreparedStatement(this, bufferSize); + } else { + throw new IllegalStateException("Create statement failed as connection state is not BEGIN"); + } + } + + /** + * Return the schema name of this connection instance. + * + * @return String + */ + @Override + public String getSchema() { + if (state == State.BEGIN) { + return this.load.getTable().getDatabase(); + } else { + throw new IllegalStateException("Get schema failed as connection state is not BEGIN"); + } + } + + /** + * Return the table name of this connection instance. + * + * @return String + */ + public String getTableName() { + if (state == State.BEGIN) { + return this.load.getTableName(); + } else { + throw new IllegalStateException("Get table failed as connection state is not BEGIN"); + } + } + + /** + * Return whether this connection is closed. + * + * @return boolean + */ + @Override + public boolean isClosed() { + synchronized (lock) { + return this.state == State.CLOSED; + } + } + + public boolean isFinished() { + return this.state.equals(State.FINISHED); + } + + /** + * Insert bucket data into buffer. + * + * @param bucket + * @return int[] + * @throws SQLException + */ + int[] insert(ObDirectLoadBucket bucket) throws SQLException { + try { + this.load.write(bucket); + int[] result = new int[bucket.getRowNum()]; + Arrays.fill(result, 1); + return result; + } catch (Exception ex) { + throw new SQLException(ex); + } + } + + /** + * Indicates the state of {@link DirectPathConnection } + */ + enum State { + + /** + * Begin transaction + */ + BEGIN, + /** + * Transaction is finished, ready to close. + */ + FINISHED, + + /** + * Transaction is closed. + */ + CLOSED; + } + + /** + * This builder used to build a new {@link DirectPathConnection } + */ + public static class Builder { + + private String host; + private int port; + + private String user; + private String tenant; + private String password; + + private String schema; + private String table; + + /** + * Client job count. + */ + private int blocks = OB_DIRECT_PATH_DEFAULT_BLOCKS; + + /** + * Server threads used to sort. + */ + private int parallel; + + private long maxErrorCount; + + private ObLoadDupActionType duplicateKeyAction; + + // Used for load data + private long serverTimeout; + + private Configuration configuration; + + public Builder host(String host) { + this.host = host; + return this; + } + + public Builder port(int port) { + this.port = port; + return this; + } + + public Builder user(String user) { + this.user = user; + return this; + } + + public Builder tenant(String tenant) { + this.tenant = tenant; + return this; + } + + public Builder password(String password) { + this.password = password; + return this; + } + + public Builder schema(String schema) { + this.schema = schema; + return this; + } + + public Builder table(String table) { + this.table = table; + return this; + } + + public Builder blocks(int blocks) { + this.blocks = blocks; + return this; + } + + public Builder parallel(int parallel) { + this.parallel = parallel; + return this; + } + + public Builder maxErrorCount(long maxErrorCount) { + this.maxErrorCount = maxErrorCount; + return this; + } + + public Builder duplicateKeyAction(ObLoadDupActionType duplicateKeyAction) { + this.duplicateKeyAction = duplicateKeyAction; + return this; + } + + public Builder serverTimeout(long serverTimeout) { + this.serverTimeout = serverTimeout; + return this; + } + + public Builder configuration(Configuration configuration) { + this.configuration = configuration; + return this; + } + + /** + * Build a new {@link DirectPathConnection } + * + * @return DirectPathConnection + */ + public DirectPathConnection build() throws Exception { + return createConnection(host, port, user, tenant, password, schema, table, // + blocks, parallel, maxErrorCount, duplicateKeyAction, serverTimeout, duplicateKeyAction).begin(); + } + + /** + * Create a new {@link DirectPathConnection } + * + * @param host + * @param port + * @param user + * @param tenant + * @param password + * @param schema + * @param table + * @param parallel + * @param maxErrorCount + * @param action + * @param serverTimeout + * @return DirectPathConnection + * @throws Exception + */ + DirectPathConnection createConnection(String host, int port, String user, String tenant, String password, String schema, String table, // + int blocks, int parallel, long maxErrorCount, ObLoadDupActionType action, long serverTimeout, ObLoadDupActionType obLoadDupActionType) throws Exception { + + checkArgument(StringUtils.isNotBlank(host), "Host is null.(host=%s)", host); + checkArgument((port > 0 && port < 65535), "Port is invalid.(port=%s)", port); + checkArgument(StringUtils.isNotBlank(user), "User Name is null.(user=%s)", user); + checkArgument(StringUtils.isNotBlank(tenant), "Tenant Name is null.(tenant=%s)", tenant); + checkArgument(StringUtils.isNotBlank(schema), "Schema Name is null.(schema=%s)", schema); + checkArgument(StringUtils.isNotBlank(table), "Table Name is null.(table=%s)", table); + + checkArgument(blocks > 0, "Client Blocks is invalid.(blocks=%s)", blocks); + checkArgument(parallel > 0, "Server Parallel is invalid.(parallel=%s)", parallel); + checkArgument(maxErrorCount > -1, "MaxErrorCount is invalid.(maxErrorCount=%s)", maxErrorCount); + checkArgument(action != null, "ObLoadDupActionType is null.(obLoadDupActionType=%s)", action); + checkArgument(serverTimeout > 0, "Server timeout is invalid.(timeout=%s)", serverTimeout); + Long heartBeatTimeout = 0L; + Long heartBeatInterval = 0L; + if (configuration != null) { + heartBeatTimeout = configuration.getLong(DirectPathConstants.HEART_BEAT_TIMEOUT, OB_DIRECT_PATH_HEART_BEAT_TIMEOUT); + heartBeatInterval = configuration.getLong(DirectPathConstants.HEART_BEAT_INTERVAL, OB_DIRECT_PATH_HEART_BEAT_INTERVAL); + parallel = configuration.getInt(DirectPathConstants.PARALLEL, parallel); + } + DirectLoaderBuilder builder = new DirectLoaderBuilder() + .host(host).port(port) + .user(user) + .tenant(tenant) + .password(password) + .schema(schema) + .table(table) + .parallel(parallel) + .maxErrorCount(maxErrorCount) + .timeout(serverTimeout) + .duplicateKeyAction(obLoadDupActionType) + .heartBeatTimeout(heartBeatTimeout) + .heartBeatInterval(heartBeatInterval); + ObTableDirectLoad directLoad = builder.build(); + + return new DirectPathConnection(directLoad, blocks, configuration); + } + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/DirectPathConstants.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/DirectPathConstants.java new file mode 100644 index 0000000000..d32c966c59 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/DirectPathConstants.java @@ -0,0 +1,12 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.directPath; + +public class DirectPathConstants { + // 以下常量已在DirectPathConnection中被正确使用 + public static final String HEART_BEAT_TIMEOUT = "heartBeatTimeout"; + + public static final String HEART_BEAT_INTERVAL = "heartBeatInterval"; + + public static final String PARALLEL = "parallel"; + + public static final String BUFFERSIZE = "bufferSize"; +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/DirectPathPreparedStatement.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/DirectPathPreparedStatement.java new file mode 100644 index 0000000000..8f4aa7cf4c --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/DirectPathPreparedStatement.java @@ -0,0 +1,164 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.directPath; + +import java.sql.SQLException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.OptionalInt; + +import com.alipay.oceanbase.rpc.direct_load.ObDirectLoadBucket; +import com.alipay.oceanbase.rpc.direct_load.exception.ObDirectLoadException; +import com.alipay.oceanbase.rpc.protocol.payload.impl.ObObj; + +import static com.google.common.base.Preconditions.checkArgument; + +public class DirectPathPreparedStatement extends AbstractRestrictedPreparedStatement { + + private ObDirectLoadBucket bucket; + private final DirectPathConnection conn; + private final Map parameters; + private final Integer bufferSize; + private static final int DEFAULT_BUFFERSIZE = 1048576; + public static final int[] EMPTY_ARRAY = new int[0]; + + /** + * Construct a new {@link DirectPathConnection } instance. + * + * @param conn + */ + public DirectPathPreparedStatement(DirectPathConnection conn) { + this.conn = conn; + this.parameters = new HashMap<>(); + this.bufferSize = DEFAULT_BUFFERSIZE; + this.bucket = new ObDirectLoadBucket(); + } + + public DirectPathPreparedStatement(DirectPathConnection conn, Integer bufferSize) { + this.conn = conn; + this.parameters = new HashMap<>(); + this.bufferSize = bufferSize; + this.bucket = new ObDirectLoadBucket(bufferSize); + } + + /** + * Return current direct path connection. + * + * @return DirectPathConnection + * @throws SQLException + */ + @Override + public DirectPathConnection getConnection() throws SQLException { + return this.conn; + } + + /** + * Copy a new row data avoid overwrite. + * + * @throws SQLException + */ + @Override + public void addBatch() throws SQLException { + checkRange(); + ObObj[] objObjArray = new ObObj[parameters.size()]; + for (Map.Entry entry : parameters.entrySet()) { + objObjArray[entry.getKey() - 1] = entry.getValue(); + } + this.addBatch(objObjArray); + } + + /** + * Add a new row into buffer with input original value list. + * + * @param values One original row data. + */ + public void addBatch(List values) { + this.addBatch(createObObjArray(values)); + } + + /** + * Add a new row into buffer with input original value array. + * + * @param values One original row data. + */ + public void addBatch(Object[] values) { + this.addBatch(createObObjArray(values)); + } + + /** + * Add a new row into buffer with input ObObj array. + * + * @param arr One row data described as ObObj. + */ + private void addBatch(ObObj[] arr) { + checkArgument(arr != null && arr.length > 0, "Input values is null"); + try { + this.bucket.addRow(arr); + } catch (ObDirectLoadException e) { + throw new RuntimeException(e); + } + } + + /** + * Buffered the row data in memory. (defined in the bucket) + * You must invoke {@code ObDirectLoadBucket.clearBatch } after executeBatch. + * + * @return int[] + * @throws SQLException + */ + @Override + public int[] executeBatch() throws SQLException { + return this.bucket.isEmpty() ? EMPTY_ARRAY : this.conn.insert(bucket); + } + + /** + * Clear batch is always recreate a new {@link ObDirectLoadBucket} + */ + @Override + public void clearBatch() { + this.parameters.clear(); + this.bucket = new ObDirectLoadBucket(bufferSize); + } + + /** + * Clear the holder parameters. + * + * @throws SQLException + */ + @Override + public void clearParameters() throws SQLException { + this.parameters.clear(); + } + + /** + * @return boolean + */ + @Override + public boolean isOracleMode() { + return false; + } + + /** + * Set parameter to the target position. + * + * @param parameterIndex Start From 1 + * @param obObj Convert original value to {@link ObObj } + * @throws SQLException + */ + @Override + protected void setParameter(int parameterIndex, ObObj obObj) throws SQLException { + checkArgument(parameterIndex > 0, "Parameter index should start from 1"); + this.parameters.put(parameterIndex, obObj); + } + + /** + * Avoid range exception: + *

+ * Map.put(1, "abc"); + * Map.put(5, "def"); // Error: parameter index is 5, but 2 values exists. + */ + private void checkRange() { + OptionalInt optionalInt = parameters.keySet().stream().mapToInt(e -> e).max(); + int parameterIndex = optionalInt.orElseThrow(() -> new IllegalArgumentException("No parameter index found")); + checkArgument(parameterIndex == parameters.size(), "Parameter index(%s) is unmatched with value list(%s)", parameterIndex, parameters.size()); + } +} \ No newline at end of file diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/ObTableDirectLoad.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/ObTableDirectLoad.java new file mode 100644 index 0000000000..859fcedd16 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/directPath/ObTableDirectLoad.java @@ -0,0 +1,154 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.directPath; + +import java.sql.SQLException; +import java.util.Objects; + +import com.alipay.oceanbase.rpc.direct_load.ObDirectLoadBucket; +import com.alipay.oceanbase.rpc.direct_load.ObDirectLoadConnection; +import com.alipay.oceanbase.rpc.direct_load.ObDirectLoadStatement; +import com.alipay.oceanbase.rpc.direct_load.ObDirectLoadTraceId; +import com.alipay.oceanbase.rpc.direct_load.exception.ObDirectLoadException; +import com.alipay.oceanbase.rpc.direct_load.protocol.payload.ObTableLoadClientStatus; +import com.alipay.oceanbase.rpc.table.ObTable; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wrapper of the direct-load API for OceanBase. + */ +public class ObTableDirectLoad implements AutoCloseable { + private static final Logger LOG = LoggerFactory.getLogger(ObTableDirectLoad.class); + + private final String tableName; + private final String schemaTableName; + private final ObDirectLoadStatement statement; + private final ObDirectLoadConnection connection; + + public ObTableDirectLoad(String schemaName, String tableName, ObDirectLoadStatement statement, ObDirectLoadConnection connection) { + Objects.requireNonNull(schemaName, "schemaName must not be null"); + Objects.requireNonNull(tableName, "tableName must not be null"); + Objects.requireNonNull(statement, "statement must not be null"); + Objects.requireNonNull(connection, "connection must not be null"); + this.tableName = tableName; + this.schemaTableName = String.format("%s.%s", schemaName, tableName); + this.statement = statement; + this.connection = connection; + } + + /** + * Begin the direct load operation. + * + * @throws ObDirectLoadException if an error occurs during the operation. + */ + public void begin() throws ObDirectLoadException { + statement.begin(); + } + + /** + * Write data into the direct load operation. + * + * @param bucket The data bucket to write. + * @throws SQLException if writing fails. + */ + public void write(ObDirectLoadBucket bucket) throws SQLException { + try { + if (bucket == null || bucket.isEmpty()) { + throw new IllegalArgumentException("Bucket must not be null or empty."); + } + LOG.info("Writing {} rows to table: {}", bucket.getRowNum(), schemaTableName); + statement.write(bucket); + LOG.info("Successfully wrote bucket data to table: {}", schemaTableName); + } catch (ObDirectLoadException e) { + LOG.error("Failed to write to table: {}", schemaTableName, e); + throw new SQLException(String.format("Failed to write to table: %s", schemaTableName), e); + } + } + + /** + * Commit the current direct load operation. + * + * @throws SQLException if commit fails. + */ + public void commit() throws SQLException { + try { + LOG.info("Committing direct load for table: {}", schemaTableName); + statement.commit(); + LOG.info("Successfully committed direct load for table: {}", schemaTableName); + } catch (ObDirectLoadException e) { + LOG.error("Failed to commit for table: {}", schemaTableName, e); + throw new SQLException(String.format("Failed to commit for table: %s", schemaTableName), e); + } + } + + /** + * Close the direct load operation. + */ + public void close() { + LOG.info("Closing direct load for table: {}", schemaTableName); + statement.close(); + connection.close(); + LOG.info("Direct load closed for table: {}", schemaTableName); + } + + /** + * Gets the status from the current connection based on the traceId + */ + public ObTableLoadClientStatus getStatus() throws SQLException { + ObDirectLoadTraceId traceId = statement.getTraceId(); + // Check if traceId is null and throw an exception with a clear message + if (traceId == null) { + throw new SQLException("traceId is null."); + } + // Retrieve the status using the traceId + ObTableLoadClientStatus status = statement.getConnection().getProtocol().getHeartBeatRpc(traceId).getStatus(); + if (status == null) { + LOG.info("Direct load connect protocol heartBeatRpc for table is null: {}", schemaTableName); + throw new SQLException("status is null."); + } + // Return status if not null; otherwise, return ERROR + return status; + } + + /** + * Gets the current table + */ + public ObTable getTable() { + try { + return this.statement.getObTablePool().getControlObTable(); + } catch (ObDirectLoadException e) { + throw new RuntimeException(e); + } + } + + public String getTableName() { + if (StringUtils.isBlank(tableName)) { + throw new IllegalArgumentException("tableName is blank."); + } + return tableName; + } + + /** + * Inserts data into the direct load operation. + * + * @param bucket The data bucket containing rows to insert. + * @throws SQLException if an error occurs during the insert operation. + */ + public void insert(ObDirectLoadBucket bucket) throws SQLException { + LOG.info("Inserting {} rows to table: {}", bucket.getRowNum(), schemaTableName); + + if (bucket.isEmpty()) { + LOG.warn("Parameter 'bucket' must not be empty."); + throw new IllegalArgumentException("Parameter 'bucket' must not be empty."); + } + + try { + // Perform the insertion into the load operation + statement.write(bucket); + LOG.info("Successfully inserted data into table: {}", schemaTableName); + } catch (Exception ex) { + LOG.error("Unexpected error during insert operation for table: {}", schemaTableName, ex); + throw new SQLException("Unexpected error during insert operation.", ex); + } + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/AbstractConnHolder.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/AbstractConnHolder.java new file mode 100644 index 0000000000..6e4d4aab51 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/AbstractConnHolder.java @@ -0,0 +1,61 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.ext; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; + +public abstract class AbstractConnHolder { + private static final Logger LOG = LoggerFactory.getLogger(AbstractConnHolder.class); + + protected final Configuration config; + protected Connection conn; + + protected String jdbcUrl; + protected String userName; + protected String password; + + protected AbstractConnHolder(Configuration config, String jdbcUrl, String userName, String password) { + this.config = config; + this.jdbcUrl = jdbcUrl; + this.userName = userName; + this.password = password; + } + + public AbstractConnHolder(Configuration config) { + this.config = config; + } + + public abstract Connection initConnection(); + + public Configuration getConfig() { + return config; + } + + public Connection getConn() { + try { + if (conn != null && !conn.isClosed()) { + return conn; + } + } catch (Exception e) { + LOG.warn("judge connection is closed or not failed. try to reconnect.", e); + } + return reconnect(); + } + + public Connection reconnect() { + DBUtil.closeDBResources(null, conn); + return initConnection(); + } + + public abstract String getJdbcUrl(); + + public abstract String getUserName(); + + public abstract void destroy(); + + public abstract void doCommit(); +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/ConnHolder.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/ConnHolder.java new file mode 100644 index 0000000000..785f47bf1a --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/ConnHolder.java @@ -0,0 +1,37 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.ext; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; + +import java.sql.Connection; + +public abstract class ConnHolder { + + protected final Configuration config; + protected Connection conn; + + public ConnHolder(Configuration config) { + this.config = config; + } + + public abstract Connection initConnection(); + + public Configuration getConfig() { + return config; + } + + public Connection getConn() { + return conn; + } + + public Connection reconnect() { + DBUtil.closeDBResources(null, conn); + return initConnection(); + } + + public abstract String getJdbcUrl(); + + public abstract String getUserName(); + + public abstract void destroy(); +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/DataBaseWriterBuffer.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/DataBaseWriterBuffer.java new file mode 100644 index 0000000000..b8ae259a9f --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/DataBaseWriterBuffer.java @@ -0,0 +1,101 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.ext; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.util.ObWriterUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * + * @author oceanbase + * + */ +public class DataBaseWriterBuffer { + private static final Logger LOG = LoggerFactory.getLogger(DataBaseWriterBuffer.class); + + private final AbstractConnHolder connHolder; + private final String dbName; + private Map> tableBuffer = new HashMap>(); + private long lastCheckMemstoreTime; + + public DataBaseWriterBuffer(Configuration config,String jdbcUrl, String userName, String password,String dbName){ + this.connHolder = new ObClientConnHolder(config, jdbcUrl, userName, password); + this.dbName=dbName; + } + + public AbstractConnHolder getConnHolder(){ + return connHolder; + } + + public void initTableBuffer(List tableList) { + for (String table : tableList) { + tableBuffer.put(table, new LinkedList()); + } + } + + public List getTableList(){ + return new ArrayList(tableBuffer.keySet()); + } + + public void addRecord(Record record, String tableName) { + LinkedList recordList = tableBuffer.get(tableName); + if (recordList == null) { + throw DataXException.asDataXException(DBUtilErrorCode.WRITE_DATA_ERROR, + String.format("The [table] calculated based on the rules does not exist. The calculated [tableName]=%s, [db]=%s. Please check the rules you configured.", + tableName, connHolder.getJdbcUrl())); + } + recordList.add(record); + } + + public Map> getTableBuffer() { + return tableBuffer; + } + + public String getDbName() { + return dbName; + } + + public long getLastCheckMemstoreTime() { + return lastCheckMemstoreTime; + } + + public void setLastCheckMemstoreTime(long lastCheckMemstoreTime) { + this.lastCheckMemstoreTime = lastCheckMemstoreTime; + } + + /** + * 检查当前DB的memstore使用状态 + *

+ * 若超过阈值,则休眠 + * + * @param memstoreCheckIntervalSecond + * @param memstoreThreshold + */ + public synchronized void checkMemstore(long memstoreCheckIntervalSecond, double memstoreThreshold) { + long now = System.currentTimeMillis(); + if (now - getLastCheckMemstoreTime() < 1000 * memstoreCheckIntervalSecond) { + return; + } + + LOG.debug(String.format("checking memstore usage: lastCheckTime=%d, now=%d, check interval=%d, threshold=%f", + getLastCheckMemstoreTime(), now, memstoreCheckIntervalSecond, memstoreThreshold)); + + Connection conn = getConnHolder().getConn(); + while (ObWriterUtils.isMemstoreFull(conn, memstoreThreshold)) { + LOG.warn("OB memstore is full,sleep 60 seconds, jdbc=" + getConnHolder().getJdbcUrl() + + ",threshold=" + memstoreThreshold); + ObWriterUtils.sleep(60000); + } + setLastCheckMemstoreTime(now); + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/DirectPathAbstractConnHolder.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/DirectPathAbstractConnHolder.java new file mode 100644 index 0000000000..c5c6dbe0ca --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/DirectPathAbstractConnHolder.java @@ -0,0 +1,61 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.ext; + +import java.sql.Connection; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class DirectPathAbstractConnHolder { + private static final Logger LOG = LoggerFactory.getLogger(AbstractConnHolder.class); + protected Configuration config; + protected String jdbcUrl; + protected String userName; + protected String password; + + protected Connection conn; + + protected DirectPathAbstractConnHolder(Configuration config, String jdbcUrl, String userName, String password) { + this.config = config; + this.jdbcUrl = jdbcUrl; + this.userName = userName; + this.password = password; + } + + public Connection reconnect() { + DBUtil.closeDBResources(null, conn); + return initConnection(); + } + + public Connection getConn() { + if (conn == null) { + return initConnection(); + } else { + try { + if (conn.isClosed()) { + return reconnect(); + } + return conn; + } catch (Exception e) { + LOG.debug("can not judge whether the hold connection is closed or not, just reuse the hold connection"); + return conn; + } + } + } + + public String getJdbcUrl() { + return jdbcUrl; + } + + public Configuration getConfig() { + return config; + } + + public void doCommit() {} + + public abstract void destroy(); + + public abstract Connection initConnection(); +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/DirectPathConnHolder.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/DirectPathConnHolder.java new file mode 100644 index 0000000000..352eda1ca4 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/DirectPathConnHolder.java @@ -0,0 +1,115 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.ext; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.Config; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.common.Table; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.directPath.DirectPathConnection; + +import com.alipay.oceanbase.rpc.protocol.payload.impl.ObLoadDupActionType; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DirectPathConnHolder extends AbstractConnHolder { + private static final Logger LOG = LoggerFactory.getLogger(DirectPathConnHolder.class); + + /** + * The server side timeout. + */ + private static final long SERVER_TIMEOUT = 24L * 60 * 60 * 1000 * 1000; + + private static final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + private String tableName; + private String host; + private int rpcPort; + private String tenantName; + private String databaseName; + private int blocks; + private int threads; + private int maxErrors; + private ObLoadDupActionType duplicateKeyAction; + + public DirectPathConnHolder(Configuration config, ServerConnectInfo connectInfo, String tableName, int threadsPerChannel) { + super(config, connectInfo.jdbcUrl, connectInfo.userName, connectInfo.password); + // direct path: + //● publicCloud & odp - single or full + //● publicCloud & observer - not support + //● !publicCloud & odp - full + //● !publicCloud & observer - single + this.userName = connectInfo.getFullUserName(); + this.host = connectInfo.host; + this.rpcPort = connectInfo.rpcPort; + this.tenantName = connectInfo.tenantName; + if (!connectInfo.publicCloud && StringUtils.isEmpty(tenantName)) { + throw new IllegalStateException("tenant name is needed when using direct path load in private cloud."); + } + this.databaseName = connectInfo.databaseName; + this.tableName = tableName; + this.blocks = config.getInt(Config.BLOCKS_COUNT, 1); + this.threads = threadsPerChannel * Math.min(blocks, 32); + this.maxErrors = config.getInt(Config.MAX_ERRORS, 0); + this.duplicateKeyAction = "insert".equalsIgnoreCase(config.getString(Config.OB_WRITE_MODE)) ? ObLoadDupActionType.IGNORE : ObLoadDupActionType.REPLACE; + } + + @Override + public Connection initConnection() { + synchronized (cache) { + conn = cache.computeIfAbsent(new Table(databaseName, tableName), e -> { + try { + return new DirectPathConnection.Builder().host(host) // + .port(rpcPort) // + .tenant(tenantName) // + .user(userName) // + .password(Optional.ofNullable(password).orElse("")) // + .schema(databaseName) // + .table(tableName) // + .blocks(blocks) // + .parallel(threads) // + .maxErrorCount(maxErrors) // + .duplicateKeyAction(duplicateKeyAction) // + .serverTimeout(SERVER_TIMEOUT) // + .configuration(config) + .build(); + } catch (Exception ex) { + throw DataXException.asDataXException(DBUtilErrorCode.CONN_DB_ERROR, ex); + } + }); + } + return conn; + } + + public String getJdbcUrl() { + return ""; + } + + public String getUserName() { + return ""; + } + + @Override + public void destroy() { + if (conn != null && ((DirectPathConnection) conn).isFinished()) { + DBUtil.closeDBResources(null, conn); + } + } + + @Override + public void doCommit() { + try { + if (conn != null) { + conn.commit(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/OBDataSourceV10.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/OBDataSourceV10.java new file mode 100644 index 0000000000..2c1f516ffa --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/OBDataSourceV10.java @@ -0,0 +1,190 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.ext; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.alibaba.datax.plugin.rdbms.reader.Key; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.util.ObWriterUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.Config; +import com.alipay.oceanbase.obproxy.datasource.ObGroupDataSource; +import com.alipay.oceanbase.obproxy.exception.ConnectionPropertiesNotSupportedException; +import com.alipay.oceanbase.obproxy.util.StringParser.IllegalFormatException; +import com.google.common.collect.Maps; + +public class OBDataSourceV10 { + private static final Logger LOG = LoggerFactory.getLogger(OBDataSourceV10.class); + + private static final Map dataSources = Maps.newHashMap(); + + private static int ocjGetConnectionTimeout = 0; + private static int ocjGlobalProxyroGetConnectionTimeout = 0; + private static int ocjMaxWaitOfCreateClusterResourceMs = 0; + + private static Configuration taskConfig; + + public static String genKey(String fullUserName, String dbName) { + //username@tenantName#clusterName/dbName + return fullUserName + "/" + dbName; + } + + public static synchronized void init(Configuration configuration, + final String fullUsername, + final String password, + final String dbName) { + taskConfig = configuration; + final String rsUrl = ""; + final String dataSourceKey = genKey(fullUsername, dbName); + final int maxActiveConnection = configuration.getInt(Config.MAX_ACTIVE_CONNECTION, Config.DEFAULT_MAX_ACTIVE_CONNECTION); + if (dataSources.containsKey(dataSourceKey)) { + dataSources.get(dataSourceKey).increseRefercnce(); + } else { + long timeout = configuration.getInt(Config.TIMEOUT, 30); + if (timeout < 30) { + timeout = 30; + } + if (ocjGetConnectionTimeout == 0) { + ocjGetConnectionTimeout = configuration.getInt(Config.OCJ_GET_CONNECT_TIMEOUT, + Config.DEFAULT_OCJ_GET_CONNECT_TIMEOUT); + ocjGlobalProxyroGetConnectionTimeout = configuration.getInt(Config.OCJ_PROXY_CONNECT_TIMEOUT, + Config.DEFAULT_OCJ_PROXY_CONNECT_TIMEOUT); + ocjMaxWaitOfCreateClusterResourceMs = configuration.getInt(Config.OCJ_CREATE_RESOURCE_TIMEOUT, + Config.DEFAULT_OCJ_CREATE_RESOURCE_TIMEOUT); + + LOG.info(String.format("initializing OCJ with ocjGetConnectionTimeout=%d, " + + "ocjGlobalProxyroGetConnectionTimeout=%d, ocjMaxWaitOfCreateClusterResourceMs=%d", + ocjGetConnectionTimeout, ocjGlobalProxyroGetConnectionTimeout, ocjMaxWaitOfCreateClusterResourceMs)); + } + DataSourceHolder holder = null; + try { + holder = new DataSourceHolder(rsUrl, fullUsername, password, dbName, maxActiveConnection, timeout); + dataSources.put(dataSourceKey, holder); + } catch (ConnectionPropertiesNotSupportedException e) { + e.printStackTrace(); + throw new DataXException(ObDataSourceErrorCode.DESC, "connect error"); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + throw new DataXException(ObDataSourceErrorCode.DESC, "connect error"); + } catch (IllegalFormatException e) { + e.printStackTrace(); + throw new DataXException(ObDataSourceErrorCode.DESC, "connect error"); + } catch (SQLException e) { + e.printStackTrace(); + throw new DataXException(ObDataSourceErrorCode.DESC, "connect error"); + } + } + } + + public static synchronized void destory(final String dataSourceKey){ + DataSourceHolder holder = dataSources.get(dataSourceKey); + holder.decreaseReference(); + if (holder.canClose()) { + dataSources.remove(dataSourceKey); + holder.close(); + LOG.info(String.format("close datasource success [%s]", dataSourceKey)); + } + } + + public static Connection getConnection(final String url) { + Connection conn = null; + try { + conn = dataSources.get(url).getconnection(); + } catch (SQLException e) { + e.printStackTrace(); + } + return conn; + } + + private static Map buildJdbcProperty() { + Map property = new HashMap(); + property.put("useServerPrepStmts", "false"); + property.put("characterEncoding", "UTF-8"); + property.put("useLocalSessionState", "false"); + property.put("rewriteBatchedStatements", "true"); + property.put("socketTimeout", "25000"); + + return property; + } + + private static class DataSourceHolder { + private volatile int reference; + private final ObGroupDataSource groupDataSource; + public static final Map jdbcProperty = buildJdbcProperty();; + + public DataSourceHolder(final String rsUrl, + final String fullUsername, + final String password, + final String dbName, + final int maxActive, + final long timeout) throws ConnectionPropertiesNotSupportedException, IllegalFormatException, IllegalArgumentException, SQLException { + this.reference = 1; + this.groupDataSource = new ObGroupDataSource(); + this.groupDataSource.setUrl(rsUrl); + this.groupDataSource.setFullUsername(fullUsername); + this.groupDataSource.setPassword(password); + this.groupDataSource.setDatabase(dbName); + this.groupDataSource.setConnectionProperties(jdbcProperty); + this.groupDataSource.setGetConnectionTimeout(ocjGetConnectionTimeout); + this.groupDataSource.setGlobalProxyroGetConnectionTimeout(ocjGlobalProxyroGetConnectionTimeout); + this.groupDataSource.setMaxWaitOfCreateClusterResourceMs(ocjMaxWaitOfCreateClusterResourceMs); + this.groupDataSource.setMaxActive(maxActive); + this.groupDataSource.setGlobalSlowQueryThresholdUs(3000000); // 3s, sql with response time more than 3s will be logged + this.groupDataSource.setGlobalCleanLogFileEnabled(true); // enable log cleanup + this.groupDataSource.setGlobalLogFileSizeThreshold(17179869184L); // 16G, log file total size + this.groupDataSource.setGlobalCleanLogFileInterval(10000); // 10s, check interval + this.groupDataSource.setInitialSize(1); + + List initSqls = new ArrayList(); + if (taskConfig != null) { + List sessionConfig = taskConfig.getList(Key.SESSION, new ArrayList(), String.class); + if (sessionConfig != null || sessionConfig.size() > 0) { + initSqls.addAll(sessionConfig); + } + } + // set up for writing timestamp columns + if (ObWriterUtils.isOracleMode()) { + initSqls.add("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS';"); + initSqls.add("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS.FF';"); + initSqls.add("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT='YYYY-MM-DD HH24:MI:SS.FF TZR TZD';"); + } + + this.groupDataSource.setConnectionInitSqls(initSqls); + + this.groupDataSource.init(); + // this.groupDataSource; + LOG.info("Create GroupDataSource rsUrl=[{}], fullUserName=[{}], dbName=[{}], getConnectionTimeout= {}ms, maxActive={}", + rsUrl, fullUsername, dbName, 5000, maxActive); + } + + public Connection getconnection() throws SQLException { + return groupDataSource.getConnection(); + } + + public synchronized void increseRefercnce() { + this.reference++; + } + + public synchronized void decreaseReference() { + this.reference--; + } + + public synchronized boolean canClose() { + return reference == 0; + } + + public synchronized void close() { + if (this.canClose()) { + groupDataSource.destroy(); + } + } + } + +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/OCJConnHolder.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/OCJConnHolder.java new file mode 100644 index 0000000000..02277fbe8c --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/OCJConnHolder.java @@ -0,0 +1,54 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.ext; + +import java.sql.Connection; +import java.sql.SQLException; + +import com.alibaba.datax.common.util.Configuration; + +/** + * wrap oceanbase java client + * + * @author oceanbase + */ + +public class OCJConnHolder extends AbstractConnHolder { + private ServerConnectInfo connectInfo; + private String dataSourceKey; + + public OCJConnHolder(Configuration config, ServerConnectInfo connInfo) { + super(config); + this.connectInfo = connInfo; + this.dataSourceKey = OBDataSourceV10.genKey(connectInfo.getFullUserName(), connectInfo.databaseName); + OBDataSourceV10.init(config, connectInfo.getFullUserName(), connectInfo.password, connectInfo.databaseName); + } + + @Override + public Connection initConnection() { + conn = OBDataSourceV10.getConnection(dataSourceKey); + return conn; + } + + @Override + public String getJdbcUrl() { + return connectInfo.jdbcUrl; + } + + @Override + public String getUserName() { + return connectInfo.userName; + } + + public void destroy() { + OBDataSourceV10.destory(this.dataSourceKey); + } + + public void doCommit() { + try { + if (conn != null) { + conn.commit(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/ObClientConnHolder.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/ObClientConnHolder.java new file mode 100644 index 0000000000..c0e885c250 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/ObClientConnHolder.java @@ -0,0 +1,74 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.ext; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.Key; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.util.ObWriterUtils; + +/** + * 数据库连接代理对象,负责创建连接,重新连接 + * + * @author oceanbase + */ +public class ObClientConnHolder extends AbstractConnHolder { + private final String jdbcUrl; + private final String userName; + private final String password; + + public ObClientConnHolder(Configuration config, String jdbcUrl, String userName, String password) { + super(config); + this.jdbcUrl = jdbcUrl; + this.userName = userName; + this.password = password; + } + + // Connect to ob with obclient and obproxy + @Override + public Connection initConnection() { + String BASIC_MESSAGE = String.format("jdbcUrl:[%s]", this.jdbcUrl); + DataBaseType dbType = DataBaseType.OceanBase; + if (ObWriterUtils.isOracleMode()) { + // set up for writing timestamp columns + List sessionConfig = config.getList(Key.SESSION, new ArrayList(), String.class); + sessionConfig.add("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + sessionConfig.add("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS.FF'"); + sessionConfig.add("ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT='YYYY-MM-DD HH24:MI:SS.FF TZR TZD'"); + config.set(Key.SESSION, sessionConfig); + } + conn = DBUtil.getConnection(dbType, jdbcUrl, userName, password); + DBUtil.dealWithSessionConfig(conn, config, dbType, BASIC_MESSAGE); + return conn; + } + + @Override + public String getJdbcUrl() { + return jdbcUrl; + } + + @Override + public String getUserName() { + return userName; + } + + @Override + public void destroy() { + DBUtil.closeDBResources(null, conn); + } + + @Override + public void doCommit() { + try { + if (conn != null) { + conn.commit(); + } + } catch (SQLException e) { + throw new RuntimeException(e); + } + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/ObDataSourceErrorCode.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/ObDataSourceErrorCode.java new file mode 100644 index 0000000000..6509c7662b --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/ObDataSourceErrorCode.java @@ -0,0 +1,31 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.ext; + +import com.alibaba.datax.common.spi.ErrorCode; + +public enum ObDataSourceErrorCode implements ErrorCode { + DESC("ObDataSourceError code","connect error"); + + private final String code; + private final String describe; + + private ObDataSourceErrorCode(String code, String describe) { + this.code = code; + this.describe = describe; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.describe; + } + + @Override + public String toString() { + return String.format("Code:[%s], Describe:[%s]. ", this.code, + this.describe); + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/ServerConnectInfo.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/ServerConnectInfo.java new file mode 100644 index 0000000000..714d731296 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/ext/ServerConnectInfo.java @@ -0,0 +1,116 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.ext; + +import static org.apache.commons.lang3.StringUtils.EMPTY; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.alibaba.datax.common.util.Configuration; + +public class ServerConnectInfo { + + public String clusterName; + public String tenantName; + // userName doesn't contain tenantName or clusterName + public String userName; + public String password; + public String databaseName; + public String ipPort; + public String jdbcUrl; + public String host; + public String port; + public boolean publicCloud; + public int rpcPort; + public Configuration config; + + public ServerConnectInfo(final String jdbcUrl, final String username, final String password, Configuration config) { + this.jdbcUrl = jdbcUrl; + this.password = password; + this.config = config; + parseJdbcUrl(jdbcUrl); + parseFullUserName(username); + } + + private void parseJdbcUrl(final String jdbcUrl) { + Pattern pattern = Pattern.compile("//([\\w\\.\\-]+:\\d+)/([^\\\\?]*)"); + Matcher matcher = pattern.matcher(jdbcUrl); + if (matcher.find()) { + String ipPort = matcher.group(1); + String dbName = matcher.group(2); + this.ipPort = ipPort; + this.host = ipPort.split(":")[0]; + this.port = ipPort.split(":")[1]; + this.databaseName = dbName; + this.publicCloud = host.endsWith("aliyuncs.com"); + } else { + throw new RuntimeException("Invalid argument:" + jdbcUrl); + } + } + + protected void parseFullUserName(final String fullUserName) { + int tenantIndex = fullUserName.indexOf("@"); + int clusterIndex = fullUserName.indexOf("#"); + // 适用于jdbcUrl以||_dsc_ob10_dsc_开头的场景 + if (fullUserName.contains(":") && tenantIndex < 0) { + String[] names = fullUserName.split(":"); + if (names.length != 3) { + throw new RuntimeException("invalid argument: " + fullUserName); + } else { + this.clusterName = names[0]; + this.tenantName = names[1]; + this.userName = names[2]; + } + } else if (tenantIndex < 0) { + // 适用于short jdbcUrl,且username中不含租户名(主要是公有云场景,此场景下不计算分区) + this.userName = fullUserName; + this.clusterName = EMPTY; + this.tenantName = EMPTY; + } else { + // 适用于short jdbcUrl,且username中含租户名 + this.userName = fullUserName.substring(0, tenantIndex); + if (clusterIndex < 0) { + this.clusterName = EMPTY; + this.tenantName = fullUserName.substring(tenantIndex + 1); + } else { + this.clusterName = fullUserName.substring(clusterIndex + 1); + this.tenantName = fullUserName.substring(tenantIndex + 1, clusterIndex); + } + } + } + + @Override + public String toString() { + return "ServerConnectInfo{" + + "clusterName='" + clusterName + '\'' + + ", tenantName='" + tenantName + '\'' + + ", userName='" + userName + '\'' + + ", password='" + password + '\'' + + ", databaseName='" + databaseName + '\'' + + ", ipPort='" + ipPort + '\'' + + ", jdbcUrl='" + jdbcUrl + '\'' + + ", host='" + host + '\'' + + ", publicCloud=" + publicCloud + + ", rpcPort=" + rpcPort + + '}'; + } + + public String getFullUserName() { + StringBuilder builder = new StringBuilder(); + builder.append(userName); + if (!EMPTY.equals(tenantName)) { + builder.append("@").append(tenantName); + } + + if (!EMPTY.equals(clusterName)) { + builder.append("#").append(clusterName); + } + if (EMPTY.equals(this.clusterName) && EMPTY.equals(this.tenantName)) { + return this.userName; + } + return builder.toString(); + } + + public void setRpcPort(int rpcPort) { + this.rpcPort = rpcPort; + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/part/IObPartCalculator.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/part/IObPartCalculator.java new file mode 100644 index 0000000000..b49ade02f5 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/part/IObPartCalculator.java @@ -0,0 +1,19 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.part; + +import com.alibaba.datax.common.element.Record; + +/** + * @author cjyyz + * @date 2023/02/07 + * @since + */ +public interface IObPartCalculator { + + /** + * 计算 Partition Id + * + * @param record + * @return Long + */ + Long calculate(Record record); +} \ No newline at end of file diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/part/ObPartitionCalculatorV1.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/part/ObPartitionCalculatorV1.java new file mode 100644 index 0000000000..96985588d8 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/part/ObPartitionCalculatorV1.java @@ -0,0 +1,109 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.part; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.ServerConnectInfo; +import com.alipay.oceanbase.obproxy.data.TableEntryKey; +import com.alipay.oceanbase.obproxy.util.ObPartitionIdCalculator; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * OceanBase 1.x和2.x的分区计算 + * + * @author cjyyz + * @date 2023/02/07 + * @since + */ +public class ObPartitionCalculatorV1 implements IObPartCalculator { + + private static final Logger LOG = LoggerFactory.getLogger(ObPartitionCalculatorV1.class); + + /** + * 分区键的位置 + */ + private List partIndexes; + + /** + * 表的全部字段名 + */ + private List columnNames; + + /** + * ocj partition calculator + */ + private ObPartitionIdCalculator calculator; + + /** + * @param connectInfo + * @param table + * @param columns + */ + public ObPartitionCalculatorV1(ServerConnectInfo connectInfo, String table, List columns) { + + initCalculator(connectInfo, table); + + if (Objects.isNull(calculator)) { + LOG.warn("partCalculator is null"); + return; + } + + this.partIndexes = new ArrayList<>(columns.size()); + this.columnNames = new ArrayList<>(columns); + + for (int i = 0; i < columns.size(); ++i) { + String columnName = columns.get(i); + if (calculator.isPartitionKeyColumn(columnName)) { + LOG.info(columnName + " is partition key."); + partIndexes.add(i); + } + } + } + + /** + * @param record + * @return Long + */ + @Override + public Long calculate(Record record) { + if (Objects.isNull(calculator)) { + return null; + } + + for (Integer i : partIndexes) { + calculator.addColumn(columnNames.get(i), record.getColumn(i).asString()); + } + return calculator.calculate(); + } + + /** + * @param connectInfo + * @param table + */ + private void initCalculator(ServerConnectInfo connectInfo, String table) { + + LOG.info(String.format("create tableEntryKey with clusterName %s, tenantName %s, databaseName %s, tableName %s", + connectInfo.clusterName, connectInfo.tenantName, connectInfo.databaseName, table)); + TableEntryKey tableEntryKey = new TableEntryKey(connectInfo.clusterName, connectInfo.tenantName, + connectInfo.databaseName, table); + + int retry = 0; + + do { + try { + if (retry > 0) { + TimeUnit.SECONDS.sleep(1); + LOG.info("retry create new part calculator {} times", retry); + } + LOG.info("create partCalculator with address: " + connectInfo.ipPort); + calculator = new ObPartitionIdCalculator(connectInfo.ipPort, tableEntryKey); + } catch (Exception ex) { + ++retry; + LOG.warn("create new part calculator failed, retry: {}", ex.getMessage()); + } + } while (calculator == null && retry < 3); + } +} \ No newline at end of file diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/part/ObPartitionCalculatorV2.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/part/ObPartitionCalculatorV2.java new file mode 100644 index 0000000000..11b7b25cd3 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/part/ObPartitionCalculatorV2.java @@ -0,0 +1,169 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.part; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.ServerConnectInfo; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.util.DbUtils; +import com.oceanbase.partition.calculator.ObPartIdCalculator; +import com.oceanbase.partition.calculator.enums.ObPartLevel; +import com.oceanbase.partition.calculator.enums.ObServerMode; +import com.oceanbase.partition.calculator.helper.TableEntryExtractor; +import com.oceanbase.partition.calculator.model.TableEntry; +import com.oceanbase.partition.calculator.model.TableEntryKey; +import com.oceanbase.partition.calculator.model.Version; +import com.oceanbase.partition.metadata.desc.ObPartColumn; +import com.oceanbase.partition.metadata.desc.ObTablePart; +import java.sql.Connection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * OceanBase 3.x和4.x的分区计算 + * + * @author cjyyz + * @date 2023/02/07 + * @since + */ +public class ObPartitionCalculatorV2 implements IObPartCalculator { + + private static final Logger LOG = LoggerFactory.getLogger(ObPartitionCalculatorV2.class); + + /** + * OB的模式以及版本信息 + */ + private ObServerMode mode; + + /** + * ob-partition-calculator 分区计算组件 + */ + private ObPartIdCalculator calculator; + + /** + * 记录columns的字段名和在record中的位置。 + * 当目标表结构的分区键是生成列时,calculator 需要从改结构中获取到生成列所依赖的字段的值 + * e.g. + * create table t1 ( + * c1 varchar(20), + * c2 varchar(20) generated always as (substr(`c1`,1,8)) + * )partition by key(c2) partitions 5 + * + * 此时,columnNameIndexMap包含的元素是 c1:0 + * 需要将c1字段的值从columnNameIndexMap中添加到{@link com.oceanbase.partition.calculator.ObPartIdCalculator#getRefColumnValues()} + */ + private Map columnNameIndexMap; + + /** + * @param connectInfo + * @param table + * @param mode + */ + public ObPartitionCalculatorV2(ServerConnectInfo connectInfo, String table, ObServerMode mode, List columns) { + this.mode = mode; + this.columnNameIndexMap = new HashMap<>(); + for (int i = 0; i < columns.size(); i++) { + columnNameIndexMap.put(columns.get(i).toLowerCase(), i); + } + initCalculator(connectInfo, table); + } + + /** + * @param record + * @return Long + */ + @Override + public Long calculate(Record record) { + if (Objects.isNull(calculator)) { + return null; + } + if (!calculator.getTableEntry().isPartitionTable()) { + return 0L; + } + return calculator.calculatePartId(filterNullableColumns(record)); + } + + /** + * 初始化分区计算组件 + * + * @param connectInfo + * @param table + */ + private void initCalculator(ServerConnectInfo connectInfo, String table) { + TableEntryKey tableEntryKey = new TableEntryKey(connectInfo.clusterName, connectInfo.tenantName, connectInfo.databaseName, table, mode); + boolean subsequentFromV4 = !mode.getVersion().isOlderThan(new Version("4.0.0.0")); + try { + TableEntry tableEntry; + try (Connection conn = getConnection(connectInfo, subsequentFromV4)){ + TableEntryExtractor extractor = new TableEntryExtractor(); + tableEntry = extractor.queryTableEntry(conn, tableEntryKey,subsequentFromV4); + } + this.calculator = new ObPartIdCalculator(false, tableEntry, subsequentFromV4); + } catch (Exception e) { + LOG.warn("create new part calculator failed. reason: {}", e.getMessage()); + } + } + + private Connection getConnection(ServerConnectInfo connectInfo, boolean subsequentFromV4) throws Exception { + // OceanBase 4.0.0.0及之后版本均使用业务租户连接计算分区 + if (subsequentFromV4) { + return DBUtil.getConnection(DataBaseType.OceanBase, connectInfo.jdbcUrl, connectInfo.getFullUserName(), connectInfo.password); + } + // OceanBase 4.0.0.0之前版本使用sys租户连接计算分区 + return DbUtils.buildSysConn(connectInfo.jdbcUrl, connectInfo.clusterName); + } + + /** + * 只选择分区字段值传入分区计算组件 + * + * @param record + * @return Object[] + */ + private Object[] filterNullableColumns(Record record) { + final ObTablePart tablePart = calculator.getTableEntry().getTablePart(); + + final Object[] filteredRecords = new Object[record.getColumnNumber()]; + + if (tablePart.getLevel().getIndex() > ObPartLevel.LEVEL_ZERO.getIndex()) { + // 从record中添加非生成列的一级分区值到filteredRecords数组中 + for (ObPartColumn partColumn : tablePart.getPartColumns()) { + if (partColumn.getColumnExpr() == null) { + int metaIndex = partColumn.getColumnIndex(); + String columnName = partColumn.getColumnName().toLowerCase(); + int idxInRecord = columnNameIndexMap.get(columnName); + filteredRecords[metaIndex] = record.getColumn(idxInRecord).asString(); + } + + } + // 从record中添加生成列的一级分区值到calculator的redColumnMap中,ObTablePart.getRefPartColumns中的字段名均为小写 + for (ObPartColumn partColumn : tablePart.getRefPartColumns()) { + String columnName = partColumn.getColumnName(); + int index = columnNameIndexMap.get(columnName); + calculator.addRefColumn(columnName, record.getColumn(index).asString()); + } + } + + if (tablePart.getLevel().getIndex() >= ObPartLevel.LEVEL_TWO.getIndex()) { + // 从record中添加非生成列的二级分区值到filteredRecords数组中 + for (ObPartColumn partColumn : tablePart.getSubPartColumns()) { + if (partColumn.getColumnExpr() == null) { + int metaIndex = partColumn.getColumnIndex(); + String columnName = partColumn.getColumnName().toLowerCase(); + int idxInRecord = columnNameIndexMap.get(columnName); + filteredRecords[metaIndex] = record.getColumn(idxInRecord).asString(); + } + + } + // 从record中添加生成列的二级分区值到calculator的redColumnMap中,ObTablePart.getRefSubPartColumns中的字段名均为小写 + for (ObPartColumn partColumn : tablePart.getRefSubPartColumns()) { + String columnName = partColumn.getColumnName(); + int index = columnNameIndexMap.get(columnName); + calculator.addRefColumn(columnName, record.getColumn(index).asString()); + } + } + return filteredRecords; + } +} \ No newline at end of file diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/AbstractInsertTask.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/AbstractInsertTask.java new file mode 100644 index 0000000000..d4f215e11d --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/AbstractInsertTask.java @@ -0,0 +1,127 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.task; + +import java.util.List; +import java.util.Queue; +import java.util.concurrent.TimeUnit; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.Config; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.AbstractConnHolder; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.ServerConnectInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractInsertTask implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(AbstractInsertTask.class); + protected final long taskId; + protected ConcurrentTableWriterTask writerTask; + protected ConcurrentTableWriterTask.ConcurrentTableWriter writer; + protected Queue> queue; + protected boolean isStop; + protected Configuration config; + protected ServerConnectInfo connInfo; + protected AbstractConnHolder connHolder; + protected long totalCost = 0; + protected long insertCount = 0; + private boolean printCost = Config.DEFAULT_PRINT_COST; + private long costBound = Config.DEFAULT_COST_BOUND; + + public AbstractInsertTask(final long taskId, Queue> recordsQueue, Configuration config, ServerConnectInfo connectInfo, ConcurrentTableWriterTask task, ConcurrentTableWriterTask.ConcurrentTableWriter writer) { + this.taskId = taskId; + this.queue = recordsQueue; + this.config = config; + this.connInfo = connectInfo; + this.isStop = false; + this.printCost = config.getBool(Config.PRINT_COST, Config.DEFAULT_PRINT_COST); + this.costBound = config.getLong(Config.COST_BOUND, Config.DEFAULT_COST_BOUND); + this.writer = writer; + this.writerTask = task; + initConnHolder(); + } + + public AbstractInsertTask(final long taskId, Queue> recordsQueue, Configuration config, ServerConnectInfo connectInfo) { + this.taskId = taskId; + this.queue = recordsQueue; + this.config = config; + this.connInfo = connectInfo; + this.isStop = false; + this.printCost = config.getBool(Config.PRINT_COST, Config.DEFAULT_PRINT_COST); + this.costBound = config.getLong(Config.COST_BOUND, Config.DEFAULT_COST_BOUND); + initConnHolder(); + } + + protected abstract void initConnHolder(); + + public void setWriterTask(ConcurrentTableWriterTask writerTask) { + this.writerTask = writerTask; + } + + public void setWriter(ConcurrentTableWriterTask.ConcurrentTableWriter writer) { + this.writer = writer; + } + + private boolean isStop() { + return isStop; + } + + public void setStop() { + isStop = true; + } + + public AbstractConnHolder getConnHolder() { + return connHolder; + } + + public void calStatistic(final long cost) { + writer.increFinishCount(); + insertCount++; + totalCost += cost; + if (this.printCost && cost > this.costBound) { + LOG.info("slow multi insert cost {}ms", cost); + } + } + + @Override + public void run() { + Thread.currentThread().setName(String.format("%d-insertTask-%d", taskId, Thread.currentThread().getId())); + LOG.debug("Task {} start to execute...", taskId); + while (!isStop()) { + try { + List records = queue.poll(); + if (null != records) { + write(records); + } else if (writerTask.isFinished()) { + writerTask.singalTaskFinish(); + LOG.debug("not more task, thread exist ..."); + break; + } else { + TimeUnit.MILLISECONDS.sleep(5); + } + } catch (InterruptedException e) { + LOG.debug("TableWriter is interrupt"); + } catch (Exception e) { + LOG.warn("ERROR UNEXPECTED ", e); + break; + } + } + LOG.debug("Thread exist..."); + } + + protected abstract void write(List records); + + public long getTotalCost() { + return totalCost; + } + + public long getInsertCount() { + return insertCount; + } + + public void destroy() { + if (connHolder != null) { + connHolder.destroy(); + } + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/ColumnMetaCache.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/ColumnMetaCache.java new file mode 100644 index 0000000000..13339e0b4b --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/ColumnMetaCache.java @@ -0,0 +1,41 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.task; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.List; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Triple; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.alibaba.datax.plugin.rdbms.util.DBUtil; + +public class ColumnMetaCache { + private static final Logger LOG = LoggerFactory.getLogger(ColumnMetaCache.class); + + private static String tableName; + private static Triple, List, List> columnMeta = null; + + public ColumnMetaCache() { + + } + + public static void init(Connection connection, final String tableName, final List columns) throws SQLException { + if (columnMeta == null) { + synchronized(ColumnMetaCache.class) { + ColumnMetaCache.tableName = tableName; + if (columnMeta == null) { + columnMeta = DBUtil.getColumnMetaData(connection, + tableName, StringUtils.join(columns, ",")); + LOG.info("fetch columnMeta of table {} success", tableName); + } + } + } + } + + public static Triple, List, List> getColumnMeta() { + return columnMeta; + } + +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/ConcurrentTableWriterTask.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/ConcurrentTableWriterTask.java new file mode 100644 index 0000000000..dae31d1ab1 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/ConcurrentTableWriterTask.java @@ -0,0 +1,637 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.task; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.Config; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.AbstractConnHolder; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.ObClientConnHolder; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.ServerConnectInfo; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.part.IObPartCalculator; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.part.ObPartitionCalculatorV1; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.part.ObPartitionCalculatorV2; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.util.ObWriterUtils; + +import com.oceanbase.partition.calculator.enums.ObServerMode; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Types; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static com.alibaba.datax.plugin.writer.oceanbasev10writer.Config.DEFAULT_SLOW_MEMSTORE_THRESHOLD; +import static com.alibaba.datax.plugin.writer.oceanbasev10writer.util.ObWriterUtils.LoadMode.FAST; +import static com.alibaba.datax.plugin.writer.oceanbasev10writer.util.ObWriterUtils.LoadMode.PAUSE; +import static com.alibaba.datax.plugin.writer.oceanbasev10writer.util.ObWriterUtils.LoadMode.SLOW; + +public class ConcurrentTableWriterTask extends CommonRdbmsWriter.Task { + private static final Logger LOG = LoggerFactory.getLogger(ConcurrentTableWriterTask.class); + + // memstore_total 与 memstore_limit 比例的阈值,一旦超过这个值,则暂停写入 + private double memstoreThreshold = Config.DEFAULT_MEMSTORE_THRESHOLD; + // memstore检查的间隔 + private long memstoreCheckIntervalSecond = Config.DEFAULT_MEMSTORE_CHECK_INTERVAL_SECOND; + // 最后一次检查 + private long lastCheckMemstoreTime; + + private volatile ObWriterUtils.LoadMode loadMode = FAST; + + private static AtomicLong totalTask = new AtomicLong(0); + private long taskId = -1; + private AtomicBoolean isMemStoreFull = new AtomicBoolean(false); + private HashMap> groupInsertValues; + private IObPartCalculator obPartCalculator; + private ConcurrentTableWriter concurrentWriter = null; + private AbstractConnHolder connHolder; + private boolean allTaskInQueue = false; + private Lock lock = new ReentrantLock(); + private Condition condition = lock.newCondition(); + private long startTime; + private String obWriteMode = "update"; + private boolean isOracleCompatibleMode = false; + private String obUpdateColumns = null; + private String dbName; + private int calPartFailedCount = 0; + private boolean directPath; + + public ConcurrentTableWriterTask(DataBaseType dataBaseType) { + super(dataBaseType); + taskId = totalTask.getAndIncrement(); + } + + @Override + public void init(Configuration config) { + super.init(config); + this.directPath = config.getBool(Config.DIRECT_PATH, false); + // OceanBase 所有操作都是 insert into on duplicate key update 模式 + // writeMode应该使用enum来定义 + this.writeMode = "update"; + obWriteMode = config.getString(Config.OB_WRITE_MODE, "update"); + ServerConnectInfo connectInfo = new ServerConnectInfo(jdbcUrl, username, password, config); + connectInfo.setRpcPort(config.getInt(Config.RPC_PORT, 0)); + dbName = connectInfo.databaseName; + //init check memstore + this.memstoreThreshold = config.getDouble(Config.MEMSTORE_THRESHOLD, Config.DEFAULT_MEMSTORE_THRESHOLD); + this.memstoreCheckIntervalSecond = config.getLong(Config.MEMSTORE_CHECK_INTERVAL_SECOND, + Config.DEFAULT_MEMSTORE_CHECK_INTERVAL_SECOND); + + this.connHolder = new ObClientConnHolder(config, connectInfo.jdbcUrl, + connectInfo.getFullUserName(), connectInfo.password); + this.isOracleCompatibleMode = ObWriterUtils.isOracleMode(); + if (isOracleCompatibleMode) { + connectInfo.databaseName = connectInfo.databaseName.toUpperCase(); + //在转义的情况下不翻译 + if (!(table.startsWith("\"") && table.endsWith("\""))) { + table = table.toUpperCase(); + } + + LOG.info(String.format("this is oracle compatible mode, change database to %s, table to %s", + connectInfo.databaseName, table)); + } + + if (config.getBool(Config.USE_PART_CALCULATOR, Config.DEFAULT_USE_PART_CALCULATOR)) { + this.obPartCalculator = createPartitionCalculator(connectInfo, ObServerMode.from(config.getString(Config.OB_COMPATIBLE_MODE), config.getString(Config.OB_VERSION))); + } else { + LOG.info("Disable partition calculation feature."); + } + + obUpdateColumns = config.getString(Config.OB_UPDATE_COLUMNS, null); + groupInsertValues = new HashMap>(); + rewriteSql(); + + if (null == concurrentWriter) { + concurrentWriter = new ConcurrentTableWriter(config, connectInfo, writeRecordSql); + allTaskInQueue = false; + } + } + + /** + * 创建需要的分区计算组件 + * + * @param connectInfo + * @return + */ + private IObPartCalculator createPartitionCalculator(ServerConnectInfo connectInfo, ObServerMode obServerMode) { + if (obServerMode.isSubsequentFrom("3.0.0.0")) { + LOG.info("oceanbase version is {}, use ob-partition-calculator to calculate partition Id.", obServerMode.getVersion()); + return new ObPartitionCalculatorV2(connectInfo, table, obServerMode, columns); + } + + LOG.info("oceanbase version is {}, use ocj to calculate partition Id.", obServerMode.getVersion()); + return new ObPartitionCalculatorV1(connectInfo, table, columns); + } + + public boolean isFinished() { + return allTaskInQueue && concurrentWriter.checkFinish(); + } + + public boolean allTaskInQueue() { + return allTaskInQueue; + } + + public void setPutAllTaskInQueue() { + this.allTaskInQueue = true; + LOG.info("ConcurrentTableWriter has put all task in queue, queueSize = {}, total = {}, finished = {}", + concurrentWriter.getTaskQueueSize(), + concurrentWriter.getTotalTaskCount(), + concurrentWriter.getFinishTaskCount()); + } + + private void rewriteSql() { + Connection conn = connHolder.initConnection(); + if (isOracleCompatibleMode && obWriteMode.equalsIgnoreCase("update")) { + // change obWriteMode to insert so the insert statement will be generated. + obWriteMode = "insert"; + } + this.writeRecordSql = ObWriterUtils.buildWriteSql(table, columns, conn, obWriteMode, obUpdateColumns); + LOG.info("writeRecordSql :{}", this.writeRecordSql); + } + + @Override + public void prepare(Configuration writerSliceConfig) { + super.prepare(writerSliceConfig); + concurrentWriter.start(); + } + + @Override + public void startWriteWithConnection(RecordReceiver recordReceiver, TaskPluginCollector taskPluginCollector, Connection connection) { + this.taskPluginCollector = taskPluginCollector; + + // 用于写入数据的时候的类型根据目的表字段类型转换 + int retryTimes = 0; + boolean needRetry = false; + do { + try { + if (retryTimes > 0) { + TimeUnit.SECONDS.sleep((1 << retryTimes)); + DBUtil.closeDBResources(null, connection); + connection = DBUtil.getConnection(dataBaseType, jdbcUrl, username, password); + LOG.warn("getColumnMetaData of table {} failed, retry the {} times ...", this.table, retryTimes); + } + ColumnMetaCache.init(connection, this.table, this.columns); + this.resultSetMetaData = ColumnMetaCache.getColumnMeta(); + needRetry = false; + } catch (SQLException e) { + needRetry = true; + ++retryTimes; + e.printStackTrace(); + LOG.warn("fetch column meta of [{}] failed..., retry {} times", this.table, retryTimes); + } catch (InterruptedException e) { + LOG.warn("startWriteWithConnection interrupt, ignored"); + } finally { + } + } while (needRetry && retryTimes < 100); + + try { + Record record; + startTime = System.currentTimeMillis(); + while ((record = recordReceiver.getFromReader()) != null) { + if (record.getColumnNumber() != this.columnNumber) { + // 源头读取字段列数与目的表字段写入列数不相等,直接报错 + LOG.error("column not equal {} != {}, record = {}", + this.columnNumber, record.getColumnNumber(), record.toString()); + throw DataXException + .asDataXException( + DBUtilErrorCode.CONF_ERROR, + String.format("Recoverable exception in OB. Roll back this write and hibernate for one minute. SQLState: %d. ErrorCode: %d", + record.getColumnNumber(), + this.columnNumber)); + } + addRecordToCache(record); + } + addLeftRecords(); + waitTaskFinish(); + } catch (Exception e) { + throw DataXException.asDataXException( + DBUtilErrorCode.WRITE_DATA_ERROR, e); + } finally { + DBUtil.closeDBResources(null, null, connection); + } + } + + public PreparedStatement fillStatement(PreparedStatement preparedStatement, Record record) + throws SQLException { + return fillPreparedStatement(preparedStatement, record); + } + + private void addLeftRecords() { + //不需要刷新Cache,已经是最后一批数据了 + for (List groupValues : groupInsertValues.values()) { + if (groupValues.size() > 0) { + addRecordsToWriteQueue(groupValues); + } + } + } + + private void addRecordToCache(final Record record) { + Long partId = null; + try { + partId = obPartCalculator == null ? Long.MAX_VALUE : obPartCalculator.calculate(record); + } catch (Exception e1) { + if (calPartFailedCount++ < 10) { + LOG.warn("fail to get partition id: " + e1.getMessage() + ", record: " + record); + } + } + + if (partId == null) { + LOG.debug("fail to calculate parition id, just put into the default buffer."); + partId = Long.MAX_VALUE; + } + + List groupValues = groupInsertValues.computeIfAbsent(partId, k -> new ArrayList(batchSize)); + groupValues.add(record); + if (groupValues.size() >= batchSize) { + groupValues = addRecordsToWriteQueue(groupValues); + groupInsertValues.put(partId, groupValues); + } + } + + /** + * @param records + * @return 返回一个新的Cache用于存储接下来的数据 + */ + private List addRecordsToWriteQueue(List records) { + int i = 0; + while (true) { + if (i > 0) { + LOG.info("retry add batch record the {} times", i); + } + try { + concurrentWriter.addBatchRecords(records); + break; + } catch (InterruptedException e) { + i++; + LOG.info("Concurrent table writer is interrupted"); + } + } + return new ArrayList(batchSize); + } + + private void checkMemStore() { + Connection checkConn = connHolder.getConn(); + try { + if (checkConn == null || checkConn.isClosed()) { + checkConn = connHolder.reconnect(); + } + } catch (Exception e) { + LOG.warn("Check connection is unusable"); + } + + long now = System.currentTimeMillis(); + if (now - lastCheckMemstoreTime < 1000 * memstoreCheckIntervalSecond) { + return; + } + double memUsedRatio = ObWriterUtils.queryMemUsedRatio(checkConn); + if (memUsedRatio >= DEFAULT_SLOW_MEMSTORE_THRESHOLD) { + this.loadMode = memUsedRatio >= memstoreThreshold ? PAUSE : SLOW; + LOG.info("Memstore used ration is {}. Load data {}", memUsedRatio, loadMode.name()); + } else { + this.loadMode = FAST; + } + lastCheckMemstoreTime = now; + } + + public boolean isMemStoreFull() { + return isMemStoreFull.get(); + } + + public boolean isShouldPause() { + return this.loadMode.equals(PAUSE); + } + + public boolean isShouldSlow() { + return this.loadMode.equals(SLOW); + } + + public void print() { + if (LOG.isDebugEnabled()) { + LOG.debug("Statistic total task {}, finished {}, queue Size {}", + concurrentWriter.getTotalTaskCount(), + concurrentWriter.getFinishTaskCount(), + concurrentWriter.getTaskQueueSize()); + concurrentWriter.printStatistics(); + } + } + + public void waitTaskFinish() { + setPutAllTaskInQueue(); + lock.lock(); + try { + while (!concurrentWriter.checkFinish()) { + condition.await(15, TimeUnit.SECONDS); + print(); + checkMemStore(); + } + if (directPath){ + concurrentWriter.doCommit(); + } + } catch (InterruptedException e) { + LOG.warn("Concurrent table writer wait task finish interrupt"); + } finally { + lock.unlock(); + } + LOG.debug("wait all InsertTask finished ..."); + } + + public void singalTaskFinish() { + lock.lock(); + condition.signal(); + lock.unlock(); + } + + @Override + public void destroy(Configuration writerSliceConfig) { + if (concurrentWriter != null) { + concurrentWriter.destory(); + } + // 把本级持有的conn关闭掉 + DBUtil.closeDBResources(null, connHolder.getConn()); + super.destroy(writerSliceConfig); + } + + public class ConcurrentTableWriter { + private BlockingQueue> queue; + private List abstractInsertTasks; + private Configuration config; + private ServerConnectInfo connectInfo; + private String rewriteRecordSql; + private AtomicLong totalTaskCount; + private AtomicLong finishTaskCount; + private final int threadCount; + + public ConcurrentTableWriter(Configuration config, ServerConnectInfo connInfo, String rewriteRecordSql) { + threadCount = config.getInt(Config.WRITER_THREAD_COUNT, Config.DEFAULT_WRITER_THREAD_COUNT); + queue = new LinkedBlockingQueue>(threadCount << 1); + abstractInsertTasks = new ArrayList(threadCount); + this.config = config; + this.connectInfo = connInfo; + this.rewriteRecordSql = rewriteRecordSql; + this.totalTaskCount = new AtomicLong(0); + this.finishTaskCount = new AtomicLong(0); + } + + public long getTotalTaskCount() { + return totalTaskCount.get(); + } + + public long getFinishTaskCount() { + return finishTaskCount.get(); + } + + public int getTaskQueueSize() { + return queue.size(); + } + + public void increFinishCount() { + finishTaskCount.incrementAndGet(); + } + + //should check after put all the task in the queue + public boolean checkFinish() { + long finishCount = finishTaskCount.get(); + long totalCount = totalTaskCount.get(); + return finishCount == totalCount; + } + + public synchronized void start() { + for (int i = 0; i < threadCount; ++i) { + LOG.info("start {} insert task.", (i + 1)); + AbstractInsertTask insertTask = null; + if (directPath) { + insertTask = new DirectPathInsertTask(taskId, queue, config, connectInfo, ConcurrentTableWriterTask.this, this); + } else { + insertTask = new InsertTask(taskId, queue, config, connectInfo, rewriteRecordSql); + } + insertTask.setWriterTask(ConcurrentTableWriterTask.this); + insertTask.setWriter(this); + abstractInsertTasks.add(insertTask); + } + WriterThreadPool.executeBatch(abstractInsertTasks); + } + + public void doCommit() { + this.abstractInsertTasks.get(0).getConnHolder().doCommit(); + } + + public int getThreadCount() { + return threadCount; + } + + public void printStatistics() { + long insertTotalCost = 0; + long insertTotalCount = 0; + for (AbstractInsertTask task : abstractInsertTasks) { + insertTotalCost += task.getTotalCost(); + insertTotalCount += task.getInsertCount(); + } + long avgCost = 0; + if (insertTotalCount != 0) { + avgCost = insertTotalCost / insertTotalCount; + } + ConcurrentTableWriterTask.LOG.debug("Insert {} times, totalCost {} ms, average {} ms", + insertTotalCount, insertTotalCost, avgCost); + } + + public void addBatchRecords(final List records) throws InterruptedException { + boolean isSucc = false; + while (!isSucc) { + isSucc = queue.offer(records, 5, TimeUnit.MILLISECONDS); + checkMemStore(); + } + totalTaskCount.incrementAndGet(); + } + + public synchronized void destory() { + if (abstractInsertTasks != null) { + for (AbstractInsertTask task : abstractInsertTasks) { + task.setStop(); + } + for (AbstractInsertTask task : abstractInsertTasks) { + task.destroy(); + } + } + } + } + + public String getTable() { + return table; + } + + // 直接使用了两个类变量:columnNumber,resultSetMetaData + protected PreparedStatement fillPreparedStatement(PreparedStatement preparedStatement, Record record) + throws SQLException { + for (int i = 0; i < this.columnNumber; i++) { + int columnSqltype = this.resultSetMetaData.getMiddle().get(i); + String typeName = this.resultSetMetaData.getRight().get(i); + preparedStatement = fillPreparedStatementColumnType(preparedStatement, i, columnSqltype, typeName, record.getColumn(i)); + } + + return preparedStatement; + } + + protected PreparedStatement fillPreparedStatementColumnType(PreparedStatement preparedStatement, int columnIndex, + int columnSqltype, String typeName, Column column) throws SQLException { + java.util.Date utilDate; + switch (columnSqltype) { + case Types.CHAR: + case Types.NCHAR: + case Types.CLOB: + case Types.NCLOB: + case Types.VARCHAR: + case Types.LONGVARCHAR: + case Types.NVARCHAR: + case Types.LONGNVARCHAR: + preparedStatement.setString(columnIndex + 1, column + .asString()); + break; + + case Types.SMALLINT: + case Types.INTEGER: + case Types.BIGINT: + case Types.NUMERIC: + case Types.DECIMAL: + case Types.FLOAT: + case Types.REAL: + case Types.DOUBLE: + String strValue = column.asString(); + if (emptyAsNull && "".equals(strValue)) { + preparedStatement.setString(columnIndex + 1, null); + } else { + preparedStatement.setString(columnIndex + 1, strValue); + } + break; + + //tinyint is a little special in some database like mysql {boolean->tinyint(1)} + case Types.TINYINT: + Long longValue = column.asLong(); + if (null == longValue) { + preparedStatement.setString(columnIndex + 1, null); + } else { + preparedStatement.setString(columnIndex + 1, longValue.toString()); + } + break; + + // for mysql bug, see http://bugs.mysql.com/bug.php?id=35115 + case Types.DATE: + if (typeName == null) { + typeName = this.resultSetMetaData.getRight().get(columnIndex); + } + + if (typeName.equalsIgnoreCase("year")) { + if (column.asBigInteger() == null) { + preparedStatement.setString(columnIndex + 1, null); + } else { + preparedStatement.setInt(columnIndex + 1, column.asBigInteger().intValue()); + } + } else { + java.sql.Date sqlDate = null; + try { + utilDate = column.asDate(); + } catch (DataXException e) { + throw new SQLException(String.format( + "Date 类型转换错误:[%s]", column)); + } + + if (null != utilDate) { + sqlDate = new java.sql.Date(utilDate.getTime()); + } + preparedStatement.setDate(columnIndex + 1, sqlDate); + } + break; + + case Types.TIME: + java.sql.Time sqlTime = null; + try { + utilDate = column.asDate(); + } catch (DataXException e) { + throw new SQLException(String.format( + "TIME 类型转换错误:[%s]", column)); + } + + if (null != utilDate) { + sqlTime = new java.sql.Time(utilDate.getTime()); + } + preparedStatement.setTime(columnIndex + 1, sqlTime); + break; + + case Types.TIMESTAMP: + java.sql.Timestamp sqlTimestamp = null; + try { + utilDate = column.asDate(); + } catch (DataXException e) { + throw new SQLException(String.format( + "TIMESTAMP 类型转换错误:[%s]", column)); + } + + if (null != utilDate) { + sqlTimestamp = new java.sql.Timestamp( + utilDate.getTime()); + } + preparedStatement.setTimestamp(columnIndex + 1, sqlTimestamp); + break; + case Types.VARBINARY: + case Types.BLOB: + case Types.LONGVARBINARY: + preparedStatement.setBytes(columnIndex + 1, column + .asBytes()); + break; + case Types.BINARY: + String isArray = column.getRawData().toString(); + if (isArray.startsWith("[") && isArray.endsWith("]")) { + preparedStatement.setString(columnIndex + 1, column + .asString()); + } else { + preparedStatement.setBytes(columnIndex + 1, column + .asBytes()); + } + break; + case Types.BOOLEAN: + preparedStatement.setBoolean(columnIndex + 1, column.asBoolean()); + break; + + // warn: bit(1) -> Types.BIT 可使用setBoolean + // warn: bit(>1) -> Types.VARBINARY 可使用setBytes + case Types.BIT: + if (this.dataBaseType == DataBaseType.MySql) { + preparedStatement.setBoolean(columnIndex + 1, column.asBoolean()); + } else { + preparedStatement.setString(columnIndex + 1, column.asString()); + } + break; + default: + throw DataXException + .asDataXException( + DBUtilErrorCode.UNSUPPORTED_TYPE, + String.format( + "您的配置文件中的列配置信息有误. 因为DataX 不支持数据库写入这种字段类型. 字段名:[%s], 字段类型:[%d], 字段Java类型:[%s]. 请修改表中该字段的类型或者不同步该字段.", + this.resultSetMetaData.getLeft() + .get(columnIndex), + this.resultSetMetaData.getMiddle() + .get(columnIndex), + this.resultSetMetaData.getRight() + .get(columnIndex))); + } + return preparedStatement; + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/DirectPathInsertTask.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/DirectPathInsertTask.java new file mode 100644 index 0000000000..185321580e --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/DirectPathInsertTask.java @@ -0,0 +1,76 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.task; + +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Queue; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Column.Type; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.common.Table; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.common.TableCache; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.directPath.DirectPathConnection; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.directPath.DirectPathPreparedStatement; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.DirectPathConnHolder; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.ServerConnectInfo; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DirectPathInsertTask extends AbstractInsertTask { + private static final Logger LOG = LoggerFactory.getLogger(DirectPathInsertTask.class); + + public DirectPathInsertTask(long taskId, Queue> recordsQueue, Configuration config, ServerConnectInfo connectInfo, ConcurrentTableWriterTask task, ConcurrentTableWriterTask.ConcurrentTableWriter writer) { + super(taskId, recordsQueue, config, connectInfo, task, writer); + } + + @Override + protected void initConnHolder() { + this.connHolder = new DirectPathConnHolder(config, connInfo, writerTask.getTable(), writer.getThreadCount()); + this.connHolder.initConnection(); + } + + @Override + protected void write(List records) { + Table table = TableCache.getInstance().getTable(connInfo.databaseName, writerTask.getTable()); + if (Table.Status.FAILURE.equals(table.getStatus())) { + return; + } + DirectPathConnection conn = (DirectPathConnection) connHolder.getConn(); + if (records != null && !records.isEmpty()) { + long startTime = System.currentTimeMillis(); + try (DirectPathPreparedStatement stmt = conn.createStatement()) { + final int columnNumber = records.get(0).getColumnNumber(); + Object[] values = new Object[columnNumber]; + for (Record record : records) { + for (int i = 0; i < columnNumber; i++) { + Column column = record.getColumn(i); + //处理一下时间类型 + if (column.getType().equals(Type.DATE)) { + values[i] = record.getColumn(i).asString(); + } else { + values[i] = record.getColumn(i).getRawData(); + } + } + stmt.addBatch(values); + } + + int[] result = stmt.executeBatch(); + + if (LOG.isDebugEnabled()) { + LOG.debug("[{}] Insert {} rows success", Thread.currentThread().getName(), Arrays.stream(result).sum()); + } + calStatistic(System.currentTimeMillis() - startTime); + stmt.clearBatch(); + } catch (Throwable ex) { + String msg = MessageFormat.format("Insert data into table \"{0}\" failed. Error: {1}", writerTask.getTable(), ex.getMessage()); + LOG.error(msg, ex); + table.setError(ex); + table.setStatus(Table.Status.FAILURE); + throw new RuntimeException(msg); + } + } + } +} \ No newline at end of file diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/InsertTask.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/InsertTask.java new file mode 100644 index 0000000000..1524d76f31 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/InsertTask.java @@ -0,0 +1,213 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.task; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.Config; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.AbstractConnHolder; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.ObClientConnHolder; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.ServerConnectInfo; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.task.ConcurrentTableWriterTask.ConcurrentTableWriter; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.util.ObWriterUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; + +public class InsertTask extends AbstractInsertTask implements Runnable { + + private static final Logger LOG = LoggerFactory.getLogger(InsertTask.class); + + private ConcurrentTableWriterTask writerTask; + private ConcurrentTableWriter writer; + + private String writeRecordSql; + private long totalCost = 0; + private long insertCount = 0; + + private BlockingQueue> queue; + private boolean isStop; + private AbstractConnHolder connHolder; + + private final long taskId; + private ServerConnectInfo connInfo; + + // 失败重试次数 + private int failTryCount = Config.DEFAULT_FAIL_TRY_COUNT; + private boolean printCost = Config.DEFAULT_PRINT_COST; + private long costBound = Config.DEFAULT_COST_BOUND; + + public InsertTask( + final long taskId, + BlockingQueue> recordsQueue, + Configuration config, + ServerConnectInfo connectInfo, + String writeRecordSql) { + super(taskId, recordsQueue, config, connectInfo); + this.taskId = taskId; + this.queue = recordsQueue; + this.connInfo = connectInfo; + failTryCount = config.getInt(Config.FAIL_TRY_COUNT, Config.DEFAULT_FAIL_TRY_COUNT); + printCost = config.getBool(Config.PRINT_COST, Config.DEFAULT_PRINT_COST); + costBound = config.getLong(Config.COST_BOUND, Config.DEFAULT_COST_BOUND); + this.connHolder = new ObClientConnHolder(config, connInfo.jdbcUrl, + connInfo.getFullUserName(), connInfo.password); + this.writeRecordSql = writeRecordSql; + this.isStop = false; + connHolder.initConnection(); + } + + protected void initConnHolder() { + + } + + public void setWriterTask(ConcurrentTableWriterTask writerTask) { + this.writerTask = writerTask; + } + + public void setWriter(ConcurrentTableWriter writer) { + this.writer = writer; + } + + private boolean isStop() { + return isStop; + } + + public void setStop() { + isStop = true; + } + + public long getTotalCost() { + return totalCost; + } + + public long getInsertCount() { + return insertCount; + } + + @Override + public void run() { + Thread.currentThread().setName(String.format("%d-insertTask-%d", taskId, Thread.currentThread().getId())); + LOG.debug("Task {} start to execute...", taskId); + while (!isStop()) { + try { + List records = queue.poll(5, TimeUnit.MILLISECONDS); + if (null != records) { + doMultiInsert(records, this.printCost, this.costBound); + } else if (writerTask.isFinished()) { + writerTask.singalTaskFinish(); + LOG.debug("not more task, thread exist ..."); + break; + } + } catch (InterruptedException e) { + LOG.debug("TableWriter is interrupt"); + } catch (Exception e) { + LOG.warn("ERROR UNEXPECTED ", e); + } + } + LOG.debug("Thread exist..."); + } + + protected void write(List records) { + + } + + public void destroy() { + connHolder.destroy(); + } + + public void calStatistic(final long cost) { + writer.increFinishCount(); + ++insertCount; + totalCost += cost; + if (this.printCost && cost > this.costBound) { + LOG.info("slow multi insert cost {}ms", cost); + } + } + + public void doMultiInsert(final List buffer, final boolean printCost, final long restrict) { + checkMemstore(); + Connection conn = connHolder.getConn(); + boolean success = false; + long cost = 0; + long startTime = 0; + try { + for (int i = 0; i < failTryCount; ++i) { + if (i > 0) { + conn = connHolder.getConn(); + LOG.info("retry {}, start do batch insert, size={}", i, buffer.size()); + checkMemstore(); + } + startTime = System.currentTimeMillis(); + PreparedStatement ps = null; + try { + conn.setAutoCommit(false); + ps = conn.prepareStatement(writeRecordSql); + for (Record record : buffer) { + ps = writerTask.fillStatement(ps, record); + ps.addBatch(); + } + ps.executeBatch(); + conn.commit(); + success = true; + cost = System.currentTimeMillis() - startTime; + calStatistic(cost); + break; + } catch (SQLException e) { + LOG.warn("Insert fatal error SqlState ={}, errorCode = {}, {}", e.getSQLState(), e.getErrorCode(), e); + if (LOG.isDebugEnabled() && (i == 0 || i > 10)) { + for (Record record : buffer) { + LOG.warn("ERROR : record {}", record); + } + } + // 按照错误码分类,分情况处理 + // 如果是OB系统级异常,则需要重建连接 + boolean fatalFail = ObWriterUtils.isFatalError(e); + if (fatalFail) { + ObWriterUtils.sleep(300000); + connHolder.reconnect(); + // 如果是可恢复的异常,则重试 + } else if (ObWriterUtils.isRecoverableError(e)) { + conn.rollback(); + ObWriterUtils.sleep(60000); + } else {// 其它异常直接退出,采用逐条写入方式 + conn.rollback(); + ObWriterUtils.sleep(1000); + break; + } + } catch (Exception e) { + e.printStackTrace(); + LOG.warn("Insert error unexpected {}", e); + } finally { + DBUtil.closeDBResources(ps, null); + } + } + } catch (SQLException e) { + LOG.warn("ERROR:retry failSql State ={}, errorCode = {}, {}", e.getSQLState(), e.getErrorCode(), e); + } + + if (!success) { + LOG.info("do one insert"); + conn = connHolder.reconnect(); + writerTask.doOneInsert(conn, buffer); + cost = System.currentTimeMillis() - startTime; + calStatistic(cost); + } + } + + private void checkMemstore() { + if (writerTask.isShouldSlow()) { + ObWriterUtils.sleep(100); + } else { + while (writerTask.isShouldPause()) { + ObWriterUtils.sleep(100); + } + } + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/SingleTableWriterTask.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/SingleTableWriterTask.java new file mode 100644 index 0000000000..d2f42de557 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/SingleTableWriterTask.java @@ -0,0 +1,152 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.task; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter; +import com.alibaba.datax.plugin.rdbms.writer.Key; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.Config; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.AbstractConnHolder; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.ext.ObClientConnHolder; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.util.ObWriterUtils; + +public class SingleTableWriterTask extends CommonRdbmsWriter.Task { + + // memstore_total 与 memstore_limit 比例的阈值,一旦超过这个值,则暂停写入 + private double memstoreThreshold = Config.DEFAULT_MEMSTORE_THRESHOLD; + + // memstore检查的间隔 + private long memstoreCheckIntervalSecond = Config.DEFAULT_MEMSTORE_CHECK_INTERVAL_SECOND; + + // 最后一次检查 + private long lastCheckMemstoreTime; + + // 失败重试次数 + private int failTryCount = Config.DEFAULT_FAIL_TRY_COUNT; + + private AbstractConnHolder connHolder; + private String obWriteMode = "update"; + private boolean isOracleCompatibleMode = false; + private String obUpdateColumns = null; + + public SingleTableWriterTask(DataBaseType dataBaseType) { + super(dataBaseType); + } + + @Override + public void init(Configuration config) { + super.init(config); + this.memstoreThreshold = config.getDouble(Config.MEMSTORE_THRESHOLD, Config.DEFAULT_MEMSTORE_THRESHOLD); + this.memstoreCheckIntervalSecond = config.getLong(Config.MEMSTORE_CHECK_INTERVAL_SECOND, + Config.DEFAULT_MEMSTORE_CHECK_INTERVAL_SECOND); + failTryCount = config.getInt(Config.FAIL_TRY_COUNT, Config.DEFAULT_FAIL_TRY_COUNT); + // OceanBase 所有操作都是 insert into on duplicate key update 模式 + // writeMode应该使用enum来定义 + this.writeMode = "update"; + this.connHolder = new ObClientConnHolder(config, jdbcUrl, username, password); + //ob1.0里面, + this.batchSize = Math.min(128, config.getInt(Key.BATCH_SIZE, 128)); + LOG.info("In Write OceanBase 1.0, Real Batch Size : " + this.batchSize); + + isOracleCompatibleMode = ObWriterUtils.isOracleMode(); + LOG.info("isOracleCompatibleMode=" + isOracleCompatibleMode); + + obUpdateColumns = config.getString(Config.OB_UPDATE_COLUMNS, null); + + obWriteMode = config.getString(Config.OB_WRITE_MODE, "update"); + if (isOracleCompatibleMode) { + obWriteMode = "insert"; + } + rewriteSql(); + } + + private void rewriteSql() { + Connection conn = connHolder.initConnection(); + this.writeRecordSql = ObWriterUtils.buildWriteSql(table, columns, conn, obWriteMode, obUpdateColumns); + } + + protected void doBatchInsert(Connection conn, List buffer) throws SQLException { + doBatchInsert(buffer); + } + + private void doBatchInsert(List buffer) { + Connection conn = connHolder.getConn(); + // 检查内存 + checkMemstore(conn); + boolean success = false; + try { + for (int i = 0; i < failTryCount; i++) { + PreparedStatement ps = null; + try { + conn.setAutoCommit(false); + ps = conn.prepareStatement(this.writeRecordSql); + for (Record record : buffer) { + ps = fillPreparedStatement(ps, record); + ps.addBatch(); + } + ps.executeBatch(); + conn.commit(); + // 标记执行正常,且退出for循环 + success = true; + break; + } catch (SQLException e) { + // 如果是OB系统级异常,则需要重建连接 + boolean fatalFail = ObWriterUtils.isFatalError(e); + if (fatalFail) { + LOG.warn("Fatal exception in OB. Roll back this write and hibernate for five minutes. SQLState: {}. ErrorCode: {}", + e.getSQLState(), e.getErrorCode(), e); + ObWriterUtils.sleep(300000); + DBUtil.closeDBResources(null, conn); + conn = connHolder.reconnect(); + // 如果是可恢复的异常,则重试 + } else if (ObWriterUtils.isRecoverableError(e)) { + LOG.warn("Recoverable exception in OB. Roll back this write and hibernate for one minute. SQLState: {}. ErrorCode: {}", + e.getSQLState(), e.getErrorCode(), e); + conn.rollback(); + ObWriterUtils.sleep(60000); + // 其它异常直接退出,采用逐条写入方式 + } else { + LOG.warn("Exception in OB. Roll back this write and hibernate for one second. Write and submit the records one by one. SQLState: {}. ErrorCode: {}", + e.getSQLState(), e.getErrorCode(), e); + conn.rollback(); + ObWriterUtils.sleep(1000); + break; + } + } finally { + DBUtil.closeDBResources(ps, null); + } + } + } catch (SQLException e) { + LOG.warn("Exception in OB. Roll back this write. Write and submit the records one by one. SQLState: {}. ErrorCode: {}", + e.getSQLState(), e.getErrorCode(), e); + } + if (!success) { + doOneInsert(conn, buffer); + } + } + + private void checkMemstore(Connection conn) { + long now = System.currentTimeMillis(); + if (now - lastCheckMemstoreTime < 1000 * memstoreCheckIntervalSecond) { + return; + } + while (ObWriterUtils.isMemstoreFull(conn, memstoreThreshold)) { + LOG.warn("OB memstore is full,sleep 60 seconds, threshold=" + memstoreThreshold); + ObWriterUtils.sleep(60000); + } + lastCheckMemstoreTime = now; + } + + @Override + public void destroy(Configuration writerSliceConfig) { + // 把本级持有的conn关闭掉 + DBUtil.closeDBResources(null, connHolder.getConn()); + super.destroy(writerSliceConfig); + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/WriterThreadPool.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/WriterThreadPool.java new file mode 100644 index 0000000000..90657373e9 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/task/WriterThreadPool.java @@ -0,0 +1,38 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.task; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.checkerframework.checker.units.qual.A; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WriterThreadPool { + private static final Logger LOG = LoggerFactory.getLogger(InsertTask.class); + + private static ExecutorService executorService = Executors.newCachedThreadPool(); + + public WriterThreadPool() { + } + + public static ExecutorService getInstance() { + return executorService; + } + + public static synchronized void shutdown() { + LOG.info("start shutdown executor service..."); + executorService.shutdown(); + LOG.info("shutdown executor service success..."); + } + + public static synchronized void execute(InsertTask task) { + executorService.execute(task); + } + + public static synchronized void executeBatch(List tasks) { + for (AbstractInsertTask task : tasks) { + executorService.execute(task); + } + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/util/DbUtils.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/util/DbUtils.java new file mode 100644 index 0000000000..adffc6f76f --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/util/DbUtils.java @@ -0,0 +1,113 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.util; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.writer.Constant; +import com.alibaba.datax.plugin.rdbms.writer.Key; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.Config; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DbUtils { + + protected static final Logger LOG = LoggerFactory.getLogger(DbUtils.class); + + public static String fetchSingleValueWithRetry(Configuration config, String query) { + final String username = config.getString(Key.USERNAME); + final String password = config.getString(Key.PASSWORD); + String jdbcUrl = config.getString(Key.JDBC_URL); + + if (jdbcUrl == null) { + List conns = config.getList(Constant.CONN_MARK, Object.class); + Configuration connConf = Configuration.from(conns.get(0).toString()); + jdbcUrl = connConf.getString(Key.JDBC_URL); + } + + Connection conn = null; + PreparedStatement stmt = null; + ResultSet result = null; + String value = null; + int retry = 0; + int failTryCount = config.getInt(Config.FAIL_TRY_COUNT, Config.DEFAULT_FAIL_TRY_COUNT); + do { + try { + if (retry > 0) { + int sleep = retry > 9 ? 500 : 1 << retry; + try { + TimeUnit.SECONDS.sleep(sleep); + } catch (InterruptedException e) { + } + LOG.warn("retry fetch value for {} the {} times", query, retry); + } + conn = DBUtil.getConnection(DataBaseType.OceanBase, jdbcUrl, username, password); + stmt = conn.prepareStatement(query); + result = stmt.executeQuery(); + if (result.next()) { + value = result.getString("Value"); + } else { + throw new RuntimeException("no values returned for " + query); + } + LOG.info("value for query [{}] is [{}]", query, value); + break; + } catch (SQLException e) { + ++retry; + LOG.warn("fetch value with {} error {}", query, e); + } finally { + DBUtil.closeDBResources(result, stmt, conn); + } + } while (retry < failTryCount); + + return value; + } + + /** + * build sys connection from ordinary jdbc url + * + * @param jdbcUrl + * @param clusterName + * @return + * @throws Exception + */ + public static Connection buildSysConn(String jdbcUrl, String clusterName) throws Exception { + jdbcUrl = jdbcUrl.replace("jdbc:mysql://", "jdbc:oceanbase://"); + int startIdx = jdbcUrl.indexOf('/', "jdbc:oceanbase://".length()); + int endIdx = jdbcUrl.lastIndexOf('?'); + String prefix = jdbcUrl.substring(0, startIdx + 1); + final String postfix = jdbcUrl.substring(endIdx); + String sysJDBCUrl = prefix + "oceanbase" + postfix; + + String tenantName = "sys"; + String[][] userConfigs = { + {"monitor", "monitor"} + }; + + Connection conn = null; + for (String[] userConfig : userConfigs) { + try { + conn = DBUtil.getConnectionWithoutRetry(DataBaseType.OceanBase, sysJDBCUrl, String.format("%s@%s#%s", userConfig[0], + tenantName, clusterName), userConfig[1]); + } catch (Exception e) { + LOG.warn("fail connecting to ob: " + e.getMessage()); + + } + if (conn == null) { + LOG.warn("fail to get connection with user " + userConfig[0] + ", try alternative user."); + } else { + break; + } + } + + if (conn == null) { + throw new Exception("fail to get connection with sys tenant."); + } + + return conn; + } +} diff --git a/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/util/ObWriterUtils.java b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/util/ObWriterUtils.java new file mode 100644 index 0000000000..a5d6b0eae8 --- /dev/null +++ b/oceanbasev10writer/src/main/java/com/alibaba/datax/plugin/writer/oceanbasev10writer/util/ObWriterUtils.java @@ -0,0 +1,507 @@ +package com.alibaba.datax.plugin.writer.oceanbasev10writer.util; + +import com.alibaba.datax.plugin.rdbms.reader.util.ObVersion; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter.Task; +import com.alibaba.datax.plugin.writer.oceanbasev10writer.Config; +import org.apache.commons.lang3.RandomUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.*; +import java.util.*; +import static com.alibaba.datax.plugin.writer.oceanbasev10writer.Config.DEFAULT_SLOW_MEMSTORE_THRESHOLD; + +public class ObWriterUtils { + + private static final String MYSQL_KEYWORDS = "ACCESSIBLE,ACCOUNT,ACTION,ADD,AFTER,AGAINST,AGGREGATE,ALGORITHM,ALL,ALTER,ALWAYS,ANALYSE,AND,ANY,AS,ASC,ASCII,ASENSITIVE,AT,AUTO_INCREMENT,AUTOEXTEND_SIZE,AVG,AVG_ROW_LENGTH,BACKUP,BEFORE,BEGIN,BETWEEN,BIGINT,BINARY,BINLOG,BIT,BLOB,BLOCK,BOOL,BOOLEAN,BOTH,BTREE,BY,BYTE,CACHE,CALL,CASCADE,CASCADED,CASE,CATALOG_NAME,CHAIN,CHANGE,CHANGED,CHANNEL,CHAR,CHARACTER,CHARSET,CHECK,CHECKSUM,CIPHER,CLASS_ORIGIN,CLIENT,CLOSE,COALESCE,CODE,COLLATE,COLLATION,COLUMN,COLUMN_FORMAT,COLUMN_NAME,COLUMNS,COMMENT,COMMIT,COMMITTED,COMPACT,COMPLETION,COMPRESSED,COMPRESSION,CONCURRENT,CONDITION,CONNECTION,CONSISTENT,CONSTRAINT,CONSTRAINT_CATALOG,CONSTRAINT_NAME,CONSTRAINT_SCHEMA,CONTAINS,CONTEXT,CONTINUE,CONVERT,CPU,CREATE,CROSS,CUBE,CURRENT,CURRENT_DATE,CURRENT_TIME,CURRENT_TIMESTAMP,CURRENT_USER,CURSOR,CURSOR_NAME,DATA,DATABASE,DATABASES,DATAFILE,DATE,DATETIME,DAY,DAY_HOUR,DAY_MICROSECOND,DAY_MINUTE,DAY_SECOND,DEALLOCATE,DEC,DECIMAL,DECLARE,DEFAULT,DEFAULT_AUTH,DEFINER,DELAY_KEY_WRITE,DELAYED,DELETE,DES_KEY_FILE,DESC,DESCRIBE,DETERMINISTIC,DIAGNOSTICS,DIRECTORY,DISABLE,DISCARD,DISK,DISTINCT,DISTINCTROW,DIV,DO,DOUBLE,DROP,DUAL,DUMPFILE,DUPLICATE,DYNAMIC,EACH,ELSE,ELSEIF,ENABLE,ENCLOSED,ENCRYPTION,END,ENDS,ENGINE,ENGINES,ENUM,ERROR,ERRORS,ESCAPE,ESCAPED,EVENT,EVENTS,EVERY,EXCHANGE,EXECUTE,EXISTS,EXIT,EXPANSION,EXPIRE,EXPLAIN,EXPORT,EXTENDED,EXTENT_SIZE,FAST,FAULTS,FETCH,FIELDS,FILE,FILE_BLOCK_SIZE,FILTER,FIRST,FIXED,FLOAT,FLOAT4,FLOAT8,FLUSH,FOLLOWS,FOR,FORCE,FOREIGN,FORMAT,FOUND,FROM,FULL,FULLTEXT,FUNCTION,GENERAL,GENERATED,GEOMETRY,GEOMETRYCOLLECTION,GET,GET_FORMAT,GLOBAL,GRANT,GRANTS,GROUP,GROUP_REPLICATION,HANDLER,HASH,HAVING,HELP,HIGH_PRIORITY,HOST,HOSTS,HOUR,HOUR_MICROSECOND,HOUR_MINUTE,HOUR_SECOND,IDENTIFIED,IF,IGNORE,IGNORE_SERVER_IDS,IMPORT,IN,INDEX,INDEXES,INFILE,INITIAL_SIZE,INNER,INOUT,INSENSITIVE,INSERT,INSERT_METHOD,INSTALL,INSTANCE,INT,INT1,INT2,INT3,INT4,INT8,INTEGER,INTERVAL,INTO,INVOKER,IO,IO_AFTER_GTIDS,IO_BEFORE_GTIDS,IO_THREAD,IPC,IS,ISOLATION,ISSUER,ITERATE,JOIN,JSON,KEY,KEY_BLOCK_SIZE,KEYS,KILL,LANGUAGE,LAST,LEADING,LEAVE,LEAVES,LEFT,LESS,LEVEL,LIKE,LIMIT,LINEAR,LINES,LINESTRING,LIST,LOAD,LOCAL,LOCALTIME,LOCALTIMESTAMP,LOCK,LOCKS,LOGFILE,LOGS,LONG,LONGBLOB,LONGTEXT,LOOP,LOW_PRIORITY,MASTER,MASTER_AUTO_POSITION,MASTER_BIND,MASTER_CONNECT_RETRY,MASTER_DELAY,MASTER_HEARTBEAT_PERIOD,MASTER_HOST,MASTER_LOG_FILE,MASTER_LOG_POS,MASTER_PASSWORD,MASTER_PORT,MASTER_RETRY_COUNT,MASTER_SERVER_ID,MASTER_SSL,MASTER_SSL_CA,MASTER_SSL_CAPATH,MASTER_SSL_CERT,MASTER_SSL_CIPHER,MASTER_SSL_CRL,MASTER_SSL_CRLPATH,MASTER_SSL_KEY,MASTER_SSL_VERIFY_SERVER_CERT,MASTER_TLS_VERSION,MASTER_USER,MATCH,MAX_CONNECTIONS_PER_HOUR,MAX_QUERIES_PER_HOUR,MAX_ROWS,MAX_SIZE,MAX_STATEMENT_TIME,MAX_UPDATES_PER_HOUR,MAX_USER_CONNECTIONS,MAXVALUE,MEDIUM,MEDIUMBLOB,MEDIUMINT,MEDIUMTEXT,MEMORY,MERGE,MESSAGE_TEXT,MICROSECOND,MIDDLEINT,MIGRATE,MIN_ROWS,MINUTE,MINUTE_MICROSECOND,MINUTE_SECOND,MOD,MODE,MODIFIES,MODIFY,MONTH,MULTILINESTRING,MULTIPOINT,MULTIPOLYGON,MUTEX,MYSQL_ERRNO,NAME,NAMES,NATIONAL,NATURAL,NCHAR,NDB,NDBCLUSTER,NEVER,NEW,NEXT,NO,NO_WAIT,NO_WRITE_TO_BINLOG,NODEGROUP,NONBLOCKING,NONE,NOT,NULL,NUMBER,NUMERIC,NVARCHAR,OFFSET,OLD_PASSWORD,ON,ONE,ONLY,OPEN,OPTIMIZE,OPTIMIZER_COSTS,OPTION,OPTIONALLY,OPTIONS,OR,ORDER,OUT,OUTER,OUTFILE,OWNER,PACK_KEYS,PAGE,PARSE_GCOL_EXPR,PARSER,PARTIAL,PARTITION,PARTITIONING,PARTITIONS,PASSWORD,PHASE,PLUGIN,PLUGIN_DIR,PLUGINS,POINT,POLYGON,PORT,PRECEDES,PRECISION,PREPARE,PRESERVE,PREV,PRIMARY,PRIVILEGES,PROCEDURE,PROCESSLIST,PROFILE,PROFILES,PROXY,PURGE,QUARTER,QUERY,QUICK,RANGE,READ,READ_ONLY,READ_WRITE,READS,REAL,REBUILD,RECOVER,REDO_BUFFER_SIZE,REDOFILE,REDUNDANT,REFERENCES,REGEXP,RELAY,RELAY_LOG_FILE,RELAY_LOG_POS,RELAY_THREAD,RELAYLOG,RELEASE,RELOAD,REMOVE,RENAME,REORGANIZE,REPAIR,REPEAT,REPEATABLE,REPLACE,REPLICATE_DO_DB,REPLICATE_DO_TABLE,REPLICATE_IGNORE_DB,REPLICATE_IGNORE_TABLE,REPLICATE_REWRITE_DB,REPLICATE_WILD_DO_TABLE,REPLICATE_WILD_IGNORE_TABLE,REPLICATION,REQUIRE,RESET,RESIGNAL,RESTORE,RESTRICT,RESUME,RETURN,RETURNED_SQLSTATE,RETURNS,REVERSE,REVOKE,RIGHT,RLIKE,ROLLBACK,ROLLUP,ROTATE,ROUTINE,ROW,ROW_COUNT,ROW_FORMAT,ROWS,RTREE,SAVEPOINT,SCHEDULE,SCHEMA,SCHEMA_NAME,SCHEMAS,SECOND,SECOND_MICROSECOND,SECURITY,SELECT,SENSITIVE,SEPARATOR,SERIAL,SERIALIZABLE,SERVER,SESSION,SET,SHARE,SHOW,SHUTDOWN,SIGNAL,SIGNED,SIMPLE,SLAVE,SLOW,SMALLINT,SNAPSHOT,SOCKET,SOME,SONAME,SOUNDS,SOURCE,SPATIAL,SPECIFIC,SQL,SQL_AFTER_GTIDS,SQL_AFTER_MTS_GAPS,SQL_BEFORE_GTIDS,SQL_BIG_RESULT,SQL_BUFFER_RESULT,SQL_CACHE,SQL_CALC_FOUND_ROWS,SQL_NO_CACHE,SQL_SMALL_RESULT,SQL_THREAD,SQL_TSI_DAY,SQL_TSI_HOUR,SQL_TSI_MINUTE,SQL_TSI_MONTH,SQL_TSI_QUARTER,SQL_TSI_SECOND,SQL_TSI_WEEK,SQL_TSI_YEAR,SQLEXCEPTION,SQLSTATE,SQLWARNING,SSL,STACKED,START,STARTING,STARTS,STATS_AUTO_RECALC,STATS_PERSISTENT,STATS_SAMPLE_PAGES,STATUS,STOP,STORAGE,STORED,STRAIGHT_JOIN,STRING,SUBCLASS_ORIGIN,SUBJECT,SUBPARTITION,SUBPARTITIONS,SUPER,SUSPEND,SWAPS,SWITCHES,TABLE,TABLE_CHECKSUM,TABLE_NAME,TABLES,TABLESPACE,TEMPORARY,TEMPTABLE,TERMINATED,TEXT,THAN,THEN,TIME,TIMESTAMP,TIMESTAMPADD,TIMESTAMPDIFF,TINYBLOB,TINYINT,TINYTEXT,TO,TRAILING,TRANSACTION,TRIGGER,TRIGGERS,TRUNCATE,TYPE,TYPES,UNCOMMITTED,UNDEFINED,UNDO,UNDO_BUFFER_SIZE,UNDOFILE,UNICODE,UNINSTALL,UNION,UNIQUE,UNKNOWN,UNLOCK,UNSIGNED,UNTIL,UPDATE,UPGRADE,USAGE,USE,USE_FRM,USER,USER_RESOURCES,USING,UTC_DATE,UTC_TIME,UTC_TIMESTAMP,VALIDATION,VALUE,VALUES,VARBINARY,VARCHAR,VARCHARACTER,VARIABLES,VARYING,VIEW,VIRTUAL,WAIT,WARNINGS,WEEK,WEIGHT_STRING,WHEN,WHERE,WHILE,WITH,WITHOUT,WORK,WRAPPER,WRITE,X509,XA,XID,XML,XOR,YEAR,YEAR_MONTH,ZEROFILL,FALSE,TRUE"; + private static final String ORACLE_KEYWORDS = "ACCESS,ADD,ALL,ALTER,AND,ANY,ARRAYLEN,AS,ASC,AUDIT,BETWEEN,BY,CHAR,CHECK,CLUSTER,COLUMN,COMMENT,COMPRESS,CONNECT,CREATE,CURRENT,DATE,DECIMAL,DEFAULT,DELETE,DESC,DISTINCT,DROP,ELSE,EXCLUSIVE,EXISTS,FILE,FLOAT,FOR,FROM,GRANT,GROUP,HAVING,IDENTIFIED,IMMEDIATE,IN,INCREMENT,INDEX,INITIAL,INSERT,INTEGER,INTERSECT,INTO,IS,LEVEL,LIKE,LOCK,LONG,MAXEXTENTS,MINUS,MODE,MODIFY,NOAUDIT,NOCOMPRESS,NOT,NOTFOUND,NOWAIT,NULL,NUMBER,OF,OFFLINE,ON,ONLINE,OPTION,OR,ORDER,PCTFREE,PRIOR,PRIVILEGES,PUBLIC,RAW,RENAME,RESOURCE,REVOKE,ROW,ROWID,ROWLABEL,ROWNUM,ROWS,SELECT,SESSION,SET,SHARE,SIZE,SMALLINT,SQLBUF,START,SUCCESSFUL,SYNONYM,TABLE,THEN,TO,TRIGGER,UID,UNION,UNIQUE,UPDATE,USER,VALIDATE,VALUES,VARCHAR,VARCHAR2,VIEW,WHENEVER,WHERE,WITH"; + + private static String CHECK_MEMSTORE = "select 1 from %s.gv$memstore t where t.total>t.mem_limit * ?"; + private static final String CHECK_MEMSTORE_4_0 = "select 1 from %s.gv$ob_memstore t where t.MEMSTORE_USED>t.MEMSTORE_LIMIT * ?"; + + private static String CHECK_MEMSTORE_RATIO = "select min(t.total/t.mem_limit) from %s.gv$memstore t"; + private static final String CHECK_MEMSTORE_RATIO_4_0 = "select min(t.MEMSTORE_USED/t.MEMSTORE_LIMIT) from %s.gv$ob_memstore t"; + + private static Set databaseKeywords; + private static String compatibleMode = null; + private static String obVersion = null; + protected static final Logger LOG = LoggerFactory.getLogger(Task.class); + private static Set keywordsFromString2HashSet(final String keywords) { + return new HashSet(Arrays.asList(keywords.split(","))); + } + + public static String escapeDatabaseKeyword(String keyword) { + if (databaseKeywords == null) { + if (isOracleMode()) { + databaseKeywords = keywordsFromString2HashSet(ORACLE_KEYWORDS); + } else { + databaseKeywords = keywordsFromString2HashSet(MYSQL_KEYWORDS); + } + } + char escapeChar = isOracleMode() ? '"' : '`'; + if (databaseKeywords.contains(keyword.toUpperCase())) { + keyword = escapeChar + keyword + escapeChar; + } + return keyword; + } + + public static void escapeDatabaseKeyword(List keywords) { + for (int i = 0; i < keywords.size(); i++) { + keywords.set(i, escapeDatabaseKeyword(keywords.get(i))); + } + } + public static Boolean isEscapeMode(String keyword){ + if(isOracleMode()){ + return keyword.startsWith("\"") && keyword.endsWith("\""); + }else{ + return keyword.startsWith("`") && keyword.endsWith("`"); + } + } + public static boolean isMemstoreFull(Connection conn, double memstoreThreshold) { + PreparedStatement ps = null; + ResultSet rs = null; + boolean result = false; + try { + String sysDbName = "oceanbase"; + if (isOracleMode()) { + sysDbName = "sys"; + } + ps = conn.prepareStatement(String.format(getMemStoreSql(), sysDbName)); + ps.setDouble(1, memstoreThreshold); + rs = ps.executeQuery(); + // 只要有满足条件的,则表示当前租户 有个机器的memstore即将满 + result = rs.next(); + } catch (Throwable e) { + LOG.error("check memstore fail" + e.getMessage()); + result = false; + } finally { + //do not need to close the statment in ob1.0 + } + + LOG.info("isMemstoreFull=" + result); + return result; + } + + public static double queryMemUsedRatio (Connection conn) { + PreparedStatement ps = null; + ResultSet rs = null; + double result = 0; + try { + String sysDbName = "oceanbase"; + if (isOracleMode()) { + sysDbName = "sys"; + } + ps = conn.prepareStatement(String.format(getMemStoreRatioSql(), sysDbName)); + rs = ps.executeQuery(); + // 只要有满足条件的,则表示当前租户 有个机器的memstore即将满 + if (rs.next()) { + result = rs.getDouble(1); + } + } catch (Throwable e) { + LOG.warn("Check memstore fail, reason: {}. Use a random value instead.", e.getMessage()); + result = RandomUtils.nextDouble(0.3D, DEFAULT_SLOW_MEMSTORE_THRESHOLD + 0.2D); + } finally { + //do not need to close the statment in ob1.0 + } + return result; + } + + public static boolean isOracleMode(){ + return (compatibleMode.equals(Config.OB_COMPATIBLE_MODE_ORACLE)); + } + + private static String getMemStoreSql() { + if (ObVersion.valueOf(obVersion).compareTo(ObVersion.V4000) >= 0) { + return CHECK_MEMSTORE_4_0; + } else { + return CHECK_MEMSTORE; + } + } + + private static String getMemStoreRatioSql() { + if (ObVersion.valueOf(obVersion).compareTo(ObVersion.V4000) >= 0) { + return CHECK_MEMSTORE_RATIO_4_0; + } else { + return CHECK_MEMSTORE_RATIO; + } + } + + public static String getCompatibleMode() { + return compatibleMode; + } + + public static void setCompatibleMode(String mode) { + compatibleMode = mode; + } + + public static void setObVersion(String version) { + obVersion = version; + } + + private static String buildDeleteSql (String tableName, List columns) { + StringBuilder builder = new StringBuilder("DELETE FROM "); + builder.append(tableName).append(" WHERE "); + for (int i = 0; i < columns.size(); i++) { + builder.append(columns.get(i)).append(" = ?"); + if (i != columns.size() - 1) { + builder.append(" and "); + } + } + return builder.toString(); + } + + private static int[] getColumnIndex(List columnsInIndex, List allColumns) { + for (int i = 0; i < allColumns.size(); i++) { + if (!ObWriterUtils.isEscapeMode(allColumns.get(i))) { + allColumns.set(i, allColumns.get(i).toUpperCase()); + } + } + int[] colIdx = new int[columnsInIndex.size()]; + for (int i = 0; i < columnsInIndex.size(); i++) { + int index = allColumns.indexOf(columnsInIndex.get(i)); + if (index < 0) { + throw new RuntimeException( + String.format("column {} is in unique or primary key but not in the column list.", + columnsInIndex.get(i))); + } + colIdx[i] = index; + } + return colIdx; + } + + public static List> buildDeleteSql(Connection conn, String dbName, String tableName, + List columns) { + List> deleteMeta = new ArrayList(); + Map> uniqueKeys = getAllUniqueIndex(conn, dbName, tableName); + for (Map.Entry> entry : uniqueKeys.entrySet()) { + List colNames = entry.getValue(); + String deleteSql = buildDeleteSql(tableName, colNames); + int[] colIdx = getColumnIndex(colNames, columns); + LOG.info("delete sql [{}], column index: {}", deleteSql, Arrays.toString(colIdx)); + deleteMeta.add(new ImmutablePair(deleteSql, colIdx)); + } + return deleteMeta; + } + + // this function is just for oracle mode + private static Map> getAllUniqueIndex(Connection conn, String dbName, String tableName) { + Map> uniqueKeys = new HashMap(); + if (tableName.contains("\\.")) { + dbName = tableName.split("\\.")[0]; + tableName = tableName.split("\\.")[1]; + } + dbName = dbName.toUpperCase(); + String sql = String.format("select cons.CONSTRAINT_NAME AS KEY_NAME, cols.COLUMN_NAME COLUMN_NAME " + + "from all_constraints cons, all_cons_columns cols " + + "WHERE cols.table_name = '%s' AND cons.constraint_type in('P', 'U') " + + " AND cons.constraint_name = cols.constraint_name AND cons.owner = cols.owner " + + " AND cols.owner = '%s' " + + "Order by KEY_NAME, cols.POSITION", tableName, dbName); + + LOG.info("get all unique keys by sql {}", sql); + + Statement stmt = null; + ResultSet rs = null; + try { + stmt = conn.createStatement(); + rs = stmt.executeQuery(sql); + while (rs.next()) { + String keyName = rs.getString("Key_name"); + String columnName = rs.getString("Column_name"); + columnName= escapeDatabaseKeyword(columnName); + if(!ObWriterUtils.isEscapeMode(columnName)){ + columnName = columnName.toUpperCase(); + } + List s = uniqueKeys.get(keyName); + if (s == null) { + s = new ArrayList<>(); + uniqueKeys.put(keyName, s); + } + s.add(columnName); + } + } catch (Throwable e) { + LOG.error("show index from table fail :" + sql, e); + } finally { + asyncClose(rs, stmt, null); + } + //ObWriterUtils.escapeDatabaseKeywords(uniqueKeys); + return uniqueKeys; + } + + /** + * + * @param tableName + * @param columnHolders + * @param conn + * @param writeMode + * @return + */ + public static String buildWriteSql(String tableName, List columnHolders, + Connection conn, String writeMode, String obUpdateColumns) { + List valueHolders = new ArrayList(columnHolders.size()); + for (int i = 0; i < columnHolders.size(); i++) { + valueHolders.add("?"); + } + String writeDataSqlTemplate = new StringBuilder().append("INSERT INTO " + tableName + " (") + .append(StringUtils.join(columnHolders, ",")).append(") VALUES(") + .append(StringUtils.join(valueHolders, ",")).append(")").toString(); + + LOG.info("write mode: " + writeMode); + + // update mode + if (!writeMode.equals("insert")) { + if (obUpdateColumns == null) { + Set skipColumns = getSkipColumns(conn, tableName); + + StringBuilder columnList = new StringBuilder(); + for (String column : skipColumns) { + columnList.append(column).append(","); + } + LOG.info("Skip columns: " + columnList.toString()); + writeDataSqlTemplate = writeDataSqlTemplate + onDuplicateKeyUpdateString(columnHolders, skipColumns); + } else { + LOG.info("Update columns: " + obUpdateColumns); + writeDataSqlTemplate = writeDataSqlTemplate + onDuplicateKeyUpdateString(obUpdateColumns); + + } + } + + return writeDataSqlTemplate; + } + + private static Set getSkipColumns(Connection conn, String tableName) { + String sql = "show index from " + tableName; + Statement stmt = null; + ResultSet rs = null; + try { + stmt = conn.createStatement(); + rs = stmt.executeQuery(sql); + Map> uniqueKeys = new HashMap>(); + while (rs.next()) { + String nonUnique = rs.getString("Non_unique"); + if (!"0".equals(nonUnique)) { + continue; + } + String keyName = rs.getString("Key_name"); + String columnName = StringUtils.upperCase(rs.getString("Column_name")); + Set s = uniqueKeys.get(keyName); + if (s == null) { + s = new HashSet<>(); + uniqueKeys.put(keyName, s); + } + s.add(columnName); + } + // If the table has only one primary/unique key, just skip the column in the update list, + // it is safe since this primary/unique key does not change when the data in this inserting + // row conflicts with existing values. + if (uniqueKeys.size() == 1) { + return uniqueKeys.values().iterator().next(); + } else if (uniqueKeys.size() > 1) { + // If this table has more than one primary/unique keys, then just skip the common columns in + // all primary/unique keys. These columns can be found in every the primary/unique keys so they + // must be intact when there are at least one primary/unique key conflicts between the new + // data and existing data. So keeping them unchanged is safe. + // + // We can not skip all the columns in primary/unique keys because there might be some fields + // which do not conflict with existing value. If we skip them in the update list of the INSERT + // statement, these fields will not get updated, then we will have some fields with new values + // while some with old values in the same row, which breaks data consistency. + Iterator keyNameIterator = uniqueKeys.keySet().iterator(); + Set skipColumns = uniqueKeys.get(keyNameIterator.next()); + while(keyNameIterator.hasNext()) { + skipColumns.retainAll(uniqueKeys.get(keyNameIterator.next())); + } + return skipColumns; + } + } catch (Throwable e) { + LOG.error("show index from table fail :" + sql, e); + } finally { + asyncClose(rs, stmt, null); + } + return Collections.emptySet(); + } + + /* + * build ON DUPLICATE KEY UPDATE sub clause from updateColumns user specified + */ + private static String onDuplicateKeyUpdateString(String updateColumns) { + if (updateColumns == null || updateColumns.length() < 1) { + return ""; + } + StringBuilder builder = new StringBuilder(); + builder.append(" ON DUPLICATE KEY UPDATE "); + List list = new ArrayList(); + for (String column : updateColumns.split(",")) { + list.add(column + "=VALUES(" + column + ")"); + } + builder.append(StringUtils.join(list, ',')); + return builder.toString(); + } + + private static String onDuplicateKeyUpdateString(List columnHolders, Set skipColumns) { + if (columnHolders == null || columnHolders.size() < 1) { + return ""; + } + StringBuilder builder = new StringBuilder(); + builder.append(" ON DUPLICATE KEY UPDATE "); + List list = new ArrayList(); + for (String column : columnHolders) { + // skip update columns + if (skipColumns.contains(column.toUpperCase())) { + continue; + } + list.add(column + "=VALUES(" + column + ")"); + } + if (!list.isEmpty()) { + builder.append(StringUtils.join(list, ',')); + } else { + // 如果除了UK 没有别的字段,则更新第一个字段 + String column = columnHolders.get(0); + builder.append(column + "=VALUES(" + column + ")"); + } + return builder.toString(); + } + + /** + * 休眠n毫秒 + * + * @param ms + * 毫秒 + */ + public static void sleep(long ms) { + try { + Thread.sleep(ms); + } catch (InterruptedException e) { + } + } + + /** + * 致命错误 + * + * @param e + * @return + */ + + public static boolean isFatalError(SQLException e) { + String sqlState = e.getSQLState(); + if (StringUtils.startsWith(sqlState, "08")) { + return true; + } + final int errorCode = Math.abs(e.getErrorCode()); + switch (errorCode) { + // Communications Errors + case 1040: // ER_CON_COUNT_ERROR + case 1042: // ER_BAD_HOST_ERROR + case 1043: // ER_HANDSHAKE_ERROR + case 1047: // ER_UNKNOWN_COM_ERROR + case 1081: // ER_IPSOCK_ERROR + case 1129: // ER_HOST_IS_BLOCKED + case 1130: // ER_HOST_NOT_PRIVILEGED + // Authentication Errors + case 1045: // ER_ACCESS_DENIED_ERROR + // Resource errors + case 1004: // ER_CANT_CREATE_FILE + case 1005: // ER_CANT_CREATE_TABLE + case 1015: // ER_CANT_LOCK + case 1021: // ER_DISK_FULL + case 1041: // ER_OUT_OF_RESOURCES + case 1094: // Unknown thread id: %lu + // Out-of-memory errors + case 1037: // ER_OUTOFMEMORY + case 1038: // ER_OUT_OF_SORTMEMORY + return true; + } + + if (StringUtils.isNotBlank(e.getMessage())) { + final String errorText = e.getMessage().toUpperCase(); + + if (errorCode == 0 + && (errorText.indexOf("COMMUNICATIONS LINK FAILURE") > -1 + || errorText.indexOf("COULD NOT CREATE CONNECTION") > -1) + || errorText.indexOf("NO DATASOURCE") > -1 || errorText.indexOf("NO ALIVE DATASOURCE") > -1 + || errorText.indexOf("NO OPERATIONS ALLOWED AFTER CONNECTION CLOSED") > -1) { + return true; + } + } + return false; + } + + /** + * 可恢复的错误 + * + * @param e + * @return + */ + public static boolean isRecoverableError(SQLException e) { + int error = Math.abs(e.getErrorCode()); + // 明确可恢复 + if (white.contains(error)) { + return true; + } + // 明确不可恢复 + if (black.contains(error)) { + return false; + } + // 超过4000的,都是OB特有的ErrorCode + return error > 4020; + } + + private static Set white = new HashSet(); + static { + int[] errList = { 1213, 1047, 1041, 1094, 4000, 4012, 4013 }; + for (int err : errList) { + white.add(err); + } + } + // 不考虑4000以下的 + private static Set black = new HashSet(); + static { + int[] errList = { 4022, 4025, 4026, 4028, 4029, 4031, 4033, 4034, 4037, 4041, 4044 }; + for (int err : errList) { + black.add(err); + } + } + + /** + * 由于ObProxy存在bug,事务超时或事务被杀时,conn的close是没有响应的 + * + * @param rs + * @param stmt + * @param conn + */ + public static void asyncClose(final ResultSet rs, final Statement stmt, final Connection conn) { + Thread t = new Thread() { + public void run() { + DBUtil.closeDBResources(rs, stmt, conn); + } + }; + t.setDaemon(true); + t.start(); + } + + /** + * + */ + public static enum LoadMode { + + /** + * Fast insert + */ + FAST, + + /** + * Insert slowly + */ + SLOW, + + /** + * Pause to insert + */ + PAUSE + } + +} diff --git a/oceanbasev10writer/src/main/libs/oceanbase-client-1.1.10.jar b/oceanbasev10writer/src/main/libs/oceanbase-client-1.1.10.jar new file mode 100644 index 0000000000..38162912f8 Binary files /dev/null and b/oceanbasev10writer/src/main/libs/oceanbase-client-1.1.10.jar differ diff --git a/oceanbasev10writer/src/main/libs/oceanbase-connector-java-3.2.0.jar b/oceanbasev10writer/src/main/libs/oceanbase-connector-java-3.2.0.jar new file mode 100644 index 0000000000..239f3dc42e Binary files /dev/null and b/oceanbasev10writer/src/main/libs/oceanbase-connector-java-3.2.0.jar differ diff --git a/oceanbasev10writer/src/main/libs/shade-ob-partition-calculator-1.0-SNAPSHOT.jar b/oceanbasev10writer/src/main/libs/shade-ob-partition-calculator-1.0-SNAPSHOT.jar new file mode 100644 index 0000000000..34453ce6e9 Binary files /dev/null and b/oceanbasev10writer/src/main/libs/shade-ob-partition-calculator-1.0-SNAPSHOT.jar differ diff --git a/oceanbasev10writer/src/main/resources/plugin.json b/oceanbasev10writer/src/main/resources/plugin.json new file mode 100644 index 0000000000..23154c31e8 --- /dev/null +++ b/oceanbasev10writer/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "oceanbasev10writer", + "class": "com.alibaba.datax.plugin.writer.oceanbasev10writer.OceanBaseV10Writer", + "description": "write data into oceanbase with sql interface", + "developer": "oceanbase" +} \ No newline at end of file diff --git a/odpsreader/pom.xml b/odpsreader/pom.xml index 5762a57b79..3f2c4acbd4 100755 --- a/odpsreader/pom.xml +++ b/odpsreader/pom.xml @@ -36,18 +36,18 @@ guava 16.0.1 - - org.bouncycastle - bcprov-jdk15on - 1.52 - system - ${basedir}/src/main/libs/bcprov-jdk15on-1.52.jar - - - com.aliyun.odps - odps-sdk-core - 0.20.7-public - + + org.xerial + sqlite-jdbc + 3.34.0 + + + + + com.aliyun.odps + odps-sdk-core + 0.38.4-public + org.mockito @@ -87,29 +87,22 @@ 1.4.10 test - - org.mockito - mockito-core - 1.8.5 - test - - - org.powermock - powermock-api-mockito - 1.4.10 - test - - - - org.powermock - powermock-module-junit4 - 1.4.10 - test + commons-codec + commons-codec + 1.8 + + + src/main/java + + **/*.properties + + + diff --git a/odpsreader/src/main/assembly/package.xml b/odpsreader/src/main/assembly/package.xml index 9ec3309e6e..db659a1796 100755 --- a/odpsreader/src/main/assembly/package.xml +++ b/odpsreader/src/main/assembly/package.xml @@ -23,13 +23,6 @@ plugin/reader/odpsreader - - src/main/libs - - *.* - - plugin/reader/odpsreader/libs - diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/ColumnType.java b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/ColumnType.java index eb674a7f67..1c771d3e69 100644 --- a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/ColumnType.java +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/ColumnType.java @@ -3,20 +3,6 @@ public enum ColumnType { PARTITION, NORMAL, CONSTANT, UNKNOWN, ; - @Override - public String toString() { - switch (this) { - case PARTITION: - return "partition"; - case NORMAL: - return "normal"; - case CONSTANT: - return "constant"; - default: - return "unknown"; - } - } - public static ColumnType asColumnType(String columnTypeString) { if ("partition".equals(columnTypeString)) { return PARTITION; diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/Constant.java b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/Constant.java index c3c674ddd1..cf34762ddf 100755 --- a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/Constant.java +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/Constant.java @@ -14,22 +14,13 @@ public class Constant { public static final String PARTITION_SPLIT_MODE = "partition"; - public static final String DEFAULT_ACCOUNT_TYPE = "aliyun"; - - public static final String TAOBAO_ACCOUNT_TYPE = "taobao"; - // 常量字段用COLUMN_CONSTANT_FLAG 首尾包住即可 public final static String COLUMN_CONSTANT_FLAG = "'"; - /** - * 以下是获取accesskey id 需要用到的常量值 - */ - public static final String SKYNET_ACCESSID = "SKYNET_ACCESSID"; - - public static final String SKYNET_ACCESSKEY = "SKYNET_ACCESSKEY"; - public static final String PARTITION_COLUMNS = "partitionColumns"; public static final String PARSED_COLUMNS = "parsedColumns"; - + + public static final String PARTITION_FILTER_HINT = "/*query*/"; + } diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/InternalColumnInfo.java b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/InternalColumnInfo.java new file mode 100644 index 0000000000..b5a15f1d76 --- /dev/null +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/InternalColumnInfo.java @@ -0,0 +1,24 @@ +package com.alibaba.datax.plugin.reader.odpsreader; + +public class InternalColumnInfo { + + private String columnName; + + private ColumnType columnType; + + public String getColumnName() { + return columnName; + } + + public void setColumnName(String columnName) { + this.columnName = columnName; + } + + public ColumnType getColumnType() { + return columnType; + } + + public void setColumnType(ColumnType columnType) { + this.columnType = columnType; + } +} diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/Key.java b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/Key.java index 9537cb9397..6f8c7d92df 100755 --- a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/Key.java +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/Key.java @@ -5,6 +5,8 @@ public class Key { public final static String ACCESS_ID = "accessId"; public final static String ACCESS_KEY = "accessKey"; + + public final static String SECURITY_TOKEN = "securityToken"; public static final String PROJECT = "project"; @@ -22,13 +24,19 @@ public class Key { // 当值为:partition 则只切分到分区;当值为:record,则当按照分区切分后达不到adviceNum时,继续按照record切分 public final static String SPLIT_MODE = "splitMode"; - // 账号类型,默认为aliyun,也可能为taobao等其他类型 - public final static String ACCOUNT_TYPE = "accountType"; - public final static String PACKAGE_AUTHORIZED_PROJECT = "packageAuthorizedProject"; public final static String IS_COMPRESS = "isCompress"; public final static String MAX_RETRY_TIME = "maxRetryTime"; + + // 分区不存在时 + public final static String SUCCESS_ON_NO_PATITION="successOnNoPartition"; + + // preSql + public final static String PRE_SQL="preSql"; + // postSql + public final static String POST_SQL="postSql"; + } diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/LocalStrings.properties b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/LocalStrings.properties new file mode 100644 index 0000000000..ef1830022d --- /dev/null +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/LocalStrings.properties @@ -0,0 +1,64 @@ +description.DATAX_R_ODPS_001=\u7F3A\u5C11\u5FC5\u586B\u53C2\u6570 +description.DATAX_R_ODPS_002=\u914D\u7F6E\u503C\u4E0D\u5408\u6CD5 +description.DATAX_R_ODPS_003=\u521B\u5EFAODPS Session\u5931\u8D25 +description.DATAX_R_ODPS_004=\u83B7\u53D6ODPS Session\u5931\u8D25 +description.DATAX_R_ODPS_005=\u8BFB\u53D6ODPS\u6570\u636E\u5931\u8D25 +description.DATAX_R_ODPS_006=\u83B7\u53D6AK\u5931\u8D25 +description.DATAX_R_ODPS_007=\u8BFB\u53D6\u6570\u636E\u53D1\u751F\u5F02\u5E38 +description.DATAX_R_ODPS_008=\u6253\u5F00RecordReader\u5931\u8D25 +description.DATAX_R_ODPS_009=ODPS\u9879\u76EE\u4E0D\u5B58\u5728 +description.DATAX_R_ODPS_010=\u8868\u4E0D\u5B58\u5728 +description.DATAX_R_ODPS_011=AK\u4E0D\u5B58\u5728 +description.DATAX_R_ODPS_012=AK\u975E\u6CD5 +description.DATAX_R_ODPS_013=AK\u62D2\u7EDD\u8BBF\u95EE +description.DATAX_R_ODPS_014=splitMode\u914D\u7F6E\u9519\u8BEF +description.DATAX_R_ODPS_015=ODPS\u8D26\u53F7\u7C7B\u578B\u9519\u8BEF +description.DATAX_R_ODPS_016=\u4E0D\u652F\u6301\u89C6\u56FE +description.DATAX_R_ODPS_017=\u5206\u533A\u914D\u7F6E\u9519\u8BEF +description.DATAX_R_ODPS_018=\u5206\u533A\u4E0D\u5B58\u5728 +description.DATAX_R_ODPS_019=\u6267\u884CODPS SQL\u5931\u8D25 +description.DATAX_R_ODPS_020=\u6267\u884CODPS SQL\u53D1\u751F\u5F02\u5E38 + + +solution.DATAX_R_ODPS_001=\u8BF7\u4FEE\u6539\u914D\u7F6E\u6587\u4EF6 +solution.DATAX_R_ODPS_002=\u8BF7\u4FEE\u6539\u914D\u7F6E\u503C +solution.DATAX_R_ODPS_003=\u8BF7\u786E\u5B9A\u914D\u7F6E\u7684AK\u6216\u8054\u7CFBODPS\u7BA1\u7406\u5458 +solution.DATAX_R_ODPS_004=\u8BF7\u8054\u7CFBODPS\u7BA1\u7406\u5458 +solution.DATAX_R_ODPS_005=\u8BF7\u8054\u7CFBODPS\u7BA1\u7406\u5458 +solution.DATAX_R_ODPS_006=\u8BF7\u786E\u5B9A\u914D\u7F6E\u7684AK +solution.DATAX_R_ODPS_007=\u8BF7\u8054\u7CFBODPS\u7BA1\u7406\u5458 +solution.DATAX_R_ODPS_008=\u8BF7\u8054\u7CFBODPS\u7BA1\u7406\u5458 +solution.DATAX_R_ODPS_009=\u8BF7\u786E\u5B9A\u914D\u7F6E\u7684\u9879\u76EE\u540D +solution.DATAX_R_ODPS_010=\u8BF7\u786E\u5B9A\u914D\u7F6E\u7684\u8868\u540D +solution.DATAX_R_ODPS_011=\u8BF7\u786E\u5B9A\u914D\u7F6E\u7684AK +solution.DATAX_R_ODPS_012=\u8BF7\u4FEE\u6539AK +solution.DATAX_R_ODPS_013=\u8BF7\u786E\u5B9AAK\u5728\u9879\u76EE\u4E2D\u7684\u6743\u9650 +solution.DATAX_R_ODPS_014=\u8BF7\u4FEE\u6539splitMode\u503C +solution.DATAX_R_ODPS_015=\u8BF7\u4FEE\u6539\u8D26\u53F7\u7C7B\u578B +solution.DATAX_R_ODPS_016=\u8BF7\u4FEE\u6539\u914D\u7F6E\u6587\u4EF6 +solution.DATAX_R_ODPS_017=\u8BF7\u4FEE\u6539\u5206\u533A\u503C +solution.DATAX_R_ODPS_018=\u8BF7\u4FEE\u6539\u914D\u7F6E\u7684\u5206\u533A\u503C +solution.DATAX_R_ODPS_019=\u8BF7\u8054\u7CFBODPS\u7BA1\u7406\u5458 +solution.DATAX_R_ODPS_020=\u8BF7\u8054\u7CFBODPS\u7BA1\u7406\u5458 + +odpsreader.1=\u6E90\u5934\u8868:{0} \u662F\u865A\u62DF\u89C6\u56FE\uFF0CDataX \u4E0D\u652F\u6301\u8BFB\u53D6\u865A\u62DF\u89C6\u56FE. +odpsreader.2=\u60A8\u6240\u914D\u7F6E\u7684 splitMode:{0} \u4E0D\u6B63\u786E. splitMode \u4EC5\u5141\u8BB8\u914D\u7F6E\u4E3A record \u6216\u8005 partition. +odpsreader.3=\u5206\u533A\u4FE1\u606F\u6CA1\u6709\u914D\u7F6E.\u7531\u4E8E\u6E90\u5934\u8868:{0} \u4E3A\u5206\u533A\u8868, \u6240\u4EE5\u60A8\u9700\u8981\u914D\u7F6E\u5176\u62BD\u53D6\u7684\u8868\u7684\u5206\u533A\u4FE1\u606F. \u683C\u5F0F\u5F62\u5982:pt=hello,ds=hangzhou\uFF0C\u8BF7\u60A8\u53C2\u8003\u6B64\u683C\u5F0F\u4FEE\u6539\u8BE5\u914D\u7F6E\u9879. +odpsreader.4=\u5206\u533A\u4FE1\u606F\u914D\u7F6E\u9519\u8BEF.\u6E90\u5934\u8868:{0} \u867D\u7136\u4E3A\u5206\u533A\u8868, \u4F46\u5176\u5B9E\u9645\u5206\u533A\u503C\u5E76\u4E0D\u5B58\u5728. \u8BF7\u786E\u8BA4\u6E90\u5934\u8868\u5DF2\u7ECF\u751F\u6210\u8BE5\u5206\u533A\uFF0C\u518D\u8FDB\u884C\u6570\u636E\u62BD\u53D6. +odpsreader.5=\u5206\u533A\u914D\u7F6E\u9519\u8BEF\uFF0C\u6839\u636E\u60A8\u6240\u914D\u7F6E\u7684\u5206\u533A\u6CA1\u6709\u5339\u914D\u5230\u6E90\u5934\u8868\u4E2D\u7684\u5206\u533A. \u6E90\u5934\u8868\u6240\u6709\u5206\u533A\u662F:[\n{0}\n], \u60A8\u914D\u7F6E\u7684\u5206\u533A\u662F:[\n{1}\n]. \u8BF7\u60A8\u6839\u636E\u5B9E\u9645\u60C5\u51B5\u518D\u4F5C\u51FA\u4FEE\u6539. +odpsreader.6=\u5206\u533A\u914D\u7F6E\u9519\u8BEF\uFF0C\u6E90\u5934\u8868:{0} \u4E3A\u975E\u5206\u533A\u8868, \u60A8\u4E0D\u80FD\u914D\u7F6E\u5206\u533A. \u8BF7\u60A8\u5220\u9664\u8BE5\u914D\u7F6E\u9879. +odpsreader.7=\u6E90\u5934\u8868:{0} \u7684\u6240\u6709\u5206\u533A\u5217\u662F:[{1}] +odpsreader.8=\u5206\u533A\u914D\u7F6E\u9519\u8BEF, \u60A8\u6240\u914D\u7F6E\u7684\u5206\u533A\u7EA7\u6570\u548C\u8BE5\u8868\u7684\u5B9E\u9645\u60C5\u51B5\u4E0D\u4E00\u81F4, \u6BD4\u5982\u5206\u533A:[{0}] \u662F {1} \u7EA7\u5206\u533A, \u800C\u5206\u533A:[{2}] \u662F {3} \u7EA7\u5206\u533A. DataX \u662F\u901A\u8FC7\u82F1\u6587\u9017\u53F7\u5224\u65AD\u60A8\u6240\u914D\u7F6E\u7684\u5206\u533A\u7EA7\u6570\u7684. \u6B63\u786E\u7684\u683C\u5F0F\u5F62\u5982\"pt=$'{bizdate'}, type=0\" \uFF0C\u8BF7\u60A8\u53C2\u8003\u793A\u4F8B\u4FEE\u6539\u8BE5\u914D\u7F6E\u9879. +odpsreader.9=\u5206\u533A\u914D\u7F6E\u9519\u8BEF, \u60A8\u6240\u914D\u7F6E\u7684\u5206\u533A:{0} \u7684\u7EA7\u6570:{1} \u4E0E\u60A8\u8981\u8BFB\u53D6\u7684 ODPS \u6E90\u5934\u8868\u7684\u5206\u533A\u7EA7\u6570:{2} \u4E0D\u76F8\u7B49. DataX \u662F\u901A\u8FC7\u82F1\u6587\u9017\u53F7\u5224\u65AD\u60A8\u6240\u914D\u7F6E\u7684\u5206\u533A\u7EA7\u6570\u7684.\u6B63\u786E\u7684\u683C\u5F0F\u5F62\u5982\"pt=$'{bizdate'}, type=0\" \uFF0C\u8BF7\u60A8\u53C2\u8003\u793A\u4F8B\u4FEE\u6539\u8BE5\u914D\u7F6E\u9879. +odpsreader.10=\u6E90\u5934\u8868:{0} \u7684\u6240\u6709\u5B57\u6BB5\u662F:[{1}] +odpsreader.11=\u8FD9\u662F\u4E00\u6761\u8B66\u544A\u4FE1\u606F\uFF0C\u60A8\u914D\u7F6E\u7684 ODPS \u8BFB\u53D6\u7684\u5217\u4E3A*\uFF0C\u8FD9\u662F\u4E0D\u63A8\u8350\u7684\u884C\u4E3A\uFF0C\u56E0\u4E3A\u5F53\u60A8\u7684\u8868\u5B57\u6BB5\u4E2A\u6570\u3001\u7C7B\u578B\u6709\u53D8\u52A8\u65F6\uFF0C\u53EF\u80FD\u5F71\u54CD\u4EFB\u52A1\u6B63\u786E\u6027\u751A\u81F3\u4F1A\u8FD0\u884C\u51FA\u9519. \u5EFA\u8BAE\u60A8\u628A\u6240\u6709\u9700\u8981\u62BD\u53D6\u7684\u5217\u90FD\u914D\u7F6E\u4E0A. +odpsreader.12=\u6E90\u5934\u8868:{0} \u7684\u5206\u533A:{1} \u6CA1\u6709\u5185\u5BB9\u53EF\u62BD\u53D6, \u8BF7\u60A8\u77E5\u6653. +odpsreader.13=\u6E90\u5934\u8868:{0} \u7684\u5206\u533A:{1} \u8BFB\u53D6\u884C\u6570\u4E3A\u8D1F\u6570, \u8BF7\u8054\u7CFB ODPS \u7BA1\u7406\u5458\u67E5\u770B\u8868\u72B6\u6001! +odpsreader.14=\u6E90\u5934\u8868:{0} \u7684\u5206\u533A:{1} \u8BFB\u53D6\u5931\u8D25, \u8BF7\u8054\u7CFB ODPS \u7BA1\u7406\u5458\u67E5\u770B\u9519\u8BEF\u8BE6\u60C5. + + +readerproxy.1=odps-read-exception, \u91CD\u8BD5\u7B2C{0}\u6B21 +readerproxy.2=\u60A8\u7684\u5206\u533A [{0}] \u89E3\u6790\u51FA\u73B0\u9519\u8BEF,\u89E3\u6790\u540E\u6B63\u786E\u7684\u914D\u7F6E\u65B9\u5F0F\u7C7B\u4F3C\u4E3A [ pt=1,dt=1 ]. +readerproxy.3=\u8868\u6240\u6709\u5206\u533A\u4FE1\u606F\u4E3A: {0} \u5176\u4E2D\u627E\u4E0D\u5230 [{1}] \u5BF9\u5E94\u7684\u5206\u533A\u503C. +readerproxy.4=\u60A8\u8BFB\u53D6\u5206\u533A [{0}] \u51FA\u73B0\u65E5\u671F\u8F6C\u6362\u5F02\u5E38, \u65E5\u671F\u7684\u5B57\u7B26\u4E32\u8868\u793A\u4E3A [{1}]. +readerproxy.5=DataX \u62BD\u53D6 ODPS \u6570\u636E\u4E0D\u652F\u6301\u5B57\u6BB5\u7C7B\u578B\u4E3A:[{0}]. \u76EE\u524D\u652F\u6301\u62BD\u53D6\u7684\u5B57\u6BB5\u7C7B\u578B\u6709\uFF1Abigint, boolean, datetime, double, decimal, string. \u60A8\u53EF\u4EE5\u9009\u62E9\u4E0D\u62BD\u53D6 DataX \u4E0D\u652F\u6301\u7684\u5B57\u6BB5\u6216\u8005\u8054\u7CFB ODPS \u7BA1\u7406\u5458\u5BFB\u6C42\u5E2E\u52A9. diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/OdpsReader.java b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/OdpsReader.java index f5cf10ca28..615cee502a 100755 --- a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/OdpsReader.java +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/OdpsReader.java @@ -5,50 +5,42 @@ import com.alibaba.datax.common.spi.Reader; import com.alibaba.datax.common.util.Configuration; import com.alibaba.datax.common.util.FilterUtil; -import com.alibaba.datax.plugin.reader.odpsreader.util.IdAndKeyUtil; -import com.alibaba.datax.plugin.reader.odpsreader.util.OdpsSplitUtil; -import com.alibaba.datax.plugin.reader.odpsreader.util.OdpsUtil; -import com.aliyun.odps.*; +import com.alibaba.datax.common.util.MessageSource; +import com.alibaba.datax.plugin.reader.odpsreader.util.*; +import com.alibaba.fastjson2.JSON; +import com.aliyun.odps.Column; +import com.aliyun.odps.Odps; +import com.aliyun.odps.Table; +import com.aliyun.odps.TableSchema; import com.aliyun.odps.tunnel.TableTunnel.DownloadSession; - +import com.aliyun.odps.type.TypeInfo; import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.tuple.MutablePair; -import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; public class OdpsReader extends Reader { public static class Job extends Reader.Job { private static final Logger LOG = LoggerFactory - .getLogger(Job.class); - - private static boolean IS_DEBUG = LOG.isDebugEnabled(); + .getLogger(Job.class); + private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(OdpsReaderErrorCode.class, Locale.ENGLISH, MessageSource.timeZone); private Configuration originalConfig; + private boolean successOnNoPartition; private Odps odps; private Table table; + @Override public void preCheck() { this.init(); + this.prepare(); } - @Override public void init() { this.originalConfig = super.getPluginJobConf(); - - //如果用户没有配置accessId/accessKey,尝试从环境变量获取 - String accountType = originalConfig.getString(Key.ACCOUNT_TYPE, Constant.DEFAULT_ACCOUNT_TYPE); - if (Constant.DEFAULT_ACCOUNT_TYPE.equalsIgnoreCase(accountType)) { - this.originalConfig = IdAndKeyUtil.parseAccessIdAndKey(this.originalConfig); - } + this.successOnNoPartition = this.originalConfig.getBool(Key.SUCCESS_ON_NO_PATITION, false); //检查必要的参数配置 OdpsUtil.checkNecessaryConfig(this.originalConfig); @@ -59,17 +51,21 @@ public void init() { dealSplitMode(this.originalConfig); this.odps = OdpsUtil.initOdps(this.originalConfig); + + } + + private void initOdpsTableInfo() { String tableName = this.originalConfig.getString(Key.TABLE); String projectName = this.originalConfig.getString(Key.PROJECT); this.table = OdpsUtil.getTable(this.odps, projectName, tableName); this.originalConfig.set(Constant.IS_PARTITIONED_TABLE, - OdpsUtil.isPartitionedTable(table)); + OdpsUtil.isPartitionedTable(table)); boolean isVirtualView = this.table.isVirtualView(); if (isVirtualView) { throw DataXException.asDataXException(OdpsReaderErrorCode.VIRTUAL_VIEW_NOT_SUPPORT, - String.format("源头表:%s 是虚拟视图,DataX 不支持读取虚拟视图.", tableName)); + MESSAGE_SOURCE.message("odpsreader.1", tableName)); } this.dealPartition(this.table); @@ -79,11 +75,11 @@ public void init() { private void dealSplitMode(Configuration originalConfig) { String splitMode = originalConfig.getString(Key.SPLIT_MODE, Constant.DEFAULT_SPLIT_MODE).trim(); if (splitMode.equalsIgnoreCase(Constant.DEFAULT_SPLIT_MODE) || - splitMode.equalsIgnoreCase(Constant.PARTITION_SPLIT_MODE)) { + splitMode.equalsIgnoreCase(Constant.PARTITION_SPLIT_MODE)) { originalConfig.set(Key.SPLIT_MODE, splitMode); } else { throw DataXException.asDataXException(OdpsReaderErrorCode.SPLIT_MODE_ERROR, - String.format("您所配置的 splitMode:%s 不正确. splitMode 仅允许配置为 record 或者 partition.", splitMode)); + MESSAGE_SOURCE.message("odpsreader.2", splitMode)); } } @@ -98,7 +94,7 @@ private void dealSplitMode(Configuration originalConfig) { */ private void dealPartition(Table table) { List userConfiguredPartitions = this.originalConfig.getList( - Key.PARTITION, String.class); + Key.PARTITION, String.class); boolean isPartitionedTable = this.originalConfig.getBool(Constant.IS_PARTITIONED_TABLE); List partitionColumns = new ArrayList(); @@ -107,60 +103,140 @@ private void dealPartition(Table table) { // 分区表,需要配置分区 if (null == userConfiguredPartitions || userConfiguredPartitions.isEmpty()) { throw DataXException.asDataXException(OdpsReaderErrorCode.PARTITION_ERROR, - String.format("分区信息没有配置.由于源头表:%s 为分区表, 所以您需要配置其抽取的表的分区信息. 格式形如:pt=hello,ds=hangzhou,请您参考此格式修改该配置项.", - table.getName())); + MESSAGE_SOURCE.message("odpsreader.3", table.getName())); } else { - List allPartitions = OdpsUtil.getTableAllPartitions(table); - - if (null == allPartitions || allPartitions.isEmpty()) { - throw DataXException.asDataXException(OdpsReaderErrorCode.PARTITION_ERROR, - String.format("分区信息配置错误.源头表:%s 虽然为分区表, 但其实际分区值并不存在. 请确认源头表已经生成该分区,再进行数据抽取.", - table.getName())); + // 获取分区列名, 支持用户配置分区列同步 + for (Column column : table.getSchema().getPartitionColumns()) { + partitionColumns.add(column.getName()); } - List parsedPartitions = expandUserConfiguredPartition( - allPartitions, userConfiguredPartitions); + List allPartitions = OdpsUtil.getTableAllPartitions(table); + List parsedPartitions = expandUserConfiguredPartition( + table, allPartitions, userConfiguredPartitions, partitionColumns.size()); if (null == parsedPartitions || parsedPartitions.isEmpty()) { - throw DataXException.asDataXException( - OdpsReaderErrorCode.PARTITION_ERROR, + if (!this.successOnNoPartition) { + // PARTITION_NOT_EXISTS_ERROR 这个异常ErrorCode在AdsWriter有使用,用户判断空分区Load Data任务不报错 + // 其他类型的异常不要使用这个错误码 + throw DataXException.asDataXException( + OdpsReaderErrorCode.PARTITION_NOT_EXISTS_ERROR, + MESSAGE_SOURCE.message("odpsreader.5", + StringUtils.join(allPartitions, "\n"), + StringUtils.join(userConfiguredPartitions, "\n"))); + } else { + LOG.warn( String.format( - "分区配置错误,根据您所配置的分区没有匹配到源头表中的分区. 源头表所有分区是:[\n%s\n], 您配置的分区是:[\n%s\n]. 请您根据实际情况在作出修改. ", - StringUtils.join(allPartitions, "\n"), - StringUtils.join(userConfiguredPartitions, "\n"))); + "The partition configuration is wrong, " + + "but you have configured the successOnNoPartition to be true to ignore the error. " + + "According to the partition you have configured, it does not match the partition in the source table. " + + "All the partitions in the source table are:[\n%s\n], the partition you configured is:[\n%s\n]. " + + "please revise it according to the actual situation.", + StringUtils.join(allPartitions, "\n"), + StringUtils.join(userConfiguredPartitions, "\n"))); + } } + LOG.info(String + .format("expand user configured partitions are : %s", JSON.toJSONString(parsedPartitions))); this.originalConfig.set(Key.PARTITION, parsedPartitions); - - for (Column column : table.getSchema() - .getPartitionColumns()) { - partitionColumns.add(column.getName()); - } } } else { // 非分区表,则不能配置分区 if (null != userConfiguredPartitions - && !userConfiguredPartitions.isEmpty()) { + && !userConfiguredPartitions.isEmpty()) { throw DataXException.asDataXException(OdpsReaderErrorCode.PARTITION_ERROR, - String.format("分区配置错误,源头表:%s 为非分区表, 您不能配置分区. 请您删除该配置项. ", table.getName())); + MESSAGE_SOURCE.message("odpsreader.6", table.getName())); } } - + this.originalConfig.set(Constant.PARTITION_COLUMNS, partitionColumns); if (isPartitionedTable) { - LOG.info("{源头表:{} 的所有分区列是:[{}]}", table.getName(), - StringUtils.join(partitionColumns, ",")); + LOG.info(MESSAGE_SOURCE.message("odpsreader.7", table.getName(), + StringUtils.join(partitionColumns, ","))); + } + } + + /** + * 将用户配置的分区(可能是直接的分区配置 dt=20170101, 可能是简单正则dt=201701*, 也可能是区间过滤条件 dt>=20170101 and dt<20170130) 和ODPS + * table所有的分区进行匹配,过滤出用户希望同步的分区集合 + * + * @param table odps table + * @param allPartitions odps table所有的分区 + * @param userConfiguredPartitions 用户配置的分区 + * @param tableOriginalPartitionDepth odps table分区级数(一级分区,二级分区,三级分区等) + * @return 返回过滤出的分区 + */ + private List expandUserConfiguredPartition(Table table, + List allPartitions, + List userConfiguredPartitions, + int tableOriginalPartitionDepth) { + + UserConfiguredPartitionClassification userConfiguredPartitionClassification = OdpsUtil + .classifyUserConfiguredPartitions(userConfiguredPartitions); + + if (userConfiguredPartitionClassification.isIncludeHintPartition()) { + List expandUserConfiguredPartitionResult = new ArrayList(); + + // 处理不包含/*query*/的分区过滤 + if (!userConfiguredPartitionClassification.getUserConfiguredNormalPartition().isEmpty()) { + expandUserConfiguredPartitionResult.addAll(expandNoHintUserConfiguredPartition(allPartitions, + userConfiguredPartitionClassification.getUserConfiguredNormalPartition(), + tableOriginalPartitionDepth)); + } + if (!allPartitions.isEmpty()) { + expandUserConfiguredPartitionResult.addAll(expandHintUserConfiguredPartition(table, + allPartitions, userConfiguredPartitionClassification.getUserConfiguredHintPartition())); + } + return expandUserConfiguredPartitionResult; + } else { + return expandNoHintUserConfiguredPartition(allPartitions, userConfiguredPartitions, + tableOriginalPartitionDepth); + } + } + + /** + * 匹配包含 HINT 条件的过滤 + * + * @param table odps table + * @param allPartitions odps table所有的分区 + * @param userHintConfiguredPartitions 用户配置的分区 + * @return 返回过滤出的分区 + */ + private List expandHintUserConfiguredPartition(Table table, + List allPartitions, + List userHintConfiguredPartitions) { + try { + // load odps table all partitions into sqlite memory database + SqliteUtil sqliteUtil = new SqliteUtil(); + sqliteUtil.loadAllPartitionsIntoSqlite(table, allPartitions); + return sqliteUtil.selectUserConfiguredPartition(userHintConfiguredPartitions); + } catch (Exception ex) { + throw DataXException.asDataXException(OdpsReaderErrorCode.PARTITION_ERROR, + String.format("Expand user configured partition has exception: %s", ex.getMessage()), ex); } } - private List expandUserConfiguredPartition( - List allPartitions, List userConfiguredPartitions) { + /** + * 匹配没有 HINT 条件的过滤,包括 简单正则匹配(dt=201701*) 和 直接匹配(dt=20170101) + * + * @param allPartitions odps table所有的分区 + * @param userNormalConfiguredPartitions 用户配置的分区 + * @param tableOriginalPartitionDepth odps table分区级数(一级分区,二级分区,三级分区等) + * @return 返回过滤出的分区 + */ + private List expandNoHintUserConfiguredPartition(List allPartitions, + List userNormalConfiguredPartitions, + int tableOriginalPartitionDepth) { // 对odps 本身的所有分区进行特殊字符的处理 + LOG.info("format partition with rules: remove all space; remove all '; replace / to ,"); + // 表里面已有分区量比较大,有些任务无关,没有打印 List allStandardPartitions = OdpsUtil - .formatPartitions(allPartitions); + .formatPartitions(allPartitions); // 对用户自身配置的所有分区进行特殊字符的处理 List allStandardUserConfiguredPartitions = OdpsUtil - .formatPartitions(userConfiguredPartitions); + .formatPartitions(userNormalConfiguredPartitions); + LOG.info("user configured partition: {}", JSON.toJSONString(userNormalConfiguredPartitions)); + LOG.info("formated partition: {}", JSON.toJSONString(allStandardUserConfiguredPartitions)); /** * 对配置的分区级数(深度)进行检查 @@ -177,20 +253,20 @@ private List expandUserConfiguredPartition( comparedPartitionDepth = comparedPartition.split(",").length; if (comparedPartitionDepth != firstPartitionDepth) { throw DataXException.asDataXException(OdpsReaderErrorCode.PARTITION_ERROR, - String.format("分区配置错误, 您所配置的分区级数和该表的实际情况不一致, 比如分区:[%s] 是 %s 级分区, 而分区:[%s] 是 %s 级分区. DataX 是通过英文逗号判断您所配置的分区级数的. 正确的格式形如\"pt=${bizdate}, type=0\" ,请您参考示例修改该配置项. ", - firstPartition, firstPartitionDepth, comparedPartition, comparedPartitionDepth)); + MESSAGE_SOURCE + .message("odpsreader.8", firstPartition, firstPartitionDepth, comparedPartition, + comparedPartitionDepth)); } } - int tableOriginalPartitionDepth = allStandardPartitions.get(0).split(",").length; if (firstPartitionDepth != tableOriginalPartitionDepth) { throw DataXException.asDataXException(OdpsReaderErrorCode.PARTITION_ERROR, - String.format("分区配置错误, 您所配置的分区:%s 的级数:%s 与您要读取的 ODPS 源头表的分区级数:%s 不相等. DataX 是通过英文逗号判断您所配置的分区级数的.正确的格式形如\"pt=${bizdate}, type=0\" ,请您参考示例修改该配置项.", - firstPartition, firstPartitionDepth, tableOriginalPartitionDepth)); + MESSAGE_SOURCE + .message("odpsreader.9", firstPartition, firstPartitionDepth, tableOriginalPartitionDepth)); } List retPartitions = FilterUtil.filterByRegulars(allStandardPartitions, - allStandardUserConfiguredPartitions); + allStandardUserConfiguredPartitions); return retPartitions; } @@ -198,11 +274,11 @@ private List expandUserConfiguredPartition( private void dealColumn(Table table) { // 用户配置的 column 之前已经确保其不为空 List userConfiguredColumns = this.originalConfig.getList( - Key.COLUMN, String.class); + Key.COLUMN, String.class); List allColumns = OdpsUtil.getTableAllColumns(table); List allNormalColumns = OdpsUtil - .getTableOriginalColumnNameList(allColumns); + .getTableOriginalColumnNameList(allColumns); StringBuilder columnMeta = new StringBuilder(); for (Column column : allColumns) { @@ -210,46 +286,75 @@ private void dealColumn(Table table) { } columnMeta.setLength(columnMeta.length() - 1); - LOG.info("源头表:{} 的所有字段是:[{}]", table.getName(), columnMeta.toString()); + LOG.info(MESSAGE_SOURCE.message("odpsreader.10", table.getName(), columnMeta.toString())); if (1 == userConfiguredColumns.size() - && "*".equals(userConfiguredColumns.get(0))) { - LOG.warn("这是一条警告信息,您配置的 ODPS 读取的列为*,这是不推荐的行为,因为当您的表字段个数、类型有变动时,可能影响任务正确性甚至会运行出错. 建议您把所有需要抽取的列都配置上. "); + && "*".equals(userConfiguredColumns.get(0))) { + LOG.warn(MESSAGE_SOURCE.message("odpsreader.11")); this.originalConfig.set(Key.COLUMN, allNormalColumns); } userConfiguredColumns = this.originalConfig.getList( - Key.COLUMN, String.class); + Key.COLUMN, String.class); /** * warn: 字符串常量需要与表原生字段tableOriginalColumnNameList 分开存放 demo: * ["id","'id'","name"] */ List allPartitionColumns = this.originalConfig.getList( - Constant.PARTITION_COLUMNS, String.class); - List> parsedColumns = OdpsUtil - .parseColumns(allNormalColumns, allPartitionColumns, - userConfiguredColumns); + Constant.PARTITION_COLUMNS, String.class); + List parsedColumns = OdpsUtil + .parseColumns(allNormalColumns, allPartitionColumns, + userConfiguredColumns); this.originalConfig.set(Constant.PARSED_COLUMNS, parsedColumns); StringBuilder sb = new StringBuilder(); sb.append("[ "); for (int i = 0, len = parsedColumns.size(); i < len; i++) { - Pair pair = parsedColumns.get(i); - sb.append(String.format(" %s : %s", pair.getLeft(), - pair.getRight())); + InternalColumnInfo pair = parsedColumns.get(i); + sb.append(String.format(" %s : %s", pair.getColumnName(), + pair.getColumnType())); if (i != len - 1) { sb.append(","); } } + + sb.append(" ]"); LOG.info("parsed column details: {} .", sb.toString()); } - @Override public void prepare() { + List preSqls = this.originalConfig.getList(Key.PRE_SQL, String.class); + if (preSqls != null && !preSqls.isEmpty()) { + LOG.info( + String.format("Beigin to exectue preSql : %s. \n Attention: these preSqls must be idempotent!!!", + JSON.toJSONString(preSqls))); + long beginTime = System.currentTimeMillis(); + + StringBuffer preSqlBuffer = new StringBuffer(); + for (String preSql : preSqls) { + preSql = preSql.trim(); + if (StringUtils.isNotBlank(preSql) && !preSql.endsWith(";")) { + preSql = String.format("%s;", preSql); + } + if (StringUtils.isNotBlank(preSql)) { + preSqlBuffer.append(preSql); + } + } + if (StringUtils.isNotBlank(preSqlBuffer.toString())) { + OdpsUtil.runSqlTaskWithRetry(this.odps, preSqlBuffer.toString(), "preSql"); + } else { + LOG.info("skip to execute the preSql: {}", JSON.toJSONString(preSqls)); + } + long endTime = System.currentTimeMillis(); + + LOG.info( + String.format("Exectue odpsreader preSql successfully! cost time: %s ms.", (endTime - beginTime))); + } + this.initOdpsTableInfo(); } @Override @@ -259,6 +364,33 @@ public List split(int adviceNumber) { @Override public void post() { + List postSqls = this.originalConfig.getList(Key.POST_SQL, String.class); + + if (postSqls != null && !postSqls.isEmpty()) { + LOG.info( + String.format("Beigin to exectue postSql : %s. \n Attention: these postSqls must be idempotent!!!", + JSON.toJSONString(postSqls))); + long beginTime = System.currentTimeMillis(); + StringBuffer postSqlBuffer = new StringBuffer(); + for (String postSql : postSqls) { + postSql = postSql.trim(); + if (StringUtils.isNotBlank(postSql) && !postSql.endsWith(";")) { + postSql = String.format("%s;", postSql); + } + if (StringUtils.isNotBlank(postSql)) { + postSqlBuffer.append(postSql); + } + } + if (StringUtils.isNotBlank(postSqlBuffer.toString())) { + OdpsUtil.runSqlTaskWithRetry(this.odps, postSqlBuffer.toString(), "postSql"); + } else { + LOG.info("skip to execute the postSql: {}", JSON.toJSONString(postSqls)); + } + + long endTime = System.currentTimeMillis(); + LOG.info( + String.format("Exectue odpsreader postSql successfully! cost time: %s ms.", (endTime - beginTime))); + } } @Override @@ -268,6 +400,7 @@ public void destroy() { public static class Task extends Reader.Task { private static final Logger LOG = LoggerFactory.getLogger(Task.class); + private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(OdpsReader.class); private Configuration readerSliceConf; private String tunnelServer; @@ -278,32 +411,35 @@ public static class Task extends Reader.Task { private boolean isPartitionedTable; private String sessionId; private boolean isCompress; + private boolean successOnNoPartition; @Override public void init() { this.readerSliceConf = super.getPluginJobConf(); this.tunnelServer = this.readerSliceConf.getString( - Key.TUNNEL_SERVER, null); + Key.TUNNEL_SERVER, null); this.odps = OdpsUtil.initOdps(this.readerSliceConf); this.projectName = this.readerSliceConf.getString(Key.PROJECT); this.tableName = this.readerSliceConf.getString(Key.TABLE); this.table = OdpsUtil.getTable(this.odps, projectName, tableName); this.isPartitionedTable = this.readerSliceConf - .getBool(Constant.IS_PARTITIONED_TABLE); + .getBool(Constant.IS_PARTITIONED_TABLE); this.sessionId = this.readerSliceConf.getString(Constant.SESSION_ID, null); - - - this.isCompress = this.readerSliceConf.getBool(Key.IS_COMPRESS, false); + this.successOnNoPartition = this.readerSliceConf.getBool(Key.SUCCESS_ON_NO_PATITION, false); // sessionId 为空的情况是:切分级别只到 partition 的情况 - if (StringUtils.isBlank(this.sessionId)) { + String partition = this.readerSliceConf.getString(Key.PARTITION); + + // 没有分区读取时, 是没有sessionId这些的 + if (this.isPartitionedTable && StringUtils.isBlank(partition) && this.successOnNoPartition) { + LOG.warn("Partition is blank, but you config successOnNoPartition[true] ,don't need to create session"); + } else if (StringUtils.isBlank(this.sessionId)) { DownloadSession session = OdpsUtil.createMasterSessionForPartitionedTable(odps, - tunnelServer, projectName, tableName, this.readerSliceConf.getString(Key.PARTITION)); + tunnelServer, projectName, tableName, this.readerSliceConf.getString(Key.PARTITION)); this.sessionId = session.getId(); } - LOG.info("sessionId:{}", this.sessionId); } @@ -316,68 +452,61 @@ public void startRead(RecordSender recordSender) { DownloadSession downloadSession = null; String partition = this.readerSliceConf.getString(Key.PARTITION); + if (this.isPartitionedTable && StringUtils.isBlank(partition) && this.successOnNoPartition) { + LOG.warn(String.format( + "Partition is blank,not need to be read")); + recordSender.flush(); + return; + } + if (this.isPartitionedTable) { downloadSession = OdpsUtil.getSlaveSessionForPartitionedTable(this.odps, this.sessionId, - this.tunnelServer, this.projectName, this.tableName, partition); + this.tunnelServer, this.projectName, this.tableName, partition); } else { downloadSession = OdpsUtil.getSlaveSessionForNonPartitionedTable(this.odps, this.sessionId, - this.tunnelServer, this.projectName, this.tableName); + this.tunnelServer, this.projectName, this.tableName); } long start = this.readerSliceConf.getLong(Constant.START_INDEX, 0); long count = this.readerSliceConf.getLong(Constant.STEP_COUNT, - downloadSession.getRecordCount()); + downloadSession.getRecordCount()); if (count > 0) { LOG.info(String.format( - "Begin to read ODPS table:%s, partition:%s, startIndex:%s, count:%s.", - this.tableName, partition, start, count)); + "Begin to read ODPS table:%s, partition:%s, startIndex:%s, count:%s.", + this.tableName, partition, start, count)); } else if (count == 0) { - LOG.warn(String.format("源头表:%s 的分区:%s 没有内容可抽取, 请您知晓.", - this.tableName, partition)); + LOG.warn(MESSAGE_SOURCE.message("odpsreader.12", this.tableName, partition)); return; } else { throw DataXException.asDataXException(OdpsReaderErrorCode.READ_DATA_FAIL, - String.format("源头表:%s 的分区:%s 读取行数为负数, 请联系 ODPS 管理员查看表状态!", - this.tableName, partition)); + MESSAGE_SOURCE.message("odpsreader.13", this.tableName, partition)); } - + TableSchema tableSchema = this.table.getSchema(); Set allColumns = new HashSet(); allColumns.addAll(tableSchema.getColumns()); allColumns.addAll(tableSchema.getPartitionColumns()); - Map columnTypeMap = new HashMap(); + Map columnTypeMap = new HashMap(); for (Column column : allColumns) { - columnTypeMap.put(column.getName(), column.getType()); + columnTypeMap.put(column.getName(), column.getTypeInfo()); } try { - List parsedColumnsTmp = this.readerSliceConf - .getListConfiguration(Constant.PARSED_COLUMNS); - List> parsedColumns = new ArrayList>(); - for (int i = 0; i < parsedColumnsTmp.size(); i++) { - Configuration eachColumnConfig = parsedColumnsTmp.get(i); - String columnName = eachColumnConfig.getString("left"); - ColumnType columnType = ColumnType - .asColumnType(eachColumnConfig.getString("right")); - parsedColumns.add(new MutablePair( - columnName, columnType)); - - } + List parsedColumns = this.readerSliceConf.getListWithJson(Constant.PARSED_COLUMNS, + InternalColumnInfo.class); ReaderProxy readerProxy = new ReaderProxy(recordSender, downloadSession, columnTypeMap, parsedColumns, partition, this.isPartitionedTable, - start, count, this.isCompress); - + start, count, this.isCompress, this.readerSliceConf); readerProxy.doRead(); } catch (Exception e) { throw DataXException.asDataXException(OdpsReaderErrorCode.READ_DATA_FAIL, - String.format("源头表:%s 的分区:%s 读取失败, 请联系 ODPS 管理员查看错误详情.", this.tableName, partition), e); + MESSAGE_SOURCE.message("odpsreader.14", this.tableName, partition), e); } } - @Override public void post() { } diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/OdpsReaderErrorCode.java b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/OdpsReaderErrorCode.java index cdda6ac862..8311d4efc3 100755 --- a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/OdpsReaderErrorCode.java +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/OdpsReaderErrorCode.java @@ -1,45 +1,53 @@ package com.alibaba.datax.plugin.reader.odpsreader; import com.alibaba.datax.common.spi.ErrorCode; +import com.alibaba.datax.common.util.MessageSource; public enum OdpsReaderErrorCode implements ErrorCode { - REQUIRED_VALUE("OdpsReader-00", "您缺失了必须填写的参数值."), - ILLEGAL_VALUE("OdpsReader-01", "您配置的值不合法."), - CREATE_DOWNLOADSESSION_FAIL("OdpsReader-03", "创建 ODPS 的 downloadSession 失败."), - GET_DOWNLOADSESSION_FAIL("OdpsReader-04", "获取 ODPS 的 downloadSession 失败."), - READ_DATA_FAIL("OdpsReader-05", "读取 ODPS 源头表失败."), - GET_ID_KEY_FAIL("OdpsReader-06", "获取 accessId/accessKey 失败."), + REQUIRED_VALUE("DATAX_R_ODPS_001", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_001"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_001")), + ILLEGAL_VALUE("DATAX_R_ODPS_002", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_002"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_002")), + CREATE_DOWNLOADSESSION_FAIL("DATAX_R_ODPS_003", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_003"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_003")), + GET_DOWNLOADSESSION_FAIL("DATAX_R_ODPS_004", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_004"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_004")), + READ_DATA_FAIL("DATAX_R_ODPS_005", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_005"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_005")), + GET_ID_KEY_FAIL("DATAX_R_ODPS_006", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_006"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_006")), - ODPS_READ_EXCEPTION("OdpsReader-07", "读取 odps 异常"), - OPEN_RECORD_READER_FAILED("OdpsReader-08", "打开 recordReader 失败."), + ODPS_READ_EXCEPTION("DATAX_R_ODPS_007", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_007"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_007")), + OPEN_RECORD_READER_FAILED("DATAX_R_ODPS_008", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_008"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_008")), - ODPS_PROJECT_NOT_FOUNT("OdpsReader-10", "您配置的值不合法, odps project 不存在."), //ODPS-0420111: Project not found + ODPS_PROJECT_NOT_FOUNT("DATAX_R_ODPS_009", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_009"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_009")), //ODPS-0420111: Project not found - ODPS_TABLE_NOT_FOUNT("OdpsReader-12", "您配置的值不合法, odps table 不存在."), // ODPS-0130131:Table not found + ODPS_TABLE_NOT_FOUNT("DATAX_R_ODPS_010", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_010"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_010")), // ODPS-0130131:Table not found - ODPS_ACCESS_KEY_ID_NOT_FOUND("OdpsReader-13", "您配置的值不合法, odps accessId,accessKey 不存在."), //ODPS-0410051:Invalid credentials - accessKeyId not found + ODPS_ACCESS_KEY_ID_NOT_FOUND("DATAX_R_ODPS_011", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_011"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_011")), //ODPS-0410051:Invalid credentials - accessKeyId not found - ODPS_ACCESS_KEY_INVALID("OdpsReader-14", "您配置的值不合法, odps accessKey 错误."), //ODPS-0410042:Invalid signature value - User signature dose not match + ODPS_ACCESS_KEY_INVALID("DATAX_R_ODPS_012", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_012"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_012")), //ODPS-0410042:Invalid signature value - User signature dose not match - ODPS_ACCESS_DENY("OdpsReader-15", "拒绝访问, 您不在 您配置的 project 中."), //ODPS-0420095: Access Denied - Authorization Failed [4002], You doesn't exist in project + ODPS_ACCESS_DENY("DATAX_R_ODPS_013", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_013"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_013")), //ODPS-0420095: Access Denied - Authorization Failed [4002], You doesn't exist in project - SPLIT_MODE_ERROR("OdpsReader-30", "splitMode配置错误."), + SPLIT_MODE_ERROR("DATAX_R_ODPS_014", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_014"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_014")), - ACCOUNT_TYPE_ERROR("OdpsReader-31", "odps 账号类型错误."), + ACCOUNT_TYPE_ERROR("DATAX_R_ODPS_015", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_015"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_015")), - VIRTUAL_VIEW_NOT_SUPPORT("OdpsReader-32", "Datax 不支持 读取虚拟视图."), + VIRTUAL_VIEW_NOT_SUPPORT("DATAX_R_ODPS_016", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_016"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_016")), - PARTITION_ERROR("OdpsReader-33", "分区配置错误."), + PARTITION_ERROR("DATAX_R_ODPS_017", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_017"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_017")), + PARTITION_NOT_EXISTS_ERROR("DATAX_R_ODPS_018", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_018"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_018")), + + RUN_SQL_FAILED("DATAX_R_ODPS_019", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_019"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_019")), + + RUN_SQL_ODPS_EXCEPTION("DATAX_R_ODPS_020", MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("description.DATAX_R_ODPS_020"),MessageSource.loadResourceBundle(OdpsReaderErrorCode.class).message("solution.DATAX_R_ODPS_020")), ; private final String code; private final String description; + private final String solution; - private OdpsReaderErrorCode(String code, String description) { + private OdpsReaderErrorCode(String code, String description,String solution) { this.code = code; this.description = description; + this.solution = solution; } @Override @@ -52,9 +60,12 @@ public String getDescription() { return this.description; } + public String getSolution() { + return solution; + } + @Override public String toString() { - return String.format("Code:[%s], Description:[%s]. ", this.code, - this.description); + return String.format("Code:%s:%s, Solution:[%s]. ", this.code,this.description,this.solution); } } diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/ReaderProxy.java b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/ReaderProxy.java index 8e069ef568..c2e88eba48 100755 --- a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/ReaderProxy.java +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/ReaderProxy.java @@ -3,40 +3,75 @@ import com.alibaba.datax.common.element.*; import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.MessageSource; import com.alibaba.datax.plugin.reader.odpsreader.util.OdpsUtil; +import com.alibaba.fastjson2.JSON; +import com.aliyun.odps.Column; import com.aliyun.odps.OdpsType; +import com.aliyun.odps.data.*; import com.aliyun.odps.data.Record; -import com.aliyun.odps.data.RecordReader; import com.aliyun.odps.tunnel.TableTunnel; -import org.apache.commons.lang3.tuple.Pair; +import com.aliyun.odps.type.ArrayTypeInfo; +import com.aliyun.odps.type.MapTypeInfo; +import com.aliyun.odps.type.TypeInfo; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.text.ParseException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.text.SimpleDateFormat; +import java.util.*; public class ReaderProxy { private static final Logger LOG = LoggerFactory .getLogger(ReaderProxy.class); + private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(ReaderProxy.class); private static boolean IS_DEBUG = LOG.isDebugEnabled(); private RecordSender recordSender; private TableTunnel.DownloadSession downloadSession; - private Map columnTypeMap; - private List> parsedColumns; + private Map columnTypeMap; + private List parsedColumns; private String partition; private boolean isPartitionTable; private long start; private long count; private boolean isCompress; + + private static final String NULL_INDICATOR = null; + // TODO 没有支持用户可配置 + // TODO 没有timezone + private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + // 读取 jvm 默认时区 + private Calendar calendarForDate = null; + private boolean useDateWithCalendar = true; + + private Calendar initCalendar(Configuration config) { + // 理论上不会有其他选择,有配置化可以随时应急 + String calendarType = config.getString("calendarType", "iso8601"); + Boolean lenient = config.getBool("calendarLenient", true); + + // 默认jvm时区 + TimeZone timeZone = TimeZone.getDefault(); + String timeZoneStr = config.getString("calendarTimeZone"); + if (StringUtils.isNotBlank(timeZoneStr)) { + // 如果用户明确指定使用用户指定的 + timeZone = TimeZone.getTimeZone(timeZoneStr); + } + + Calendar calendarForDate = new Calendar.Builder().setCalendarType(calendarType).setLenient(lenient) + .setTimeZone(timeZone).build(); + return calendarForDate; + } public ReaderProxy(RecordSender recordSender, TableTunnel.DownloadSession downloadSession, - Map columnTypeMap, - List> parsedColumns, String partition, - boolean isPartitionTable, long start, long count, boolean isCompress) { + Map columnTypeMap, + List parsedColumns, String partition, + boolean isPartitionTable, long start, long count, boolean isCompress, Configuration taskConfig) { this.recordSender = recordSender; this.downloadSession = downloadSession; this.columnTypeMap = columnTypeMap; @@ -46,14 +81,24 @@ public ReaderProxy(RecordSender recordSender, TableTunnel.DownloadSession downlo this.start = start; this.count = count; this.isCompress = isCompress; + + this.calendarForDate = this.initCalendar(taskConfig); + this.useDateWithCalendar = taskConfig.getBool("useDateWithCalendar", true); } // warn: odps 分区列和正常列不能重名, 所有列都不不区分大小写 public void doRead() { try { LOG.info("start={}, count={}",start, count); - //RecordReader recordReader = downloadSession.openRecordReader(start, count, isCompress); - RecordReader recordReader = OdpsUtil.getRecordReader(downloadSession, start, count, isCompress); + List userConfigNormalColumns = OdpsUtil.getNormalColumns(this.parsedColumns, this.columnTypeMap); + RecordReader recordReader = null; + // fix #ODPS-52184/10332469, updateColumnsSize表示如果用户指定的读取源表列数100列以内的话,则进行列裁剪优化; + int updateColumnsSize = 100; + if(userConfigNormalColumns.size() <= updateColumnsSize){ + recordReader = OdpsUtil.getRecordReader(downloadSession, start, count, isCompress, userConfigNormalColumns); + } else { + recordReader = OdpsUtil.getRecordReader(downloadSession, start, count, isCompress); + } Record odpsRecord; Map partitionMap = this @@ -72,7 +117,7 @@ public void doRead() { } catch (InterruptedException ignored) { } recordReader = downloadSession.openRecordReader(start, count, isCompress); - LOG.warn("odps-read-exception, 重试第{}次", retryTimes); + LOG.warn(MESSAGE_SOURCE.message("readerproxy.1", retryTimes)); retryTimes++; continue; } else { @@ -90,9 +135,9 @@ public void doRead() { // warn: for PARTITION||NORMAL columnTypeMap's key // sets(columnName) is big than parsedColumns's left // sets(columnName), always contain - for (Pair pair : this.parsedColumns) { - String columnName = pair.getLeft(); - switch (pair.getRight()) { + for (InternalColumnInfo pair : this.parsedColumns) { + String columnName = pair.getColumnName(); + switch (pair.getColumnType()) { case PARTITION: String partitionColumnValue = this .getPartitionColumnValue(partitionMap, @@ -144,9 +189,7 @@ private Map parseCurrentPartitionValue() { throw DataXException .asDataXException( OdpsReaderErrorCode.ILLEGAL_VALUE, - String.format( - "您的分区 [%s] 解析出现错误,解析后正确的配置方式类似为 [ pt=1,dt=1 ].", - eachPartition)); + MESSAGE_SOURCE.message("readerproxy.2", eachPartition)); } // warn: translate to lower case, it's more comfortable to // compare whit user's input columns @@ -157,7 +200,7 @@ private Map parseCurrentPartitionValue() { } if (IS_DEBUG) { LOG.debug(String.format("partition value details: %s", - com.alibaba.fastjson.JSON.toJSONString(partitionMap))); + com.alibaba.fastjson2.JSON.toJSONString(partitionMap))); } return partitionMap; } @@ -168,9 +211,8 @@ private String getPartitionColumnValue(Map partitionMap, partitionColumnName = partitionColumnName.toLowerCase(); // it's will never happen, but add this checking if (!partitionMap.containsKey(partitionColumnName)) { - String errorMessage = String.format( - "表所有分区信息为: %s 其中找不到 [%s] 对应的分区值.", - com.alibaba.fastjson.JSON.toJSONString(partitionMap), + String errorMessage = MESSAGE_SOURCE.message("readerproxy.3", + com.alibaba.fastjson2.JSON.toJSONString(partitionMap), partitionColumnName); throw DataXException.asDataXException( OdpsReaderErrorCode.READ_DATA_FAIL, errorMessage); @@ -190,7 +232,7 @@ private String getPartitionColumnValue(Map partitionMap, * every line record of odps table * @param dataXRecord * every datax record, to be send to writer. method getXXX() case sensitive - * @param type + * @param typeInfo * odps column type * @param columnNameValue * for partition column it's column value, for normal column it's @@ -199,83 +241,681 @@ private String getPartitionColumnValue(Map partitionMap, * true means partition column and false means normal column * */ private void odpsColumnToDataXField(Record odpsRecord, - com.alibaba.datax.common.element.Record dataXRecord, OdpsType type, + com.alibaba.datax.common.element.Record dataXRecord, TypeInfo typeInfo, String columnNameValue, boolean isPartitionColumn) { + + ArrayRecord record = (ArrayRecord) odpsRecord; + + OdpsType type = typeInfo.getOdpsType(); + switch (type) { - case BIGINT: { - if (isPartitionColumn) { - dataXRecord.addColumn(new LongColumn(columnNameValue)); - } else { - dataXRecord.addColumn(new LongColumn(odpsRecord - .getBigint(columnNameValue))); + case BIGINT: { + if (isPartitionColumn) { + dataXRecord.addColumn(new LongColumn(columnNameValue)); + } else { + dataXRecord.addColumn(new LongColumn(record + .getBigint(columnNameValue))); + } + break; } - break; - } - case BOOLEAN: { - if (isPartitionColumn) { - dataXRecord.addColumn(new BoolColumn(columnNameValue)); - } else { - dataXRecord.addColumn(new BoolColumn(odpsRecord - .getBoolean(columnNameValue))); + case BOOLEAN: { + if (isPartitionColumn) { + dataXRecord.addColumn(new BoolColumn(columnNameValue)); + } else { + dataXRecord.addColumn(new BoolColumn(record + .getBoolean(columnNameValue))); + } + break; } - break; - } - case DATETIME: { - if (isPartitionColumn) { - try { - dataXRecord.addColumn(new DateColumn(ColumnCast + case DATE: + case DATETIME: { + // odps分区列,目前支持TINYINT、SMALLINT、INT、BIGINT、VARCHAR和STRING类型 + if (isPartitionColumn) { + try { + dataXRecord.addColumn(new DateColumn(ColumnCast + .string2Date(new StringColumn(columnNameValue)))); + } catch (ParseException e) { + String errMessage = MESSAGE_SOURCE.message("readerproxy.4", + this.partition, columnNameValue); + LOG.error(errMessage); + throw DataXException.asDataXException( + OdpsReaderErrorCode.READ_DATA_FAIL, errMessage, e); + } + } else { + if (com.aliyun.odps.OdpsType.DATETIME == type) { + dataXRecord.addColumn(new DateColumn(record + .getDatetime(columnNameValue))); + } else { + if (this.useDateWithCalendar) { + dataXRecord.addColumn(new DateColumn(record. + getDate(columnNameValue, this.calendarForDate))); + } else { + dataXRecord.addColumn(new DateColumn(record + .getDate(columnNameValue))); + } + + } + } + + break; + } + case DOUBLE: { + if (isPartitionColumn) { + dataXRecord.addColumn(new DoubleColumn(columnNameValue)); + } else { + dataXRecord.addColumn(new DoubleColumn(record + .getDouble(columnNameValue))); + } + break; + } + case DECIMAL: { + if(isPartitionColumn) { + dataXRecord.addColumn(new DoubleColumn(columnNameValue)); + } else { + dataXRecord.addColumn(new DoubleColumn(record.getDecimal(columnNameValue))); + } + break; + } + case STRING: { + if (isPartitionColumn) { + dataXRecord.addColumn(new StringColumn(columnNameValue)); + } else { + dataXRecord.addColumn(new StringColumn(record + .getString(columnNameValue))); + } + break; + } + case TINYINT: + if (isPartitionColumn) { + dataXRecord.addColumn(new LongColumn(columnNameValue)); + } else { + Byte value = record.getTinyint(columnNameValue); + Integer intValue = value != null ? value.intValue() : null; + dataXRecord.addColumn(new LongColumn(intValue)); + } + break; + case SMALLINT: { + if (isPartitionColumn) { + dataXRecord.addColumn(new LongColumn(columnNameValue)); + } else { + Short value = record.getSmallint(columnNameValue); + Long valueInLong = null; + if (null != value) { + valueInLong = value.longValue(); + } + dataXRecord.addColumn(new LongColumn(valueInLong)); + } + break; + } + case INT: { + if (isPartitionColumn) { + dataXRecord.addColumn(new LongColumn(columnNameValue)); + } else { + dataXRecord.addColumn(new LongColumn(record + .getInt(columnNameValue))); + } + break; + } + case FLOAT: { + if (isPartitionColumn) { + dataXRecord.addColumn(new DoubleColumn(columnNameValue)); + } else { + dataXRecord.addColumn(new DoubleColumn(record + .getFloat(columnNameValue))); + } + break; + } + case VARCHAR: { + if (isPartitionColumn) { + dataXRecord.addColumn(new StringColumn(columnNameValue)); + } else { + Varchar value = record.getVarchar(columnNameValue); + String columnValue = value != null ? value.getValue() : null; + dataXRecord.addColumn(new StringColumn(columnValue)); + } + break; + } + case TIMESTAMP: { + if (isPartitionColumn) { + try { + dataXRecord.addColumn(new DateColumn(ColumnCast .string2Date(new StringColumn(columnNameValue)))); - } catch (ParseException e) { - LOG.error(String.format("", this.partition)); - String errMessage = String.format( - "您读取分区 [%s] 出现日期转换异常, 日期的字符串表示为 [%s].", + } catch (ParseException e) { + String errMessage = MESSAGE_SOURCE.message("readerproxy.4", this.partition, columnNameValue); - LOG.error(errMessage); - throw DataXException.asDataXException( + LOG.error(errMessage); + throw DataXException.asDataXException( OdpsReaderErrorCode.READ_DATA_FAIL, errMessage, e); + } + } else { + dataXRecord.addColumn(new DateColumn(record + .getTimestamp(columnNameValue))); } - } else { - dataXRecord.addColumn(new DateColumn(odpsRecord - .getDatetime(columnNameValue))); - } - break; - } - case DOUBLE: { - if (isPartitionColumn) { - dataXRecord.addColumn(new DoubleColumn(columnNameValue)); - } else { - dataXRecord.addColumn(new DoubleColumn(odpsRecord - .getDouble(columnNameValue))); + break; } - break; + case BINARY: { + if (isPartitionColumn) { + dataXRecord.addColumn(new BytesColumn(columnNameValue.getBytes())); + } else { +// dataXRecord.addColumn(new BytesColumn(record +// .getBinary(columnNameValue).data())); + Binary binaryData = record.getBinary(columnNameValue); + if (null == binaryData) { + dataXRecord.addColumn(new BytesColumn(null)); + } else { + dataXRecord.addColumn(new BytesColumn(binaryData.data())); + } + } + break; + } + case ARRAY: { + if (isPartitionColumn) { + dataXRecord.addColumn(new StringColumn(columnNameValue)); + } else { + List arrayValue = record.getArray(columnNameValue); + if (arrayValue == null) { + dataXRecord.addColumn(new StringColumn(null)); + } else { + dataXRecord.addColumn(new StringColumn(JSON.toJSONString(transOdpsArrayToJavaList(arrayValue, (ArrayTypeInfo)typeInfo)))); + } + } + break; + } + case MAP: { + if (isPartitionColumn) { + dataXRecord.addColumn(new StringColumn(columnNameValue)); + } else { + Map mapValue = record.getMap(columnNameValue); + if (mapValue == null) { + dataXRecord.addColumn(new StringColumn(null)); + } else { + dataXRecord.addColumn(new StringColumn(JSON.toJSONString(transOdpsMapToJavaMap(mapValue, (MapTypeInfo)typeInfo)))); + } + } + break; + } + case STRUCT: { + if (isPartitionColumn) { + dataXRecord.addColumn(new StringColumn(columnNameValue)); + } else { + Struct structValue = record.getStruct(columnNameValue); + if (structValue == null) { + dataXRecord.addColumn(new StringColumn(null)); + } else { + dataXRecord.addColumn(new StringColumn(JSON.toJSONString(transOdpsStructToJavaMap(structValue)))); + } + } + break; + } + default: + throw DataXException.asDataXException( + OdpsReaderErrorCode.ILLEGAL_VALUE, + MESSAGE_SOURCE.message("readerproxy.5", type)); } - case DECIMAL: { - if(isPartitionColumn) { - dataXRecord.addColumn(new DoubleColumn(columnNameValue)); - } else { - dataXRecord.addColumn(new DoubleColumn(odpsRecord.getDecimal(columnNameValue))); + } + + private List transOdpsArrayToJavaList(List odpsArray, ArrayTypeInfo typeInfo) { + TypeInfo eleType = typeInfo.getElementTypeInfo(); + List result = new ArrayList(); + switch (eleType.getOdpsType()) { + // warn:array [1.2, 3.4] 被转为了:"["1.2", "3.4"]", 本来应该被转换成 "[1.2, 3.4]" + // 注意回归Case覆盖 + case BIGINT: + case DOUBLE: + case INT: + case FLOAT: + case DECIMAL: + case TINYINT: + case SMALLINT: + for (Object item : odpsArray) { + Object object = item; + result.add(object == null ? NULL_INDICATOR : object); + } + return result; + case BOOLEAN: // 未调整array 问题 + case STRING: + case VARCHAR: + case CHAR: + case TIMESTAMP: + case DATE: + for (Object item : odpsArray) { + Object object = item; + result.add(object == null ? NULL_INDICATOR : object.toString()); + } + return result; + /** + * 日期类型 + */ + case DATETIME: + for (Object item : odpsArray) { + Date dateVal = (Date) item; + result.add(dateVal == null ? NULL_INDICATOR : dateFormat.format(dateVal)); + } + return result; + /** + * 字节数组 + */ + case BINARY: + for (Object item : odpsArray) { + Binary binaryVal = (Binary) item; + result.add(binaryVal == null ? NULL_INDICATOR : + Base64.encodeBase64(binaryVal.data())); + } + return result; + /** + * 日期间隔 + */ + case INTERVAL_DAY_TIME: + for (Object item : odpsArray) { + IntervalDayTime dayTimeVal = (IntervalDayTime) item; + result.add(dayTimeVal == null ? NULL_INDICATOR : + transIntervalDayTimeToJavaMap(dayTimeVal)); + } + return result; + /** + * 年份间隔 + */ + case INTERVAL_YEAR_MONTH: + for (Object item : odpsArray) { + IntervalYearMonth yearMonthVal = (IntervalYearMonth) item; + result.add(yearMonthVal == null ? NULL_INDICATOR : + transIntervalYearMonthToJavaMap(yearMonthVal)); + } + return result; + /** + * 结构体 + */ + case STRUCT: + for (Object item : odpsArray) { + Struct structVal = (Struct) item; + result.add(structVal == null ? NULL_INDICATOR : + transOdpsStructToJavaMap(structVal)); } - break; + return result; + /** + * MAP类型 + */ + case MAP: + for (Object item : odpsArray) { + Map mapVal = (Map) item; + result.add(mapVal == null ? NULL_INDICATOR : + transOdpsMapToJavaMap(mapVal, (MapTypeInfo) eleType)); + } + return result; + /** + * ARRAY类型 + */ + case ARRAY: + for (Object item : odpsArray) { + List arrayVal = (List) item; + result.add(arrayVal == null ? NULL_INDICATOR : + transOdpsArrayToJavaList(arrayVal, (ArrayTypeInfo) eleType)); + } + return result; + default: + throw new IllegalArgumentException("decode record failed. column type: " + eleType.getTypeName()); } - case STRING: { - if (isPartitionColumn) { - dataXRecord.addColumn(new StringColumn(columnNameValue)); - } else { - dataXRecord.addColumn(new StringColumn(odpsRecord - .getString(columnNameValue))); + } + + private Map transOdpsMapToJavaMap(Map odpsMap, MapTypeInfo typeInfo) { + TypeInfo keyType = typeInfo.getKeyTypeInfo(); + TypeInfo valueType = typeInfo.getValueTypeInfo(); + Map result = new HashMap(); + Set entrySet = null; + switch (valueType.getOdpsType()) { + case BIGINT: + case DOUBLE: + case BOOLEAN: + case STRING: + case DECIMAL: + case TINYINT: + case SMALLINT: + case INT: + case FLOAT: + case CHAR: + case VARCHAR: + case DATE: + case TIMESTAMP: + switch (keyType.getOdpsType()) { + case DATETIME: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Object value = item.getValue(); + result.put(dateFormat.format((Date)item.getKey()), value == null ? NULL_INDICATOR : value.toString()); + } + return result; + case BINARY: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Object value = item.getValue(); + result.put(Base64.encodeBase64(((Binary)item.getKey()).data()), + value == null ? NULL_INDICATOR : value.toString()); + } + return result; + default: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Object value = item.getValue(); + result.put(item.getKey(), value == null ? NULL_INDICATOR : value.toString()); + } + return result; } - break; + /** + * 日期类型 + */ + case DATETIME: + switch (keyType.getOdpsType()) { + case DATETIME: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Date dateVal = (Date) item.getValue(); + result.put(dateFormat.format((Date)item.getKey()), + dateVal == null ? NULL_INDICATOR : dateFormat.format(dateVal)); + } + return result; + case BINARY: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Date dateVal = (Date) item.getValue(); + result.put(Base64.encodeBase64(((Binary)item.getKey()).data()), + dateVal == null ? NULL_INDICATOR : dateFormat.format(dateVal)); + } + return result; + default: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Date dateVal = (Date) item.getValue(); + result.put(item.getKey(), dateVal == null ? NULL_INDICATOR : dateFormat.format(dateVal)); + } + return result; + } + /** + * 字节数组 + */ + case BINARY: + switch (keyType.getOdpsType()) { + case DATETIME: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Binary binaryVal = (Binary) item.getValue(); + result.put(dateFormat.format((Date)item.getKey()), binaryVal == null ? NULL_INDICATOR : + Base64.encodeBase64(binaryVal.data())); + } + return result; + case BINARY: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Binary binaryVal = (Binary) item.getValue(); + result.put(Base64.encodeBase64(((Binary)item.getKey()).data()), + binaryVal == null ? NULL_INDICATOR : + Base64.encodeBase64(binaryVal.data())); + } + return result; + default: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Binary binaryVal = (Binary) item.getValue(); + result.put(item.getKey(), binaryVal == null ? NULL_INDICATOR : + Base64.encodeBase64(binaryVal.data())); + } + return result; + } + /** + * 日期间隔 + */ + case INTERVAL_DAY_TIME: + switch (keyType.getOdpsType()) { + case DATETIME: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + IntervalDayTime dayTimeVal = (IntervalDayTime) item.getValue(); + result.put(dateFormat.format((Date)item.getKey()), dayTimeVal == null ? NULL_INDICATOR : + transIntervalDayTimeToJavaMap(dayTimeVal)); + } + return result; + case BINARY: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + IntervalDayTime dayTimeVal = (IntervalDayTime) item.getValue(); + result.put(Base64.encodeBase64(((Binary)item.getKey()).data()), + dayTimeVal == null ? NULL_INDICATOR : + transIntervalDayTimeToJavaMap(dayTimeVal)); + } + return result; + default: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + IntervalDayTime dayTimeVal = (IntervalDayTime) item.getValue(); + result.put(item.getKey(), dayTimeVal == null ? NULL_INDICATOR : + transIntervalDayTimeToJavaMap(dayTimeVal)); + } + return result; + } + /** + * 年份间隔 + */ + case INTERVAL_YEAR_MONTH: + switch (keyType.getOdpsType()) { + case DATETIME: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + IntervalYearMonth yearMonthVal = (IntervalYearMonth) item.getValue(); + result.put(dateFormat.format((Date)item.getKey()), yearMonthVal == null ? NULL_INDICATOR : + transIntervalYearMonthToJavaMap(yearMonthVal)); + } + return result; + case BINARY: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + IntervalYearMonth yearMonthVal = (IntervalYearMonth) item.getValue(); + result.put(Base64.encodeBase64(((Binary)item.getKey()).data()), + yearMonthVal == null ? NULL_INDICATOR : + transIntervalYearMonthToJavaMap(yearMonthVal)); + } + return result; + default: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + IntervalYearMonth yearMonthVal = (IntervalYearMonth) item.getValue(); + result.put(item.getKey(), yearMonthVal == null ? NULL_INDICATOR : + transIntervalYearMonthToJavaMap(yearMonthVal)); + } + return result; + } + /** + * 结构体 + */ + case STRUCT: + switch (keyType.getOdpsType()) { + case DATETIME: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Struct structVal = (Struct) item.getValue(); + result.put(dateFormat.format((Date)item.getKey()), structVal == null ? NULL_INDICATOR : + transOdpsStructToJavaMap(structVal)); + } + return result; + case BINARY: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Struct structVal = (Struct) item.getValue(); + result.put(Base64.encodeBase64(((Binary)item.getKey()).data()), + structVal == null ? NULL_INDICATOR : + transOdpsStructToJavaMap(structVal)); + } + return result; + default: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Struct structVal = (Struct) item.getValue(); + result.put(item.getKey(), structVal == null ? NULL_INDICATOR : + transOdpsStructToJavaMap(structVal)); + } + return result; + } + /** + * MAP类型 + */ + case MAP: + switch (keyType.getOdpsType()) { + case DATETIME: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Map mapVal = (Map) item.getValue(); + result.put(dateFormat.format((Date)item.getKey()),mapVal == null ? NULL_INDICATOR : + transOdpsMapToJavaMap(mapVal, (MapTypeInfo) valueType)); + } + return result; + case BINARY: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Map mapVal = (Map) item.getValue(); + result.put(Base64.encodeBase64(((Binary)item.getKey()).data()), + mapVal == null ? NULL_INDICATOR : transOdpsMapToJavaMap(mapVal, (MapTypeInfo) valueType)); + } + return result; + default: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + Map mapVal = (Map) item.getValue(); + result.put(item.getKey(), mapVal == null ? NULL_INDICATOR : + transOdpsMapToJavaMap(mapVal, (MapTypeInfo) valueType)); + } + return result; + } + /** + * ARRAY类型 + */ + case ARRAY: + switch (keyType.getOdpsType()) { + case DATETIME: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + List arrayVal = (List) item.getValue(); + result.put(dateFormat.format((Date)item.getKey()),arrayVal == null ? NULL_INDICATOR : + transOdpsArrayToJavaList(arrayVal, (ArrayTypeInfo) valueType)); + } + return result; + case BINARY: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + List arrayVal = (List) item.getValue(); + result.put(Base64.encodeBase64(((Binary)item.getKey()).data()), + arrayVal == null ? NULL_INDICATOR : transOdpsArrayToJavaList(arrayVal, (ArrayTypeInfo) valueType)); + } + return result; + default: + entrySet = odpsMap.entrySet(); + for (Map.Entry item : entrySet) { + List arrayVal = (List) item.getValue(); + result.put(item.getKey(), arrayVal == null ? NULL_INDICATOR : + transOdpsArrayToJavaList(arrayVal, (ArrayTypeInfo) valueType)); + } + return result; + } + default: + throw new IllegalArgumentException("decode record failed. column type: " + valueType.getTypeName()); } - default: - throw DataXException - .asDataXException( - OdpsReaderErrorCode.ILLEGAL_VALUE, - String.format( - "DataX 抽取 ODPS 数据不支持字段类型为:[%s]. 目前支持抽取的字段类型有:bigint, boolean, datetime, double, decimal, string. " - + "您可以选择不抽取 DataX 不支持的字段或者联系 ODPS 管理员寻求帮助.", - type)); + } + + private Map transIntervalDayTimeToJavaMap(IntervalDayTime dayTime) { + Map result = new HashMap(); + result.put("totalSeconds", dayTime.getTotalSeconds()); + result.put("nanos", (long)dayTime.getNanos()); + return result; + } + + private Map transOdpsStructToJavaMap(Struct odpsStruct) { + Map result = new HashMap(); + for (int i = 0; i < odpsStruct.getFieldCount(); i++) { + String fieldName = odpsStruct.getFieldName(i); + Object fieldValue = odpsStruct.getFieldValue(i); + TypeInfo fieldType = odpsStruct.getFieldTypeInfo(i); + switch (fieldType.getOdpsType()) { + case BIGINT: + case DOUBLE: + case BOOLEAN: + case STRING: + case DECIMAL: + case TINYINT: + case SMALLINT: + case INT: + case FLOAT: + case VARCHAR: + case CHAR: + case TIMESTAMP: + case DATE: + result.put(fieldName, fieldValue == null ? NULL_INDICATOR : fieldValue.toString()); + break; + /** + * 日期类型 + */ + case DATETIME: + Date dateVal = (Date) fieldValue; + result.put(fieldName, dateVal == null ? NULL_INDICATOR : dateFormat.format(dateVal)); + break; + /** + * 字节数组 + */ + case BINARY: + Binary binaryVal = (Binary) fieldValue; + result.put(fieldName, binaryVal == null ? NULL_INDICATOR : + Base64.encodeBase64(binaryVal.data())); + break; + /** + * 日期间隔 + */ + case INTERVAL_DAY_TIME: + IntervalDayTime dayTimeVal = (IntervalDayTime) fieldValue; + result.put(fieldName, dayTimeVal == null ? NULL_INDICATOR : + transIntervalDayTimeToJavaMap(dayTimeVal)); + break; + /** + * 年份间隔 + */ + case INTERVAL_YEAR_MONTH: + IntervalYearMonth yearMonthVal = (IntervalYearMonth) fieldValue; + result.put(fieldName, yearMonthVal == null ? NULL_INDICATOR : + transIntervalYearMonthToJavaMap(yearMonthVal)); + break; + /** + * 结构体 + */ + case STRUCT: + Struct structVal = (Struct) fieldValue; + result.put(fieldName, structVal == null ? NULL_INDICATOR : + transOdpsStructToJavaMap(structVal)); + break; + /** + * MAP类型 + */ + case MAP: + Map mapVal = (Map) fieldValue; + result.put(fieldName, mapVal == null ? NULL_INDICATOR : + transOdpsMapToJavaMap(mapVal, (MapTypeInfo) fieldType)); + break; + /** + * ARRAY类型 + */ + case ARRAY: + List arrayVal = (List) fieldValue; + result.put(fieldName, arrayVal == null ? NULL_INDICATOR : + transOdpsArrayToJavaList(arrayVal, (ArrayTypeInfo) fieldType)); + break; + default: + throw new IllegalArgumentException("decode record failed. column type: " + fieldType.getTypeName()); + } } - } + return result; + } + + private Map transIntervalYearMonthToJavaMap(IntervalYearMonth yearMonth) { + Map result = new HashMap(); + result.put("years", yearMonth.getYears()); + result.put("months", yearMonth.getMonths()); + return result; + } + } diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/IdAndKeyUtil.java b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/IdAndKeyUtil.java deleted file mode 100644 index faa90a987d..0000000000 --- a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/IdAndKeyUtil.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * (C) 2010-2014 Alibaba Group Holding Limited. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.datax.plugin.reader.odpsreader.util; - -import com.alibaba.datax.common.exception.DataXException; -import com.alibaba.datax.common.util.Configuration; -import com.alibaba.datax.plugin.reader.odpsreader.Constant; -import com.alibaba.datax.plugin.reader.odpsreader.Key; -import com.alibaba.datax.plugin.reader.odpsreader.OdpsReaderErrorCode; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; - -public class IdAndKeyUtil { - private static Logger LOG = LoggerFactory.getLogger(IdAndKeyUtil.class); - - public static Configuration parseAccessIdAndKey(Configuration originalConfig) { - String accessId = originalConfig.getString(Key.ACCESS_ID); - String accessKey = originalConfig.getString(Key.ACCESS_KEY); - - // 只要 accessId,accessKey 二者配置了一个,就理解为是用户本意是要直接手动配置其 accessid/accessKey - if (StringUtils.isNotBlank(accessId) || StringUtils.isNotBlank(accessKey)) { - LOG.info("Try to get accessId/accessKey from your config."); - //通过如下语句,进行检查是否确实配置了 - accessId = originalConfig.getNecessaryValue(Key.ACCESS_ID, OdpsReaderErrorCode.REQUIRED_VALUE); - accessKey = originalConfig.getNecessaryValue(Key.ACCESS_KEY, OdpsReaderErrorCode.REQUIRED_VALUE); - //检查完毕,返回即可 - return originalConfig; - } else { - Map envProp = System.getenv(); - return getAccessIdAndKeyFromEnv(originalConfig, envProp); - } - } - - private static Configuration getAccessIdAndKeyFromEnv(Configuration originalConfig, - Map envProp) { - String accessId = null; - String accessKey = null; - - String skynetAccessID = envProp.get(Constant.SKYNET_ACCESSID); - String skynetAccessKey = envProp.get(Constant.SKYNET_ACCESSKEY); - - if (StringUtils.isNotBlank(skynetAccessID) - || StringUtils.isNotBlank(skynetAccessKey)) { - /** - * 环境变量中,如果存在SKYNET_ACCESSID/SKYNET_ACCESSKEy(只要有其中一个变量,则认为一定是两个都存在的!), - * 则使用其值作为odps的accessId/accessKey(会解密) - */ - - LOG.info("Try to get accessId/accessKey from environment."); - accessId = skynetAccessID; - accessKey = DESCipher.decrypt(skynetAccessKey); - if (StringUtils.isNotBlank(accessKey)) { - originalConfig.set(Key.ACCESS_ID, accessId); - originalConfig.set(Key.ACCESS_KEY, accessKey); - LOG.info("Get accessId/accessKey from environment variables successfully."); - } else { - throw DataXException.asDataXException(OdpsReaderErrorCode.GET_ID_KEY_FAIL, - String.format("从环境变量中获取accessId/accessKey 失败, accessId=[%s]", accessId)); - } - } else { - // 无处获取(既没有配置在作业中,也没用在环境变量中) - throw DataXException.asDataXException(OdpsReaderErrorCode.GET_ID_KEY_FAIL, - "无法获取到accessId/accessKey. 它们既不存在于您的配置中,也不存在于环境变量中."); - } - - return originalConfig; - } -} diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/LocalStrings.properties b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/LocalStrings.properties new file mode 100644 index 0000000000..897ce23244 --- /dev/null +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/LocalStrings.properties @@ -0,0 +1,25 @@ +descipher.1=\u957F\u5EA6\u4E0D\u662F\u5076\u6570 + +idandkeyutil.1=\u4ECE\u73AF\u5883\u53D8\u91CF\u4E2D\u83B7\u53D6accessId/accessKey \u5931\u8D25, accessId=[{0}] +idandkeyutil.2=\u65E0\u6CD5\u83B7\u53D6\u5230accessId/accessKey. \u5B83\u4EEC\u65E2\u4E0D\u5B58\u5728\u4E8E\u60A8\u7684\u914D\u7F6E\u4E2D\uFF0C\u4E5F\u4E0D\u5B58\u5728\u4E8E\u73AF\u5883\u53D8\u91CF\u4E2D. + + +odpssplitutil.1=\u60A8\u6240\u914D\u7F6E\u7684\u5206\u533A\u4E0D\u80FD\u4E3A\u7A7A\u767D. +odpssplitutil.2=\u5207\u5206\u7684 recordCount \u4E0D\u80FD\u4E3A\u8D1F\u6570.recordCount={0} +odpssplitutil.3=\u5207\u5206\u7684 adviceNum \u4E0D\u80FD\u4E3A\u8D1F\u6570.adviceNum={0} +odpssplitutil.4=\u6CE8\u610F: \u7531\u4E8E\u60A8\u914D\u7F6E\u4E86successOnNoPartition\u503C\u4E3Atrue (\u5373\u5F53\u5206\u533A\u503C\u4E0D\u5B58\u5728\u65F6, \u540C\u6B65\u4EFB\u52A1\u4E0D\u62A5\u9519), \u60A8\u8BBE\u7F6E\u7684\u5206\u533A\u65E0\u6CD5\u5339\u914D\u5230ODPS\u8868\u4E2D\u5BF9\u5E94\u7684\u5206\u533A, \u540C\u6B65\u4EFB\u52A1\u7EE7\u7EED... + +odpsutil.1=datax\u83B7\u53D6\u4E0D\u5230\u6E90\u8868\u7684\u5217\u4FE1\u606F\uFF0C \u7531\u4E8E\u60A8\u672A\u914D\u7F6E\u8BFB\u53D6\u6E90\u5934\u8868\u7684\u5217\u4FE1\u606F. datax\u65E0\u6CD5\u77E5\u9053\u8BE5\u62BD\u53D6\u8868\u7684\u54EA\u4E9B\u5B57\u6BB5\u7684\u6570\u636E\uFF0C \u6B63\u786E\u7684\u914D\u7F6E\u65B9\u5F0F\u662F\u7ED9 column \u914D\u7F6E\u4E0A\u60A8\u9700\u8981\u8BFB\u53D6\u7684\u5217\u540D\u79F0,\u7528\u82F1\u6587\u9017\u53F7\u5206\u9694. +odpsutil.2=\u60A8\u6240\u914D\u7F6E\u7684maxRetryTime \u503C\u9519\u8BEF. \u8BE5\u503C\u4E0D\u80FD\u5C0F\u4E8E1, \u4E14\u4E0D\u80FD\u5927\u4E8E {0}. \u63A8\u8350\u7684\u914D\u7F6E\u65B9\u5F0F\u662F\u7ED9maxRetryTime \u914D\u7F6E1-11\u4E4B\u95F4\u7684\u67D0\u4E2A\u503C. \u8BF7\u60A8\u68C0\u67E5\u914D\u7F6E\u5E76\u505A\u51FA\u76F8\u5E94\u4FEE\u6539. +odpsutil.3=\u4E0D\u652F\u6301\u7684\u8D26\u53F7\u7C7B\u578B:[{0}]. \u8D26\u53F7\u7C7B\u578B\u76EE\u524D\u4EC5\u652F\u6301aliyun, taobao. +odpsutil.4=\u60A8\u6240\u914D\u7F6E\u7684\u5206\u533A\u4E0D\u80FD\u4E3A\u7A7A\u767D. +odpsutil.5=\u6E90\u5934\u8868\u7684\u5217\u914D\u7F6E\u9519\u8BEF. \u60A8\u6240\u914D\u7F6E\u7684\u5217 [{0}] \u4E0D\u5B58\u5728. +odpsutil.6=open RecordReader\u5931\u8D25. \u8BF7\u8054\u7CFB ODPS \u7BA1\u7406\u5458\u5904\u7406. +odpsutil.7=\u52A0\u8F7D ODPS \u6E90\u5934\u8868:{0} \u5931\u8D25. \u8BF7\u68C0\u67E5\u60A8\u914D\u7F6E\u7684 ODPS \u6E90\u5934\u8868\u7684 [project] \u662F\u5426\u6B63\u786E. +odpsutil.8=\u52A0\u8F7D ODPS \u6E90\u5934\u8868:{0} \u5931\u8D25. \u8BF7\u68C0\u67E5\u60A8\u914D\u7F6E\u7684 ODPS \u6E90\u5934\u8868\u7684 [table] \u662F\u5426\u6B63\u786E. +odpsutil.9=\u52A0\u8F7D ODPS \u6E90\u5934\u8868:{0} \u5931\u8D25. \u8BF7\u68C0\u67E5\u60A8\u914D\u7F6E\u7684 ODPS \u6E90\u5934\u8868\u7684 [accessId] [accessKey]\u662F\u5426\u6B63\u786E. +odpsutil.10=\u52A0\u8F7D ODPS \u6E90\u5934\u8868:{0} \u5931\u8D25. \u8BF7\u68C0\u67E5\u60A8\u914D\u7F6E\u7684 ODPS \u6E90\u5934\u8868\u7684 [accessKey] \u662F\u5426\u6B63\u786E. +odpsutil.11=\u52A0\u8F7D ODPS \u6E90\u5934\u8868:{0} \u5931\u8D25. \u8BF7\u68C0\u67E5\u60A8\u914D\u7F6E\u7684 ODPS \u6E90\u5934\u8868\u7684 [accessId] [accessKey] [project]\u662F\u5426\u5339\u914D. +odpsutil.12=\u52A0\u8F7D ODPS \u6E90\u5934\u8868:{0} \u5931\u8D25. \u8BF7\u68C0\u67E5\u60A8\u914D\u7F6E\u7684 ODPS \u6E90\u5934\u8868\u7684 project,table,accessId,accessKey,odpsServer\u7B49\u503C. +odpsutil.13=\u6267\u884C ODPS SQL\u5931\u8D25, \u8FD4\u56DE\u503C\u4E3A:{0}. \u8BF7\u4ED4\u7EC6\u68C0\u67E5ODPS SQL\u662F\u5426\u6B63\u786E, \u5982\u679C\u68C0\u67E5\u65E0\u8BEF, \u8BF7\u8054\u7CFB ODPS \u503C\u73ED\u540C\u5B66\u5904\u7406. SQL \u5185\u5BB9\u4E3A:[\n{1}\n]. +odpsutil.14=\u6267\u884C ODPS SQL \u65F6\u629B\u51FA\u5F02\u5E38, \u8BF7\u4ED4\u7EC6\u68C0\u67E5ODPS SQL\u662F\u5426\u6B63\u786E, \u5982\u679C\u68C0\u67E5\u65E0\u8BEF, \u8BF7\u8054\u7CFB ODPS \u503C\u73ED\u540C\u5B66\u5904\u7406. SQL \u5185\u5BB9\u4E3A:[\n{0}\n]. \ No newline at end of file diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/OdpsSplitUtil.java b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/OdpsSplitUtil.java index b7f4f1aaf3..2030033da7 100755 --- a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/OdpsSplitUtil.java +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/OdpsSplitUtil.java @@ -2,19 +2,26 @@ import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.MessageSource; import com.alibaba.datax.common.util.RangeSplitUtil; import com.alibaba.datax.plugin.reader.odpsreader.Constant; import com.alibaba.datax.plugin.reader.odpsreader.Key; import com.alibaba.datax.plugin.reader.odpsreader.OdpsReaderErrorCode; import com.aliyun.odps.Odps; import com.aliyun.odps.tunnel.TableTunnel.DownloadSession; + import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.List; public final class OdpsSplitUtil { + private static final Logger LOG = LoggerFactory.getLogger(OdpsSplitUtil.class); + + private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(OdpsSplitUtil.class); public static List doSplit(Configuration originalConfig, Odps odps, int adviceNum) { @@ -36,9 +43,17 @@ private static List splitPartitionedTable(Odps odps, Configuratio List partitions = originalConfig.getList(Key.PARTITION, String.class); + if ((null == partitions || partitions.isEmpty()) && originalConfig.getBool(Key.SUCCESS_ON_NO_PATITION, false)) { + Configuration tempConfig = originalConfig.clone(); + tempConfig.set(Key.PARTITION, null); + splittedConfigs.add(tempConfig); + LOG.warn(MESSAGE_SOURCE.message("odpssplitutil.4")); + return splittedConfigs; + } + if (null == partitions || partitions.isEmpty()) { throw DataXException.asDataXException(OdpsReaderErrorCode.ILLEGAL_VALUE, - "您所配置的分区不能为空白."); + MESSAGE_SOURCE.message("odpssplitutil.1")); } //splitMode 默认为 record @@ -141,11 +156,11 @@ private static List splitOnePartition(Odps odps, */ private static List> splitRecordCount(long recordCount, int adviceNum) { if(recordCount<0){ - throw new IllegalArgumentException("切分的 recordCount 不能为负数.recordCount=" + recordCount); + throw new IllegalArgumentException(MESSAGE_SOURCE.message("odpssplitutil.2", recordCount)); } if(adviceNum<1){ - throw new IllegalArgumentException("切分的 adviceNum 不能为负数.adviceNum=" + adviceNum); + throw new IllegalArgumentException(MESSAGE_SOURCE.message("odpssplitutil.3", adviceNum)); } List> result = new ArrayList>(); diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/OdpsUtil.java b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/OdpsUtil.java index 2aa3f66e4a..0ff34a81ba 100755 --- a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/OdpsUtil.java +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/OdpsUtil.java @@ -2,16 +2,23 @@ import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.DataXCaseEnvUtil; +import com.alibaba.datax.common.util.MessageSource; import com.alibaba.datax.common.util.RetryUtil; import com.alibaba.datax.plugin.reader.odpsreader.ColumnType; import com.alibaba.datax.plugin.reader.odpsreader.Constant; +import com.alibaba.datax.plugin.reader.odpsreader.InternalColumnInfo; import com.alibaba.datax.plugin.reader.odpsreader.Key; import com.alibaba.datax.plugin.reader.odpsreader.OdpsReaderErrorCode; import com.aliyun.odps.*; +import com.aliyun.odps.Column; import com.aliyun.odps.account.Account; import com.aliyun.odps.account.AliyunAccount; +import com.aliyun.odps.account.StsAccount; import com.aliyun.odps.data.RecordReader; +import com.aliyun.odps.task.SQLTask; import com.aliyun.odps.tunnel.TableTunnel; +import com.aliyun.odps.type.TypeInfo; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.MutablePair; @@ -19,13 +26,12 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; import java.util.concurrent.Callable; public final class OdpsUtil { private static final Logger LOG = LoggerFactory.getLogger(OdpsUtil.class); + private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(OdpsUtil.class); public static int MAX_RETRY_TIME = 10; @@ -37,8 +43,8 @@ public static void checkNecessaryConfig(Configuration originalConfig) { if (null == originalConfig.getList(Key.COLUMN) || originalConfig.getList(Key.COLUMN, String.class).isEmpty()) { - throw DataXException.asDataXException(OdpsReaderErrorCode.REQUIRED_VALUE, "datax获取不到源表的列信息, 由于您未配置读取源头表的列信息. datax无法知道该抽取表的哪些字段的数据 " + - "正确的配置方式是给 column 配置上您需要读取的列名称,用英文逗号分隔."); + throw DataXException.asDataXException(OdpsReaderErrorCode.REQUIRED_VALUE, + MESSAGE_SOURCE.message("odpsutil.1")); } } @@ -47,8 +53,8 @@ public static void dealMaxRetryTime(Configuration originalConfig) { int maxRetryTime = originalConfig.getInt(Key.MAX_RETRY_TIME, OdpsUtil.MAX_RETRY_TIME); if (maxRetryTime < 1 || maxRetryTime > OdpsUtil.MAX_RETRY_TIME) { - throw DataXException.asDataXException(OdpsReaderErrorCode.ILLEGAL_VALUE, "您所配置的maxRetryTime 值错误. 该值不能小于1, 且不能大于 " + OdpsUtil.MAX_RETRY_TIME + - ". 推荐的配置方式是给maxRetryTime 配置1-11之间的某个值. 请您检查配置并做出相应修改."); + throw DataXException.asDataXException(OdpsReaderErrorCode.ILLEGAL_VALUE, + MESSAGE_SOURCE.message("odpsutil.2", OdpsUtil.MAX_RETRY_TIME)); } MAX_RETRY_TIME = maxRetryTime; } @@ -59,36 +65,35 @@ public static Odps initOdps(Configuration originalConfig) { String accessId = originalConfig.getString(Key.ACCESS_ID); String accessKey = originalConfig.getString(Key.ACCESS_KEY); String project = originalConfig.getString(Key.PROJECT); + String securityToken = originalConfig.getString(Key.SECURITY_TOKEN); String packageAuthorizedProject = originalConfig.getString(Key.PACKAGE_AUTHORIZED_PROJECT); String defaultProject; - if(StringUtils.isBlank(packageAuthorizedProject)) { + if (StringUtils.isBlank(packageAuthorizedProject)) { defaultProject = project; } else { defaultProject = packageAuthorizedProject; } - String accountType = originalConfig.getString(Key.ACCOUNT_TYPE, - Constant.DEFAULT_ACCOUNT_TYPE); Account account = null; - if (accountType.equalsIgnoreCase(Constant.DEFAULT_ACCOUNT_TYPE)) { - account = new AliyunAccount(accessId, accessKey); + if (StringUtils.isNotBlank(securityToken)) { + account = new StsAccount(accessId, accessKey, securityToken); } else { - throw DataXException.asDataXException(OdpsReaderErrorCode.ACCOUNT_TYPE_ERROR, - String.format("不支持的账号类型:[%s]. 账号类型目前仅支持aliyun, taobao.", accountType)); + account = new AliyunAccount(accessId, accessKey); } Odps odps = new Odps(account); boolean isPreCheck = originalConfig.getBool("dryRun", false); - if(isPreCheck) { + if (isPreCheck) { odps.getRestClient().setConnectTimeout(3); odps.getRestClient().setReadTimeout(3); odps.getRestClient().setRetryTimes(2); } odps.setDefaultProject(defaultProject); odps.setEndpoint(odpsServer); + odps.setUserAgent("DATAX"); return odps; } @@ -103,7 +108,7 @@ public Table call() throws Exception { table.reload(); return table; } - }, 3, 1000, false); + }, DataXCaseEnvUtil.getRetryTimes(3), DataXCaseEnvUtil.getRetryInterval(1000), DataXCaseEnvUtil.getRetryExponential(false)); } catch (Exception e) { throwDataXExceptionWhenReloadTable(e, tableName); } @@ -154,7 +159,7 @@ public static List getTableOriginalColumnNameList( public static String formatPartition(String partition) { if (StringUtils.isBlank(partition)) { throw DataXException.asDataXException(OdpsReaderErrorCode.ILLEGAL_VALUE, - "您所配置的分区不能为空白."); + MESSAGE_SOURCE.message("odpsutil.4")); } else { return partition.trim().replaceAll(" *= *", "=") .replaceAll(" */ *", ",").replaceAll(" *, *", ",") @@ -175,19 +180,47 @@ public static List formatPartitions(List partitions) { } } - public static List> parseColumns( + /** + * 将用户配置的分区分类成两类: + * (1) 包含 HINT 的区间过滤; + * (2) 不包含 HINT 的普通模式 + * @param userConfiguredPartitions + * @return + */ + public static UserConfiguredPartitionClassification classifyUserConfiguredPartitions(List userConfiguredPartitions){ + UserConfiguredPartitionClassification userConfiguredPartitionClassification = new UserConfiguredPartitionClassification(); + + List userConfiguredHintPartition = new ArrayList(); + List userConfiguredNormalPartition = new ArrayList(); + boolean isIncludeHintPartition = false; + for (String userConfiguredPartition : userConfiguredPartitions){ + if (StringUtils.isNotBlank(userConfiguredPartition)){ + if (userConfiguredPartition.trim().toLowerCase().startsWith(Constant.PARTITION_FILTER_HINT)) { + userConfiguredHintPartition.add(userConfiguredPartition.trim()); + isIncludeHintPartition = true; + }else { + userConfiguredNormalPartition.add(userConfiguredPartition.trim()); + } + } + } + userConfiguredPartitionClassification.setIncludeHintPartition(isIncludeHintPartition); + userConfiguredPartitionClassification.setUserConfiguredHintPartition(userConfiguredHintPartition); + userConfiguredPartitionClassification.setUserConfiguredNormalPartition(userConfiguredNormalPartition); + return userConfiguredPartitionClassification; + } + + public static List parseColumns( List allNormalColumns, List allPartitionColumns, List userConfiguredColumns) { - List> parsededColumns = new ArrayList>(); + List parsededColumns = new ArrayList(); // warn: upper & lower case for (String column : userConfiguredColumns) { - MutablePair pair = new MutablePair(); - + InternalColumnInfo pair = new InternalColumnInfo(); // if constant column if (OdpsUtil.checkIfConstantColumn(column)) { // remove first and last ' - pair.setLeft(column.substring(1, column.length() - 1)); - pair.setRight(ColumnType.CONSTANT); + pair.setColumnName(column.substring(1, column.length() - 1)); + pair.setColumnType(ColumnType.CONSTANT); parsededColumns.add(pair); continue; } @@ -196,8 +229,8 @@ public static List> parseColumns( // repeated in partitioning columns int index = OdpsUtil.indexOfIgnoreCase(allNormalColumns, column); if (0 <= index) { - pair.setLeft(allNormalColumns.get(index)); - pair.setRight(ColumnType.NORMAL); + pair.setColumnName(allNormalColumns.get(index)); + pair.setColumnType(ColumnType.NORMAL); parsededColumns.add(pair); continue; } @@ -205,22 +238,22 @@ public static List> parseColumns( // if partition column index = OdpsUtil.indexOfIgnoreCase(allPartitionColumns, column); if (0 <= index) { - pair.setLeft(allPartitionColumns.get(index)); - pair.setRight(ColumnType.PARTITION); + pair.setColumnName(allPartitionColumns.get(index)); + pair.setColumnType(ColumnType.PARTITION); parsededColumns.add(pair); continue; } // not exist column throw DataXException.asDataXException( OdpsReaderErrorCode.ILLEGAL_VALUE, - String.format("源头表的列配置错误. 您所配置的列 [%s] 不存在.", column)); + MESSAGE_SOURCE.message("odpsutil.5", column)); } return parsededColumns; } - + private static int indexOfIgnoreCase(List columnCollection, - String column) { + String column) { int index = -1; for (int i = 0; i < columnCollection.size(); i++) { if (columnCollection.get(i).equalsIgnoreCase(column)) { @@ -255,7 +288,7 @@ public TableTunnel.DownloadSession call() throws Exception { return tunnel.createDownloadSession( projectName, tableName); } - }, MAX_RETRY_TIME, 1000, true); + }, DataXCaseEnvUtil.getRetryTimes(MAX_RETRY_TIME), DataXCaseEnvUtil.getRetryInterval(1000), DataXCaseEnvUtil.getRetryExponential(true)); } catch (Exception e) { throw DataXException.asDataXException(OdpsReaderErrorCode.CREATE_DOWNLOADSESSION_FAIL, e); } @@ -276,7 +309,7 @@ public TableTunnel.DownloadSession call() throws Exception { return tunnel.getDownloadSession( projectName, tableName, sessionId); } - }, MAX_RETRY_TIME ,1000, true); + }, DataXCaseEnvUtil.getRetryTimes(MAX_RETRY_TIME), DataXCaseEnvUtil.getRetryInterval(1000), DataXCaseEnvUtil.getRetryExponential(true)); } catch (Exception e) { throw DataXException.asDataXException(OdpsReaderErrorCode.GET_DOWNLOADSESSION_FAIL, e); } @@ -299,7 +332,7 @@ public TableTunnel.DownloadSession call() throws Exception { return tunnel.createDownloadSession( projectName, tableName, partitionSpec); } - }, MAX_RETRY_TIME, 1000, true); + }, DataXCaseEnvUtil.getRetryTimes(MAX_RETRY_TIME), DataXCaseEnvUtil.getRetryInterval(1000), DataXCaseEnvUtil.getRetryExponential(true)); } catch (Exception e) { throw DataXException.asDataXException(OdpsReaderErrorCode.CREATE_DOWNLOADSESSION_FAIL, e); } @@ -321,58 +354,152 @@ public TableTunnel.DownloadSession call() throws Exception { return tunnel.getDownloadSession( projectName, tableName, partitionSpec, sessionId); } - }, MAX_RETRY_TIME, 1000, true); + }, DataXCaseEnvUtil.getRetryTimes(MAX_RETRY_TIME), DataXCaseEnvUtil.getRetryInterval(1000), DataXCaseEnvUtil.getRetryExponential(true)); } catch (Exception e) { throw DataXException.asDataXException(OdpsReaderErrorCode.GET_DOWNLOADSESSION_FAIL, e); } } + /** + * odpsreader采用的直接读取所有列的downloadSession + */ + public static RecordReader getRecordReader(final TableTunnel.DownloadSession downloadSession, final long start, final long count, + final boolean isCompress) { + try { + return RetryUtil.executeWithRetry(new Callable() { + @Override + public RecordReader call() throws Exception { + return downloadSession.openRecordReader(start, count, isCompress); + } + }, DataXCaseEnvUtil.getRetryTimes(MAX_RETRY_TIME), DataXCaseEnvUtil.getRetryInterval(1000), DataXCaseEnvUtil.getRetryExponential(true)); + } catch (Exception e) { + throw DataXException.asDataXException(OdpsReaderErrorCode.OPEN_RECORD_READER_FAILED, + MESSAGE_SOURCE.message("odpsutil.6"), e); + } + } + /** + * odpsreader采用的指定读取某些列的downloadSession + */ public static RecordReader getRecordReader(final TableTunnel.DownloadSession downloadSession, final long start, final long count, - final boolean isCompress) { + final boolean isCompress, final List columns) { try { return RetryUtil.executeWithRetry(new Callable() { @Override public RecordReader call() throws Exception { - return downloadSession.openRecordReader(start, count, isCompress); + return downloadSession.openRecordReader(start, count, isCompress, columns); } - }, MAX_RETRY_TIME, 1000, true); + }, DataXCaseEnvUtil.getRetryTimes(MAX_RETRY_TIME), DataXCaseEnvUtil.getRetryInterval(1000), DataXCaseEnvUtil.getRetryExponential(true)); } catch (Exception e) { throw DataXException.asDataXException(OdpsReaderErrorCode.OPEN_RECORD_READER_FAILED, - "open RecordReader失败. 请联系 ODPS 管理员处理.", e); + MESSAGE_SOURCE.message("odpsutil.6"), e); } } + /** * table.reload() 方法抛出的 odps 异常 转化为更清晰的 datax 异常 抛出 */ public static void throwDataXExceptionWhenReloadTable(Exception e, String tableName) { - if(e.getMessage() != null) { - if(e.getMessage().contains(OdpsExceptionMsg.ODPS_PROJECT_NOT_FOUNT)) { + if (e.getMessage() != null) { + if (e.getMessage().contains(OdpsExceptionMsg.ODPS_PROJECT_NOT_FOUNT)) { throw DataXException.asDataXException(OdpsReaderErrorCode.ODPS_PROJECT_NOT_FOUNT, - String.format("加载 ODPS 源头表:%s 失败. " + - "请检查您配置的 ODPS 源头表的 [project] 是否正确.", tableName), e); - } else if(e.getMessage().contains(OdpsExceptionMsg.ODPS_TABLE_NOT_FOUNT)) { + MESSAGE_SOURCE.message("odpsutil.7", tableName), e); + } else if (e.getMessage().contains(OdpsExceptionMsg.ODPS_TABLE_NOT_FOUNT)) { throw DataXException.asDataXException(OdpsReaderErrorCode.ODPS_TABLE_NOT_FOUNT, - String.format("加载 ODPS 源头表:%s 失败. " + - "请检查您配置的 ODPS 源头表的 [table] 是否正确.", tableName), e); - } else if(e.getMessage().contains(OdpsExceptionMsg.ODPS_ACCESS_KEY_ID_NOT_FOUND)) { + MESSAGE_SOURCE.message("odpsutil.8", tableName), e); + } else if (e.getMessage().contains(OdpsExceptionMsg.ODPS_ACCESS_KEY_ID_NOT_FOUND)) { throw DataXException.asDataXException(OdpsReaderErrorCode.ODPS_ACCESS_KEY_ID_NOT_FOUND, - String.format("加载 ODPS 源头表:%s 失败. " + - "请检查您配置的 ODPS 源头表的 [accessId] [accessKey]是否正确.", tableName), e); - } else if(e.getMessage().contains(OdpsExceptionMsg.ODPS_ACCESS_KEY_INVALID)) { + MESSAGE_SOURCE.message("odpsutil.9", tableName), e); + } else if (e.getMessage().contains(OdpsExceptionMsg.ODPS_ACCESS_KEY_INVALID)) { throw DataXException.asDataXException(OdpsReaderErrorCode.ODPS_ACCESS_KEY_INVALID, - String.format("加载 ODPS 源头表:%s 失败. " + - "请检查您配置的 ODPS 源头表的 [accessKey] 是否正确.", tableName), e); - } else if(e.getMessage().contains(OdpsExceptionMsg.ODPS_ACCESS_DENY)) { + MESSAGE_SOURCE.message("odpsutil.10", tableName), e); + } else if (e.getMessage().contains(OdpsExceptionMsg.ODPS_ACCESS_DENY)) { throw DataXException.asDataXException(OdpsReaderErrorCode.ODPS_ACCESS_DENY, - String.format("加载 ODPS 源头表:%s 失败. " + - "请检查您配置的 ODPS 源头表的 [accessId] [accessKey] [project]是否匹配.", tableName), e); + MESSAGE_SOURCE.message("odpsutil.11", tableName), e); } } throw DataXException.asDataXException(OdpsReaderErrorCode.ILLEGAL_VALUE, - String.format("加载 ODPS 源头表:%s 失败. " + - "请检查您配置的 ODPS 源头表的 project,table,accessId,accessKey,odpsServer等值.", tableName), e); + MESSAGE_SOURCE.message("odpsutil.12", tableName), e); + } + + public static List getNormalColumns(List parsedColumns, + Map columnTypeMap) { + List userConfigNormalColumns = new ArrayList(); + Set columnNameSet = new HashSet(); + for (InternalColumnInfo columnInfo : parsedColumns) { + if (columnInfo.getColumnType() == ColumnType.NORMAL) { + String columnName = columnInfo.getColumnName(); + if (!columnNameSet.contains(columnName)) { + Column column = new Column(columnName, columnTypeMap.get(columnName)); + userConfigNormalColumns.add(column); + columnNameSet.add(columnName); + } + } + } + return userConfigNormalColumns; + } + + /** + * 执行odps preSql和postSql + * + * @param odps: odps client + * @param sql : 要执行的odps sql语句, 因为会有重试, 所以sql 必须为幂等的 + * @param tag : "preSql" or "postSql" + */ + public static void runSqlTaskWithRetry(final Odps odps, final String sql, final String tag){ + //重试次数 + int retryTimes = 10; + //重试间隔(ms) + long sleepTimeInMilliSecond = 1000L; + try { + RetryUtil.executeWithRetry(new Callable() { + @Override + public Void call() throws Exception { + long beginTime = System.currentTimeMillis(); + + runSqlTask(odps, sql, tag); + + long endIime = System.currentTimeMillis(); + LOG.info(String.format("exectue odps sql: %s finished, cost time : %s ms", + sql, (endIime - beginTime))); + return null; + } + }, DataXCaseEnvUtil.getRetryTimes(retryTimes), DataXCaseEnvUtil.getRetryInterval(sleepTimeInMilliSecond), DataXCaseEnvUtil.getRetryExponential(true)); + } catch (Exception e) { + String errMessage = String.format("Retry %s times to exectue sql :[%s] failed! Exception: %s", + retryTimes, e.getMessage()); + throw DataXException.asDataXException(OdpsReaderErrorCode.RUN_SQL_ODPS_EXCEPTION, errMessage, e); + } + } + + public static void runSqlTask(Odps odps, String sql, String tag) { + if (StringUtils.isBlank(sql)) { + return; + } + + String taskName = String.format("datax_odpsreader_%s_%s", tag, UUID.randomUUID().toString().replace('-', '_')); + + LOG.info("Try to start sqlTask:[{}] to run odps sql:[\n{}\n] .", taskName, sql); + + Instance instance; + Instance.TaskStatus status; + try { + Map hints = new HashMap(); + hints.put("odps.sql.submit.mode", "script"); + instance = SQLTask.run(odps, odps.getDefaultProject(), sql, taskName, hints, null); + instance.waitForSuccess(); + status = instance.getTaskStatus().get(taskName); + if (!Instance.TaskStatus.Status.SUCCESS.equals(status.getStatus())) { + throw DataXException.asDataXException(OdpsReaderErrorCode.RUN_SQL_FAILED, + MESSAGE_SOURCE.message("odpsutil.13", sql)); + } + } catch (DataXException e) { + throw e; + } catch (Exception e) { + throw DataXException.asDataXException(OdpsReaderErrorCode.RUN_SQL_ODPS_EXCEPTION, + MESSAGE_SOURCE.message("odpsutil.14", sql), e); + } } } diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/SqliteUtil.java b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/SqliteUtil.java new file mode 100644 index 0000000000..70c2226728 --- /dev/null +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/SqliteUtil.java @@ -0,0 +1,103 @@ +package com.alibaba.datax.plugin.reader.odpsreader.util; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.ResultSetMetaData; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.List; + +import com.alibaba.datax.plugin.reader.odpsreader.Constant; +import com.aliyun.odps.Partition; +import com.aliyun.odps.Table; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SqliteUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger(SqliteUtil.class); + + private Connection connection = null; + private Statement stmt = null; + + private String partitionName = "partitionName"; + + private String createSQLTemplate = "Create Table DataXODPSReaderPPR (" + partitionName +" String, %s)"; + private String insertSQLTemplate = "Insert Into DataXODPSReaderPPR Values (%s)"; + private String selectSQLTemplate = "Select * From DataXODPSReaderPPR Where %s"; + + public SqliteUtil() throws ClassNotFoundException, SQLException { + + Class.forName("org.sqlite.JDBC"); + this.connection = DriverManager.getConnection("jdbc:sqlite::memory:"); + this.stmt = this.connection.createStatement(); + } + + public void loadAllPartitionsIntoSqlite(Table table, List allOriginPartitions) throws SQLException { + List partitionColumnList = new ArrayList(); + String partition = allOriginPartitions.get(0); + String[] partitionSpecs = partition.split(","); + List partitionKeyList = new ArrayList(); + for (String partitionKeyValue : partitionSpecs) { + String partitionKey = partitionKeyValue.split("=")[0]; + partitionColumnList.add(String.format("%s String", partitionKey)); + partitionKeyList.add(partitionKey); + } + String createSQL = String.format(createSQLTemplate, StringUtils.join(partitionColumnList.toArray(), ",")); + LOGGER.info(createSQL); + this.stmt.execute(createSQL); + + insertAllOriginPartitionIntoSqlite(table, partitionKeyList); + } + + /** + * 根据用户配置的过滤条件, 从sqlite中select出符合的partition列表 + * @param userHintConfiguredPartitions + * @return + */ + public List selectUserConfiguredPartition(List userHintConfiguredPartitions) throws SQLException { + List selectedPartitionsFromSqlite = new ArrayList(); + for (String partitionWhereConditions : userHintConfiguredPartitions) { + String selectUserConfiguredPartitionsSql = String.format(selectSQLTemplate, + StringUtils.remove(partitionWhereConditions, Constant.PARTITION_FILTER_HINT)); + LOGGER.info(selectUserConfiguredPartitionsSql); + ResultSet rs = stmt.executeQuery(selectUserConfiguredPartitionsSql); + while (rs.next()) { + selectedPartitionsFromSqlite.add(getPartitionsValue(rs)); + } + } + return selectedPartitionsFromSqlite; + } + + private String getPartitionsValue (ResultSet rs) throws SQLException { + List partitions = new ArrayList(); + ResultSetMetaData rsMetaData = rs.getMetaData(); + Integer columnCounter = rs.getMetaData().getColumnCount(); + for (int columnIndex = 2; columnIndex <= columnCounter; columnIndex++) { + partitions.add(String.format("%s=%s", rsMetaData.getColumnName(columnIndex), rs.getString(columnIndex))); + } + return StringUtils.join(partitions, ","); + } + + /** + * 将odps table里所有partition值载入sqlite中 + * @param table + * @param partitionKeyList + * @throws SQLException + */ + private void insertAllOriginPartitionIntoSqlite(Table table, List partitionKeyList) throws SQLException { + List partitions = table.getPartitions(); + for (Partition partition : partitions){ + List partitionColumnValue = new ArrayList(); + partitionColumnValue.add("\""+partition.getPartitionSpec().toString()+"\""); + for (String partitionKey : partitionKeyList) { + partitionColumnValue.add("\""+partition.getPartitionSpec().get(partitionKey)+"\""); + } + String insertPartitionValueSql = String.format(insertSQLTemplate, StringUtils.join(partitionColumnValue, ",")); + this.stmt.execute(insertPartitionValueSql); + } + } +} diff --git a/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/UserConfiguredPartitionClassification.java b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/UserConfiguredPartitionClassification.java new file mode 100644 index 0000000000..1a979969fc --- /dev/null +++ b/odpsreader/src/main/java/com/alibaba/datax/plugin/reader/odpsreader/util/UserConfiguredPartitionClassification.java @@ -0,0 +1,39 @@ +package com.alibaba.datax.plugin.reader.odpsreader.util; + +import java.util.List; + +public class UserConfiguredPartitionClassification { + + //包含/*query*/的partition, 例如: /*query*/ dt>=20170101 and dt<= 20170109 + private List userConfiguredHintPartition; + + //不包含/*query*/的partition, 例如: dt=20170101 或者 dt=201701* + private List userConfiguredNormalPartition; + + //是否包含hint的partition + private boolean isIncludeHintPartition; + + public List getUserConfiguredHintPartition() { + return userConfiguredHintPartition; + } + + public void setUserConfiguredHintPartition(List userConfiguredHintPartition) { + this.userConfiguredHintPartition = userConfiguredHintPartition; + } + + public List getUserConfiguredNormalPartition() { + return userConfiguredNormalPartition; + } + + public void setUserConfiguredNormalPartition(List userConfiguredNormalPartition) { + this.userConfiguredNormalPartition = userConfiguredNormalPartition; + } + + public boolean isIncludeHintPartition() { + return isIncludeHintPartition; + } + + public void setIncludeHintPartition(boolean includeHintPartition) { + isIncludeHintPartition = includeHintPartition; + } +} diff --git a/odpsreader/src/main/libs/bcprov-jdk15on-1.52.jar b/odpsreader/src/main/libs/bcprov-jdk15on-1.52.jar deleted file mode 100644 index 6c54dd901c..0000000000 Binary files a/odpsreader/src/main/libs/bcprov-jdk15on-1.52.jar and /dev/null differ diff --git a/odpswriter/doc/odpswriter.md b/odpswriter/doc/odpswriter.md index d81672b02c..845dd1d3d0 100644 --- a/odpswriter/doc/odpswriter.md +++ b/odpswriter/doc/odpswriter.md @@ -71,8 +71,7 @@ ODPSWriter插件用于实现往ODPS插入或者更新数据,主要提供给etl "accessKey": "xxxx", "truncate": true, "odpsServer": "http://sxxx/api", - "tunnelServer": "http://xxx", - "accountType": "aliyun" + "tunnelServer": "http://xxx" } } } diff --git a/odpswriter/pom.xml b/odpswriter/pom.xml index 8073ec43d3..c253e3fc7e 100755 --- a/odpswriter/pom.xml +++ b/odpswriter/pom.xml @@ -31,17 +31,10 @@ logback-classic - org.bouncycastle - bcprov-jdk15on - 1.52 - system - ${basedir}/src/main/libs/bcprov-jdk15on-1.52.jar + com.aliyun.odps + odps-sdk-core + 0.38.4-public - - com.aliyun.odps - odps-sdk-core - 0.20.7-public - @@ -51,6 +44,14 @@ + + + + org.mockito mockito-core @@ -70,9 +71,30 @@ test + + + org.aspectj + aspectjweaver + 1.8.10 + + + + commons-codec + commons-codec + 1.8 + + + + + src/main/java + + **/*.properties + + + diff --git a/odpswriter/src/main/assembly/package.xml b/odpswriter/src/main/assembly/package.xml index 7d3c91b51b..0ef0b43b18 100755 --- a/odpswriter/src/main/assembly/package.xml +++ b/odpswriter/src/main/assembly/package.xml @@ -23,13 +23,6 @@ plugin/writer/odpswriter - - src/main/libs - - *.* - - plugin/writer/odpswriter/libs - diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/Constant.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/Constant.java index 22bcc16cb3..efedfea9d1 100755 --- a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/Constant.java +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/Constant.java @@ -2,14 +2,37 @@ public class Constant { - public static final String SKYNET_ACCESSID = "SKYNET_ACCESSID"; - public static final String SKYNET_ACCESSKEY = "SKYNET_ACCESSKEY"; + public static final String COLUMN_POSITION = "columnPosition"; - public static final String DEFAULT_ACCOUNT_TYPE = "aliyun"; + /* + * 每个task独立维护一个proxy列表,一共会生成 task并发量 * 分区数量 的proxy,每个proxy会创建 blocksizeInMB(一般是64M) 大小的数组 + * 因此极易OOM, + * 假设默认情况下768M的内存,实际最多只能创建 12 个proxy,8G内存最多只能创建126个proxy,所以最多只允许创建一定数量的proxy,对应到分区数量 1:1 + * + * blockSizeInMB 减小可以减少内存消耗,但是意味着更高频率的网络请求,会对odps服务器造成较大压力 + * + * 另外,可以考虑proxy不用常驻内存,但是需要增加复杂的控制逻辑 + * 但是一般情况下用户作为分区值得数据是有规律的,比如按照时间,2020-08的数据已经同步完成了,并且后面没有这个分区的数据了,对应的proxy还放在内存中, + * 会造成很大的内存浪费。所以有必要对某些proxy进行回收。 + * + * 这里采用是否回收某个proxy的标准是:在最近时间内是否有过数据传输。 + * + * + * 需要注意的问题! + * 多个任务公用一个proxy,写入时需要抢锁,多并发的性能会受到很大影响,相当于单个分区时串行写入 + * 这个对性能影响很大,需要避免这种方式,还是尽量各个task有独立的proxy,只是需要去控制内存的使用,只能是控制每个task保有的proxy数量了 + * + * 还可以考虑修改proxy的数组大小,但是设置太小不确定会不会影响性能。可以测试一下 + */ - public static final String TAOBAO_ACCOUNT_TYPE = "taobao"; + public static final Long PROXY_MAX_IDLE_TIME_MS =60 * 1000L; // 60s没有动作就回收 + + public static final Long MAX_PARTITION_CNT = 200L; + + public static final int UTF8_ENCODED_CHAR_MAX_SIZE = 6; + + public static final int DEFAULT_FIELD_MAX_SIZE = 8 * 1024 * 1024; - public static final String COLUMN_POSITION = "columnPosition"; } diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/DateTransForm.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/DateTransForm.java new file mode 100644 index 0000000000..dedc9eccda --- /dev/null +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/DateTransForm.java @@ -0,0 +1,57 @@ +package com.alibaba.datax.plugin.writer.odpswriter; + +public class DateTransForm { + /** + * 列名称 + */ + private String colName; + + /** + * 之前是什么格式 + */ + private String fromFormat; + + /** + * 要转换成什么格式 + */ + private String toFormat; + + public DateTransForm(String colName, String fromFormat, String toFormat) { + this.colName = colName; + this.fromFormat = fromFormat; + this.toFormat = toFormat; + } + + public String getColName() { + return colName; + } + + public void setColName(String colName) { + this.colName = colName; + } + + public String getFromFormat() { + return fromFormat; + } + + public void setFromFormat(String fromFormat) { + this.fromFormat = fromFormat; + } + + public String getToFormat() { + return toFormat; + } + + public void setToFormat(String toFormat) { + this.toFormat = toFormat; + } + + @Override + public String toString() { + return "DateTransForm{" + + "colName='" + colName + '\'' + + ", fromFormat='" + fromFormat + '\'' + + ", toFormat='" + toFormat + '\'' + + '}'; + } +} diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/Key.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/Key.java index f578d72d9a..8dff8a4cd3 100755 --- a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/Key.java +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/Key.java @@ -11,6 +11,8 @@ public final class Key { public final static String ACCESS_KEY = "accessKey"; + public final static String SECURITY_TOKEN = "securityToken"; + public final static String PROJECT = "project"; public final static String TABLE = "table"; @@ -28,7 +30,59 @@ public final class Key { //boolean 类型,default:false public final static String EMPTY_AS_NULL = "emptyAsNull"; - public final static String ACCOUNT_TYPE = "accountType"; - public final static String IS_COMPRESS = "isCompress"; + + // preSql + public final static String PRE_SQL="preSql"; + + // postSql + public final static String POST_SQL="postSql"; + + public final static String CONSISTENCY_COMMIT = "consistencyCommit"; + + public final static String UPLOAD_ID = "uploadId"; + + public final static String TASK_COUNT = "taskCount"; + + /** + * support dynamic partition,支持动态分区,即根据读取到的record的某一列或几列来确定该record应该存入哪个分区 + * 1. 如何确定根据哪些列:根据目的表哪几列是分区列,再根据对应的column来路由 + * 2. 何时创建upload session:由于是动态分区,因此无法在初始化时确定分区,也就无法在初始化时创建 upload session,只有再读取到具体record之后才能创建 + * 3. 缓存 upload sesseion:每当出现新的分区,则创建新的session,同时将该分区对应的session缓存下来,以备下次又有需要存入该分区的记录 + * 4. 参数检查:不必要检查分区是否配置 + */ + public final static String SUPPORT_DYNAMIC_PARTITION = "supportDynamicPartition"; + + /** + * 动态分区下,用户如果将源表的某一个时间列映射到分区列,存在如下需求场景:源表的该时间列精确到秒,当时同步到odps表时,只想保留到天,并存入对应的天分区 + * 格式: + * "partitionColumnMapping":[ + * { + * "name":"pt", // 必填 + * "srcDateFormat":"YYYY-MM-dd hh:mm:ss", // 可选,可能源表中的时间列是 String 类型,此时必须通过 fromDateFormat 来指定源表中该列的日期格式 + * "dateFormat":"YYYY-MM-dd" // 必填 + * }, + * { + * ... + * }, + * + * ... + * ] + */ + public final static String PARTITION_COL_MAPPING = "partitionColumnMapping"; + public final static String PARTITION_COL_MAPPING_NAME = "name"; + public final static String PARTITION_COL_MAPPING_SRC_COL_DATEFORMAT = "srcDateFormat"; + public final static String PARTITION_COL_MAPPING_DATEFORMAT = "dateFormat"; + public final static String WRITE_TIMEOUT_IN_MS = "writeTimeoutInMs"; + + public static final String OVER_LENGTH_RULE = "overLengthRule"; + //截断后保留的最大长度 + public static final String MAX_FIELD_LENGTH = "maxFieldLength"; + //odps本身支持的最大长度 + public static final String MAX_ODPS_FIELD_LENGTH = "maxOdpsFieldLength"; + public static final String ENABLE_OVER_LENGTH_OUTPUT = "enableOverLengthOutput"; + public static final String MAX_OVER_LENGTH_OUTPUT_COUNT = "maxOverLengthOutputCount"; + + //动态分区写入模式下,内存使用率达到80%则flush时间间隔,单位分钟 + public static final String DYNAMIC_PARTITION_MEM_USAGE_FLUSH_INTERVAL_IN_MINUTE = "dynamicPartitionMemUsageFlushIntervalInMinute"; } diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/LocalStrings.properties b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/LocalStrings.properties new file mode 100644 index 0000000000..be7862af38 --- /dev/null +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/LocalStrings.properties @@ -0,0 +1,34 @@ +errorcode.required_value=\u60a8\u7f3a\u5931\u4e86\u5fc5\u987b\u586b\u5199\u7684\u53c2\u6570\u503c. +errorcode.illegal_value=\u60a8\u914d\u7f6e\u7684\u503c\u4e0d\u5408\u6cd5. +errorcode.unsupported_column_type=DataX \u4e0d\u652f\u6301\u5199\u5165 ODPS \u7684\u76ee\u7684\u8868\u7684\u6b64\u79cd\u6570\u636e\u7c7b\u578b. +errorcode.table_truncate_error=\u6e05\u7a7a ODPS \u76ee\u7684\u8868\u65f6\u51fa\u9519. +errorcode.create_master_upload_fail=\u521b\u5efa ODPS \u7684 uploadSession \u5931\u8d25. +errorcode.get_slave_upload_fail=\u83b7\u53d6 ODPS \u7684 uploadSession \u5931\u8d25. +errorcode.get_id_key_fail=\u83b7\u53d6 accessId/accessKey \u5931\u8d25. +errorcode.get_partition_fail=\u83b7\u53d6 ODPS \u76ee\u7684\u8868\u7684\u6240\u6709\u5206\u533a\u5931\u8d25. +errorcode.add_partition_failed=\u6dfb\u52a0\u5206\u533a\u5230 ODPS \u76ee\u7684\u8868\u5931\u8d25. +errorcode.writer_record_fail=\u5199\u5165\u6570\u636e\u5230 ODPS \u76ee\u7684\u8868\u5931\u8d25. +errorcode.commit_block_fail=\u63d0\u4ea4 block \u5230 ODPS \u76ee\u7684\u8868\u5931\u8d25. +errorcode.run_sql_failed=\u6267\u884c ODPS Sql \u5931\u8d25. +errorcode.check_if_partitioned_table_failed=\u68c0\u67e5 ODPS \u76ee\u7684\u8868:%s \u662f\u5426\u4e3a\u5206\u533a\u8868\u5931\u8d25. +errorcode.run_sql_odps_exception=\u6267\u884c ODPS Sql \u65f6\u629b\u51fa\u5f02\u5e38, \u53ef\u91cd\u8bd5 +errorcode.account_type_error=\u8d26\u53f7\u7c7b\u578b\u9519\u8bef. +errorcode.partition_error=\u5206\u533a\u914d\u7f6e\u9519\u8bef. +errorcode.column_not_exist=\u7528\u6237\u914d\u7f6e\u7684\u5217\u4e0d\u5b58\u5728. +errorcode.odps_project_not_fount=\u60a8\u914d\u7f6e\u7684\u503c\u4e0d\u5408\u6cd5, odps project \u4e0d\u5b58\u5728. +errorcode.odps_table_not_fount=\u60a8\u914d\u7f6e\u7684\u503c\u4e0d\u5408\u6cd5, odps table \u4e0d\u5b58\u5728 +errorcode.odps_access_key_id_not_found=\u60a8\u914d\u7f6e\u7684\u503c\u4e0d\u5408\u6cd5, odps accessId,accessKey \u4e0d\u5b58\u5728 +errorcode.odps_access_key_invalid=\u60a8\u914d\u7f6e\u7684\u503c\u4e0d\u5408\u6cd5, odps accessKey \u9519\u8bef +errorcode.odps_access_deny=\u62d2\u7edd\u8bbf\u95ee, \u60a8\u4e0d\u5728 \u60a8\u914d\u7f6e\u7684 project \u4e2d + + +odpswriter.1=\u8d26\u53f7\u7c7b\u578b\u9519\u8bef\uff0c\u56e0\u4e3a\u4f60\u7684\u8d26\u53f7 [{0}] \u4e0d\u662fdatax\u76ee\u524d\u652f\u6301\u7684\u8d26\u53f7\u7c7b\u578b\uff0c\u76ee\u524d\u4ec5\u652f\u6301aliyun, taobao\u8d26\u53f7\uff0c\u8bf7\u4fee\u6539\u60a8\u7684\u8d26\u53f7\u4fe1\u606f. +odpswriter.2=\u8fd9\u662f\u4e00\u6761\u9700\u8981\u6ce8\u610f\u7684\u4fe1\u606f \u7531\u4e8e\u60a8\u7684\u4f5c\u4e1a\u914d\u7f6e\u4e86\u5199\u5165 ODPS \u7684\u76ee\u7684\u8868\u65f6emptyAsNull=true, \u6240\u4ee5 DataX\u5c06\u4f1a\u628a\u957f\u5ea6\u4e3a0\u7684\u7a7a\u5b57\u7b26\u4e32\u4f5c\u4e3a java \u7684 null \u5199\u5165 ODPS. +odpswriter.3=\u60a8\u914d\u7f6e\u7684blockSizeInMB:{0} \u53c2\u6570\u9519\u8bef. \u6b63\u786e\u7684\u914d\u7f6e\u662f[1-512]\u4e4b\u95f4\u7684\u6574\u6570. \u8bf7\u4fee\u6539\u6b64\u53c2\u6570\u7684\u503c\u4e3a\u8be5\u533a\u95f4\u5185\u7684\u6570\u503c +odpswriter.4=\u5199\u5165 ODPS \u76ee\u7684\u8868\u5931\u8d25. \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. + + +odpswriterproxy.1=\u4eb2\uff0c\u914d\u7f6e\u4e2d\u7684\u6e90\u8868\u7684\u5217\u4e2a\u6570\u548c\u76ee\u7684\u7aef\u8868\u4e0d\u4e00\u81f4\uff0c\u6e90\u8868\u4e2d\u60a8\u914d\u7f6e\u7684\u5217\u6570\u662f:{0} \u5927\u4e8e\u76ee\u7684\u7aef\u7684\u5217\u6570\u662f:{1} , \u8fd9\u6837\u4f1a\u5bfc\u81f4\u6e90\u5934\u6570\u636e\u65e0\u6cd5\u6b63\u786e\u5bfc\u5165\u76ee\u7684\u7aef, \u8bf7\u68c0\u67e5\u60a8\u7684\u914d\u7f6e\u5e76\u4fee\u6539. +odpswriterproxy.2=\u6e90\u8868\u7684\u5217\u4e2a\u6570\u5c0f\u4e8e\u76ee\u7684\u8868\u7684\u5217\u4e2a\u6570\uff0c\u6e90\u8868\u5217\u6570\u662f:{0} \u76ee\u7684\u8868\u5217\u6570\u662f:{1} , \u6570\u76ee\u4e0d\u5339\u914d. DataX \u4f1a\u628a\u76ee\u7684\u7aef\u591a\u51fa\u7684\u5217\u7684\u503c\u8bbe\u7f6e\u4e3a\u7a7a\u503c. \u5982\u679c\u8fd9\u4e2a\u9ed8\u8ba4\u914d\u7f6e\u4e0d\u7b26\u5408\u60a8\u7684\u671f\u671b\uff0c\u8bf7\u4fdd\u6301\u6e90\u8868\u548c\u76ee\u7684\u8868\u914d\u7f6e\u7684\u5217\u6570\u76ee\u4fdd\u6301\u4e00\u81f4. +odpswriterproxy.3=Odps decimal \u7c7b\u578b\u7684\u6574\u6570\u4f4d\u4e2a\u6570\u4e0d\u80fd\u8d85\u8fc735 +odpswriterproxy.4=\u5199\u5165 ODPS \u76ee\u7684\u8868\u65f6\u9047\u5230\u4e86\u810f\u6570\u636e: \u7b2c[{0}]\u4e2a\u5b57\u6bb5 {1} \u7684\u6570\u636e\u51fa\u73b0\u9519\u8bef\uff0c\u8bf7\u68c0\u67e5\u8be5\u6570\u636e\u5e76\u4f5c\u51fa\u4fee\u6539 \u6216\u8005\u60a8\u53ef\u4ee5\u589e\u5927\u9600\u503c\uff0c\u5ffd\u7565\u8fd9\u6761\u8bb0\u5f55. \ No newline at end of file diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/OdpsWriter.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/OdpsWriter.java index 60deb5dd30..9b7276fa51 100755 --- a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/OdpsWriter.java +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/OdpsWriter.java @@ -8,29 +8,49 @@ import com.alibaba.datax.common.statistics.PerfRecord; import com.alibaba.datax.common.util.Configuration; import com.alibaba.datax.common.util.ListUtil; -import com.alibaba.datax.plugin.writer.odpswriter.util.IdAndKeyUtil; -import com.alibaba.datax.plugin.writer.odpswriter.util.OdpsUtil; - +import com.alibaba.datax.common.util.MessageSource; +import com.alibaba.datax.plugin.writer.odpswriter.model.PartitionInfo; +import com.alibaba.datax.plugin.writer.odpswriter.model.UserDefinedFunction; +import com.alibaba.datax.plugin.writer.odpswriter.util.*; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; import com.aliyun.odps.Odps; import com.aliyun.odps.Table; import com.aliyun.odps.TableSchema; import com.aliyun.odps.tunnel.TableTunnel; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryUsage; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +import static com.alibaba.datax.plugin.writer.odpswriter.util.CustomPartitionUtils.getListWithJson; /** * 已修改为:每个 task 各自创建自己的 upload,拥有自己的 uploadId,并在 task 中完成对对应 block 的提交。 */ public class OdpsWriter extends Writer { + public static HashSet partitionsDealedTruncate = new HashSet<>(); + static final Object lockForPartitionDealedTruncate = new Object(); + public static AtomicInteger partitionCnt = new AtomicInteger(0); + public static Long maxPartitionCnt; + public static AtomicLong globalTotalTruncatedRecordNumber = new AtomicLong(0); + public static Long maxOutputOverLengthRecord; + public static int maxOdpsFieldLength = Constant.DEFAULT_FIELD_MAX_SIZE; + public static class Job extends Writer.Job { private static final Logger LOG = LoggerFactory .getLogger(Job.class); + private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(OdpsWriter.class); private static final boolean IS_DEBUG = LOG.isDebugEnabled(); @@ -42,11 +62,12 @@ public static class Job extends Writer.Job { private String tableName; private String tunnelServer; private String partition; - private String accountType; private boolean truncate; private String uploadId; private TableTunnel.UploadSession masterUpload; private int blockSizeInMB; + private boolean consistencyCommit; + private boolean supportDynamicPartition; public void preCheck() { this.init(); @@ -54,74 +75,74 @@ public void preCheck() { } public void doPreCheck() { - //检查accessId,accessKey配置 - if (Constant.DEFAULT_ACCOUNT_TYPE - .equalsIgnoreCase(this.accountType)) { - this.originalConfig = IdAndKeyUtil.parseAccessIdAndKey(this.originalConfig); - String accessId = this.originalConfig.getString(Key.ACCESS_ID); - String accessKey = this.originalConfig.getString(Key.ACCESS_KEY); - if (IS_DEBUG) { - LOG.debug("accessId:[{}], accessKey:[{}] .", accessId, - accessKey); - } - LOG.info("accessId:[{}] .", accessId); - } - // init odps config - this.odps = OdpsUtil.initOdpsProject(this.originalConfig); - - //检查表等配置是否正确 - this.table = OdpsUtil.getTable(odps,this.projectName,this.tableName); //检查列信息是否正确 List allColumns = OdpsUtil.getAllColumns(this.table.getSchema()); LOG.info("allColumnList: {} .", StringUtils.join(allColumns, ',')); - dealColumn(this.originalConfig, allColumns); + List allPartColumns = OdpsUtil.getAllPartColumns(this.table.getSchema()); + LOG.info("allPartColumnsList: {} .", StringUtils.join(allPartColumns, ',')); + dealColumn(this.originalConfig, allColumns, allPartColumns); //检查分区信息是否正确 - OdpsUtil.preCheckPartition(this.odps, this.table, this.partition, this.truncate); + if (!supportDynamicPartition) { + OdpsUtil.preCheckPartition(this.odps, this.table, this.partition, this.truncate); + } } @Override public void init() { this.originalConfig = super.getPluginJobConf(); + OdpsUtil.checkNecessaryConfig(this.originalConfig); OdpsUtil.dealMaxRetryTime(this.originalConfig); + + this.projectName = this.originalConfig.getString(Key.PROJECT); this.tableName = this.originalConfig.getString(Key.TABLE); this.tunnelServer = this.originalConfig.getString(Key.TUNNEL_SERVER, null); + // init odps config + this.odps = OdpsUtil.initOdpsProject(this.originalConfig); + + //检查表等配置是否正确 + this.table = OdpsUtil.getTable(odps, this.projectName, this.tableName); + + // 处理动态分区参数,以及动态分区相关配置是否合法,如果没有配置动态分区,则根据列映射配置决定是否启用 + this.dealDynamicPartition(); + //check isCompress this.originalConfig.getBool(Key.IS_COMPRESS, false); - this.partition = OdpsUtil.formatPartition(this.originalConfig - .getString(Key.PARTITION, "")); - this.originalConfig.set(Key.PARTITION, this.partition); - - this.accountType = this.originalConfig.getString(Key.ACCOUNT_TYPE, - Constant.DEFAULT_ACCOUNT_TYPE); - if (!Constant.DEFAULT_ACCOUNT_TYPE.equalsIgnoreCase(this.accountType) && - !Constant.TAOBAO_ACCOUNT_TYPE.equalsIgnoreCase(this.accountType)) { - throw DataXException.asDataXException(OdpsWriterErrorCode.ACCOUNT_TYPE_ERROR, - String.format("账号类型错误,因为你的账号 [%s] 不是datax目前支持的账号类型,目前仅支持aliyun, taobao账号,请修改您的账号信息.", accountType)); + // 如果不是动态分区写入,则检查分区配置,动态分区写入不用检查 + if (!this.supportDynamicPartition) { + this.partition = OdpsUtil.formatPartition(this.originalConfig + .getString(Key.PARTITION, ""), true); + this.originalConfig.set(Key.PARTITION, this.partition); } - this.originalConfig.set(Key.ACCOUNT_TYPE, this.accountType); this.truncate = this.originalConfig.getBool(Key.TRUNCATE); + this.consistencyCommit = this.originalConfig.getBool(Key.CONSISTENCY_COMMIT, false); + boolean emptyAsNull = this.originalConfig.getBool(Key.EMPTY_AS_NULL, false); this.originalConfig.set(Key.EMPTY_AS_NULL, emptyAsNull); if (emptyAsNull) { - LOG.warn("这是一条需要注意的信息 由于您的作业配置了写入 ODPS 的目的表时emptyAsNull=true, 所以 DataX将会把长度为0的空字符串作为 java 的 null 写入 ODPS."); + LOG.warn(MESSAGE_SOURCE.message("odpswriter.2")); } this.blockSizeInMB = this.originalConfig.getInt(Key.BLOCK_SIZE_IN_MB, 64); - if(this.blockSizeInMB < 8) { + if (this.blockSizeInMB < 8) { this.blockSizeInMB = 8; } this.originalConfig.set(Key.BLOCK_SIZE_IN_MB, this.blockSizeInMB); LOG.info("blockSizeInMB={}.", this.blockSizeInMB); + maxPartitionCnt = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax() / 1024 / 1024 / this.blockSizeInMB; + if (maxPartitionCnt < Constant.MAX_PARTITION_CNT) { + maxPartitionCnt = Constant.MAX_PARTITION_CNT; + } + LOG.info("maxPartitionCnt={}", maxPartitionCnt); if (IS_DEBUG) { LOG.debug("After master init(), job config now is: [\n{}\n] .", @@ -129,29 +150,95 @@ public void init() { } } - @Override - public void prepare() { - String accessId = null; - String accessKey = null; - if (Constant.DEFAULT_ACCOUNT_TYPE - .equalsIgnoreCase(this.accountType)) { - this.originalConfig = IdAndKeyUtil.parseAccessIdAndKey(this.originalConfig); - accessId = this.originalConfig.getString(Key.ACCESS_ID); - accessKey = this.originalConfig.getString(Key.ACCESS_KEY); - if (IS_DEBUG) { - LOG.debug("accessId:[{}], accessKey:[{}] .", accessId, - accessKey); + private void dealDynamicPartition() { + /* + * 如果显示配置了 supportDynamicPartition,则以配置为准 + * 如果没有配置,表为分区表且 列映射中包所有含分区列 + */ + List partitionCols = OdpsUtil.getAllPartColumns(this.table.getSchema()); + List configCols = this.originalConfig.getList(Key.COLUMN, String.class); + LOG.info("partition columns:{}", partitionCols); + LOG.info("config columns:{}", configCols); + LOG.info("support dynamic partition:{}",this.originalConfig.getBool(Key.SUPPORT_DYNAMIC_PARTITION)); + LOG.info("partition format type:{}",this.originalConfig.getString("partitionFormatType")); + if (this.originalConfig.getKeys().contains(Key.SUPPORT_DYNAMIC_PARTITION)) { + this.supportDynamicPartition = this.originalConfig.getBool(Key.SUPPORT_DYNAMIC_PARTITION); + if (supportDynamicPartition) { + // 自定义分区 + if("custom".equalsIgnoreCase(originalConfig.getString("partitionFormatType"))){ + List partitions = getListWithJson(originalConfig,"customPartitionColumns",PartitionInfo.class); + // 自定义分区配置必须与实际分区列完全一致 + if (!ListUtil.checkIfAllSameValue(partitions.stream().map(item->item.getName()).collect(Collectors.toList()), partitionCols)) { + throw DataXException.asDataXException("custom partition config is not same as real partition info."); + } + } else { + // 设置动态分区写入为真--检查是否所有分区列都配置在了列映射中,不满足则抛出异常 + if (!ListUtil.checkIfBInA(configCols, partitionCols, false)) { + throw DataXException.asDataXException("You config supportDynamicPartition as true, but didn't config all partition columns"); + } + } + } else { + // 设置动态分区写入为假--确保列映射中没有配置分区列,配置则抛出异常 + if (ListUtil.checkIfHasSameValue(configCols, partitionCols)) { + throw DataXException.asDataXException("You should config all partition columns in column param, or you can specify a static partition param"); + } + } + } else { + if (OdpsUtil.isPartitionedTable(table)) { + // 分区表,列映射配置了分区,同时检查所有分区列要么都被配置,要么都没有配置 + if (ListUtil.checkIfBInA(configCols, partitionCols, false)) { + // 所有的partition 列都配置在了column中 + this.supportDynamicPartition = true; + } else { + // 并非所有partition列都配置在了column中,此时还需检查是否只配置了部分,如果只配置了部分,则报错 + if (ListUtil.checkIfHasSameValue(configCols, partitionCols)) { + throw DataXException.asDataXException("You should config all partition columns in column param, or you can specify a static partition param"); + } + // 分区列没有配置任何分区列,则设置为false + this.supportDynamicPartition = false; + } + } else { + LOG.info("{} is not a partition tale, set supportDynamicParition as false", this.tableName); + this.supportDynamicPartition = false; } - LOG.info("accessId:[{}] .", accessId); } + // 分布式下不支持动态分区写入,如果是分布式模式则报错 + LOG.info("current run mode: {}", System.getProperty("datax.executeMode")); + if (supportDynamicPartition && StringUtils.equalsIgnoreCase("distribute", System.getProperty("datax.executeMode"))) { + LOG.error("Distribute mode don't support dynamic partition writing"); + System.exit(1); + } + } + + @Override + public void prepare() { // init odps config this.odps = OdpsUtil.initOdpsProject(this.originalConfig); + List preSqls = this.originalConfig.getList(Key.PRE_SQL, String.class); + if (preSqls != null && !preSqls.isEmpty()) { + LOG.info(String.format("Beigin to exectue preSql : %s. \n Attention: these preSqls must be idempotent!!!", + JSON.toJSONString(preSqls))); + long beginTime = System.currentTimeMillis(); + for (String preSql : preSqls) { + preSql = preSql.trim(); + if (!preSql.endsWith(";")) { + preSql = String.format("%s;", preSql); + } + OdpsUtil.runSqlTaskWithRetry(this.odps, preSql, "preSql"); + } + long endTime = System.currentTimeMillis(); + LOG.info(String.format("Exectue odpswriter preSql successfully! cost time: %s ms.", (endTime - beginTime))); + } + //检查表等配置是否正确 - this.table = OdpsUtil.getTable(odps,this.projectName,this.tableName); + this.table = OdpsUtil.getTable(odps, this.projectName, this.tableName); - OdpsUtil.dealTruncate(this.odps, this.table, this.partition, this.truncate); + // 如果是动态分区写入,因为无需配置分区信息,因此也无法在任务初始化时进行 truncate + if (!supportDynamicPartition) { + OdpsUtil.dealTruncate(this.odps, this.table, this.partition, this.truncate); + } } /** @@ -169,20 +256,34 @@ public List split(int mandatoryNumber) { tableTunnel.setEndpoint(tunnelServer); } - this.masterUpload = OdpsUtil.createMasterTunnelUpload( - tableTunnel, this.projectName, this.tableName, this.partition); - this.uploadId = this.masterUpload.getId(); - LOG.info("Master uploadId:[{}].", this.uploadId); - - TableSchema schema = this.masterUpload.getSchema(); + TableSchema schema = this.table.getSchema(); List allColumns = OdpsUtil.getAllColumns(schema); LOG.info("allColumnList: {} .", StringUtils.join(allColumns, ',')); - - dealColumn(this.originalConfig, allColumns); + List allPartColumns = OdpsUtil.getAllPartColumns(this.table.getSchema()); + LOG.info("allPartColumnsList: {} .", StringUtils.join(allPartColumns, ',')); + dealColumn(this.originalConfig, allColumns, allPartColumns); + this.originalConfig.set("allColumns", allColumns); + + // 动态分区模式下,无法事先根据分区创建好 session, + if (!supportDynamicPartition) { + this.masterUpload = OdpsUtil.createMasterTunnelUpload( + tableTunnel, this.projectName, this.tableName, this.partition); + this.uploadId = this.masterUpload.getId(); + LOG.info("Master uploadId:[{}].", this.uploadId); + } for (int i = 0; i < mandatoryNumber; i++) { Configuration tempConfig = this.originalConfig.clone(); + // 非动态分区模式下,设置了统一提交,则需要克隆主 upload session,否则各个 task "各自为战" + if (!supportDynamicPartition && this.consistencyCommit) { + tempConfig.set(Key.UPLOAD_ID, uploadId); + tempConfig.set(Key.TASK_COUNT, mandatoryNumber); + } + + // 设置task的supportDynamicPartition属性 + tempConfig.set(Key.SUPPORT_DYNAMIC_PARTITION, this.supportDynamicPartition); + configurations.add(tempConfig); } @@ -190,14 +291,18 @@ public List split(int mandatoryNumber) { LOG.debug("After master split, the job config now is:[\n{}\n].", this.originalConfig); } - this.masterUpload = null; - return configurations; } - private void dealColumn(Configuration originalConfig, List allColumns) { + private void dealColumn(Configuration originalConfig, List allColumns, List allPartColumns) { //之前已经检查了userConfiguredColumns 一定不为空 List userConfiguredColumns = originalConfig.getList(Key.COLUMN, String.class); + + // 动态分区下column不支持配置* + if (supportDynamicPartition && userConfiguredColumns.contains("*")) { + throw DataXException.asDataXException(OdpsWriterErrorCode.ILLEGAL_VALUE, + "In dynamic partition write mode you can't specify column with *."); + } if (1 == userConfiguredColumns.size() && "*".equals(userConfiguredColumns.get(0))) { userConfiguredColumns = allColumns; originalConfig.set(Key.COLUMN, allColumns); @@ -206,15 +311,51 @@ private void dealColumn(Configuration originalConfig, List allColumns) { ListUtil.makeSureNoValueDuplicate(userConfiguredColumns, false); //检查列是否存在,大小写不敏感 - ListUtil.makeSureBInA(allColumns, userConfiguredColumns, false); + if (supportDynamicPartition) { + List allColumnList = new ArrayList(); + allColumnList.addAll(allColumns); + allColumnList.addAll(allPartColumns); + ListUtil.makeSureBInA(allColumnList, userConfiguredColumns, false); + } else { + ListUtil.makeSureBInA(allColumns, userConfiguredColumns, false); + } } - List columnPositions = OdpsUtil.parsePosition(allColumns, userConfiguredColumns); + // 获取配置的所有数据列在目标表中所有数据列中的真正位置, -1 代表该列为分区列 + List columnPositions = OdpsUtil.parsePosition(allColumns, allPartColumns, userConfiguredColumns); originalConfig.set(Constant.COLUMN_POSITION, columnPositions); } @Override public void post() { + + if (supportDynamicPartition) { + LOG.info("Total create partition cnt:{}", partitionCnt); + } + + if (!supportDynamicPartition && this.consistencyCommit) { + LOG.info("Master which uploadId=[{}] begin to commit blocks.", this.uploadId); + OdpsUtil.masterComplete(this.masterUpload); + LOG.info("Master which uploadId=[{}] commit blocks ok.", this.uploadId); + } + + List postSqls = this.originalConfig.getList(Key.POST_SQL, String.class); + if (postSqls != null && !postSqls.isEmpty()) { + LOG.info(String.format("Beigin to exectue postSql : %s. \n Attention: these postSqls must be idempotent!!!", + JSON.toJSONString(postSqls))); + long beginTime = System.currentTimeMillis(); + for (String postSql : postSqls) { + postSql = postSql.trim(); + if (!postSql.endsWith(";")) { + postSql = String.format("%s;", postSql); + } + OdpsUtil.runSqlTaskWithRetry(this.odps, postSql, "postSql"); + } + long endTime = System.currentTimeMillis(); + LOG.info(String.format("Exectue odpswriter postSql successfully! cost time: %s ms.", (endTime - beginTime))); + } + + LOG.info("truncated record count: {}", globalTotalTruncatedRecordNumber.intValue() ); } @Override @@ -226,6 +367,7 @@ public void destroy() { public static class Task extends Writer.Task { private static final Logger LOG = LoggerFactory .getLogger(Task.class); + private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(OdpsWriter.class); private static final boolean IS_DEBUG = LOG.isDebugEnabled(); @@ -246,18 +388,54 @@ public static class Task extends Writer.Task { private List blocks; private int blockSizeInMB; + private boolean consistencyCommit; + + private int taskId; + private int taskCount; + private Integer failoverState = 0; //0 未failover 1准备failover 2已提交,不能failover private byte[] lock = new byte[0]; + private List allColumns; + + /* + * Partition 和 session 的对应关系,处理 record 时,路由到哪个分区,则通过对应的 proxy 上传 + * Key 为 所有分区列的值按配置顺序拼接 + */ + private HashMap>> partitionUploadSessionHashMap; + private Boolean supportDynamicPartition; + private TableTunnel tableTunnel; + private Table table; + + /** + * 保存分区列格式转换规则,只支持源表是 Date 列,或者内容为日期的 String 列 + */ + private HashMap dateTransFormMap; + + private Long writeTimeOutInMs; + + private String overLengthRule; + private int maxFieldLength; + private Boolean enableOverLengthOutput; + + /** + * 动态分区写入模式下,内存使用率达到80%则flush时间间隔,单位分钟 + * 默认5分钟做flush, 避免出现频繁的flush导致小文件问题 + */ + private int dynamicPartitionMemUsageFlushIntervalInMinute = 1; + + private long latestFlushTime = 0; @Override public void init() { this.sliceConfig = super.getPluginJobConf(); + // 默认十分钟超时时间 + this.writeTimeOutInMs = this.sliceConfig.getLong(Key.WRITE_TIMEOUT_IN_MS, 10 * 60 * 1000); this.projectName = this.sliceConfig.getString(Key.PROJECT); this.tableName = this.sliceConfig.getString(Key.TABLE); this.tunnelServer = this.sliceConfig.getString(Key.TUNNEL_SERVER, null); this.partition = OdpsUtil.formatPartition(this.sliceConfig - .getString(Key.PARTITION, "")); + .getString(Key.PARTITION, ""), true); this.sliceConfig.set(Key.PARTITION, this.partition); this.emptyAsNull = this.sliceConfig.getBool(Key.EMPTY_AS_NULL); @@ -265,9 +443,49 @@ public void init() { this.isCompress = this.sliceConfig.getBool(Key.IS_COMPRESS, false); if (this.blockSizeInMB < 1 || this.blockSizeInMB > 512) { throw DataXException.asDataXException(OdpsWriterErrorCode.ILLEGAL_VALUE, - String.format("您配置的blockSizeInMB:%s 参数错误. 正确的配置是[1-512]之间的整数. 请修改此参数的值为该区间内的数值", this.blockSizeInMB)); + MESSAGE_SOURCE.message("odpswriter.3", this.blockSizeInMB)); } + this.taskId = this.getTaskId(); + this.taskCount = this.sliceConfig.getInt(Key.TASK_COUNT, 0); + + this.supportDynamicPartition = this.sliceConfig.getBool(Key.SUPPORT_DYNAMIC_PARTITION, false); + + if (!supportDynamicPartition) { + this.consistencyCommit = this.sliceConfig.getBool(Key.CONSISTENCY_COMMIT, false); + if (consistencyCommit) { + this.uploadId = this.sliceConfig.getString(Key.UPLOAD_ID); + if (this.uploadId == null || this.uploadId.isEmpty()) { + throw DataXException.asDataXException(OdpsWriterErrorCode.ILLEGAL_VALUE, + MESSAGE_SOURCE.message("odpswriter.3", this.uploadId)); + } + } + } else { + this.partitionUploadSessionHashMap = new HashMap<>(); + + // 根据 partColFormats 参数初始化 dateTransFormMap + String dateTransListStr = this.sliceConfig.getString(Key.PARTITION_COL_MAPPING); + if (StringUtils.isNotBlank(dateTransListStr)) { + this.dateTransFormMap = new HashMap<>(); + JSONArray dateTransFormJsonArray = JSONArray.parseArray(dateTransListStr); + for (Object dateTransFormJson : dateTransFormJsonArray) { + DateTransForm dateTransForm = new DateTransForm( + ((JSONObject)dateTransFormJson).getString(Key.PARTITION_COL_MAPPING_NAME), + ((JSONObject)dateTransFormJson).getString(Key.PARTITION_COL_MAPPING_SRC_COL_DATEFORMAT), + ((JSONObject)dateTransFormJson).getString(Key.PARTITION_COL_MAPPING_DATEFORMAT)); + this.dateTransFormMap.put(((JSONObject)dateTransFormJson).getString(Key.PARTITION_COL_MAPPING_NAME), dateTransForm); + } + } + } + this.allColumns = this.sliceConfig.getList("allColumns", String.class); + this.overLengthRule = this.sliceConfig.getString(Key.OVER_LENGTH_RULE, "keepOn").toUpperCase(); + this.maxFieldLength = this.sliceConfig.getInt(Key.MAX_FIELD_LENGTH, Constant.DEFAULT_FIELD_MAX_SIZE); + this.enableOverLengthOutput = this.sliceConfig.getBool(Key.ENABLE_OVER_LENGTH_OUTPUT, true); + maxOutputOverLengthRecord = this.sliceConfig.getLong(Key.MAX_OVER_LENGTH_OUTPUT_COUNT); + maxOdpsFieldLength = this.sliceConfig.getInt(Key.MAX_ODPS_FIELD_LENGTH, Constant.DEFAULT_FIELD_MAX_SIZE); + + this.dynamicPartitionMemUsageFlushIntervalInMinute = this.sliceConfig.getInt(Key.DYNAMIC_PARTITION_MEM_USAGE_FLUSH_INTERVAL_IN_MINUTE, + 1); if (IS_DEBUG) { LOG.debug("After init in task, sliceConfig now is:[\n{}\n].", this.sliceConfig); } @@ -277,24 +495,32 @@ public void init() { @Override public void prepare() { this.odps = OdpsUtil.initOdpsProject(this.sliceConfig); + this.tableTunnel = new TableTunnel(this.odps); - TableTunnel tableTunnel = new TableTunnel(this.odps); - if (StringUtils.isNoneBlank(tunnelServer)) { - tableTunnel.setEndpoint(tunnelServer); + if (! supportDynamicPartition ) { + if (StringUtils.isNoneBlank(tunnelServer)) { + tableTunnel.setEndpoint(tunnelServer); + } + if (this.consistencyCommit) { + this.managerUpload = OdpsUtil.getSlaveTunnelUpload(this.tableTunnel, this.projectName, this.tableName, + this.partition, this.uploadId); + } else { + this.managerUpload = OdpsUtil.createMasterTunnelUpload(this.tableTunnel, this.projectName, + this.tableName, this.partition); + this.uploadId = this.managerUpload.getId(); + } + LOG.info("task uploadId:[{}].", this.uploadId); + this.workerUpload = OdpsUtil.getSlaveTunnelUpload(this.tableTunnel, this.projectName, + this.tableName, this.partition, uploadId); + } else { + this.table = OdpsUtil.getTable(this.odps, this.projectName, this.tableName); } - - this.managerUpload = OdpsUtil.createMasterTunnelUpload(tableTunnel, this.projectName, - this.tableName, this.partition); - this.uploadId = this.managerUpload.getId(); - LOG.info("task uploadId:[{}].", this.uploadId); - - this.workerUpload = OdpsUtil.getSlaveTunnelUpload(tableTunnel, this.projectName, - this.tableName, this.partition, uploadId); } @Override public void startWrite(RecordReceiver recordReceiver) { blocks = new ArrayList(); + List currentWriteBlocks; AtomicLong blockId = new AtomicLong(0); @@ -304,35 +530,212 @@ public void startWrite(RecordReceiver recordReceiver) { try { TaskPluginCollector taskPluginCollector = super.getTaskPluginCollector(); - OdpsWriterProxy proxy = new OdpsWriterProxy(this.workerUpload, this.blockSizeInMB, blockId, - columnPositions, taskPluginCollector, this.emptyAsNull, this.isCompress); + OdpsWriterProxy proxy; + // 可以配置化,保平安 + boolean checkWithGetSize = this.sliceConfig.getBool("checkWithGetSize", true); + if (!supportDynamicPartition) { + if (this.consistencyCommit) { + proxy = new OdpsWriterProxy(this.workerUpload, this.blockSizeInMB, blockId, taskId, taskCount, + columnPositions, taskPluginCollector, this.emptyAsNull, this.isCompress, checkWithGetSize, this.allColumns, this.writeTimeOutInMs, this.sliceConfig, this.overLengthRule, this.maxFieldLength, this.enableOverLengthOutput); + } else { + proxy = new OdpsWriterProxy(this.workerUpload, this.blockSizeInMB, blockId, + columnPositions, taskPluginCollector, this.emptyAsNull, this.isCompress, checkWithGetSize, this.allColumns, false, this.writeTimeOutInMs, this.sliceConfig, this.overLengthRule, this.maxFieldLength, this.enableOverLengthOutput); + } + currentWriteBlocks = blocks; + } else { + proxy = null; + currentWriteBlocks = null; + } com.alibaba.datax.common.element.Record dataXRecord = null; - PerfRecord blockClose = new PerfRecord(super.getTaskGroupId(),super.getTaskId(), PerfRecord.PHASE.ODPS_BLOCK_CLOSE); + PerfRecord blockClose = new PerfRecord(super.getTaskGroupId(), super.getTaskId(), PerfRecord.PHASE.ODPS_BLOCK_CLOSE); blockClose.start(); long blockCloseUsedTime = 0; + boolean columnCntChecked = false; while ((dataXRecord = recordReceiver.getFromReader()) != null) { - blockCloseUsedTime += proxy.writeOneRecord(dataXRecord, blocks); + if (supportDynamicPartition) { + if (!columnCntChecked) { + // 动态分区模式下,读写两端的column数量必须相同 + if (dataXRecord.getColumnNumber() != this.sliceConfig.getList(Key.COLUMN).size()) { + throw DataXException.asDataXException(OdpsWriterErrorCode.ILLEGAL_VALUE, + "In dynamic partition write mode you must make sure reader and writer has same column count."); + } + columnCntChecked = true; + } + + // 如果是动态分区模式,则需要根据record内容来选择proxy + + String partitionFormatType = sliceConfig.getString("partitionFormatType"); + String partition; + if("custom".equalsIgnoreCase(partitionFormatType)){ + List partitions = getListWithJson(sliceConfig,"customPartitionColumns",PartitionInfo.class); + List functions = getListWithJson(sliceConfig,"customPartitionFunctions",UserDefinedFunction.class); + + partition = CustomPartitionUtils.generate(dataXRecord,functions, + partitions,sliceConfig.getList(Key.COLUMN, String.class)); + }else{ + partition = OdpsUtil.getPartColValFromDataXRecord(dataXRecord, columnPositions, + this.sliceConfig.getList(Key.COLUMN, String.class), + this.dateTransFormMap); + partition = OdpsUtil.formatPartition(partition, false); + } + + Pair> proxyBlocksPair = this.partitionUploadSessionHashMap.get(partition); + if (null != proxyBlocksPair) { + proxy = proxyBlocksPair.getLeft(); + currentWriteBlocks = proxyBlocksPair.getRight(); + if (null == proxy || null == currentWriteBlocks) { + throw DataXException.asDataXException("Get OdpsWriterProxy failed."); + } + } else { + /* + * 第一次写入该目标分区:处理truncate + * truncate 为 true,且还没有被truncate过,则truncate,加互斥锁 + */ + Boolean truncate = this.sliceConfig.getBool(Key.TRUNCATE); + if (truncate && !partitionsDealedTruncate.contains(partition)) { + synchronized (lockForPartitionDealedTruncate) { + if (!partitionsDealedTruncate.contains(partition)) { + LOG.info("Start to truncate partition {}", partition); + OdpsUtil.dealTruncate(this.odps, this.table, partition, truncate); + partitionsDealedTruncate.add(partition); + } + /* + * 判断分区是否创建过多,如果创建过多,则报错 + */ + if (partitionCnt.addAndGet(1) > maxPartitionCnt) { + throw new DataXException("Create too many partitions. Please make sure you config the right partition column"); + } + } + } + TableTunnel.UploadSession uploadSession = OdpsUtil.createMasterTunnelUpload(tableTunnel, this.projectName, + this.tableName, partition); + proxy = new OdpsWriterProxy(uploadSession, this.blockSizeInMB, blockId, + columnPositions, taskPluginCollector, this.emptyAsNull, this.isCompress, checkWithGetSize, this.allColumns, true, this.writeTimeOutInMs, this.sliceConfig, this.overLengthRule, this.maxFieldLength, this.enableOverLengthOutput); + currentWriteBlocks = new ArrayList<>(); + partitionUploadSessionHashMap.put(partition, new MutablePair<>(proxy, currentWriteBlocks)); + } + } + blockCloseUsedTime += proxy.writeOneRecord(dataXRecord, currentWriteBlocks); + + // 动态分区写入模式下,如果内存使用达到一定程度 80%,清理较久没有活动且缓存较多数据的分区 + if (supportDynamicPartition) { + boolean isNeedFush = checkIfNeedFlush(); + if (isNeedFush) { + LOG.info("====The memory used exceed 80%, start to clear...==="); + int releaseCnt = 0; + int remainCnt = 0; + for (String onePartition : partitionUploadSessionHashMap.keySet()) { + OdpsWriterProxy oneIdleProxy = partitionUploadSessionHashMap.get(onePartition) == null ? null : partitionUploadSessionHashMap.get(onePartition).getLeft(); + if (oneIdleProxy == null) { + continue; + } + + Long idleTime = System.currentTimeMillis() - oneIdleProxy.getLastActiveTime(); + if (idleTime > Constant.PROXY_MAX_IDLE_TIME_MS || oneIdleProxy.getCurrentTotalBytes() > (this.blockSizeInMB*1014*1024 / 2)) { + // 如果空闲一定时间,先把数据写出 + LOG.info("{} partition has no data last {} seconds, so release its uploadSession", onePartition, Constant.PROXY_MAX_IDLE_TIME_MS / 1000); + currentWriteBlocks = partitionUploadSessionHashMap.get(onePartition).getRight(); + blockCloseUsedTime += oneIdleProxy.writeRemainingRecord(currentWriteBlocks); + // 再清除 + partitionUploadSessionHashMap.put(onePartition, null); + releaseCnt++; + } else { + remainCnt++; + } + } + + // 释放的不足够多,再释放一次,这次随机释放,直到释放数量达到一半 + for (String onePartition : partitionUploadSessionHashMap.keySet()) { + if (releaseCnt >= remainCnt) { + break; + } + + if (partitionUploadSessionHashMap.get(onePartition) != null) { + OdpsWriterProxy oneIdleProxy = partitionUploadSessionHashMap.get(onePartition).getLeft(); + currentWriteBlocks = partitionUploadSessionHashMap.get(onePartition).getRight(); + blockCloseUsedTime += oneIdleProxy.writeRemainingRecord(currentWriteBlocks); + partitionUploadSessionHashMap.put(onePartition, null); + + releaseCnt++; + remainCnt--; + } + + } + + this.latestFlushTime = System.currentTimeMillis(); + LOG.info("===complete==="); + } + + } } - blockCloseUsedTime += proxy.writeRemainingRecord(blocks); - blockClose.end(blockCloseUsedTime); + // 对所有分区进行剩余 records 写入 + if (supportDynamicPartition) { + for (String partition : partitionUploadSessionHashMap.keySet()) { + if (partitionUploadSessionHashMap.get(partition) == null) { + continue; + } + proxy = partitionUploadSessionHashMap.get(partition).getLeft(); + currentWriteBlocks = partitionUploadSessionHashMap.get(partition).getRight(); + blockCloseUsedTime += proxy.writeRemainingRecord(currentWriteBlocks); + blockClose.end(blockCloseUsedTime); + } + } + else { + blockCloseUsedTime += proxy.writeRemainingRecord(blocks); + blockClose.end(blockCloseUsedTime); + } } catch (Exception e) { - throw DataXException.asDataXException(OdpsWriterErrorCode.WRITER_RECORD_FAIL, "写入 ODPS 目的表失败. 请联系 ODPS 管理员处理.", e); + throw DataXException.asDataXException(OdpsWriterErrorCode.WRITER_RECORD_FAIL, MESSAGE_SOURCE.message("odpswriter.4"), e); } } + private boolean checkIfNeedFlush() { + + //检查是否到达flush时间,超过flush间隔时间 + boolean isArriveFlushTime = (System.currentTimeMillis() - this.latestFlushTime) > this.dynamicPartitionMemUsageFlushIntervalInMinute * 60 * 1000; + if (!isArriveFlushTime) { + //如果flush时间没有到,直接return掉 + return false; + } + + MemoryUsage memoryUsage = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); + boolean isMemUsageExceed = (double)memoryUsage.getUsed() / memoryUsage.getMax() > 0.8f; + return isMemUsageExceed; + } + @Override public void post() { - synchronized (lock){ - if(failoverState==0){ + synchronized (lock) { + if (failoverState == 0) { failoverState = 2; - LOG.info("Slave which uploadId=[{}] begin to commit blocks:[\n{}\n].", this.uploadId, - StringUtils.join(blocks, ",")); - OdpsUtil.masterCompleteBlocks(this.managerUpload, blocks.toArray(new Long[0])); - LOG.info("Slave which uploadId=[{}] commit blocks ok.", this.uploadId); - }else{ + if (! supportDynamicPartition) { + if (! this.consistencyCommit) { + LOG.info("Slave which uploadId=[{}] begin to commit blocks:[\n{}\n].", this.uploadId, + StringUtils.join(blocks, ",")); + OdpsUtil.masterCompleteBlocks(this.managerUpload, blocks.toArray(new Long[0])); + LOG.info("Slave which uploadId=[{}] commit blocks ok.", this.uploadId); + } else { + LOG.info("Slave which uploadId=[{}] begin to check blocks:[\n{}\n].", this.uploadId, + StringUtils.join(blocks, ",")); + OdpsUtil.checkBlockComplete(this.managerUpload, blocks.toArray(new Long[0])); + LOG.info("Slave which uploadId=[{}] check blocks ok.", this.uploadId); + } + } else { + for (String partition : partitionUploadSessionHashMap.keySet()) { + OdpsWriterProxy proxy = partitionUploadSessionHashMap.get(partition).getLeft(); + List blocks = partitionUploadSessionHashMap.get(partition).getRight(); + TableTunnel.UploadSession uploadSession = proxy.getSlaveUpload(); + LOG.info("Slave which uploadId=[{}] begin to check blocks:[\n{}\n].", uploadSession.getId(), + StringUtils.join(blocks, ",")); + OdpsUtil.masterCompleteBlocks(uploadSession, blocks.toArray(new Long[0])); + LOG.info("Slave which uploadId=[{}] check blocks ok.", uploadSession.getId()); + } + } + + } else { throw DataXException.asDataXException(CommonErrorCode.SHUT_DOWN_TASK, ""); } } @@ -343,9 +746,9 @@ public void destroy() { } @Override - public boolean supportFailOver(){ - synchronized (lock){ - if(failoverState==0){ + public boolean supportFailOver() { + synchronized (lock) { + if (failoverState == 0) { failoverState = 1; return true; } diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/OdpsWriterErrorCode.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/OdpsWriterErrorCode.java index 02020c046e..35f2ed155b 100755 --- a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/OdpsWriterErrorCode.java +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/OdpsWriterErrorCode.java @@ -1,42 +1,43 @@ package com.alibaba.datax.plugin.writer.odpswriter; import com.alibaba.datax.common.spi.ErrorCode; +import com.alibaba.datax.common.util.MessageSource; public enum OdpsWriterErrorCode implements ErrorCode { - REQUIRED_VALUE("OdpsWriter-00", "您缺失了必须填写的参数值."), - ILLEGAL_VALUE("OdpsWriter-01", "您配置的值不合法."), - UNSUPPORTED_COLUMN_TYPE("OdpsWriter-02", "DataX 不支持写入 ODPS 的目的表的此种数据类型."), + REQUIRED_VALUE("OdpsWriter-00", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.required_value")), + ILLEGAL_VALUE("OdpsWriter-01", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.illegal_value")), + UNSUPPORTED_COLUMN_TYPE("OdpsWriter-02", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.unsupported_column_type")), - TABLE_TRUNCATE_ERROR("OdpsWriter-03", "清空 ODPS 目的表时出错."), - CREATE_MASTER_UPLOAD_FAIL("OdpsWriter-04", "创建 ODPS 的 uploadSession 失败."), - GET_SLAVE_UPLOAD_FAIL("OdpsWriter-05", "获取 ODPS 的 uploadSession 失败."), - GET_ID_KEY_FAIL("OdpsWriter-06", "获取 accessId/accessKey 失败."), - GET_PARTITION_FAIL("OdpsWriter-07", "获取 ODPS 目的表的所有分区失败."), + TABLE_TRUNCATE_ERROR("OdpsWriter-03", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.table_truncate_error")), + CREATE_MASTER_UPLOAD_FAIL("OdpsWriter-04", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.create_master_upload_fail")), + GET_SLAVE_UPLOAD_FAIL("OdpsWriter-05", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.get_slave_upload_fail")), + GET_ID_KEY_FAIL("OdpsWriter-06", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.get_id_key_fail")), + GET_PARTITION_FAIL("OdpsWriter-07", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.get_partition_fail")), - ADD_PARTITION_FAILED("OdpsWriter-08", "添加分区到 ODPS 目的表失败."), - WRITER_RECORD_FAIL("OdpsWriter-09", "写入数据到 ODPS 目的表失败."), + ADD_PARTITION_FAILED("OdpsWriter-08", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.add_partition_failed")), + WRITER_RECORD_FAIL("OdpsWriter-09", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.writer_record_fail")), - COMMIT_BLOCK_FAIL("OdpsWriter-10", "提交 block 到 ODPS 目的表失败."), - RUN_SQL_FAILED("OdpsWriter-11", "执行 ODPS Sql 失败."), - CHECK_IF_PARTITIONED_TABLE_FAILED("OdpsWriter-12", "检查 ODPS 目的表:%s 是否为分区表失败."), + COMMIT_BLOCK_FAIL("OdpsWriter-10", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.commit_block_fail")), + RUN_SQL_FAILED("OdpsWriter-11", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.run_sql_failed")), + CHECK_IF_PARTITIONED_TABLE_FAILED("OdpsWriter-12", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.check_if_partitioned_table_failed")), - RUN_SQL_ODPS_EXCEPTION("OdpsWriter-13", "执行 ODPS Sql 时抛出异常, 可重试"), + RUN_SQL_ODPS_EXCEPTION("OdpsWriter-13", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.run_sql_odps_exception")), - ACCOUNT_TYPE_ERROR("OdpsWriter-30", "账号类型错误."), + ACCOUNT_TYPE_ERROR("OdpsWriter-30", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.account_type_error")), - PARTITION_ERROR("OdpsWriter-31", "分区配置错误."), + PARTITION_ERROR("OdpsWriter-31", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.partition_error")), - COLUMN_NOT_EXIST("OdpsWriter-32", "用户配置的列不存在."), + COLUMN_NOT_EXIST("OdpsWriter-32", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.column_not_exist")), - ODPS_PROJECT_NOT_FOUNT("OdpsWriter-100", "您配置的值不合法, odps project 不存在."), //ODPS-0420111: Project not found + ODPS_PROJECT_NOT_FOUNT("OdpsWriter-100", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.odps_project_not_fount")), //ODPS-0420111: Project not found - ODPS_TABLE_NOT_FOUNT("OdpsWriter-101", "您配置的值不合法, odps table 不存在"), // ODPS-0130131:Table not found + ODPS_TABLE_NOT_FOUNT("OdpsWriter-101", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.odps_table_not_fount")), // ODPS-0130131:Table not found - ODPS_ACCESS_KEY_ID_NOT_FOUND("OdpsWriter-102", "您配置的值不合法, odps accessId,accessKey 不存在"), //ODPS-0410051:Invalid credentials - accessKeyId not found + ODPS_ACCESS_KEY_ID_NOT_FOUND("OdpsWriter-102", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.odps_access_key_id_not_found")), //ODPS-0410051:Invalid credentials - accessKeyId not found - ODPS_ACCESS_KEY_INVALID("OdpsWriter-103", "您配置的值不合法, odps accessKey 错误"), //ODPS-0410042:Invalid signature value - User signature dose not match; + ODPS_ACCESS_KEY_INVALID("OdpsWriter-103", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.odps_access_key_invalid")), //ODPS-0410042:Invalid signature value - User signature dose not match; - ODPS_ACCESS_DENY("OdpsWriter-104", "拒绝访问, 您不在 您配置的 project 中") //ODPS-0420095: Access Denied - Authorization Failed [4002], You doesn't exist in project + ODPS_ACCESS_DENY("OdpsWriter-104", MessageSource.loadResourceBundle(OdpsWriterErrorCode.class).message("errorcode.odps_access_deny")) //ODPS-0420095: Access Denied - Authorization Failed [4002], You doesn't exist in project ; diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/OdpsWriterProxy.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/OdpsWriterProxy.java index 9833616c5d..e7c95be114 100755 --- a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/OdpsWriterProxy.java +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/OdpsWriterProxy.java @@ -3,29 +3,58 @@ import com.alibaba.datax.common.element.StringColumn; import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.MessageSource; import com.alibaba.datax.plugin.writer.odpswriter.util.OdpsUtil; - -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; import com.aliyun.odps.OdpsType; import com.aliyun.odps.TableSchema; - +import com.aliyun.odps.data.ArrayRecord; +import com.aliyun.odps.data.Binary; +import com.aliyun.odps.data.Char; +import com.aliyun.odps.data.IntervalDayTime; +import com.aliyun.odps.data.IntervalYearMonth; import com.aliyun.odps.data.Record; - +import com.aliyun.odps.data.SimpleStruct; +import com.aliyun.odps.data.Struct; +import com.aliyun.odps.data.Varchar; import com.aliyun.odps.tunnel.TableTunnel; - import com.aliyun.odps.tunnel.TunnelException; import com.aliyun.odps.tunnel.io.ProtobufRecordPack; +import com.aliyun.odps.type.ArrayTypeInfo; +import com.aliyun.odps.type.CharTypeInfo; +import com.aliyun.odps.type.MapTypeInfo; +import com.aliyun.odps.type.StructTypeInfo; +import com.aliyun.odps.type.TypeInfo; +import com.aliyun.odps.type.VarcharTypeInfo; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.math.BigDecimal; +import java.sql.Timestamp; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TimeZone; import java.util.concurrent.atomic.AtomicLong; public class OdpsWriterProxy { - private static final Logger LOG = LoggerFactory - .getLogger(OdpsWriterProxy.class); + private static final Logger LOG = LoggerFactory.getLogger(OdpsWriterProxy.class); + private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(OdpsWriterProxy.class); private volatile boolean printColumnLess;// 是否打印对于源头字段数小于 ODPS 目的表的行的日志 @@ -39,18 +68,98 @@ public class OdpsWriterProxy { private AtomicLong blockId; private List columnPositions; - private List tableOriginalColumnTypeList; + private List tableOriginalColumnTypeList; private boolean emptyAsNull; private boolean isCompress; + + private int taskId; + private int taskCOUNT; + private boolean consistencyCommit = false; + private boolean checkWithGetSize = true; + private List allColumns; + private String overLengthRule; + private int maxFieldLength; + private Boolean enableOverLengthOutput; + + /** + * 记录最近一次活动时间,动态分区写入模式下,超过一定时间不活动,则关闭这个proxy + */ + private Long lastActiveTime; + + /** + * 写block超时时间 + */ + private Long writeTimeoutInMs; - public OdpsWriterProxy(TableTunnel.UploadSession slaveUpload, int blockSizeInMB, - AtomicLong blockId, List columnPositions, - TaskPluginCollector taskPluginCollector, boolean emptyAsNull, boolean isCompress) - throws IOException, TunnelException { + private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + // 读取 jvm 默认时区 + private Calendar calendarForDate = null; + private boolean useDateWithCalendar = true; + + private Calendar initCalendar(Configuration config) { + // 理论上不会有其他选择,有配置化可以随时应急 + String calendarType = config.getString("calendarType", "iso8601"); + Boolean lenient = config.getBool("calendarLenient", true); + + // 默认jvm时区 + TimeZone timeZone = TimeZone.getDefault(); + String timeZoneStr = config.getString("calendarTimeZone"); + if (StringUtils.isNotBlank(timeZoneStr)) { + // 如果用户明确指定使用用户指定的 + timeZone = TimeZone.getTimeZone(timeZoneStr); + } + + Calendar calendarForDate = new Calendar.Builder().setCalendarType(calendarType).setLenient(lenient) + .setTimeZone(timeZone).build(); + return calendarForDate; + } + + public OdpsWriterProxy(TableTunnel.UploadSession slaveUpload, int blockSizeInMB, AtomicLong blockId, + List columnPositions, TaskPluginCollector taskPluginCollector, boolean emptyAsNull, + boolean isCompress, boolean checkWithGetSize, List allColumns, boolean initBufSizeZero, + Long writeTimeoutInMs, Configuration taskConfig, String overLengthRule, int maxFieldLength, + Boolean enableOverLengthOutput) throws IOException, TunnelException { this.slaveUpload = slaveUpload; this.schema = this.slaveUpload.getSchema(); - this.tableOriginalColumnTypeList = OdpsUtil - .getTableOriginalColumnTypeList(this.schema); + this.tableOriginalColumnTypeList = OdpsUtil.getTableOriginalColumnTypeList(this.schema); + + this.blockId = blockId; + this.columnPositions = columnPositions; + this.taskPluginCollector = taskPluginCollector; + this.emptyAsNull = emptyAsNull; + this.isCompress = isCompress; + + // 初始化与 buffer 区相关的值 + this.maxBufferSize = (blockSizeInMB - 4) * 1024 * 1024; + if (initBufSizeZero) { + // 动态分区下初始化为0,随着写入的reord变多慢慢增加 + this.protobufCapacity = 0; + } else { + this.protobufCapacity = blockSizeInMB * 1024 * 1024; + } + this.protobufRecordPack = new ProtobufRecordPack(this.schema, null, this.protobufCapacity); + this.printColumnLess = true; + this.checkWithGetSize = checkWithGetSize; + + this.allColumns = allColumns; + this.overLengthRule = overLengthRule; + this.maxFieldLength = maxFieldLength; + this.enableOverLengthOutput = enableOverLengthOutput; + + this.writeTimeoutInMs = writeTimeoutInMs; + + this.calendarForDate = this.initCalendar(taskConfig); + this.useDateWithCalendar = taskConfig.getBool("useDateWithCalendar", true); + } + + public OdpsWriterProxy(TableTunnel.UploadSession slaveUpload, int blockSizeInMB, AtomicLong blockId, int taskId, + int taskCount, List columnPositions, TaskPluginCollector taskPluginCollector, boolean emptyAsNull, + boolean isCompress, boolean checkWithGetSize, List allColumns, Long writeTimeoutInMs, Configuration taskConfig, + String overLengthRule, int maxFieldLength, Boolean enableOverLengthOutput) throws IOException, TunnelException { + this.slaveUpload = slaveUpload; + this.schema = this.slaveUpload.getSchema(); + this.tableOriginalColumnTypeList = OdpsUtil.getTableOriginalColumnTypeList(this.schema); this.blockId = blockId; this.columnPositions = columnPositions; @@ -63,12 +172,38 @@ public OdpsWriterProxy(TableTunnel.UploadSession slaveUpload, int blockSizeInMB, this.protobufCapacity = blockSizeInMB * 1024 * 1024; this.protobufRecordPack = new ProtobufRecordPack(this.schema, null, this.protobufCapacity); printColumnLess = true; + + this.taskId = taskId; + this.taskCOUNT = taskCount; + this.consistencyCommit = true; + this.checkWithGetSize = checkWithGetSize; + this.allColumns = allColumns; + this.overLengthRule = overLengthRule; + this.maxFieldLength = maxFieldLength; + this.enableOverLengthOutput = enableOverLengthOutput; + + this.writeTimeoutInMs = writeTimeoutInMs; + + this.calendarForDate = this.initCalendar(taskConfig); + this.useDateWithCalendar = taskConfig.getBool("useDateWithCalendar", true); + } + + public long getCurrentBlockId() { + if (this.consistencyCommit) { + return this.taskId + this.taskCOUNT * (this.blockId.get()); + } else { + return this.blockId.get(); + } + } + public TableTunnel.UploadSession getSlaveUpload() { + return this.slaveUpload; } + + public long writeOneRecord(com.alibaba.datax.common.element.Record dataXRecord, List blocks) + throws Exception { - public long writeOneRecord( - com.alibaba.datax.common.element.Record dataXRecord, - List blocks) throws Exception { + this.lastActiveTime = System.currentTimeMillis(); Record record = dataxRecordToOdpsRecord(dataXRecord); @@ -77,12 +212,11 @@ public long writeOneRecord( } protobufRecordPack.append(record); - if (protobufRecordPack.getTotalBytes() >= maxBufferSize) { + if (protobufRecordPack.getProtobufStream().size() >= maxBufferSize) { long startTimeInNs = System.nanoTime(); - OdpsUtil.slaveWriteOneBlock(this.slaveUpload, - protobufRecordPack, blockId.get(), this.isCompress); - LOG.info("write block {} ok.", blockId.get()); - blocks.add(blockId.get()); + OdpsUtil.slaveWriteOneBlock(this.slaveUpload, protobufRecordPack, getCurrentBlockId(), this.writeTimeoutInMs); + LOG.info("write block {} ok.", getCurrentBlockId()); + blocks.add(getCurrentBlockId()); protobufRecordPack.reset(); this.blockId.incrementAndGet(); return System.nanoTime() - startTimeInNs; @@ -92,13 +226,20 @@ public long writeOneRecord( public long writeRemainingRecord(List blocks) throws Exception { // complete protobuf stream, then write to http - if (protobufRecordPack.getTotalBytes() != 0) { + // protobufRecordPack.getTotalBytes() 慕明: getTotalBytes并不一定保证能拿到写入的字节数,按你们的逻辑应该是用getTotalBytesWritten + // if (protobufRecordPack.getTotalBytes() != 0) { + boolean hasRemindData = false; + if (this.checkWithGetSize) { + hasRemindData = protobufRecordPack.getSize() != 0; + } else { + hasRemindData = protobufRecordPack.getTotalBytes() != 0; + } + if (hasRemindData) { long startTimeInNs = System.nanoTime(); - OdpsUtil.slaveWriteOneBlock(this.slaveUpload, - protobufRecordPack, blockId.get(), this.isCompress); - LOG.info("write block {} ok.", blockId.get()); + OdpsUtil.slaveWriteOneBlock(this.slaveUpload, protobufRecordPack, getCurrentBlockId(), this.writeTimeoutInMs); + LOG.info("write block {} ok.", getCurrentBlockId()); - blocks.add(blockId.get()); + blocks.add(getCurrentBlockId()); // reset the buffer for next block protobufRecordPack.reset(); return System.nanoTime() - startTimeInNs; @@ -106,85 +247,846 @@ public long writeRemainingRecord(List blocks) throws Exception { return 0; } - public Record dataxRecordToOdpsRecord( - com.alibaba.datax.common.element.Record dataXRecord) throws Exception { + public Record dataxRecordToOdpsRecord(com.alibaba.datax.common.element.Record dataXRecord) throws Exception { int sourceColumnCount = dataXRecord.getColumnNumber(); - Record odpsRecord = slaveUpload.newRecord(); + ArrayRecord odpsRecord = (ArrayRecord) slaveUpload.newRecord(); int userConfiguredColumnNumber = this.columnPositions.size(); -//todo + if (sourceColumnCount > userConfiguredColumnNumber) { - throw DataXException - .asDataXException( - OdpsWriterErrorCode.ILLEGAL_VALUE, - String.format( - "亲,配置中的源表的列个数和目的端表不一致,源表中您配置的列数是:%s 大于目的端的列数是:%s , 这样会导致源头数据无法正确导入目的端, 请检查您的配置并修改.", - sourceColumnCount, - userConfiguredColumnNumber)); + throw DataXException.asDataXException(OdpsWriterErrorCode.ILLEGAL_VALUE, + MESSAGE_SOURCE.message("odpswriterproxy.1", sourceColumnCount, userConfiguredColumnNumber)); } else if (sourceColumnCount < userConfiguredColumnNumber) { if (printColumnLess) { - LOG.warn( - "源表的列个数小于目的表的列个数,源表列数是:{} 目的表列数是:{} , 数目不匹配. DataX 会把目的端多出的列的值设置为空值. 如果这个默认配置不符合您的期望,请保持源表和目的表配置的列数目保持一致.", - sourceColumnCount, userConfiguredColumnNumber); + LOG.warn(MESSAGE_SOURCE.message("odpswriterproxy.2", sourceColumnCount, userConfiguredColumnNumber)); } printColumnLess = false; } - int currentIndex; + int currentIndex = 0; int sourceIndex = 0; try { com.alibaba.datax.common.element.Column columnValue; for (; sourceIndex < sourceColumnCount; sourceIndex++) { + // 跳过分区列 + if (this.columnPositions.get(sourceIndex) == -1) { + continue; + } currentIndex = columnPositions.get(sourceIndex); - OdpsType type = this.tableOriginalColumnTypeList - .get(currentIndex); + TypeInfo typeInfo = this.tableOriginalColumnTypeList.get(currentIndex); + OdpsType type = typeInfo.getOdpsType(); + String typeName = typeInfo.getTypeName(); columnValue = dataXRecord.getColumn(sourceIndex); if (columnValue == null) { continue; } // for compatible dt lib, "" as null - if(this.emptyAsNull && columnValue instanceof StringColumn && "".equals(columnValue.asString())){ + if (this.emptyAsNull && columnValue instanceof StringColumn && "".equals(columnValue.asString())) { continue; } switch (type) { - case STRING: - odpsRecord.setString(currentIndex, columnValue.asString()); - break; - case BIGINT: - odpsRecord.setBigint(currentIndex, columnValue.asLong()); - break; - case BOOLEAN: - odpsRecord.setBoolean(currentIndex, columnValue.asBoolean()); - break; - case DATETIME: - odpsRecord.setDatetime(currentIndex, columnValue.asDate()); - break; - case DOUBLE: - odpsRecord.setDouble(currentIndex, columnValue.asDouble()); - break; - case DECIMAL: - odpsRecord.setDecimal(currentIndex, columnValue.asBigDecimal()); - String columnStr = columnValue.asString(); - if(columnStr != null && columnStr.indexOf(".") >= 36) { - throw new Exception("Odps decimal 类型的整数位个数不能超过35"); + case STRING: + String newValue = (String)OdpsUtil.processOverLengthData(columnValue.asString(), OdpsType.STRING, this.overLengthRule, this.maxFieldLength, this.enableOverLengthOutput); + odpsRecord.setString(currentIndex, newValue); + break; + case BIGINT: + odpsRecord.setBigint(currentIndex, columnValue.asLong()); + break; + case BOOLEAN: + odpsRecord.setBoolean(currentIndex, columnValue.asBoolean()); + break; + case DATETIME: + odpsRecord.setDatetime(currentIndex, columnValue.asDate()); +// Date datetimeData = columnValue.asDate(); +// if (null == datetimeData) { +// odpsRecord.setDatetime(currentIndex, null); +// } else { +// Timestamp dateDataForOdps = new Timestamp(datetimeData.getTime()); +// if (datetimeData instanceof java.sql.Timestamp) { +// dateDataForOdps.setNanos(((java.sql.Timestamp)datetimeData).getNanos()); +// } +// odpsRecord.setDatetime(currentIndex, dateDataForOdps); +// } + break; + case DATE: + Date dateData = columnValue.asDate(); + if (null == dateData) { + odpsRecord.setDatetime(currentIndex, null); + } else { + if (this.useDateWithCalendar) { + odpsRecord.setDate(currentIndex, new java.sql.Date(dateData.getTime()), this.calendarForDate); + } else { + odpsRecord.setDatetime(currentIndex, new java.sql.Date(dateData.getTime())); } - default: - break; + } + break; + case DOUBLE: + odpsRecord.setDouble(currentIndex, columnValue.asDouble()); + break; + case FLOAT: + Double floatValue = columnValue.asDouble(); + if (null == floatValue) { + ((ArrayRecord) odpsRecord).setFloat(currentIndex, null); + } else { + ((ArrayRecord) odpsRecord).setFloat(currentIndex, floatValue.floatValue()); + } + break; + case DECIMAL: + odpsRecord.setDecimal(currentIndex, columnValue.asBigDecimal()); + String columnStr = columnValue.asString(); + if (columnStr != null && columnStr.indexOf(".") >= 36) { + throw new Exception(MESSAGE_SOURCE.message("odpswriterproxy.3")); + } + break; + case TINYINT: + Long tinyintValueStr = columnValue.asLong(); + if (null == tinyintValueStr) { + ((ArrayRecord) odpsRecord).setTinyint(currentIndex, null); + } else { + ((ArrayRecord) odpsRecord).setTinyint(currentIndex, + Byte.valueOf(String.valueOf(tinyintValueStr))); + } + break; + case SMALLINT: + Long smallIntValue = columnValue.asLong(); + if (null == smallIntValue) { + ((ArrayRecord) odpsRecord).setSmallint(currentIndex, null); + } else { + ((ArrayRecord) odpsRecord).setSmallint(currentIndex, smallIntValue.shortValue()); + } + break; + case INT: + Long intValue = columnValue.asLong(); + if (null == intValue) { + ((ArrayRecord) odpsRecord).setInt(currentIndex, null); + } else { + ((ArrayRecord) odpsRecord).setInt(currentIndex, intValue.intValue()); + } + break; + case VARCHAR: + // warn: columnValue.asString() 为 null 时 , odps sdk 有 BUG + // 不能用 Varchar 的默认构造函数,不然有 NPE + String varcharValueStr = columnValue.asString(); + Varchar varcharData = null; + if (varcharValueStr != null){ + varcharData = new Varchar(columnValue.asString()); + } + ((ArrayRecord) odpsRecord).setVarchar(currentIndex, varcharData); + break; + case CHAR: + String charValueStr = columnValue.asString(); + Char charData = null; + if (charValueStr != null ){ + charData = new Char(charValueStr); + } + ((ArrayRecord) odpsRecord).setChar(currentIndex, charData); + break; + case TIMESTAMP: + Date timestampData = columnValue.asDate(); + if (null == timestampData) { + ((ArrayRecord) odpsRecord).setTimestamp(currentIndex, null); + } else { + Timestamp timestampDataForOdps = new Timestamp(timestampData.getTime()); + if (timestampData instanceof java.sql.Timestamp) { + // 纳秒 + timestampDataForOdps.setNanos(((java.sql.Timestamp)timestampData).getNanos()); + } + // warn优化:如果原来类型就是Timestamp,直接使用就少创建了一个对象 + ((ArrayRecord) odpsRecord).setTimestamp(currentIndex, timestampDataForOdps); + } + break; + case BINARY: + Binary newBinaryData = (Binary)OdpsUtil.processOverLengthData(new Binary(columnValue.asBytes()), OdpsType.BINARY, this.overLengthRule, this.maxFieldLength, this.enableOverLengthOutput); + ((ArrayRecord) odpsRecord).setBinary(currentIndex,columnValue.asBytes() == null ? null : newBinaryData); + break; + case ARRAY: + JSONArray arrayJson = JSON.parseArray(columnValue.asString()); + ((ArrayRecord) odpsRecord).setArray(currentIndex, parseArray(arrayJson, (ArrayTypeInfo) typeInfo)); + break; + case MAP: + JSONObject mapJson = JSON.parseObject(columnValue.asString()); + ((ArrayRecord) odpsRecord).setMap(currentIndex, parseMap(mapJson, (MapTypeInfo) typeInfo)); + break; + case STRUCT: + JSONObject structJson = JSON.parseObject(columnValue.asString()); + ((ArrayRecord) odpsRecord).setStruct(currentIndex, + parseStruct(structJson, (StructTypeInfo) typeInfo)); + break; + default: + break; } } return odpsRecord; } catch (Exception e) { - String message = String.format( - "写入 ODPS 目的表时遇到了脏数据: 第[%s]个字段的数据出现错误,请检查该数据并作出修改 或者您可以增大阀值,忽略这条记录.", sourceIndex); - this.taskPluginCollector.collectDirtyRecord(dataXRecord, e, - message); + String dirtyColumnName = ""; + try { + dirtyColumnName = this.allColumns.get(currentIndex); + } catch (Exception ignoreEx) { + // ignore + } + String message = MESSAGE_SOURCE.message("odpswriterproxy.4", sourceIndex, dirtyColumnName); + this.taskPluginCollector.collectDirtyRecord(dataXRecord, e, message); + return null; + } + } + + private List parseArray(JSONArray jsonArray, ArrayTypeInfo arrayTypeInfo) throws ParseException { + if (null == jsonArray) { return null; } + List result = new ArrayList(); + switch (arrayTypeInfo.getElementTypeInfo().getOdpsType()) { + case BIGINT: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(jsonArray.getLong(i)); + } + return result; + /** + * 双精度浮点 + */ + case DOUBLE: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(jsonArray.getDouble(i)); + } + return result; + /** + * 布尔型 + */ + case BOOLEAN: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(jsonArray.getBoolean(i)); + } + return result; + /** + * 日期类型 + */ + case DATETIME: + // TODO 精度 + for (int i = 0; i < jsonArray.size(); i++) { + result.add(dateFormat.parse(jsonArray.getString(i))); + } + return result; + /** + * 字符串类型 + */ + case STRING: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(jsonArray.getString(i)); + } + return result; + /** + * 精确小数类型 + */ + case DECIMAL: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(jsonArray.getBigDecimal(i)); + } + return result; + /** + * 1字节有符号整型 + */ + case TINYINT: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(jsonArray.getByte(i)); + } + return result; + /** + * 2字节有符号整型 + */ + case SMALLINT: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(jsonArray.getShort(i)); + } + return result; + /** + * 4字节有符号整型 + */ + case INT: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(jsonArray.getInteger(i)); + } + return result; + /** + * 单精度浮点 + */ + case FLOAT: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(jsonArray.getFloat(i)); + } + return result; + /** + * 固定长度字符串 + */ + case CHAR: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(new Char(jsonArray.getString(i), + ((CharTypeInfo) arrayTypeInfo.getElementTypeInfo()).getLength())); + } + return result; + /** + * 可变长度字符串 + */ + case VARCHAR: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(new Varchar(jsonArray.getString(i), + ((VarcharTypeInfo) arrayTypeInfo.getElementTypeInfo()).getLength())); + } + return result; + /** + * 时间类型 + */ + case DATE: + // TODO string -> date need timezone + // TODO how to use odps Record + for (int i = 0; i < jsonArray.size(); i++) { + result.add(java.sql.Date.valueOf(jsonArray.getString(i))); + } + return result; + /** + * 时间戳 + */ + case TIMESTAMP: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(Timestamp.valueOf(jsonArray.getString(i))); + } + return result; + /** + * 字节数组 + */ + case BINARY: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(Base64.decodeBase64(jsonArray.getString(i))); + } + return result; + /** + * 日期间隔 + */ + case INTERVAL_DAY_TIME: + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject json = jsonArray.getJSONObject(i); + result.add(new IntervalDayTime(json.getInteger("totalSeconds"), json.getInteger("nanos"))); + } + return result; + /** + * 年份间隔 + */ + case INTERVAL_YEAR_MONTH: + for (int i = 0; i < jsonArray.size(); i++) { + JSONObject json = jsonArray.getJSONObject(i); + result.add(new IntervalYearMonth(json.getInteger("years"), json.getInteger("months"))); + } + return result; + /** + * 结构体 + */ + case STRUCT: + for (int i = 0; i < jsonArray.size(); i++) { + result.add( + parseStruct(jsonArray.getJSONObject(i), (StructTypeInfo) arrayTypeInfo.getElementTypeInfo())); + } + return result; + /** + * MAP类型 + */ + case MAP: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(parseMap(jsonArray.getJSONObject(i), (MapTypeInfo) arrayTypeInfo.getElementTypeInfo())); + } + return result; + /** + * ARRAY类型 + */ + case ARRAY: + for (int i = 0; i < jsonArray.size(); i++) { + result.add(parseArray(jsonArray.getJSONArray(i), (ArrayTypeInfo) arrayTypeInfo.getElementTypeInfo())); + } + return result; + + default: + return result; + } + } + + private Map parseMap(JSONObject json, MapTypeInfo typeInfo) throws ParseException { + if (json == null) { + return null; + } + Map keyMap = new HashMap(); + Set keys = json.keySet(); + switch (typeInfo.getKeyTypeInfo().getOdpsType()) { + case BIGINT: + for (String item : keys) { + keyMap.put(Long.parseLong(item), item); + } + break; + /** + * 双精度浮点 + */ + case DOUBLE: + for (String item : keys) { + keyMap.put(Double.parseDouble(item), item); + } + break; + /** + * 布尔型 + */ + case BOOLEAN: + for (String item : keys) { + keyMap.put(Boolean.parseBoolean(item), item); + } + break; + /** + * 日期类型 + */ + case DATETIME: + // TODO 精度 + for (String item : keys) { + keyMap.put(dateFormat.parse(item), item); + } + break; + /** + * 字符串类型 + */ + case STRING: + for (String item : keys) { + keyMap.put(item, item); + } + break; + /** + * 精确小数类型 + */ + case DECIMAL: + for (String item : keys) { + keyMap.put(new BigDecimal(item), item); + } + break; + /** + * 1字节有符号整型 + */ + case TINYINT: + for (String item : keys) { + keyMap.put(Byte.parseByte(item), item); + } + break; + /** + * 2字节有符号整型 + */ + case SMALLINT: + for (String item : keys) { + keyMap.put(Short.parseShort(item), item); + } + break; + /** + * 4字节有符号整型 + */ + case INT: + for (String item : keys) { + keyMap.put(Integer.parseInt(item), item); + } + break; + /** + * 单精度浮点 + */ + case FLOAT: + for (String item : keys) { + keyMap.put(Float.parseFloat(item), item); + } + break; + /** + * 固定长度字符串 + */ + case CHAR: + for (String item : keys) { + keyMap.put(new Char(item, ((CharTypeInfo) typeInfo.getKeyTypeInfo()).getLength()), item); + } + break; + /** + * 可变长度字符串 + */ + case VARCHAR: + for (String item : keys) { + keyMap.put(new Varchar(item, ((VarcharTypeInfo) typeInfo.getKeyTypeInfo()).getLength()), item); + } + break; + /** + * 时间类型 + */ + case DATE: + // TODO string -> date need timezone + // TODO how to use odps Record + for (String item : keys) { + keyMap.put(java.sql.Date.valueOf(item), item); + } + break; + /** + * 时间戳 + */ + case TIMESTAMP: + for (String item : keys) { + keyMap.put(Timestamp.valueOf(item), item); + } + break; + /** + * 字节数组 + */ + case BINARY: + for (String item : keys) { + keyMap.put(new Binary(Base64.decodeBase64(item)), item); + } + break; + /** + * 日期间隔 + */ + case INTERVAL_DAY_TIME: + for (String item : keys) { + JSONObject jsonObject = JSON.parseObject(item); + keyMap.put(new IntervalDayTime(jsonObject.getInteger("totalSeconds"), jsonObject.getInteger("nanos")), + item); + } + break; + /** + * 年份间隔 + */ + case INTERVAL_YEAR_MONTH: + for (String item : keys) { + JSONObject jsonObject = JSON.parseObject(item); + keyMap.put(new IntervalYearMonth(jsonObject.getInteger("years"), jsonObject.getInteger("months")), + item); + } + break; + default: + break; + // TODO throw an exception + } + Map result = new HashMap(); + // process map value + switch (typeInfo.getValueTypeInfo().getOdpsType()) { + case BIGINT: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), json.getLong(item.getValue())); + } + return result; + /** + * 双精度浮点 + */ + case DOUBLE: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), json.getDouble(item.getValue())); + } + return result; + /** + * 布尔型 + */ + case BOOLEAN: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), json.getBoolean(item.getValue())); + } + return result; + /** + * 日期类型 + */ + case DATETIME: + // TODO 精度 + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), dateFormat.parse(json.getString(item.getValue()))); + } + return result; + /** + * 字符串类型 + */ + case STRING: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), json.getString(item.getValue())); + } + return result; + /** + * 精确小数类型 + */ + case DECIMAL: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), json.getBigDecimal(item.getValue())); + } + return result; + /** + * 1字节有符号整型 + */ + case TINYINT: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), json.getByte(item.getValue())); + } + return result; + /** + * 2字节有符号整型 + */ + case SMALLINT: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), json.getShort(item.getValue())); + } + return result; + /** + * 4字节有符号整型 + */ + case INT: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), json.getInteger(item.getValue())); + } + return result; + /** + * 单精度浮点 + */ + case FLOAT: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), json.getFloat(item.getValue())); + } + return result; + /** + * 固定长度字符串 + */ + case CHAR: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), new Char(json.getString(item.getValue()), + ((CharTypeInfo) typeInfo.getValueTypeInfo()).getLength())); + } + return result; + /** + * 可变长度字符串 + */ + case VARCHAR: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), new Varchar(json.getString(item.getValue()), + ((VarcharTypeInfo) typeInfo.getValueTypeInfo()).getLength())); + } + return result; + /** + * 时间类型 + */ + case DATE: + // TODO string -> date need timezone + // TODO how to use odps Record + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), java.sql.Date.valueOf(json.getString(item.getValue()))); + } + return result; + /** + * 时间戳 + */ + case TIMESTAMP: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), Timestamp.valueOf(json.getString(item.getValue()))); + } + return result; + /** + * 字节数组 + */ + case BINARY: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), new Binary(Base64.decodeBase64(json.getString(item.getValue())))); + } + return result; + /** + * 日期间隔 + */ + case INTERVAL_DAY_TIME: + for (Map.Entry item : keyMap.entrySet()) { + JSONObject jsonObject = json.getJSONObject(item.getValue()); + result.put(item.getKey(), + new IntervalDayTime(jsonObject.getInteger("totalSeconds"), jsonObject.getInteger("nanos"))); + } + return result; + /** + * 年份间隔 + */ + case INTERVAL_YEAR_MONTH: + for (Map.Entry item : keyMap.entrySet()) { + JSONObject jsonObject = json.getJSONObject(item.getValue()); + result.put(item.getKey(), + new IntervalYearMonth(jsonObject.getInteger("years"), jsonObject.getInteger("months"))); + } + return result; + /** + * 结构体 + */ + case STRUCT: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), + parseStruct(json.getJSONObject(item.getValue()), (StructTypeInfo) typeInfo.getValueTypeInfo())); + } + return result; + /** + * MAP类型 + */ + case MAP: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), + parseMap(json.getJSONObject(item.getValue()), (MapTypeInfo) typeInfo.getValueTypeInfo())); + } + return result; + /** + * ARRAY类型 + */ + case ARRAY: + for (Map.Entry item : keyMap.entrySet()) { + result.put(item.getKey(), + parseArray(json.getJSONArray(item.getValue()), (ArrayTypeInfo) typeInfo.getValueTypeInfo())); + } + return result; + + default: + throw new IllegalArgumentException("decode record failed. column type: " + typeInfo.getTypeName()); + } + } + + public Struct parseStruct(JSONObject json, StructTypeInfo struct) throws ParseException { + if (null == json) { + return null; + } + List fieldNames = struct.getFieldNames(); + List typeInfos = struct.getFieldTypeInfos(); + List structValues = new ArrayList(); + for (int i = 0; i < fieldNames.size(); i++) { + String fieldName = fieldNames.get(i); + switch (typeInfos.get(i).getOdpsType()) { + case BIGINT: + structValues.add(json.getLong(fieldName)); + break; + /** + * 双精度浮点 + */ + case DOUBLE: + structValues.add(json.getDouble(fieldName)); + break; + /** + * 布尔型 + */ + case BOOLEAN: + structValues.add(json.getBoolean(fieldName)); + break; + /** + * 日期类型 + */ + case DATETIME: + // TODO 精度 + structValues.add(dateFormat.parse(json.getString(fieldName))); + break; + /** + * 字符串类型 + */ + case STRING: + structValues.add(json.getString(fieldName)); + break; + /** + * 精确小数类型 + */ + case DECIMAL: + structValues.add(json.getBigDecimal(fieldName)); + break; + /** + * 1字节有符号整型 + */ + case TINYINT: + structValues.add(json.getByte(fieldName)); + break; + /** + * 2字节有符号整型 + */ + case SMALLINT: + structValues.add(json.getShort(fieldName)); + break; + /** + * 4字节有符号整型 + */ + case INT: + structValues.add(json.getInteger(fieldName)); + break; + /** + * 单精度浮点 + */ + case FLOAT: + structValues.add(json.getFloat(fieldName)); + break; + /** + * 固定长度字符串 + */ + case CHAR: + structValues.add(new Char(json.getString(fieldName), ((CharTypeInfo) typeInfos.get(i)).getLength())); + break; + /** + * 可变长度字符串 + */ + case VARCHAR: + structValues + .add(new Varchar(json.getString(fieldName), ((VarcharTypeInfo) typeInfos.get(i)).getLength())); + break; + /** + * 时间类型 + */ + case DATE: + // TODO string -> date need timezone + // TODO how to use odps Record + structValues.add(java.sql.Date.valueOf(json.getString(fieldName))); + break; + /** + * 时间戳 + */ + case TIMESTAMP: + structValues.add(Timestamp.valueOf(json.getString(fieldName))); + break; + /** + * 字节数组 + */ + case BINARY: + structValues.add(Base64.decodeBase64(json.getString(fieldName))); + break; + /** + * 日期间隔 + */ + case INTERVAL_DAY_TIME: + // TODO special process as map object + structValues.add(new IntervalDayTime(json.getInteger("totalSeconds"), json.getInteger("nanos"))); + /** + * 年份间隔 + */ + case INTERVAL_YEAR_MONTH: + structValues.add(new IntervalYearMonth(json.getInteger("years"), json.getInteger("months"))); + /** + * 结构体 + */ + case STRUCT: + structValues.add(parseStruct(json.getJSONObject(fieldName), (StructTypeInfo) typeInfos.get(i))); + break; + /** + * MAP类型 + */ + case MAP: + structValues.add(parseMap(json.getJSONObject(fieldName), (MapTypeInfo) typeInfos.get(i))); + break; + /** + * ARRAY类型 + */ + case ARRAY: + structValues.add(parseArray(json.getJSONArray(fieldName), (ArrayTypeInfo) typeInfos.get(i))); + break; + } + } + + SimpleStruct simpleStruct = new SimpleStruct(struct, structValues); + return simpleStruct; + } + + public Long getLastActiveTime() { + return lastActiveTime; + } + + public void setLastActiveTime(Long lastActiveTime) { + this.lastActiveTime = lastActiveTime; + } + public Long getCurrentTotalBytes() throws IOException { + return this.protobufRecordPack.getTotalBytes(); } } diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/model/PartitionInfo.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/model/PartitionInfo.java new file mode 100644 index 0000000000..f293d8ccbe --- /dev/null +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/model/PartitionInfo.java @@ -0,0 +1,87 @@ +package com.alibaba.datax.plugin.writer.odpswriter.model; + +public class PartitionInfo { + /** + * 字段名 + */ + private String name; + /** + * String + */ + private String type; + /** + * eventTime or function + * yyyy/MM/dd/HH/mm + * 可自定义组合 + */ + private String valueMode; + private String value; + private String comment; + /** + * 自定义分区有效 + * eventTime / constant + * function + */ + private String category; + /** + * 当 partitionType 为function时 + * functionExpression 为 valueMode 对应的expression + */ + private String functionExpression; + + public String getFunctionExpression() { + return functionExpression; + } + + public void setFunctionExpression(String functionExpression) { + this.functionExpression = functionExpression; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValueMode() { + return valueMode; + } + + public void setValueMode(String valueMode) { + this.valueMode = valueMode; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/model/UserDefinedFunction.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/model/UserDefinedFunction.java new file mode 100644 index 0000000000..55c8a1145c --- /dev/null +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/model/UserDefinedFunction.java @@ -0,0 +1,44 @@ +package com.alibaba.datax.plugin.writer.odpswriter.model; + +import java.io.Serializable; +import java.util.List; + +public class UserDefinedFunction implements Serializable { + private static final long serialVersionUID = 1L; + private String name; + private String expression; + private String inputColumn; + private List variableRule; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getExpression() { + return expression; + } + + public void setExpression(String expression) { + this.expression = expression; + } + + public String getInputColumn() { + return inputColumn; + } + + public void setInputColumn(String inputColumn) { + this.inputColumn = inputColumn; + } + + public List getVariableRule() { + return variableRule; + } + + public void setVariableRule(List variableRule) { + this.variableRule = variableRule; + } +} diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/model/UserDefinedFunctionRule.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/model/UserDefinedFunctionRule.java new file mode 100644 index 0000000000..5676eb4574 --- /dev/null +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/model/UserDefinedFunctionRule.java @@ -0,0 +1,26 @@ +package com.alibaba.datax.plugin.writer.odpswriter.model; + +import java.io.Serializable; +import java.util.List; + +public class UserDefinedFunctionRule implements Serializable { + private static final long serialVersionUID = 1L; + private String type; + private List params; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getParams() { + return params; + } + + public void setParams(List params) { + this.params = params; + } +} diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/CustomPartitionUtils.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/CustomPartitionUtils.java new file mode 100644 index 0000000000..6153a8204b --- /dev/null +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/CustomPartitionUtils.java @@ -0,0 +1,54 @@ +package com.alibaba.datax.plugin.writer.odpswriter.util; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.odpswriter.model.PartitionInfo; +import com.alibaba.datax.plugin.writer.odpswriter.model.UserDefinedFunction; +import com.alibaba.fastjson2.JSON; +import com.google.common.base.Joiner; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.util.*; +import java.util.stream.Collectors; + +public class CustomPartitionUtils implements Serializable { + private static final long serialVersionUID = 1L; + protected static Logger logger = LoggerFactory.getLogger(CustomPartitionUtils.class); + + public static List getListWithJson(Configuration config, String path, Class clazz) { + Object object = config.get(path, List.class); + if (null == object) { + return null; + } + + return JSON.parseArray(JSON.toJSONString(object), clazz); + } + + public static String generate(Record record, List functions, List partitions, + List allColumns) { + for (PartitionInfo partitionInfo : partitions) { + partitionInfo.setValue(buildPartitionValue(partitionInfo, functions, record, allColumns)); + } + List partitionList = partitions.stream() + .map(item -> String.format("%s='%s'", item.getName(), item.getValue())) + .collect(Collectors.toList()); + return Joiner.on(",").join(partitionList); + } + + private static String buildPartitionValue(PartitionInfo partitionInfo, List functions, Record record, + List allColumns) { +// logger.info("try build partition value:partitionInfo:\n{},functions:\n{}", +// JSON.toJSONString(partitionInfo), JSON.toJSONString(functions)); + if (StringUtils.isBlank(partitionInfo.getCategory()) + || "eventTime".equalsIgnoreCase(partitionInfo.getCategory()) + || "constant".equalsIgnoreCase(partitionInfo.getCategory())) { + // 直接输出原样字符串 + return partitionInfo.getValueMode(); +// throw new RuntimeException("not support partition category:" + partitionInfo.getCategory()); + } + throw new RuntimeException("un support partition info type:" + partitionInfo.getCategory()); + } +} diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/DESCipher.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/DESCipher.java deleted file mode 100755 index bf7f5a8832..0000000000 --- a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/DESCipher.java +++ /dev/null @@ -1,355 +0,0 @@ -/** - * (C) 2010-2014 Alibaba Group Holding Limited. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.datax.plugin.writer.odpswriter.util; - -import javax.crypto.Cipher; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.DESKeySpec; -import java.security.SecureRandom; - -/** - *   * DES加解密,支持与delphi交互(字符串编码需统一为UTF-8) - * - *   * - * - *   * @author wym - * - *    - */ - -public class DESCipher { - - /** - *   * 密钥 - * - *    - */ - - public static final String KEY = "u4Gqu4Z8"; - - private final static String DES = "DES"; - - /** - *   * 加密 - * - *   * - * - *   * @param src - * - *   * 明文(字节) - * - *   * @param key - * - *   * 密钥,长度必须是8的倍数 - * - *   * @return 密文(字节) - * - *   * @throws Exception - * - *    - */ - - public static byte[] encrypt(byte[] src, byte[] key) throws Exception { - - // DES算法要求有一个可信任的随机数源 - - SecureRandom sr = new SecureRandom(); - - // 从原始密匙数据创建DESKeySpec对象 - - DESKeySpec dks = new DESKeySpec(key); - - // 创建一个密匙工厂,然后用它把DESKeySpec转换成 - - // 一个SecretKey对象 - - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES); - - SecretKey securekey = keyFactory.generateSecret(dks); - - // Cipher对象实际完成加密操作 - - Cipher cipher = Cipher.getInstance(DES); - - // 用密匙初始化Cipher对象 - - cipher.init(Cipher.ENCRYPT_MODE, securekey, sr); - - // 现在,获取数据并加密 - - // 正式执行加密操作 - - return cipher.doFinal(src); - - } - - /** - *   * 解密 - * - *   * - * - *   * @param src - * - *   * 密文(字节) - * - *   * @param key - * - *   * 密钥,长度必须是8的倍数 - * - *   * @return 明文(字节) - * - *   * @throws Exception - * - *    - */ - - public static byte[] decrypt(byte[] src, byte[] key) throws Exception { - - // DES算法要求有一个可信任的随机数源 - - SecureRandom sr = new SecureRandom(); - - // 从原始密匙数据创建一个DESKeySpec对象 - - DESKeySpec dks = new DESKeySpec(key); - - // 创建一个密匙工厂,然后用它把DESKeySpec对象转换成 - - // 一个SecretKey对象 - - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(DES); - - SecretKey securekey = keyFactory.generateSecret(dks); - - // Cipher对象实际完成解密操作 - - Cipher cipher = Cipher.getInstance(DES); - - // 用密匙初始化Cipher对象 - - cipher.init(Cipher.DECRYPT_MODE, securekey, sr); - - // 现在,获取数据并解密 - - // 正式执行解密操作 - - return cipher.doFinal(src); - - } - - /** - *   * 加密 - * - *   * - * - *   * @param src - * - *   * 明文(字节) - * - *   * @return 密文(字节) - * - *   * @throws Exception - * - *    - */ - - public static byte[] encrypt(byte[] src) throws Exception { - - return encrypt(src, KEY.getBytes()); - - } - - /** - *   * 解密 - * - *   * - * - *   * @param src - * - *   * 密文(字节) - * - *   * @return 明文(字节) - * - *   * @throws Exception - * - *    - */ - - public static byte[] decrypt(byte[] src) throws Exception { - - return decrypt(src, KEY.getBytes()); - - } - - /** - *   * 加密 - * - *   * - * - *   * @param src - * - *   * 明文(字符串) - * - *   * @return 密文(16进制字符串) - * - *   * @throws Exception - * - *    - */ - - public final static String encrypt(String src) { - - try { - - return byte2hex(encrypt(src.getBytes(), KEY.getBytes())); - - } catch (Exception e) { - - e.printStackTrace(); - - } - - return null; - - } - - /** - *   * 解密 - * - *   * - * - *   * @param src - * - *   * 密文(字符串) - * - *   * @return 明文(字符串) - * - *   * @throws Exception - * - *    - */ - - public final static String decrypt(String src) { - try { - - return new String(decrypt(hex2byte(src.getBytes()), KEY.getBytes())); - - } catch (Exception e) { - - e.printStackTrace(); - - } - - return null; - - } - - /** - *   * 加密 - * - *   * - * - *   * @param src - * - *   * 明文(字节) - * - *   * @return 密文(16进制字符串) - * - *   * @throws Exception - * - *    - */ - - public static String encryptToString(byte[] src) throws Exception { - - return encrypt(new String(src)); - - } - - /** - *   * 解密 - * - *   * - * - *   * @param src - * - *   * 密文(字节) - * - *   * @return 明文(字符串) - * - *   * @throws Exception - * - *    - */ - - public static String decryptToString(byte[] src) throws Exception { - - return decrypt(new String(src)); - - } - - public static String byte2hex(byte[] b) { - - String hs = ""; - - String stmp = ""; - - for (int n = 0; n < b.length; n++) { - - stmp = (Integer.toHexString(b[n] & 0XFF)); - - if (stmp.length() == 1) - - hs = hs + "0" + stmp; - - else - - hs = hs + stmp; - - } - - return hs.toUpperCase(); - - } - - public static byte[] hex2byte(byte[] b) { - - if ((b.length % 2) != 0) - - throw new IllegalArgumentException("长度不是偶数"); - - byte[] b2 = new byte[b.length / 2]; - - for (int n = 0; n < b.length; n += 2) { - - String item = new String(b, n, 2); - - b2[n / 2] = (byte) Integer.parseInt(item, 16); - - } - return b2; - - } - - /* - * public static void main(String[] args) { try { String src = "cheetah"; - * String crypto = DESCipher.encrypt(src); System.out.println("密文[" + src + - * "]:" + crypto); System.out.println("解密后:" + DESCipher.decrypt(crypto)); } - * catch (Exception e) { e.printStackTrace(); } } - */ -} diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/IdAndKeyUtil.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/IdAndKeyUtil.java deleted file mode 100755 index 95e4b56b54..0000000000 --- a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/IdAndKeyUtil.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * (C) 2010-2014 Alibaba Group Holding Limited. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.alibaba.datax.plugin.writer.odpswriter.util; - -import com.alibaba.datax.common.exception.DataXException; -import com.alibaba.datax.common.util.Configuration; -import com.alibaba.datax.plugin.writer.odpswriter.Constant; -import com.alibaba.datax.plugin.writer.odpswriter.Key; -import com.alibaba.datax.plugin.writer.odpswriter.OdpsWriterErrorCode; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; - -public class IdAndKeyUtil { - private static Logger LOG = LoggerFactory.getLogger(IdAndKeyUtil.class); - - public static Configuration parseAccessIdAndKey(Configuration originalConfig) { - String accessId = originalConfig.getString(Key.ACCESS_ID); - String accessKey = originalConfig.getString(Key.ACCESS_KEY); - - // 只要 accessId,accessKey 二者配置了一个,就理解为是用户本意是要直接手动配置其 accessid/accessKey - if (StringUtils.isNotBlank(accessId) || StringUtils.isNotBlank(accessKey)) { - LOG.info("Try to get accessId/accessKey from your config."); - //通过如下语句,进行检查是否确实配置了 - accessId = originalConfig.getNecessaryValue(Key.ACCESS_ID, OdpsWriterErrorCode.REQUIRED_VALUE); - accessKey = originalConfig.getNecessaryValue(Key.ACCESS_KEY, OdpsWriterErrorCode.REQUIRED_VALUE); - //检查完毕,返回即可 - return originalConfig; - } else { - Map envProp = System.getenv(); - return getAccessIdAndKeyFromEnv(originalConfig, envProp); - } - } - - private static Configuration getAccessIdAndKeyFromEnv(Configuration originalConfig, - Map envProp) { - String accessId = null; - String accessKey = null; - - String skynetAccessID = envProp.get(Constant.SKYNET_ACCESSID); - String skynetAccessKey = envProp.get(Constant.SKYNET_ACCESSKEY); - - if (StringUtils.isNotBlank(skynetAccessID) - || StringUtils.isNotBlank(skynetAccessKey)) { - /** - * 环境变量中,如果存在SKYNET_ACCESSID/SKYNET_ACCESSKEy(只要有其中一个变量,则认为一定是两个都存在的!), - * 则使用其值作为odps的accessId/accessKey(会解密) - */ - - LOG.info("Try to get accessId/accessKey from environment."); - accessId = skynetAccessID; - accessKey = DESCipher.decrypt(skynetAccessKey); - if (StringUtils.isNotBlank(accessKey)) { - originalConfig.set(Key.ACCESS_ID, accessId); - originalConfig.set(Key.ACCESS_KEY, accessKey); - LOG.info("Get accessId/accessKey from environment variables successfully."); - } else { - throw DataXException.asDataXException(OdpsWriterErrorCode.GET_ID_KEY_FAIL, - String.format("从环境变量中获取accessId/accessKey 失败, accessId=[%s]", accessId)); - } - } else { - // 无处获取(既没有配置在作业中,也没用在环境变量中) - throw DataXException.asDataXException(OdpsWriterErrorCode.GET_ID_KEY_FAIL, - "无法获取到accessId/accessKey. 它们既不存在于您的配置中,也不存在于环境变量中."); - } - - return originalConfig; - } -} diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/LocalStrings.properties b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/LocalStrings.properties new file mode 100644 index 0000000000..289c70fa05 --- /dev/null +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/LocalStrings.properties @@ -0,0 +1,39 @@ +descipher.1=\u957f\u5ea6\u4e0d\u662f\u5076\u6570 + +idandkeyutil.1=\u4ece\u73af\u5883\u53d8\u91cf\u4e2d\u83b7\u53d6accessId/accessKey \u5931\u8d25, accessId=[{0}] +idandkeyutil.2=\u65e0\u6cd5\u83b7\u53d6\u5230accessId/accessKey. \u5b83\u4eec\u65e2\u4e0d\u5b58\u5728\u4e8e\u60a8\u7684\u914d\u7f6e\u4e2d\uff0c\u4e5f\u4e0d\u5b58\u5728\u4e8e\u73af\u5883\u53d8\u91cf\u4e2d. + +odpsutil.1=\u60a8\u672a\u914d\u7f6e\u5199\u5165 ODPS \u76ee\u7684\u8868\u7684\u5217\u4fe1\u606f. \u6b63\u786e\u7684\u914d\u7f6e\u65b9\u5f0f\u662f\u7ed9datax\u7684 column \u9879\u914d\u7f6e\u4e0a\u60a8\u9700\u8981\u8bfb\u53d6\u7684\u5217\u540d\u79f0,\u7528\u82f1\u6587\u9017\u53f7\u5206\u9694 \u4f8b\u5982: \"column\": [\"id\",\"name\"]. +odpsutil.2=[truncate]\u662f\u5fc5\u586b\u914d\u7f6e\u9879, \u610f\u601d\u662f\u5199\u5165 ODPS \u76ee\u7684\u8868\u524d\u662f\u5426\u6e05\u7a7a\u8868/\u5206\u533a. \u8bf7\u60a8\u589e\u52a0 truncate \u7684\u914d\u7f6e\uff0c\u6839\u636e\u4e1a\u52a1\u9700\u8981\u9009\u62e9\u4e0atrue \u6216\u8005 false. +odpsutil.3=\u60a8\u6240\u914d\u7f6e\u7684maxRetryTime \u503c\u9519\u8bef. \u8be5\u503c\u4e0d\u80fd\u5c0f\u4e8e1, \u4e14\u4e0d\u80fd\u5927\u4e8e {0}. \u63a8\u8350\u7684\u914d\u7f6e\u65b9\u5f0f\u662f\u7ed9maxRetryTime \u914d\u7f6e1-11\u4e4b\u95f4\u7684\u67d0\u4e2a\u503c. \u8bf7\u60a8\u68c0\u67e5\u914d\u7f6e\u5e76\u505a\u51fa\u76f8\u5e94\u4fee\u6539. +odpsutil.4=\u4e0d\u652f\u6301\u7684\u8d26\u53f7\u7c7b\u578b:[{0}]. \u8d26\u53f7\u7c7b\u578b\u76ee\u524d\u4ec5\u652f\u6301aliyun, taobao. +odpsutil.5=\u83b7\u53d6 ODPS \u76ee\u7684\u8868:{0} \u7684\u6240\u6709\u5206\u533a\u5931\u8d25. \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. +odpsutil.6=\u68c0\u67e5 ODPS \u76ee\u7684\u8868:{0} \u662f\u5426\u4e3a\u5206\u533a\u8868\u5931\u8d25, \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. +odpsutil.7=\u6e05\u7a7a ODPS \u76ee\u7684\u8868:{0} \u5931\u8d25, \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. +odpsutil.8=\u6dfb\u52a0 ODPS \u76ee\u7684\u8868\u7684\u5206\u533a\u5931\u8d25. \u9519\u8bef\u53d1\u751f\u5728\u6dfb\u52a0 ODPS \u7684\u9879\u76ee:{0} \u7684\u8868:{1} \u7684\u5206\u533a:{2}. \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. +odpsutil.9=\u521b\u5efaTunnelUpload\u5931\u8d25. \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. +odpsutil.10=\u521b\u5efaTunnelUpload\u5931\u8d25. \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. +odpsutil.11=\u83b7\u53d6TunnelUpload\u5931\u8d25. \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. +odpsutil.12=\u83b7\u53d6TunnelUpload\u5931\u8d25. \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. +odpsutil.13=Drop ODPS \u76ee\u7684\u8868\u5206\u533a\u5931\u8d25. \u9519\u8bef\u53d1\u751f\u5728\u9879\u76ee:{0} \u7684\u8868:{1} \u7684\u5206\u533a:{2} .\u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. +odpsutil.14=ODPS \u76ee\u7684\u8868\u81ea\u8eab\u7684 partition:{0} \u683c\u5f0f\u4e0d\u5bf9. \u6b63\u786e\u7684\u683c\u5f0f\u5f62\u5982: pt=1,ds=hangzhou +odpsutil.15=ODPS \u76ee\u7684\u8868\u5728\u8fd0\u884c ODPS SQL\u5931\u8d25, \u8fd4\u56de\u503c\u4e3a:{0}. \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. SQL \u5185\u5bb9\u4e3a:[\n{1}\n]. +odpsutil.16=ODPS \u76ee\u7684\u8868\u5728\u8fd0\u884c ODPS SQL \u65f6\u629b\u51fa\u5f02\u5e38, \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. SQL \u5185\u5bb9\u4e3a:[\n{0}\n]. +odpsutil.17=ODPS \u76ee\u7684\u8868\u5728\u63d0\u4ea4 block:[\n{0}\n] \u65f6\u5931\u8d25, uploadId=[{1}]. \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. +odpsutil.18=ODPS \u76ee\u7684\u8868\u5199 block:{0} \u5931\u8d25\uff0c uploadId=[{1}]. \u8bf7\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5904\u7406. +odpsutil.19=ODPS \u76ee\u7684\u8868\u7684\u5217\u914d\u7f6e\u9519\u8bef. \u7531\u4e8e\u60a8\u6240\u914d\u7f6e\u7684\u5217:{0} \u4e0d\u5b58\u5728\uff0c\u4f1a\u5bfc\u81f4datax\u65e0\u6cd5\u6b63\u5e38\u63d2\u5165\u6570\u636e\uff0c\u8bf7\u68c0\u67e5\u8be5\u5217\u662f\u5426\u5b58\u5728\uff0c\u5982\u679c\u5b58\u5728\u8bf7\u68c0\u67e5\u5927\u5c0f\u5199\u7b49\u914d\u7f6e. +odpsutil.20=DataX \u5199\u5165 ODPS \u8868\u4e0d\u652f\u6301\u8be5\u5b57\u6bb5\u7c7b\u578b:[{0}]. \u76ee\u524d\u652f\u6301\u62bd\u53d6\u7684\u5b57\u6bb5\u7c7b\u578b\u6709\uff1abigint, boolean, datetime, double, string. \u60a8\u53ef\u4ee5\u9009\u62e9\u4e0d\u62bd\u53d6 DataX \u4e0d\u652f\u6301\u7684\u5b57\u6bb5\u6216\u8005\u8054\u7cfb ODPS \u7ba1\u7406\u5458\u5bfb\u6c42\u5e2e\u52a9. +odpsutil.21=\u60a8\u6ca1\u6709\u914d\u7f6e\u5206\u533a\u4fe1\u606f\uff0c\u56e0\u4e3a\u4f60\u914d\u7f6e\u7684\u8868\u662f\u5206\u533a\u8868:{0} \u5982\u679c\u9700\u8981\u8fdb\u884c truncate \u64cd\u4f5c\uff0c\u5fc5\u987b\u6307\u5b9a\u9700\u8981\u6e05\u7a7a\u7684\u5177\u4f53\u5206\u533a. \u8bf7\u4fee\u6539\u5206\u533a\u914d\u7f6e\uff0c\u683c\u5f0f\u5f62\u5982 pt=$'{bizdate'} . +odpsutil.22=\u5206\u533a\u4fe1\u606f\u914d\u7f6e\u9519\u8bef\uff0c\u4f60\u7684ODPS\u8868\u662f\u975e\u5206\u533a\u8868:{0} \u8fdb\u884c truncate \u64cd\u4f5c\u65f6\u4e0d\u9700\u8981\u6307\u5b9a\u5177\u4f53\u5206\u533a\u503c. \u8bf7\u68c0\u67e5\u60a8\u7684\u5206\u533a\u914d\u7f6e\uff0c\u5220\u9664\u8be5\u914d\u7f6e\u9879\u7684\u503c. +odpsutil.23=\u60a8\u7684\u76ee\u7684\u8868\u662f\u5206\u533a\u8868\uff0c\u5199\u5165\u5206\u533a\u8868:{0} \u65f6\u5fc5\u987b\u6307\u5b9a\u5177\u4f53\u5206\u533a\u503c. \u8bf7\u4fee\u6539\u60a8\u7684\u5206\u533a\u914d\u7f6e\u4fe1\u606f\uff0c\u683c\u5f0f\u5f62\u5982 \u683c\u5f0f\u5f62\u5982 pt=$'{bizdate'}. +odpsutil.24=\u60a8\u7684\u76ee\u7684\u8868\u662f\u975e\u5206\u533a\u8868\uff0c\u5199\u5165\u975e\u5206\u533a\u8868:{0} \u65f6\u4e0d\u9700\u8981\u6307\u5b9a\u5177\u4f53\u5206\u533a\u503c. \u8bf7\u5220\u9664\u5206\u533a\u914d\u7f6e\u4fe1\u606f +odpsutil.25=\u60a8\u6ca1\u6709\u914d\u7f6e\u5206\u533a\u4fe1\u606f\uff0c\u56e0\u4e3a\u4f60\u914d\u7f6e\u7684\u8868\u662f\u5206\u533a\u8868:{0} \u5982\u679c\u9700\u8981\u8fdb\u884c truncate \u64cd\u4f5c\uff0c\u5fc5\u987b\u6307\u5b9a\u9700\u8981\u6e05\u7a7a\u7684\u5177\u4f53\u5206\u533a. \u8bf7\u4fee\u6539\u5206\u533a\u914d\u7f6e\uff0c\u683c\u5f0f\u5f62\u5982 pt=$'{bizdate'} . +odpsutil.26=\u5206\u533a\u4fe1\u606f\u914d\u7f6e\u9519\u8bef\uff0c\u4f60\u7684ODPS\u8868\u662f\u975e\u5206\u533a\u8868:{0} \u8fdb\u884c truncate \u64cd\u4f5c\u65f6\u4e0d\u9700\u8981\u6307\u5b9a\u5177\u4f53\u5206\u533a\u503c. \u8bf7\u68c0\u67e5\u60a8\u7684\u5206\u533a\u914d\u7f6e\uff0c\u5220\u9664\u8be5\u914d\u7f6e\u9879\u7684\u503c. +odpsutil.27=\u60a8\u7684\u76ee\u7684\u8868\u662f\u5206\u533a\u8868\uff0c\u5199\u5165\u5206\u533a\u8868:{0} \u65f6\u5fc5\u987b\u6307\u5b9a\u5177\u4f53\u5206\u533a\u503c. \u8bf7\u4fee\u6539\u60a8\u7684\u5206\u533a\u914d\u7f6e\u4fe1\u606f\uff0c\u683c\u5f0f\u5f62\u5982 \u683c\u5f0f\u5f62\u5982 pt=$'{bizdate'}. +odpsutil.28=\u60a8\u7684\u76ee\u7684\u8868\u662f\u975e\u5206\u533a\u8868\uff0c\u5199\u5165\u975e\u5206\u533a\u8868:{0} \u65f6\u4e0d\u9700\u8981\u6307\u5b9a\u5177\u4f53\u5206\u533a\u503c. \u8bf7\u5220\u9664\u5206\u533a\u914d\u7f6e\u4fe1\u606f +odpsutil.29=\u52a0\u8f7d ODPS \u76ee\u7684\u8868:{0} \u5931\u8d25. \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684 ODPS \u76ee\u7684\u8868\u7684 [project] \u662f\u5426\u6b63\u786e. +odpsutil.30=\u52a0\u8f7d ODPS \u76ee\u7684\u8868:{0} \u5931\u8d25. \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684 ODPS \u76ee\u7684\u8868\u7684 [table] \u662f\u5426\u6b63\u786e. +odpsutil.31=\u52a0\u8f7d ODPS \u76ee\u7684\u8868:{0} \u5931\u8d25. \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684 ODPS \u76ee\u7684\u8868\u7684 [accessId] [accessKey]\u662f\u5426\u6b63\u786e. +odpsutil.32=\u52a0\u8f7d ODPS \u76ee\u7684\u8868:{0} \u5931\u8d25. \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684 ODPS \u76ee\u7684\u8868\u7684 [accessKey] \u662f\u5426\u6b63\u786e. +odpsutil.33=\u52a0\u8f7d ODPS \u76ee\u7684\u8868:{0} \u5931\u8d25. \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684 ODPS \u76ee\u7684\u8868\u7684 [accessId] [accessKey] [project]\u662f\u5426\u5339\u914d. +odpsutil.34=\u52a0\u8f7d ODPS \u76ee\u7684\u8868:{0} \u5931\u8d25. \u8bf7\u68c0\u67e5\u60a8\u914d\u7f6e\u7684 ODPS \u76ee\u7684\u8868\u7684 project,table,accessId,accessKey,odpsServer\u7b49\u503c. \ No newline at end of file diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/OdpsExceptionMsg.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/OdpsExceptionMsg.java index d613eefda9..ae6f275c19 100644 --- a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/OdpsExceptionMsg.java +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/OdpsExceptionMsg.java @@ -1,8 +1,5 @@ package com.alibaba.datax.plugin.writer.odpswriter.util; -/** - * Created by hongjiao.hj on 2015/6/9. - */ public class OdpsExceptionMsg { public static final String ODPS_PROJECT_NOT_FOUNT = "ODPS-0420111: Project not found"; diff --git a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/OdpsUtil.java b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/OdpsUtil.java index 2a401b696c..a3a372af80 100755 --- a/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/OdpsUtil.java +++ b/odpswriter/src/main/java/com/alibaba/datax/plugin/writer/odpswriter/util/OdpsUtil.java @@ -1,29 +1,35 @@ package com.alibaba.datax.plugin.writer.odpswriter.util; +import com.alibaba.datax.common.element.*; import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.MessageSource; import com.alibaba.datax.common.util.RetryUtil; -import com.alibaba.datax.plugin.writer.odpswriter.Constant; -import com.alibaba.datax.plugin.writer.odpswriter.Key; - -import com.alibaba.datax.plugin.writer.odpswriter.OdpsWriterErrorCode; +import com.alibaba.datax.plugin.writer.odpswriter.*; import com.aliyun.odps.*; +import com.aliyun.odps.Column; import com.aliyun.odps.account.Account; import com.aliyun.odps.account.AliyunAccount; +import com.aliyun.odps.data.ResultSet; +import com.aliyun.odps.data.Binary; import com.aliyun.odps.task.SQLTask; import com.aliyun.odps.tunnel.TableTunnel; - import com.aliyun.odps.tunnel.io.ProtobufRecordPack; import com.aliyun.odps.tunnel.io.TunnelRecordWriter; +import com.aliyun.odps.type.TypeInfo; + import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.time.DateFormatUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.text.SimpleDateFormat; import java.util.*; import java.util.concurrent.Callable; public class OdpsUtil { private static final Logger LOG = LoggerFactory.getLogger(OdpsUtil.class); + private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(OdpsUtil.class); public static int MAX_RETRY_TIME = 10; @@ -38,15 +44,14 @@ public static void checkNecessaryConfig(Configuration originalConfig) { if (null == originalConfig.getList(Key.COLUMN) || originalConfig.getList(Key.COLUMN, String.class).isEmpty()) { - throw DataXException.asDataXException(OdpsWriterErrorCode.REQUIRED_VALUE, "您未配置写入 ODPS 目的表的列信息. " + - "正确的配置方式是给datax的 column 项配置上您需要读取的列名称,用英文逗号分隔 例如: \"column\": [\"id\",\"name\"]."); + throw DataXException.asDataXException(OdpsWriterErrorCode.REQUIRED_VALUE, MESSAGE_SOURCE.message("odpsutil.1")); } // getBool 内部要求,值只能为 true,false 的字符串(大小写不敏感),其他一律报错,不再有默认配置 + // 如果是动态分区写入,不进行truncate Boolean truncate = originalConfig.getBool(Key.TRUNCATE); if (null == truncate) { - throw DataXException.asDataXException(OdpsWriterErrorCode.REQUIRED_VALUE, "[truncate]是必填配置项, 意思是写入 ODPS 目的表前是否清空表/分区. " + - "请您增加 truncate 的配置,根据业务需要选择上true 或者 false."); + throw DataXException.asDataXException(OdpsWriterErrorCode.REQUIRED_VALUE, MESSAGE_SOURCE.message("odpsutil.2")); } } @@ -54,36 +59,38 @@ public static void dealMaxRetryTime(Configuration originalConfig) { int maxRetryTime = originalConfig.getInt(Key.MAX_RETRY_TIME, OdpsUtil.MAX_RETRY_TIME); if (maxRetryTime < 1 || maxRetryTime > OdpsUtil.MAX_RETRY_TIME) { - throw DataXException.asDataXException(OdpsWriterErrorCode.ILLEGAL_VALUE, "您所配置的maxRetryTime 值错误. 该值不能小于1, 且不能大于 " + OdpsUtil.MAX_RETRY_TIME + - ". 推荐的配置方式是给maxRetryTime 配置1-11之间的某个值. 请您检查配置并做出相应修改."); + throw DataXException.asDataXException(OdpsWriterErrorCode.ILLEGAL_VALUE, MESSAGE_SOURCE.message("odpsutil.3", OdpsUtil.MAX_RETRY_TIME)); } MAX_RETRY_TIME = maxRetryTime; } - public static String formatPartition(String partitionString) { + public static String formatPartition(String partitionString, Boolean printLog) { if (null == partitionString) { return null; } - - return partitionString.trim().replaceAll(" *= *", "=").replaceAll(" */ *", ",") + String parsedPartition = partitionString.trim().replaceAll(" *= *", "=").replaceAll(" */ *", ",") .replaceAll(" *, *", ",").replaceAll("'", ""); + if (printLog) { + LOG.info("format partition with rules: remove all space; remove all '; replace / to ,"); + LOG.info("original partiton {} parsed partition {}", partitionString, parsedPartition); + } + return parsedPartition; } public static Odps initOdpsProject(Configuration originalConfig) { - String accountType = originalConfig.getString(Key.ACCOUNT_TYPE); String accessId = originalConfig.getString(Key.ACCESS_ID); String accessKey = originalConfig.getString(Key.ACCESS_KEY); String odpsServer = originalConfig.getString(Key.ODPS_SERVER); String project = originalConfig.getString(Key.PROJECT); + String securityToken = originalConfig.getString(Key.SECURITY_TOKEN); Account account; - if (accountType.equalsIgnoreCase(Constant.DEFAULT_ACCOUNT_TYPE)) { - account = new AliyunAccount(accessId, accessKey); + if (StringUtils.isNotBlank(securityToken)) { + account = new com.aliyun.odps.account.StsAccount(accessId, accessKey, securityToken); } else { - throw DataXException.asDataXException(OdpsWriterErrorCode.ACCOUNT_TYPE_ERROR, - String.format("不支持的账号类型:[%s]. 账号类型目前仅支持aliyun, taobao.", accountType)); + account = new AliyunAccount(accessId, accessKey); } Odps odps = new Odps(account); @@ -95,6 +102,7 @@ public static Odps initOdpsProject(Configuration originalConfig) { } odps.setDefaultProject(project); odps.setEndpoint(odpsServer); + odps.setUserAgent("DATAX"); return odps; } @@ -124,8 +132,7 @@ public static List listOdpsPartitions(Table table) { parts.add(partition.getPartitionSpec().toString()); } } catch (Exception e) { - throw DataXException.asDataXException(OdpsWriterErrorCode.GET_PARTITION_FAIL, String.format("获取 ODPS 目的表:%s 的所有分区失败. 请联系 ODPS 管理员处理.", - table.getName()), e); + throw DataXException.asDataXException(OdpsWriterErrorCode.GET_PARTITION_FAIL, MESSAGE_SOURCE.message("odpsutil.5", table.getName()), e); } return parts; } @@ -140,37 +147,45 @@ public static boolean isPartitionedTable(Table table) { } } catch (Exception e) { throw DataXException.asDataXException(OdpsWriterErrorCode.CHECK_IF_PARTITIONED_TABLE_FAILED, - String.format("检查 ODPS 目的表:%s 是否为分区表失败, 请联系 ODPS 管理员处理.", table.getName()), e); + MESSAGE_SOURCE.message("odpsutil.6", table.getName()), e); } return false; } public static void truncateNonPartitionedTable(Odps odps, Table tab) { - String truncateNonPartitionedTableSql = "truncate table " + tab.getName() + ";"; + truncateNonPartitionedTable(odps, tab.getName()); + } + + public static void truncateNonPartitionedTable(Odps odps, String tableName) { + String truncateNonPartitionedTableSql = "truncate table " + tableName + ";"; try { - runSqlTaskWithRetry(odps, truncateNonPartitionedTableSql, MAX_RETRY_TIME, 1000, true); + LOG.info("truncate non partitioned table with sql: {}", truncateNonPartitionedTableSql); + runSqlTaskWithRetry(odps, truncateNonPartitionedTableSql, MAX_RETRY_TIME, 1000, true, "truncate", null); } catch (Exception e) { throw DataXException.asDataXException(OdpsWriterErrorCode.TABLE_TRUNCATE_ERROR, - String.format(" 清空 ODPS 目的表:%s 失败, 请联系 ODPS 管理员处理.", tab.getName()), e); + MESSAGE_SOURCE.message("odpsutil.7", tableName), e); } } public static void truncatePartition(Odps odps, Table table, String partition) { if (isPartitionExist(table, partition)) { + LOG.info("partition {} is already exist, truncate it to clean old data", partition); dropPart(odps, table, partition); } + LOG.info("begin to add partition {}", partition); addPart(odps, table, partition); } private static boolean isPartitionExist(Table table, String partition) { // check if exist partition 返回值不为 null List odpsParts = OdpsUtil.listOdpsPartitions(table); - int j = 0; for (; j < odpsParts.size(); j++) { if (odpsParts.get(j).replaceAll("'", "").equals(partition)) { + LOG.info("found a partiton {} equals to (ignore ' if contains) configured partiton {}", + odpsParts.get(j), partition); break; } } @@ -185,11 +200,14 @@ public static void addPart(Odps odps, Table table, String partition) { addPart.append("alter table ").append(table.getName()).append(" add IF NOT EXISTS partition(") .append(partSpec).append(");"); try { - runSqlTaskWithRetry(odps, addPart.toString(), MAX_RETRY_TIME, 1000, true); + Map hints = new HashMap(); + //开启ODPS SQL TYPE2.0类型 + hints.put("odps.sql.type.system.odps2", "true"); + LOG.info("add partition with sql: {}", addPart.toString()); + runSqlTaskWithRetry(odps, addPart.toString(), MAX_RETRY_TIME, 1000, true, "addPart", hints); } catch (Exception e) { throw DataXException.asDataXException(OdpsWriterErrorCode.ADD_PARTITION_FAILED, - String.format("添加 ODPS 目的表的分区失败. 错误发生在添加 ODPS 的项目:%s 的表:%s 的分区:%s. 请联系 ODPS 管理员处理.", - table.getProject(), table.getName(), partition), e); + MESSAGE_SOURCE.message("odpsutil.8", table.getProject(), table.getName(), partition), e); } } @@ -206,7 +224,7 @@ public TableTunnel.UploadSession call() throws Exception { }, MAX_RETRY_TIME, 1000L, true); } catch (Exception e) { throw DataXException.asDataXException(OdpsWriterErrorCode.CREATE_MASTER_UPLOAD_FAIL, - "创建TunnelUpload失败. 请联系 ODPS 管理员处理.", e); + MESSAGE_SOURCE.message("odpsutil.9"), e); } } else { final PartitionSpec partitionSpec = new PartitionSpec(partition); @@ -219,7 +237,7 @@ public TableTunnel.UploadSession call() throws Exception { }, MAX_RETRY_TIME, 1000L, true); } catch (Exception e) { throw DataXException.asDataXException(OdpsWriterErrorCode.CREATE_MASTER_UPLOAD_FAIL, - "创建TunnelUpload失败. 请联系 ODPS 管理员处理.", e); + MESSAGE_SOURCE.message("odpsutil.10"), e); } } } @@ -238,7 +256,7 @@ public TableTunnel.UploadSession call() throws Exception { } catch (Exception e) { throw DataXException.asDataXException(OdpsWriterErrorCode.GET_SLAVE_UPLOAD_FAIL, - "获取TunnelUpload失败. 请联系 ODPS 管理员处理.", e); + MESSAGE_SOURCE.message("odpsutil.11"), e); } } else { final PartitionSpec partitionSpec = new PartitionSpec(partition); @@ -252,7 +270,7 @@ public TableTunnel.UploadSession call() throws Exception { } catch (Exception e) { throw DataXException.asDataXException(OdpsWriterErrorCode.GET_SLAVE_UPLOAD_FAIL, - "获取TunnelUpload失败. 请联系 ODPS 管理员处理.", e); + MESSAGE_SOURCE.message("odpsutil.12"), e); } } } @@ -265,11 +283,14 @@ private static void dropPart(Odps odps, Table table, String partition) { .append(" drop IF EXISTS partition(").append(partSpec) .append(");"); try { - runSqlTaskWithRetry(odps, dropPart.toString(), MAX_RETRY_TIME, 1000, true); + Map hints = new HashMap(); + //开启ODPS SQL TYPE2.0类型 + hints.put("odps.sql.type.system.odps2", "true"); + LOG.info("drop partition with sql: {}", dropPart.toString()); + runSqlTaskWithRetry(odps, dropPart.toString(), MAX_RETRY_TIME, 1000, true, "truncate", hints); } catch (Exception e) { throw DataXException.asDataXException(OdpsWriterErrorCode.ADD_PARTITION_FAILED, - String.format("Drop ODPS 目的表分区失败. 错误发生在项目:%s 的表:%s 的分区:%s .请联系 ODPS 管理员处理.", - table.getProject(), table.getName(), partition), e); + MESSAGE_SOURCE.message("odpsutil.13", table.getProject(), table.getName(), partition), e); } } @@ -281,7 +302,7 @@ private static String getPartSpec(String partition) { String[] kv = part.split("="); if (kv.length != 2) { throw DataXException.asDataXException(OdpsWriterErrorCode.ILLEGAL_VALUE, - String.format("ODPS 目的表自身的 partition:%s 格式不对. 正确的格式形如: pt=1,ds=hangzhou", partition)); + MESSAGE_SOURCE.message("odpsutil.14", partition)); } partSpec.append(kv[0]).append("="); partSpec.append("'").append(kv[1].replace("'", "")).append("'"); @@ -292,6 +313,38 @@ private static String getPartSpec(String partition) { return partSpec.toString(); } + public static Instance runSqlTaskWithRetry(final Odps odps, final String sql, String tag) { + try { + long beginTime = System.currentTimeMillis(); + + Instance instance = runSqlTaskWithRetry(odps, sql, MAX_RETRY_TIME, 1000, true, tag, null); + + long endIime = System.currentTimeMillis(); + LOG.info(String.format("exectue odps sql: %s finished, cost time : %s ms", + sql, (endIime - beginTime))); + return instance; + } catch (Exception e) { + throw DataXException.asDataXException(OdpsWriterErrorCode.RUN_SQL_ODPS_EXCEPTION, + MESSAGE_SOURCE.message("odpsutil.16", sql), e); + } + } + + public static ResultSet getSqlTaskRecordsWithRetry(final Odps odps, final String sql, String tag) { + Instance instance = runSqlTaskWithRetry(odps, sql, tag); + if (instance == null) { + LOG.error("can not get odps instance from sql {}", sql); + throw DataXException.asDataXException(OdpsWriterErrorCode.RUN_SQL_ODPS_EXCEPTION, + MESSAGE_SOURCE.message("odpsutil.16", sql)); + } + try { + return SQLTask.getResultSet(instance, instance.getTaskNames().iterator().next()); + } catch (Exception e) { + throw DataXException.asDataXException(OdpsWriterErrorCode.RUN_SQL_ODPS_EXCEPTION, + MESSAGE_SOURCE.message("odpsutil.16", sql), e); + } + } + + /** * 该方法只有在 sql 为幂等的才可以使用,且odps抛出异常时候才会进行重试 * @@ -299,12 +352,12 @@ private static String getPartSpec(String partition) { * @param query 执行sql * @throws Exception */ - public static void runSqlTaskWithRetry(final Odps odps, final String query, int retryTimes, - long sleepTimeInMilliSecond, boolean exponential) throws Exception { + public static Instance runSqlTaskWithRetry(final Odps odps, final String query, int retryTimes, + long sleepTimeInMilliSecond, boolean exponential, String tag, + Map hints) throws Exception { for(int i = 0; i < retryTimes; i++) { try { - runSqlTask(odps, query); - return; + return runSqlTask(odps, query, tag, hints); } catch (DataXException e) { if (OdpsWriterErrorCode.RUN_SQL_ODPS_EXCEPTION.equals(e.getErrorCode())) { LOG.debug("Exception when calling callable", e); @@ -337,37 +390,86 @@ public static void runSqlTaskWithRetry(final Odps odps, final String query, int throw e; } } + return null; } - public static void runSqlTask(Odps odps, String query) { + public static Instance runSqlTask(Odps odps, String query, String tag, Map hints) { if (StringUtils.isBlank(query)) { - return; + return null; } - String taskName = "datax_odpswriter_trunacte_" + UUID.randomUUID().toString().replace('-', '_'); - + String taskName = String.format("datax_odpswriter_%s_%s", tag, UUID.randomUUID().toString().replace('-', '_')); LOG.info("Try to start sqlTask:[{}] to run odps sql:[\n{}\n] .", taskName, query); //todo:biz_id set (目前ddl先不做) Instance instance; Instance.TaskStatus status; try { - instance = SQLTask.run(odps, odps.getDefaultProject(), query, taskName, null, null); + instance = SQLTask.run(odps, odps.getDefaultProject(), query, taskName, hints, null); instance.waitForSuccess(); status = instance.getTaskStatus().get(taskName); if (!Instance.TaskStatus.Status.SUCCESS.equals(status.getStatus())) { throw DataXException.asDataXException(OdpsWriterErrorCode.RUN_SQL_FAILED, - String.format("ODPS 目的表在运行 ODPS SQL失败, 返回值为:%s. 请联系 ODPS 管理员处理. SQL 内容为:[\n%s\n].", instance.getTaskResults().get(taskName), - query)); + MESSAGE_SOURCE.message("odpsutil.15", query)); } + return instance; } catch (DataXException e) { throw e; } catch (Exception e) { throw DataXException.asDataXException(OdpsWriterErrorCode.RUN_SQL_ODPS_EXCEPTION, - String.format("ODPS 目的表在运行 ODPS SQL 时抛出异常, 请联系 ODPS 管理员处理. SQL 内容为:[\n%s\n].", query), e); + MESSAGE_SOURCE.message("odpsutil.16", query), e); } } + + public static String generateTaskName(String tag) { + return String.format("datax_odpswriter_%s_%s", tag, UUID.randomUUID().toString().replace('-', '_')); + } + + public static void checkBlockComplete(final TableTunnel.UploadSession masterUpload, final Long[] blocks) { + Long[] serverBlocks; + try { + serverBlocks = + RetryUtil.executeWithRetry(new Callable() { + @Override + public Long[] call() throws Exception { + return masterUpload.getBlockList(); + } + }, MAX_RETRY_TIME, 1000L, true); + } catch (Exception e) { + throw DataXException.asDataXException(OdpsWriterErrorCode.COMMIT_BLOCK_FAIL, + MESSAGE_SOURCE.message("odpsutil.17", masterUpload.getId()), e); + } + + HashMap serverBlockMap = new HashMap(); + for (Long blockId : serverBlocks) { + serverBlockMap.put(blockId, true); + } + + for (Long blockId : blocks) { + if (!serverBlockMap.containsKey(blockId)) { + throw DataXException.asDataXException(OdpsWriterErrorCode.COMMIT_BLOCK_FAIL, + "BlockId[" + blockId + "] upload failed!"); + } + } + + } + + public static void masterComplete(final TableTunnel.UploadSession masterUpload) { + try { + RetryUtil.executeWithRetry(new Callable() { + @Override + public Void call() throws Exception { + masterUpload.commit(); + return null; + } + }, MAX_RETRY_TIME, 1000L, true); + } catch (Exception e) { + throw DataXException.asDataXException(OdpsWriterErrorCode.COMMIT_BLOCK_FAIL, + MESSAGE_SOURCE.message("odpsutil.17", masterUpload.getId()), e); + } + } + public static void masterCompleteBlocks(final TableTunnel.UploadSession masterUpload, final Long[] blocks) { try { RetryUtil.executeWithRetry(new Callable() { @@ -379,30 +481,28 @@ public Void call() throws Exception { }, MAX_RETRY_TIME, 1000L, true); } catch (Exception e) { throw DataXException.asDataXException(OdpsWriterErrorCode.COMMIT_BLOCK_FAIL, - String.format("ODPS 目的表在提交 block:[\n%s\n] 时失败, uploadId=[%s]. 请联系 ODPS 管理员处理.", StringUtils.join(blocks, ","), masterUpload.getId()), e); + MESSAGE_SOURCE.message("odpsutil.17", StringUtils.join(blocks, ","), masterUpload.getId()), e); } } public static void slaveWriteOneBlock(final TableTunnel.UploadSession slaveUpload, final ProtobufRecordPack protobufRecordPack, - final long blockId, final boolean isCompress) { + final long blockId, final Long timeoutInMs) { try { RetryUtil.executeWithRetry(new Callable() { @Override public Void call() throws Exception { - TunnelRecordWriter tunnelRecordWriter = (TunnelRecordWriter)slaveUpload.openRecordWriter(blockId, isCompress); - tunnelRecordWriter.write(protobufRecordPack); - tunnelRecordWriter.close(); + slaveUpload.writeBlock(blockId, protobufRecordPack, timeoutInMs); return null; } }, MAX_RETRY_TIME, 1000L, true); } catch (Exception e) { throw DataXException.asDataXException(OdpsWriterErrorCode.WRITER_RECORD_FAIL, - String.format("ODPS 目的表写 block:%s 失败, uploadId=[%s]. 请联系 ODPS 管理员处理.", blockId, slaveUpload.getId()), e); + MESSAGE_SOURCE.message("odpsutil.18", blockId, slaveUpload.getId()), e); } } - public static List parsePosition(List allColumnList, + public static List parsePosition(List allColumnList, List allPartColumnList, List userConfiguredColumns) { List retList = new ArrayList(); @@ -416,9 +516,20 @@ public static List parsePosition(List allColumnList, break; } } + + if (null != allPartColumnList) { + for (int i = 0, len = allPartColumnList.size(); i < len; i++) { + if (allPartColumnList.get(i).equalsIgnoreCase(col)) { + retList.add(-1); + hasColumn = true; + break; + } + } + } + if (!hasColumn) { throw DataXException.asDataXException(OdpsWriterErrorCode.COLUMN_NOT_EXIST, - String.format("ODPS 目的表的列配置错误. 由于您所配置的列:%s 不存在,会导致datax无法正常插入数据,请检查该列是否存在,如果存在请检查大小写等配置.", col)); + MESSAGE_SOURCE.message("odpsutil.19", col)); } } return retList; @@ -436,22 +547,81 @@ public static List getAllColumns(TableSchema schema) { for(Column column: columns) { allColumns.add(column.getName()); type = column.getType(); - if (type == OdpsType.ARRAY || type == OdpsType.MAP) { - throw DataXException.asDataXException(OdpsWriterErrorCode.UNSUPPORTED_COLUMN_TYPE, - String.format("DataX 写入 ODPS 表不支持该字段类型:[%s]. 目前支持抽取的字段类型有:bigint, boolean, datetime, double, string. " + - "您可以选择不抽取 DataX 不支持的字段或者联系 ODPS 管理员寻求帮助.", - type)); - } } return allColumns; } - public static List getTableOriginalColumnTypeList(TableSchema schema) { - List tableOriginalColumnTypeList = new ArrayList(); + public static List getAllPartColumns(TableSchema schema) { + if (null == schema) { + throw new IllegalArgumentException("parameter schema can not be null."); + } + + List allPartColumns = new ArrayList<>(); + + List partCols = schema.getPartitionColumns(); + + for (Column column : partCols) { + allPartColumns.add(column.getName()); + } + + return allPartColumns; + } + + public static String getPartColValFromDataXRecord(com.alibaba.datax.common.element.Record dataxRecord, + List positions, List userConfiguredColumns, + Map dateTransFormMap) { + StringBuilder partition = new StringBuilder(); + for (int i = 0, len = dataxRecord.getColumnNumber(); i < len; i++) { + if (positions.get(i) == -1) { + if (partition.length() > 0) { + partition.append(","); + } + String partName = userConfiguredColumns.get(i); + //todo: 这里应该根据分区列的类型做转换,这里先直接toString转换了 + com.alibaba.datax.common.element.Column partitionCol = dataxRecord.getColumn(i); + String partVal = partitionCol.getRawData().toString(); + if (StringUtils.isBlank(partVal)) { + throw new DataXException(OdpsWriterErrorCode.ILLEGAL_VALUE, String.format( + "value of column %s exit null value, it can not be used as partition column", partName)); + } + + // 如果分区列的值的格式是一个日期,并且用户设置列的转换规则 + DateTransForm dateTransForm = null; + if (null != dateTransFormMap) { + dateTransForm = dateTransFormMap.get(partName); + } + if (null != dateTransForm) { + try { + // 日期列 + if (partitionCol.getType().equals(com.alibaba.datax.common.element.Column.Type.DATE)) { + partVal = OdpsUtil.date2StringWithFormat(partitionCol.asDate(), dateTransForm.getToFormat()); + } + // String 列,需要先按照 fromFormat 转换为日期 + if (partitionCol.getType().equals(com.alibaba.datax.common.element.Column.Type.STRING)) { + partVal = OdpsUtil.date2StringWithFormat(partitionCol.asDate(dateTransForm.getFromFormat()), dateTransForm.getToFormat()); + } + } catch (DataXException e) { + LOG.warn("Parse {} with format {} error! Please check the column config and {} config. So user original value '{}'. Detail info: {}", + partVal, dateTransForm.toString(), Key.PARTITION_COL_MAPPING, partVal, e); + } + } + + partition.append(partName).append("=").append(partVal); + } + } + return partition.toString(); + } + + public static String date2StringWithFormat(Date date, String dateFormat) { + return DateFormatUtils.format(date, dateFormat, TimeZone.getTimeZone("GMT+8")); + } + + public static List getTableOriginalColumnTypeList(TableSchema schema) { + List tableOriginalColumnTypeList = new ArrayList(); List columns = schema.getColumns(); for (Column column : columns) { - tableOriginalColumnTypeList.add(column.getType()); + tableOriginalColumnTypeList.add(column.getTypeInfo()); } return tableOriginalColumnTypeList; @@ -465,8 +635,7 @@ public static void dealTruncate(Odps odps, Table table, String partition, boolea if (isPartitionedTable) { //分区表 if (StringUtils.isBlank(partition)) { - throw DataXException.asDataXException(OdpsWriterErrorCode.PARTITION_ERROR, String.format("您没有配置分区信息,因为你配置的表是分区表:%s 如果需要进行 truncate 操作,必须指定需要清空的具体分区. 请修改分区配置,格式形如 pt=${bizdate} .", - table.getName())); + throw DataXException.asDataXException(OdpsWriterErrorCode.PARTITION_ERROR, MESSAGE_SOURCE.message("odpsutil.21", table.getName())); } else { LOG.info("Try to truncate partition=[{}] in table=[{}].", partition, table.getName()); OdpsUtil.truncatePartition(odps, table, partition); @@ -474,8 +643,7 @@ public static void dealTruncate(Odps odps, Table table, String partition, boolea } else { //非分区表 if (StringUtils.isNotBlank(partition)) { - throw DataXException.asDataXException(OdpsWriterErrorCode.PARTITION_ERROR, String.format("分区信息配置错误,你的ODPS表是非分区表:%s 进行 truncate 操作时不需要指定具体分区值. 请检查您的分区配置,删除该配置项的值.", - table.getName())); + throw DataXException.asDataXException(OdpsWriterErrorCode.PARTITION_ERROR, MESSAGE_SOURCE.message("odpsutil.22", table.getName())); } else { LOG.info("Try to truncate table:[{}].", table.getName()); OdpsUtil.truncateNonPartitionedTable(odps, table); @@ -487,7 +655,7 @@ public static void dealTruncate(Odps odps, Table table, String partition, boolea //分区表 if (StringUtils.isBlank(partition)) { throw DataXException.asDataXException(OdpsWriterErrorCode.PARTITION_ERROR, - String.format("您的目的表是分区表,写入分区表:%s 时必须指定具体分区值. 请修改您的分区配置信息,格式形如 格式形如 pt=${bizdate}.", table.getName())); + MESSAGE_SOURCE.message("odpsutil.23", table.getName())); } else { boolean isPartitionExists = OdpsUtil.isPartitionExist(table, partition); if (!isPartitionExists) { @@ -500,7 +668,7 @@ public static void dealTruncate(Odps odps, Table table, String partition, boolea //非分区表 if (StringUtils.isNotBlank(partition)) { throw DataXException.asDataXException(OdpsWriterErrorCode.PARTITION_ERROR, - String.format("您的目的表是非分区表,写入非分区表:%s 时不需要指定具体分区值. 请删除分区配置信息", table.getName())); + MESSAGE_SOURCE.message("odpsutil.24", table.getName())); } } } @@ -523,14 +691,12 @@ public static void preCheckPartition(Odps odps, Table table, String partition, b if (isPartitionedTable) { //分区表 if (StringUtils.isBlank(partition)) { - throw DataXException.asDataXException(OdpsWriterErrorCode.PARTITION_ERROR, String.format("您没有配置分区信息,因为你配置的表是分区表:%s 如果需要进行 truncate 操作,必须指定需要清空的具体分区. 请修改分区配置,格式形如 pt=${bizdate} .", - table.getName())); + throw DataXException.asDataXException(OdpsWriterErrorCode.PARTITION_ERROR, MESSAGE_SOURCE.message("odpsutil.25", table.getName())); } } else { //非分区表 if (StringUtils.isNotBlank(partition)) { - throw DataXException.asDataXException(OdpsWriterErrorCode.PARTITION_ERROR, String.format("分区信息配置错误,你的ODPS表是非分区表:%s 进行 truncate 操作时不需要指定具体分区值. 请检查您的分区配置,删除该配置项的值.", - table.getName())); + throw DataXException.asDataXException(OdpsWriterErrorCode.PARTITION_ERROR, MESSAGE_SOURCE.message("odpsutil.26", table.getName())); } } } else { @@ -539,13 +705,13 @@ public static void preCheckPartition(Odps odps, Table table, String partition, b //分区表 if (StringUtils.isBlank(partition)) { throw DataXException.asDataXException(OdpsWriterErrorCode.PARTITION_ERROR, - String.format("您的目的表是分区表,写入分区表:%s 时必须指定具体分区值. 请修改您的分区配置信息,格式形如 格式形如 pt=${bizdate}.", table.getName())); + MESSAGE_SOURCE.message("odpsutil.27", table.getName())); } } else { //非分区表 if (StringUtils.isNotBlank(partition)) { throw DataXException.asDataXException(OdpsWriterErrorCode.PARTITION_ERROR, - String.format("您的目的表是非分区表,写入非分区表:%s 时不需要指定具体分区值. 请删除分区配置信息", table.getName())); + MESSAGE_SOURCE.message("odpsutil.28", table.getName())); } } } @@ -558,29 +724,286 @@ public static void throwDataXExceptionWhenReloadTable(Exception e, String tableN if(e.getMessage() != null) { if(e.getMessage().contains(OdpsExceptionMsg.ODPS_PROJECT_NOT_FOUNT)) { throw DataXException.asDataXException(OdpsWriterErrorCode.ODPS_PROJECT_NOT_FOUNT, - String.format("加载 ODPS 目的表:%s 失败. " + - "请检查您配置的 ODPS 目的表的 [project] 是否正确.", tableName), e); + MESSAGE_SOURCE.message("odpsutil.29", tableName), e); } else if(e.getMessage().contains(OdpsExceptionMsg.ODPS_TABLE_NOT_FOUNT)) { throw DataXException.asDataXException(OdpsWriterErrorCode.ODPS_TABLE_NOT_FOUNT, - String.format("加载 ODPS 目的表:%s 失败. " + - "请检查您配置的 ODPS 目的表的 [table] 是否正确.", tableName), e); + MESSAGE_SOURCE.message("odpsutil.30", tableName), e); } else if(e.getMessage().contains(OdpsExceptionMsg.ODPS_ACCESS_KEY_ID_NOT_FOUND)) { throw DataXException.asDataXException(OdpsWriterErrorCode.ODPS_ACCESS_KEY_ID_NOT_FOUND, - String.format("加载 ODPS 目的表:%s 失败. " + - "请检查您配置的 ODPS 目的表的 [accessId] [accessKey]是否正确.", tableName), e); + MESSAGE_SOURCE.message("odpsutil.31", tableName), e); } else if(e.getMessage().contains(OdpsExceptionMsg.ODPS_ACCESS_KEY_INVALID)) { throw DataXException.asDataXException(OdpsWriterErrorCode.ODPS_ACCESS_KEY_INVALID, - String.format("加载 ODPS 目的表:%s 失败. " + - "请检查您配置的 ODPS 目的表的 [accessKey] 是否正确.", tableName), e); + MESSAGE_SOURCE.message("odpsutil.32", tableName), e); } else if(e.getMessage().contains(OdpsExceptionMsg.ODPS_ACCESS_DENY)) { throw DataXException.asDataXException(OdpsWriterErrorCode.ODPS_ACCESS_DENY, - String.format("加载 ODPS 目的表:%s 失败. " + - "请检查您配置的 ODPS 目的表的 [accessId] [accessKey] [project]是否匹配.", tableName), e); + MESSAGE_SOURCE.message("odpsutil.33", tableName), e); } } throw DataXException.asDataXException(OdpsWriterErrorCode.ILLEGAL_VALUE, - String.format("加载 ODPS 目的表:%s 失败. " + - "请检查您配置的 ODPS 目的表的 project,table,accessId,accessKey,odpsServer等值.", tableName), e); + MESSAGE_SOURCE.message("odpsutil.34", tableName), e); + } + + /** + * count统计数据,自动创建统计表 + * @param tableName 统计表名字 + * @return + */ + public static String getCreateSummaryTableDDL(String tableName) { + return String.format("CREATE TABLE IF NOT EXISTS %s " + + "(src_table_name STRING, " + + "dest_table_name STRING, " + + "src_row_num BIGINT, " + + "src_query_time DATETIME, " + + "read_succeed_records BIGINT," + + "write_succeed_records BIGINT," + + "dest_row_num BIGINT, " + + "write_time DATETIME);", + tableName); + } + + /** + * count统计数据,获取count dml + * @param tableName + * @return + */ + public static String countTableSql(final String tableName, final String partition) { + if (StringUtils.isNotBlank(partition)) { + String[] partitions = partition.split("\\,"); + String p = String.join(" and ", partitions); + return String.format("SELECT COUNT(1) AS odps_num FROM %s WHERE %s;", tableName, p); + } else { + return String.format("SELECT COUNT(1) AS odps_num FROM %s;", tableName); + } + } + + /** + * count统计数据 dml 对应字段,用于查询 + * @return + */ + public static String countName() { + return "odps_num"; + } + + /** + * count统计数据dml + * @param summaryTableName 统计数据写入表 + * @param sourceTableName datax reader 表 + * @param destTableName datax writer 表 + * @param srcCount reader表行数 + * @param queryTime reader表查询时间 + * @param destCount writer 表行书 + * @return insert dml sql + */ + public static String getInsertSummaryTableSql(String summaryTableName, String sourceTableName, String destTableName, + Long srcCount, String queryTime, Number readSucceedRecords, + Number writeSucceedRecords, Long destCount) { + final String sql = "INSERT INTO %s (src_table_name,dest_table_name," + + " src_row_num, src_query_time, read_succeed_records, write_succeed_records, dest_row_num, write_time) VALUES ( %s );"; + + String insertData = String.format("'%s', '%s', %s, %s, %s, %s, %s, getdate()", + sourceTableName, destTableName, srcCount, queryTime, readSucceedRecords, writeSucceedRecords, destCount ); + return String.format(sql, summaryTableName, insertData); + } + + public static void createTable(Odps odps, String tableName, final String sql) { + try { + LOG.info("create table with sql: {}", sql); + runSqlTaskWithRetry(odps, sql, MAX_RETRY_TIME, 1000, true, "create", null); + } catch (Exception e) { + throw DataXException.asDataXException(OdpsWriterErrorCode.RUN_SQL_FAILED, + MESSAGE_SOURCE.message("odpsutil.7", tableName), e); + } + } + + public static void createTableFromTable(Odps odps, String resourceTable, String targetTable) { + TableSchema schema = odps.tables().get(resourceTable).getSchema(); + StringBuilder builder = new StringBuilder(); + Iterator iterator = schema.getColumns().iterator(); + while (iterator.hasNext()) { + Column c = iterator.next(); + builder.append(String.format(" %s %s ", c.getName(), c.getTypeInfo().getTypeName())); + if (iterator.hasNext()) { + builder.append(","); + } + } + String createTableSql = String.format("CREATE TABLE IF NOT EXISTS %s (%s);", targetTable, builder.toString()); + + try { + LOG.info("create table with sql: {}", createTableSql); + runSqlTaskWithRetry(odps, createTableSql, MAX_RETRY_TIME, 1000, true, "create", null); + } catch (Exception e) { + throw DataXException.asDataXException(OdpsWriterErrorCode.RUN_SQL_FAILED, + MESSAGE_SOURCE.message("odpsutil.7", targetTable), e); + } + } + + public static Object truncateSingleFieldData(OdpsType type, Object data, int limit, Boolean enableOverLengthOutput) { + if (data == null) { + return data; + } + if (OdpsType.STRING.equals(type)) { + if(enableOverLengthOutput) { + LOG.warn( + "InvalidData: The string's length is more than " + limit + " bytes. content:" + data); + } + LOG.info("before truncate string length:" + ((String) data).length()); + //确保特殊字符场景下的截断 + limit -= Constant.UTF8_ENCODED_CHAR_MAX_SIZE; + data = cutString((String) data, limit); + LOG.info("after truncate string length:" + ((String) data).length()); + } else if (OdpsType.BINARY.equals(type)) { + byte[] oriDataBytes = ((Binary) data).data(); + if(oriDataBytes == null){ + return data; + } + int originLength = oriDataBytes.length; + if (originLength <= limit) { + return data; + } + if(enableOverLengthOutput) { + LOG.warn("InvalidData: The binary's length is more than " + limit + " bytes. content:" + byteArrToHex(oriDataBytes)); + } + LOG.info("before truncate binary length:" + oriDataBytes.length); + byte[] newData = new byte[limit]; + System.arraycopy(oriDataBytes, 0, newData, 0, limit); + LOG.info("after truncate binary length:" + newData.length); + return new Binary(newData); + } + return data; + } + public static Object setNull(OdpsType type,Object data, int limit, Boolean enableOverLengthOutput) { + if (data == null ) { + return null; + } + if (OdpsType.STRING.equals(type)) { + if(enableOverLengthOutput) { + LOG.warn( + "InvalidData: The string's length is more than " + limit + " bytes. content:" + data); + } + return null; + } else if (OdpsType.BINARY.equals(type)) { + byte[] oriDataBytes = ((Binary) data).data(); + int originLength = oriDataBytes.length; + if (originLength > limit) { + if(enableOverLengthOutput) { + LOG.warn("InvalidData: The binary's length is more than " + limit + " bytes. content:" + new String(oriDataBytes)); + } + return null; + } + } + return data; + } + public static boolean validateStringLength(String value, long limit) { + try { + if (value.length() * Constant.UTF8_ENCODED_CHAR_MAX_SIZE > limit + && value.getBytes("utf-8").length > limit) { + return false; + } + } catch (Exception e) { + e.printStackTrace(); + return true; + } + return true; + } + public static String cutString(String sourceString, int cutBytes) { + if (sourceString == null || "".equals(sourceString.trim()) || cutBytes < 1) { + return ""; + } + int lastIndex = 0; + boolean stopFlag = false; + int totalBytes = 0; + for (int i = 0; i < sourceString.length(); i++) { + String s = Integer.toBinaryString(sourceString.charAt(i)); + if (s.length() > 8) { + totalBytes += 3; + } else { + totalBytes += 1; + } + if (!stopFlag) { + if (totalBytes == cutBytes) { + lastIndex = i; + stopFlag = true; + } else if (totalBytes > cutBytes) { + lastIndex = i - 1; + stopFlag = true; + } + } + } + if (!stopFlag) { + return sourceString; + } else { + return sourceString.substring(0, lastIndex + 1); + } + } + public static boolean dataOverLength(OdpsType type, Object data, int limit){ + if (data == null ) { + return false; + } + if (OdpsType.STRING.equals(type)) { + if(!OdpsUtil.validateStringLength((String)data, limit)){ + return true; + } + }else if (OdpsType.BINARY.equals(type)){ + byte[] oriDataBytes = ((Binary) data).data(); + if(oriDataBytes == null){ + return false; + } + int originLength = oriDataBytes.length; + if (originLength > limit) { + return true; + } + } + return false; + } + public static Object processOverLengthData(Object data, OdpsType type, String overLengthRule, int maxFieldLength, Boolean enableOverLengthOutput) { + try{ + //超长数据检查 + if(OdpsWriter.maxOutputOverLengthRecord != null && OdpsWriter.globalTotalTruncatedRecordNumber.get() >= OdpsWriter.maxOutputOverLengthRecord){ + enableOverLengthOutput = false; + } + if ("truncate".equalsIgnoreCase(overLengthRule)) { + if (OdpsUtil.dataOverLength(type, data, OdpsWriter.maxOdpsFieldLength)) { + Object newData = OdpsUtil.truncateSingleFieldData(type, data, maxFieldLength, enableOverLengthOutput); + OdpsWriter.globalTotalTruncatedRecordNumber.incrementAndGet(); + return newData; + } + } else if ("setNull".equalsIgnoreCase(overLengthRule)) { + if (OdpsUtil.dataOverLength(type, data, OdpsWriter.maxOdpsFieldLength)) { + OdpsWriter.globalTotalTruncatedRecordNumber.incrementAndGet(); + return OdpsUtil.setNull(type, data, maxFieldLength, enableOverLengthOutput); + } + } + }catch (Throwable e){ + LOG.warn("truncate overLength data failed!", e); + } + return data; + } + private static final char HEX_CHAR_ARR[] = {'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'}; + /** + * 字节数组转十六进制字符串 + * @param btArr + * @return + */ + public static String byteArrToHex(byte[] btArr) { + char strArr[] = new char[btArr.length * 2]; + int i = 0; + for (byte bt : btArr) { + strArr[i++] = HEX_CHAR_ARR[bt>>>4 & 0xf]; + strArr[i++] = HEX_CHAR_ARR[bt & 0xf]; + } + return new String(strArr); + } + public static byte[] hexToByteArr(String hexStr) { + char[] charArr = hexStr.toCharArray(); + byte btArr[] = new byte[charArr.length / 2]; + int index = 0; + for (int i = 0; i < charArr.length; i++) { + int highBit = hexStr.indexOf(charArr[i]); + int lowBit = hexStr.indexOf(charArr[++i]); + btArr[index] = (byte) (highBit << 4 | lowBit); + index++; + } + return btArr; } } diff --git a/odpswriter/src/main/libs/bcprov-jdk15on-1.52.jar b/odpswriter/src/main/libs/bcprov-jdk15on-1.52.jar deleted file mode 100644 index 6c54dd901c..0000000000 Binary files a/odpswriter/src/main/libs/bcprov-jdk15on-1.52.jar and /dev/null differ diff --git a/opentsdbreader/pom.xml b/opentsdbreader/pom.xml index aa3461d8f1..b10fba0207 100644 --- a/opentsdbreader/pom.xml +++ b/opentsdbreader/pom.xml @@ -21,17 +21,14 @@ 3.3.2 - 4.4 + 4.5 2.4 - - 1.2.28 - 2.3.2 - 4.12 + 4.13.1 2.9.9 @@ -47,10 +44,6 @@ slf4j-log4j12 org.slf4j - - fastjson - com.alibaba - commons-math3 org.apache.commons @@ -92,9 +85,8 @@ - com.alibaba - fastjson - ${fastjson.version} + com.alibaba.fastjson2 + fastjson2 diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/CliQuery.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/CliQuery.java index fe8dce2b99..88822089f2 100644 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/CliQuery.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/CliQuery.java @@ -6,14 +6,30 @@ import java.util.ArrayList; import java.util.HashMap; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:CliQuery - * - * @author Benedict Jin - * @since 2019-04-17 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . final class CliQuery { /** diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/Connection4TSDB.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/Connection4TSDB.java index 97a841cfad..4b75acb9a0 100644 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/Connection4TSDB.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/Connection4TSDB.java @@ -4,14 +4,30 @@ import java.util.List; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:Connection for TSDB-like databases - * - * @author Benedict Jin - * @since 2019-03-29 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . public interface Connection4TSDB { /** diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/DataPoint4TSDB.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/DataPoint4TSDB.java index 1f69024508..e8a84fb216 100644 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/DataPoint4TSDB.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/DataPoint4TSDB.java @@ -1,17 +1,33 @@ package com.alibaba.datax.plugin.reader.conn; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import java.util.Map; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:DataPoint for TSDB - * - * @author Benedict Jin - * @since 2019-04-10 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . public class DataPoint4TSDB { private long timestamp; diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/DumpSeries.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/DumpSeries.java index 56ab0bc2ab..4aed1458a3 100644 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/DumpSeries.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/DumpSeries.java @@ -12,14 +12,30 @@ import java.util.*; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:Tool to dump the data straight from HBase - * - * @author Benedict Jin - * @since 2019-04-17 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . final class DumpSeries { private static final Logger LOG = LoggerFactory.getLogger(DumpSeries.class); diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/OpenTSDBConnection.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/OpenTSDBConnection.java index 9e7f12c93c..49ba5fb36b 100644 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/OpenTSDBConnection.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/OpenTSDBConnection.java @@ -2,19 +2,35 @@ import com.alibaba.datax.common.plugin.RecordSender; import com.alibaba.datax.plugin.reader.util.TSDBUtils; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import org.apache.commons.lang3.StringUtils; import java.util.List; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:OpenTSDB Connection - * - * @author Benedict Jin - * @since 2019-03-29 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . public class OpenTSDBConnection implements Connection4TSDB { private String address; diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/OpenTSDBDump.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/OpenTSDBDump.java index 5ed0a314b3..6f3c551a2a 100644 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/OpenTSDBDump.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/conn/OpenTSDBDump.java @@ -1,20 +1,36 @@ package com.alibaba.datax.plugin.reader.conn; import com.alibaba.datax.common.plugin.RecordSender; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import net.opentsdb.core.TSDB; import net.opentsdb.utils.Config; import java.util.Map; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:OpenTSDB Dump - * - * @author Benedict Jin - * @since 2019-04-15 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . final class OpenTSDBDump { private static TSDB TSDB_INSTANCE; diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/Constant.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/Constant.java index 6017d4e5eb..286443de59 100644 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/Constant.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/Constant.java @@ -1,13 +1,29 @@ package com.alibaba.datax.plugin.reader.opentsdbreader; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:Key - * - * @author Benedict Jin - * @since 2019-04-18 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . public final class Constant { static final String DEFAULT_DATA_FORMAT = "yyyy-MM-dd HH:mm:ss"; diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/Key.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/Key.java index 5b8c4adc82..2d2c284437 100644 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/Key.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/Key.java @@ -1,13 +1,29 @@ package com.alibaba.datax.plugin.reader.opentsdbreader; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:Key - * - * @author Benedict Jin - * @since 2019-04-18 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . public class Key { static final String ENDPOINT = "endpoint"; diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/OpenTSDBReader.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/OpenTSDBReader.java index d57456d15e..7790a2b1cf 100755 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/OpenTSDBReader.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/OpenTSDBReader.java @@ -6,7 +6,7 @@ import com.alibaba.datax.common.util.Configuration; import com.alibaba.datax.plugin.reader.conn.OpenTSDBConnection; import com.alibaba.datax.plugin.reader.util.TimeUtils; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.slf4j.Logger; @@ -18,14 +18,30 @@ import java.util.Collections; import java.util.List; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:Key - * - * @author Benedict Jin - * @since 2019-04-18 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . @SuppressWarnings("unused") public class OpenTSDBReader extends Reader { diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/OpenTSDBReaderErrorCode.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/OpenTSDBReaderErrorCode.java index 0d9de4c458..479936c668 100755 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/OpenTSDBReaderErrorCode.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/opentsdbreader/OpenTSDBReaderErrorCode.java @@ -2,14 +2,30 @@ import com.alibaba.datax.common.spi.ErrorCode; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:OpenTSDB Reader Error Code - * - * @author Benedict Jin - * @since 2019-04-18 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . public enum OpenTSDBReaderErrorCode implements ErrorCode { REQUIRED_VALUE("OpenTSDBReader-00", "缺失必要的值"), diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/HttpUtils.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/HttpUtils.java index cdf5c9c1ab..fa82b63458 100644 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/HttpUtils.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/HttpUtils.java @@ -1,6 +1,6 @@ package com.alibaba.datax.plugin.reader.util; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import org.apache.http.client.fluent.Content; import org.apache.http.client.fluent.Request; import org.apache.http.entity.ContentType; @@ -9,14 +9,30 @@ import java.util.Map; import java.util.concurrent.TimeUnit; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:HttpUtils - * - * @author Benedict Jin - * @since 2019-03-29 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . public final class HttpUtils { public final static Charset UTF_8 = Charset.forName("UTF-8"); diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/TSDBUtils.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/TSDBUtils.java index 72c7fd62c3..9f1e38d55a 100644 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/TSDBUtils.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/TSDBUtils.java @@ -1,20 +1,36 @@ package com.alibaba.datax.plugin.reader.util; import com.alibaba.datax.plugin.reader.conn.DataPoint4TSDB; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.List; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:TSDB Utils - * - * @author Benedict Jin - * @since 2019-03-29 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . public final class TSDBUtils { private static final Logger LOG = LoggerFactory.getLogger(TSDBUtils.class); diff --git a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/TimeUtils.java b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/TimeUtils.java index 9bc11b3630..7d6bd11255 100644 --- a/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/TimeUtils.java +++ b/opentsdbreader/src/main/java/com/alibaba/datax/plugin/reader/util/TimeUtils.java @@ -2,14 +2,30 @@ import java.util.concurrent.TimeUnit; -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:TimeUtils - * - * @author Benedict Jin - * @since 2019-04-22 - */ +//This file is part of OpenTSDB. + +//Copyright (C) 2010-2012 The OpenTSDB Authors. +//Copyright(C)2019 Alibaba Group Holding Ltd. + +// + +//This program is free software: you can redistribute it and/or modify it + +//under the terms of the GNU Lesser General Public License as published by + +//the Free Software Foundation, either version 2.1 of the License, or (at your + +//option) any later version. This program is distributed in the hope that it + +//will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty + +//of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser + +//General Public License for more details. You should have received a copy + +//of the GNU Lesser General Public License along with this program. If not, + +//see . public final class TimeUtils { private TimeUtils() { diff --git a/opentsdbreader/src/main/resources/plugin.json b/opentsdbreader/src/main/resources/plugin.json index 692a9853f8..5c9cbed980 100755 --- a/opentsdbreader/src/main/resources/plugin.json +++ b/opentsdbreader/src/main/resources/plugin.json @@ -6,5 +6,5 @@ "mechanism": "根据时间和 metric 直连底层 HBase 存储,从而 Scan 出符合条件的数据点", "warn": "指定起止时间会自动忽略分钟和秒,转为整点时刻,例如 2019-4-18 的 [3:35, 4:55) 会被转为 [3:00, 4:00)" }, - "developer": "Benedict Jin" + "developer": "alibaba" } diff --git a/oraclereader/pom.xml b/oraclereader/pom.xml index ae8e06fa44..d60e5ebf6f 100755 --- a/oraclereader/pom.xml +++ b/oraclereader/pom.xml @@ -44,8 +44,6 @@ com.oracle ojdbc6 11.2.0.3 - system - ${basedir}/src/main/lib/ojdbc6-11.2.0.3.jar diff --git a/oraclereader/src/main/assembly/package.xml b/oraclereader/src/main/assembly/package.xml index a0c9fd1c70..a954a30d5e 100755 --- a/oraclereader/src/main/assembly/package.xml +++ b/oraclereader/src/main/assembly/package.xml @@ -15,13 +15,6 @@ plugin_job_template.json plugin/reader/oraclereader - - - src/main/lib - - ojdbc6-11.2.0.3.jar - - plugin/reader/oraclereader/libs target/ diff --git a/oraclereader/src/main/lib/ojdbc6-11.2.0.3.jar b/oraclereader/src/main/lib/ojdbc6-11.2.0.3.jar deleted file mode 100644 index 01da074d5a..0000000000 Binary files a/oraclereader/src/main/lib/ojdbc6-11.2.0.3.jar and /dev/null differ diff --git a/oraclewriter/pom.xml b/oraclewriter/pom.xml index 95b78cafa3..1e8d027432 100755 --- a/oraclewriter/pom.xml +++ b/oraclewriter/pom.xml @@ -42,8 +42,6 @@ com.oracle ojdbc6 11.2.0.3 - system - ${basedir}/src/main/lib/ojdbc6-11.2.0.3.jar diff --git a/oraclewriter/src/main/assembly/package.xml b/oraclewriter/src/main/assembly/package.xml index 09a25d1a2e..9dab0c8e18 100755 --- a/oraclewriter/src/main/assembly/package.xml +++ b/oraclewriter/src/main/assembly/package.xml @@ -16,13 +16,6 @@ plugin/writer/oraclewriter - - src/main/lib - - ojdbc6-11.2.0.3.jar - - plugin/writer/oraclewriter/libs - target/ diff --git a/oraclewriter/src/main/lib/ojdbc6-11.2.0.3.jar b/oraclewriter/src/main/lib/ojdbc6-11.2.0.3.jar deleted file mode 100644 index 01da074d5a..0000000000 Binary files a/oraclewriter/src/main/lib/ojdbc6-11.2.0.3.jar and /dev/null differ diff --git a/oscarwriter/pom.xml b/oscarwriter/pom.xml new file mode 100644 index 0000000000..06249a26e9 --- /dev/null +++ b/oscarwriter/pom.xml @@ -0,0 +1,90 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + oscarwriter + oscarwriter + jar + writer data into oscar database + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + + + com.csicit.thirdparty + oscar + 1.0.1 + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + + \ No newline at end of file diff --git a/oscarwriter/src/main/assembly/package.xml b/oscarwriter/src/main/assembly/package.xml new file mode 100644 index 0000000000..2401372ec0 --- /dev/null +++ b/oscarwriter/src/main/assembly/package.xml @@ -0,0 +1,42 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/writer/oscarwriter + + + src/main/lib + + oscarJDBC.jar + + plugin/writer/oscarwriter/libs + + + target/ + + oscarwriter-0.0.1-SNAPSHOT.jar + + plugin/writer/oscarwriter + + + + + + false + plugin/writer/oscarwriter/libs + runtime + + + diff --git a/oscarwriter/src/main/java/com/alibaba/datax/plugin/writer/oscarwriter/OscarWriter.java b/oscarwriter/src/main/java/com/alibaba/datax/plugin/writer/oscarwriter/OscarWriter.java new file mode 100644 index 0000000000..0602bb4473 --- /dev/null +++ b/oscarwriter/src/main/java/com/alibaba/datax/plugin/writer/oscarwriter/OscarWriter.java @@ -0,0 +1,90 @@ +package com.alibaba.datax.plugin.writer.oscarwriter; + +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter; + +import java.util.List; + +public class OscarWriter extends Writer { + private static final DataBaseType DATABASE_TYPE = DataBaseType.Oscar; + + public static class Job extends Writer.Job { + private Configuration originalConfig = null; + private CommonRdbmsWriter.Job commonRdbmsWriterJob; + + @Override + public void preCheck() { + this.init(); + this.commonRdbmsWriterJob.writerPreCheck(this.originalConfig, DATABASE_TYPE); + } + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + + this.commonRdbmsWriterJob = new CommonRdbmsWriter.Job( + DATABASE_TYPE); + this.commonRdbmsWriterJob.init(this.originalConfig); + } + + @Override + public void prepare() { + this.commonRdbmsWriterJob.prepare(this.originalConfig); + } + + @Override + public List split(int mandatoryNumber) { + return this.commonRdbmsWriterJob.split(this.originalConfig, + mandatoryNumber); + } + + @Override + public void post() { + this.commonRdbmsWriterJob.post(this.originalConfig); + } + + @Override + public void destroy() { + this.commonRdbmsWriterJob.destroy(this.originalConfig); + } + + } + + public static class Task extends Writer.Task { + private Configuration writerSliceConfig; + private CommonRdbmsWriter.Task commonRdbmsWriterTask; + + @Override + public void init() { + this.writerSliceConfig = super.getPluginJobConf(); + this.commonRdbmsWriterTask = new CommonRdbmsWriter.Task(DATABASE_TYPE); + this.commonRdbmsWriterTask.init(this.writerSliceConfig); + } + + @Override + public void prepare() { + this.commonRdbmsWriterTask.prepare(this.writerSliceConfig); + } + + @Override + public void startWrite(RecordReceiver recordReceiver) { + this.commonRdbmsWriterTask.startWrite(recordReceiver, + this.writerSliceConfig, super.getTaskPluginCollector()); + } + + @Override + public void post() { + this.commonRdbmsWriterTask.post(this.writerSliceConfig); + } + + @Override + public void destroy() { + this.commonRdbmsWriterTask.destroy(this.writerSliceConfig); + } + + } + +} diff --git a/oscarwriter/src/main/java/com/alibaba/datax/plugin/writer/oscarwriter/OscarWriterErrorCode.java b/oscarwriter/src/main/java/com/alibaba/datax/plugin/writer/oscarwriter/OscarWriterErrorCode.java new file mode 100644 index 0000000000..7ae21576e3 --- /dev/null +++ b/oscarwriter/src/main/java/com/alibaba/datax/plugin/writer/oscarwriter/OscarWriterErrorCode.java @@ -0,0 +1,31 @@ +package com.alibaba.datax.plugin.writer.oscarwriter; + +import com.alibaba.datax.common.spi.ErrorCode; + +public enum OscarWriterErrorCode implements ErrorCode { + ; + + private final String code; + private final String describe; + + private OscarWriterErrorCode(String code, String describe) { + this.code = code; + this.describe = describe; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.describe; + } + + @Override + public String toString() { + return String.format("Code:[%s], Describe:[%s]. ", this.code, + this.describe); + } +} diff --git a/oscarwriter/src/main/resources/plugin.json b/oscarwriter/src/main/resources/plugin.json new file mode 100644 index 0000000000..43adfbfeab --- /dev/null +++ b/oscarwriter/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "oscarwriter", + "class": "com.alibaba.datax.plugin.writer.oscarwriter.OscarWriter", + "description": "useScene: prod. mechanism: Jdbc connection using the database, execute insert sql. warn: The more you know about the database, the less problems you encounter.", + "developer": "alibaba" +} \ No newline at end of file diff --git a/oscarwriter/src/main/resources/plugin_job_template.json b/oscarwriter/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..3c5e0707c2 --- /dev/null +++ b/oscarwriter/src/main/resources/plugin_job_template.json @@ -0,0 +1,15 @@ +{ + "name": "oscarwriter", + "parameter": { + "username": "", + "password": "", + "column": [], + "preSql": [], + "connection": [ + { + "jdbcUrl": "", + "table": [] + } + ] + } +} \ No newline at end of file diff --git a/ossreader/doc/ossreader.md b/ossreader/doc/ossreader.md index e0259a2a58..51d757bc27 100644 --- a/ossreader/doc/ossreader.md +++ b/ossreader/doc/ossreader.md @@ -26,6 +26,8 @@ OSSReader实现了从OSS读取数据并转为DataX协议的功能,OSS本身是 6. 多个object可以支持并发读取。 +7. 支持读取 parquet orc 文件 + 我们暂时不能做到: 1. 单个Object(File)支持多线程并发读取,这里涉及到单个Object内部切分算法。二期考虑支持。 @@ -37,7 +39,7 @@ OSSReader实现了从OSS读取数据并转为DataX协议的功能,OSS本身是 ### 3.1 配置样例 - +读取 txt, csv 格式样例 ```json { "job": { @@ -80,6 +82,63 @@ OSSReader实现了从OSS读取数据并转为DataX协议的功能,OSS本身是 } } ``` +读取 orc 格式样例 +```json +{ + "stepType": "oss", + "parameter": { + "endpoint": "http://oss.aliyuncs.com", + "accessId": "", + "accessKey": "", + "bucket": "myBucket", + "fileFormat": "orc", + "path": "/tests/case61/orc__691b6815_9260_4037_9899_****", + "column": [ + { + "index": 0, + "type": "long" + }, + { + "index": "1", + "type": "string" + }, + { + "index": "2", + "type": "string" + } + ] + } +} +``` +读取 parquet 格式样例 +```json +{ + "stepType": "oss", + "parameter": { + "endpoint": "http://oss.aliyuncs.com", + "accessId": "", + "accessKey": "", + "bucket": "myBucket", + "fileFormat": "parquet", + "path": "/parquet", + "parquetSchema":"message m { optional BINARY registration_dttm (UTF8); optional Int64 id; optional BINARY first_name (UTF8); optional BINARY last_name (UTF8); optional BINARY email (UTF8); optional BINARY gender (UTF8); optional BINARY ip_address (UTF8); optional BINARY cc (UTF8); optional BINARY country (UTF8); optional BINARY birthdate (UTF8); optional DOUBLE salary; optional BINARY title (UTF8); optional BINARY comments (UTF8); }", + "column": [ + { + "index": 0, + "type": "long" + }, + { + "index": "1", + "type": "string" + }, + { + "index": "2", + "type": "string" + } + ] + } +} +``` ### 3.2 参数说明 diff --git a/ossreader/pom.xml b/ossreader/pom.xml index 1feb42ff9e..d27b6a3dfb 100755 --- a/ossreader/pom.xml +++ b/ossreader/pom.xml @@ -11,6 +11,17 @@ jar + + org.apache.logging.log4j + log4j-api + 2.17.1 + + + + org.apache.logging.log4j + log4j-core + 2.17.1 + com.alibaba.datax datax-common @@ -43,13 +54,19 @@ com.aliyun.oss aliyun-sdk-oss - 2.2.3 + 3.4.2 junit junit test + + com.alibaba.datax + hdfsreader + 0.0.1-SNAPSHOT + compile + diff --git a/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/Key.java b/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/Key.java index e836fbbd09..efa953437b 100755 --- a/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/Key.java +++ b/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/Key.java @@ -18,4 +18,29 @@ public class Key { public static final String CNAME = "cname"; + public static final String SUCCESS_ON_NO_Object = "successOnNoObject"; + + public static final String PROXY_HOST = "proxyHost"; + + public static final String PROXY_PORT = "proxyPort"; + + public static final String PROXY_USERNAME = "proxyUsername"; + + public static final String PROXY_PASSWORD = "proxyPassword"; + + public static final String PROXY_DOMAIN = "proxyDomain"; + + public static final String PROXY_WORKSTATION = "proxyWorkstation"; + + public static final String HDOOP_CONFIG = "hadoopConfig"; + + public static final String FS_OSS_ACCESSID = "fs.oss.accessKeyId"; + + public static final String FS_OSS_ACCESSKEY = "fs.oss.accessKeySecret"; + + public static final String FS_OSS_ENDPOINT = "fs.oss.endpoint"; + + /*判断分片是否均匀的标准,是否有分片长度超出平均值的百分比*/ + public static final String BALANCE_THRESHOLD = "balanceThreshold"; + } diff --git a/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/OssInputStream.java b/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/OssInputStream.java new file mode 100644 index 0000000000..a43146e7cf --- /dev/null +++ b/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/OssInputStream.java @@ -0,0 +1,132 @@ +package com.alibaba.datax.plugin.reader.ossreader; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.RetryUtil; +import com.aliyun.oss.OSSClient; +import com.aliyun.oss.model.GetObjectRequest; +import com.aliyun.oss.model.OSSObject; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.concurrent.Callable; + +/** + * @Author: guxuan + * @Date 2022-05-17 15:52 + */ +public class OssInputStream extends InputStream { + + private final OSSClient ossClient; + private GetObjectRequest getObjectRequest; + + private long startIndex = 0; + private long endIndex = -1; + + private InputStream inputStream; + + /** + * retryTimes : 重试次数, 默认值是60次; + * description: 能够cover住的网络断连时间= retryTimes*(socket_timeout+sleepTime); + * 默认cover住的网络断连时间= 60*(5+5) = 600秒. + */ + private int retryTimes = 60; + + private static final Logger LOG = LoggerFactory.getLogger(OssInputStream.class); + + /** + * 如果start为0, end为1000, inputstream范围是[0,1000],共1001个字节 + * + * @param ossClient + * @param bucket + * @param object + * @param start inputstream start index + * @param end inputstream end index + */ + public OssInputStream(final OSSClient ossClient, final String bucket, final String object, long start, long end) { + this.ossClient = ossClient; + this.getObjectRequest = new GetObjectRequest(bucket, object); + this.startIndex = start; + this.getObjectRequest.setRange(this.startIndex, end); + this.endIndex = end; + try { + RetryUtil.executeWithRetry(new Callable() { + @Override + public Boolean call() throws Exception { + OSSObject ossObject = ossClient.getObject(getObjectRequest); + // 读取InputStream + inputStream = ossObject.getObjectContent(); + return true; + } + }, this.retryTimes, 5000, false); + } catch (Exception e) { + throw DataXException.asDataXException( + OssReaderErrorCode.RUNTIME_EXCEPTION,e.getMessage(), e); + } + } + + public OssInputStream(final OSSClient ossClient, final String bucket, final String object) { + this.ossClient = ossClient; + this.getObjectRequest = new GetObjectRequest(bucket, object); + this.getObjectRequest.setRange(startIndex, -1); + try { + RetryUtil.executeWithRetry(new Callable() { + @Override + public Boolean call() throws Exception { + OSSObject ossObject = ossClient.getObject(getObjectRequest); + // 读取InputStream + inputStream = ossObject.getObjectContent(); + return true; + } + }, this.retryTimes, 5000, false); + } catch (Exception e) { + throw DataXException.asDataXException( + OssReaderErrorCode.RUNTIME_EXCEPTION, e.getMessage(), e); + } + } + + @Override + public int read() throws IOException { + int cbyte; + try { + cbyte = RetryUtil.executeWithRetry(new Callable() { + @Override + public Integer call() throws Exception { + try { + int c = inputStream.read(); + startIndex++; + return c; + } catch (Exception e) { + LOG.warn(e.getMessage(),e); + /** + * 必须将inputStream先关闭, 否则会造成连接泄漏 + */ + IOUtils.closeQuietly(inputStream); + // getOssRangeInuptStream时,如果网络不连通,则会抛出异常,RetryUtil捕获异常进行重试 + inputStream = getOssRangeInuptStream(startIndex); + int c = inputStream.read(); + startIndex++; + return c; + } + } + }, this.retryTimes,5000, false); + return cbyte; + } catch (Exception e) { + throw DataXException.asDataXException( + OssReaderErrorCode.RUNTIME_EXCEPTION, e.getMessage(), e); + } + } + + private InputStream getOssRangeInuptStream(final long startIndex) { + LOG.info("Start to retry reading [inputStream] from Byte {}", startIndex); + // 第二个参数值设为-1,表示不设置结束的字节位置,读取startIndex及其以后的所有数据 + getObjectRequest.setRange(startIndex, this.endIndex); + // 范围下载 + OSSObject ossObject = ossClient.getObject(getObjectRequest); + // 读取InputStream + LOG.info("Start to retry reading [inputStream] from Byte {}", startIndex); + return ossObject.getObjectContent(); + } +} diff --git a/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/OssReader.java b/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/OssReader.java index ce4f0875b4..9b76c53e4a 100755 --- a/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/OssReader.java +++ b/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/OssReader.java @@ -4,33 +4,37 @@ import com.alibaba.datax.common.plugin.RecordSender; import com.alibaba.datax.common.spi.Reader; import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.hdfsreader.HdfsReader; +import com.alibaba.datax.plugin.reader.ossreader.util.HdfsParquetUtil; +import com.alibaba.datax.plugin.reader.ossreader.util.OssSplitUtil; import com.alibaba.datax.plugin.reader.ossreader.util.OssUtil; +import com.alibaba.datax.plugin.unstructuredstorage.FileFormat; import com.alibaba.datax.plugin.unstructuredstorage.reader.UnstructuredStorageReaderUtil; +import com.alibaba.datax.plugin.unstructuredstorage.reader.binaryFileUtil.BinaryFileReaderUtil; +import com.alibaba.datax.plugin.unstructuredstorage.reader.split.StartEndPair; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; import com.aliyun.oss.ClientException; import com.aliyun.oss.OSSClient; import com.aliyun.oss.OSSException; import com.aliyun.oss.model.ListObjectsRequest; -import com.aliyun.oss.model.OSSObject; import com.aliyun.oss.model.OSSObjectSummary; import com.aliyun.oss.model.ObjectListing; -import com.google.common.collect.Sets; -import org.apache.commons.io.Charsets; +import com.aliyun.oss.model.ObjectMetadata; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.InputStream; -import java.nio.charset.UnsupportedCharsetException; import java.util.ArrayList; import java.util.List; -import java.util.Set; +import java.util.Locale; import java.util.regex.Pattern; -/** - * Created by mengxin.liumx on 2014/12/7. - */ public class OssReader extends Reader { public static class Job extends Reader.Job { private static final Logger LOG = LoggerFactory @@ -38,194 +42,204 @@ public static class Job extends Reader.Job { private Configuration readerOriginConfig = null; + private OSSClient ossClient = null; + private String endpoint; + private String accessId; + private String accessKey; + private String bucket; + private boolean successOnNoObject; + private Boolean isBinaryFile; + + private List objects; + private List> objectSizePairs; /*用于任务切分的依据*/ + + private String fileFormat; + + private HdfsReader.Job hdfsReaderJob; + private boolean useHdfsReaderProxy = false; + @Override public void init() { LOG.debug("init() begin..."); this.readerOriginConfig = this.getPluginJobConf(); + this.basicValidateParameter(); + this.fileFormat = this.readerOriginConfig.getString(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.FILE_FORMAT, + com.alibaba.datax.plugin.unstructuredstorage.reader.Constant.DEFAULT_FILE_FORMAT); + this.useHdfsReaderProxy = HdfsParquetUtil.isUseHdfsWriterProxy(this.fileFormat); + if(useHdfsReaderProxy){ + HdfsParquetUtil.adaptConfiguration(this.readerOriginConfig); + this.hdfsReaderJob = new HdfsReader.Job(); + this.hdfsReaderJob.setJobPluginCollector(this.getJobPluginCollector()); + this.hdfsReaderJob.setPeerPluginJobConf(this.getPeerPluginJobConf()); + this.hdfsReaderJob.setPeerPluginName(this.getPeerPluginName()); + this.hdfsReaderJob.setPluginJobConf(this.getPluginJobConf()); + this.hdfsReaderJob.init(); + return; + } + + this.isBinaryFile = FileFormat.getFileFormatByConfiguration(this.readerOriginConfig).isBinary(); this.validate(); + UnstructuredStorageReaderUtil.validateCsvReaderConfig(this.readerOriginConfig); + this.successOnNoObject = this.readerOriginConfig.getBool( + Key.SUCCESS_ON_NO_Object, false); LOG.debug("init() ok and end..."); } - private void validate() { - String endpoint = this.readerOriginConfig.getString(Key.ENDPOINT); + + private void basicValidateParameter(){ + endpoint = this.readerOriginConfig.getString(Key.ENDPOINT); if (StringUtils.isBlank(endpoint)) { throw DataXException.asDataXException( - OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, - "您需要指定 endpoint"); + OssReaderErrorCode.CONFIG_INVALID_EXCEPTION,"invalid endpoint"); } - String accessId = this.readerOriginConfig.getString(Key.ACCESSID); + accessId = this.readerOriginConfig.getString(Key.ACCESSID); if (StringUtils.isBlank(accessId)) { throw DataXException.asDataXException( OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, - "您需要指定 accessId"); + "invalid accessId"); } - String accessKey = this.readerOriginConfig.getString(Key.ACCESSKEY); + accessKey = this.readerOriginConfig.getString(Key.ACCESSKEY); if (StringUtils.isBlank(accessKey)) { throw DataXException.asDataXException( OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, - "您需要指定 accessKey"); + "invalid accessKey"); } + } + // warn: 提前验证endpoint,accessId,accessKey,bucket,object的有效性 + private void validate() { + // fxxk + // ossClient = new OSSClient(endpoint,accessId,accessKey); + ossClient = OssUtil.initOssClient(this.readerOriginConfig); + - String bucket = this.readerOriginConfig.getString(Key.BUCKET); + bucket = this.readerOriginConfig.getString(Key.BUCKET); if (StringUtils.isBlank(bucket)) { throw DataXException.asDataXException( OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, - "您需要指定 endpoint"); - } - - String object = this.readerOriginConfig.getString(Key.OBJECT); - if (StringUtils.isBlank(object)) { + "invalid bucket"); + }else if(!ossClient.doesBucketExist(bucket)){ throw DataXException.asDataXException( OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, - "您需要指定 object"); + "invalid bucket"); } - String fieldDelimiter = this.readerOriginConfig - .getString(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.FIELD_DELIMITER); - // warn: need length 1 - if (null == fieldDelimiter || fieldDelimiter.length() == 0) { + String object = this.readerOriginConfig.getString(Key.OBJECT); + if (StringUtils.isBlank(object)) { throw DataXException.asDataXException( OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, - "您需要指定 fieldDelimiter"); + "invalid object"); } - String encoding = this.readerOriginConfig - .getString( - com.alibaba.datax.plugin.unstructuredstorage.reader.Key.ENCODING, - com.alibaba.datax.plugin.unstructuredstorage.reader.Constant.DEFAULT_ENCODING); - try { - Charsets.toCharset(encoding); - } catch (UnsupportedCharsetException uce) { - throw DataXException.asDataXException( - OssReaderErrorCode.ILLEGAL_VALUE, - String.format("不支持的编码格式 : [%s]", encoding), uce); - } catch (Exception e) { - throw DataXException.asDataXException( - OssReaderErrorCode.ILLEGAL_VALUE, - String.format("运行配置异常 : %s", e.getMessage()), e); + if (this.isBinaryFile){ + return; } + UnstructuredStorageReaderUtil.validateParameter(this.readerOriginConfig); + } - // 检测是column 是否为 ["*"] 若是则填为空 - List column = this.readerOriginConfig - .getListConfiguration(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.COLUMN); - if (null != column - && 1 == column.size() - && ("\"*\"".equals(column.get(0).toString()) || "'*'" - .equals(column.get(0).toString()))) { - readerOriginConfig - .set(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.COLUMN, - new ArrayList()); - } else { - // column: 1. index type 2.value type 3.when type is Data, may - // have - // format - List columns = this.readerOriginConfig - .getListConfiguration(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.COLUMN); - - if (null == columns || columns.size() == 0) { - throw DataXException.asDataXException( - OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, - "您需要指定 columns"); - } - - if (null != columns && columns.size() != 0) { - for (Configuration eachColumnConf : columns) { - eachColumnConf - .getNecessaryValue( - com.alibaba.datax.plugin.unstructuredstorage.reader.Key.TYPE, - OssReaderErrorCode.REQUIRED_VALUE); - Integer columnIndex = eachColumnConf - .getInt(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.INDEX); - String columnValue = eachColumnConf - .getString(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.VALUE); - - if (null == columnIndex && null == columnValue) { - throw DataXException.asDataXException( - OssReaderErrorCode.NO_INDEX_VALUE, - "由于您配置了type, 则至少需要配置 index 或 value"); - } - - if (null != columnIndex && null != columnValue) { - throw DataXException.asDataXException( - OssReaderErrorCode.MIXED_INDEX_VALUE, - "您混合配置了index, value, 每一列同时仅能选择其中一种"); - } - - } - } - } - // only support compress: gzip,bzip2,zip - String compress = this.readerOriginConfig - .getString(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.COMPRESS); - if (StringUtils.isBlank(compress)) { - this.readerOriginConfig - .set(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.COMPRESS, - null); - } else { - Set supportedCompress = Sets - .newHashSet("gzip", "bzip2", "zip"); - compress = compress.toLowerCase().trim(); - if (!supportedCompress.contains(compress)) { - throw DataXException - .asDataXException( - OssReaderErrorCode.ILLEGAL_VALUE, - String.format( - "仅支持 gzip, bzip2, zip 文件压缩格式 , 不支持您配置的文件压缩格式: [%s]", - compress)); - } - this.readerOriginConfig - .set(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.COMPRESS, - compress); - } - } @Override public void prepare() { - LOG.debug("prepare()"); + if(useHdfsReaderProxy){ + this.hdfsReaderJob.prepare(); + return; + } + // 将每个单独的 object 作为一个 slice + this.objectSizePairs = parseOriginObjectSizePairs(readerOriginConfig.getList(Key.OBJECT, String.class)); + this.objects = parseOriginObjects(readerOriginConfig.getList(Key.OBJECT, String.class)); + UnstructuredStorageReaderUtil.setSourceFileName(readerOriginConfig, this.objects); + UnstructuredStorageReaderUtil.setSourceFile(readerOriginConfig, this.objects); } @Override public void post() { + if(useHdfsReaderProxy){ + this.hdfsReaderJob.post(); + return; + } LOG.debug("post()"); } @Override public void destroy() { + if(useHdfsReaderProxy){ + this.hdfsReaderJob.destroy(); + return; + } LOG.debug("destroy()"); } @Override public List split(int adviceNumber) { LOG.debug("split() begin..."); - List readerSplitConfigs = new ArrayList(); + if(useHdfsReaderProxy){ + return hdfsReaderJob.split(adviceNumber); + } + List readerSplitConfigs; - // 将每个单独的 object 作为一个 slice - List objects = parseOriginObjects(readerOriginConfig - .getList(Constant.OBJECT, String.class)); - if (0 == objects.size()) { + if (0 == objects.size() && this.successOnNoObject) { + readerSplitConfigs = new ArrayList(); + Configuration splitedConfig = this.readerOriginConfig.clone(); + splitedConfig.set(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.SPLIT_SLICE_CONFIG, null); + readerSplitConfigs.add(splitedConfig); + LOG.info(String.format("no OSS object to be read")); + LOG.debug("split() ok and end..."); + return readerSplitConfigs; + }else if (0 == objects.size()) { throw DataXException.asDataXException( OssReaderErrorCode.EMPTY_BUCKET_EXCEPTION, - String.format( - "未能找到待读取的Object,请确认您的配置项bucket: %s object: %s", + String.format("Unable to find the object to read. Please confirm your configured item [bucket]: %s object: %s", this.readerOriginConfig.get(Key.BUCKET), this.readerOriginConfig.get(Key.OBJECT))); } - for (String object : objects) { - Configuration splitedConfig = this.readerOriginConfig.clone(); - splitedConfig.set(Constant.OBJECT, object); - readerSplitConfigs.add(splitedConfig); - LOG.info(String.format("OSS object to be read:%s", object)); + /** + * 当文件类型是text纯文本文件,并且不是压缩的情况下, + * 可以对纯文本文件进行内部切分实现并发读取, 如果用户不希望对文件拆分, 可以指定fileFormat为csv + * + * 注意:这里判断文件是否为text以及是否压缩,信息都是通过任务配置项来获取的 + * + * 这里抽出一个方法来判断是否需要分片 + * */ + OssSplitUtil ossFileSplit = new OssSplitUtil(this.ossClient, this.bucket); + long t1 = System.currentTimeMillis(); + readerSplitConfigs = ossFileSplit.getSplitedConfigurations(this.readerOriginConfig, this.objectSizePairs, + adviceNumber); + long t2 = System.currentTimeMillis(); + LOG.info("all split done, cost {}ms", t2 - t1); + /** + * 在日志中告知用户,为什么实际datax切分跑的channel数会小于用户配置的channel数 + * 注意:这里的报告的原因不准确,报的原因是一个文件一个task,所以最终切分数小于用户配置数,实际原因还有单文件切分时, + * 单文件的大小太小(理论64M一个block),导致问题比较少 + */ + if(readerSplitConfigs.size() < adviceNumber){ + LOG.info("[Note]: During OSSReader data synchronization, one file can only be synchronized in one task. You want to synchronize {} files " + + "and the number is less than the number of channels you configured: {}. " + + "Therefore, please take note that DataX will actually have {} sub-tasks, that is, the actual concurrent channels = {}", + objects.size(), adviceNumber, objects.size(), objects.size()); } - LOG.debug("split() ok and end..."); + LOG.info("split() ok and end..."); return readerSplitConfigs; } private List parseOriginObjects(List originObjects) { - List parsedObjects = new ArrayList(); + List objList = new ArrayList<>(); + + if (this.objectSizePairs == null) { + this.objectSizePairs = parseOriginObjectSizePairs(originObjects); + } + + for (Pair objSizePair : this.objectSizePairs) { + objList.add(objSizePair.getKey()); + } + + return objList; + } + + private List> parseOriginObjectSizePairs(List originObjects) { + List> parsedObjectSizePaires = new ArrayList>(); for (String object : originObjects) { int firstMetaChar = (object.indexOf('*') > object.indexOf('?')) ? object @@ -236,52 +250,130 @@ private List parseOriginObjects(List originObjects) { IOUtils.DIR_SEPARATOR, firstMetaChar); String parentDir = object .substring(0, lastDirSeparator + 1); - List remoteObjects = getRemoteObjects(parentDir); + List> allRemoteObjectSizePairs = getAllRemoteObjectsKeyAndSizeInDir(parentDir); Pattern pattern = Pattern.compile(object.replace("*", ".*") .replace("?", ".?")); - for (String remoteObject : remoteObjects) { - if (pattern.matcher(remoteObject).matches()) { - parsedObjects.add(remoteObject); + for (Pair remoteObjectSizePair : allRemoteObjectSizePairs) { + if (pattern.matcher(remoteObjectSizePair.getKey()).matches()) { + parsedObjectSizePaires.add(remoteObjectSizePair); + LOG.info(String + .format("add object [%s] as a candidate to be read.", + remoteObjectSizePair.getKey())); } } } else { - parsedObjects.add(object); + // 如果没有配正则匹配,那么需要对用户自己配置的object存在性进行检测 + try{ + ossClient.getObject(bucket, object); + ObjectMetadata objMeta = ossClient.getObjectMetadata(bucket, object); + parsedObjectSizePaires.add(new MutablePair(object, objMeta.getContentLength() <= OssSplitUtil.SINGLE_FILE_SPLIT_THRESHOLD_IN_SIZE ? -1L : objMeta.getContentLength())); + LOG.info(String.format( + "add object [%s] as a candidate to be read.", + object)); + }catch (Exception e){ + trackOssDetailException(e, object); + } } } - return parsedObjects; + return parsedObjectSizePaires; } - private List getRemoteObjects(String parentDir) - throws OSSException, ClientException { + // 对oss配置异常信息进行细分定位 + private void trackOssDetailException(Exception e, String object){ + // 对异常信息进行细分定位 + String errorMessage = e.getMessage(); + if(StringUtils.isNotBlank(errorMessage)){ + if(errorMessage.contains("UnknownHost")){ + // endPoint配置错误 + throw DataXException.asDataXException( + OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, + "The endpoint you configured is not correct. Please check the endpoint configuration", e); + }else if(errorMessage.contains("InvalidAccessKeyId")){ + // accessId配置错误 + throw DataXException.asDataXException( + OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, + "The accessId you configured is not correct. Please check the accessId configuration", e); + }else if(errorMessage.contains("SignatureDoesNotMatch")){ + // accessKey配置错误 + throw DataXException.asDataXException( + OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, + "The accessKey you configured is not correct. Please check the accessId configuration", e); + }else if(errorMessage.contains("NoSuchKey")){ + if (e instanceof OSSException) { + OSSException ossException = (OSSException) e; + if ("NoSuchKey".equalsIgnoreCase(ossException + .getErrorCode()) && this.successOnNoObject) { + LOG.warn(String.format("oss file %s is not exits to read:", object), e); + return; + } + } + // object配置错误 + throw DataXException.asDataXException( + OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, + "The object you configured is not correct. Please check the accessId configuration"); + }else{ + // 其他错误 + throw DataXException.asDataXException( + OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, + String.format("Please check whether the configuration of [endpoint], [accessId], [accessKey], [bucket], and [object] are correct. Error reason: %s",e.getMessage()), e); + } + }else{ + throw DataXException.asDataXException( + OssReaderErrorCode.CONFIG_INVALID_EXCEPTION, + "The configured json is invalid", e); + } + } + + private List> getAllRemoteObjectsKeyAndSizeInDir(String parentDir) + throws OSSException, ClientException{ + List> objectSizePairs = new ArrayList>(); + List objectListings = getRemoteObjectListings(parentDir); + + if (objectListings.size() == 0) { + return objectSizePairs; + } + + for (ObjectListing objectList : objectListings){ + for (OSSObjectSummary objectSummary : objectList.getObjectSummaries()) { + Pair objNameSize = new MutablePair(objectSummary.getKey(), objectSummary.getSize() <= OssSplitUtil.SINGLE_FILE_SPLIT_THRESHOLD_IN_SIZE ? -1L : objectSummary.getSize()); + objectSizePairs.add(objNameSize); + } + } - LOG.debug(String.format("父文件夹 : %s", parentDir)); + return objectSizePairs; + } + + private List getRemoteObjectListings(String parentDir) throws OSSException, ClientException { + + List remoteObjectListings = new ArrayList(); + + LOG.debug("Parent folder: {}", parentDir); List remoteObjects = new ArrayList(); OSSClient client = OssUtil.initOssClient(readerOriginConfig); + try { ListObjectsRequest listObjectsRequest = new ListObjectsRequest( readerOriginConfig.getString(Key.BUCKET)); listObjectsRequest.setPrefix(parentDir); - ObjectListing objectList; + ObjectListing remoteObjectList; do { - objectList = client.listObjects(listObjectsRequest); - for (OSSObjectSummary objectSummary : objectList - .getObjectSummaries()) { - LOG.debug(String.format("找到文件 : %s", - objectSummary.getKey())); - remoteObjects.add(objectSummary.getKey()); + remoteObjectList = client.listObjects(listObjectsRequest); + if (null != remoteObjectList) { + LOG.info("ListObjects prefix: {} requestId: {}", remoteObjectList.getPrefix(), remoteObjectList.getRequestId()); + } else { + LOG.info("ListObjectsRequest get null"); } - listObjectsRequest.setMarker(objectList.getNextMarker()); + remoteObjectListings.add(remoteObjectList); + listObjectsRequest.setMarker(remoteObjectList.getNextMarker()); LOG.debug(listObjectsRequest.getMarker()); - LOG.debug(String.valueOf(objectList.isTruncated())); - - } while (objectList.isTruncated()); - } catch (IllegalArgumentException e) { - throw DataXException.asDataXException( - OssReaderErrorCode.OSS_EXCEPTION, e.getMessage()); + LOG.debug(String.valueOf(remoteObjectList.isTruncated())); + } while (remoteObjectList.isTruncated()); + } catch (Exception e) { + trackOssDetailException(e, null); } - return remoteObjects; + return remoteObjectListings; } } @@ -289,30 +381,116 @@ public static class Task extends Reader.Task { private static Logger LOG = LoggerFactory.getLogger(Reader.Task.class); private Configuration readerSliceConfig; + private Boolean isBinaryFile; + private Integer blockSizeInByte; + private List allWorksForTask; + private boolean originSkipHeader; + private OSSClient ossClient; + private String fileFormat; + private HdfsReader.Task hdfsReaderTask; + private boolean useHdfsReaderProxy = false; + + @Override + public void init() { + this.readerSliceConfig = this.getPluginJobConf(); + this.fileFormat = this.readerSliceConfig.getString(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.FILE_FORMAT, + com.alibaba.datax.plugin.unstructuredstorage.reader.Constant.DEFAULT_FILE_FORMAT); + this.useHdfsReaderProxy = HdfsParquetUtil.isUseHdfsWriterProxy(this.fileFormat); + if(useHdfsReaderProxy){ + this.hdfsReaderTask = new HdfsReader.Task(); + this.hdfsReaderTask.setPeerPluginJobConf(this.getPeerPluginJobConf()); + this.hdfsReaderTask.setPeerPluginName(this.getPeerPluginName()); + this.hdfsReaderTask.setPluginJobConf(this.getPluginJobConf()); + this.hdfsReaderTask.setReaderPluginSplitConf(this.getReaderPluginSplitConf()); + this.hdfsReaderTask.setTaskGroupId(this.getTaskGroupId()); + this.hdfsReaderTask.setTaskId(this.getTaskId()); + this.hdfsReaderTask.setTaskPluginCollector(this.getTaskPluginCollector()); + this.hdfsReaderTask.init(); + return; + } + String allWorksForTaskStr = this.readerSliceConfig + .getString(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.SPLIT_SLICE_CONFIG); + if (StringUtils.isBlank(allWorksForTaskStr)) { + allWorksForTaskStr = "[]"; + } + this.allWorksForTask = JSON.parseObject(allWorksForTaskStr, new TypeReference>() { + }); + this.isBinaryFile = FileFormat.getFileFormatByConfiguration(this.readerSliceConfig).isBinary(); + this.blockSizeInByte = this.readerSliceConfig.getInt( + com.alibaba.datax.plugin.unstructuredstorage.reader.Key.BLOCK_SIZE_IN_BYTE, + com.alibaba.datax.plugin.unstructuredstorage.reader.Constant.DEFAULT_BLOCK_SIZE_IN_BYTE); + this.originSkipHeader = this.readerSliceConfig + .getBool(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.SKIP_HEADER, false); + } + + @Override + public void prepare() { + LOG.info("task prepare() begin..."); + if(useHdfsReaderProxy){ + this.hdfsReaderTask.prepare(); + return; + } + } + @Override public void startRead(RecordSender recordSender) { - LOG.debug("read start"); - String object = readerSliceConfig.getString(Key.OBJECT); - OSSClient client = OssUtil.initOssClient(readerSliceConfig); - - OSSObject ossObject = client.getObject( - readerSliceConfig.getString(Key.BUCKET), object); - InputStream objectStream = ossObject.getObjectContent(); - UnstructuredStorageReaderUtil.readFromStream(objectStream, object, - this.readerSliceConfig, recordSender, - this.getTaskPluginCollector()); + if(useHdfsReaderProxy){ + this.hdfsReaderTask.startRead(recordSender); + return; + } + boolean successOnNoObject = this.readerSliceConfig.getBool(Key.SUCCESS_ON_NO_Object, false); + if (this.allWorksForTask.isEmpty() && successOnNoObject) { + recordSender.flush(); + return; + } + String bucket = this.readerSliceConfig.getString(Key.BUCKET); + this.ossClient = OssUtil.initOssClient(this.readerSliceConfig); + for (StartEndPair eachSlice : this.allWorksForTask) { + String object = eachSlice.getFilePath(); + Long start = eachSlice.getStart(); + Long end = eachSlice.getEnd(); + LOG.info(String.format("read bucket=[%s] object=[%s], range: [start=%s, end=%s] start...", bucket, + object, start, end)); + InputStream ossInputStream = new OssInputStream(ossClient, bucket, object, start, end); + // 检查是否要跳过表头, 防止重复跳过首行 + Boolean skipHeaderValue = this.originSkipHeader && (0L == start); + this.readerSliceConfig.set(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.SKIP_HEADER, + skipHeaderValue); + try { + if (!this.isBinaryFile) { + UnstructuredStorageReaderUtil.readFromStream(ossInputStream, object, this.readerSliceConfig, + recordSender, this.getTaskPluginCollector()); + } else { + BinaryFileReaderUtil.readFromStream(ossInputStream, object, recordSender, this.blockSizeInByte); + } + } finally { + IOUtils.closeQuietly(ossInputStream); + } + } recordSender.flush(); } @Override - public void init() { - this.readerSliceConfig = this.getPluginJobConf(); + public void post() { + LOG.info("task post() begin..."); + if(useHdfsReaderProxy){ + this.hdfsReaderTask.post(); + return; + } } @Override public void destroy() { - + if(useHdfsReaderProxy){ + this.hdfsReaderTask.destroy(); + return; + } + try { + // this.ossClient.shutdown(); + } catch (Exception e) { + LOG.warn("shutdown ossclient meet a exception:" + e.getMessage(), e); + } } } } diff --git a/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/util/HdfsParquetUtil.java b/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/util/HdfsParquetUtil.java new file mode 100644 index 0000000000..3012c84a72 --- /dev/null +++ b/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/util/HdfsParquetUtil.java @@ -0,0 +1,40 @@ +package com.alibaba.datax.plugin.reader.ossreader.util; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.ossreader.Key; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; + +/** + * @Author: guxuan + * @Date 2022-05-17 15:46 + */ +public class HdfsParquetUtil { + public static boolean isUseHdfsWriterProxy( String fileFormat){ + if("orc".equalsIgnoreCase(fileFormat) || "parquet".equalsIgnoreCase(fileFormat)){ + return true; + } + return false; + } + + /** + * 配置readerOriginConfig 适配hdfsreader读取oss parquet + * https://help.aliyun.com/knowledge_detail/74344.html + * @param readerOriginConfig + */ + public static void adaptConfiguration(Configuration readerOriginConfig){ + String bucket = readerOriginConfig.getString(Key.BUCKET); + String fs =String.format("oss://%s",bucket); + readerOriginConfig.set(com.alibaba.datax.plugin.reader.hdfsreader.Key.DEFAULT_FS,fs); + readerOriginConfig.set(com.alibaba.datax.plugin.reader.hdfsreader.Key.FILETYPE, + readerOriginConfig.getString(com.alibaba.datax.plugin.unstructuredstorage.writer.Key.FILE_FORMAT)); + /** + * "path"、 "column" 相互一致 + */ + JSONObject hadoopConfig = new JSONObject(); + hadoopConfig.put(Key.FS_OSS_ACCESSID,readerOriginConfig.getString(Key.ACCESSID)); + hadoopConfig.put(Key.FS_OSS_ACCESSKEY,readerOriginConfig.getString(Key.ACCESSKEY)); + hadoopConfig.put(Key.FS_OSS_ENDPOINT,readerOriginConfig.getString(Key.ENDPOINT)); + readerOriginConfig.set(Key.HDOOP_CONFIG,Configuration.from(JSON.toJSONString(hadoopConfig))); + } +} diff --git a/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/util/OssSplitUtil.java b/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/util/OssSplitUtil.java new file mode 100644 index 0000000000..6ba80999d9 --- /dev/null +++ b/ossreader/src/main/java/com/alibaba/datax/plugin/reader/ossreader/util/OssSplitUtil.java @@ -0,0 +1,485 @@ +package com.alibaba.datax.plugin.reader.ossreader.util; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.ossreader.OssInputStream; +import com.alibaba.datax.plugin.unstructuredstorage.reader.Key; +import com.alibaba.datax.plugin.unstructuredstorage.reader.UnstructuredStorageReaderErrorCode; +import com.alibaba.datax.plugin.unstructuredstorage.reader.split.StartEndPair; +import com.alibaba.datax.plugin.unstructuredstorage.reader.split.UnstructuredSplitUtil; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import com.aliyun.oss.OSSClient; +import com.aliyun.oss.model.GetObjectRequest; +import com.aliyun.oss.model.OSSObject; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +/** + * @Author: guxuan + * @Date 2022-05-17 15:48 + */ +public class OssSplitUtil extends UnstructuredSplitUtil { + + private static final Logger LOG = LoggerFactory.getLogger(UnstructuredSplitUtil.class); + public static final Long SINGLE_FILE_SPLIT_THRESHOLD_IN_SIZE = 64 * 1024 * 1024L; // 小于 1MB 的文件不做内部切分 + private OSSClient ossClient; + private String bucketName; + private Double balanceThreshold; + private Long avgLen = -1L; + private Integer splitGroupNum = -1; + + public OssSplitUtil(OSSClient ossClient, String bucketName) { + super(false); + this.ossClient = ossClient; + this.bucketName = bucketName; + } + + @Override + public Long getFileTotalLength(String filePath) { + // 获取object字节总数 + GetObjectRequest getObjectRequest = new GetObjectRequest(this.bucketName, filePath); + OSSObject ossObject = this.ossClient.getObject(getObjectRequest); + return ossObject.getObjectMetadata().getContentLength(); + } + + @Override + public InputStream getFileInputStream(StartEndPair startEndPair) { + InputStream inputStream = new OssInputStream(this.ossClient, this.bucketName, startEndPair.getFilePath(), + startEndPair.getStart(), startEndPair.getEnd()); + return inputStream; + } + + private Boolean canSplitSingleFile(Configuration jobConfig) { + Boolean enableInnerSplit = jobConfig.getBool(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.ENABLE_INNER_SPLIT, true); + if (!enableInnerSplit) { + return false; + } + + // 默认不切分 + String fileFormat = jobConfig.getString(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.FILE_FORMAT, + com.alibaba.datax.plugin.unstructuredstorage.reader.Constant.DEFAULT_FILE_FORMAT); + String compressType = jobConfig.getString(com.alibaba.datax.plugin.unstructuredstorage.reader.Key.COMPRESS); + + // 如果不满足"是text格式且非压缩文件",则直接返回false + if (! StringUtils.equalsIgnoreCase(fileFormat, com.alibaba.datax.plugin.unstructuredstorage.reader.Constant.FILE_FORMAT_TEXT) || + ! StringUtils.isBlank(compressType)) { + return false; + } + + // todo: 判断文件是否为软连接文件,如果为软连接文件,则不支持内部切分 + + return true; + } + + private boolean isGroupsBalance(List groups) { + assert (groups != null); + + if(groups.size() <= 1) { + return true; + } + + double avg = (double) this.avgLen * (1.0 + this.balanceThreshold/100); + for (Group group : groups) { + if(group.getFilledLenght() > avg) { + return false; + } + } + return true; + } + + /* + * 把 allObjectKeySizePares 分成 N 组,尽量使得各组中文件 size 之和 近似 + * */ + private List splitObjectToGroups(List> allObjKeySizePares, Integer N) { + List groups; + + // 若文件数 <= N,则每个文件分一个组 + if(allObjKeySizePares.size() <= N) { + groups = new ArrayList<>(); + int index = 0; + for (Pair pair : allObjKeySizePares) { + // capacity 初始化为avgLen + Group group = new Group(avgLen); + FileBlock fileBlock = new FileBlock(pair.getKey(), 0L, pair.getValue() - 1); + group.fill(fileBlock); + groups.add(group); + } + + // 文件不足N,则以空group补全 + for (int i = groups.size(); i < N; i++) { + groups.add(new Group(avgLen)); + } + + return groups; + } + + //文件数量 > N + //对 allObjKeySizePairs 按照 size 从大到小排序 + allObjKeySizePares.sort(new Comparator>() { + @Override + public int compare(Pair o1, Pair o2) { + if (o1.getValue().compareTo(o2.getValue()) < 0) { + return 1; + } + if (o1.getValue().equals(o2.getValue())) { + return 0; + } + return -1; + } + }); + + groups = new ArrayList<>(N); + + for (int i = 0; i < N; i++) { + Group group = new Group(avgLen); + groups.add(group); + } + + for (Pair pair : allObjKeySizePares) { + FileBlock fileBlock = new FileBlock(pair.getKey(), 0L, pair.getValue() - 1); + + // 对于avgLen < 0 的极端情况,直接将文件按照数量均分到各个group + if (avgLen > 0 && pair.getValue() >= avgLen) { + // 若果文件size > avgLen,则独立成组(放在一个空的group中 + for (int index = 0; index < N; index++) { + if (groups.get(index).isEmpty()) { + groups.get(index).fill(fileBlock); + break; + } + } + } else { + // 如果文件小于平均长度,则将其放在一个当前能够容纳,且容量最接近的 group 中 + int selectedIndex = 0, index = 0; + // 先找到第一个能容纳的 + for (; index < N; index++) { + if (groups.get(index).getCapacity() >= fileBlock.getSize()) { + selectedIndex = index; + } + } + // 找到能容纳且剩余容量最小的 + for (;index < N; index++) { + if (groups.get(index).getCapacity() >= fileBlock.getSize() + && groups.get(index).getCapacity() < groups.get(selectedIndex).getCapacity()) { + selectedIndex = index; + } + } + groups.get(selectedIndex).fill(fileBlock); + } + + } + + return groups; + } + + private void reBalanceGroup(List groups) { + LOG.info("reBalance start"); + assert (groups != null && groups.size() > 0); + // 对某些group内部的文件进行进一步切分 + /* 1. 选出负载最小和最大的组 */ + Group groupMinLoad = groups.get(0); + Group groupMaxLoad = groups.get(0); + for (Group group : groups) { + if (group.getFilledLenght() > groupMaxLoad.getFilledLenght()) { + groupMaxLoad = group; + } + + if (group.getFilledLenght() < groupMinLoad.getFilledLenght()) { + groupMinLoad = group; + } + } + + /* 2. 将 groupMaxLoad 最大文件切分出部分放入 groupMinLoad + * 大小为 min{grouMaxLoad.length - mean, mean - groupMinLoad.length} */ + Long splitLen = Math.min(groupMinLoad.getCapacity(), groupMaxLoad.getOverloadLength()); + FileBlock splitOutBlock = groupMaxLoad.split(splitLen, this.ossClient, this.bucketName); + + groupMinLoad.fill(splitOutBlock); + LOG.info("reBalance end"); + } + + private Long getTotoalLenOfObjList(List> objKeySizePares) { + Long totalLen = 0L; + for (Pair pair : objKeySizePares) { + totalLen += (pair.getValue() < 0 ? 1 : pair.getValue()); + } + + return totalLen; + } + + public List getSplitedConfigurations(Configuration originConfiguration, List> objKeySizePares, + int adviceNumber) { + List configurationList = new ArrayList<>(); + + this.splitGroupNum = adviceNumber; + this.avgLen = (long)Math.ceil((double)this.getTotoalLenOfObjList(objKeySizePares) / this.splitGroupNum); + this.balanceThreshold = originConfiguration.getDouble(com.alibaba.datax.plugin.reader.ossreader.Key.BALANCE_THRESHOLD, 10.0); + + List groups = this.splitObjectToGroups(objKeySizePares, this.splitGroupNum); + + // 划分后,各个组间如果长度确实比较近似,则不需要进一步对单个文件进行内部切分,反之,则需要对单个文件进行内部切分以进行进一步的调整 + if (canSplitSingleFile(originConfiguration)) { + // 防止文件内部单行过大,对循环加以限制,理论上最多只需要调整 splitGroupNum 次 + Integer i = 0; + Long timeStart = System.currentTimeMillis(); + while (i++ < splitGroupNum && ! this.isGroupsBalance(groups)) { + this.reBalanceGroup(groups); + } + Long timeEnd = System.currentTimeMillis(); + LOG.info("split groups cost {} ms", timeEnd - timeStart); + } + + LOG.info("Splited gourps:\n"); + for (Group group : groups) { + LOG.info(group.toString()); + } + + // 根据Groups划分结果初始化各个分片任务配置 + for (Group group : groups) { + Configuration configuration = originConfiguration.clone(); + // 根据groups初始化分片 + List startEndPairs = new ArrayList<>(); + for (FileBlock fileBlock : group.getFileBLocks()) { + if (canSplitSingleFile(originConfiguration)) { + startEndPairs.add(new StartEndPair(fileBlock.getStartOffset(), fileBlock.getEndOffset(), fileBlock.getObjName())); + } else { + // 如果不支持内部切分,则设置结束位点为-1,直接读取文件全部内容 + // 对于软连接文件,这是必要的 30190064 + startEndPairs.add(new StartEndPair(fileBlock.getStartOffset(), -1L, fileBlock.getObjName())); + } + } + configuration.set(Key.SPLIT_SLICE_CONFIG, startEndPairs); + configurationList.add(configuration); + } + + return configurationList; + } +} + +class Group { + /* + * fileBlockList 表示该Group中对应的文件块列表,单个文件块用一个三元组 表示 + * */ + private List fileBLockList; + private Long capacity; + private Long filledLenght; + private static final Logger LOG = LoggerFactory.getLogger(Group.class); + + Group (Long capacity) { + this(new ArrayList<>(), capacity); + } + + Group (List fileBLockList, Long capacity) { + this.capacity = capacity; + this.fileBLockList = fileBLockList; + this.filledLenght = 0L; + for (FileBlock fileBlock : fileBLockList) { + this.filledLenght += fileBlock.getSize(); + this.capacity -= fileBlock.getSize(); + } + } + + void fill(FileBlock fileBlock) { + if (null == fileBlock) { + return; + } + this.fileBLockList.add(fileBlock); + this.capacity -= fileBlock.getSize(); + this.filledLenght += fileBlock.getSize(); + } + + void take(FileBlock fileBlock) { + this.capacity += fileBlock.getSize(); + this.filledLenght -= fileBlock.getSize(); + this.fileBLockList.remove(fileBlock); + } + + Long getCapacity() { + return this.capacity; + } + + void setCapacity(Long capacity) { + this.capacity = capacity; + } + + Long getFilledLenght() { + return this.filledLenght; + } + + public boolean isEmpty() { + return this.fileBLockList.isEmpty(); + } + + public boolean isFull() { + return this.capacity <= 0; + } + + List getFileBLocks() { + return this.fileBLockList; + } + + private Integer getBiggestFileBlock() { + Integer index = 0; + Long maxSize = -1L; + for (int i = 0; i < this.fileBLockList.size(); i++) { + if (this.fileBLockList.get(index).getSize() > maxSize) { + index = i; + } + } + return index; + } + + /* + * 对Group进行切分,切分逻辑为:对最大block进行切分,前splitLen个字节作为一个新块 + * */ + FileBlock split(Long splitLen, OSSClient ossClient, String ossBucketName) { + Integer bigBlockIndex = this.getBiggestFileBlock(); + FileBlock bigBlock = this.fileBLockList.get(bigBlockIndex); + // 如果最大块的不足 10MB,则不进行内部切分直接返回 + if (bigBlock.getSize() <= OssSplitUtil.SINGLE_FILE_SPLIT_THRESHOLD_IN_SIZE) { + return null; + } + + FileBlock outBlock; + FileBlock remainBlock; + + this.take(bigBlock); + + // 如果splitLen 大于 最大块的长度, 则直接把最大块切分出去 + if (splitLen >= bigBlock.getSize()) { + outBlock = new FileBlock(bigBlock); + } else { + Long originalEnd = bigBlock.getEndOffset(); + outBlock = new FileBlock(bigBlock.getObjName(), bigBlock.getStartOffset(), bigBlock.getStartOffset() + splitLen - 1); + + // 校准第一个block的结束位点,即往后推到第一个换行符 + InputStream inputStream = new OssInputStream(ossClient, ossBucketName, outBlock.getObjName(), outBlock.getEndOffset(), originalEnd); + Long endForward = this.getLFIndex(inputStream); + outBlock.setEndOffset(outBlock.getEndOffset() + endForward); + + // outblock取的是前边部分record,切分除去后,剩余部分可能为空,这时候不生成remainBlock,确保有剩余(outBlock.end > originEnd)时再生成remainBlock. + if (outBlock.getEndOffset() < originalEnd) { + remainBlock = new FileBlock(bigBlock.getObjName(), outBlock.getEndOffset() + 1, originalEnd); + this.fill(remainBlock); + } + } + + return outBlock; + } + + Long getOverloadLength() { + return Math.max(0, -this.capacity); + } + + /** + * 获取到输入流开始的第一个'\n'偏移量 + * + * @param inputStream + * 输入流 + * @return + */ + public Long getLFIndex(InputStream inputStream) { + Long hasReadByteIndex = -1L; + int ch = 0; + while (ch != -1) { + try { + ch = inputStream.read(); + } catch (IOException e) { + throw DataXException.asDataXException(UnstructuredStorageReaderErrorCode.READ_FILE_IO_ERROR, + String.format("inputstream read Byte has exception: %s", e.getMessage()), e); + } + hasReadByteIndex++; + + if (ch == '\n') { + return hasReadByteIndex; + } + } + return -1L; + } + + public String toString() { + JSONArray fbList = new JSONArray(); + int index = 0; + for (FileBlock fb : this.fileBLockList) { + JSONObject jsonObject = new JSONObject(); + jsonObject.put(String.format("block[%d]", index++), fb.toString()); + fbList.add(jsonObject); + } + return fbList.toString(); + } +} + +class FileBlock { + private String objName; + private Long startOffset; + private Long endOffset; + private Long size; + + FileBlock(String objName, Long startOffset, Long endOffset) { + assert (StringUtils.isNotBlank(objName) && startOffset >= 0 ); + assert (endOffset == -1 || startOffset <= endOffset); + + this.objName = objName; + this.startOffset = startOffset; + // endOffset < 0 的情况下,统一设置为-1,size 设置为0 + this.endOffset = endOffset < 0 ? -1 : endOffset; + this.size = endOffset < 0 ? 1 : this.endOffset - this.startOffset + 1; + } + + public FileBlock(String objName) { + this(objName, 0L, -1L); + } + + public FileBlock(String objName, Pair starEndPair) { + this(objName, starEndPair.getKey(), starEndPair.getValue()); + } + + public FileBlock(FileBlock fileBlock) { + assert (fileBlock != null); + this.objName = fileBlock.objName; + this.startOffset = fileBlock.startOffset; + this.endOffset = fileBlock.endOffset; + this.size = fileBlock.size; + } + + Long getSize() { + return this.size; + } + + Long getStartOffset() { + return this.startOffset; + } + + void setStartOffset(Long startOffset) { + Long deltaSize = this.startOffset - startOffset; + this.startOffset = startOffset; + this.size += deltaSize; + } + + Long getEndOffset() { + return this.endOffset; + } + + void setEndOffset(Long endOffset) { + Long deltaSize = endOffset - this.endOffset; + this.endOffset = endOffset; + //size随之调整 + this.size += deltaSize; + } + + String getObjName() { + return this.objName; + } + + public String toString() { + return String.format("<%s,%d,%d>", this.objName, this.startOffset, this.endOffset); + } +} diff --git a/osswriter/doc/osswriter.md b/osswriter/doc/osswriter.md index 1a3d3e478a..0c23e698c8 100644 --- a/osswriter/doc/osswriter.md +++ b/osswriter/doc/osswriter.md @@ -18,7 +18,7 @@ OSSWriter提供了向OSS写入类CSV格式的一个或者多个表文件。 OSSWriter实现了从DataX协议转为OSS中的TXT文件功能,OSS本身是无结构化数据存储,OSSWriter需要在如下几个方面增加: -1. 支持且仅支持写入 TXT的文件,且要求TXT中shema为一张二维表。 +1. 支持写入 TXT的文件,且要求TXT中shema为一张二维表。 2. 支持类CSV格式文件,自定义分隔符。 @@ -28,6 +28,8 @@ OSSWriter实现了从DataX协议转为OSS中的TXT文件功能,OSS本身是无 7. 文件支持滚动,当文件大于某个size值或者行数值,文件需要切换。 [暂不支持] +8. 支持写 PARQUET、ORC 文件 + 我们不能做到: 1. 单个文件不能支持并发写入。 @@ -37,7 +39,7 @@ OSSWriter实现了从DataX协议转为OSS中的TXT文件功能,OSS本身是无 ### 3.1 配置样例 - +写 txt文件样例 ```json { "job": { @@ -65,7 +67,90 @@ OSSWriter实现了从DataX协议转为OSS中的TXT文件功能,OSS本身是无 } } ``` +写 orc 文件样例 +```json +{ + "job": { + "setting": {}, + "content": [ + { + "reader": {}, + "writer": { + "name": "osswriter", + "parameter": { + "endpoint": "http://oss.aliyuncs.com", + "accessId": "", + "accessKey": "", + "bucket": "myBucket", + "fileName": "test", + "encoding": "UTF-8", + "column": [ + { + "name": "col1", + "type": "BIGINT" + }, + { + "name": "col2", + "type": "DOUBLE" + }, + { + "name": "col3", + "type": "STRING" + } + ], + "fileFormat": "orc", + "path": "/tests/case61", + "writeMode": "append" + } + } + } + ] + } +} +``` +写 parquet 文件样例 +```json +{ + "job": { + "setting": {}, + "content": [ + { + "reader": {}, + "writer": { + "name": "osswriter", + "parameter": { + "endpoint": "http://oss.aliyuncs.com", + "accessId": "", + "accessKey": "", + "bucket": "myBucket", + "fileName": "test", + "encoding": "UTF-8", + "column": [ + { + "name": "col1", + "type": "BIGINT" + }, + { + "name": "col2", + "type": "DOUBLE" + }, + { + "name": "col3", + "type": "STRING" + } + ], + "parquetSchema": "message test { required int64 int64_col;\n required binary str_col (UTF8);\nrequired group params (MAP) {\nrepeated group key_value {\nrequired binary key (UTF8);\nrequired binary value (UTF8);\n}\n}\nrequired group params_arr (LIST) {\n repeated group list {\n required binary element (UTF8);\n }\n}\nrequired group params_struct {\n required int64 id;\n required binary name (UTF8);\n }\nrequired group params_arr_complex (LIST) {\n repeated group list {\n required group element {\n required int64 id;\n required binary name (UTF8);\n}\n }\n}\nrequired group params_complex (MAP) {\nrepeated group key_value {\nrequired binary key (UTF8);\nrequired group value {\n required int64 id;\n required binary name (UTF8);\n }\n}\n}\nrequired group params_struct_complex {\n required int64 id;\n required group detail {\n required int64 id;\n required binary name (UTF8);\n }\n }\n}", + "fileFormat": "parquet", + "path": "/tests/case61", + "writeMode": "append" + } + } + } + ] + } +} +``` ### 3.2 参数说明 * **endpoint** diff --git a/osswriter/pom.xml b/osswriter/pom.xml index 90d84c10d0..ac4029e0e9 100644 --- a/osswriter/pom.xml +++ b/osswriter/pom.xml @@ -10,6 +10,17 @@ osswriter jar + + org.apache.logging.log4j + log4j-api + 2.17.1 + + + + org.apache.logging.log4j + log4j-core + 2.17.1 + com.alibaba.datax datax-common @@ -44,6 +55,60 @@ aliyun-sdk-oss 2.2.3 + + + + org.apache.parquet + parquet-column + 1.8.1 + + + org.apache.parquet + parquet-avro + 1.8.1 + + + org.apache.parquet + parquet-common + 1.8.1 + + + org.apache.parquet + parquet-format + 2.3.1 + + + org.apache.parquet + parquet-jackson + 1.8.1 + + + org.apache.parquet + parquet-encoding + 1.8.1 + + + org.apache.parquet + parquet-hadoop + 1.8.1 + + + com.twitter + parquet-hadoop-bundle + 1.6.0 + + + com.alibaba.datax + hdfswriter + 0.0.1-SNAPSHOT + compile + + + com.alibaba.datax + datax-core + 0.0.1-SNAPSHOT + compile + @@ -77,4 +142,4 @@ - \ No newline at end of file + diff --git a/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/Constant.java b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/Constant.java index 5bf2eb46e3..b5c7110b5c 100644 --- a/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/Constant.java +++ b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/Constant.java @@ -6,4 +6,11 @@ public class Constant { public static final String OBJECT = "object"; public static final int SOCKETTIMEOUT = 5000000; + public static final String DEFAULT_NULL_FORMAT = "null"; + + /** + * 每一个上传的Part都有一个标识它的号码(part number,范围是1-10000) + * https://help.aliyun.com/document_detail/31993.html + */ + public static final int MAX_BLOCK_SIZE = 10000; } diff --git a/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/Key.java b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/Key.java index b922f59c0c..8ce263b052 100644 --- a/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/Key.java +++ b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/Key.java @@ -16,4 +16,55 @@ public class Key { public static final String CNAME = "cname"; + public static final String PARTITION = "partition"; + + /** + * encrypt: 是否需要将数据在oss上加密存储 + */ + public static final String ENCRYPT = "encrypt"; + + public static final String BLOCK_SIZE_IN_MB = "blockSizeInMB"; + + public static final String OSS_CONFIG = "oss"; + public static final String POSTGRESQL_CONFIG = "postgresql"; + + public static final String PROXY_HOST = "proxyHost"; + + public static final String PROXY_PORT = "proxyPort"; + + public static final String PROXY_USERNAME = "proxyUsername"; + + public static final String PROXY_PASSWORD = "proxyPassword"; + + public static final String PROXY_DOMAIN = "proxyDomain"; + + public static final String PROXY_WORKSTATION = "proxyWorkstation"; + + public static final String HDOOP_CONFIG = "hadoopConfig"; + + public static final String FS_OSS_ACCESSID = "fs.oss.accessKeyId"; + + public static final String FS_OSS_ACCESSKEY = "fs.oss.accessKeySecret"; + + public static final String FS_OSS_ENDPOINT = "fs.oss.endpoint"; + /** + * 多个task是否写单个object文件: + * false 多个task写多个object(默认是false, 保持向前兼容) + * true 多个task写单个object + */ + public static final String WRITE_SINGLE_OBJECT = "writeSingleObject"; + + public static final String UPLOAD_ID = "uploadId"; + + /** + * Only for parquet or orc fileType + */ + public static final String PATH = "path"; + /** + * Only for parquet or orc fileType + */ + public static final String FILE_NAME = "fileName"; + + public static final String GENERATE_EMPTY_FILE = "generateEmptyFile"; + } diff --git a/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/OssSingleObject.java b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/OssSingleObject.java new file mode 100644 index 0000000000..415cf39cdf --- /dev/null +++ b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/OssSingleObject.java @@ -0,0 +1,78 @@ +package com.alibaba.datax.plugin.writer.osswriter; + +import com.alibaba.datax.common.exception.DataXException; +import com.aliyun.oss.model.PartETag; +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @Author: guxuan + * @Date 2022-05-17 16:30 + */ +public class OssSingleObject { + private static Logger logger = LoggerFactory.getLogger(OssSingleObject.class); + + /** + * 一个uploadId即一个oss对象 + */ + public static String uploadId; + + /** + * 将最后一个未提交的block全部缓存到lastBlockBuffer中 + */ + private static byte[] lastBlockBuffer; + + /** + * 当前part number + */ + public static AtomicInteger currentPartNumber = new AtomicInteger(1); + + /** + * 所有已经提交的block + * 注:allPartETags是线程安全的list + */ + public static List allPartETags = Collections.synchronizedList(new ArrayList()); + + /** + * 将每个task最后未upload的block加入到lastBlockBuffer, + * 如果lastBlockBuffer的大小已经超过blockSizeInByte,则需要upload一次, 防止task过多导致lastBlockBuffer暴增OOM + * + * @param lastBlock + * @param ossWriterProxy + * @param blockSizeInByte + * @param object + */ + public synchronized static void addLastBlockBuffer(byte[] lastBlock, + OssWriterProxy ossWriterProxy, + long blockSizeInByte, + String object, OssWriterProxy.HeaderProvider headerProvider) { + lastBlockBuffer = ArrayUtils.addAll(lastBlockBuffer, lastBlock); + //lastBlockBuffer大小超过blockSizeInByte则需要upload part + if (lastBlockBuffer != null && lastBlockBuffer.length >= blockSizeInByte) { + logger.info("write last block buffer part size [{}] to object [{}], all has uploaded part size:{}, current part number:{}, uploadId:{}", + lastBlockBuffer.length, object, allPartETags.size(), currentPartNumber.intValue(), uploadId); + try { + ossWriterProxy.uploadOnePartForSingleObject(lastBlockBuffer, uploadId, allPartETags, object, headerProvider); + } catch (Exception e) { + logger.error("upload part error: {}", e.getMessage(), e); + throw DataXException.asDataXException(e.getMessage()); + } + //currentPartNumber自增 + currentPartNumber.incrementAndGet(); + //清空lastBlockBuffer + lastBlockBuffer = null; + } + + } + + public static byte[] getLastBlockBuffer() { + return lastBlockBuffer; + } + +} diff --git a/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/OssWriter.java b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/OssWriter.java index 90a34ad7bf..f96a8e0195 100644 --- a/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/OssWriter.java +++ b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/OssWriter.java @@ -1,18 +1,21 @@ package com.alibaba.datax.plugin.writer.osswriter; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.StringWriter; +import java.io.*; import java.text.DateFormat; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.UUID; +import java.util.*; import java.util.concurrent.Callable; +import com.alibaba.datax.common.element.BytesColumn; +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.util.RangeSplitUtil; +import com.alibaba.datax.plugin.unstructuredstorage.FileFormat; +import com.alibaba.datax.plugin.unstructuredstorage.writer.binaryFileUtil.BinaryFileWriterUtil; +import com.alibaba.datax.plugin.writer.hdfswriter.HdfsWriter; +import com.alibaba.datax.plugin.writer.osswriter.util.HandlerUtil; +import com.alibaba.datax.plugin.writer.osswriter.util.HdfsParquetUtil; +import com.alibaba.fastjson2.JSON; +import com.aliyun.oss.model.*; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -31,50 +34,121 @@ import com.aliyun.oss.ClientException; import com.aliyun.oss.OSSClient; import com.aliyun.oss.OSSException; -import com.aliyun.oss.model.CompleteMultipartUploadRequest; -import com.aliyun.oss.model.CompleteMultipartUploadResult; -import com.aliyun.oss.model.InitiateMultipartUploadRequest; -import com.aliyun.oss.model.InitiateMultipartUploadResult; -import com.aliyun.oss.model.OSSObjectSummary; -import com.aliyun.oss.model.ObjectListing; -import com.aliyun.oss.model.PartETag; -import com.aliyun.oss.model.UploadPartRequest; -import com.aliyun.oss.model.UploadPartResult; + +import static com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.*; /** * Created by haiwei.luo on 15-02-09. */ public class OssWriter extends Writer { + + public static int parseParentPathLength(List path) { + if (path == null || path.size() != 1) { + throw DataXException.asDataXException( + OssWriterErrorCode.CONFIG_INVALID_EXCEPTION, String.format("only support configure one path in binary copy mode, your config: %s", JSON.toJSONString(path))); + } + String eachPath = path.get(0); + int endMark; + for (endMark = 0; endMark < eachPath.length(); endMark++) { + if ('*' != eachPath.charAt(endMark) && '?' != eachPath.charAt(endMark)) { + continue; + } else { + break; + } + } + + int lastDirSeparator = eachPath.lastIndexOf(IOUtils.DIR_SEPARATOR) + 1; + if (endMark < eachPath.length()) { + lastDirSeparator = eachPath.substring(0, endMark).lastIndexOf(IOUtils.DIR_SEPARATOR) + 1; + } + return lastDirSeparator; + } + public static class Job extends Writer.Job { private static final Logger LOG = LoggerFactory.getLogger(Job.class); private Configuration writerSliceConfig = null; private OSSClient ossClient = null; + private Configuration peerPluginJobConf; + private Boolean isBinaryFile; + private String objectDir; + private String syncMode; + private String fileFormat; + private String encoding; + private HdfsWriter.Job hdfsWriterJob; + private boolean useHdfsWriterProxy = false; + private boolean writeSingleObject; + private OssWriterProxy ossWriterProxy; + private String bucket; + private String object; + private List header; + + @Override + public void preHandler(Configuration jobConfiguration) { + HandlerUtil.preHandler(jobConfiguration); + } + @Override public void init() { this.writerSliceConfig = this.getPluginJobConf(); + this.basicValidateParameter(); + this.fileFormat = this.writerSliceConfig.getString( + com.alibaba.datax.plugin.unstructuredstorage.writer.Key.FILE_FORMAT, + com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.FILE_FORMAT_TEXT); + this.encoding = this.writerSliceConfig.getString( + com.alibaba.datax.plugin.unstructuredstorage.writer.Key.ENCODING, + com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.DEFAULT_ENCODING); + this.useHdfsWriterProxy = HdfsParquetUtil.isUseHdfsWriterProxy(this.fileFormat); + if(useHdfsWriterProxy){ + this.hdfsWriterJob = new HdfsWriter.Job(); + HdfsParquetUtil.adaptConfiguration(this.hdfsWriterJob, this.writerSliceConfig); + + this.hdfsWriterJob.setJobPluginCollector(this.getJobPluginCollector()); + this.hdfsWriterJob.setPeerPluginJobConf(this.getPeerPluginJobConf()); + this.hdfsWriterJob.setPeerPluginName(this.getPeerPluginName()); + this.hdfsWriterJob.setPluginJobConf(this.getPluginJobConf()); + this.hdfsWriterJob.init(); + return; + } + this.peerPluginJobConf = this.getPeerPluginJobConf(); + this.isBinaryFile = FileFormat.getFileFormatByConfiguration(this.peerPluginJobConf).isBinary(); + this.syncMode = this.writerSliceConfig + .getString(com.alibaba.datax.plugin.unstructuredstorage.writer.Key.SYNC_MODE, ""); + this.writeSingleObject = this.writerSliceConfig.getBool(Key.WRITE_SINGLE_OBJECT, false); + this.header = this.writerSliceConfig + .getList(com.alibaba.datax.plugin.unstructuredstorage.writer.Key.HEADER, null, String.class); this.validateParameter(); this.ossClient = OssUtil.initOssClient(this.writerSliceConfig); + this.ossWriterProxy = new OssWriterProxy(this.writerSliceConfig, this.ossClient); + } + + private void basicValidateParameter(){ + this.writerSliceConfig.getNecessaryValue(Key.ENDPOINT, OssWriterErrorCode.REQUIRED_VALUE); + this.writerSliceConfig.getNecessaryValue(Key.ACCESSID, OssWriterErrorCode.REQUIRED_VALUE); + this.writerSliceConfig.getNecessaryValue(Key.ACCESSKEY, OssWriterErrorCode.REQUIRED_VALUE); + this.writerSliceConfig.getNecessaryValue(Key.BUCKET, OssWriterErrorCode.REQUIRED_VALUE); } private void validateParameter() { - this.writerSliceConfig.getNecessaryValue(Key.ENDPOINT, - OssWriterErrorCode.REQUIRED_VALUE); - this.writerSliceConfig.getNecessaryValue(Key.ACCESSID, - OssWriterErrorCode.REQUIRED_VALUE); - this.writerSliceConfig.getNecessaryValue(Key.ACCESSKEY, - OssWriterErrorCode.REQUIRED_VALUE); - this.writerSliceConfig.getNecessaryValue(Key.BUCKET, - OssWriterErrorCode.REQUIRED_VALUE); - this.writerSliceConfig.getNecessaryValue(Key.OBJECT, - OssWriterErrorCode.REQUIRED_VALUE); + this.writerSliceConfig.getBool(Key.ENCRYPT); + + if (this.isBinaryFile){ + BinaryFileWriterUtil.validateParameter(this.writerSliceConfig); + return; + } + + if (!this.isPeer2PeerCopyMode()) { + // 非对等拷贝模式下必选 + this.writerSliceConfig.getNecessaryValue(Key.OBJECT, + OssWriterErrorCode.REQUIRED_VALUE); + } + // warn: do not support compress!! String compress = this.writerSliceConfig .getString(com.alibaba.datax.plugin.unstructuredstorage.writer.Key.COMPRESS); if (StringUtils.isNotBlank(compress)) { - String errorMessage = String.format( - "OSS写暂时不支持压缩, 该压缩配置项[%s]不起效用", compress); + String errorMessage = String.format("OSS writes do not support compression for the moment. The compressed item %s does not work", compress); LOG.error(errorMessage); throw DataXException.asDataXException( OssWriterErrorCode.ILLEGAL_VALUE, errorMessage); @@ -82,107 +156,376 @@ private void validateParameter() { } UnstructuredStorageWriterUtil .validateParameter(this.writerSliceConfig); - + LOG.info("writeSingleObject is: {}", this.writeSingleObject); } @Override public void prepare() { LOG.info("begin do prepare..."); - String bucket = this.writerSliceConfig.getString(Key.BUCKET); - String object = this.writerSliceConfig.getString(Key.OBJECT); + if(useHdfsWriterProxy){ + this.hdfsWriterJob.prepare(); + return; + } + this.bucket = this.writerSliceConfig.getString(Key.BUCKET); + this.object = this.writerSliceConfig.getString(Key.OBJECT); String writeMode = this.writerSliceConfig .getString(com.alibaba.datax.plugin.unstructuredstorage.writer.Key.WRITE_MODE); - // warn: bucket is not exists, create it - try { - // warn: do not create bucket for user - if (!this.ossClient.doesBucketExist(bucket)) { - // this.ossClient.createBucket(bucket); - String errorMessage = String.format( - "您配置的bucket [%s] 不存在, 请您确认您的配置项.", bucket); - LOG.error(errorMessage); - throw DataXException.asDataXException( - OssWriterErrorCode.ILLEGAL_VALUE, errorMessage); - } - LOG.info(String.format("access control details [%s].", - this.ossClient.getBucketAcl(bucket).toString())); - - // truncate option handler - if ("truncate".equals(writeMode)) { - LOG.info(String - .format("由于您配置了writeMode truncate, 开始清理 [%s] 下面以 [%s] 开头的Object", - bucket, object)); - // warn: 默认情况下,如果Bucket中的Object数量大于100,则只会返回100个Object - while (true) { - ObjectListing listing = null; - LOG.info("list objects with listObject(bucket, object)"); - listing = this.ossClient.listObjects(bucket, object); - List objectSummarys = listing - .getObjectSummaries(); - for (OSSObjectSummary objectSummary : objectSummarys) { - LOG.info(String.format("delete oss object [%s].", - objectSummary.getKey())); - this.ossClient.deleteObject(bucket, - objectSummary.getKey()); - } - if (objectSummarys.isEmpty()) { - break; + List sourceFileName = this.peerPluginJobConf.getList(SOURCE_FILE_NAME, new ArrayList(), + String.class); + this.objectDir = this.getObjectDir(object); + + // 对等拷贝模式下将源头获取的文件列表在目的端删除 + if (this.isPeer2PeerCopyMode()) { + String fullObjectName = null; + String truncateMode = this.writerSliceConfig.getString("truncateMode", "objectMatch"); + // 前缀删除模式 + if ("prefix".equalsIgnoreCase(truncateMode)) { + BinaryFileWriterUtil.checkFileNameIfRepeatedThrowException(sourceFileName); + if (TRUNCATE.equals(writeMode)) { + LOG.info("You have configured [writeMode] [truncate], so the system will start to clear the objects starting with [{}] under [{}]. ", bucket, object); + // warn: 默认情况下,如果Bucket中的Object数量大于100,则只会返回100个Object + while (true) { + ObjectListing listing = null; + LOG.info("list objects with listObject(bucket, object)"); + listing = this.ossClient.listObjects(bucket, object); + List objectSummarys = listing + .getObjectSummaries(); + if (objectSummarys.isEmpty()) { + break; + } + List objects2Delete = new ArrayList(); + for (OSSObjectSummary objectSummary : objectSummarys) { + objects2Delete.add(objectSummary.getKey()); + } + LOG.info(String.format("[prefix truncate mode]delete oss object [%s].", JSON.toJSONString(objects2Delete))); + DeleteObjectsRequest deleteRequest = new DeleteObjectsRequest(bucket); + deleteRequest.setKeys(objects2Delete); + deleteRequest.setQuiet(true);// 简单模式 + DeleteObjectsResult deleteResult = this.ossClient.deleteObjects(deleteRequest); + assert deleteResult.getDeletedObjects().isEmpty(); + LOG.warn("OSS request id:{}, objects delete failed:{}", deleteResult.getRequestId(), + JSON.toJSONString(deleteResult.getDeletedObjects())); } + + }else { + throw DataXException.asDataXException(OssWriterErrorCode.ILLEGAL_VALUE, + "only support truncate writeMode in copy sync mode."); } - } else if ("append".equals(writeMode)) { - LOG.info(String - .format("由于您配置了writeMode append, 写入前不做清理工作, 数据写入Bucket [%s] 下, 写入相应Object的前缀为 [%s]", - bucket, object)); - } else if ("nonConflict".equals(writeMode)) { - LOG.info(String - .format("由于您配置了writeMode nonConflict, 开始检查Bucket [%s] 下面以 [%s] 命名开头的Object", - bucket, object)); - ObjectListing listing = this.ossClient.listObjects(bucket, - object); - if (0 < listing.getObjectSummaries().size()) { - StringBuilder objectKeys = new StringBuilder(); - objectKeys.append("[ "); - for (OSSObjectSummary ossObjectSummary : listing - .getObjectSummaries()) { - objectKeys.append(ossObjectSummary.getKey() + " ,"); + } else { + if (TRUNCATE.equals(writeMode)) { + sourceFileName = this.peerPluginJobConf.getList(com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.SOURCE_FILE, new ArrayList(), + String.class); + List readerPath = this.peerPluginJobConf.getList(com.alibaba.datax.plugin.unstructuredstorage.writer.Key.PATH, new ArrayList(), + String.class); + int parentPathLength = OssWriter.parseParentPathLength(readerPath); + this.writerSliceConfig.set("__parentPathLength", parentPathLength); + BinaryFileWriterUtil.checkFileNameIfRepeatedThrowException(sourceFileName); + + // 原样文件名删除模式 + int splitCount = sourceFileName.size() / 1000 + 1; + List> splitResult = RangeSplitUtil.doListSplit(sourceFileName, splitCount); + for (List eachSlice : splitResult) { + assert eachSlice.size() <= 1000; + if (eachSlice.isEmpty()) { + continue; + } + List ossObjFullPath = new ArrayList(); + for (String eachObj : eachSlice) { + fullObjectName = String.format("%s%s", objectDir, eachObj.substring(parentPathLength, eachObj.length())); + ossObjFullPath.add(fullObjectName); + } + LOG.info(String.format("[origin object name truncate mode]delete oss object [%s].", JSON.toJSONString(ossObjFullPath))); + DeleteObjectsRequest deleteRequest = new DeleteObjectsRequest(bucket); + deleteRequest.setKeys(ossObjFullPath); + deleteRequest.setQuiet(true);// 简单模式 + DeleteObjectsResult deleteResult = this.ossClient.deleteObjects(deleteRequest); + assert deleteResult.getDeletedObjects().isEmpty(); + LOG.warn("OSS request id:{}, objects delete failed:{}", deleteResult.getRequestId(), + JSON.toJSONString(deleteResult.getDeletedObjects())); } - objectKeys.append(" ]"); - LOG.info(String.format( - "object with prefix [%s] details: %s", object, - objectKeys.toString())); - throw DataXException - .asDataXException( - OssWriterErrorCode.ILLEGAL_VALUE, - String.format( - "您配置的Bucket: [%s] 下面存在其Object有前缀 [%s].", - bucket, object)); + } else { + throw DataXException.asDataXException(OssWriterErrorCode.ILLEGAL_VALUE, + "only support truncate writeMode in copy sync mode."); } } - } catch (OSSException e) { - throw DataXException.asDataXException( - OssWriterErrorCode.OSS_COMM_ERROR, e.getMessage()); - } catch (ClientException e) { - throw DataXException.asDataXException( - OssWriterErrorCode.OSS_COMM_ERROR, e.getMessage()); + return; + } else { + // warn: 源头表不是半结构化或者不是对等copy模式时走前缀删除策略 + // warn: bucket is not exists, create it + try { + // warn: do not create bucket for user + if (!this.ossClient.doesBucketExist(bucket)) { + // this.ossClient.createBucket(bucket); + String errorMessage = String.format("The [bucket]: %s you configured does not exist. Please confirm your configuration items. ", bucket); + LOG.error(errorMessage); + throw DataXException.asDataXException( + OssWriterErrorCode.ILLEGAL_VALUE, errorMessage); + } + LOG.info(String.format("access control details [%s].", + this.ossClient.getBucketAcl(bucket).toString())); + + if (writeSingleObject) { + doPrepareForSingleObject(bucket, object, writeMode); + } else { + doPrepareForMutliObject(bucket, object, writeMode); + } + } catch (OSSException e) { + throw DataXException.asDataXException( + OssWriterErrorCode.OSS_COMM_ERROR, e.getMessage(), e); + } catch (ClientException e) { + throw DataXException.asDataXException( + OssWriterErrorCode.OSS_COMM_ERROR, e.getMessage(), e); + } + } + } + + /** + * 执行多个task写单个object prepare逻辑 + * + * @param bucket + * @param object + * @param writeMode + */ + private void doPrepareForSingleObject(String bucket, String object, String writeMode) { + boolean doesObjectExist = this.ossClient.doesObjectExist(bucket, object); + LOG.info("does object [{}] exist in bucket {} : {}", object, bucket, doesObjectExist); + if (TRUNCATE.equals(writeMode)) { + LOG.info("Because you have configured writeMode truncate, and writeSingleObject is true, start cleaning up the duplicate object [{}] under [{}]", bucket, object); + if (doesObjectExist) { + LOG.info("object [{}] has exist in bucket, delete it!", object, bucket); + this.ossClient.deleteObject(bucket, object); + } + } else if (APPEND.equals(writeMode)) { + throw DataXException + .asDataXException( + OssWriterErrorCode.ILLEGAL_VALUE, + "Illegal value"); + } else if (NOCONFLICT.equals(writeMode)) { + LOG.info("Because you have configured writeMode nonConflict, and writeSingleObject is true, start checking bucket [{}] under the same name object [{}]", bucket, object); + if (doesObjectExist) { + throw DataXException + .asDataXException( + OssWriterErrorCode.ILLEGAL_VALUE, + String.format("Buffet you configured: %s There is a duplicate name of Object %s", bucket, object)); + } + } + } + + /** + * 执行多个task写多个object的prepare逻辑,这个是osswriter已有的逻辑,需要保持向前兼容性 + * + * @param bucket + * @param object + * @param writeMode + */ + private void doPrepareForMutliObject(String bucket, String object, String writeMode) { + // truncate option handler + if (TRUNCATE.equals(writeMode)) { + LOG.info("You have configured [writeMode] [truncate], so the system will start to clear the objects starting with [{}] under [{}]. ", bucket, object); + // warn: 默认情况下,如果Bucket中的Object数量大于100,则只会返回100个Object + while (true) { + ObjectListing listing = null; + LOG.info("list objects with listObject(bucket, object)"); + listing = this.ossClient.listObjects(bucket, object); + List objectSummarys = listing + .getObjectSummaries(); + for (OSSObjectSummary objectSummary : objectSummarys) { + LOG.info(String.format("delete oss object [%s].", + objectSummary.getKey())); + this.ossClient.deleteObject(bucket, + objectSummary.getKey()); + } + if (objectSummarys.isEmpty()) { + break; + } + } + } else if (APPEND.equals(writeMode)) { + LOG.info("You have configured [writeMode] [append], so the system won\\u2019t perform the clearing before writing. Data is written to objects with the name prefix of [{}] under the bucket: [{}]. ", bucket, object); + } else if (NOCONFLICT.equals(writeMode)) { + LOG.info("You have configured [writeMode] [nonConflict], so the system will start to check objects whose names start with [{}] under the bucket: [{}]. ", bucket, object); + ObjectListing listing = this.ossClient.listObjects(bucket, + object); + if (0 < listing.getObjectSummaries().size()) { + StringBuilder objectKeys = new StringBuilder(); + objectKeys.append("[ "); + for (OSSObjectSummary ossObjectSummary : listing + .getObjectSummaries()) { + objectKeys.append(ossObjectSummary.getKey() + " ,"); + } + objectKeys.append(" ]"); + LOG.info(String.format( + "object with prefix [%s] details: %s", object, + objectKeys.toString())); + throw DataXException + .asDataXException( + OssWriterErrorCode.ILLEGAL_VALUE, + String.format("The [bucket] you configured: %s contains objects with the name prefix of %s.", bucket, object)); + } } } @Override public void post() { + if(useHdfsWriterProxy){ + this.hdfsWriterJob.post(); + return; + } + if (this.writeSingleObject) { + try { + /**1. 合并上传最后一个block*/ + LOG.info("Has upload part size: {}", OssSingleObject.allPartETags.size()); + if (OssSingleObject.getLastBlockBuffer() != null && OssSingleObject.getLastBlockBuffer().length != 0) { + byte[] byteBuffer = OssSingleObject.getLastBlockBuffer(); + LOG.info("post writer single object last merge block size is : {}", byteBuffer.length); + this.ossWriterProxy.uploadOnePartForSingleObject(byteBuffer, OssSingleObject.uploadId, + OssSingleObject.allPartETags, this.object, this::getHeaderBytes); + } + + if (OssSingleObject.allPartETags.size() == 0) { + LOG.warn("allPartETags size is 0, there is no part of data need to be complete uploaded, " + + "skip complete multipart upload!"); + this.ossWriterProxy.abortMultipartUpload(this.object,OssSingleObject.uploadId); + return; + } + + /**2. 完成complete upload */ + LOG.info("begin complete multi part upload, bucket:{}, object:{}, uploadId:{}, all has upload part size:{}", + this.bucket, this.object, OssSingleObject.uploadId, OssSingleObject.allPartETags.size()); + orderPartETages(OssSingleObject.allPartETags); + CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest( + this.bucket, this.object, OssSingleObject.uploadId, OssSingleObject.allPartETags); + CompleteMultipartUploadResult completeMultipartUploadResult = this.ossWriterProxy.completeMultipartUpload(completeMultipartUploadRequest); + LOG.info(String.format("post final object etag is:[%s]", completeMultipartUploadResult.getETag())); + } catch (Exception e) { + LOG.error("osswriter post error: {}", e.getMessage(), e); + throw DataXException.asDataXException(e.getMessage()); + } + } + } + + private byte[] getHeaderBytes() throws IOException { + if (null != this.header && !this.header.isEmpty()) { + // write header to writer + try (StringWriter sw = new StringWriter(); + UnstructuredWriter headerWriter = UnstructuredStorageWriterUtil. + produceUnstructuredWriter(this.fileFormat, this.writerSliceConfig, sw)) { + headerWriter.writeOneRecord(this.header); + return sw.toString().getBytes(this.encoding); + } + } + return new byte[0]; + } + /** + * 对allPartETags做递增排序 + * + * @param allPartETags + * @return + */ + private void orderPartETages(List allPartETags) { + Collections.sort(allPartETags, new Comparator() { + @Override + public int compare(PartETag o1, PartETag o2) { + //按照partNumber递增排序 + return o1.getPartNumber() - o2.getPartNumber(); + } + }); } @Override public void destroy() { - + if(useHdfsWriterProxy){ + this.hdfsWriterJob.destroy(); + return; + } + try { + // this.ossClient.shutdown(); + } catch (Exception e) { + LOG.warn("shutdown ossclient meet a exception:" + e.getMessage(), e); + } } @Override public List split(int mandatoryNumber) { LOG.info("begin do split..."); + if(useHdfsWriterProxy){ + return this.hdfsWriterJob.split(mandatoryNumber); + } + List writerSplitConfigs = new ArrayList(); + + // warn: 这个地方其实可能有bug,datax frame其实会shuffle, 文件内部切分也不好支持这个诉求 + if(this.isPeer2PeerCopyMode()){ + // 有这个需求风险: 源头oss的文件 abc/123/data.txt yixiao.txt 2个文件对等拷贝过来, 这个场景下data.txt + // yixiao.txt 只能放一个目录 + List readerSplitConfigs = this.getReaderPluginSplitConf(); + for (int i = 0; i < readerSplitConfigs.size(); i++) { + Configuration splitedTaskConfig = writerSliceConfig.clone(); + splitedTaskConfig.set(Key.OBJECT, objectDir); + splitedTaskConfig.set(com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.BINARY, + this.isBinaryFile); + writerSplitConfigs.add(splitedTaskConfig); + } + } else { + if (this.writeSingleObject) { + writerSplitConfigs = doSplitForWriteSingleObject(mandatoryNumber); + } else { + writerSplitConfigs = doSplitForWriteMultiObject(mandatoryNumber); + } + } + LOG.info("end do split. split size: {}", writerSplitConfigs.size()); + return writerSplitConfigs; + } + + /** + * 针对多个task写单个文件模式,新增split逻辑 + * + * @param mandatoryNumber + * @return + */ + private List doSplitForWriteSingleObject(int mandatoryNumber) { + LOG.info("writeSingleObject is true, begin do split for write single object."); List writerSplitConfigs = new ArrayList(); String object = this.writerSliceConfig.getString(Key.OBJECT); - String bucket = this.writerSliceConfig.getString(Key.BUCKET); + InitiateMultipartUploadRequest uploadRequest = this.ossWriterProxy.getInitiateMultipartUploadRequest( + object); + + InitiateMultipartUploadResult uploadResult; + try { + uploadResult = this.ossWriterProxy.initiateMultipartUpload( + uploadRequest); + } catch (Exception e) { + LOG.error("initiateMultipartUpload error: {}", e.getMessage(), e); + throw DataXException.asDataXException(e.getMessage()); + } + /** + * 如果需要写同一个object,需要保证使用同一个upload Id + * see: https://help.aliyun.com/document_detail/31993.html + */ + String uploadId = uploadResult.getUploadId(); + OssSingleObject.uploadId = uploadId; + LOG.info("writeSingleObject use uploadId: {}", uploadId); + + for (int i = 0; i < mandatoryNumber; i++) { + Configuration splitedTaskConfig = this.writerSliceConfig + .clone(); + splitedTaskConfig.set(Key.OBJECT, object); + splitedTaskConfig.set(Key.UPLOAD_ID, uploadId); + writerSplitConfigs.add(splitedTaskConfig); + } + return writerSplitConfigs; + } + + /** + * osswriter多个task写多个object文件split逻辑,历史已有该逻辑,保持向前兼容性 + * + * @param mandatoryNumber + * @return + */ + private List doSplitForWriteMultiObject(int mandatoryNumber) { + List writerSplitConfigs = new ArrayList(); + String bucket = this.writerSliceConfig.getString(Key.BUCKET); + String object = this.writerSliceConfig.getString(Key.OBJECT); Set allObjects = new HashSet(); try { List ossObjectlisting = this.ossClient @@ -192,10 +535,10 @@ public List split(int mandatoryNumber) { } } catch (OSSException e) { throw DataXException.asDataXException( - OssWriterErrorCode.OSS_COMM_ERROR, e.getMessage()); + OssWriterErrorCode.OSS_COMM_ERROR, e.getMessage(), e); } catch (ClientException e) { throw DataXException.asDataXException( - OssWriterErrorCode.OSS_COMM_ERROR, e.getMessage()); + OssWriterErrorCode.OSS_COMM_ERROR, e.getMessage(), e); } String objectSuffix; @@ -223,9 +566,25 @@ public List split(int mandatoryNumber) { writerSplitConfigs.add(splitedTaskConfig); } - LOG.info("end do split."); return writerSplitConfigs; } + + private boolean isPeer2PeerCopyMode() { + return this.isBinaryFile + || com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.SYNC_MODE_VALUE_COPY + .equalsIgnoreCase(this.syncMode); + } + + private String getObjectDir(String object) { + String dir = null; + if (StringUtils.isBlank(object)) { + dir = ""; + } else { + dir = object.trim(); + dir = dir.endsWith("/") ? dir : String.format("%s/", dir); + } + return dir; + } } public static class Task extends Writer.Task { @@ -237,17 +596,48 @@ public static class Task extends Writer.Task { private String object; private String nullFormat; private String encoding; - private char fieldDelimiter; private String dateFormat; private DateFormat dateParse; private String fileFormat; private List header; private Long maxFileSize;// MB private String suffix; + private Boolean encrypt;// 是否在服务器端进行加密存储 + private long blockSizeInByte; + private Boolean isBinaryFile; + private String objectDir; + private String syncMode; + private int parentPathLength; + private String byteEncoding; + private HdfsWriter.Task hdfsWriterTask; + private boolean useHdfsWriterProxy = false; + private boolean writeSingleObject; + private String uploadId; + private OssWriterProxy ossWriterProxy; + private List partition; + private boolean generateEmptyFile; @Override public void init() { this.writerSliceConfig = this.getPluginJobConf(); + this.fileFormat = this.writerSliceConfig + .getString( + com.alibaba.datax.plugin.unstructuredstorage.writer.Key.FILE_FORMAT, + com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.FILE_FORMAT_TEXT); + this.useHdfsWriterProxy = HdfsParquetUtil.isUseHdfsWriterProxy(this.fileFormat); + if(useHdfsWriterProxy){ + this.hdfsWriterTask = new HdfsWriter.Task(); + this.hdfsWriterTask.setPeerPluginJobConf(this.getPeerPluginJobConf()); + this.hdfsWriterTask.setPeerPluginName(this.getPeerPluginName()); + this.hdfsWriterTask.setPluginJobConf(this.getPluginJobConf()); + this.hdfsWriterTask.setReaderPluginSplitConf(this.getReaderPluginSplitConf()); + this.hdfsWriterTask.setTaskGroupId(this.getTaskGroupId()); + this.hdfsWriterTask.setTaskId(this.getTaskId()); + this.hdfsWriterTask.setTaskPluginCollector(this.getTaskPluginCollector()); + this.hdfsWriterTask.init(); + return; + } + this.ossClient = OssUtil.initOssClient(this.writerSliceConfig); this.bucket = this.writerSliceConfig.getString(Key.BUCKET); this.object = this.writerSliceConfig.getString(Key.OBJECT); @@ -264,14 +654,6 @@ public void init() { .getString( com.alibaba.datax.plugin.unstructuredstorage.writer.Key.ENCODING, com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.DEFAULT_ENCODING); - this.fieldDelimiter = this.writerSliceConfig - .getChar( - com.alibaba.datax.plugin.unstructuredstorage.writer.Key.FIELD_DELIMITER, - com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.DEFAULT_FIELD_DELIMITER); - this.fileFormat = this.writerSliceConfig - .getString( - com.alibaba.datax.plugin.unstructuredstorage.writer.Key.FILE_FORMAT, - com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.FILE_FORMAT_TEXT); this.header = this.writerSliceConfig .getList( com.alibaba.datax.plugin.unstructuredstorage.writer.Key.HEADER, @@ -285,67 +667,334 @@ public void init() { com.alibaba.datax.plugin.unstructuredstorage.writer.Key.SUFFIX, com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.DEFAULT_SUFFIX); this.suffix = this.suffix.trim();// warn: need trim + this.encrypt = this.writerSliceConfig.getBool(Key.ENCRYPT, false); + + // 设置每块字符串长度 + this.blockSizeInByte = this.writerSliceConfig.getLong(Key.BLOCK_SIZE_IN_MB, 10L) * 1024 * 1024; + + this.isBinaryFile = this.writerSliceConfig.getBool( + com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.BINARY, false); + + this.objectDir = this.getObjectDir(this.object); + this.syncMode = this.writerSliceConfig + .getString(com.alibaba.datax.plugin.unstructuredstorage.writer.Key.SYNC_MODE, ""); + this.parentPathLength = this.writerSliceConfig.getInt("__parentPathLength", 0); + + this.byteEncoding = this.writerSliceConfig + .getString(com.alibaba.datax.plugin.unstructuredstorage.writer.Key.BYTE_ENCODING); + + this.writeSingleObject = this.writerSliceConfig.getBool(Key.WRITE_SINGLE_OBJECT, false); + this.uploadId = this.writerSliceConfig.getString(Key.UPLOAD_ID); + this.ossWriterProxy = new OssWriterProxy(this.writerSliceConfig, this.ossClient); + this.partition = this.writerSliceConfig.getList(Key.PARTITION, new ArrayList<>(), String.class); + //是否生成空文件开关 + this.generateEmptyFile = this.writerSliceConfig.getBool(Key.GENERATE_EMPTY_FILE,true); } @Override public void startWrite(RecordReceiver lineReceiver) { + if(useHdfsWriterProxy){ + hdfsWriterTask.startWrite(lineReceiver); + return; + } + if (this.isPeer2PeerCopyMode()) { + // 对等拷贝 + this.startWriteBinaryFile(lineReceiver); + } else if (this.writeSingleObject) { + this.startWriteSingleObjectUnstructedStorageFile(lineReceiver); + } else { + this.startWriteUnstructedStorageFile(lineReceiver,generateEmptyFile); + } + } + + /** + * 单object写入 + * + * @param lineReceiver + */ + public void startWriteSingleObjectUnstructedStorageFile(RecordReceiver lineReceiver) { + + try { + Record record; + String currentObject = this.object; + List currentPartETags = new ArrayList(); + + //warn: may be StringBuffer->StringBuilder + StringWriter sw = new StringWriter(); + StringBuffer sb = sw.getBuffer(); + UnstructuredWriter unstructuredWriter = UnstructuredStorageWriterUtil. + produceUnstructuredWriter(this.fileFormat, this.writerSliceConfig, sw); + + while ((record = lineReceiver.getFromReader()) != null) { + //单文件同步暂不支持轮转[目前单文件支持同步约最大100GB大小] + if (OssSingleObject.currentPartNumber.intValue() > Constant.MAX_BLOCK_SIZE) { + throw DataXException.asDataXException(String.format("When writeSingleObject is true, the write size of your single object has exceeded the maximum value of %s MB.", + (Constant.MAX_BLOCK_SIZE * this.blockSizeInByte / 1024 / 1024))); + } + + // write: upload data to current object + UnstructuredStorageWriterUtil.transportOneRecord(record, + this.nullFormat, this.dateParse, + this.getTaskPluginCollector(), unstructuredWriter, this.byteEncoding); + + // 达到 this.blockSizeInByte ,上传文件块 + if (sb.length() >= this.blockSizeInByte) { + LOG.info(String + .format("write to bucket: [%s] object: [%s] with oss uploadId: [%s], currentPartNumber: %s", + this.bucket, currentObject, + this.uploadId, OssSingleObject.currentPartNumber.intValue())); + byte[] byteArray = sw.toString().getBytes(this.encoding); + this.ossWriterProxy.uploadOnePartForSingleObject(byteArray, this.uploadId, currentPartETags, currentObject, this::getHeaderBytes); + sb.setLength(0); + } + } + //将本task所有upload的part加入到allPartETags中 + OssSingleObject.allPartETags.addAll(currentPartETags); + + //将task未写完的最后一个block加入到 OssSingleObject.lastBlockBuffer 中,待job阶段合并上传 + if (sb.length() > 0) { + byte[] lastBlock = sw.toString().getBytes(this.encoding); + LOG.info("begin add last block to buffer, last block size: {}", lastBlock.length); + OssSingleObject.addLastBlockBuffer(lastBlock, this.ossWriterProxy, this.blockSizeInByte, this.object, this::getHeaderBytes); + } + } catch (IOException e) { + // 脏数据UnstructuredStorageWriterUtil.transportOneRecord已经记录,header + // 都是字符串不认为有脏数据 + throw DataXException.asDataXException( + OssWriterErrorCode.Write_OBJECT_ERROR, e.getMessage(), e); + } catch (Exception e) { + throw DataXException.asDataXException( + OssWriterErrorCode.Write_OBJECT_ERROR, e.getMessage(), e); + } + LOG.info("single oss object end do write"); + } + + private byte[] getHeaderBytes() throws IOException { + if (null != this.header && !this.header.isEmpty()) { + // write header to writer + try (StringWriter sw = new StringWriter(); + UnstructuredWriter headerWriter = UnstructuredStorageWriterUtil. + produceUnstructuredWriter(this.fileFormat, this.writerSliceConfig, sw)) { + headerWriter.writeOneRecord(this.header); + return sw.toString().getBytes(this.encoding); + } + } + return new byte[0]; + } + + /** + * 同步音视频等无结构化文件 + * warn: 代码和startWriteUnstructedStorageFile重复程度太高,后续需要继续重构 + */ + private void startWriteBinaryFile(RecordReceiver lineReceiver) { + Record record; + String currentObject = null; + InitiateMultipartUploadRequest currentInitiateMultipartUploadRequest; + InitiateMultipartUploadResult currentInitiateMultipartUploadResult = null; + String lastUploadId = null; + boolean gotData = false; + List currentPartETags = null; + int currentPartNumber = 1; + Map meta; + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + long currentSize = 0; + try { + // warn + boolean needInitMultipartTransform = true; + while ((record = lineReceiver.getFromReader()) != null) { + Column column = record.getColumn(0); + meta = record.getMeta(); + assert meta != null; + gotData = true; + String objectNameTmp = meta + .get(com.alibaba.datax.plugin.unstructuredstorage.writer.Key.META_KEY_FILE_PATH); + String fullObjectNameTmp = String.format("%s%s", this.objectDir, objectNameTmp.substring(this.parentPathLength, objectNameTmp.length())); + + // init: 2 condition begin new multipart upload + if (needInitMultipartTransform || !StringUtils.equals(currentObject, fullObjectNameTmp)) { + // 先将上一个分块上传的request complete掉 + if (null != currentInitiateMultipartUploadResult) { + // 如果还有部分分库数据没有提交,则先提交 + if (currentSize > 0) { + this.ossWriterProxy.uploadOnePart(byteArrayOutputStream.toByteArray(), currentPartNumber, + currentInitiateMultipartUploadResult, currentPartETags, currentObject); + currentPartNumber++; + currentSize = 0; + byteArrayOutputStream.reset(); + } + // TODO 如果当前文件是空文件 + String commitKey = currentInitiateMultipartUploadResult.getKey(); + LOG.info(String.format( + "current object [%s] size %s, complete current multipart upload %s and begin new one", + commitKey, currentPartNumber * this.blockSizeInByte, + currentInitiateMultipartUploadResult.getUploadId())); + CompleteMultipartUploadRequest currentCompleteMultipartUploadRequest = new CompleteMultipartUploadRequest( + this.bucket, commitKey, currentInitiateMultipartUploadResult.getUploadId(), + currentPartETags); + CompleteMultipartUploadResult currentCompleteMultipartUploadResult = this.ossWriterProxy.completeMultipartUpload( + currentCompleteMultipartUploadRequest); + lastUploadId = currentInitiateMultipartUploadResult.getUploadId(); + LOG.info(String.format("final object [%s] etag is:[%s]", commitKey, + currentCompleteMultipartUploadResult.getETag())); + } + // 这里发现一个全新的文件需要分块上传 + currentObject = fullObjectNameTmp; + currentInitiateMultipartUploadRequest = this.ossWriterProxy.getInitiateMultipartUploadRequest(currentObject); + currentInitiateMultipartUploadResult = this.ossWriterProxy.initiateMultipartUpload( + currentInitiateMultipartUploadRequest); + currentPartETags = new ArrayList(); + LOG.info(String.format("write to bucket: [%s] object: [%s] with oss uploadId: [%s]", + this.bucket, currentObject, currentInitiateMultipartUploadResult.getUploadId())); + // warn + needInitMultipartTransform = false; + currentPartNumber = 1; + } + // write: upload data to current object + byte[] data; + if (column instanceof BytesColumn) { + data = column.asBytes(); + byteArrayOutputStream.write(data); + currentSize += data.length; + } else { + String message = "the type of column must be BytesColumn!"; + throw DataXException.asDataXException(OssWriterErrorCode.Write_OBJECT_ERROR, message); + } + if (currentSize >= this.blockSizeInByte) { + this.ossWriterProxy.uploadOnePart(byteArrayOutputStream.toByteArray(), currentPartNumber, + currentInitiateMultipartUploadResult, currentPartETags, currentObject); + currentPartNumber++; + currentSize = 0; + byteArrayOutputStream.reset(); + } + } + + // TODO binary 模式读取,源头为空文件时是有问题的 + if (!gotData) { + LOG.info("Receive no data from the source."); + currentInitiateMultipartUploadRequest = new InitiateMultipartUploadRequest(this.bucket, + currentObject); + currentInitiateMultipartUploadResult = this.ossWriterProxy.initiateMultipartUpload( + currentInitiateMultipartUploadRequest); + currentPartETags = new ArrayList(); + } + + // warn: may be some data stall in byteArrayOutputStream + if (byteArrayOutputStream.size() > 0) { + this.ossWriterProxy.uploadOnePart(byteArrayOutputStream.toByteArray(), currentPartNumber, + currentInitiateMultipartUploadResult, currentPartETags, currentObject); + currentPartNumber++; + } + + // 避免重复提交 + if (!StringUtils.equals(lastUploadId, currentInitiateMultipartUploadResult.getUploadId())) { + CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest( + this.bucket, currentObject, currentInitiateMultipartUploadResult.getUploadId(), + currentPartETags); + CompleteMultipartUploadResult completeMultipartUploadResult = this.ossWriterProxy.completeMultipartUpload( + completeMultipartUploadRequest); + LOG.info(String.format("final object etag is:[%s]", completeMultipartUploadResult.getETag())); + } + } catch (IOException e) { + // 脏数据UnstructuredStorageWriterUtil.transportOneRecord已经记录,header + // 都是字符串不认为有脏数据 + throw DataXException.asDataXException(OssWriterErrorCode.Write_OBJECT_ERROR, e.getMessage(), e); + } catch (Exception e) { + throw DataXException.asDataXException(OssWriterErrorCode.Write_OBJECT_ERROR, e.getMessage(), e); + } + LOG.info("end do write"); + } + + /** + * 开始写半结构化文件 + * + * @param lineReceiver + */ + private void startWriteUnstructedStorageFile(RecordReceiver lineReceiver, boolean generateEmptyFile){ // 设置每块字符串长度 - final long partSize = 1024 * 1024 * 10L; - long numberCacul = (this.maxFileSize * 1024 * 1024L) / partSize; + long numberCacul = (this.maxFileSize * 1024 * 1024L) / this.blockSizeInByte; final long maxPartNumber = numberCacul >= 1 ? numberCacul : 1; int objectRollingNumber = 0; - //warn: may be StringBuffer->StringBuilder - StringWriter sw = new StringWriter(); - StringBuffer sb = sw.getBuffer(); - UnstructuredWriter unstructuredWriter = TextCsvWriterManager - .produceUnstructuredWriter(this.fileFormat, - this.fieldDelimiter, sw); - Record record = null; - - LOG.info(String.format( - "begin do write, each object maxFileSize: [%s]MB...", - maxPartNumber * 10)); + Record record; String currentObject = this.object; - InitiateMultipartUploadRequest currentInitiateMultipartUploadRequest = null; + if (this.isPeer2PeerCopyMode()) { + currentObject = null; + } else { + // 加上suffix + currentObject = appedSuffixTo(currentObject); + } + InitiateMultipartUploadRequest currentInitiateMultipartUploadRequest; InitiateMultipartUploadResult currentInitiateMultipartUploadResult = null; + String lastUploadId = null; boolean gotData = false; List currentPartETags = null; // to do: // 可以根据currentPartNumber做分块级别的重试,InitiateMultipartUploadRequest多次一个currentPartNumber会覆盖原有 int currentPartNumber = 1; + Map meta; + + //warn: may be StringBuffer->StringBuilder + StringWriter sw = new StringWriter(); + StringBuffer sb = sw.getBuffer(); + UnstructuredWriter unstructuredWriter = UnstructuredStorageWriterUtil. + produceUnstructuredWriter(this.fileFormat, this.writerSliceConfig, sw); + LOG.info(String.format( + "begin do write, each object maxFileSize: [%s]MB...", + maxPartNumber * 10)); try { - // warn + // warn 源头可能是MySQL中,导致没有meta这个第一次初始化标示省不掉 boolean needInitMultipartTransform = true; while ((record = lineReceiver.getFromReader()) != null) { + meta = record.getMeta(); gotData = true; - // init:begin new multipart upload - if (needInitMultipartTransform) { - if (objectRollingNumber == 0) { - if (StringUtils.isBlank(this.suffix)) { - currentObject = this.object; - } else { - currentObject = String.format("%s%s", - this.object, this.suffix); - } - } else { - // currentObject is like(no suffix) - // myfile__9b886b70fbef11e59a3600163e00068c_1 - if (StringUtils.isBlank(this.suffix)) { - currentObject = String.format("%s_%s", - this.object, objectRollingNumber); - } else { - // or with suffix - // myfile__9b886b70fbef11e59a3600163e00068c_1.csv - currentObject = String.format("%s_%s%s", - this.object, objectRollingNumber, - this.suffix); + // init: 2 condition begin new multipart upload 轮转策略(文件名规则)不一致 + // condition: 对等拷贝模式 && Record中的Meta切换文件名 && + // condition: 类log4j日志轮转 && !对等拷贝模式 + boolean realyNeedInitUploadRequest = false; + if (this.isPeer2PeerCopyMode()) { + assert meta != null; + String objectNameTmp = meta + .get(com.alibaba.datax.plugin.unstructuredstorage.writer.Key.META_KEY_FILE_PATH); + String fullObjectNameTmp = String.format("%s%s", this.objectDir, objectNameTmp.substring(this.parentPathLength, objectNameTmp.length())); + if (!StringUtils.equals(currentObject, fullObjectNameTmp)) { + currentObject = fullObjectNameTmp; + realyNeedInitUploadRequest = true; + } + } else { + if (needInitMultipartTransform || currentPartNumber > maxPartNumber) { + currentObject = getCurrentObject(objectRollingNumber, record); + objectRollingNumber++; + realyNeedInitUploadRequest = true; + } + } + + if (realyNeedInitUploadRequest) { + // 先将上一个分块上传的request complete掉 + if (null != currentInitiateMultipartUploadResult) { + if (sb.length() > 0) { + this.uploadOnePart(sw, currentPartNumber, currentInitiateMultipartUploadResult, + currentPartETags, currentObject); + currentPartNumber++; + sb.setLength(0); } + // TODO 如果当前文件是空文件 + String commitKey = currentInitiateMultipartUploadResult.getKey(); + LOG.info(String.format( + "current object [%s] size %s, complete current multipart upload %s and begin new one", + commitKey, currentPartNumber * this.blockSizeInByte, + currentInitiateMultipartUploadResult.getUploadId())); + CompleteMultipartUploadRequest currentCompleteMultipartUploadRequest = new CompleteMultipartUploadRequest( + this.bucket, commitKey, currentInitiateMultipartUploadResult.getUploadId(), + currentPartETags); + CompleteMultipartUploadResult currentCompleteMultipartUploadResult = this.ossWriterProxy.completeMultipartUpload( + currentCompleteMultipartUploadRequest); + lastUploadId = currentInitiateMultipartUploadResult.getUploadId(); + LOG.info(String.format("final object [%s] etag is:[%s]", commitKey, + currentCompleteMultipartUploadResult.getETag())); } - objectRollingNumber++; - currentInitiateMultipartUploadRequest = new InitiateMultipartUploadRequest( - this.bucket, currentObject); - currentInitiateMultipartUploadResult = this.ossClient - .initiateMultipartUpload(currentInitiateMultipartUploadRequest); + + currentInitiateMultipartUploadRequest = this.ossWriterProxy.getInitiateMultipartUploadRequest(currentObject); + currentInitiateMultipartUploadResult = this.ossWriterProxy.initiateMultipartUpload(currentInitiateMultipartUploadRequest); currentPartETags = new ArrayList(); LOG.info(String .format("write to bucket: [%s] object: [%s] with oss uploadId: [%s]", @@ -365,43 +1014,22 @@ public void startWrite(RecordReceiver lineReceiver) { // write: upload data to current object UnstructuredStorageWriterUtil.transportOneRecord(record, this.nullFormat, this.dateParse, - this.getTaskPluginCollector(), unstructuredWriter); + this.getTaskPluginCollector(), unstructuredWriter, this.byteEncoding); - if (sb.length() >= partSize) { + if (sb.length() >= this.blockSizeInByte) { this.uploadOnePart(sw, currentPartNumber, currentInitiateMultipartUploadResult, currentPartETags, currentObject); currentPartNumber++; sb.setLength(0); } - - // save: end current multipart upload - if (currentPartNumber > maxPartNumber) { - LOG.info(String - .format("current object [%s] size > %s, complete current multipart upload and begin new one", - currentObject, currentPartNumber - * partSize)); - CompleteMultipartUploadRequest currentCompleteMultipartUploadRequest = new CompleteMultipartUploadRequest( - this.bucket, currentObject, - currentInitiateMultipartUploadResult - .getUploadId(), currentPartETags); - CompleteMultipartUploadResult currentCompleteMultipartUploadResult = this.ossClient - .completeMultipartUpload(currentCompleteMultipartUploadRequest); - LOG.info(String.format( - "final object [%s] etag is:[%s]", - currentObject, - currentCompleteMultipartUploadResult.getETag())); - // warn - needInitMultipartTransform = true; - } } if (!gotData) { LOG.info("Receive no data from the source."); currentInitiateMultipartUploadRequest = new InitiateMultipartUploadRequest( this.bucket, currentObject); - currentInitiateMultipartUploadResult = this.ossClient - .initiateMultipartUpload(currentInitiateMultipartUploadRequest); + currentInitiateMultipartUploadResult = this.ossWriterProxy.initiateMultipartUpload(currentInitiateMultipartUploadRequest); currentPartETags = new ArrayList(); // each object's header if (null != this.header && !this.header.isEmpty()) { @@ -414,81 +1042,145 @@ public void startWrite(RecordReceiver lineReceiver) { currentInitiateMultipartUploadResult, currentPartETags, currentObject); } - CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest( - this.bucket, currentObject, - currentInitiateMultipartUploadResult.getUploadId(), - currentPartETags); - CompleteMultipartUploadResult completeMultipartUploadResult = this.ossClient - .completeMultipartUpload(completeMultipartUploadRequest); - LOG.info(String.format("final object etag is:[%s]", - completeMultipartUploadResult.getETag())); + + // 避免重复提交 + if (!StringUtils.equals(lastUploadId, currentInitiateMultipartUploadResult.getUploadId())) { + CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest( + this.bucket, currentObject, + currentInitiateMultipartUploadResult.getUploadId(), + currentPartETags); + if (gotData) { + completeUpload(completeMultipartUploadRequest); + } else{ + if (generateEmptyFile) { + LOG.info("Due to without data, oss will generate empty file, " + + "the generateEmptyFile is {}, you can set it false to avoid this",generateEmptyFile); + completeUpload(completeMultipartUploadRequest); + } else { + LOG.info("The generateEmptyFile is false, datax will not generate empty file"); + } + } + } } catch (IOException e) { // 脏数据UnstructuredStorageWriterUtil.transportOneRecord已经记录,header // 都是字符串不认为有脏数据 throw DataXException.asDataXException( - OssWriterErrorCode.Write_OBJECT_ERROR, e.getMessage()); + OssWriterErrorCode.Write_OBJECT_ERROR, e.getMessage(), e); } catch (Exception e) { throw DataXException.asDataXException( - OssWriterErrorCode.Write_OBJECT_ERROR, e.getMessage()); + OssWriterErrorCode.Write_OBJECT_ERROR, e.getMessage(), e); } LOG.info("end do write"); } + private void completeUpload(CompleteMultipartUploadRequest completeMultipartUploadRequest) throws Exception { + CompleteMultipartUploadResult completeMultipartUploadResult = this.ossWriterProxy.completeMultipartUpload(completeMultipartUploadRequest); + LOG.info(String.format("final object etag is:[%s]", + completeMultipartUploadResult.getETag())); + } + + + private String getCurrentObject(int objectRollingNumber, Record record) { + String currentObject = this.object; + + if (!this.partition.isEmpty()) { + String partitionValues = getPartitionValues(record); + currentObject = String.format("%s_%s", currentObject, partitionValues); + } + + if (objectRollingNumber > 0) { + currentObject = String.format("%s_%s", currentObject, objectRollingNumber); + } + + currentObject = appedSuffixTo(currentObject); + + return currentObject; + } + + private String getPartitionValues(Record record) { + // config like "partition": "ds,venture" + String partitionValues = ""; + // assume that partition columns are located in the last of order + for (int i = 0; i < this.partition.size(); i++) { + partitionValues += record.getColumn(record.getColumnNumber() - 1 - i).asString(); + } + return partitionValues; + } + + private String appedSuffixTo(String currentObject) { + StringBuilder sbCurrentObject = new StringBuilder(currentObject); + + if (StringUtils.isNotBlank(this.suffix)) { + if (!this.suffix.startsWith(".")) { + sbCurrentObject.append("."); + } + sbCurrentObject.append(suffix); + } + + return sbCurrentObject.toString(); + } + /** * 对于同一个UploadID,该号码不但唯一标识这一块数据,也标识了这块数据在整个文件内的相对位置。 * 如果你用同一个part号码,上传了新的数据,那么OSS上已有的这个号码的Part数据将被覆盖。 - * + * * @throws Exception * */ private void uploadOnePart( final StringWriter sw, final int partNumber, - final InitiateMultipartUploadResult initiateMultipartUploadResult, + final InitiateMultipartUploadResult currentInitiateMultipartUploadResult, final List partETags, final String currentObject) throws Exception { final String encoding = this.encoding; - final String bucket = this.bucket; - final OSSClient ossClient = this.ossClient; - RetryUtil.executeWithRetry(new Callable() { - @Override - public Boolean call() throws Exception { - byte[] byteArray = sw.toString().getBytes(encoding); - InputStream inputStream = new ByteArrayInputStream( - byteArray); - // 创建UploadPartRequest,上传分块 - UploadPartRequest uploadPartRequest = new UploadPartRequest(); - uploadPartRequest.setBucketName(bucket); - uploadPartRequest.setKey(currentObject); - uploadPartRequest.setUploadId(initiateMultipartUploadResult - .getUploadId()); - uploadPartRequest.setInputStream(inputStream); - uploadPartRequest.setPartSize(byteArray.length); - uploadPartRequest.setPartNumber(partNumber); - UploadPartResult uploadPartResult = ossClient - .uploadPart(uploadPartRequest); - partETags.add(uploadPartResult.getPartETag()); - LOG.info(String - .format("upload part [%s] size [%s] Byte has been completed.", - partNumber, byteArray.length)); - IOUtils.closeQuietly(inputStream); - return true; - } - }, 3, 1000L, false); + final byte[] byteArray = sw.toString().getBytes(encoding); + this.ossWriterProxy.uploadOnePart(byteArray, partNumber, currentInitiateMultipartUploadResult, partETags, currentObject); } @Override public void prepare() { - + if(useHdfsWriterProxy){ + hdfsWriterTask.prepare(); + return; + } } @Override public void post() { - + if(useHdfsWriterProxy){ + hdfsWriterTask.post(); + return; + } } @Override public void destroy() { + if(useHdfsWriterProxy){ + hdfsWriterTask.destroy(); + return; + } + try { + // this.ossClient.shutdown(); + } catch (Exception e) { + LOG.warn("shutdown ossclient meet a exception:" + e.getMessage(), e); + } + } + + private boolean isPeer2PeerCopyMode() { + return this.isBinaryFile + || com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.SYNC_MODE_VALUE_COPY + .equalsIgnoreCase(this.syncMode); + } + private String getObjectDir(String object) { + String dir = null; + if (StringUtils.isBlank(object)) { + dir = ""; + } else { + dir = object.trim(); + dir = dir.endsWith("/") ? dir : String.format("%s/", dir); + } + return dir; } } } diff --git a/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/OssWriterProxy.java b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/OssWriterProxy.java new file mode 100644 index 0000000000..45516f73d4 --- /dev/null +++ b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/OssWriterProxy.java @@ -0,0 +1,171 @@ +package com.alibaba.datax.plugin.writer.osswriter; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.RetryUtil; +import com.aliyun.oss.OSSClient; +import com.aliyun.oss.model.*; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.util.List; +import java.util.concurrent.Callable; + +/** + * @Author: guxuan + * @Date 2022-05-17 16:29 + */ +public class OssWriterProxy { + private static Logger logger = LoggerFactory.getLogger(OssWriterProxy.class); + + private OSSClient ossClient; + private Configuration configuration; + /** + * 是否在服务器端进行加密存储 + */ + private Boolean encrypt; + private String bucket; + + + public OssWriterProxy (Configuration configuration, OSSClient ossClient) { + this.configuration = configuration; + this.ossClient = ossClient; + this.encrypt = configuration.getBool(Key.ENCRYPT, false); + this.bucket = configuration.getString(Key.BUCKET); + } + + public InitiateMultipartUploadRequest getInitiateMultipartUploadRequest(String currentObject){ + InitiateMultipartUploadRequest currentInitiateMultipartUploadRequest; + if( !this.encrypt ) { + currentInitiateMultipartUploadRequest = new InitiateMultipartUploadRequest( + this.bucket, currentObject); + } else { + // 将数据加密存储在oss + ObjectMetadata objectMetadata = new ObjectMetadata(); + objectMetadata.setHeader("x-oss-server-side-encryption", + ObjectMetadata.AES_256_SERVER_SIDE_ENCRYPTION); + currentInitiateMultipartUploadRequest = new InitiateMultipartUploadRequest( + this.bucket, currentObject, objectMetadata); + } + return currentInitiateMultipartUploadRequest; + } + + public InitiateMultipartUploadResult initiateMultipartUpload( + final InitiateMultipartUploadRequest currentInitiateMultipartUploadRequest) throws Exception { + final OSSClient ossClient = this.ossClient; + return RetryUtil.executeWithRetry(new Callable() { + @Override + public InitiateMultipartUploadResult call() throws Exception { + return ossClient.initiateMultipartUpload(currentInitiateMultipartUploadRequest); + } + }, 10, 1000L, false); + } + + public CompleteMultipartUploadResult completeMultipartUpload( + final CompleteMultipartUploadRequest currentCompleteMultipartUploadRequest) throws Exception { + + final OSSClient ossClient = this.ossClient; + return RetryUtil.executeWithRetry(new Callable() { + @Override + public CompleteMultipartUploadResult call() throws Exception { + return ossClient.completeMultipartUpload(currentCompleteMultipartUploadRequest); + } + }, 10, 1000L, false); + } + + public void uploadOnePart( + final byte[] byteArray, + final int partNumber, + final InitiateMultipartUploadResult currentInitiateMultipartUploadResult, + final List partETags, + final String currentObject) + throws Exception { + final String bucket = this.bucket; + final OSSClient ossClient = this.ossClient; + RetryUtil.executeWithRetry(new Callable() { + @Override + public Boolean call() throws Exception { + InputStream inputStream = new ByteArrayInputStream( + byteArray); + // 创建UploadPartRequest,上传分块 + UploadPartRequest uploadPartRequest = new UploadPartRequest(); + uploadPartRequest.setBucketName(bucket); + uploadPartRequest.setKey(currentObject); + uploadPartRequest.setUploadId(currentInitiateMultipartUploadResult.getUploadId()); + uploadPartRequest.setInputStream(inputStream); + uploadPartRequest.setPartSize(byteArray.length); + uploadPartRequest.setPartNumber(partNumber); + UploadPartResult uploadPartResult = ossClient + .uploadPart(uploadPartRequest); + partETags.add(uploadPartResult.getPartETag()); + logger.info(String + .format("upload part [%s] size [%s] Byte has been completed.", + partNumber, byteArray.length)); + IOUtils.closeQuietly(inputStream); + return true; + } + }, 10, 1000L, false); + } + + public void abortMultipartUpload(final String currentObject, final String uploadId) { + final String bucket = this.bucket; + final OSSClient ossClient = this.ossClient; + try { + RetryUtil.executeWithRetry((Callable) () -> { + AbortMultipartUploadRequest abortMultipartUploadRequest = + new AbortMultipartUploadRequest(bucket, currentObject, uploadId); + ossClient.abortMultipartUpload(abortMultipartUploadRequest); + return null; + }, 5, 1, true); + } catch (Throwable e) { + logger.error(String.format("AbortMultipartUpload failed, msg is %s",e.getMessage()), e); + } + } + + public void uploadOnePartForSingleObject( + final byte[] byteArray, + final String uploadId, + final List partETags, + final String currentObject, + final HeaderProvider headerProvider) + throws Exception { + final String bucket = this.bucket; + final OSSClient ossClient = this.ossClient; + RetryUtil.executeWithRetry(new Callable() { + @Override + public Boolean call() throws Exception { + // 创建UploadPartRequest,上传分块 + UploadPartRequest uploadPartRequest = new UploadPartRequest(); + uploadPartRequest.setPartNumber(OssSingleObject.currentPartNumber.getAndIncrement()); + byte[] data = byteArray; + if (uploadPartRequest.getPartNumber() == 1) { + // write header + byte[] headerBytes = headerProvider.getHeader(); + logger.info("write header to part {}. header size: {}", + uploadPartRequest.getPartNumber(), ArrayUtils.getLength(headerBytes)); + data = ArrayUtils.addAll(headerBytes, byteArray); + } + ByteArrayInputStream inputStream = new ByteArrayInputStream(data); + uploadPartRequest.setBucketName(bucket); + uploadPartRequest.setKey(currentObject); + uploadPartRequest.setUploadId(uploadId); + uploadPartRequest.setInputStream(inputStream); + uploadPartRequest.setPartSize(data.length); + UploadPartResult uploadPartResult = ossClient + .uploadPart(uploadPartRequest); + partETags.add(uploadPartResult.getPartETag()); + logger.info("upload part number [{}] size [{}] Byte has been completed, uploadId: {}.", + uploadPartRequest.getPartNumber(), data.length, uploadId); + IOUtils.closeQuietly(inputStream); + return true; + } + }, 10, 1000L, false); + } + + public interface HeaderProvider { + byte[] getHeader() throws Exception; + } +} diff --git a/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/parquet/ParquetFileProccessor.java b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/parquet/ParquetFileProccessor.java new file mode 100644 index 0000000000..c88a6f109f --- /dev/null +++ b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/parquet/ParquetFileProccessor.java @@ -0,0 +1,49 @@ +package com.alibaba.datax.plugin.writer.osswriter.parquet; + +import org.apache.hadoop.fs.Path; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; + +import parquet.hadoop.ParquetWriter; +import parquet.hadoop.metadata.CompressionCodecName; +import parquet.schema.MessageType; + +import java.io.IOException; + +/** + * @Author: guxuan + * @Date 2022-05-17 16:23 + */ +public class ParquetFileProccessor extends ParquetWriter { + private Path path; + + public ParquetFileProccessor(Path path, MessageType schema, Configuration taskConfig, + TaskPluginCollector taskPluginCollector) throws IOException { + this(path, schema, false, taskConfig, taskPluginCollector); + this.path = path; + } + + public ParquetFileProccessor(Path path, MessageType schema, boolean enableDictionary, Configuration taskConfig, + TaskPluginCollector taskPluginCollector) throws IOException { + this(path, schema, CompressionCodecName.UNCOMPRESSED, enableDictionary, taskConfig, taskPluginCollector); + this.path = path; + } + + public ParquetFileProccessor(Path path, MessageType schema, CompressionCodecName codecName, + boolean enableDictionary, Configuration taskConfig, TaskPluginCollector taskPluginCollector) + throws IOException { + super(path, new ParquetFileSupport(schema, taskConfig, taskPluginCollector), codecName, DEFAULT_BLOCK_SIZE, + DEFAULT_PAGE_SIZE, enableDictionary, false); + this.path = path; + } + + public byte[] getParquetRawData() { + if (null == this.path) { + return null; + } else { + return null; + } + } +} diff --git a/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/parquet/ParquetFileSupport.java b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/parquet/ParquetFileSupport.java new file mode 100644 index 0000000000..c3ff777c87 --- /dev/null +++ b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/parquet/ParquetFileSupport.java @@ -0,0 +1,355 @@ +package com.alibaba.datax.plugin.writer.osswriter.parquet; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.plugin.unstructuredstorage.writer.Key; +import com.alibaba.datax.plugin.writer.osswriter.Constant; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONArray; +import com.alibaba.fastjson2.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.apache.hadoop.conf.Configuration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import parquet.hadoop.api.WriteSupport; +import parquet.io.api.Binary; +import parquet.io.api.RecordConsumer; +import parquet.schema.*; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.List; + +/** + * @Author: guxuan + * @Date 2022-05-17 16:25 + */ +public class ParquetFileSupport extends WriteSupport { + public static final Logger LOGGER = LoggerFactory.getLogger(ParquetFileSupport.class); + private MessageType schema; + private RecordConsumer recordConsumer; + private boolean printStackTrace = true; + + // 不通类型的nullFormat + private String nullFormat; + + private String dateFormat; + private DateFormat dateParse; + private Binary binaryForNull; + private TaskPluginCollector taskPluginCollector; + + public ParquetFileSupport(MessageType schema, com.alibaba.datax.common.util.Configuration taskConfig, TaskPluginCollector taskPluginCollector) { + this.schema = schema; + // 不通类型的nullFormat + this.nullFormat = taskConfig.getString(Key.NULL_FORMAT, Constant.DEFAULT_NULL_FORMAT); + this.binaryForNull = Binary.fromString(this.nullFormat); + + this.dateFormat = taskConfig.getString(Key.DATE_FORMAT, null); + if (StringUtils.isNotBlank(this.dateFormat)) { + this.dateParse = new SimpleDateFormat(dateFormat); + } + + this.taskPluginCollector = taskPluginCollector; + } + + @Override + public WriteContext init(Configuration configuration) { + return new WriteContext(schema, new HashMap()); + } + + @Override + public void prepareForWrite(RecordConsumer recordConsumer) { + this.recordConsumer = recordConsumer; + } + + @Override + public void write(Record values) { + LOGGER.info("Writing parquet data using fields mode(The correct mode.)"); + List types = this.schema.getFields(); + if (values != null && types != null && values.getColumnNumber() == types.size()) { + recordConsumer.startMessage(); + writeFields(types, values); + recordConsumer.endMessage(); + } + } + + private void writeFields(List types, Record values) { + for (int i = 0; i < types.size(); i++) { + Type type = types.get(i); + Column value = values.getColumn(i); + if (value != null) { + try { + if (type.isPrimitive()) { + writePrimitiveType(type, value, i); + } else { + writeGroupType(type, (JSON) JSON.parse(value.asString()), i); + } + } catch (Exception e) { + if (printStackTrace) { + printStackTrace = false; + LOGGER.warn("write to parquet error: {}", e.getMessage(), e); + } + // dirty data + if (null != this.taskPluginCollector) { + // job post 里面的merge taskPluginCollector 为null + this.taskPluginCollector.collectDirtyRecord(values, e, e.getMessage()); + } + } + } + } + } + + private void writeFields(List types, JSONObject values) { + for (int i = 0; i < types.size(); i++) { + Type type = types.get(i); + Object value = values.get(type.getName()); + + if (value != null) { + try { + if (type.isPrimitive()) { + writePrimitiveType(type, value, i); + } else { + writeGroupType(type, (JSON) value, i); + } + } catch (Exception e) { + if (printStackTrace) { + printStackTrace = false; + LOGGER.warn("write to parquet error: {}", e.getMessage(), e); + } + } + } else { + recordConsumer.addBinary(this.binaryForNull); + } + } + } + + private void writeGroupType(Type type, JSON value, int index) { + GroupType groupType = type.asGroupType(); + OriginalType originalType = groupType.getOriginalType(); + if (originalType != null) { + switch (originalType) { + case MAP: + writeMap(groupType, value, index); + break; + case LIST: + writeList(groupType, value, index); + break; + default: + break; + } + } else { + // struct + writeStruct(groupType, value, index); + } + } + + private void writeMap(GroupType groupType, JSON value, int index) { + if (value == null) { + return; + } + + JSONObject json = (JSONObject) value; + + if (json.isEmpty()) { + return; + } + + recordConsumer.startField(groupType.getName(), index); + + recordConsumer.startGroup(); + + // map + // key_value start + recordConsumer.startField("key_value", 0); + recordConsumer.startGroup(); + + List keyValueFields = groupType.getFields().get(0).asGroupType().getFields(); + Type keyType = keyValueFields.get(0); + Type valueType = keyValueFields.get(1); + for (String key : json.keySet()) { + // key + writePrimitiveType(keyType, key, 0); + + // value + if (valueType.isPrimitive()) { + writePrimitiveType(valueType, json.get(key), 1); + } else { + writeGroupType(valueType, (JSON) json.get(key), 1); + } + } + + recordConsumer.endGroup(); + recordConsumer.endField("key_value", 0); + // key_value end + + recordConsumer.endGroup(); + recordConsumer.endField(groupType.getName(), index); + } + + private void writeList(GroupType groupType, JSON value, int index) { + if (value == null) { + return; + } + + JSONArray json = (JSONArray) value; + + if (json.isEmpty()) { + return; + } + + recordConsumer.startField(groupType.getName(), index); + // list + recordConsumer.startGroup(); + + + // list start + recordConsumer.startField("list", 0); + recordConsumer.startGroup(); + + Type elementType = groupType.getFields().get(0).asGroupType().getFields().get(0); + + if (elementType.isPrimitive()) { + for (Object elementValue : json) { + writePrimitiveType(elementType, elementValue, 0); + } + } else { + for (Object elementValue : json) { + writeGroupType(elementType, (JSON) elementValue, 0); + } + } + + recordConsumer.endGroup(); + recordConsumer.endField("list", 0); + // list end + recordConsumer.endGroup(); + + recordConsumer.endField(groupType.getName(), index); + } + + private void writeStruct(GroupType groupType, JSON value, int index) { + if (value == null) { + return; + } + JSONObject json = (JSONObject) value; + if (json.isEmpty()) { + return; + } + + recordConsumer.startField(groupType.getName(), index); + // struct start + recordConsumer.startGroup(); + + writeFields(groupType.getFields(), json); + recordConsumer.endGroup(); + // struct end + recordConsumer.endField(groupType.getName(), index); + } + + private void writePrimitiveType(Type type, Object value, int index) { + if (value == null) { + return; + } + + recordConsumer.startField(type.getName(), index); + PrimitiveType primitiveType = type.asPrimitiveType(); + + switch (primitiveType.getPrimitiveTypeName()) { + case BOOLEAN: + recordConsumer.addBoolean((Boolean) value); + break; + case FLOAT: + if (value instanceof Float) { + recordConsumer.addFloat(((Float) value).floatValue()); + } else if (value instanceof Double) { + recordConsumer.addFloat(((Double) value).floatValue()); + } else if (value instanceof Long) { + recordConsumer.addFloat(((Long) value).floatValue()); + } else if (value instanceof Integer) { + recordConsumer.addFloat(((Integer) value).floatValue()); + } + break; + case DOUBLE: + if (value instanceof Float) { + recordConsumer.addDouble(((Float) value).doubleValue()); + } else if (value instanceof Double) { + recordConsumer.addDouble(((Double) value).doubleValue()); + } else if (value instanceof Long) { + recordConsumer.addDouble(((Long) value).doubleValue()); + } else if (value instanceof Integer) { + recordConsumer.addDouble(((Integer) value).doubleValue()); + } + break; + case INT32: + if (value instanceof Integer) { + recordConsumer.addInteger((Integer) value); + } else if (value instanceof Long) { + recordConsumer.addInteger(((Long) value).intValue()); + } else { + new IllegalArgumentException( + String.format("Invalid value: %s(clazz: %s) for field: %s", value, value.getClass(), type.getName()) + ); + } + break; + case INT64: + case INT96: + if (value instanceof Integer) { + recordConsumer.addLong(((Integer) value).longValue()); + } else if (value instanceof Long) { + recordConsumer.addInteger(((Long) value).intValue()); + } else { + new IllegalArgumentException( + String.format("Invalid value: %s(clazz: %s) for field: %s", value, value.getClass(), type.getName()) + ); + } + break; + case BINARY: + default: + recordConsumer.addBinary(Binary.fromString((String) value)); + break; + } + recordConsumer.endField(type.getName(), index); + } + + private void writePrimitiveType(Type type, Column value, int index) { + if (value == null || value.getRawData() == null) { + return; + } + + recordConsumer.startField(type.getName(), index); + PrimitiveType primitiveType = type.asPrimitiveType(); + switch (primitiveType.getPrimitiveTypeName()) { + case BOOLEAN: + recordConsumer.addBoolean(value.asBoolean()); + break; + case FLOAT: + recordConsumer.addFloat(value.asDouble().floatValue()); + break; + case DOUBLE: + recordConsumer.addDouble(value.asDouble()); + break; + case INT32: + recordConsumer.addInteger(value.asLong().intValue()); + break; + case INT64: + case INT96: + recordConsumer.addLong(value.asLong()); + break; + case BINARY: + String valueAsString2Write = null; + if (Column.Type.DATE == value.getType() && null != this.dateParse) { + valueAsString2Write = dateParse.format(value.asDate()); + } + else { + valueAsString2Write = value.asString(); + } + recordConsumer.addBinary(Binary.fromString(valueAsString2Write)); + break; + default: + recordConsumer.addBinary(Binary.fromString(value.asString())); + break; + } + recordConsumer.endField(type.getName(), index); + } +} diff --git a/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/util/HandlerUtil.java b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/util/HandlerUtil.java new file mode 100644 index 0000000000..488c119ce5 --- /dev/null +++ b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/util/HandlerUtil.java @@ -0,0 +1,38 @@ +package com.alibaba.datax.plugin.writer.osswriter.util; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.core.util.container.CoreConstant; +import com.alibaba.datax.plugin.writer.osswriter.Key; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @Author: guxuan + * @Date 2022-05-17 16:35 + */ +public class HandlerUtil { + + private static final Logger LOG = LoggerFactory.getLogger(HandlerUtil.class); + + /** + * 将configuration处理成 ODPS->OSS的 config + * + * @param jobConfiguration + */ + public static void preHandler(Configuration jobConfiguration) { + LOG.info("================ OssWriter Phase 1 preHandler starting... ================ "); + Configuration writerOriginPluginConf = jobConfiguration.getConfiguration( + CoreConstant.DATAX_JOB_CONTENT_WRITER_PARAMETER); + Configuration writerOssPluginConf = writerOriginPluginConf.getConfiguration(Key.OSS_CONFIG); + Configuration newWriterPluginConf = Configuration.newDefault(); + jobConfiguration.remove(CoreConstant.DATAX_JOB_CONTENT_WRITER_PARAMETER); + //将postgresqlwriter的pg配置注入到postgresqlConfig中, 供后面的postHandler使用 + writerOriginPluginConf.remove(Key.OSS_CONFIG); + newWriterPluginConf.set(Key.POSTGRESQL_CONFIG, writerOriginPluginConf); + newWriterPluginConf.merge(writerOssPluginConf, true); + //设置writer的名称为osswriter + jobConfiguration.set(CoreConstant.DATAX_JOB_CONTENT_WRITER_NAME, "osswriter"); + jobConfiguration.set(CoreConstant.DATAX_JOB_CONTENT_WRITER_PARAMETER, newWriterPluginConf); + LOG.info("================ OssWriter Phase 1 preHandler end... ================ "); + } +} diff --git a/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/util/HdfsParquetUtil.java b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/util/HdfsParquetUtil.java new file mode 100644 index 0000000000..dc102dac73 --- /dev/null +++ b/osswriter/src/main/java/com/alibaba/datax/plugin/writer/osswriter/util/HdfsParquetUtil.java @@ -0,0 +1,145 @@ +package com.alibaba.datax.plugin.writer.osswriter.util; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.hdfswriter.HdfsWriter; +import com.alibaba.datax.plugin.writer.osswriter.Key; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.hadoop.fs.FileSystem; +import org.apache.hadoop.fs.Path; +import org.apache.hadoop.fs.aliyun.oss.AliyunOSSFileSystem; +import org.apache.hadoop.mapred.JobConf; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: guxuan + * @Date 2022-05-17 16:35 + */ +public class HdfsParquetUtil { + + + private static final Logger logger = LoggerFactory.getLogger(HdfsParquetUtil.class); + + public static boolean isUseHdfsWriterProxy( String fileFormat){ + if("orc".equalsIgnoreCase(fileFormat) || "parquet".equalsIgnoreCase(fileFormat)){ + return true; + } + return false; + } + + /** + * 配置writerSliceConfig 适配hdfswriter写oss parquet + * https://help.aliyun.com/knowledge_detail/74344.html + * @param hdfsWriterJob + * @param writerSliceConfig + */ + public static void adaptConfiguration(HdfsWriter.Job hdfsWriterJob, Configuration writerSliceConfig){ + String fileFormat = writerSliceConfig.getString( + com.alibaba.datax.plugin.unstructuredstorage.writer.Key.FILE_FORMAT, + com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.FILE_FORMAT_TEXT); + + String bucket = writerSliceConfig.getString(Key.BUCKET); + String fs =String.format("oss://%s",bucket); + writerSliceConfig.set(com.alibaba.datax.plugin.writer.hdfswriter.Key.DEFAULT_FS,fs); + writerSliceConfig.set(com.alibaba.datax.plugin.writer.hdfswriter.Key.FILE_TYPE, + writerSliceConfig.getString(com.alibaba.datax.plugin.unstructuredstorage.writer.Key.FILE_FORMAT)); + + /** + * "writeMode"、 "compress"、"encoding" 、path、fileName 相互一致 + */ + JSONObject hadoopConfig = new JSONObject(); + hadoopConfig.put(Key.FS_OSS_ACCESSID,writerSliceConfig.getString(Key.ACCESSID)); + hadoopConfig.put(Key.FS_OSS_ACCESSKEY,writerSliceConfig.getString(Key.ACCESSKEY)); + hadoopConfig.put(Key.FS_OSS_ENDPOINT,writerSliceConfig.getString(Key.ENDPOINT)); + writerSliceConfig.set(Key.HDOOP_CONFIG,Configuration.from(JSON.toJSONString(hadoopConfig))); + + String object = writerSliceConfig.getString(Key.OBJECT); + String path = writerSliceConfig.getString(Key.PATH); + String fielName = writerSliceConfig.getString(Key.FILE_NAME); + + if (StringUtils.isNotBlank(object) && (StringUtils.isNotBlank(path) || StringUtils.isNotBlank(fielName))) { + logger.warn("You configure both the \"object\" property and the \"path\" or \"fileName\" property, ignoring the object property. " + + "It is recommended to remove the \"path\" or \"fileName\" attribute, which has been deprecated."); + } + + //兼容之前配置了PATH的datax任务, 如果已经配置了PATH,则无需从object里解析 + if (StringUtils.isBlank(path)) { + Validate.notBlank(object, "object can't be blank!"); + writerSliceConfig.set(Key.PATH, getPathAndFileNameFromObject(object.trim()).get(Key.PATH)); + } + //兼容之前配置了fileName的datax任务,如果已经配置了fileName,则无需从object里解析 + if (StringUtils.isBlank(fielName)) { + Validate.notBlank(object, "object can't be blank!"); + writerSliceConfig.set(Key.FILE_NAME, getPathAndFileNameFromObject(object.trim()).get(Key.FILE_NAME)); + } + if (StringUtils.equalsIgnoreCase(fileFormat, "parquet")) { + hdfsWriterJob.unitizeParquetConfig(writerSliceConfig); + } + + } + + + + /** + * 从object中 解析出 path和fileName + * + * 举例1: + * /hello/aaa/bbb/ccc.txt + * path: /hello/aaa/bbb + * fileName: ccc.txt + * + * 举例2: + * hello/aaa/bbb/ccc.txt + * path: /hello/aaa/bbb + * fileName: ccc.txt + * + * 举例3: + * ccc.txt + * path: / + * fileName: ccc.txt + * + * 举例4: + * /ccc.txt + * path: / + * fileName: ccc.txt + * + * @param object + * @return + */ + public static Map getPathAndFileNameFromObject(String object) { + Map pathAndFileName = new HashMap<>(); + + boolean isContainsBackslash = object.contains("/"); + + //object里没有包含"/", 则将path设置为 "/", fileName设置为 object + if (!isContainsBackslash) { + pathAndFileName.put(Key.PATH, "/"); + pathAndFileName.put(Key.FILE_NAME, object); + return pathAndFileName; + } + + if (!object.startsWith("/")) { + object = "/" + object; + } + + int lastIndex = object.lastIndexOf("/"); + String path = object.substring(0, lastIndex); + String fileName = object.substring(lastIndex + 1); + + path = StringUtils.isNotBlank(path) ? path : "/"; + + logger.info("path: {}", path); + logger.info("fileName: {}", fileName); + + pathAndFileName.put(Key.PATH, path); + pathAndFileName.put(Key.FILE_NAME, fileName); + return pathAndFileName; + } +} diff --git a/otsreader/doc/otsreader.md b/otsreader/doc/otsreader.md index 1297dbd69e..77b4edfe0f 100644 --- a/otsreader/doc/otsreader.md +++ b/otsreader/doc/otsreader.md @@ -13,7 +13,7 @@ OTSReader插件实现了从OTS读取数据,并可以通过用户指定抽取 * 范围抽取 * 指定分片抽取 -OTS是构建在阿里云飞天分布式系统之上的 NoSQL数据库服务,提供海量结构化数据的存储和实时访问。OTS 以实例和表的形式组织数据,通过数据分片和负载均衡技术,实现规模上的无缝扩展。 +本版本的OTSReader新增了支持多版本数据的读取功能,同时兼容旧版本的配置文件 ## 2 实现原理 @@ -25,201 +25,425 @@ OTSReader会根据OTS的表范围,按照Datax并发的数目N,将范围等 ### 3.1 配置样例 -* 配置一个从OTS全表同步抽取数据到本地的作业: +#### 3.1.1 +* 配置一个从OTS表读取单版本数据的reader: ``` { - "job": { - "setting": { + "job": { + "setting": { + "speed": { + //设置传输速度,单位为byte/s,DataX运行会尽可能达到该速度但是不超过它. + "byte": 1048576 + } + //出错限制 + "errorLimit": { + //出错的record条数上限,当大于该值即报错。 + "record": 0, + //出错的record百分比上限 1.0表示100%,0.02表示2% + "percentage": 0.02 + } + }, + "content": [ + { + "reader": { + "name": "otsreader-internal", + "parameter": { + "endpoint":"", + "accessId":"", + "accessKey":"", + "instanceName":"", + "table": "", + //version定义了是否使用新版本插件 可选值:false || true + "newVersion":"false", + //mode定义了读取数据的格式(普通数据/多版本数据),可选值:normal || multiversion + "mode": "normal", + + // 导出的范围,读取的范围是[begin,end),左闭右开的区间 + // begin小于end,表示正序读取数据 + // begin大于end,表示反序读取数据 + // begin和end不能相等 + // type支持的类型有如下几类: + // string、int、binary + // binary输入的方式采用二进制的Base64字符串形式传入 + // INF_MIN 表示无限小 + // INF_MAX 表示无限大 + "range":{ + // 可选,默认表示从无限小开始读取 + // 这个值的输入可以填写空数组,或者PK前缀,亦或者完整的PK,在正序读取数据时,默认填充PK后缀为INF_MIN,反序为INF_MAX + // 例子: + // 如果用户的表有2个PK,类型分别为string、int,那么如下3种输入都是合法,如: + // 1. [] --> 表示从表的开始位置读取 + // 2. [{"type":"string", "value":"a"}] --> 表示从[{"type":"string", "value":"a"},{"type":"INF_MIN"}] + // 3. [{"type":"string", "value":"a"},{"type":"INF_MIN"}] + // + // binary类型的PK列比较特殊,因为Json不支持直接输入二进制数,所以系统定义:用户如果要传入 + // 二进制,必须使用(Java)Base64.encodeBase64String方法,将二进制转换为一个可视化的字符串,然后将这个字符串填入value中 + // 例子(Java): + // byte[] bytes = "hello".getBytes(); # 构造一个二进制数据,这里使用字符串hello的byte值 + // String inputValue = Base64.encodeBase64String(bytes) # 调用Base64方法,将二进制转换为可视化的字符串 + // 上面的代码执行之后,可以获得inputValue为"aGVsbG8=" + // 最终写入配置:{"type":"binary","value" : "aGVsbG8="} + + "begin":[{"type":"string", "value":"a"},{"type":"INF_MIN"}], + + // 默认表示读取到无限大结束 + // 这个值得输入可以填写空数组,或者PK前缀,亦或者完整的PK,在正序读取数据时,默认填充PK后缀为INF_MAX,反序为INF_MIN + // 可选 + "end":[{"type":"string", "value":"a"},{"type":"INF_MAX"}], + + // 当前用户数据较多时,需要开启并发导出,Split可以将当前范围的的数据按照切分点切分为多个并发任务 + // 可选 + // 1. split中的输入值只能PK的第一列(分片建),且值的类型必须和PartitionKey一致 + // 2. 值的范围必须在begin和end之间 + // 3. split内部的值必须根据begin和end的正反序关系而递增或者递减 + "split":[{"type":"string", "value":"b"}, {"type":"string", "value":"c"}] + }, + + + // 指定要导出的列,支持普通列和常量列 + // 格式 + // 普通列格式:{"name":"{your column name}"} + // 常量列格式:{"type":"", "value":""} , type支持string、int、binary、bool、double + // binary类型需要使用base64转换成对应的字符串传入 + // 注意: + // 1. PK列也是需要用户在下面单独指定 + "column": [ + {"name":"pk1"}, // 普通列,下同 + {"name":"pk2"}, + {"name":"attr1"}, + {"type":"string","value" : ""} // 指定常量列,下同 + {"type":"int","value" : ""} + {"type":"double","value" : ""} + // binary类型的常量列比较特殊,因为Json不支持直接输入二进制数,所以系统定义:用户如果要传入 + // 二进制,必须使用(Java)Base64.encodeBase64String方法,将二进制转换为一个可视化的字符串,然后将这个字符串填入value中 + // 例子(Java): + // byte[] bytes = "hello".getBytes(); # 构造一个二进制数据,这里使用字符串hello的byte值 + // String inputValue = Base64.encodeBase64String(bytes) # 调用Base64方法,将二进制转换为可视化的字符串 + // 上面的代码执行之后,可以获得inputValue为"aGVsbG8=" + // 最终写入配置:{"type":"binary","value" : "aGVsbG8="} + + {"type":"binary","value" : "aGVsbG8="} + ], + } }, - "content": [ - { - "reader": { - "name": "otsreader", - "parameter": { - /* ----------- 必填 --------------*/ - "endpoint":"", - "accessId":"", - "accessKey":"", - "instanceName":"", - - // 导出数据表的表名 - "table":"", - - // 需要导出的列名,支持重复列和常量列,区分大小写 - // 常量列:类型支持STRING,INT,DOUBLE,BOOL和BINARY - // 备注:BINARY需要通过Base64转换为对应的字符串传入插件 - "column":[ - {"name":"col1"}, // 普通列 - {"name":"col2"}, // 普通列 - {"name":"col3"}, // 普通列 - {"type":"STRING", "value" : "bazhen"}, // 常量列(字符串) - {"type":"INT", "value" : ""}, // 常量列(整形) - {"type":"DOUBLE", "value" : ""}, // 常量列(浮点) - {"type":"BOOL", "value" : ""}, // 常量列(布尔) - {"type":"BINARY", "value" : "Base64(bin)"} // 常量列(二进制),使用Base64编码完成 - ], - "range":{ - // 导出数据的起始范围 - // 支持INF_MIN, INF_MAX, STRING, INT - "begin":[ - {"type":"INF_MIN"}, - ], - // 导出数据的结束范围 - // 支持INF_MIN, INF_MAX, STRING, INT - "end":[ - {"type":"INF_MAX"}, - ] - } - } - }, - "writer": {} - } - ] - } + "writer": { + //writer类型 + "name": "streamwriter", + //是否打印内容 + "parameter": { + "print": true + } + } + } + ] + } } ``` -* 配置一个定义抽取范围的OTSReader: +#### 3.1.2 +* 配置一个从OTS表读取多版本数据的reader(仅在newVersion == true时支持): ``` { - "job": { - "setting": { - "speed": { - "byte":10485760 + "job": { + "setting": { + "speed": { + //设置传输速度,单位为byte/s,DataX运行会尽可能达到该速度但是不超过它. + "byte": 1048576 + } + //出错限制 + "errorLimit": { + //出错的record条数上限,当大于该值即报错。 + "record": 0, + //出错的record百分比上限 1.0表示100%,0.02表示2% + "percentage": 0.02 + } + }, + "content": [ + { + "reader": { + "name": "otsreader-internal", + "parameter": { + "endpoint":"", + "accessId":"", + "accessKey":"", + "instanceName":"", + "table": "", + //version定义了是否使用新版本插件 可选值:false || true + "newVersion":"true", + //mode定义了读取数据的格式(普通数据/多版本数据),可选值:normal || multiversion + "mode": "multiversion", + + // 导出的范围,,读取的范围是[begin,end),左闭右开的区间 + // begin小于end,表示正序读取数据 + // begin大于end,表示反序读取数据 + // begin和end不能相等 + // type支持的类型有如下几类: + // string、int、binary + // binary输入的方式采用二进制的Base64字符串形式传入 + // INF_MIN 表示无限小 + // INF_MAX 表示无限大 + "range":{ + // 可选,默认表示从无限小开始读取 + // 这个值的输入可以填写空数组,或者PK前缀,亦或者完整的PK,在正序读取数据时,默认填充PK后缀为INF_MIN,反序为INF_MAX + // 例子: + // 如果用户的表有2个PK,类型分别为string、int,那么如下3种输入都是合法,如: + // 1. [] --> 表示从表的开始位置读取 + // 2. [{"type":"string", "value":"a"}] --> 表示从[{"type":"string", "value":"a"},{"type":"INF_MIN"}] + // 3. [{"type":"string", "value":"a"},{"type":"INF_MIN"}] + // + // binary类型的PK列比较特殊,因为Json不支持直接输入二进制数,所以系统定义:用户如果要传入 + // 二进制,必须使用(Java)Base64.encodeBase64String方法,将二进制转换为一个可视化的字符串,然后将这个字符串填入value中 + // 例子(Java): + // byte[] bytes = "hello".getBytes(); # 构造一个二进制数据,这里使用字符串hello的byte值 + // String inputValue = Base64.encodeBase64String(bytes) # 调用Base64方法,将二进制转换为可视化的字符串 + // 上面的代码执行之后,可以获得inputValue为"aGVsbG8=" + // 最终写入配置:{"type":"binary","value" : "aGVsbG8="} + + "begin":[{"type":"string", "value":"a"},{"type":"INF_MIN"}], + + // 默认表示读取到无限大结束 + // 这个值得输入可以填写空数组,或者PK前缀,亦或者完整的PK,在正序读取数据时,默认填充PK后缀为INF_MAX,反序为INF_MIN + // 可选 + "end":[{"type":"string", "value":"g"},{"type":"INF_MAX"}], + + // 当前用户数据较多时,需要开启并发导出,Split可以将当前范围的的数据按照切分点切分为多个并发任务 + // 可选 + // 1. split中的输入值只能PK的第一列(分片建),且值的类型必须和PartitionKey一致 + // 2. 值的范围必须在begin和end之间 + // 3. split内部的值必须根据begin和end的正反序关系而递增或者递减 + "split":[{"type":"string", "value":"b"}, {"type":"string", "value":"c"}] + }, + + // 指定要导出的列,在多版本模式下只支持普通列 + // 格式: + // 普通列格式:{"name":"{your column name}"} + // 可选,默认导出所有列的所有版本 + // 注意: + // 1.在多版本模式下,不支持常量列 + // 2.PK列不能指定,导出4元组中默认包括完整的PK + // 3.不能重复指定列 + "column": [ + {"name":"attr1"} + ], + + // 请求数据的Time Range,读取的范围是[begin,end),左闭右开的区间 + // 可选,默认读取全部版本 + // 注意:begin必须小于end + "timeRange":{ + // 可选,默认为0 + // 取值范围是0~LONG_MAX + "begin":1400000000, + // 可选,默认为Long Max(9223372036854775807L) + // 取值范围是0~LONG_MAX + "end" :1600000000 }, - "errorLimit":0.0 + + // 请求的指定Version + // 可选,默认读取所有版本 + // 取值范围是1~INT32_MAX + "maxVersion":10, + } }, - "content": [ - { - "reader": { - "name": "otsreader", - "parameter": { - "endpoint":"", - "accessId":"", - "accessKey":"", - "instanceName":"", - - // 导出数据表的表名 - "table":"", - - // 需要导出的列名,支持重复类和常量列,区分大小写 - // 常量列:类型支持STRING,INT,DOUBLE,BOOL和BINARY - // 备注:BINARY需要通过Base64转换为对应的字符串传入插件 - "column":[ - {"name":"col1"}, // 普通列 - {"name":"col2"}, // 普通列 - {"name":"col3"}, // 普通列 - {"type":"STRING","value" : ""}, // 常量列(字符串) - {"type":"INT","value" : ""}, // 常量列(整形) - {"type":"DOUBLE","value" : ""}, // 常量列(浮点) - {"type":"BOOL","value" : ""}, // 常量列(布尔) - {"type":"BINARY","value" : "Base64(bin)"} // 常量列(二进制) - ], - "range":{ - // 导出数据的起始范围 - // 支持INF_MIN, INF_MAX, STRING, INT - "begin":[ - {"type":"INF_MIN"}, - {"type":"INF_MAX"}, - {"type":"STRING", "value":"hello"}, - {"type":"INT", "value":"2999"}, - ], - // 导出数据的结束范围 - // 支持INF_MIN, INF_MAX, STRING, INT - "end":[ - {"type":"INF_MAX"}, - {"type":"INF_MIN"}, - {"type":"STRING", "value":"hello"}, - {"type":"INT", "value":"2999"}, - ] - } - } - }, - "writer": {} - } - ] - } + "writer": { + //writer类型 + "name": "streamwriter", + //是否打印内容 + "parameter": { + "print": true + } + } + } + ] + } } ``` +#### 3.1.3 +* 配置一个从OTS **时序表**读取数据的reader(仅在newVersion == true时支持): +```json +{ + "job": { + "setting": { + "speed": { + // 读取时序数据的通道数 + "channel": 5 + } + }, + "content": [ + { + "reader": { + "name": "otsreader", + "parameter": { + "endpoint": "", + "accessId": "", + "accessKey": "", + "instanceName": "", + "table": "", + // 读时序数据mode必须为normal + "mode": "normal", + // 读时序数据newVersion必须为true + "newVersion": "true", + // 配置该表为时序表 + "isTimeseriesTable":"true", + // 配置需要读取时间线的measurementName字段,非必需 + // 为空则读取全表数据 + "measurementName":"measurement_5", + // column是一个数组,每个元素表示一列 + // 对于常量列,需要配置以下字段: + // 1. type : 字段值类型,必需 + // 支持类型 : string, int, double, bool, binary + // 2. value : 字段值,必需 + // + // 对于普通列,需要配置以下字段: + // 1. name : 列名,必需 + // 时间线的'度量名称'使用_m_name标识,数据类型为String + // 时间线的'数据源'使用_data_source标识,数据类型为String + // 时间线的'标签'使用_tags标识,数据类型为String + // 时间线的'时间戳'使用_time标识,数据类型为Long + // 2. is_timeseries_tag : 是否为tags字段内部的键值,非必需,默认为false。 + // 3. type : 字段值类型,非必需,默认为string。 + // 支持类型 : string, int, double, bool, binary + "column": [ + { + "name": "_m_name" + }, + { + "name": "tagA", + "is_timeseries_tag":"true" + }, + { + "name": "double_0", + "type":"DOUBLE" + }, + { + "name": "string_0", + "type":"STRING" + }, + { + "name": "long_0", + "type":"int" + }, + { + "name": "binary_0", + "type":"BINARY" + }, + { + "name": "bool_0", + "type":"BOOL" + }, + { + "type":"STRING", + "value":"testString" + } + ] + } + }, + "writer": { + + } + } + ] + } +} +``` ### 3.2 参数说明 * **endpoint** - * 描述:OTS Server的EndPoint地址,例如http://bazhen.cn−hangzhou.ots.aliyuncs.com。 + * 描述:OTS Server的EndPoint地址,例如http://bazhen.cn−hangzhou.ots.aliyuncs.com。 - * 必选:是
+ * 必选:是
- * 默认值:无
+ * 默认值:无
* **accessId** - * 描述:OTS的accessId
+ * 描述:OTS的accessId
- * 必选:是
+ * 必选:是
- * 默认值:无
+ * 默认值:无
* **accessKey** - * 描述:OTS的accessKey
+ * 描述:OTS的accessKey
- * 必选:是
+ * 必选:是
- * 默认值:无
+ * 默认值:无
* **instanceName** - * 描述:OTS的实例名称,实例是用户使用和管理 OTS 服务的实体,用户在开通 OTS 服务之后,需要通过管理控制台来创建实例,然后在实例内进行表的创建和管理。实例是 OTS 资源管理的基础单元,OTS 对应用程序的访问控制和资源计量都在实例级别完成。
+ * 描述:OTS的实例名称,实例是用户使用和管理 OTS 服务的实体,用户在开通 OTS 服务之后,需要通过管理控制台来创建实例,然后在实例内进行表的创建和管理。实例是 OTS 资源管理的基础单元,OTS 对应用程序的访问控制和资源计量都在实例级别完成。
- * 必选:是
+ * 必选:是
- * 默认值:无
+ * 默认值:无
* **table** - * 描述:所选取的需要抽取的表名称,这里有且只能填写一张表。在OTS不存在多表同步的需求。
+ * 描述:所选取的需要抽取的表名称,这里有且只能填写一张表。在OTS不存在多表同步的需求。
+ + * 必选:是
+ + * 默认值:无
+ +* **newVersion** + + * 描述:version定义了使用的ots SDK版本。
+ * true,新版本插件,使用com.alicloud.openservices.tablestore的依赖(推荐) + * false,旧版本插件,使用com.aliyun.openservices.ots的依赖,**不支持多版本数据的读取** + + * 必选:否
- * 必选:是
+ * 默认值:false
- * 默认值:无
+* **mode** + + * 描述:读取为多版本格式的数据,目前有两种模式。
+ * normal,对应普通的数据 + * multiVersion,写入数据为多版本格式的数据,多版本模式下,配置参数有所不同,详见3.1.2 + + * 必选:否
+ + * 默认值:normal
* **column** - * 描述:所配置的表中需要同步的列名集合,使用JSON的数组描述字段信息。由于OTS本身是NoSQL系统,在OTSReader抽取数据过程中,必须指定相应地字段名称。 + * 描述:所配置的表中需要同步的列名集合,使用JSON的数组描述字段信息。由于OTS本身是NoSQL系统,在OTSReader抽取数据过程中,必须指定相应地字段名称。 - 支持普通的列读取,例如: {"name":"col1"} + 支持普通的列读取,例如: {"name":"col1"} - 支持部分列读取,如用户不配置该列,则OTSReader不予读取。 + 支持部分列读取,如用户不配置该列,则OTSReader不予读取。 - 支持常量列读取,例如: {"type":"STRING", "value" : "DataX"}。使用type描述常量类型,目前支持STRING、INT、DOUBLE、BOOL、BINARY(用户使用Base64编码填写)、INF_MIN(OTS的系统限定最小值,使用该值用户不能填写value属性,否则报错)、INF_MAX(OTS的系统限定最大值,使用该值用户不能填写value属性,否则报错)。 + 支持常量列读取,例如: {"type":"STRING", "value" : "DataX"}。使用type描述常量类型,目前支持STRING、INT、DOUBLE、BOOL、BINARY(用户使用Base64编码填写)、INF_MIN(OTS的系统限定最小值,使用该值用户不能填写value属性,否则报错)、INF_MAX(OTS的系统限定最大值,使用该值用户不能填写value属性,否则报错)。 - 不支持函数或者自定义表达式,由于OTS本身不提供类似SQL的函数或者表达式功能,OTSReader也不能提供函数或表达式列功能。 + 不支持函数或者自定义表达式,由于OTS本身不提供类似SQL的函数或者表达式功能,OTSReader也不能提供函数或表达式列功能。 - * 必选:是
+ * 必选:是
- * 默认值:无
+ * 默认值:无
* **begin/end** - * 描述:该配置项必须配对使用,用于支持OTS表范围抽取。begin/end中描述的是OTS **PrimaryKey**的区间分布状态,而且必须保证区间覆盖到所有的PrimaryKey,**需要指定该表下所有的PrimaryKey范围,不能遗漏任意一个PrimaryKey**,对于无限大小的区间,可以使用{"type":"INF_MIN"},{"type":"INF_MAX"}指代。例如对一张主键为 [DeviceID, SellerID]的OTS进行抽取任务,begin/end可以配置为: + * 描述:该配置项必须配对使用,用于支持OTS表范围抽取。begin/end中描述的是OTS **PrimaryKey**的区间分布状态,而且必须保证区间覆盖到所有的PrimaryKey,**需要指定该表下所有的PrimaryKey范围,不能遗漏任意一个PrimaryKey**,对于无限大小的区间,可以使用{"type":"INF_MIN"},{"type":"INF_MAX"}指代。例如对一张主键为 [DeviceID, SellerID]的OTS进行抽取任务,begin/end可以配置为: - ```json - "range": { - "begin": { - {"type":"INF_MIN"}, //指定deviceID最小值 - {"type":"INT", "value":"0"} //指定deviceID最小值 - }, - "end": { - {"type":"INF_MAX"}, //指定deviceID抽取最大值 - {"type":"INT", "value":"9999"} //指定deviceID抽取最大值 - } - } - ``` + ```json + "range": { + "begin": { + {"type":"INF_MIN"}, //指定deviceID最小值 + {"type":"INT", "value":"0"} //指定deviceID最小值 + }, + "end": { + {"type":"INF_MAX"}, //指定deviceID抽取最大值 + {"type":"INT", "value":"9999"} //指定deviceID抽取最大值 + } + } + ``` 如果要对上述表抽取全表,可以使用如下配置: @@ -237,42 +461,42 @@ OTSReader会根据OTS的表范围,按照Datax并发的数目N,将范围等 } ``` - * 必选:是
+ * 必选:否
- * 默认值:空
+ * 默认值:读取全部值
* **split** - * 描述:该配置项属于高级配置项,是用户自己定义切分配置信息,普通情况下不建议用户使用。适用场景通常在OTS数据存储发生热点,使用OTSReader自动切分的策略不能生效情况下,使用用户自定义的切分规则。split指定是的在Begin、End区间内的切分点,且只能是partitionKey的切分点信息,即在split仅配置partitionKey,而不需要指定全部的PrimaryKey。 - - 例如对一张主键为 [DeviceID, SellerID]的OTS进行抽取任务,可以配置为: - - ```json - "range": { - "begin": { - {"type":"INF_MIN"}, //指定deviceID最小值 - {"type":"INF_MIN"} //指定deviceID最小值 - }, - "end": { - {"type":"INF_MAX"}, //指定deviceID抽取最大值 - {"type":"INF_MAX"} //指定deviceID抽取最大值 - }, - // 用户指定的切分点,如果指定了切分点,Job将按照begin、end和split进行Task的切分, - // 切分的列只能是Partition Key(ParimaryKey的第一列) - // 支持INF_MIN, INF_MAX, STRING, INT - "split":[ - {"type":"STRING", "value":"1"}, - {"type":"STRING", "value":"2"}, - {"type":"STRING", "value":"3"}, - {"type":"STRING", "value":"4"}, - {"type":"STRING", "value":"5"} - ] - } - ``` - - * 必选:否
- - * 默认值:无
+ * 描述:该配置项属于高级配置项,是用户自己定义切分配置信息,普通情况下不建议用户使用。适用场景通常在OTS数据存储发生热点,使用OTSReader自动切分的策略不能生效情况下,使用用户自定义的切分规则。split指定是的在Begin、End区间内的切分点,且只能是partitionKey的切分点信息,即在split仅配置partitionKey,而不需要指定全部的PrimaryKey。 + + 例如对一张主键为 [DeviceID, SellerID]的OTS进行抽取任务,可以配置为: + + ```json + "range": { + "begin": { + {"type":"INF_MIN"}, //指定deviceID最小值 + {"type":"INF_MIN"} //指定deviceID最小值 + }, + "end": { + {"type":"INF_MAX"}, //指定deviceID抽取最大值 + {"type":"INF_MAX"} //指定deviceID抽取最大值 + }, + // 用户指定的切分点,如果指定了切分点,Job将按照begin、end和split进行Task的切分, + // 切分的列只能是Partition Key(ParimaryKey的第一列) + // 支持INF_MIN, INF_MAX, STRING, INT + "split":[ + {"type":"STRING", "value":"1"}, + {"type":"STRING", "value":"2"}, + {"type":"STRING", "value":"3"}, + {"type":"STRING", "value":"4"}, + {"type":"STRING", "value":"5"} + ] + } + ``` + + * 必选:否
+ + * 默认值:无
### 3.3 类型转换 @@ -291,44 +515,14 @@ OTSReader会根据OTS的表范围,按照Datax并发的数目N,将范围等 * 注意,OTS本身不支持日期型类型。应用层一般使用Long报错时间的Unix TimeStamp。 -## 4 性能报告 - -### 4.1 环境准备 - -#### 4.1.1 数据特征 - -15列String(10 Byte), 2两列Integer(8 Byte),总计168Byte/r。 -#### 4.1.2 机器参数 +## 4 约束限制 -OTS端:3台前端机,5台后端机 - -DataX运行端: 24核CPU, 98GB内存 - -#### 4.1.3 DataX jvm 参数 - - -Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError - -### 4.2 测试报告 - -#### 4.2.1 测试报告 - -|并发数|DataX CPU|OTS 流量|DATAX流量 | 前端QPS| 前端延时| -|--------|--------| --------|--------|--------|------| -|2| 36% |6.3M/s |12739 rec/s | 4.7 | 308ms | -|11| 155% | 32M/s |60732 rec/s | 23.9 | 412ms | -|50| 377% | 73M/s |145139 rec/s | 54 | 874ms | -|100| 448% | 82M/s | 156262 rec/s |60 | 1570ms | - - - -## 5 约束限制 - -### 5.1 一致性约束 +### 4.1 一致性约束 OTS是类BigTable的存储系统,OTS本身能够保证单行写事务性,无法提供跨行级别的事务。对于OTSReader而言也无法提供全表的一致性视图。例如对于OTSReader在0点启动的数据同步任务,在整个表数据同步过程中,OTSReader同样会抽取到后续更新的数据,无法提供准确的0点时刻该表一致性视图。 -### 5.2 增量数据同步 +### 4.2 增量数据同步 OTS本质上KV存储,目前只能针对PK进行范围查询,暂不支持按照字段范围抽取数据。因此只能对于增量查询,如果PK能够表示范围信息,例如自增ID,或者时间戳。 @@ -336,5 +530,4 @@ OTS本质上KV存储,目前只能针对PK进行范围查询,暂不支持按 时间戳, OTSReader可以通过PK过滤时间戳,通过制定Range范围进行增量抽取。这样使用的前提是OTS中的PrimaryKey必须包含主键时间列(时间主键需要使用OTS应用方生成。) -## 6 FAQ - +## 5 FAQ diff --git a/otsreader/pom.xml b/otsreader/pom.xml index bd01742392..dad538bf1d 100644 --- a/otsreader/pom.xml +++ b/otsreader/pom.xml @@ -1,5 +1,5 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.alibaba.datax @@ -34,14 +34,45 @@ com.aliyun.openservices ots-public 2.2.4 - + + + log4j-core + org.apache.logging.log4j + + + + + com.aliyun.openservices + tablestore + 5.13.13 + + + log4j-core + org.apache.logging.log4j + + + com.google.code.gson gson 2.2.4 + + com.alibaba + fastjson + 1.2.83_noneautotype + compile + + + + src/main/java + + **/*.properties + + + @@ -77,10 +108,6 @@ maven-surefire-plugin 2.5 - all - 10 - true - -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=. **/unittest/*.java **/functiontest/*.java @@ -90,4 +117,3 @@ - diff --git a/otsreader/src/main/assembly/package.xml b/otsreader/src/main/assembly/package.xml index 7ee305d14a..cb90f3e8fd 100644 --- a/otsreader/src/main/assembly/package.xml +++ b/otsreader/src/main/assembly/package.xml @@ -12,8 +12,8 @@ src/main/resources plugin.json - plugin_job_template.json - + plugin_job_template.json +
plugin/reader/otsreader
diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/IOtsReaderMasterProxy.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/IOtsReaderMasterProxy.java new file mode 100644 index 0000000000..ee622e1690 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/IOtsReaderMasterProxy.java @@ -0,0 +1,15 @@ +package com.alibaba.datax.plugin.reader.otsreader; + +import java.util.List; + +import com.alibaba.datax.common.util.Configuration; + +public interface IOtsReaderMasterProxy { + + public void init(Configuration param) throws Exception; + + public List split(int num) throws Exception; + + public void close(); + +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/IOtsReaderSlaveProxy.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/IOtsReaderSlaveProxy.java new file mode 100644 index 0000000000..d1100a2a13 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/IOtsReaderSlaveProxy.java @@ -0,0 +1,26 @@ +package com.alibaba.datax.plugin.reader.otsreader; + +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.util.Configuration; + +/** + * OTS Reader工作进程接口 + */ +public interface IOtsReaderSlaveProxy { + /** + * 初始化函数,解析配置、初始化相关资源 + */ + public void init(Configuration configuration); + + /** + * 关闭函数,释放资源 + */ + public void close(); + + /** + * 数据导出函数 + * @param recordSender + * @throws Exception + */ + public void startRead(RecordSender recordSender) throws Exception; +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReader.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReader.java index 8880c07eda..c6bc44b881 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReader.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReader.java @@ -1,45 +1,48 @@ package com.alibaba.datax.plugin.reader.otsreader; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.plugin.RecordSender; import com.alibaba.datax.common.spi.Reader; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.datax.plugin.reader.otsreader.utils.Common; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSConf; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSMode; +import com.alibaba.datax.plugin.reader.otsreader.utils.Constant; +import com.alibaba.datax.plugin.reader.otsreader.utils.GsonParser; +import com.alibaba.datax.plugin.reader.otsreader.utils.OtsReaderError; +import com.alicloud.openservices.tablestore.TableStoreException; import com.aliyun.openservices.ots.ClientException; -import com.aliyun.openservices.ots.OTSException; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + public class OtsReader extends Reader { public static class Job extends Reader.Job { private static final Logger LOG = LoggerFactory.getLogger(Job.class); - private OtsReaderMasterProxy proxy = new OtsReaderMasterProxy(); + //private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(OtsReader.class); + private IOtsReaderMasterProxy proxy = null; + @Override - public void init() { + public void init() { LOG.info("init() begin ..."); + + proxy = new OtsReaderMasterProxy(); try { this.proxy.init(getPluginJobConf()); - } catch (OTSException e) { - LOG.error("OTSException. ErrorCode:{}, ErrorMsg:{}, RequestId:{}", - new Object[]{e.getErrorCode(), e.getMessage(), e.getRequestId()}); - LOG.error("Stack", e); - throw DataXException.asDataXException(new OtsReaderError(e.getErrorCode(), "OTS端的错误"), Common.getDetailMessage(e), e); + } catch (TableStoreException e) { + LOG.error("OTSException: {}", e.toString(), e); + throw DataXException.asDataXException(new OtsReaderError(e.getErrorCode(), "OTS ERROR"), e.toString(), e); } catch (ClientException e) { - LOG.error("ClientException. ErrorCode:{}, ErrorMsg:{}", - new Object[]{e.getErrorCode(), e.getMessage()}); - LOG.error("Stack", e); - throw DataXException.asDataXException(new OtsReaderError(e.getErrorCode(), "OTS端的错误"), Common.getDetailMessage(e), e); - } catch (IllegalArgumentException e) { - LOG.error("IllegalArgumentException. ErrorMsg:{}", e.getMessage(), e); - throw DataXException.asDataXException(OtsReaderError.INVALID_PARAM, Common.getDetailMessage(e), e); + LOG.error("ClientException: {}", e.toString(), e); + throw DataXException.asDataXException(OtsReaderError.ERROR, e.toString(), e); } catch (Exception e) { - LOG.error("Exception. ErrorMsg:{}", e.getMessage(), e); - throw DataXException.asDataXException(OtsReaderError.ERROR, Common.getDetailMessage(e), e); + LOG.error("Exception. ErrorMsg:{}", e.toString(), e); + throw DataXException.asDataXException(OtsReaderError.ERROR, e.toString(), e); } + LOG.info("init() end ..."); } @@ -60,22 +63,9 @@ public List split(int adviceNumber) { try { confs = this.proxy.split(adviceNumber); - } catch (OTSException e) { - LOG.error("OTSException. ErrorCode:{}, ErrorMsg:{}, RequestId:{}", - new Object[]{e.getErrorCode(), e.getMessage(), e.getRequestId()}); - LOG.error("Stack", e); - throw DataXException.asDataXException(new OtsReaderError(e.getErrorCode(), "OTS端的错误"), Common.getDetailMessage(e), e); - } catch (ClientException e) { - LOG.error("ClientException. ErrorCode:{}, ErrorMsg:{}", - new Object[]{e.getErrorCode(), e.getMessage()}); - LOG.error("Stack", e); - throw DataXException.asDataXException(new OtsReaderError(e.getErrorCode(), "OTS端的错误"), Common.getDetailMessage(e), e); - } catch (IllegalArgumentException e) { - LOG.error("IllegalArgumentException. ErrorMsg:{}", e.getMessage(), e); - throw DataXException.asDataXException(OtsReaderError.INVALID_PARAM, Common.getDetailMessage(e), e); } catch (Exception e) { LOG.error("Exception. ErrorMsg:{}", e.getMessage(), e); - throw DataXException.asDataXException(OtsReaderError.ERROR, Common.getDetailMessage(e), e); + throw DataXException.asDataXException(OtsReaderError.ERROR, e.toString(), e); } LOG.info("split() end ..."); @@ -85,39 +75,60 @@ public List split(int adviceNumber) { public static class Task extends Reader.Task { private static final Logger LOG = LoggerFactory.getLogger(Task.class); - private OtsReaderSlaveProxy proxy = new OtsReaderSlaveProxy(); + //private static final MessageSource MESSAGE_SOURCE = MessageSource.loadResourceBundle(OtsReader.class); + private IOtsReaderSlaveProxy proxy = null; @Override public void init() { + + OTSConf conf = GsonParser.jsonToConf((String) this.getPluginJobConf().get(Constant.ConfigKey.CONF)); + // 是否使用新接口 + if(conf.isNewVersion()) { + if (conf.getMode() == OTSMode.MULTI_VERSION) { + LOG.info("init OtsReaderSlaveProxyMultiVersion"); + proxy = new OtsReaderSlaveMultiVersionProxy(); + } else { + LOG.info("init OtsReaderSlaveProxyNormal"); + proxy = new OtsReaderSlaveNormalProxy(); + } + + } + else{ + String metaMode = conf.getMetaMode(); + if (StringUtils.isNotBlank(metaMode) && !metaMode.equalsIgnoreCase("false")) { + LOG.info("init OtsMetaReaderSlaveProxy"); + proxy = new OtsReaderSlaveMetaProxy(); + } else { + LOG.info("init OtsReaderSlaveProxyOld"); + proxy = new OtsReaderSlaveProxyOld(); + } + } + + proxy.init(this.getPluginJobConf()); } @Override public void destroy() { + try { + proxy.close(); + } catch (Exception e) { + LOG.error("Exception. ErrorMsg:{}", e.toString(), e); + throw DataXException.asDataXException(OtsReaderError.ERROR, e.toString(), e); + } } @Override public void startRead(RecordSender recordSender) { - LOG.info("startRead() begin ..."); + try { - this.proxy.read(recordSender,getPluginJobConf()); - } catch (OTSException e) { - LOG.error("OTSException. ErrorCode:{}, ErrorMsg:{}, RequestId:{}", - new Object[]{e.getErrorCode(), e.getMessage(), e.getRequestId()}); - LOG.error("Stack", e); - throw DataXException.asDataXException(new OtsReaderError(e.getErrorCode(), "OTS端的错误"), Common.getDetailMessage(e), e); - } catch (ClientException e) { - LOG.error("ClientException. ErrorCode:{}, ErrorMsg:{}", - new Object[]{e.getErrorCode(), e.getMessage()}); - LOG.error("Stack", e); - throw DataXException.asDataXException(new OtsReaderError(e.getErrorCode(), "OTS端的错误"), Common.getDetailMessage(e), e); - } catch (IllegalArgumentException e) { - LOG.error("IllegalArgumentException. ErrorMsg:{}", e.getMessage(), e); - throw DataXException.asDataXException(OtsReaderError.INVALID_PARAM, Common.getDetailMessage(e), e); + proxy.startRead(recordSender); } catch (Exception e) { - LOG.error("Exception. ErrorMsg:{}", e.getMessage(), e); - throw DataXException.asDataXException(OtsReaderError.ERROR, Common.getDetailMessage(e), e); + LOG.error("Exception. ErrorMsg:{}", e.toString(), e); + throw DataXException.asDataXException(OtsReaderError.ERROR, e.toString(), e); } - LOG.info("startRead() end ..."); + + + } } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderMasterProxy.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderMasterProxy.java index 2b758f0683..4ecdd8c158 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderMasterProxy.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderMasterProxy.java @@ -1,221 +1,243 @@ package com.alibaba.datax.plugin.reader.otsreader; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.alibaba.datax.common.util.Configuration; import com.alibaba.datax.plugin.reader.otsreader.callable.GetFirstRowPrimaryKeyCallable; -import com.alibaba.datax.plugin.reader.otsreader.callable.GetTableMetaCallable; import com.alibaba.datax.plugin.reader.otsreader.model.OTSConf; -import com.alibaba.datax.plugin.reader.otsreader.model.OTSConst; import com.alibaba.datax.plugin.reader.otsreader.model.OTSRange; -import com.alibaba.datax.plugin.reader.otsreader.utils.ParamChecker; -import com.alibaba.datax.plugin.reader.otsreader.utils.Common; -import com.alibaba.datax.plugin.reader.otsreader.utils.GsonParser; -import com.alibaba.datax.plugin.reader.otsreader.utils.ReaderModelParser; -import com.alibaba.datax.plugin.reader.otsreader.utils.RangeSplit; -import com.alibaba.datax.plugin.reader.otsreader.utils.RetryHelper; -import com.aliyun.openservices.ots.OTSClient; -import com.aliyun.openservices.ots.model.Direction; -import com.aliyun.openservices.ots.model.PrimaryKeyValue; -import com.aliyun.openservices.ots.model.RangeRowQueryCriteria; -import com.aliyun.openservices.ots.model.RowPrimaryKey; -import com.aliyun.openservices.ots.model.TableMeta; +import com.alibaba.datax.plugin.reader.otsreader.utils.*; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.model.*; +import com.alicloud.openservices.tablestore.model.timeseries.ScanTimeseriesDataResponse; +import com.alicloud.openservices.tablestore.model.timeseries.TimeseriesScanSplitInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -public class OtsReaderMasterProxy { +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; - private OTSConf conf = new OTSConf(); +public class OtsReaderMasterProxy implements IOtsReaderMasterProxy { - private OTSRange range = null; + private static final Logger LOG = LoggerFactory.getLogger(OtsReaderMasterProxy.class); + private OTSConf conf = null; + private TableMeta meta = null; + private SyncClientInterface ots = null; + private Direction direction = null; - private OTSClient ots = null; - private TableMeta meta = null; + public OTSConf getConf() { + return conf; + } - private Direction direction = null; + public TableMeta getMeta() { + return meta; + } - private static final Logger LOG = LoggerFactory.getLogger(OtsReaderMasterProxy.class); + public SyncClientInterface getOts() { + return ots; + } + + public void setOts(SyncClientInterface ots) { + this.ots = ots; + } /** - * 1.检查参数是否为 - * null,endpoint,accessid,accesskey,instance-name,table,column,range-begin,range-end,range-split - * 2.检查参数是否为空字符串 - * endpoint,accessid,accesskey,instance-name,table - * 3.检查是否为空数组 - * column - * 4.检查Range的类型个个数是否和PrimaryKey匹配 - * column,range-begin,range-end - * 5.检查Range Split 顺序和类型是否Range一致,类型是否于PartitionKey一致 - * column-split + * 基于配置传入的配置文件,解析为对应的参数 + * * @param param * @throws Exception */ - public void init(Configuration param) throws Exception { - // 默认参数 - // 每次重试的时间都是上一次的一倍,当sleep时间大于30秒时,Sleep重试时间不在增长。18次能覆盖OTS的Failover时间5分钟 - conf.setRetry(param.getInt(OTSConst.RETRY, 18)); - conf.setSleepInMilliSecond(param.getInt(OTSConst.SLEEP_IN_MILLI_SECOND, 100)); - - // 必选参数 - conf.setEndpoint(ParamChecker.checkStringAndGet(param, Key.OTS_ENDPOINT)); - conf.setAccessId(ParamChecker.checkStringAndGet(param, Key.OTS_ACCESSID)); - conf.setAccesskey(ParamChecker.checkStringAndGet(param, Key.OTS_ACCESSKEY)); - conf.setInstanceName(ParamChecker.checkStringAndGet(param, Key.OTS_INSTANCE_NAME)); - conf.setTableName(ParamChecker.checkStringAndGet(param, Key.TABLE_NAME)); - - ots = new OTSClient( - this.conf.getEndpoint(), - this.conf.getAccessId(), - this.conf.getAccesskey(), - this.conf.getInstanceName()); - - meta = getTableMeta(ots, conf.getTableName()); - LOG.info("Table Meta : {}", GsonParser.metaToJson(meta)); - - conf.setColumns(ReaderModelParser.parseOTSColumnList(ParamChecker.checkListAndGet(param, Key.COLUMN, true))); - - Map rangeMap = ParamChecker.checkMapAndGet(param, Key.RANGE, true); - conf.setRangeBegin(ReaderModelParser.parsePrimaryKey(ParamChecker.checkListAndGet(rangeMap, Key.RANGE_BEGIN, false))); - conf.setRangeEnd(ReaderModelParser.parsePrimaryKey(ParamChecker.checkListAndGet(rangeMap, Key.RANGE_END, false))); - - range = ParamChecker.checkRangeAndGet(meta, this.conf.getRangeBegin(), this.conf.getRangeEnd()); - - direction = ParamChecker.checkDirectionAndEnd(meta, range.getBegin(), range.getEnd()); - LOG.info("Direction : {}", direction); - - List points = ReaderModelParser.parsePrimaryKey(ParamChecker.checkListAndGet(rangeMap, Key.RANGE_SPLIT)); - ParamChecker.checkInputSplitPoints(meta, range, direction, points); - conf.setRangeSplit(points); + public void init(Configuration param) throws Exception { + // 基于预定义的Json格式,检查传入参数是否符合Conf定义规范 + conf = OTSConf.load(param); + + // Init ots + ots = OtsHelper.getOTSInstance(conf); + + // 宽行表init + if (!conf.isTimeseriesTable()) { + // 获取TableMeta + meta = OtsHelper.getTableMeta( + ots, + conf.getTableName(), + conf.getRetry(), + conf.getRetryPauseInMillisecond()); + + // 基于Meta检查Conf是否正确 + ParamChecker.checkAndSetOTSConf(conf, meta); + direction = ParamChecker.checkDirectionAndEnd(meta, conf.getRange().getBegin(), conf.getRange().getEnd()); + } + // 时序表 检查tablestore SDK version + if (conf.isTimeseriesTable()){ + Common.checkTableStoreSDKVersion(); + } + + } + + public List split(int mandatoryNumber) throws Exception { + LOG.info("Expect split num : " + mandatoryNumber); - public List split(int num) throws Exception { - LOG.info("Expect split num : " + num); - List configurations = new ArrayList(); - List ranges = null; - - if (this.conf.getRangeSplit() != null) { // 用户显示指定了拆分范围 + if (conf.isTimeseriesTable()) {{ // 时序表全部采用默认切分策略 + LOG.info("Begin timeseries table defaultRangeSplit"); + configurations = getTimeseriesConfigurationBySplit(mandatoryNumber); + LOG.info("End timeseries table defaultRangeSplit"); + }} + else if (this.conf.getRange().getSplit().size() != 0) { // 用户显示指定了拆分范围 LOG.info("Begin userDefinedRangeSplit"); - ranges = userDefinedRangeSplit(meta, range, this.conf.getRangeSplit()); + configurations = getNormalConfigurationBySplit(); LOG.info("End userDefinedRangeSplit"); - } else { // 采用默认的切分算法 + } else { // 采用默认的切分算法 LOG.info("Begin defaultRangeSplit"); - ranges = defaultRangeSplit(ots, meta, range, num); + configurations = getDefaultConfiguration(mandatoryNumber); LOG.info("End defaultRangeSplit"); } - // 解决大量的Split Point序列化消耗内存的问题 - // 因为slave中不会使用这个配置,所以置为空 - this.conf.setRangeSplit(null); - - for (OTSRange item : ranges) { - Configuration configuration = Configuration.newDefault(); - configuration.set(OTSConst.OTS_CONF, GsonParser.confToJson(this.conf)); - configuration.set(OTSConst.OTS_RANGE, GsonParser.rangeToJson(item)); - configuration.set(OTSConst.OTS_DIRECTION, GsonParser.directionToJson(direction)); - configurations.add(configuration); - } - - LOG.info("Configuration list count : " + configurations.size()); - + LOG.info("Expect split num: "+ mandatoryNumber +", and final configuration list count : " + configurations.size()); return configurations; } - public OTSConf getConf() { - return conf; - } - public void close() { ots.shutdown(); } - // private function - - private TableMeta getTableMeta(OTSClient ots, String tableName) throws Exception { - return RetryHelper.executeWithRetry( - new GetTableMetaCallable(ots, tableName), + /** + * timeseries split信息,根据切分数配置多个Task + */ + private List getTimeseriesConfigurationBySplit(int mandatoryNumber) throws Exception { + List timeseriesScanSplitInfoList = OtsHelper.splitTimeseriesScan( + ots, + conf.getTableName(), + conf.getMeasurementName(), + mandatoryNumber, conf.getRetry(), - conf.getSleepInMilliSecond() - ); + conf.getRetryPauseInMillisecond()); + List configurations = new ArrayList<>(); + + for (int i = 0; i < timeseriesScanSplitInfoList.size(); i++) { + Configuration configuration = Configuration.newDefault(); + configuration.set(Constant.ConfigKey.CONF, GsonParser.confToJson(conf)); + configuration.set(Constant.ConfigKey.SPLIT_INFO, GsonParser.timeseriesScanSplitInfoToString(timeseriesScanSplitInfoList.get(i))); + configurations.add(configuration); + } + return configurations; } - private RowPrimaryKey getPKOfFirstRow( - OTSRange range , Direction direction) throws Exception { + /** + * 根据用户配置的split信息,将配置文件基于Range范围转换为多个Task的配置 + */ + private List getNormalConfigurationBySplit() { + List> primaryKeys = new ArrayList>(); + primaryKeys.add(conf.getRange().getBegin()); + for (PrimaryKeyColumn column : conf.getRange().getSplit()) { + List point = new ArrayList(); + point.add(column); + ParamChecker.fillPrimaryKey(this.meta.getPrimaryKeyList(), point, PrimaryKeyValue.INF_MIN); + primaryKeys.add(point); + } + primaryKeys.add(conf.getRange().getEnd()); - RangeRowQueryCriteria cur = new RangeRowQueryCriteria(this.conf.getTableName()); - cur.setInclusiveStartPrimaryKey(range.getBegin()); - cur.setExclusiveEndPrimaryKey(range.getEnd()); - cur.setLimit(1); - cur.setColumnsToGet(Common.getPrimaryKeyNameList(meta)); - cur.setDirection(direction); + List configurations = new ArrayList(primaryKeys.size() - 1); - return RetryHelper.executeWithRetry( - new GetFirstRowPrimaryKeyCallable(ots, meta, cur), - conf.getRetry(), - conf.getSleepInMilliSecond() - ); + for (int i = 0; i < primaryKeys.size() - 1; i++) { + OTSRange range = new OTSRange(); + range.setBegin(primaryKeys.get(i)); + range.setEnd(primaryKeys.get(i + 1)); + + Configuration configuration = Configuration.newDefault(); + configuration.set(Constant.ConfigKey.CONF, GsonParser.confToJson(conf)); + configuration.set(Constant.ConfigKey.RANGE, GsonParser.rangeToJson(range)); + configuration.set(Constant.ConfigKey.META, GsonParser.metaToJson(meta)); + configurations.add(configuration); + } + return configurations; } - private List defaultRangeSplit(OTSClient ots, TableMeta meta, OTSRange range, int num) throws Exception { + private List getDefaultConfiguration(int num) throws Exception { if (num == 1) { List ranges = new ArrayList(); + OTSRange range = new OTSRange(); + range.setBegin(conf.getRange().getBegin()); + range.setEnd(conf.getRange().getEnd()); ranges.add(range); - return ranges; + + return getConfigurationsFromRanges(ranges); } - + OTSRange reverseRange = new OTSRange(); - reverseRange.setBegin(range.getEnd()); - reverseRange.setEnd(range.getBegin()); + reverseRange.setBegin(conf.getRange().getEnd()); + reverseRange.setEnd(conf.getRange().getBegin()); Direction reverseDirection = (direction == Direction.FORWARD ? Direction.BACKWARD : Direction.FORWARD); - RowPrimaryKey realBegin = getPKOfFirstRow(range, direction); - RowPrimaryKey realEnd = getPKOfFirstRow(reverseRange, reverseDirection); - + List realBegin = getPKOfFirstRow(conf.getRange(), direction); + List realEnd = getPKOfFirstRow(reverseRange, reverseDirection); + // 因为如果其中一行为空,表示这个范围内至多有一行数据 // 所以不再细分,直接使用用户定义的范围 if (realBegin == null || realEnd == null) { List ranges = new ArrayList(); - ranges.add(range); - return ranges; + ranges.add(conf.getRange()); + return getConfigurationsFromRanges(ranges); } - + // 如果出现realBegin,realEnd的方向和direction不一致的情况,直接返回range int cmp = Common.compareRangeBeginAndEnd(meta, realBegin, realEnd); Direction realDirection = cmp > 0 ? Direction.BACKWARD : Direction.FORWARD; if (realDirection != direction) { LOG.warn("Expect '" + direction + "', but direction of realBegin and readlEnd is '" + realDirection + "'"); List ranges = new ArrayList(); - ranges.add(range); - return ranges; + ranges.add(conf.getRange()); + return getConfigurationsFromRanges(ranges); } List ranges = RangeSplit.rangeSplitByCount(meta, realBegin, realEnd, num); if (ranges.isEmpty()) { // 当PartitionKey相等时,工具内部不会切分Range - ranges.add(range); + ranges.add(conf.getRange()); } else { // replace first and last OTSRange first = ranges.get(0); OTSRange last = ranges.get(ranges.size() - 1); - first.setBegin(range.getBegin()); - last.setEnd(range.getEnd()); + first.setBegin(conf.getRange().getBegin()); + last.setEnd(conf.getRange().getEnd()); } - - return ranges; + + return getConfigurationsFromRanges(ranges); } - private List userDefinedRangeSplit(TableMeta meta, OTSRange range, List points) { - List ranges = RangeSplit.rangeSplitByPoint(meta, range.getBegin(), range.getEnd(), points); - if (ranges.isEmpty()) { // 当PartitionKey相等时,工具内部不会切分Range - ranges.add(range); + private List getConfigurationsFromRanges(List ranges){ + List configurationList = new ArrayList<>(); + for (OTSRange range:ranges + ) { + Configuration configuration = Configuration.newDefault(); + configuration.set(Constant.ConfigKey.CONF, GsonParser.confToJson(conf)); + configuration.set(Constant.ConfigKey.RANGE, GsonParser.rangeToJson(range)); + configuration.set(Constant.ConfigKey.META, GsonParser.metaToJson(meta)); + configurationList.add(configuration); } - return ranges; + return configurationList; + } + + private List getPKOfFirstRow( + OTSRange range , Direction direction) throws Exception { + + RangeRowQueryCriteria cur = new RangeRowQueryCriteria(this.conf.getTableName()); + cur.setInclusiveStartPrimaryKey(new PrimaryKey(range.getBegin())); + cur.setExclusiveEndPrimaryKey(new PrimaryKey(range.getEnd())); + cur.setLimit(1); + cur.addColumnsToGet(Common.getPrimaryKeyNameList(meta)); + cur.setDirection(direction); + cur.setMaxVersions(1); + + return RetryHelper.executeWithRetry( + new GetFirstRowPrimaryKeyCallable(ots, meta, cur), + conf.getRetry(), + conf.getRetryPauseInMillisecond() + ); } + } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveMetaProxy.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveMetaProxy.java new file mode 100644 index 0000000000..f9860194dc --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveMetaProxy.java @@ -0,0 +1,160 @@ +package com.alibaba.datax.plugin.reader.otsreader; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import com.alibaba.datax.plugin.reader.otsreader.model.OTSConf; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSRange; +import com.alibaba.datax.plugin.reader.otsreader.utils.Constant; +import com.alibaba.datax.plugin.reader.otsreader.utils.Key; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.element.StringColumn; +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.otsreader.utils.ParamCheckerOld; +import com.alibaba.datax.plugin.reader.otsreader.utils.ReaderModelParser; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSColumn; +import com.alibaba.datax.plugin.reader.otsreader.utils.DefaultNoRetry; +import com.alibaba.datax.plugin.reader.otsreader.utils.GsonParser; +import com.alibaba.fastjson.JSON; +import com.aliyun.openservices.ots.OTSClient; +import com.aliyun.openservices.ots.OTSServiceConfiguration; +import com.aliyun.openservices.ots.model.DescribeTableRequest; +import com.aliyun.openservices.ots.model.DescribeTableResult; +import com.aliyun.openservices.ots.model.ListTableResult; +import com.aliyun.openservices.ots.model.PrimaryKeyType; +import com.aliyun.openservices.ots.model.ReservedThroughputDetails; +import com.aliyun.openservices.ots.model.TableMeta; + +public class OtsReaderSlaveMetaProxy implements IOtsReaderSlaveProxy { + + private OTSClient ots = null; + private OTSConf conf = null; + private OTSRange range = null; + private com.alicloud.openservices.tablestore.model.TableMeta meta = null; + private Configuration configuration = null; + private static final Logger LOG = LoggerFactory.getLogger(OtsReaderSlaveMetaProxy.class); + + + @Override + public void init(Configuration configuration) { + OTSServiceConfiguration configure = new OTSServiceConfiguration(); + configure.setRetryStrategy(new DefaultNoRetry()); + + this.configuration = configuration; + conf = GsonParser.jsonToConf((String) configuration.get(Constant.ConfigKey.CONF)); + range = GsonParser.jsonToRange((String) configuration.get(Constant.ConfigKey.RANGE)); + meta = GsonParser.jsonToMeta((String) configuration.get(Constant.ConfigKey.META)); + + String endpoint = conf.getEndpoint(); + String accessId = conf.getAccessId(); + String accessKey = conf.getAccessKey(); + String instanceName = conf.getInstanceName(); + + ots = new OTSClient(endpoint, accessId, accessKey, instanceName, null, configure, null); + } + + @Override + public void close() { + ots.shutdown(); + } + + @Override + public void startRead(RecordSender recordSender) throws Exception { + List columns = ReaderModelParser + .parseOTSColumnList(ParamCheckerOld.checkListAndGet(configuration, Key.COLUMN, true)); + String metaMode = conf.getMetaMode(); // column + + + ListTableResult listTableResult = null; + try { + listTableResult = ots.listTable(); + LOG.info(String.format("ots listTable requestId:%s, traceId:%s", listTableResult.getRequestID(), + listTableResult.getTraceId())); + List allTables = listTableResult.getTableNames(); + for (String eachTable : allTables) { + DescribeTableRequest describeTableRequest = new DescribeTableRequest(); + describeTableRequest.setTableName(eachTable); + DescribeTableResult describeTableResult = ots.describeTable(describeTableRequest); + LOG.info(String.format("ots describeTable requestId:%s, traceId:%s", describeTableResult.getRequestID(), + describeTableResult.getTraceId())); + + TableMeta tableMeta = describeTableResult.getTableMeta(); + // table_name: first_table + // table primary key: type, data type: STRING + // table primary key: db_name, data type: STRING + // table primary key: table_name, data type: STRING + // Reserved throughput: read(0), write(0) + // last increase time: 1502881295 + // last decrease time: None + // number of decreases today: 0 + + String tableName = tableMeta.getTableName(); + Map primaryKey = tableMeta.getPrimaryKey(); + ReservedThroughputDetails reservedThroughputDetails = describeTableResult + .getReservedThroughputDetails(); + int reservedThroughputRead = reservedThroughputDetails.getCapacityUnit().getReadCapacityUnit(); + int reservedThroughputWrite = reservedThroughputDetails.getCapacityUnit().getWriteCapacityUnit(); + long lastIncreaseTime = reservedThroughputDetails.getLastIncreaseTime(); + long lastDecreaseTime = reservedThroughputDetails.getLastDecreaseTime(); + int numberOfDecreasesToday = reservedThroughputDetails.getNumberOfDecreasesToday(); + + Map allData = new HashMap(); + allData.put("endpoint", conf.getEndpoint()); + allData.put("instanceName", conf.getInstanceName()); + allData.put("table", tableName); + // allData.put("primaryKey", JSON.toJSONString(primaryKey)); + allData.put("reservedThroughputRead", reservedThroughputRead + ""); + allData.put("reservedThroughputWrite", reservedThroughputWrite + ""); + allData.put("lastIncreaseTime", lastIncreaseTime + ""); + allData.put("lastDecreaseTime", lastDecreaseTime + ""); + allData.put("numberOfDecreasesToday", numberOfDecreasesToday + ""); + + // 可扩展的可配置的形式 + if ("column".equalsIgnoreCase(metaMode)) { + // 如果是列元数据模式并且column中配置的name是primaryKey,映射成多行DataX Record + List primaryKeyRecords = new ArrayList(); + for (Entry eachPk : primaryKey.entrySet()) { + Record line = recordSender.createRecord(); + for (OTSColumn col : columns) { + if (col.getColumnType() == OTSColumn.OTSColumnType.CONST) { + line.addColumn(col.getValue()); + } else if ("primaryKey.name".equalsIgnoreCase(col.getName())) { + line.addColumn(new StringColumn(eachPk.getKey())); + } else if ("primaryKey.type".equalsIgnoreCase(col.getName())) { + line.addColumn(new StringColumn(eachPk.getValue().name())); + } else { + String v = allData.get(col.getName()); + line.addColumn(new StringColumn(v)); + } + } + LOG.debug("Reader send record : {}", line.toString()); + recordSender.sendToWriter(line); + primaryKeyRecords.add(line); + } + } else { + Record line = recordSender.createRecord(); + for (OTSColumn col : columns) { + if (col.getColumnType() == OTSColumn.OTSColumnType.CONST) { + line.addColumn(col.getValue()); + } else { + String v = allData.get(col.getName()); + line.addColumn(new StringColumn(v)); + } + } + LOG.debug("Reader send record : {}", line.toString()); + recordSender.sendToWriter(line); + } + } + } catch (Exception e) { + LOG.warn(JSON.toJSONString(listTableResult), e); + } + + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveMultiVersionProxy.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveMultiVersionProxy.java new file mode 100644 index 0000000000..818a507ee9 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveMultiVersionProxy.java @@ -0,0 +1,102 @@ +package com.alibaba.datax.plugin.reader.otsreader; + +import com.alibaba.datax.common.element.LongColumn; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.element.StringColumn; +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSConf; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSRange; +import com.alibaba.datax.plugin.reader.otsreader.utils.*; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.model.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class OtsReaderSlaveMultiVersionProxy implements IOtsReaderSlaveProxy { + private OTSConf conf = null; + private OTSRange range = null; + private TableMeta meta = null; + private SyncClientInterface ots = null; + + private static final Logger LOG = LoggerFactory.getLogger(OtsReaderSlaveMultiVersionProxy.class); + + @Override + public void init(Configuration configuration) { + conf = GsonParser.jsonToConf((String) configuration.get(Constant.ConfigKey.CONF)); + range = GsonParser.jsonToRange((String) configuration.get(Constant.ConfigKey.RANGE)); + meta = GsonParser.jsonToMeta((String) configuration.get(Constant.ConfigKey.META)); + + this.ots = OtsHelper.getOTSInstance(conf); + } + + @Override + public void close() { + ots.shutdown(); + } + + private void sendToDatax(RecordSender recordSender, PrimaryKey pk, Column c) { + Record line = recordSender.createRecord(); + //------------------------- + // 四元组 pk, column name, timestamp, value + //------------------------- + + // pk + for( PrimaryKeyColumn pkc : pk.getPrimaryKeyColumns()) { + line.addColumn(TranformHelper.otsPrimaryKeyColumnToDataxColumn(pkc)); + } + // column name + line.addColumn(new StringColumn(c.getName())); + // Timestamp + line.addColumn(new LongColumn(c.getTimestamp())); + // Value + line.addColumn(TranformHelper.otsColumnToDataxColumn(c)); + + recordSender.sendToWriter(line); + } + + private void sendToDatax(RecordSender recordSender, Row row) { + PrimaryKey pk = row.getPrimaryKey(); + for (Column c : row.getColumns()) { + sendToDatax(recordSender, pk, c); + } + } + + /** + * 将获取到的数据采用4元组的方式传递给datax + * @param recordSender + * @param result + */ + private void sendToDatax(RecordSender recordSender, GetRangeResponse result) { + LOG.debug("Per request get row count : " + result.getRows().size()); + for (Row row : result.getRows()) { + sendToDatax(recordSender, row); + } + } + + @Override + public void startRead(RecordSender recordSender) throws Exception { + + PrimaryKey inclusiveStartPrimaryKey = new PrimaryKey(range.getBegin()); + PrimaryKey exclusiveEndPrimaryKey = new PrimaryKey(range.getEnd()); + PrimaryKey next = inclusiveStartPrimaryKey; + + RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(conf.getTableName()); + rangeRowQueryCriteria.setExclusiveEndPrimaryKey(exclusiveEndPrimaryKey); + rangeRowQueryCriteria.setDirection(Common.getDirection(range.getBegin(), range.getEnd())); + rangeRowQueryCriteria.setTimeRange(conf.getMulti().getTimeRange()); + rangeRowQueryCriteria.setMaxVersions(conf.getMulti().getMaxVersion()); + rangeRowQueryCriteria.addColumnsToGet(Common.toColumnToGet(conf.getColumn(), meta)); + + do{ + rangeRowQueryCriteria.setInclusiveStartPrimaryKey(next); + GetRangeResponse result = OtsHelper.getRange( + ots, + rangeRowQueryCriteria, + conf.getRetry(), + conf.getRetryPauseInMillisecond()); + sendToDatax(recordSender, result); + next = result.getNextStartPrimaryKey(); + } while(next != null); + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveNormalProxy.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveNormalProxy.java new file mode 100644 index 0000000000..f7d89b1537 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveNormalProxy.java @@ -0,0 +1,256 @@ +package com.alibaba.datax.plugin.reader.otsreader; + +import com.alibaba.datax.common.element.LongColumn; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.element.StringColumn; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSColumn; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSConf; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSCriticalException; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSRange; +import com.alibaba.datax.plugin.reader.otsreader.utils.*; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.core.utils.Pair; +import com.alicloud.openservices.tablestore.model.*; +import com.alicloud.openservices.tablestore.model.timeseries.ScanTimeseriesDataRequest; +import com.alicloud.openservices.tablestore.model.timeseries.ScanTimeseriesDataResponse; +import com.alicloud.openservices.tablestore.model.timeseries.TimeseriesRow; +import com.alicloud.openservices.tablestore.model.timeseries.TimeseriesScanSplitInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class OtsReaderSlaveNormalProxy implements IOtsReaderSlaveProxy { + private static final Logger LOG = LoggerFactory.getLogger(OtsReaderSlaveNormalProxy.class); + private OTSConf conf = null; + private OTSRange range = null; + private TableMeta meta = null; + private SyncClientInterface ots = null; + private TimeseriesScanSplitInfo splitInfo = null; + + @Override + public void init(Configuration configuration) { + conf = GsonParser.jsonToConf((String) configuration.get(Constant.ConfigKey.CONF)); + if (!conf.isTimeseriesTable()) { + range = GsonParser.jsonToRange((String) configuration.get(Constant.ConfigKey.RANGE)); + meta = GsonParser.jsonToMeta((String) configuration.get(Constant.ConfigKey.META)); + } else { + splitInfo = GsonParser.stringToTimeseriesScanSplitInfo((String) configuration.get(Constant.ConfigKey.SPLIT_INFO)); + // 时序表 检查tablestore SDK version + try{ + Common.checkTableStoreSDKVersion(); + } + catch (Exception e){ + LOG.error("Exception. ErrorMsg:{}", e.getMessage(), e); + throw DataXException.asDataXException(OtsReaderError.ERROR, e.toString(), e); + } + } + + + this.ots = OtsHelper.getOTSInstance(conf); + } + + @Override + public void close() { + ots.shutdown(); + } + + private void sendToDatax(RecordSender recordSender, Row row) { + Record line = recordSender.createRecord(); + + PrimaryKey pk = row.getPrimaryKey(); + for (OTSColumn column : conf.getColumn()) { + if (column.getColumnType() == OTSColumn.OTSColumnType.NORMAL) { + // 获取指定的列 + PrimaryKeyColumn value = pk.getPrimaryKeyColumn(column.getName()); + if (value != null) { + line.addColumn(TranformHelper.otsPrimaryKeyColumnToDataxColumn(value)); + } else { + Column c = row.getLatestColumn(column.getName()); + if (c != null) { + line.addColumn(TranformHelper.otsColumnToDataxColumn(c)); + } else { + // 这里使用StringColumn的无参构造函数构造对象,而不是用null,下 + // 游(writer)应该通过获取Column,然后通过Column的数据接口的返回值 + // 是否是null来判断改Column是否为null + // Datax其他插件的也是使用这种方式,约定俗成,并没有使用直接向record中注入null方式代表空 + line.addColumn(new StringColumn()); + } + } + } else { + line.addColumn(column.getValue()); + } + } + recordSender.sendToWriter(line); + } + + private void sendToDatax(RecordSender recordSender, TimeseriesRow row) { + + + Record line = recordSender.createRecord(); + // 对于配置项中的每一列 + for (int i = 0; i < conf.getColumn().size(); i++) { + OTSColumn column = conf.getColumn().get(i); + // 如果不是常数列 + if (column.getColumnType() == OTSColumn.OTSColumnType.NORMAL) { + // 如果是tags内字段 + if (conf.getColumn().get(i).getTimeseriesTag()) { + String s = row.getTimeseriesKey().getTags().get(column.getName()); + line.addColumn(new StringColumn(s)); + } + // 如果为measurement字段 + else if (column.getName().equals(Constant.ConfigKey.TimeseriesPKColumn.MEASUREMENT_NAME)) { + String s = row.getTimeseriesKey().getMeasurementName(); + line.addColumn(new StringColumn(s)); + } + // 如果为dataSource字段 + else if (column.getName().equals(Constant.ConfigKey.TimeseriesPKColumn.DATA_SOURCE)) { + String s = row.getTimeseriesKey().getDataSource(); + line.addColumn(new StringColumn(s)); + } + // 如果为tags字段 + else if (column.getName().equals(Constant.ConfigKey.TimeseriesPKColumn.TAGS)) { + line.addColumn(new StringColumn(row.getTimeseriesKey().buildTagsString())); + } + else if (column.getName().equals(Constant.ConfigKey.TimeseriesPKColumn.TIME)) { + Long l = row.getTimeInUs(); + line.addColumn(new LongColumn(l)); + } + // 否则为field内字段 + else { + ColumnValue c = row.getFields().get(column.getName()); + if (c == null) { + LOG.warn("Get column {} : type {} failed, use empty string instead", column.getName(), conf.getColumn().get(i).getValueType()); + line.addColumn(new StringColumn()); + } else if (c.getType() != conf.getColumn().get(i).getValueType()) { + LOG.warn("Get column {} failed, expected type: {}, actual type: {}. Sending actual type to writer.", column.getName(), conf.getColumn().get(i).getValueType(), c.getType()); + line.addColumn(TranformHelper.otsColumnToDataxColumn(c)); + } else { + line.addColumn(TranformHelper.otsColumnToDataxColumn(c)); + } + } + } + // 如果是常数列 + else { + line.addColumn(column.getValue()); + } + } + recordSender.sendToWriter(line); + } + + /** + * 将获取到的数据根据用户配置Column的方式传递给datax + * + * @param recordSender + * @param result + */ + private void sendToDatax(RecordSender recordSender, GetRangeResponse result) { + for (Row row : result.getRows()) { + sendToDatax(recordSender, row); + } + } + + private void sendToDatax(RecordSender recordSender, ScanTimeseriesDataResponse result) { + for (TimeseriesRow row : result.getRows()) { + sendToDatax(recordSender, row); + } + } + + @Override + public void startRead(RecordSender recordSender) throws Exception { + if (conf.isTimeseriesTable()) { + readTimeseriesTable(recordSender); + } else { + readNormalTable(recordSender); + } + } + + public void readTimeseriesTable(RecordSender recordSender) throws Exception { + + List timeseriesPkName = new ArrayList<>(); + timeseriesPkName.add(Constant.ConfigKey.TimeseriesPKColumn.MEASUREMENT_NAME); + timeseriesPkName.add(Constant.ConfigKey.TimeseriesPKColumn.DATA_SOURCE); + timeseriesPkName.add(Constant.ConfigKey.TimeseriesPKColumn.TAGS); + timeseriesPkName.add(Constant.ConfigKey.TimeseriesPKColumn.TIME); + + ScanTimeseriesDataRequest scanTimeseriesDataRequest = new ScanTimeseriesDataRequest(conf.getTableName()); + List> fieldsToGet = new ArrayList<>(); + for (int i = 0; i < conf.getColumn().size(); i++) { + /** + * 如果所配置列 + * 1. 不是常量列(即列名不为null) + * 2. 列名不在["measurementName","dataSource","tags"]中 + * 3. 不是tags内的字段 + * 则为需要获取的field字段。 + */ + String fieldName = conf.getColumn().get(i).getName(); + if (fieldName != null && !timeseriesPkName.contains(fieldName) && !conf.getColumn().get(i).getTimeseriesTag()) { + Pair pair = new Pair<>(fieldName, conf.getColumn().get(i).getValueType()); + fieldsToGet.add(pair); + } + } + scanTimeseriesDataRequest.setFieldsToGet(fieldsToGet); + scanTimeseriesDataRequest.setSplitInfo(splitInfo); + + while (true) { + ScanTimeseriesDataResponse response = OtsHelper.scanTimeseriesData( + ots, + scanTimeseriesDataRequest, + conf.getRetry(), + conf.getRetryPauseInMillisecond()); + sendToDatax(recordSender, response); + if (response.getNextToken() == null) { + break; + } + scanTimeseriesDataRequest.setNextToken(response.getNextToken()); + } + } + + public void readNormalTable(RecordSender recordSender) throws Exception { + PrimaryKey inclusiveStartPrimaryKey = new PrimaryKey(range.getBegin()); + PrimaryKey exclusiveEndPrimaryKey = new PrimaryKey(range.getEnd()); + PrimaryKey next = inclusiveStartPrimaryKey; + + RangeRowQueryCriteria rangeRowQueryCriteria = new RangeRowQueryCriteria(conf.getTableName()); + rangeRowQueryCriteria.setExclusiveEndPrimaryKey(exclusiveEndPrimaryKey); + rangeRowQueryCriteria.setDirection(Common.getDirection(range.getBegin(), range.getEnd())); + rangeRowQueryCriteria.setMaxVersions(1); + rangeRowQueryCriteria.addColumnsToGet(Common.toColumnToGet(conf.getColumn(), meta)); + + do { + rangeRowQueryCriteria.setInclusiveStartPrimaryKey(next); + GetRangeResponse result = OtsHelper.getRange( + ots, + rangeRowQueryCriteria, + conf.getRetry(), + conf.getRetryPauseInMillisecond()); + sendToDatax(recordSender, result); + next = result.getNextStartPrimaryKey(); + } while (next != null); + } + + + public void setConf(OTSConf conf) { + this.conf = conf; + } + + + public void setRange(OTSRange range) { + this.range = range; + } + + + public void setMeta(TableMeta meta) { + this.meta = meta; + } + + + public void setOts(SyncClientInterface ots) { + this.ots = ots; + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveProxy.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveProxy.java deleted file mode 100644 index e64b4e7e24..0000000000 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveProxy.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.alibaba.datax.plugin.reader.otsreader; - -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.alibaba.datax.common.element.Record; -import com.alibaba.datax.common.plugin.RecordSender; -import com.alibaba.datax.common.util.Configuration; -import com.alibaba.datax.plugin.reader.otsreader.callable.GetRangeCallable; -import com.alibaba.datax.plugin.reader.otsreader.model.OTSColumn; -import com.alibaba.datax.plugin.reader.otsreader.model.OTSConf; -import com.alibaba.datax.plugin.reader.otsreader.model.OTSConst; -import com.alibaba.datax.plugin.reader.otsreader.model.OTSRange; -import com.alibaba.datax.plugin.reader.otsreader.utils.Common; -import com.alibaba.datax.plugin.reader.otsreader.utils.GsonParser; -import com.alibaba.datax.plugin.reader.otsreader.utils.DefaultNoRetry; -import com.alibaba.datax.plugin.reader.otsreader.utils.RetryHelper; -import com.aliyun.openservices.ots.OTSClientAsync; -import com.aliyun.openservices.ots.OTSServiceConfiguration; -import com.aliyun.openservices.ots.model.Direction; -import com.aliyun.openservices.ots.model.GetRangeRequest; -import com.aliyun.openservices.ots.model.GetRangeResult; -import com.aliyun.openservices.ots.model.OTSFuture; -import com.aliyun.openservices.ots.model.RangeRowQueryCriteria; -import com.aliyun.openservices.ots.model.Row; -import com.aliyun.openservices.ots.model.RowPrimaryKey; - -public class OtsReaderSlaveProxy { - - class RequestItem { - private RangeRowQueryCriteria criteria; - private OTSFuture future; - - RequestItem(RangeRowQueryCriteria criteria, OTSFuture future) { - this.criteria = criteria; - this.future = future; - } - - public RangeRowQueryCriteria getCriteria() { - return criteria; - } - - public OTSFuture getFuture() { - return future; - } - } - - private static final Logger LOG = LoggerFactory.getLogger(OtsReaderSlaveProxy.class); - - private void rowsToSender(List rows, RecordSender sender, List columns) { - for (Row row : rows) { - Record line = sender.createRecord(); - line = Common.parseRowToLine(row, columns, line); - - LOG.debug("Reader send record : {}", line.toString()); - - sender.sendToWriter(line); - } - } - - private RangeRowQueryCriteria generateRangeRowQueryCriteria(String tableName, RowPrimaryKey begin, RowPrimaryKey end, Direction direction, List columns) { - RangeRowQueryCriteria criteria = new RangeRowQueryCriteria(tableName); - criteria.setInclusiveStartPrimaryKey(begin); - criteria.setDirection(direction); - criteria.setColumnsToGet(columns); - criteria.setLimit(-1); - criteria.setExclusiveEndPrimaryKey(end); - return criteria; - } - - private RequestItem generateRequestItem( - OTSClientAsync ots, - OTSConf conf, - RowPrimaryKey begin, - RowPrimaryKey end, - Direction direction, - List columns) throws Exception { - RangeRowQueryCriteria criteria = generateRangeRowQueryCriteria(conf.getTableName(), begin, end, direction, columns); - - GetRangeRequest request = new GetRangeRequest(); - request.setRangeRowQueryCriteria(criteria); - OTSFuture future = ots.getRange(request); - - return new RequestItem(criteria, future); - } - - public void read(RecordSender sender, Configuration configuration) throws Exception { - LOG.info("read begin."); - - OTSConf conf = GsonParser.jsonToConf(configuration.getString(OTSConst.OTS_CONF)); - OTSRange range = GsonParser.jsonToRange(configuration.getString(OTSConst.OTS_RANGE)); - Direction direction = GsonParser.jsonToDirection(configuration.getString(OTSConst.OTS_DIRECTION)); - - OTSServiceConfiguration configure = new OTSServiceConfiguration(); - configure.setRetryStrategy(new DefaultNoRetry()); - - OTSClientAsync ots = new OTSClientAsync( - conf.getEndpoint(), - conf.getAccessId(), - conf.getAccesskey(), - conf.getInstanceName(), - null, - configure, - null); - - RowPrimaryKey token = range.getBegin(); - List columns = Common.getNormalColumnNameList(conf.getColumns()); - - RequestItem request = null; - - do { - LOG.debug("Next token : {}", GsonParser.rowPrimaryKeyToJson(token)); - if (request == null) { - request = generateRequestItem(ots, conf, token, range.getEnd(), direction, columns); - } else { - RequestItem req = request; - - GetRangeResult result = RetryHelper.executeWithRetry( - new GetRangeCallable(ots, req.getCriteria(), req.getFuture()), - conf.getRetry(), - conf.getSleepInMilliSecond() - ); - if ((token = result.getNextStartPrimaryKey()) != null) { - request = generateRequestItem(ots, conf, token, range.getEnd(), direction, columns); - } - - rowsToSender(result.getRows(), sender, conf.getColumns()); - } - } while (token != null); - ots.shutdown(); - LOG.info("read end."); - } -} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveProxyOld.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveProxyOld.java new file mode 100644 index 0000000000..72eb885e9b --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderSlaveProxyOld.java @@ -0,0 +1,181 @@ +package com.alibaba.datax.plugin.reader.otsreader; + +import java.util.List; + +import com.alibaba.datax.plugin.reader.otsreader.model.OTSRange; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSColumn; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSConf; +import com.alibaba.datax.plugin.reader.otsreader.utils.*; +import com.alicloud.openservices.tablestore.model.PrimaryKeyColumn; +import com.aliyun.openservices.ots.model.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.otsreader.callable.GetRangeCallableOld; +import com.aliyun.openservices.ots.OTSClientAsync; +import com.aliyun.openservices.ots.OTSServiceConfiguration; + +public class OtsReaderSlaveProxyOld implements IOtsReaderSlaveProxy { + + + private OTSClientAsync ots = null; + private OTSConf conf = null; + private OTSRange range = null; + + class RequestItem { + private RangeRowQueryCriteria criteria; + private OTSFuture future; + + RequestItem(RangeRowQueryCriteria criteria, OTSFuture future) { + this.criteria = criteria; + this.future = future; + } + + public RangeRowQueryCriteria getCriteria() { + return criteria; + } + + public OTSFuture getFuture() { + return future; + } + } + + private static final Logger LOG = LoggerFactory.getLogger(OtsReaderSlaveProxyOld.class); + + private void rowsToSender(List rows, RecordSender sender, List columns) { + for (Row row : rows) { + Record line = sender.createRecord(); + line = CommonOld.parseRowToLine(row, columns, line); + + LOG.debug("Reader send record : {}", line.toString()); + + sender.sendToWriter(line); + } + } + + private RangeRowQueryCriteria generateRangeRowQueryCriteria(String tableName, RowPrimaryKey begin, RowPrimaryKey end, Direction direction, List columns) { + RangeRowQueryCriteria criteria = new RangeRowQueryCriteria(tableName); + criteria.setInclusiveStartPrimaryKey(begin); + criteria.setDirection(direction); + criteria.setColumnsToGet(columns); + criteria.setLimit(-1); + criteria.setExclusiveEndPrimaryKey(end); + return criteria; + } + + private RequestItem generateRequestItem( + OTSClientAsync ots, + OTSConf conf, + RowPrimaryKey begin, + RowPrimaryKey end, + Direction direction, + List columns) throws Exception { + RangeRowQueryCriteria criteria = generateRangeRowQueryCriteria(conf.getTableName(), begin, end, direction, columns); + + GetRangeRequest request = new GetRangeRequest(); + request.setRangeRowQueryCriteria(criteria); + OTSFuture future = ots.getRange(request); + + return new RequestItem(criteria, future); + } + + @Override + public void init(Configuration configuration) { + conf = GsonParser.jsonToConf(configuration.getString(Constant.ConfigKey.CONF)); + range = GsonParser.jsonToRange(configuration.getString(Constant.ConfigKey.RANGE)); + + OTSServiceConfiguration configure = new OTSServiceConfiguration(); + configure.setRetryStrategy(new DefaultNoRetry()); + + ots = new OTSClientAsync( + conf.getEndpoint(), + conf.getAccessId(), + conf.getAccessKey(), + conf.getInstanceName(), + null, + configure, + null); + } + + @Override + public void close() { + ots.shutdown(); + } + + @Override + public void startRead(RecordSender recordSender) throws Exception { + RowPrimaryKey token = pKColumnList2RowPrimaryKey(range.getBegin()); + + List columns = CommonOld.getNormalColumnNameList(conf.getColumn()); + Direction direction = null; + switch (Common.getDirection(range.getBegin(), range.getEnd())){ + case FORWARD: + direction = Direction.FORWARD; + break; + case BACKWARD: + default: + direction = Direction.BACKWARD; + } + RequestItem request = null; + + do { + LOG.debug("Next token : {}", GsonParser.rowPrimaryKeyToJson(token)); + if (request == null) { + request = generateRequestItem(ots, conf, token, pKColumnList2RowPrimaryKey(range.getEnd()), direction, columns); + } else { + RequestItem req = request; + + GetRangeResult result = RetryHelperOld.executeWithRetry( + new GetRangeCallableOld(ots, req.getCriteria(), req.getFuture()), + conf.getRetry(), + // TODO + 100 + ); + if ((token = result.getNextStartPrimaryKey()) != null) { + request = generateRequestItem(ots, conf, token, pKColumnList2RowPrimaryKey(range.getEnd()), direction, columns); + } + + rowsToSender(result.getRows(), recordSender, conf.getColumn()); + } + } while (token != null); + } + + /** + * 将 {@link com.alicloud.openservices.tablestore.model.PrimaryKeyColumn}的列表转为{@link com.aliyun.openservices.ots.model.RowPrimaryKey} + * @param list + * @return + */ + public RowPrimaryKey pKColumnList2RowPrimaryKey(List list){ + RowPrimaryKey rowPrimaryKey = new RowPrimaryKey(); + for(PrimaryKeyColumn pk : list){ + PrimaryKeyValue v = null; + if(pk.getValue() == com.alicloud.openservices.tablestore.model.PrimaryKeyValue.INF_MAX){ + v = PrimaryKeyValue.INF_MAX; + } else if (pk.getValue() == com.alicloud.openservices.tablestore.model.PrimaryKeyValue.INF_MIN) { + v = PrimaryKeyValue.INF_MIN; + } + // 非INF_MAX 或 INF_MIN + else{ + switch (pk.getValue().getType()){ + case STRING: + v = PrimaryKeyValue.fromString(pk.getValue().asString()); + break; + case INTEGER: + v = PrimaryKeyValue.fromLong(pk.getValue().asLong()); + break; + case BINARY: + v = PrimaryKeyValue.fromBinary(pk.getValue().asBinary()); + break; + default: + throw new IllegalArgumentException("the pKColumnList to RowPrimaryKey conversion failed"); + } + } + + rowPrimaryKey.addPrimaryKeyColumn(pk.getName(),v); + } + return rowPrimaryKey; + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/adaptor/ColumnAdaptor.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/adaptor/ColumnAdaptor.java new file mode 100644 index 0000000000..b2e14b5c1e --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/adaptor/ColumnAdaptor.java @@ -0,0 +1,63 @@ +package com.alibaba.datax.plugin.reader.otsreader.adaptor; + +import com.alibaba.datax.common.element.*; +import com.google.gson.*; +import org.apache.commons.codec.binary.Base64; + +import java.lang.reflect.Type; + +public class ColumnAdaptor implements JsonDeserializer, JsonSerializer{ + private final static String TYPE = "type"; + private final static String RAW = "rawData"; + + @Override + public JsonElement serialize(Column obj, Type t, + JsonSerializationContext c) { + JsonObject json = new JsonObject(); + + String rawData = null; + switch (obj.getType()){ + case BOOL: + rawData = String.valueOf(obj.getRawData()); break; + case BYTES: + rawData = Base64.encodeBase64String((byte[]) obj.getRawData()); break; + case DOUBLE: + rawData = String.valueOf(obj.getRawData());break; + case LONG: + rawData = String.valueOf(obj.getRawData());break; + case STRING: + rawData = String.valueOf(obj.getRawData());break; + default: + throw new IllegalArgumentException("Unsupport parse the column type:" + obj.getType().toString()); + + } + json.add(TYPE, new JsonPrimitive(obj.getType().toString())); + json.add(RAW, new JsonPrimitive(rawData)); + return json; + } + + @Override + public Column deserialize(JsonElement ele, Type t, + JsonDeserializationContext c) throws JsonParseException { + JsonObject obj = ele.getAsJsonObject(); + + String strType = obj.getAsJsonPrimitive(TYPE).getAsString(); + String strRaw = obj.getAsJsonPrimitive(RAW).getAsString(); + Column.Type type = Column.Type.valueOf(strType); + switch (type){ + case BOOL: + return new BoolColumn(strRaw); + case BYTES: + return new BytesColumn(Base64.decodeBase64(strRaw)); + case DOUBLE: + return new DoubleColumn(strRaw); + case LONG: + return new LongColumn(strRaw); + case STRING: + return new StringColumn(strRaw); + default: + throw new IllegalArgumentException("Unsupport parse the column type:" + type.toString()); + + } + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/adaptor/OTSColumnAdaptor.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/adaptor/OTSColumnAdaptor.java deleted file mode 100644 index 25f9b682c2..0000000000 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/adaptor/OTSColumnAdaptor.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.alibaba.datax.plugin.reader.otsreader.adaptor; - -import java.lang.reflect.Type; - -import org.apache.commons.codec.binary.Base64; - -import com.alibaba.datax.plugin.reader.otsreader.model.OTSColumn; -import com.aliyun.openservices.ots.model.ColumnType; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; - -public class OTSColumnAdaptor implements JsonDeserializer, JsonSerializer{ - private final static String NAME = "name"; - private final static String COLUMN_TYPE = "column_type"; - private final static String VALUE_TYPE = "value_type"; - private final static String VALUE = "value"; - - private void serializeConstColumn(JsonObject json, OTSColumn obj) { - switch (obj.getValueType()) { - case STRING : - json.add(VALUE_TYPE, new JsonPrimitive(ColumnType.STRING.toString())); - json.add(VALUE, new JsonPrimitive(obj.getValue().asString())); - break; - case INTEGER : - json.add(VALUE_TYPE, new JsonPrimitive(ColumnType.INTEGER.toString())); - json.add(VALUE, new JsonPrimitive(obj.getValue().asLong())); - break; - case DOUBLE : - json.add(VALUE_TYPE, new JsonPrimitive(ColumnType.DOUBLE.toString())); - json.add(VALUE, new JsonPrimitive(obj.getValue().asDouble())); - break; - case BOOLEAN : - json.add(VALUE_TYPE, new JsonPrimitive(ColumnType.BOOLEAN.toString())); - json.add(VALUE, new JsonPrimitive(obj.getValue().asBoolean())); - break; - case BINARY : - json.add(VALUE_TYPE, new JsonPrimitive(ColumnType.BINARY.toString())); - json.add(VALUE, new JsonPrimitive(Base64.encodeBase64String(obj.getValue().asBytes()))); - break; - default: - throw new IllegalArgumentException("Unsupport serialize the type : " + obj.getValueType() + ""); - } - } - - private OTSColumn deserializeConstColumn(JsonObject obj) { - String strType = obj.getAsJsonPrimitive(VALUE_TYPE).getAsString(); - ColumnType type = ColumnType.valueOf(strType); - - JsonPrimitive jsonValue = obj.getAsJsonPrimitive(VALUE); - - switch (type) { - case STRING : - return OTSColumn.fromConstStringColumn(jsonValue.getAsString()); - case INTEGER : - return OTSColumn.fromConstIntegerColumn(jsonValue.getAsLong()); - case DOUBLE : - return OTSColumn.fromConstDoubleColumn(jsonValue.getAsDouble()); - case BOOLEAN : - return OTSColumn.fromConstBoolColumn(jsonValue.getAsBoolean()); - case BINARY : - return OTSColumn.fromConstBytesColumn(Base64.decodeBase64(jsonValue.getAsString())); - default: - throw new IllegalArgumentException("Unsupport deserialize the type : " + type + ""); - } - } - - private void serializeNormalColumn(JsonObject json, OTSColumn obj) { - json.add(NAME, new JsonPrimitive(obj.getName())); - } - - private OTSColumn deserializeNormarlColumn(JsonObject obj) { - return OTSColumn.fromNormalColumn(obj.getAsJsonPrimitive(NAME).getAsString()); - } - - @Override - public JsonElement serialize(OTSColumn obj, Type t, - JsonSerializationContext c) { - JsonObject json = new JsonObject(); - - switch (obj.getColumnType()) { - case CONST: - json.add(COLUMN_TYPE, new JsonPrimitive(OTSColumn.OTSColumnType.CONST.toString())); - serializeConstColumn(json, obj); - break; - case NORMAL: - json.add(COLUMN_TYPE, new JsonPrimitive(OTSColumn.OTSColumnType.NORMAL.toString())); - serializeNormalColumn(json, obj); - break; - default: - throw new IllegalArgumentException("Unsupport serialize the type : " + obj.getColumnType() + ""); - } - return json; - } - - @Override - public OTSColumn deserialize(JsonElement ele, Type t, - JsonDeserializationContext c) throws JsonParseException { - JsonObject obj = ele.getAsJsonObject(); - String strColumnType = obj.getAsJsonPrimitive(COLUMN_TYPE).getAsString(); - OTSColumn.OTSColumnType columnType = OTSColumn.OTSColumnType.valueOf(strColumnType); - - switch(columnType) { - case CONST: - return deserializeConstColumn(obj); - case NORMAL: - return deserializeNormarlColumn(obj); - default: - throw new IllegalArgumentException("Unsupport deserialize the type : " + columnType + ""); - } - } -} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/adaptor/PrimaryKeyValueAdaptor.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/adaptor/PrimaryKeyValueAdaptor.java index 1a49ea476f..240427aeaf 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/adaptor/PrimaryKeyValueAdaptor.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/adaptor/PrimaryKeyValueAdaptor.java @@ -1,18 +1,12 @@ package com.alibaba.datax.plugin.reader.otsreader.adaptor; -import java.lang.reflect.Type; +import com.alicloud.openservices.tablestore.model.ColumnType; +import com.alicloud.openservices.tablestore.model.PrimaryKeyType; +import com.alicloud.openservices.tablestore.model.PrimaryKeyValue; +import com.google.gson.*; +import org.apache.commons.codec.binary.Base64; -import com.aliyun.openservices.ots.model.ColumnType; -import com.aliyun.openservices.ots.model.PrimaryKeyType; -import com.aliyun.openservices.ots.model.PrimaryKeyValue; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.JsonSerializationContext; -import com.google.gson.JsonSerializer; +import java.lang.reflect.Type; /** * {"type":"INF_MIN", "value":""} @@ -31,27 +25,29 @@ public JsonElement serialize(PrimaryKeyValue obj, Type t, JsonSerializationContext c) { JsonObject json = new JsonObject(); - if (obj == PrimaryKeyValue.INF_MIN) { + if (obj.isInfMin()) { json.add(TYPE, new JsonPrimitive(INF_MIN)); - json.add(VALUE, new JsonPrimitive("")); return json; } - if (obj == PrimaryKeyValue.INF_MAX) { + if (obj.isInfMax()) { json.add(TYPE, new JsonPrimitive(INF_MAX)); - json.add(VALUE, new JsonPrimitive("")); return json; } switch (obj.getType()) { case STRING : - json.add(TYPE, new JsonPrimitive(ColumnType.STRING.toString())); + json.add(TYPE, new JsonPrimitive(ColumnType.STRING.toString())); json.add(VALUE, new JsonPrimitive(obj.asString())); break; case INTEGER : json.add(TYPE, new JsonPrimitive(ColumnType.INTEGER.toString())); json.add(VALUE, new JsonPrimitive(obj.asLong())); break; + case BINARY : + json.add(TYPE, new JsonPrimitive(ColumnType.BINARY.toString())); + json.add(VALUE, new JsonPrimitive(Base64.encodeBase64String(obj.asBinary()))); + break; default: throw new IllegalArgumentException("Unsupport serialize the type : " + obj.getType() + ""); } @@ -64,16 +60,17 @@ public PrimaryKeyValue deserialize(JsonElement ele, Type t, JsonObject obj = ele.getAsJsonObject(); String strType = obj.getAsJsonPrimitive(TYPE).getAsString(); - JsonPrimitive jsonValue = obj.getAsJsonPrimitive(VALUE); - if (strType.equals(INF_MIN)) { + if (strType.equalsIgnoreCase(INF_MIN)) { return PrimaryKeyValue.INF_MIN; } - if (strType.equals(INF_MAX)) { + if (strType.equalsIgnoreCase(INF_MAX)) { return PrimaryKeyValue.INF_MAX; } + JsonPrimitive jsonValue = obj.getAsJsonPrimitive(VALUE); + PrimaryKeyValue value = null; PrimaryKeyType type = PrimaryKeyType.valueOf(strType); switch(type) { @@ -83,6 +80,9 @@ public PrimaryKeyValue deserialize(JsonElement ele, Type t, case INTEGER : value = PrimaryKeyValue.fromLong(jsonValue.getAsLong()); break; + case BINARY : + value = PrimaryKeyValue.fromBinary(Base64.decodeBase64(jsonValue.getAsString())); + break; default: throw new IllegalArgumentException("Unsupport deserialize the type : " + type + ""); } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetFirstRowPrimaryKeyCallable.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetFirstRowPrimaryKeyCallable.java index f004c0ff6f..cdcae91ad1 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetFirstRowPrimaryKeyCallable.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetFirstRowPrimaryKeyCallable.java @@ -1,53 +1,42 @@ package com.alibaba.datax.plugin.reader.otsreader.callable; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.model.*; + +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; -import com.aliyun.openservices.ots.OTSClient; -import com.aliyun.openservices.ots.model.ColumnType; -import com.aliyun.openservices.ots.model.ColumnValue; -import com.aliyun.openservices.ots.model.GetRangeRequest; -import com.aliyun.openservices.ots.model.GetRangeResult; -import com.aliyun.openservices.ots.model.PrimaryKeyType; -import com.aliyun.openservices.ots.model.PrimaryKeyValue; -import com.aliyun.openservices.ots.model.RangeRowQueryCriteria; -import com.aliyun.openservices.ots.model.Row; -import com.aliyun.openservices.ots.model.RowPrimaryKey; -import com.aliyun.openservices.ots.model.TableMeta; - -public class GetFirstRowPrimaryKeyCallable implements Callable{ - - private OTSClient ots = null; +public class GetFirstRowPrimaryKeyCallable implements Callable> { + + private SyncClientInterface ots = null; private TableMeta meta = null; private RangeRowQueryCriteria criteria = null; - - public GetFirstRowPrimaryKeyCallable(OTSClient ots, TableMeta meta, RangeRowQueryCriteria criteria) { + + public GetFirstRowPrimaryKeyCallable(SyncClientInterface ots, TableMeta meta, RangeRowQueryCriteria criteria) { this.ots = ots; this.meta = meta; this.criteria = criteria; } - + @Override - public RowPrimaryKey call() throws Exception { - RowPrimaryKey ret = new RowPrimaryKey(); + public List call() throws Exception { + List ret = new ArrayList<>(); GetRangeRequest request = new GetRangeRequest(); request.setRangeRowQueryCriteria(criteria); - GetRangeResult result = ots.getRange(request); - List rows = result.getRows(); - if(rows.isEmpty()) { + GetRangeResponse response = ots.getRange(request); + List rows = response.getRows(); + if (rows.isEmpty()) { return null;// no data - } + } Row row = rows.get(0); - Map pk = meta.getPrimaryKey(); - for (String key:pk.keySet()) { - ColumnValue v = row.getColumns().get(key); - if (v.getType() == ColumnType.INTEGER) { - ret.addPrimaryKeyColumn(key, PrimaryKeyValue.fromLong(v.asLong())); - } else { - ret.addPrimaryKeyColumn(key, PrimaryKeyValue.fromString(v.asString())); - } + Map pk = meta.getPrimaryKeyMap(); + + for (String key : pk.keySet()) { + PrimaryKeyColumn v = row.getPrimaryKey().getPrimaryKeyColumnsMap().get(key); + ret.add(v); } return ret; } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetRangeCallable.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetRangeCallable.java index 2cd1398a66..995d491cce 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetRangeCallable.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetRangeCallable.java @@ -1,35 +1,26 @@ package com.alibaba.datax.plugin.reader.otsreader.callable; -import java.util.concurrent.Callable; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.model.GetRangeRequest; +import com.alicloud.openservices.tablestore.model.GetRangeResponse; +import com.alicloud.openservices.tablestore.model.RangeRowQueryCriteria; -import com.aliyun.openservices.ots.OTSClientAsync; -import com.aliyun.openservices.ots.model.GetRangeRequest; -import com.aliyun.openservices.ots.model.GetRangeResult; -import com.aliyun.openservices.ots.model.OTSFuture; -import com.aliyun.openservices.ots.model.RangeRowQueryCriteria; +import java.util.concurrent.Callable; -public class GetRangeCallable implements Callable { +public class GetRangeCallable implements Callable { - private OTSClientAsync ots; + private SyncClientInterface ots; private RangeRowQueryCriteria criteria; - private OTSFuture future; - public GetRangeCallable(OTSClientAsync ots, RangeRowQueryCriteria criteria, OTSFuture future) { + public GetRangeCallable(SyncClientInterface ots, RangeRowQueryCriteria criteria) { this.ots = ots; this.criteria = criteria; - this.future = future; } @Override - public GetRangeResult call() throws Exception { - try { - return future.get(); - } catch (Exception e) { - GetRangeRequest request = new GetRangeRequest(); - request.setRangeRowQueryCriteria(criteria); - future = ots.getRange(request); - throw e; - } + public GetRangeResponse call() throws Exception { + GetRangeRequest request = new GetRangeRequest(); + request.setRangeRowQueryCriteria(criteria); + return ots.getRange(request); } - -} +} \ No newline at end of file diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetRangeCallableOld.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetRangeCallableOld.java new file mode 100644 index 0000000000..c0434126e4 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetRangeCallableOld.java @@ -0,0 +1,35 @@ +package com.alibaba.datax.plugin.reader.otsreader.callable; + +import java.util.concurrent.Callable; + +import com.aliyun.openservices.ots.OTSClientAsync; +import com.aliyun.openservices.ots.model.GetRangeRequest; +import com.aliyun.openservices.ots.model.GetRangeResult; +import com.aliyun.openservices.ots.model.OTSFuture; +import com.aliyun.openservices.ots.model.RangeRowQueryCriteria; + +public class GetRangeCallableOld implements Callable { + + private OTSClientAsync ots; + private RangeRowQueryCriteria criteria; + private OTSFuture future; + + public GetRangeCallableOld(OTSClientAsync ots, RangeRowQueryCriteria criteria, OTSFuture future) { + this.ots = ots; + this.criteria = criteria; + this.future = future; + } + + @Override + public GetRangeResult call() throws Exception { + try { + return future.get(); + } catch (Exception e) { + GetRangeRequest request = new GetRangeRequest(); + request.setRangeRowQueryCriteria(criteria); + future = ots.getRange(request); + throw e; + } + } + +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetTableMetaCallable.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetTableMetaCallable.java index 2884e12b14..36a122c286 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetTableMetaCallable.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetTableMetaCallable.java @@ -1,18 +1,19 @@ package com.alibaba.datax.plugin.reader.otsreader.callable; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.model.DescribeTableRequest; +import com.alicloud.openservices.tablestore.model.DescribeTableResponse; +import com.alicloud.openservices.tablestore.model.TableMeta; + import java.util.concurrent.Callable; -import com.aliyun.openservices.ots.OTSClient; -import com.aliyun.openservices.ots.model.DescribeTableRequest; -import com.aliyun.openservices.ots.model.DescribeTableResult; -import com.aliyun.openservices.ots.model.TableMeta; public class GetTableMetaCallable implements Callable{ - private OTSClient ots = null; + private SyncClientInterface ots = null; private String tableName = null; - public GetTableMetaCallable(OTSClient ots, String tableName) { + public GetTableMetaCallable(SyncClientInterface ots, String tableName) { this.ots = ots; this.tableName = tableName; } @@ -21,9 +22,9 @@ public GetTableMetaCallable(OTSClient ots, String tableName) { public TableMeta call() throws Exception { DescribeTableRequest describeTableRequest = new DescribeTableRequest(); describeTableRequest.setTableName(tableName); - DescribeTableResult result = ots.describeTable(describeTableRequest); + DescribeTableResponse result = ots.describeTable(describeTableRequest); TableMeta tableMeta = result.getTableMeta(); return tableMeta; } -} +} \ No newline at end of file diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetTimeseriesSplitCallable.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetTimeseriesSplitCallable.java new file mode 100644 index 0000000000..96521c4129 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/GetTimeseriesSplitCallable.java @@ -0,0 +1,38 @@ +package com.alibaba.datax.plugin.reader.otsreader.callable; + +import com.alicloud.openservices.tablestore.SyncClient; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.TimeseriesClient; +import com.alicloud.openservices.tablestore.model.timeseries.SplitTimeseriesScanTaskRequest; +import com.alicloud.openservices.tablestore.model.timeseries.SplitTimeseriesScanTaskResponse; +import com.alicloud.openservices.tablestore.model.timeseries.TimeseriesScanSplitInfo; + +import java.util.List; +import java.util.concurrent.Callable; + +public class GetTimeseriesSplitCallable implements Callable> { + + private TimeseriesClient client = null; + private String timeseriesTableName = null; + private String measurementName = null; + private int splitCountHint = 1; + + + public GetTimeseriesSplitCallable(SyncClientInterface ots, String timeseriesTableName, String measurementName, int splitCountHint) { + this.client = ((SyncClient) ots).asTimeseriesClient(); + this.timeseriesTableName = timeseriesTableName; + this.measurementName = measurementName; + this.splitCountHint = splitCountHint; + } + + @Override + public List call() throws Exception { + SplitTimeseriesScanTaskRequest request = new SplitTimeseriesScanTaskRequest(timeseriesTableName, splitCountHint); + if (measurementName.length() != 0) { + request.setMeasurementName(measurementName); + } + + SplitTimeseriesScanTaskResponse response = client.splitTimeseriesScanTask(request); + return response.getSplitInfos(); + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/ScanTimeseriesDataCallable.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/ScanTimeseriesDataCallable.java new file mode 100644 index 0000000000..726d0e5d35 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/callable/ScanTimeseriesDataCallable.java @@ -0,0 +1,27 @@ +package com.alibaba.datax.plugin.reader.otsreader.callable; + +import com.alicloud.openservices.tablestore.SyncClient; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.TimeseriesClient; +import com.alicloud.openservices.tablestore.model.timeseries.ScanTimeseriesDataRequest; +import com.alicloud.openservices.tablestore.model.timeseries.ScanTimeseriesDataResponse; +import com.alicloud.openservices.tablestore.model.timeseries.TimeseriesScanSplitInfo; + +import java.util.List; +import java.util.concurrent.Callable; + +public class ScanTimeseriesDataCallable implements Callable { + + private TimeseriesClient client = null; + private ScanTimeseriesDataRequest request = null; + + public ScanTimeseriesDataCallable(SyncClientInterface ots, ScanTimeseriesDataRequest scanTimeseriesDataRequest){ + this.client = ((SyncClient) ots).asTimeseriesClient(); + this.request = scanTimeseriesDataRequest; + } + + @Override + public ScanTimeseriesDataResponse call() throws Exception { + return client.scanTimeseriesData(request); + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/DefaultNoRetry.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/DefaultNoRetry.java new file mode 100644 index 0000000000..b286472d5f --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/DefaultNoRetry.java @@ -0,0 +1,32 @@ +package com.alibaba.datax.plugin.reader.otsreader.model; + + +import com.alicloud.openservices.tablestore.model.DefaultRetryStrategy; +import com.alicloud.openservices.tablestore.model.RetryStrategy; + +public class DefaultNoRetry extends DefaultRetryStrategy { + + public DefaultNoRetry() { + super(); + } + + @Override + public RetryStrategy clone() { + return super.clone(); + } + + @Override + public int getRetries() { + return super.getRetries(); + } + + @Override + public boolean shouldRetry(String action, Exception ex) { + return false; + } + + @Override + public long nextPause(String action, Exception ex) { + return super.nextPause(action, ex); + } +} \ No newline at end of file diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSColumn.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSColumn.java index 129ccd2fd2..809f4c3888 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSColumn.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSColumn.java @@ -1,19 +1,18 @@ package com.alibaba.datax.plugin.reader.otsreader.model; -import com.alibaba.datax.common.element.BoolColumn; -import com.alibaba.datax.common.element.BytesColumn; -import com.alibaba.datax.common.element.Column; -import com.alibaba.datax.common.element.DoubleColumn; -import com.alibaba.datax.common.element.LongColumn; -import com.alibaba.datax.common.element.StringColumn; -import com.aliyun.openservices.ots.model.ColumnType; +import com.alibaba.datax.common.element.*; +import com.alicloud.openservices.tablestore.model.ColumnType; public class OTSColumn { private String name; private Column value; + private OTSColumnType columnType; + + // 时序数据column配置 private ColumnType valueType; - + private Boolean isTimeseriesTag; + public static enum OTSColumnType { NORMAL, // 普通列 CONST // 常量列 @@ -24,10 +23,9 @@ private OTSColumn(String name) { this.columnType = OTSColumnType.NORMAL; } - private OTSColumn(Column value, ColumnType type) { + private OTSColumn(Column value) { this.value = value; this.columnType = OTSColumnType.CONST; - this.valueType = type; } public static OTSColumn fromNormalColumn(String name) { @@ -39,23 +37,23 @@ public static OTSColumn fromNormalColumn(String name) { } public static OTSColumn fromConstStringColumn(String value) { - return new OTSColumn(new StringColumn(value), ColumnType.STRING); + return new OTSColumn(new StringColumn(value)); } public static OTSColumn fromConstIntegerColumn(long value) { - return new OTSColumn(new LongColumn(value), ColumnType.INTEGER); + return new OTSColumn(new LongColumn(value)); } public static OTSColumn fromConstDoubleColumn(double value) { - return new OTSColumn(new DoubleColumn(value), ColumnType.DOUBLE); + return new OTSColumn(new DoubleColumn(value)); } public static OTSColumn fromConstBoolColumn(boolean value) { - return new OTSColumn(new BoolColumn(value), ColumnType.BOOLEAN); + return new OTSColumn(new BoolColumn(value)); } public static OTSColumn fromConstBytesColumn(byte[] value) { - return new OTSColumn(new BytesColumn(value), ColumnType.BINARY); + return new OTSColumn(new BytesColumn(value)); } public Column getValue() { @@ -65,12 +63,25 @@ public Column getValue() { public OTSColumnType getColumnType() { return columnType; } - + + + public String getName() { + return name; + } + public ColumnType getValueType() { return valueType; } - public String getName() { - return name; + public void setValueType(ColumnType valueType) { + this.valueType = valueType; + } + + public Boolean getTimeseriesTag() { + return isTimeseriesTag; + } + + public void setTimeseriesTag(Boolean timeseriesTag) { + isTimeseriesTag = timeseriesTag; } -} +} \ No newline at end of file diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSConf.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSConf.java index 8b109a39e9..cbfd8f6a38 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSConf.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSConf.java @@ -1,90 +1,245 @@ package com.alibaba.datax.plugin.reader.otsreader.model; -import java.util.List; +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.otsreader.utils.Constant; +import com.alibaba.datax.plugin.reader.otsreader.utils.Key; +import com.alibaba.datax.plugin.reader.otsreader.utils.ParamChecker; +import com.alicloud.openservices.tablestore.model.ColumnType; -import com.aliyun.openservices.ots.model.PrimaryKeyValue; +import java.util.List; public class OTSConf { - private String endpoint= null; + private String endpoint = null; private String accessId = null; - private String accesskey = null; + private String accessKey = null; private String instanceName = null; private String tableName = null; + private OTSRange range = null; + private List column = null; + private OTSMode mode = null; + + @Deprecated + private String metaMode = ""; + + private boolean newVersion = false; + /** + * 以下配置仅用于timeseries数据读取 + */ + private boolean isTimeseriesTable = false; + private String measurementName = null; + /** + * 以上配置仅用于timeseries数据读取 + */ + private OTSMultiVersionConf multi = null; - private List rangeBegin = null; - private List rangeEnd = null; - private List rangeSplit = null; - - private List columns = null; - - private int retry; - private int sleepInMilliSecond; - + private int retry = Constant.ConfigDefaultValue.RETRY; + private int retryPauseInMillisecond = Constant.ConfigDefaultValue.RETRY_PAUSE_IN_MILLISECOND; + private int ioThreadCount = Constant.ConfigDefaultValue.IO_THREAD_COUNT; + private int maxConnectionCount = Constant.ConfigDefaultValue.MAX_CONNECTION_COUNT; + private int socketTimeoutInMillisecond = Constant.ConfigDefaultValue.SOCKET_TIMEOUT_IN_MILLISECOND; + private int connectTimeoutInMillisecond = Constant.ConfigDefaultValue.CONNECT_TIMEOUT_IN_MILLISECOND; + + public int getIoThreadCount() { + return ioThreadCount; + } + + public void setIoThreadCount(int ioThreadCount) { + this.ioThreadCount = ioThreadCount; + } + + public int getMaxConnectCount() { + return maxConnectionCount; + } + + public void setMaxConnectCount(int maxConnectCount) { + this.maxConnectionCount = maxConnectCount; + } + + public int getSocketTimeoutInMillisecond() { + return socketTimeoutInMillisecond; + } + + public void setSocketTimeoutInMillisecond(int socketTimeoutInMillisecond) { + this.socketTimeoutInMillisecond = socketTimeoutInMillisecond; + } + + public int getConnectTimeoutInMillisecond() { + return connectTimeoutInMillisecond; + } + + public void setConnectTimeoutInMillisecond(int connectTimeoutInMillisecond) { + this.connectTimeoutInMillisecond = connectTimeoutInMillisecond; + } + + public int getRetry() { + return retry; + } + + public void setRetry(int retry) { + this.retry = retry; + } + + public int getRetryPauseInMillisecond() { + return retryPauseInMillisecond; + } + + public void setRetryPauseInMillisecond(int sleepInMillisecond) { + this.retryPauseInMillisecond = sleepInMillisecond; + } + public String getEndpoint() { return endpoint; } + public void setEndpoint(String endpoint) { this.endpoint = endpoint; } + public String getAccessId() { return accessId; } + public void setAccessId(String accessId) { this.accessId = accessId; } - public String getAccesskey() { - return accesskey; + + public String getAccessKey() { + return accessKey; } - public void setAccesskey(String accesskey) { - this.accesskey = accesskey; + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; } + public String getInstanceName() { return instanceName; } + public void setInstanceName(String instanceName) { this.instanceName = instanceName; } + public String getTableName() { return tableName; } + public void setTableName(String tableName) { this.tableName = tableName; } - public List getColumns() { - return columns; + public OTSRange getRange() { + return range; } - public void setColumns(List columns) { - this.columns = columns; + + public void setRange(OTSRange range) { + this.range = range; } - public int getRetry() { - return retry; + + public OTSMode getMode() { + return mode; } - public void setRetry(int retry) { - this.retry = retry; + + public void setMode(OTSMode mode) { + this.mode = mode; } - public int getSleepInMilliSecond() { - return sleepInMilliSecond; + + public OTSMultiVersionConf getMulti() { + return multi; } - public void setSleepInMilliSecond(int sleepInMilliSecond) { - this.sleepInMilliSecond = sleepInMilliSecond; + + public void setMulti(OTSMultiVersionConf multi) { + this.multi = multi; } - public List getRangeBegin() { - return rangeBegin; + + public List getColumn() { + return column; } - public void setRangeBegin(List rangeBegin) { - this.rangeBegin = rangeBegin; + + public void setColumn(List column) { + this.column = column; } - public List getRangeEnd() { - return rangeEnd; + + public boolean isNewVersion() { + return newVersion; } - public void setRangeEnd(List rangeEnd) { - this.rangeEnd = rangeEnd; + + public void setNewVersion(boolean newVersion) { + this.newVersion = newVersion; + } + + @Deprecated + public String getMetaMode() { + return metaMode; + } + + @Deprecated + public void setMetaMode(String metaMode) { + this.metaMode = metaMode; + } + + public boolean isTimeseriesTable() { + return isTimeseriesTable; + } + + public void setTimeseriesTable(boolean timeseriesTable) { + isTimeseriesTable = timeseriesTable; } - public List getRangeSplit() { - return rangeSplit; + + public String getMeasurementName() { + return measurementName; + } + + public void setMeasurementName(String measurementName) { + this.measurementName = measurementName; } - public void setRangeSplit(List rangeSplit) { - this.rangeSplit = rangeSplit; + + public static OTSConf load(Configuration param) throws OTSCriticalException { + OTSConf c = new OTSConf(); + + // account + c.setEndpoint(ParamChecker.checkStringAndGet(param, Key.OTS_ENDPOINT, true)); + c.setAccessId(ParamChecker.checkStringAndGet(param, Key.OTS_ACCESSID, true)); + c.setAccessKey(ParamChecker.checkStringAndGet(param, Key.OTS_ACCESSKEY, true)); + c.setInstanceName(ParamChecker.checkStringAndGet(param, Key.OTS_INSTANCE_NAME, true)); + c.setTableName(ParamChecker.checkStringAndGet(param, Key.TABLE_NAME, true)); + + c.setRetry(param.getInt(Constant.ConfigKey.RETRY, Constant.ConfigDefaultValue.RETRY)); + c.setRetryPauseInMillisecond(param.getInt(Constant.ConfigKey.RETRY_PAUSE_IN_MILLISECOND, Constant.ConfigDefaultValue.RETRY_PAUSE_IN_MILLISECOND)); + c.setIoThreadCount(param.getInt(Constant.ConfigKey.IO_THREAD_COUNT, Constant.ConfigDefaultValue.IO_THREAD_COUNT)); + c.setMaxConnectCount(param.getInt(Constant.ConfigKey.MAX_CONNECTION_COUNT, Constant.ConfigDefaultValue.MAX_CONNECTION_COUNT)); + c.setSocketTimeoutInMillisecond(param.getInt(Constant.ConfigKey.SOCKET_TIMEOUTIN_MILLISECOND, Constant.ConfigDefaultValue.SOCKET_TIMEOUT_IN_MILLISECOND)); + c.setConnectTimeoutInMillisecond(param.getInt(Constant.ConfigKey.CONNECT_TIMEOUT_IN_MILLISECOND, Constant.ConfigDefaultValue.CONNECT_TIMEOUT_IN_MILLISECOND)); + + // range + c.setRange(ParamChecker.checkRangeAndGet(param)); + + // mode 可选参数 + c.setMode(ParamChecker.checkModeAndGet(param)); + //isNewVersion 可选参数 + c.setNewVersion(param.getBool(Key.NEW_VERSION, false)); + // metaMode 旧版本配置 + c.setMetaMode(param.getString(Key.META_MODE, "")); + + + + // 读时序表配置项 + c.setTimeseriesTable(param.getBool(Key.IS_TIMESERIES_TABLE, false)); + // column + if(!c.isTimeseriesTable()){ + //非时序表 + c.setColumn(ParamChecker.checkOTSColumnAndGet(param, c.getMode())); + } + else{ + // 时序表 + c.setMeasurementName(param.getString(Key.MEASUREMENT_NAME, "")); + c.setColumn(ParamChecker.checkTimeseriesColumnAndGet(param)); + ParamChecker.checkTimeseriesMode(c.getMode(), c.isNewVersion()); + } + + if (c.getMode() == OTSMode.MULTI_VERSION) { + c.setMulti(OTSMultiVersionConf.load(param)); + } + return c; } } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSCriticalException.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSCriticalException.java new file mode 100644 index 0000000000..f02346bcba --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSCriticalException.java @@ -0,0 +1,24 @@ +package com.alibaba.datax.plugin.reader.otsreader.model; + +/** + * 插件错误异常,该异常主要用于描述插件的异常退出 + * @author redchen + */ +public class OTSCriticalException extends Exception{ + + private static final long serialVersionUID = 5820460098894295722L; + + public OTSCriticalException() {} + + public OTSCriticalException(String message) { + super(message); + } + + public OTSCriticalException(Throwable a) { + super(a); + } + + public OTSCriticalException(String message, Throwable a) { + super(message, a); + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSErrorCode.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSErrorCode.java new file mode 100644 index 0000000000..0c537fcec2 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSErrorCode.java @@ -0,0 +1,115 @@ +/** + * Copyright (C) Alibaba Cloud Computing + * All rights reserved. + * + * 版权所有 (C)阿里云计算有限公司 + */ + +package com.alibaba.datax.plugin.reader.otsreader.model; + +/** + * 表示来自开放结构化数据服务(Open Table Service,OTS)的错误代码。 + * + */ +public class OTSErrorCode { + /** + * 用户身份验证失败。 + */ + public static final String AUTHORIZATION_FAILURE = "OTSAuthFailed"; + + /** + * 服务器内部错误。 + */ + public static final String INTERNAL_SERVER_ERROR = "OTSInternalServerError"; + + /** + * 参数错误。 + */ + public static final String INVALID_PARAMETER = "OTSParameterInvalid"; + + /** + * 整个请求过大。 + */ + public static final String REQUEST_TOO_LARGE = "OTSRequestBodyTooLarge"; + + /** + * 客户端请求超时。 + */ + public static final String REQUEST_TIMEOUT = "OTSRequestTimeout"; + + /** + * 用户的配额已经用满。 + */ + public static final String QUOTA_EXHAUSTED = "OTSQuotaExhausted"; + + /** + * 内部服务器发生failover,导致表的部分分区不可服务。 + */ + public static final String PARTITION_UNAVAILABLE = "OTSPartitionUnavailable"; + + /** + * 表刚被创建还无法立马提供服务。 + */ + public static final String TABLE_NOT_READY = "OTSTableNotReady"; + + /** + * 请求的表不存在。 + */ + public static final String OBJECT_NOT_EXIST = "OTSObjectNotExist"; + + /** + * 请求创建的表已经存在。 + */ + public static final String OBJECT_ALREADY_EXIST = "OTSObjectAlreadyExist"; + + /** + * 多个并发的请求写同一行数据,导致冲突。 + */ + public static final String ROW_OPEARTION_CONFLICT = "OTSRowOperationConflict"; + + /** + * 主键不匹配。 + */ + public static final String INVALID_PK = "OTSInvalidPK"; + + /** + * 读写能力调整过于频繁。 + */ + public static final String TOO_FREQUENT_RESERVED_THROUGHPUT_ADJUSTMENT = "OTSTooFrequentReservedThroughputAdjustment"; + + /** + * 该行总列数超出限制。 + */ + public static final String OUT_OF_COLUMN_COUNT_LIMIT = "OTSOutOfColumnCountLimit"; + + /** + * 该行所有列数据大小总和超出限制。 + */ + public static final String OUT_OF_ROW_SIZE_LIMIT = "OTSOutOfRowSizeLimit"; + + /** + * 剩余预留读写能力不足。 + */ + public static final String NOT_ENOUGH_CAPACITY_UNIT = "OTSNotEnoughCapacityUnit"; + + /** + * 预查条件检查失败。 + */ + public static final String CONDITION_CHECK_FAIL = "OTSConditionCheckFail"; + + /** + * 在OTS内部操作超时。 + */ + public static final String STORAGE_TIMEOUT = "OTSTimeout"; + + /** + * 在OTS内部有服务器不可访问。 + */ + public static final String SERVER_UNAVAILABLE = "OTSServerUnavailable"; + + /** + * OTS内部服务器繁忙。 + */ + public static final String SERVER_BUSY = "OTSServerBusy"; + +} \ No newline at end of file diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSMode.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSMode.java new file mode 100644 index 0000000000..88c6ee67e9 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSMode.java @@ -0,0 +1,6 @@ +package com.alibaba.datax.plugin.reader.otsreader.model; + +public enum OTSMode { + NORMAL, + MULTI_VERSION +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSMultiVersionConf.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSMultiVersionConf.java new file mode 100644 index 0000000000..72a8e1b734 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSMultiVersionConf.java @@ -0,0 +1,35 @@ +package com.alibaba.datax.plugin.reader.otsreader.model; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.reader.otsreader.utils.Constant; +import com.alibaba.datax.plugin.reader.otsreader.utils.ParamChecker; +import com.alicloud.openservices.tablestore.model.TimeRange; + +public class OTSMultiVersionConf { + + private TimeRange timeRange = null; + private int maxVersion = -1; + + public TimeRange getTimeRange() { + return timeRange; + } + + public void setTimeRange(TimeRange timeRange) { + this.timeRange = timeRange; + } + + public int getMaxVersion() { + return maxVersion; + } + + public void setMaxVersion(int maxVersion) { + this.maxVersion = maxVersion; + } + + public static OTSMultiVersionConf load(Configuration param) throws OTSCriticalException { + OTSMultiVersionConf conf = new OTSMultiVersionConf(); + conf.setTimeRange(ParamChecker.checkTimeRangeAndGet(param)); + conf.setMaxVersion(param.getInt(Constant.ConfigKey.MAX_VERSION, Constant.ConfigDefaultValue.MAX_VERSION)); + return conf; + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSPrimaryKeyColumn.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSPrimaryKeyColumn.java index eaec50ce5b..44a37c0cd6 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSPrimaryKeyColumn.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSPrimaryKeyColumn.java @@ -15,8 +15,41 @@ public void setName(String name) { public PrimaryKeyType getType() { return type; } + + public com.alicloud.openservices.tablestore.model.PrimaryKeyType getType(Boolean newVersion) { + com.alicloud.openservices.tablestore.model.PrimaryKeyType res = null; + switch (this.type){ + case BINARY: + res = com.alicloud.openservices.tablestore.model.PrimaryKeyType.BINARY; + break; + case INTEGER: + res = com.alicloud.openservices.tablestore.model.PrimaryKeyType.INTEGER; + break; + case STRING: + default: + res = com.alicloud.openservices.tablestore.model.PrimaryKeyType.STRING; + break; + } + return res; + } + public void setType(PrimaryKeyType type) { this.type = type; } + + public void setType(com.alicloud.openservices.tablestore.model.PrimaryKeyType type) { + switch (type){ + case BINARY: + this.type = PrimaryKeyType.BINARY; + break; + case INTEGER: + this.type = PrimaryKeyType.INTEGER; + break; + case STRING: + default: + this.type = PrimaryKeyType.STRING; + break; + } + } } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSRange.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSRange.java index 8ebfcf7ea3..eb3095e653 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSRange.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/model/OTSRange.java @@ -1,29 +1,31 @@ package com.alibaba.datax.plugin.reader.otsreader.model; -import com.aliyun.openservices.ots.model.RowPrimaryKey; +import com.alicloud.openservices.tablestore.model.PrimaryKeyColumn; + +import java.util.List; + public class OTSRange { + private List begin = null; + private List end = null; + private List split = null; - private RowPrimaryKey begin = null; - private RowPrimaryKey end = null; - - public OTSRange() {} - - public OTSRange(RowPrimaryKey begin, RowPrimaryKey end) { - this.begin = begin; - this.end = end; - } - - public RowPrimaryKey getBegin() { + public List getBegin() { return begin; } - public void setBegin(RowPrimaryKey begin) { + public void setBegin(List begin) { this.begin = begin; } - public RowPrimaryKey getEnd() { + public List getEnd() { return end; } - public void setEnd(RowPrimaryKey end) { + public void setEnd(List end) { this.end = end; } + public List getSplit() { + return split; + } + public void setSplit(List split) { + this.split = split; + } } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/Common.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/Common.java index 7bb3f52ea1..90065d5d0f 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/Common.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/Common.java @@ -1,89 +1,77 @@ package com.alibaba.datax.plugin.reader.otsreader.utils; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSColumn; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSCriticalException; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSPrimaryKeyColumn; +import com.alicloud.openservices.tablestore.model.*; +import com.alicloud.openservices.tablestore.model.timeseries.ScanTimeseriesDataResponse; + +import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Map; -import com.alibaba.datax.common.element.BoolColumn; -import com.alibaba.datax.common.element.BytesColumn; -import com.alibaba.datax.common.element.DoubleColumn; -import com.alibaba.datax.common.element.LongColumn; -import com.alibaba.datax.common.element.Record; -import com.alibaba.datax.common.element.StringColumn; -import com.alibaba.datax.plugin.reader.otsreader.model.OTSColumn; -import com.alibaba.datax.plugin.reader.otsreader.model.OTSPrimaryKeyColumn; -import com.aliyun.openservices.ots.ClientException; -import com.aliyun.openservices.ots.OTSException; -import com.aliyun.openservices.ots.model.ColumnValue; -import com.aliyun.openservices.ots.model.PrimaryKeyValue; -import com.aliyun.openservices.ots.model.Row; -import com.aliyun.openservices.ots.model.RowPrimaryKey; -import com.aliyun.openservices.ots.model.TableMeta; - public class Common { - - public static int primaryKeyValueCmp(PrimaryKeyValue v1, PrimaryKeyValue v2) { - if (v1.getType() != null && v2.getType() != null) { - if (v1.getType() != v2.getType()) { - throw new IllegalArgumentException( - "Not same column type, column1:" + v1.getType() + ", column2:" + v2.getType()); - } - switch (v1.getType()) { - case INTEGER: - Long l1 = Long.valueOf(v1.asLong()); - Long l2 = Long.valueOf(v2.asLong()); - return l1.compareTo(l2); - case STRING: - return v1.asString().compareTo(v2.asString()); - default: - throw new IllegalArgumentException("Unsuporrt compare the type: " + v1.getType() + "."); - } - } else { - if (v1 == v2) { - return 0; - } else { - if (v1 == PrimaryKeyValue.INF_MIN) { - return -1; - } else if (v1 == PrimaryKeyValue.INF_MAX) { - return 1; - } - - if (v2 == PrimaryKeyValue.INF_MAX) { - return -1; - } else if (v2 == PrimaryKeyValue.INF_MIN) { - return 1; - } + public static List toColumnToGet(List columns, TableMeta meta) { + Map pk = meta.getPrimaryKeyMap(); + List names = new ArrayList(); + for (OTSColumn c : columns) { + if (c.getColumnType() == OTSColumn.OTSColumnType.NORMAL && !pk.containsKey(c.getName())) { + names.add(c.getName()); } } - return 0; + return names; } - + + public static List getPrimaryKeyNameList(TableMeta meta) { + List names = new ArrayList(); + names.addAll(meta.getPrimaryKeyMap().keySet()); + return names; + } + public static OTSPrimaryKeyColumn getPartitionKey(TableMeta meta) { List keys = new ArrayList(); - keys.addAll(meta.getPrimaryKey().keySet()); + keys.addAll(meta.getPrimaryKeyMap().keySet()); String key = keys.get(0); OTSPrimaryKeyColumn col = new OTSPrimaryKeyColumn(); col.setName(key); - col.setType(meta.getPrimaryKey().get(key)); + col.setType(meta.getPrimaryKeyMap().get(key)); return col; } - - public static List getPrimaryKeyNameList(TableMeta meta) { - List names = new ArrayList(); - names.addAll(meta.getPrimaryKey().keySet()); - return names; + + public static Direction getDirection(List begin, List end) throws OTSCriticalException { + int cmp = CompareHelper.comparePrimaryKeyColumnList(begin, end); + if (cmp < 0) { + return Direction.FORWARD; + } else if (cmp > 0) { + return Direction.BACKWARD; + } else { + throw new OTSCriticalException("Bug branch, the begin of range equals end of range."); + } } - public static int compareRangeBeginAndEnd(TableMeta meta, RowPrimaryKey begin, RowPrimaryKey end) { - if (begin.getPrimaryKey().size() != end.getPrimaryKey().size()) { - throw new IllegalArgumentException("Input size of begin not equal size of end, begin size : " + begin.getPrimaryKey().size() + - ", end size : " + end.getPrimaryKey().size() + "."); + public static int compareRangeBeginAndEnd(TableMeta meta, List begin, List end) { + if (begin.size() != end.size()) { + throw new IllegalArgumentException("Input size of begin not equal size of end, begin size : " + begin.size() + + ", end size : " + end.size() + "."); + } + + Map beginMap = new HashMap<>(); + Map endMap = new HashMap<>(); + + for(PrimaryKeyColumn primaryKeyColumn : begin){ + beginMap.put(primaryKeyColumn.getName(), primaryKeyColumn.getValue()); } - for (String key : meta.getPrimaryKey().keySet()) { - PrimaryKeyValue v1 = begin.getPrimaryKey().get(key); - PrimaryKeyValue v2 = end.getPrimaryKey().get(key); + for(PrimaryKeyColumn primaryKeyColumn : end){ + endMap.put(primaryKeyColumn.getName(), primaryKeyColumn.getValue()); + } + + for (String key : meta.getPrimaryKeyMap().keySet()) { + PrimaryKeyValue v1 = beginMap.get(key); + PrimaryKeyValue v2 = endMap.get(key); int cmp = primaryKeyValueCmp(v1, v2); if (cmp != 0) { return cmp; @@ -92,70 +80,56 @@ public static int compareRangeBeginAndEnd(TableMeta meta, RowPrimaryKey begin, R return 0; } - public static List getNormalColumnNameList(List columns) { - List normalColumns = new ArrayList(); - for (OTSColumn col : columns) { - if (col.getColumnType() == OTSColumn.OTSColumnType.NORMAL) { - normalColumns.add(col.getName()); + + public static int primaryKeyValueCmp(PrimaryKeyValue v1, PrimaryKeyValue v2) { + if (v1.getType() != null && v2.getType() != null) { + if (v1.getType() != v2.getType()) { + throw new IllegalArgumentException( + "Not same column type, column1:" + v1.getType() + ", column2:" + v2.getType()); } - } - return normalColumns; - } - - public static Record parseRowToLine(Row row, List columns, Record line) { - Map values = row.getColumns(); - for (OTSColumn col : columns) { - if (col.getColumnType() == OTSColumn.OTSColumnType.CONST) { - line.addColumn(col.getValue()); + switch (v1.getType()) { + case INTEGER: + Long l1 = Long.valueOf(v1.asLong()); + Long l2 = Long.valueOf(v2.asLong()); + return l1.compareTo(l2); + case STRING: + return v1.asString().compareTo(v2.asString()); + default: + throw new IllegalArgumentException("Unsuporrt compare the type: " + v1.getType() + "."); + } + } else { + if (v1 == v2) { + return 0; } else { - ColumnValue v = values.get(col.getName()); - if (v == null) { - line.addColumn(new StringColumn(null)); - } else { - switch(v.getType()) { - case STRING: line.addColumn(new StringColumn(v.asString())); break; - case INTEGER: line.addColumn(new LongColumn(v.asLong())); break; - case DOUBLE: line.addColumn(new DoubleColumn(v.asDouble())); break; - case BOOLEAN: line.addColumn(new BoolColumn(v.asBoolean())); break; - case BINARY: line.addColumn(new BytesColumn(v.asBinary())); break; - default: - throw new IllegalArgumentException("Unsuporrt tranform the type: " + col.getValue().getType() + "."); - } + if (v1 == PrimaryKeyValue.INF_MIN) { + return -1; + } else if (v1 == PrimaryKeyValue.INF_MAX) { + return 1; + } + + if (v2 == PrimaryKeyValue.INF_MAX) { + return -1; + } else if (v2 == PrimaryKeyValue.INF_MIN) { + return 1; } } } - return line; - } - - public static String getDetailMessage(Exception exception) { - if (exception instanceof OTSException) { - OTSException e = (OTSException) exception; - return "OTSException[ErrorCode:" + e.getErrorCode() + ", ErrorMessage:" + e.getMessage() + ", RequestId:" + e.getRequestId() + "]"; - } else if (exception instanceof ClientException) { - ClientException e = (ClientException) exception; - return "ClientException[ErrorCode:" + e.getErrorCode() + ", ErrorMessage:" + e.getMessage() + "]"; - } else if (exception instanceof IllegalArgumentException) { - IllegalArgumentException e = (IllegalArgumentException) exception; - return "IllegalArgumentException[ErrorMessage:" + e.getMessage() + "]"; - } else { - return "Exception[ErrorMessage:" + exception.getMessage() + "]"; - } + return 0; } - - public static long getDelaySendMillinSeconds(int hadRetryTimes, int initSleepInMilliSecond) { - - if (hadRetryTimes <= 0) { - return 0; - } - int sleepTime = initSleepInMilliSecond; - for (int i = 1; i < hadRetryTimes; i++) { - sleepTime += sleepTime; - if (sleepTime > 30000) { - sleepTime = 30000; + public static void checkTableStoreSDKVersion() throws OTSCriticalException { + Field[] fields = ScanTimeseriesDataResponse.class.getFields(); + String sdkVersion = null; + for (Field f : fields){ + if (f.getName().equals("_VERSION_")){ + sdkVersion = ScanTimeseriesDataResponse._VERSION_; break; - } + } + } + if (sdkVersion == null){ + throw new OTSCriticalException("Check ots java SDK failed. Please check the version of tableStore maven dependency."); + }else if (Integer.parseInt(sdkVersion) < 20230111){ + throw new OTSCriticalException("Check tableStore java SDK failed. The expected version number is greater than 20230111, actually version : " + sdkVersion + "."); } - return sleepTime; } } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/CommonOld.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/CommonOld.java new file mode 100644 index 0000000000..d5c565f434 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/CommonOld.java @@ -0,0 +1,112 @@ +package com.alibaba.datax.plugin.reader.otsreader.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import com.alibaba.datax.common.element.BoolColumn; +import com.alibaba.datax.common.element.BytesColumn; +import com.alibaba.datax.common.element.DoubleColumn; +import com.alibaba.datax.common.element.LongColumn; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.element.StringColumn; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSColumn; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSPrimaryKeyColumn; +import com.aliyun.openservices.ots.ClientException; +import com.aliyun.openservices.ots.OTSException; +import com.aliyun.openservices.ots.model.ColumnValue; +import com.aliyun.openservices.ots.model.PrimaryKeyValue; +import com.aliyun.openservices.ots.model.Row; +import com.aliyun.openservices.ots.model.RowPrimaryKey; +import com.aliyun.openservices.ots.model.TableMeta; + +public class CommonOld { + public static int primaryKeyValueCmp(PrimaryKeyValue v1, PrimaryKeyValue v2) { + if (v1.getType() != null && v2.getType() != null) { + if (v1.getType() != v2.getType()) { + throw new IllegalArgumentException( + "Not same column type, column1:" + v1.getType() + ", column2:" + v2.getType()); + } + switch (v1.getType()) { + case INTEGER: + Long l1 = Long.valueOf(v1.asLong()); + Long l2 = Long.valueOf(v2.asLong()); + return l1.compareTo(l2); + case STRING: + return v1.asString().compareTo(v2.asString()); + default: + throw new IllegalArgumentException("Unsuporrt compare the type: " + v1.getType() + "."); + } + } else { + if (v1 == v2) { + return 0; + } else { + if (v1 == PrimaryKeyValue.INF_MIN) { + return -1; + } else if (v1 == PrimaryKeyValue.INF_MAX) { + return 1; + } + + if (v2 == PrimaryKeyValue.INF_MAX) { + return -1; + } else if (v2 == PrimaryKeyValue.INF_MIN) { + return 1; + } + } + } + return 0; + } + + + public static List getNormalColumnNameList(List columns) { + List normalColumns = new ArrayList(); + for (OTSColumn col : columns) { + if (col.getColumnType() == OTSColumn.OTSColumnType.NORMAL) { + normalColumns.add(col.getName()); + } + } + return normalColumns; + } + + public static Record parseRowToLine(Row row, List columns, Record line) { + Map values = row.getColumns(); + for (OTSColumn col : columns) { + if (col.getColumnType() == OTSColumn.OTSColumnType.CONST) { + line.addColumn(col.getValue()); + } else { + ColumnValue v = values.get(col.getName()); + if (v == null) { + line.addColumn(new StringColumn(null)); + } else { + switch(v.getType()) { + case STRING: line.addColumn(new StringColumn(v.asString())); break; + case INTEGER: line.addColumn(new LongColumn(v.asLong())); break; + case DOUBLE: line.addColumn(new DoubleColumn(v.asDouble())); break; + case BOOLEAN: line.addColumn(new BoolColumn(v.asBoolean())); break; + case BINARY: line.addColumn(new BytesColumn(v.asBinary())); break; + default: + throw new IllegalArgumentException("Unsuporrt tranform the type: " + col.getValue().getType() + "."); + } + } + } + } + return line; + } + + public static long getDelaySendMillinSeconds(int hadRetryTimes, int initSleepInMilliSecond) { + + if (hadRetryTimes <= 0) { + return 0; + } + + int sleepTime = initSleepInMilliSecond; + for (int i = 1; i < hadRetryTimes; i++) { + sleepTime += sleepTime; + if (sleepTime > 30000) { + sleepTime = 30000; + break; + } + } + return sleepTime; + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/CompareHelper.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/CompareHelper.java new file mode 100644 index 0000000000..19e0642104 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/CompareHelper.java @@ -0,0 +1,37 @@ +package com.alibaba.datax.plugin.reader.otsreader.utils; + +import com.alicloud.openservices.tablestore.model.PrimaryKeyColumn; + +import java.util.List; + + +public class CompareHelper { + /** + * 比较PrimaryKeyColumn List的大小 + * 返回 + * -1 表示before小于after + * 0 表示before等于after + * 1 表示before大于after + * + * @param before + * @param after + * @return + */ + public static int comparePrimaryKeyColumnList(List before, List after) { + int size = before.size() < after.size() ? before.size() : after.size(); + + for (int i = 0; i < size; i++) { + int cmp = before.get(i).compareTo(after.get(i)); + if (cmp != 0) { + return cmp; + } + } + + if (before.size() < after.size() ) { + return -1; + } else if (before.size() > after.size() ) { + return 1; + } + return 0; + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/Constant.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/Constant.java new file mode 100644 index 0000000000..90273bfb9d --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/Constant.java @@ -0,0 +1,92 @@ +package com.alibaba.datax.plugin.reader.otsreader.utils; + +public class Constant { + /** + * Json中的Key名字定义 + */ +public class ConfigKey { + public static final String CONF = "conf"; + public static final String RANGE = "range"; + public static final String META = "meta"; + public static final String SPLIT_INFO = "splitInfo"; + + public static final String TIME_RANGE = "timeRange"; + public static final String MAX_VERSION = "maxVersion"; + + public static final String RETRY = "maxRetryTime"; + public static final String RETRY_PAUSE_IN_MILLISECOND = "retryPauseInMillisecond"; + public static final String IO_THREAD_COUNT = "ioThreadCount"; + public static final String MAX_CONNECTION_COUNT = "maxConnectionCount"; + public static final String SOCKET_TIMEOUTIN_MILLISECOND = "socketTimeoutInMillisecond"; + public static final String CONNECT_TIMEOUT_IN_MILLISECOND = "connectTimeoutInMillisecond"; + + public class Range { + public static final String BEGIN = "begin"; + public static final String END = "end"; + public static final String SPLIT = "split"; + }; + + public class PrimaryKeyColumn { + public static final String TYPE = "type"; + public static final String VALUE = "value"; + }; + + public class TimeseriesPKColumn { + public static final String MEASUREMENT_NAME = "_m_name"; + public static final String DATA_SOURCE = "_data_source"; + public static final String TAGS = "_tags"; + public static final String TIME = "_time"; + } + + public class Column { + public static final String NAME = "name"; + public static final String TYPE = "type"; + public static final String VALUE = "value"; + public static final String IS_TAG = "is_timeseries_tag"; + }; + + public class TimeRange { + public static final String BEGIN = "begin"; + public static final String END = "end"; + } + }; + + /** + * 定义的配置文件中value type中可取的值 + */ + public class ValueType { + public static final String INF_MIN = "INF_MIN"; + public static final String INF_MAX = "INF_MAX"; + public static final String STRING = "string"; + public static final String INTEGER = "int"; + public static final String BINARY = "binary"; + public static final String DOUBLE = "double"; + public static final String BOOLEAN = "bool"; + }; + + /** + * 全局默认常量定义 + */ + public class ConfigDefaultValue { + public static final int RETRY = 18; + public static final int RETRY_PAUSE_IN_MILLISECOND = 100; + public static final int IO_THREAD_COUNT = 1; + public static final int MAX_CONNECTION_COUNT = 1; + public static final int SOCKET_TIMEOUT_IN_MILLISECOND = 10000; + public static final int CONNECT_TIMEOUT_IN_MILLISECOND = 10000; + + public static final int MAX_VERSION = Integer.MAX_VALUE; + + public static final String DEFAULT_NAME = "DEFAULT_NAME"; + + public class Mode { + public static final String NORMAL = "normal"; + public static final String MULTI_VERSION = "multiVersion"; + } + + public class TimeRange { + public static final long MIN = 0; + public static final long MAX = Long.MAX_VALUE; + } + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/GsonParser.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/GsonParser.java index a82f335006..205f536dab 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/GsonParser.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/GsonParser.java @@ -1,23 +1,26 @@ package com.alibaba.datax.plugin.reader.otsreader.utils; -import com.alibaba.datax.plugin.reader.otsreader.adaptor.OTSColumnAdaptor; +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.plugin.reader.otsreader.adaptor.ColumnAdaptor; import com.alibaba.datax.plugin.reader.otsreader.adaptor.PrimaryKeyValueAdaptor; -import com.alibaba.datax.plugin.reader.otsreader.model.OTSColumn; import com.alibaba.datax.plugin.reader.otsreader.model.OTSConf; import com.alibaba.datax.plugin.reader.otsreader.model.OTSRange; +import com.alicloud.openservices.tablestore.model.PrimaryKeyValue; +import com.alicloud.openservices.tablestore.model.TableMeta; +import com.alicloud.openservices.tablestore.model.timeseries.TimeseriesScanSplitInfo; import com.aliyun.openservices.ots.model.Direction; -import com.aliyun.openservices.ots.model.PrimaryKeyValue; import com.aliyun.openservices.ots.model.RowPrimaryKey; -import com.aliyun.openservices.ots.model.TableMeta; import com.google.gson.Gson; import com.google.gson.GsonBuilder; +import java.util.Map; + public class GsonParser { private static Gson gsonBuilder() { return new GsonBuilder() - .registerTypeAdapter(OTSColumn.class, new OTSColumnAdaptor()) .registerTypeAdapter(PrimaryKeyValue.class, new PrimaryKeyValueAdaptor()) + .registerTypeAdapter(Column.class, new ColumnAdaptor()) .create(); } @@ -40,24 +43,39 @@ public static OTSConf jsonToConf (String jsonStr) { Gson g = gsonBuilder(); return g.fromJson(jsonStr, OTSConf.class); } + + public static String metaToJson (TableMeta meta) { + Gson g = gsonBuilder(); + return g.toJson(meta); + } - public static String directionToJson (Direction direction) { + public static TableMeta jsonToMeta (String jsonStr) { Gson g = gsonBuilder(); - return g.toJson(direction); + return g.fromJson(jsonStr, TableMeta.class); } - public static Direction jsonToDirection (String jsonStr) { + public static String timeseriesScanSplitInfoToString(TimeseriesScanSplitInfo timeseriesScanSplitInfo){ Gson g = gsonBuilder(); - return g.fromJson(jsonStr, Direction.class); + return g.toJson(timeseriesScanSplitInfo); } - - public static String metaToJson (TableMeta meta) { + + public static TimeseriesScanSplitInfo stringToTimeseriesScanSplitInfo(String jsonStr){ Gson g = gsonBuilder(); - return g.toJson(meta); + return g.fromJson(jsonStr, TimeseriesScanSplitInfo.class); } - + + public static Direction jsonToDirection (String jsonStr) { + Gson g = gsonBuilder(); + return g.fromJson(jsonStr, Direction.class); + } + public static String rowPrimaryKeyToJson (RowPrimaryKey row) { Gson g = gsonBuilder(); return g.toJson(row); } + + public static String mapToJson (Map map) { + Gson g = gsonBuilder(); + return g.toJson(map); + } } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/Key.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/Key.java similarity index 81% rename from otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/Key.java rename to otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/Key.java index da6d4a5f78..6628e4d3ad 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/Key.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/Key.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.alibaba.datax.plugin.reader.otsreader; +package com.alibaba.datax.plugin.reader.otsreader.utils; public final class Key { /* ots account configuration */ @@ -46,5 +46,13 @@ public final class Key { public final static String RANGE_END = "end"; public final static String RANGE_SPLIT = "split"; + + public final static String META_MODE = "metaMode"; + + public final static String MODE = "mode"; + public final static String NEW_VERSION = "newVersion"; + + public final static String IS_TIMESERIES_TABLE = "isTimeseriesTable"; + public final static String MEASUREMENT_NAME = "measurementName"; } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/OtsHelper.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/OtsHelper.java new file mode 100644 index 0000000000..060507b66b --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/OtsHelper.java @@ -0,0 +1,82 @@ +package com.alibaba.datax.plugin.reader.otsreader.utils; + +import com.alibaba.datax.plugin.reader.otsreader.callable.GetRangeCallable; +import com.alibaba.datax.plugin.reader.otsreader.callable.GetTableMetaCallable; +import com.alibaba.datax.plugin.reader.otsreader.callable.GetTimeseriesSplitCallable; +import com.alibaba.datax.plugin.reader.otsreader.callable.ScanTimeseriesDataCallable; +import com.alibaba.datax.plugin.reader.otsreader.model.DefaultNoRetry; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSConf; +import com.alicloud.openservices.tablestore.ClientConfiguration; +import com.alicloud.openservices.tablestore.SyncClient; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.core.utils.Pair; +import com.alicloud.openservices.tablestore.model.ColumnType; +import com.alicloud.openservices.tablestore.model.GetRangeResponse; +import com.alicloud.openservices.tablestore.model.RangeRowQueryCriteria; +import com.alicloud.openservices.tablestore.model.TableMeta; +import com.alicloud.openservices.tablestore.model.timeseries.ScanTimeseriesDataRequest; +import com.alicloud.openservices.tablestore.model.timeseries.ScanTimeseriesDataResponse; +import com.alicloud.openservices.tablestore.model.timeseries.TimeseriesScanSplitInfo; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class OtsHelper { + + public static SyncClientInterface getOTSInstance(OTSConf conf) { + ClientConfiguration clientConfigure = new ClientConfiguration(); + clientConfigure.setIoThreadCount(conf.getIoThreadCount()); + clientConfigure.setMaxConnections(conf.getMaxConnectCount()); + clientConfigure.setSocketTimeoutInMillisecond(conf.getSocketTimeoutInMillisecond()); + clientConfigure.setConnectionTimeoutInMillisecond(conf.getConnectTimeoutInMillisecond()); + clientConfigure.setRetryStrategy(new DefaultNoRetry()); + + SyncClient ots = new SyncClient( + conf.getEndpoint(), + conf.getAccessId(), + conf.getAccessKey(), + conf.getInstanceName(), + clientConfigure); + + + Map extraHeaders = new HashMap(); + extraHeaders.put("x-ots-sdk-type", "public"); + extraHeaders.put("x-ots-request-source", "datax-otsreader"); + ots.setExtraHeaders(extraHeaders); + + return ots; + } + + public static TableMeta getTableMeta(SyncClientInterface ots, String tableName, int retry, int sleepInMillisecond) throws Exception { + return RetryHelper.executeWithRetry( + new GetTableMetaCallable(ots, tableName), + retry, + sleepInMillisecond + ); + } + + public static GetRangeResponse getRange(SyncClientInterface ots, RangeRowQueryCriteria rangeRowQueryCriteria, int retry, int sleepInMillisecond) throws Exception { + return RetryHelper.executeWithRetry( + new GetRangeCallable(ots, rangeRowQueryCriteria), + retry, + sleepInMillisecond + ); + } + + public static List splitTimeseriesScan(SyncClientInterface ots, String tableName, String measurementName, int splitCountHint, int retry, int sleepInMillisecond) throws Exception { + return RetryHelper.executeWithRetry( + new GetTimeseriesSplitCallable(ots, tableName, measurementName, splitCountHint), + retry, + sleepInMillisecond + ); + } + + public static ScanTimeseriesDataResponse scanTimeseriesData(SyncClientInterface ots, ScanTimeseriesDataRequest scanTimeseriesDataRequest, int retry, int sleepInMillisecond) throws Exception { + return RetryHelper.executeWithRetry( + new ScanTimeseriesDataCallable(ots, scanTimeseriesDataRequest), + retry, + sleepInMillisecond + ); + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderError.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/OtsReaderError.java similarity index 76% rename from otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderError.java rename to otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/OtsReaderError.java index 05a13c1a72..b578dcde30 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/OtsReaderError.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/OtsReaderError.java @@ -1,4 +1,4 @@ -package com.alibaba.datax.plugin.reader.otsreader; +package com.alibaba.datax.plugin.reader.otsreader.utils; import com.alibaba.datax.common.spi.ErrorCode; @@ -14,10 +14,10 @@ public class OtsReaderError implements ErrorCode { public final static OtsReaderError ERROR = new OtsReaderError( "OtsReaderError", - "该错误表示插件的内部错误,表示系统没有处理到的异常"); + "This error represents an internal error of the otsreader plugin, which indicates that the system is not processed."); public final static OtsReaderError INVALID_PARAM = new OtsReaderError( "OtsReaderInvalidParameter", - "该错误表示参数错误,表示用户输入了错误的参数格式等"); + "This error represents a parameter error, indicating that the user entered the wrong parameter format."); public OtsReaderError (String code) { this.code = code; diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ParamChecker.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ParamChecker.java index fbcdc9722e..b2139fc101 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ParamChecker.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ParamChecker.java @@ -1,245 +1,467 @@ package com.alibaba.datax.plugin.reader.otsreader.utils; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - +import com.alibaba.datax.common.element.Column; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.datax.plugin.reader.otsreader.model.OTSPrimaryKeyColumn; -import com.alibaba.datax.plugin.reader.otsreader.model.OTSRange; -import com.aliyun.openservices.ots.model.Direction; -import com.aliyun.openservices.ots.model.PrimaryKeyType; -import com.aliyun.openservices.ots.model.PrimaryKeyValue; -import com.aliyun.openservices.ots.model.RowPrimaryKey; -import com.aliyun.openservices.ots.model.TableMeta; +import com.alibaba.datax.plugin.reader.otsreader.model.*; +import com.alicloud.openservices.tablestore.model.*; + +import java.util.*; public class ParamChecker { - private static void throwNotExistException(String key) { - throw new IllegalArgumentException("The param '" + key + "' is not exist."); + private static void throwNotExistException() { + throw new IllegalArgumentException("missing the key."); } - private static void throwStringLengthZeroException(String key) { - throw new IllegalArgumentException("The param length of '" + key + "' is zero."); + private static void throwStringLengthZeroException() { + throw new IllegalArgumentException("input the key is empty string."); } - private static void throwEmptyException(String key) { - throw new IllegalArgumentException("The param '" + key + "' is empty."); + public static String checkStringAndGet(Configuration param, String key, boolean isTrim) throws OTSCriticalException { + try { + String value = param.getString(key); + if (isTrim) { + value = value != null ? value.trim() : null; + } + if (null == value) { + throwNotExistException(); + } else if (value.length() == 0) { + throwStringLengthZeroException(); + } + return value; + } catch(RuntimeException e) { + throw new OTSCriticalException("Parse '"+ key +"' fail, " + e.getMessage(), e); + } } - private static void throwNotListException(String key) { - throw new IllegalArgumentException("The param '" + key + "' is not a json array."); - } + public static Direction checkDirectionAndEnd(TableMeta meta, List begin, List end) { + Direction direction = null; + int cmp = Common.compareRangeBeginAndEnd(meta, begin, end) ; - private static void throwNotMapException(String key) { - throw new IllegalArgumentException("The param '" + key + "' is not a json map."); + if (cmp > 0) { + direction = Direction.BACKWARD; + } else if (cmp < 0) { + direction = Direction.FORWARD; + } else { + throw new IllegalArgumentException("Value of 'range-begin' equal value of 'range-end'."); + } + return direction; } - public static String checkStringAndGet(Configuration param, String key) { - String value = param.getString(key); - if (null == value) { - throwNotExistException(key); - } else if (value.length() == 0) { - throwStringLengthZeroException(key); + public static List checkInputPrimaryKeyAndGet(TableMeta meta, List range) { + if (meta.getPrimaryKeyMap().size() != range.size()) { + throw new IllegalArgumentException(String.format( + "Input size of values not equal size of primary key. input size:%d, primary key size:%d .", + range.size(), meta.getPrimaryKeyMap().size())); + } + List pk = new ArrayList<>(); + int i = 0; + for (Map.Entry e: meta.getPrimaryKeyMap().entrySet()) { + PrimaryKeyValue value = range.get(i); + if (e.getValue() != value.getType() && value != PrimaryKeyValue.INF_MIN && value != PrimaryKeyValue.INF_MAX) { + throw new IllegalArgumentException( + "Input range type not match primary key. Input type:" + value.getType() + ", Primary Key Type:"+ e.getValue() +", Index:" + i + ); + } else { + pk.add(new PrimaryKeyColumn(e.getKey(), value)); + } + i++; } - return value; + return pk; } - public static List checkListAndGet(Configuration param, String key, boolean isCheckEmpty) { - List value = null; + public static OTSRange checkRangeAndGet(Configuration param) throws OTSCriticalException { try { - value = param.getList(key); - } catch (ClassCastException e) { - throwNotListException(key); - } - if (null == value) { - throwNotExistException(key); - } else if (isCheckEmpty && value.isEmpty()) { - throwEmptyException(key); + OTSRange range = new OTSRange(); + Map value = param.getMap(Key.RANGE); + // 用户可以不用配置range,默认表示导出全表 + if (value == null) { + return range; + } + + /** + * Range格式:{ + * "begin":[], + * "end":[] + * } + */ + + // begin + // 如果不存在,表示从表开始位置读取 + Object arrayObj = value.get(Constant.ConfigKey.Range.BEGIN); + if (arrayObj != null) { + range.setBegin(ParamParser.parsePrimaryKeyColumnArray(arrayObj)); + } + + // end + // 如果不存在,表示读取到表的结束位置 + arrayObj = value.get(Constant.ConfigKey.Range.END); + if (arrayObj != null) { + range.setEnd(ParamParser.parsePrimaryKeyColumnArray(arrayObj)); + } + + // split + // 如果不存在,表示不做切分 + arrayObj = value.get(Constant.ConfigKey.Range.SPLIT); + if (arrayObj != null) { + range.setSplit(ParamParser.parsePrimaryKeyColumnArray(arrayObj)); + } + + return range; + } catch (RuntimeException e) { + throw new OTSCriticalException("Parse 'range' fail, " + e.getMessage(), e); } - return value; + } - public static List checkListAndGet(Map range, String key) { - Object obj = range.get(key); - if (null == obj) { - return null; + public static TimeRange checkTimeRangeAndGet(Configuration param) throws OTSCriticalException { + try { + + long begin = Constant.ConfigDefaultValue.TimeRange.MIN; + long end = Constant.ConfigDefaultValue.TimeRange.MAX; + + Map value = param.getMap(Constant.ConfigKey.TIME_RANGE); + // 用户可以不用配置time range,默认表示导出全表 + if (value == null) { + return new TimeRange(begin, end); + } + + /** + * TimeRange格式:{ + * "begin":, + * "end": + * } + */ + + // begin + // 如果不存在,表示从表开始位置读取 + Object obj = value.get(Constant.ConfigKey.TimeRange.BEGIN); + if (obj != null) { + begin = ParamParser.parseTimeRangeItem(obj, Constant.ConfigKey.TimeRange.BEGIN); + } + + // end + // 如果不存在,表示读取到表的结束位置 + obj = value.get(Constant.ConfigKey.TimeRange.END); + if (obj != null) { + end = ParamParser.parseTimeRangeItem(obj, Constant.ConfigKey.TimeRange.END); + } + + TimeRange range = new TimeRange(begin, end); + return range; + } catch (RuntimeException e) { + throw new OTSCriticalException("Parse 'timeRange' fail, " + e.getMessage(), e); } - return checkListAndGet(range, key, false); } - public static List checkListAndGet(Map range, String key, boolean isCheckEmpty) { - Object obj = range.get(key); - if (null == obj) { - throwNotExistException(key); - } - if (obj instanceof List) { - @SuppressWarnings("unchecked") - List value = (List)obj; - if (isCheckEmpty && value.isEmpty()) { - throwEmptyException(key); + private static void checkColumnByMode(List columns , OTSMode mode) { + if (mode == OTSMode.MULTI_VERSION) { + for (OTSColumn c : columns) { + if (c.getColumnType() != OTSColumn.OTSColumnType.NORMAL) { + throw new IllegalArgumentException("in mode:'multiVersion', the 'column' only support specify column_name not const column."); + } } - return value; } else { - throw new IllegalArgumentException("Can not parse list of '" + key + "' from map."); + if (columns.isEmpty()) { + throw new IllegalArgumentException("in mode:'normal', the 'column' must specify at least one column_name or const column."); + } } } - public static List checkListAndGet(Map range, String key, List defaultList) { - Object obj = range.get(key); - if (null == obj) { - return defaultList; - } - if (obj instanceof List) { - @SuppressWarnings("unchecked") - List value = (List)obj; - return value; - } else { - throw new IllegalArgumentException("Can not parse list of '" + key + "' from map."); + public static List checkOTSColumnAndGet(Configuration param, OTSMode mode) throws OTSCriticalException { + try { + List value = param.getList(Key.COLUMN); + // 用户可以不用配置Column + if (value == null) { + value = Collections.emptyList(); + } + + /** + * Column格式:[ + * {"Name":"pk1"}, + * {"type":"Binary","value" : "base64()"} + * ] + */ + List columns = ParamParser.parseOTSColumnArray(value); + checkColumnByMode(columns, mode); + return columns; + } catch (RuntimeException e) { + throw new OTSCriticalException("Parse 'column' fail, " + e.getMessage(), e); } } - public static Map checkMapAndGet(Configuration param, String key, boolean isCheckEmpty) { - Map value = null; + public static List checkTimeseriesColumnAndGet(Configuration param) throws OTSCriticalException { try { - value = param.getMap(key); - } catch (ClassCastException e) { - throwNotMapException(key); + List value = param.getList(Key.COLUMN); + List columns = ParamParser.parseOTSColumnArray(value); + + List columnTypes = checkColumnTypeAndGet(param); + List isTags = checkColumnIsTagAndGet(param); + + for (int i = 0; i < columns.size(); i++) { + columns.get(i).setValueType(columnTypes.get(i)); + columns.get(i).setTimeseriesTag(isTags.get(i)); + } + + checkColumnByMode(columns, OTSMode.NORMAL); + return columns; + } catch (RuntimeException e) { + throw new OTSCriticalException("Parse 'column' fail, " + e.getMessage(), e); } - if (null == value) { - throwNotExistException(key); - } else if (isCheckEmpty && value.isEmpty()) { - throwEmptyException(key); + } + + public static List checkColumnTypeAndGet(Configuration param) throws OTSCriticalException { + try { + List value = param.getList(Key.COLUMN); + List columnTypes = ParamParser.parseColumnTypeArray(value); + return columnTypes; + } catch (RuntimeException e) { + throw new OTSCriticalException("Parse 'type of column' fail, " + e.getMessage(), e); } - return value; } - public static RowPrimaryKey checkInputPrimaryKeyAndGet(TableMeta meta, List range) { - if (meta.getPrimaryKey().size() != range.size()) { - throw new IllegalArgumentException(String.format( - "Input size of values not equal size of primary key. input size:%d, primary key size:%d .", - range.size(), meta.getPrimaryKey().size())); + public static List checkColumnIsTagAndGet(Configuration param) throws OTSCriticalException { + try { + List value = param.getList(Key.COLUMN); + List columnIsTag = ParamParser.parseColumnIsTagArray(value); + return columnIsTag; + } catch (RuntimeException e) { + throw new OTSCriticalException("Parse 'isTag of column' fail, " + e.getMessage(), e); } - RowPrimaryKey pk = new RowPrimaryKey(); - int i = 0; - for (Entry e: meta.getPrimaryKey().entrySet()) { - PrimaryKeyValue value = range.get(i); - if (e.getValue() != value.getType() && value != PrimaryKeyValue.INF_MIN && value != PrimaryKeyValue.INF_MAX) { - throw new IllegalArgumentException( - "Input range type not match primary key. Input type:" + value.getType() + ", Primary Key Type:"+ e.getValue() +", Index:" + i - ); + } + + public static OTSMode checkModeAndGet(Configuration param) throws OTSCriticalException { + try { + String modeValue = param.getString(Key.MODE, "normal"); + if (modeValue.equalsIgnoreCase(Constant.ConfigDefaultValue.Mode.NORMAL)) { + return OTSMode.NORMAL; + } else if (modeValue.equalsIgnoreCase(Constant.ConfigDefaultValue.Mode.MULTI_VERSION)) { + return OTSMode.MULTI_VERSION; } else { - pk.addPrimaryKeyColumn(e.getKey(), value); + throw new IllegalArgumentException("the 'mode' only support 'normal' and 'multiVersion' not '"+ modeValue +"'."); } - i++; + } catch(RuntimeException e) { + throw new OTSCriticalException("Parse 'mode' fail, " + e.getMessage(), e); } - return pk; } - public static OTSRange checkRangeAndGet(TableMeta meta, List begin, List end) { - OTSRange range = new OTSRange(); - if (begin.size() == 0 && end.size() == 0) { - RowPrimaryKey beginRow = new RowPrimaryKey(); - RowPrimaryKey endRow = new RowPrimaryKey(); - for (String name : meta.getPrimaryKey().keySet()) { - beginRow.addPrimaryKeyColumn(name, PrimaryKeyValue.INF_MIN); - endRow.addPrimaryKeyColumn(name, PrimaryKeyValue.INF_MAX); - } - range.setBegin(beginRow); - range.setEnd(endRow); - } else { - RowPrimaryKey beginRow = checkInputPrimaryKeyAndGet(meta, begin); - RowPrimaryKey endRow = checkInputPrimaryKeyAndGet(meta, end); - range.setBegin(beginRow); - range.setEnd(endRow); + public static void checkTimeseriesMode(OTSMode mode, Boolean isNewVersion) throws OTSCriticalException { + if (mode == OTSMode.MULTI_VERSION){ + throw new OTSCriticalException("Timeseries table do not support mode : multiVersion." ); + } else if (!isNewVersion){ + throw new OTSCriticalException("Timeseries table is only supported in newVersion, please set \"newVersion\": \"true\"." ); } - return range; } - public static Direction checkDirectionAndEnd(TableMeta meta, RowPrimaryKey begin, RowPrimaryKey end) { - Direction direction = null; - int cmp = Common.compareRangeBeginAndEnd(meta, begin, end) ; + public static List checkAndGetPrimaryKey( + List pk, + List pkSchema, + String jsonKey){ + List result = new ArrayList(); + if(pk != null) { + if (pk.size() > pkSchema.size()) { + throw new IllegalArgumentException("The '"+ jsonKey +"', input primary key column size more than table meta, input size: "+ pk.size() + +", meta pk size:" + pkSchema.size()); + } else { + //类型检查 + for (int i = 0; i < pk.size(); i++) { + PrimaryKeyValue pkc = pk.get(i).getValue(); + PrimaryKeySchema pkcs = pkSchema.get(i); - if (cmp > 0) { - direction = Direction.BACKWARD; - } else if (cmp < 0) { - direction = Direction.FORWARD; + if (!pkc.isInfMin() && !pkc.isInfMax() ) { + if (pkc.getType() != pkcs.getType()) { + throw new IllegalArgumentException( + "The '"+ jsonKey +"', input primary key column type mismath table meta, input type:"+ pkc.getType() + +", meta pk type:"+ pkcs.getType() + +", index:" + i); + } + } + result.add(new PrimaryKeyColumn(pkcs.getName(), pkc)); + } + } + return result; } else { - throw new IllegalArgumentException("Value of 'range-begin' equal value of 'range-end'."); + return new ArrayList(); } - return direction; } /** - * 检查类型是否一致,是否重复,方向是否一致 - * @param direction - * @param before - * @param after + * 检查split的类型是否和PartitionKey一致 + * @param points + * @param pkSchema */ - private static void checkDirection(Direction direction, PrimaryKeyValue before, PrimaryKeyValue after) { - int cmp = Common.primaryKeyValueCmp(before, after); - if (cmp > 0) { // 反向 - if (direction == Direction.FORWARD) { - throw new IllegalArgumentException("Input direction of 'range-split' is FORWARD, but direction of 'range' is BACKWARD."); - } - } else if (cmp < 0) { // 正向 - if (direction == Direction.BACKWARD) { - throw new IllegalArgumentException("Input direction of 'range-split' is BACKWARD, but direction of 'range' is FORWARD."); + private static List checkAndGetSplit( + List points, + List pkSchema){ + List result = new ArrayList(); + if (points == null) { + return result; + } + + // check 类型是否和PartitionKey一致即可 + PrimaryKeySchema partitionKeySchema = pkSchema.get(0); + for (int i = 0 ; i < points.size(); i++) { + PrimaryKeyColumn p = points.get(i); + if (!p.getValue().isInfMin() && !p.getValue().isInfMax()) { + if (p.getValue().getType() != partitionKeySchema.getType()) { + throw new IllegalArgumentException("The 'split', input primary key column type is mismatch partition key, input type: "+ p.getValue().getType().toString() + +", partition key type:" + partitionKeySchema.getType().toString() + +", index:" + i); + } } - } else { // 重复列 - throw new IllegalArgumentException("Multi same column in 'range-split'."); + result.add(new PrimaryKeyColumn(partitionKeySchema.getName(), p.getValue())); } + + return result; } - /** - * 检查 points中的所有点是否是在Begin和end之间 - * @param begin - * @param end - * @param points - */ - private static void checkPointsRange(Direction direction, PrimaryKeyValue begin, PrimaryKeyValue end, List points) { - if (direction == Direction.FORWARD) { - if (!(Common.primaryKeyValueCmp(begin, points.get(0)) < 0 && Common.primaryKeyValueCmp(end, points.get(points.size() - 1)) > 0)) { - throw new IllegalArgumentException("The item of 'range-split' is not within scope of 'range-begin' and 'range-end'."); + public static void fillPrimaryKey(List pkSchema, List pk, PrimaryKeyValue fillValue) { + for(int i = pk.size(); i < pkSchema.size(); i++) { + pk.add(new PrimaryKeyColumn(pkSchema.get(i).getName(), fillValue)); + } + } + + private static void fillBeginAndEnd( + List begin, + List end, + List pkSchema) { + if (begin.isEmpty()) { + fillPrimaryKey(pkSchema, begin, PrimaryKeyValue.INF_MIN); + } + if (end.isEmpty()) { + fillPrimaryKey(pkSchema, end, PrimaryKeyValue.INF_MAX); + } + int cmp = CompareHelper.comparePrimaryKeyColumnList(begin, end); + if (cmp == 0) { + // begin.size()和end.size()理论上必然相等,但是考虑到语义的清晰性,显示的给出begin.size() == end.size() + if (begin.size() == end.size() && begin.size() < pkSchema.size()) { + fillPrimaryKey(pkSchema, begin, PrimaryKeyValue.INF_MIN); + fillPrimaryKey(pkSchema, end, PrimaryKeyValue.INF_MAX); + } else { + throw new IllegalArgumentException("The 'begin' can not be equal with 'end'."); } - } else { - if (!(Common.primaryKeyValueCmp(begin, points.get(0)) > 0 && Common.primaryKeyValueCmp(end, points.get(points.size() - 1)) < 0)) { - throw new IllegalArgumentException("The item of 'range-split' is not within scope of 'range-begin' and 'range-end'."); + } else if (cmp < 0) { // 升序 + fillPrimaryKey(pkSchema, begin, PrimaryKeyValue.INF_MIN); + fillPrimaryKey(pkSchema, end, PrimaryKeyValue.INF_MAX); + } else { // 降序 + fillPrimaryKey(pkSchema, begin, PrimaryKeyValue.INF_MAX); + fillPrimaryKey(pkSchema, end, PrimaryKeyValue.INF_MIN); + } + } + + private static void checkBeginAndEndAndSplit( + List begin, + List end, + List split) { + int cmp = CompareHelper.comparePrimaryKeyColumnList(begin, end); + + if (!split.isEmpty()) { + if (cmp < 0) { // 升序 + // 检查是否是升序 + for (int i = 0 ; i < split.size() - 1; i++) { + PrimaryKeyColumn before = split.get(i); + PrimaryKeyColumn after = split.get(i + 1); + if (before.compareTo(after) >=0) { // 升序 + throw new IllegalArgumentException("In 'split', the item value is not increasing, index: " + i); + } + } + if (begin.get(0).compareTo(split.get(0)) >= 0) { + throw new IllegalArgumentException("The 'begin' must be less than head of 'split'."); + } + if (split.get(split.size() - 1).compareTo(end.get(0)) >= 0) { + throw new IllegalArgumentException("tail of 'split' must be less than 'end'."); + } + } else if (cmp > 0) {// 降序 + // 检查是否是降序 + for (int i = 0 ; i < split.size() - 1; i++) { + PrimaryKeyColumn before = split.get(i); + PrimaryKeyColumn after = split.get(i + 1); + if (before.compareTo(after) <= 0) { // 升序 + throw new IllegalArgumentException("In 'split', the item value is not descending, index: " + i); + } + } + if (begin.get(0).compareTo(split.get(0)) <= 0) { + throw new IllegalArgumentException("The 'begin' must be large than head of 'split'."); + } + if (split.get(split.size() - 1).compareTo(end.get(0)) <= 0) { + throw new IllegalArgumentException("tail of 'split' must be large than 'end'."); + } + } else { + throw new IllegalArgumentException("The 'begin' can not equal with 'end'."); } } } /** - * 1.检测用户的输入类型是否和PartitionKey一致 - * 2.顺序是否和Range一致 - * 3.是否有重复列 - * 4.检查points的范围是否在range内 - * @param meta - * @param points + * 填充不完整的PK + * 检查Begin、End、Split 3者之间的关系是否符合预期 + * @param begin + * @param end + * @param split */ - public static void checkInputSplitPoints(TableMeta meta, OTSRange range, Direction direction, List points) { - if (null == points || points.isEmpty()) { - return; - } + private static void fillAndcheckBeginAndEndAndSplit( + List begin, + List end, + List split, + List pkSchema + ) { + + fillBeginAndEnd(begin, end, pkSchema); + checkBeginAndEndAndSplit(begin, end, split); + } + + public static void checkAndSetOTSRange(OTSRange range, TableMeta meta) throws OTSCriticalException { + try { + List pkSchema = meta.getPrimaryKeyList(); - OTSPrimaryKeyColumn part = Common.getPartitionKey(meta); + // 检查是begin和end否和PK类型一致 + range.setBegin(checkAndGetPrimaryKey(range.getBegin(), pkSchema, Constant.ConfigKey.Range.BEGIN)); + range.setEnd(checkAndGetPrimaryKey(range.getEnd(), pkSchema, Constant.ConfigKey.Range.END)); + range.setSplit(checkAndGetSplit(range.getSplit(), pkSchema)); - // 处理第一个 - PrimaryKeyValue item = points.get(0); - if ( item.getType() != part.getType()) { - throw new IllegalArgumentException("Input type of 'range-split' not match partition key. " - + "Item of 'range-split' type:" + item.getType()+ ", Partition type:" + part.getType()); + // 1.填充Begin和End + // 2.检查begin,end,split顺序是否正确 + fillAndcheckBeginAndEndAndSplit(range.getBegin(), range.getEnd(), range.getSplit(), pkSchema); + } catch(RuntimeException e) { + throw new OTSCriticalException("Parse 'range' fail, " + e.getMessage(), e); } + } + + public static void checkAndSetColumn(List columns, TableMeta meta, OTSMode mode) throws OTSCriticalException { + try { + if (mode == OTSMode.MULTI_VERSION) { + Set uniqueColumn = new HashSet(); + Map pk = meta.getPrimaryKeyMap(); + for (OTSColumn c : columns) { + // 是否包括PK列 + if (pk.get(c.getName()) != null) { + throw new IllegalArgumentException("in mode:'multiVersion', the 'column' can not include primary key column, input:"+ c.getName() +"."); + } + // 是否有重复列 + if (uniqueColumn.contains(c.getName())) { + throw new IllegalArgumentException("in mode:'multiVersion', the 'column' can not include same column, input:"+ c.getName() +"."); + } else { + uniqueColumn.add(c.getName()); + } + } + } - for (int i = 0 ; i < points.size() - 1; i++) { - PrimaryKeyValue before = points.get(i); - PrimaryKeyValue after = points.get(i + 1); - checkDirection(direction, before, after); + } catch(RuntimeException e) { + throw new OTSCriticalException("Parse 'column' fail, " + e.getMessage(), e); } + } - PrimaryKeyValue begin = range.getBegin().getPrimaryKey().get(part.getName()); - PrimaryKeyValue end = range.getEnd().getPrimaryKey().get(part.getName()); + public static void normalCheck(OTSConf conf) { + // 旧版本不支持multiVersion模式 + if(!conf.isNewVersion() && conf.getMode() == OTSMode.MULTI_VERSION){ + throw new IllegalArgumentException("in mode:'multiVersion' :The old version do not support multiVersion mode. Please add config in otsreader: \"newVersion\":\"true\" ."); + } + } - checkPointsRange(direction, begin, end, points); + public static void checkAndSetOTSConf(OTSConf conf, TableMeta meta) throws OTSCriticalException { + normalCheck(conf); + checkAndSetOTSRange(conf.getRange(), meta); + checkAndSetColumn(conf.getColumn(), meta, conf.getMode()); } + } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ParamCheckerOld.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ParamCheckerOld.java new file mode 100644 index 0000000000..3489ab3582 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ParamCheckerOld.java @@ -0,0 +1,36 @@ +package com.alibaba.datax.plugin.reader.otsreader.utils; + +import com.alibaba.datax.common.util.Configuration; + +import java.util.List; + +public class ParamCheckerOld { + + private static void throwNotExistException(String key) { + throw new IllegalArgumentException("The param '" + key + "' is not exist."); + } + + private static void throwEmptyException(String key) { + throw new IllegalArgumentException("The param '" + key + "' is empty."); + } + + private static void throwNotListException(String key) { + throw new IllegalArgumentException("The param '" + key + "' is not a json array."); + } + + public static List checkListAndGet(Configuration param, String key, boolean isCheckEmpty) { + List value = null; + try { + value = param.getList(key); + } catch (ClassCastException e) { + throwNotListException(key); + } + if (null == value) { + throwNotExistException(key); + } else if (isCheckEmpty && value.isEmpty()) { + throwEmptyException(key); + } + return value; + } + +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ParamParser.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ParamParser.java new file mode 100644 index 0000000000..862b915cf0 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ParamParser.java @@ -0,0 +1,255 @@ +package com.alibaba.datax.plugin.reader.otsreader.utils; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSColumn; +import com.alibaba.datax.plugin.reader.otsreader.model.OTSCriticalException; +import com.alicloud.openservices.tablestore.model.ColumnType; +import com.alicloud.openservices.tablestore.model.PrimaryKeyColumn; +import com.alicloud.openservices.tablestore.model.PrimaryKeyValue; +import org.apache.commons.codec.binary.Base64; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class ParamParser { + + // ------------------------------------------------------------------------ + // Range解析相关的逻辑 + // ------------------------------------------------------------------------ + + private static PrimaryKeyValue parsePrimaryKeyValue(String type) { + return parsePrimaryKeyValue(type, null); + } + + private static PrimaryKeyValue parsePrimaryKeyValue(String type, String value) { + if (type.equalsIgnoreCase(Constant.ValueType.INF_MIN)) { + return PrimaryKeyValue.INF_MIN; + } else if (type.equalsIgnoreCase(Constant.ValueType.INF_MAX)) { + return PrimaryKeyValue.INF_MAX; + } else { + if (value != null) { + if (type.equalsIgnoreCase(Constant.ValueType.STRING)) { + return PrimaryKeyValue.fromString(value); + } else if (type.equalsIgnoreCase(Constant.ValueType.INTEGER)) { + return PrimaryKeyValue.fromLong(Long.valueOf(value)); + } else if (type.equalsIgnoreCase(Constant.ValueType.BINARY)) { + return PrimaryKeyValue.fromBinary(Base64.decodeBase64(value)); + } else { + throw new IllegalArgumentException("the column type only support :['INF_MIN', 'INF_MAX', 'string', 'int', 'binary']"); + } + } else { + throw new IllegalArgumentException("the column is missing the field 'value', input 'type':" + type); + } + } + } + + private static PrimaryKeyColumn parsePrimaryKeyColumn(Map item) { + Object typeObj = item.get(Constant.ConfigKey.PrimaryKeyColumn.TYPE); + Object valueObj = item.get(Constant.ConfigKey.PrimaryKeyColumn.VALUE); + + if (typeObj != null && valueObj != null) { + if (typeObj instanceof String && valueObj instanceof String) { + return new PrimaryKeyColumn( + Constant.ConfigDefaultValue.DEFAULT_NAME, + parsePrimaryKeyValue((String)typeObj, (String)valueObj) + ); + } else { + throw new IllegalArgumentException( + "the column's 'type' and 'value' must be string value, " + + "but type of 'type' is :" + typeObj.getClass() + + ", type of 'value' is :" + valueObj.getClass() + ); + } + } else if (typeObj != null) { + if (typeObj instanceof String) { + return new PrimaryKeyColumn( + Constant.ConfigDefaultValue.DEFAULT_NAME, + parsePrimaryKeyValue((String)typeObj) + ); + } else { + throw new IllegalArgumentException( + "the column's 'type' must be string value, " + + "but type of 'type' is :" + typeObj.getClass() + ); + } + } else { + throw new IllegalArgumentException("the column must include 'type' and 'value'."); + } + } + + @SuppressWarnings("unchecked") + public static List parsePrimaryKeyColumnArray(Object arrayObj) throws OTSCriticalException { + try { + List columns = new ArrayList(); + if (arrayObj instanceof List) { + List array = (List) arrayObj; + for (Object o : array) { + if (o instanceof Map) { + Map column = (Map) o; + columns.add(parsePrimaryKeyColumn(column)); + } else { + throw new IllegalArgumentException("input primary key column must be map object, but input type:" + o.getClass()); + } + } + } else { + throw new IllegalArgumentException("input 'begin','end','split' must be list object, but input type:" + arrayObj.getClass()); + } + return columns; + } catch (RuntimeException e) { + // 因为基础模块本身可能抛出一些错误,为了方便定位具体的出错位置,在此把Range加入到Error Message中 + throw new OTSCriticalException("Parse 'range' fail, " + e.getMessage(), e); + } + } + + // ------------------------------------------------------------------------ + // Column解析相关的逻辑 + // ------------------------------------------------------------------------ + + private static OTSColumn parseOTSColumn(Object obj) { + if (obj instanceof String) { + return OTSColumn.fromNormalColumn((String)obj); + } else { + throw new IllegalArgumentException("the 'name' must be string, but input:" + obj.getClass()); + } + } + + private static OTSColumn parseOTSColumn(Object typeObj, Object valueObj) { + if (typeObj instanceof String && valueObj instanceof String) { + String type = (String)typeObj; + String value = (String)valueObj; + + if (type.equalsIgnoreCase(Constant.ValueType.STRING)) { + return OTSColumn.fromConstStringColumn(value); + } else if (type.equalsIgnoreCase(Constant.ValueType.INTEGER)) { + return OTSColumn.fromConstIntegerColumn(Long.valueOf(value)); + } else if (type.equalsIgnoreCase(Constant.ValueType.DOUBLE)) { + return OTSColumn.fromConstDoubleColumn(Double.valueOf(value)); + } else if (type.equalsIgnoreCase(Constant.ValueType.BOOLEAN)) { + return OTSColumn.fromConstBoolColumn(Boolean.valueOf(value)); + } else if (type.equalsIgnoreCase(Constant.ValueType.BINARY)) { + return OTSColumn.fromConstBytesColumn(Base64.decodeBase64(value)); + } else { + throw new IllegalArgumentException("the const column type only support :['string', 'int', 'double', 'bool', 'binary']"); + } + } else { + throw new IllegalArgumentException("the 'type' and 'value' must be string, but 'type''s type:" + typeObj.getClass() + " 'value''s type:" + valueObj.getClass()); + } + } + + private static OTSColumn parseOTSColumn(Map column) { + Object typeObj = column.get(Constant.ConfigKey.Column.TYPE); + Object valueObj = column.get(Constant.ConfigKey.Column.VALUE); + Object nameObj = column.get(Constant.ConfigKey.Column.NAME); + + if (nameObj != null) { + return parseOTSColumn(nameObj); + } else if (typeObj != null && valueObj != null) { + return parseOTSColumn(typeObj, valueObj); + } else { + throw new IllegalArgumentException("the item of column format support '{\"name\":\"\"}' or '{\"type\":\"\", \"value\":\"\"}'."); + } + } + + @SuppressWarnings("unchecked") + public static List parseOTSColumnArray(List value) throws OTSCriticalException { + try { + List result = new ArrayList(); + for (Object item:value) { + if (item instanceof Map){ + Map column = (Map) item; + result.add(ParamParser.parseOTSColumn(column)); + } else { + throw new IllegalArgumentException("the item of column must be map object, but input: " + item.getClass()); + } + } + return result; + } catch (RuntimeException e) { + // 因为基础模块本身可能抛出一些错误,为了方便定位具体的出错位置,在此把Column加入到Error Message中 + throw new OTSCriticalException("Parse 'column' fail. " + e.getMessage(), e); + } + } + + private static ColumnType parseTimeseriesColumnType(Map column) { + Object typeObj = column.getOrDefault(Constant.ConfigKey.Column.TYPE, ""); + if (typeObj instanceof String) { + String type = (String)typeObj; + + if (type.equalsIgnoreCase(Constant.ValueType.STRING)) { + return ColumnType.STRING; + } else if (type.equalsIgnoreCase(Constant.ValueType.INTEGER)) { + return ColumnType.INTEGER; + } else if (type.equalsIgnoreCase(Constant.ValueType.DOUBLE)) { + return ColumnType.DOUBLE; + } else if (type.equalsIgnoreCase(Constant.ValueType.BOOLEAN)) { + return ColumnType.BOOLEAN; + } else if (type.equalsIgnoreCase(Constant.ValueType.BINARY)) { + return ColumnType.BINARY; + } else if (type.length() == 0){ + return ColumnType.STRING; + }else { + throw new IllegalArgumentException("the timeseries column type only support :['string', 'int', 'double', 'bool', 'binary']"); + } + } else { + throw new IllegalArgumentException("the 'type' must be string, but 'type''s type:" + typeObj.getClass()); + } + } + + public static List parseColumnTypeArray(List value) throws OTSCriticalException { + try { + List result = new ArrayList(); + for (Object item:value) { + if (item instanceof Map){ + Map column = (Map) item; + result.add(ParamParser.parseTimeseriesColumnType(column)); + } else { + throw new IllegalArgumentException("the item of column must be map object, but input: " + item.getClass()); + } + } + return result; + } catch (RuntimeException e) { + throw new OTSCriticalException("Parse 'timeseries column type' fail. " + e.getMessage(), e); + } + } + + private static Boolean parseTimeseriesColumnIsTag(Map column) { + Object isTagParameter = column.getOrDefault(Constant.ConfigKey.Column.IS_TAG, ""); + if (isTagParameter instanceof String) { + String isTag = (String)isTagParameter; + return Boolean.valueOf(isTag); + } else { + throw new IllegalArgumentException("the 'isTag' must be string, but 'isTag''s type:" + isTagParameter.getClass()); + } + } + + public static List parseColumnIsTagArray(List value) throws OTSCriticalException { + try { + List result = new ArrayList(); + for (Object item:value) { + if (item instanceof Map){ + Map column = (Map) item; + result.add(ParamParser.parseTimeseriesColumnIsTag(column)); + } else { + throw new IllegalArgumentException("the item of column must be map object, but input: " + item.getClass()); + } + } + return result; + } catch (RuntimeException e) { + throw new OTSCriticalException("Parse 'timeseries column isTag' fail. " + e.getMessage(), e); + } + } + + // ------------------------------------------------------------------------ + // TimeRange解析相关的逻辑 + // ------------------------------------------------------------------------ + + public static long parseTimeRangeItem(Object obj, String key) { + if (obj instanceof Integer) { + return (Integer)obj; + } else if (obj instanceof Long) { + return (Long)obj; + } else { + throw new IllegalArgumentException("the '"+ key +"' must be int, but input:" + obj.getClass()); + } + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/RangeSplit.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/RangeSplit.java index 74caac3f7a..fbef92799c 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/RangeSplit.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/RangeSplit.java @@ -1,17 +1,15 @@ package com.alibaba.datax.plugin.reader.otsreader.utils; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - import com.alibaba.datax.plugin.reader.otsreader.model.OTSPrimaryKeyColumn; import com.alibaba.datax.plugin.reader.otsreader.model.OTSRange; -import com.aliyun.openservices.ots.model.PrimaryKeyType; -import com.aliyun.openservices.ots.model.PrimaryKeyValue; -import com.aliyun.openservices.ots.model.RowPrimaryKey; -import com.aliyun.openservices.ots.model.TableMeta; +import com.alicloud.openservices.tablestore.model.PrimaryKeyColumn; +import com.alicloud.openservices.tablestore.model.PrimaryKeyType; +import com.alicloud.openservices.tablestore.model.PrimaryKeyValue; +import com.alicloud.openservices.tablestore.model.TableMeta; + + +import java.math.BigInteger; +import java.util.*; /** * 主要提供对范围的解析 @@ -35,8 +33,8 @@ private static String bigIntegerToString(BigInteger baseValue, * * 注意:该方法只支持begin小于end * - * @param beginStr - * @param endStr + * @param begin + * @param end * @param count * @return */ @@ -88,7 +86,6 @@ private static List splitCodePoint(int begin, int end, int count) { * @return */ public static List splitStringRange(String begin, String end, int count) { - if (count <= 1) { throw new IllegalArgumentException("Input count <= 1 ."); } @@ -136,15 +133,14 @@ public int compare(String arg0, String arg1) { } results.add(end); - return results; } /** * begin 一定要小于 end - * @param begin - * @param end - * @param count + * @param bigBegin + * @param bigEnd + * @param bigCount * @return */ private static List splitIntegerRange(BigInteger bigBegin, BigInteger bigEnd, BigInteger bigCount) { @@ -228,20 +224,31 @@ public static List splitRangeByPrimaryKeyType( } public static List rangeSplitByCount(TableMeta meta, - RowPrimaryKey begin, RowPrimaryKey end, int count) { + List begin, List end, int count) { List results = new ArrayList(); OTSPrimaryKeyColumn partitionKey = Common.getPartitionKey(meta); - PrimaryKeyValue beginPartitionKey = begin.getPrimaryKey().get( + Map beginMap = new HashMap<>(); + Map endMap = new HashMap<>(); + + for(PrimaryKeyColumn primaryKeyColumn : begin){ + beginMap.put(primaryKeyColumn.getName(), primaryKeyColumn.getValue()); + } + for(PrimaryKeyColumn primaryKeyColumn : end){ + endMap.put(primaryKeyColumn.getName(), primaryKeyColumn.getValue()); + } + + + PrimaryKeyValue beginPartitionKey = beginMap.get( partitionKey.getName()); - PrimaryKeyValue endPartitionKey = end.getPrimaryKey().get( + PrimaryKeyValue endPartitionKey = endMap.get( partitionKey.getName()); // 第一,先对PartitionKey列进行拆分 List ranges = RangeSplit.splitRangeByPrimaryKeyType( - partitionKey.getType(), beginPartitionKey, endPartitionKey, + partitionKey.getType(true), beginPartitionKey, endPartitionKey, count); if (ranges.isEmpty()) { @@ -250,130 +257,44 @@ public static List rangeSplitByCount(TableMeta meta, int size = ranges.size(); for (int i = 0; i < size - 1; i++) { - RowPrimaryKey bPk = new RowPrimaryKey(); - RowPrimaryKey ePk = new RowPrimaryKey(); + List bPk = new ArrayList<>(); + List ePk = new ArrayList<>(); - bPk.addPrimaryKeyColumn(partitionKey.getName(), ranges.get(i)); - ePk.addPrimaryKeyColumn(partitionKey.getName(), ranges.get(i + 1)); + bPk.add(new PrimaryKeyColumn(partitionKey.getName(), ranges.get(i))); + ePk.add(new PrimaryKeyColumn(partitionKey.getName(), ranges.get(i + 1))); - results.add(new OTSRange(bPk, ePk)); + OTSRange range = new OTSRange(); + range.setBegin(bPk); + range.setEnd(ePk); + results.add(range); } // 第二,填充非PartitionKey的ParimaryKey列 // 注意:在填充过程中,需要使用用户给定的Begin和End来替换切分出来的第一个Range // 的Begin和最后一个Range的End - List keys = new ArrayList(meta.getPrimaryKey().size()); - keys.addAll(meta.getPrimaryKey().keySet()); + List keys = new ArrayList(meta.getPrimaryKeyMap().size()); + keys.addAll(meta.getPrimaryKeyMap().keySet()); for (int i = 0; i < results.size(); i++) { for (int j = 1; j < keys.size(); j++) { OTSRange c = results.get(i); - RowPrimaryKey beginPK = c.getBegin(); - RowPrimaryKey endPK = c.getEnd(); + List beginPK = c.getBegin(); + List endPK = c.getEnd(); String key = keys.get(j); if (i == 0) { // 第一行 - beginPK.addPrimaryKeyColumn(key, - begin.getPrimaryKey().get(key)); - endPK.addPrimaryKeyColumn(key, PrimaryKeyValue.INF_MIN); + beginPK.add(new PrimaryKeyColumn(key, + beginMap.get(key))); + endPK.add(new PrimaryKeyColumn(key, PrimaryKeyValue.INF_MIN)); } else if (i == results.size() - 1) {// 最后一行 - beginPK.addPrimaryKeyColumn(key, PrimaryKeyValue.INF_MIN); - endPK.addPrimaryKeyColumn(key, end.getPrimaryKey().get(key)); + beginPK.add(new PrimaryKeyColumn(key, PrimaryKeyValue.INF_MIN)); + endPK.add(new PrimaryKeyColumn(key, endMap.get(key))); } else { - beginPK.addPrimaryKeyColumn(key, PrimaryKeyValue.INF_MIN); - endPK.addPrimaryKeyColumn(key, PrimaryKeyValue.INF_MIN); + beginPK.add(new PrimaryKeyColumn(key, PrimaryKeyValue.INF_MIN)); + endPK.add(new PrimaryKeyColumn(key, PrimaryKeyValue.INF_MIN)); } } } return results; } - - private static List getCompletePK(int num, - PrimaryKeyValue value) { - List values = new ArrayList(); - for (int j = 0; j < num; j++) { - if (j == 0) { - values.add(value); - } else { - // 这里在填充PK时,系统需要选择特定的值填充于此 - // 系统默认填充INF_MIN - values.add(PrimaryKeyValue.INF_MIN); - } - } - return values; - } - - /** - * 根据输入的范围begin和end,从target中取得对应的point - * @param begin - * @param end - * @param target - * @return - */ - public static List getSplitPoint(PrimaryKeyValue begin, PrimaryKeyValue end, List target) { - List result = new ArrayList(); - - int cmp = Common.primaryKeyValueCmp(begin, end); - - if (cmp == 0) { - return result; - } - - result.add(begin); - - Comparator comparator = new Comparator(){ - public int compare(PrimaryKeyValue arg0, PrimaryKeyValue arg1) { - return Common.primaryKeyValueCmp(arg0, arg1); - } - }; - - if (cmp > 0) { // 如果是逆序,则 reverse Comparator - comparator = Collections.reverseOrder(comparator); - } - - Collections.sort(target, comparator); - - for (PrimaryKeyValue value:target) { - if (comparator.compare(value, begin) > 0 && comparator.compare(value, end) < 0) { - result.add(value); - } - } - result.add(end); - - return result; - } - - public static List rangeSplitByPoint(TableMeta meta, RowPrimaryKey beginPK, RowPrimaryKey endPK, - List splits) { - - List results = new ArrayList(); - - int pkCount = meta.getPrimaryKey().size(); - - String partName = Common.getPartitionKey(meta).getName(); - PrimaryKeyValue begin = beginPK.getPrimaryKey().get(partName); - PrimaryKeyValue end = endPK.getPrimaryKey().get(partName); - - List newSplits = getSplitPoint(begin, end, splits); - - if (newSplits.isEmpty()) { - return results; - } - - for (int i = 0; i < newSplits.size() - 1; i++) { - OTSRange item = new OTSRange( - ParamChecker.checkInputPrimaryKeyAndGet(meta, - getCompletePK(pkCount, newSplits.get(i))), - ParamChecker.checkInputPrimaryKeyAndGet(meta, - getCompletePK(pkCount, newSplits.get(i + 1)))); - results.add(item); - } - // replace first and last - OTSRange first = results.get(0); - OTSRange last = results.get(results.size() - 1); - - first.setBegin(beginPK); - last.setEnd(endPK); - return results; - } } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ReaderModelParser.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ReaderModelParser.java index 8e1dfd4159..081532a669 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ReaderModelParser.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/ReaderModelParser.java @@ -55,7 +55,7 @@ public static OTSColumn parseConstColumn(String type, String value) { } public static OTSColumn parseOTSColumn(Map item) { - if (item.containsKey(OTSConst.NAME) && item.size() == 1) { + if (item.containsKey(OTSConst.NAME)) { Object name = item.get(OTSConst.NAME); if (name instanceof String) { String nameStr = (String) name; diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/RetryHelper.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/RetryHelper.java index 8ed412670c..318b7b51ff 100644 --- a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/RetryHelper.java +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/RetryHelper.java @@ -1,15 +1,14 @@ package com.alibaba.datax.plugin.reader.otsreader.utils; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.Callable; - +import com.alibaba.datax.plugin.reader.otsreader.model.OTSErrorCode; +import com.alicloud.openservices.tablestore.ClientException; +import com.alicloud.openservices.tablestore.TableStoreException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.aliyun.openservices.ots.ClientException; -import com.aliyun.openservices.ots.OTSErrorCode; -import com.aliyun.openservices.ots.OTSException; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; public class RetryHelper { @@ -19,7 +18,7 @@ public class RetryHelper { public static V executeWithRetry(Callable callable, int maxRetryTimes, int sleepInMilliSecond) throws Exception { int retryTimes = 0; while (true){ - Thread.sleep(Common.getDelaySendMillinSeconds(retryTimes, sleepInMilliSecond)); + Thread.sleep(getDelaySendMillinSeconds(retryTimes, sleepInMilliSecond)); try { return callable.call(); } catch (Exception e) { @@ -60,9 +59,9 @@ public static boolean canRetry(String otsErrorCode) { } public static boolean canRetry(Exception exception) { - OTSException e = null; - if (exception instanceof OTSException) { - e = (OTSException) exception; + TableStoreException e = null; + if (exception instanceof TableStoreException) { + e = (TableStoreException) exception; LOG.warn( "OTSException:ErrorCode:{}, ErrorMsg:{}, RequestId:{}", new Object[]{e.getErrorCode(), e.getMessage(), e.getRequestId()} @@ -72,12 +71,29 @@ public static boolean canRetry(Exception exception) { } else if (exception instanceof ClientException) { ClientException ce = (ClientException) exception; LOG.warn( - "ClientException:{}, ErrorMsg:{}", - new Object[]{ce.getErrorCode(), ce.getMessage()} + "ClientException:{}", + new Object[]{ce.getMessage()} ); return true; } else { return false; } } + + public static long getDelaySendMillinSeconds(int hadRetryTimes, int initSleepInMilliSecond) { + + if (hadRetryTimes <= 0) { + return 0; + } + + int sleepTime = initSleepInMilliSecond; + for (int i = 1; i < hadRetryTimes; i++) { + sleepTime += sleepTime; + if (sleepTime > 30000) { + sleepTime = 30000; + break; + } + } + return sleepTime; + } } diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/RetryHelperOld.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/RetryHelperOld.java new file mode 100644 index 0000000000..28ad4ee3d4 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/RetryHelperOld.java @@ -0,0 +1,83 @@ +package com.alibaba.datax.plugin.reader.otsreader.utils; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.aliyun.openservices.ots.ClientException; +import com.aliyun.openservices.ots.OTSErrorCode; +import com.aliyun.openservices.ots.OTSException; + +public class RetryHelperOld { + + private static final Logger LOG = LoggerFactory.getLogger(RetryHelperOld.class); + private static final Set noRetryErrorCode = prepareNoRetryErrorCode(); + + public static V executeWithRetry(Callable callable, int maxRetryTimes, int sleepInMilliSecond) throws Exception { + int retryTimes = 0; + while (true){ + Thread.sleep(CommonOld.getDelaySendMillinSeconds(retryTimes, sleepInMilliSecond)); + try { + return callable.call(); + } catch (Exception e) { + LOG.warn("Call callable fail, {}", e.getMessage()); + if (!canRetry(e)){ + LOG.error("Can not retry for Exception.", e); + throw e; + } else if (retryTimes >= maxRetryTimes) { + LOG.error("Retry times more than limition. maxRetryTimes : {}", maxRetryTimes); + throw e; + } + retryTimes++; + LOG.warn("Retry time : {}", retryTimes); + } + } + } + + private static Set prepareNoRetryErrorCode() { + Set pool = new HashSet(); + pool.add(OTSErrorCode.AUTHORIZATION_FAILURE); + pool.add(OTSErrorCode.INVALID_PARAMETER); + pool.add(OTSErrorCode.REQUEST_TOO_LARGE); + pool.add(OTSErrorCode.OBJECT_NOT_EXIST); + pool.add(OTSErrorCode.OBJECT_ALREADY_EXIST); + pool.add(OTSErrorCode.INVALID_PK); + pool.add(OTSErrorCode.OUT_OF_COLUMN_COUNT_LIMIT); + pool.add(OTSErrorCode.OUT_OF_ROW_SIZE_LIMIT); + pool.add(OTSErrorCode.CONDITION_CHECK_FAIL); + return pool; + } + + public static boolean canRetry(String otsErrorCode) { + if (noRetryErrorCode.contains(otsErrorCode)) { + return false; + } else { + return true; + } + } + + public static boolean canRetry(Exception exception) { + OTSException e = null; + if (exception instanceof OTSException) { + e = (OTSException) exception; + LOG.warn( + "OTSException:ErrorCode:{}, ErrorMsg:{}, RequestId:{}", + new Object[]{e.getErrorCode(), e.getMessage(), e.getRequestId()} + ); + return canRetry(e.getErrorCode()); + + } else if (exception instanceof ClientException) { + ClientException ce = (ClientException) exception; + LOG.warn( + "ClientException:{}, ErrorMsg:{}", + new Object[]{ce.getErrorCode(), ce.getMessage()} + ); + return true; + } else { + return false; + } + } +} diff --git a/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/TranformHelper.java b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/TranformHelper.java new file mode 100644 index 0000000000..b082e65804 --- /dev/null +++ b/otsreader/src/main/java/com/alibaba/datax/plugin/reader/otsreader/utils/TranformHelper.java @@ -0,0 +1,42 @@ +package com.alibaba.datax.plugin.reader.otsreader.utils; + +import com.alibaba.datax.common.element.*; +import com.alicloud.openservices.tablestore.model.PrimaryKeyColumn; + +public class TranformHelper { + + public static Column otsPrimaryKeyColumnToDataxColumn(PrimaryKeyColumn pkc) { + switch (pkc.getValue().getType()) { + case STRING:return new StringColumn(pkc.getValue().asString()); + case INTEGER:return new LongColumn(pkc.getValue().asLong()); + case BINARY:return new BytesColumn(pkc.getValue().asBinary()); + default: + throw new IllegalArgumentException("PrimaryKey unsuporrt tranform the type: " + pkc.getValue().getType() + "."); + } + } + + public static Column otsColumnToDataxColumn(com.alicloud.openservices.tablestore.model.Column c) { + switch (c.getValue().getType()) { + case STRING:return new StringColumn(c.getValue().asString()); + case INTEGER:return new LongColumn(c.getValue().asLong()); + case BINARY:return new BytesColumn(c.getValue().asBinary()); + case BOOLEAN:return new BoolColumn(c.getValue().asBoolean()); + case DOUBLE:return new DoubleColumn(c.getValue().asDouble()); + default: + throw new IllegalArgumentException("Column unsuporrt tranform the type: " + c.getValue().getType() + "."); + + } + } + + public static Column otsColumnToDataxColumn(com.alicloud.openservices.tablestore.model.ColumnValue c) { + switch (c.getType()) { + case STRING:return new StringColumn(c.asString()); + case INTEGER:return new LongColumn(c.asLong()); + case BINARY:return new BytesColumn(c.asBinary()); + case BOOLEAN:return new BoolColumn(c.asBoolean()); + case DOUBLE:return new DoubleColumn(c.asDouble()); + default: + throw new IllegalArgumentException("Column unsuporrt tranform the type: " + c.getType() + "."); + } + } +} diff --git a/otsreader/src/main/resources/plugin.json b/otsreader/src/main/resources/plugin.json index bfd956273a..4b55e374cb 100644 --- a/otsreader/src/main/resources/plugin.json +++ b/otsreader/src/main/resources/plugin.json @@ -3,4 +3,4 @@ "class": "com.alibaba.datax.plugin.reader.otsreader.OtsReader", "description": "", "developer": "alibaba" -} \ No newline at end of file +} diff --git a/otsstreamreader/README.md b/otsstreamreader/README.md index c861a737ba..5e68f1eb83 100644 --- a/otsstreamreader/README.md +++ b/otsstreamreader/README.md @@ -1,127 +1,152 @@ ## TableStore增量数据导出通道:TableStoreStreamReader +本文为您介绍OTSStream Reader支持的数据类型、读取方式、字段映射和数据源等参数及配置示例。 +## 列模式 -### 快速介绍 - -TableStoreStreamReader插件主要用于TableStore的增量数据导出,增量数据可以看作操作日志,除了数据本身外还附有操作信息。 - -与全量导出插件不同,增量导出插件只有多版本模式,同时不支持指定列。这是与增量导出的原理有关的,导出的格式下面有详细介绍。 - -使用插件前必须确保表上已经开启Stream功能,可以在建表的时候指定开启,或者使用SDK的UpdateTable接口开启。 - - 开启Stream的方法: - SyncClient client = new SyncClient("", "", "", ""); - 1. 建表的时候开启: - CreateTableRequest createTableRequest = new CreateTableRequest(tableMeta); - createTableRequest.setStreamSpecification(new StreamSpecification(true, 24)); // 24代表增量数据保留24小时 - client.createTable(createTableRequest); - - 2. 如果建表时未开启,可以通过UpdateTable开启: - UpdateTableRequest updateTableRequest = new UpdateTableRequest("tableName"); - updateTableRequest.setStreamSpecification(new StreamSpecification(true, 24)); - client.updateTable(updateTableRequest); - -### 实现原理 - -首先用户使用SDK的UpdateTable功能,指定开启Stream并设置过期时间,即开启了增量功能。 - -开启后,TableStore服务端就会将用户的操作日志额外保存起来, -每个分区有一个有序的操作日志队列,每条操作日志会在一定时间后被垃圾回收,这个时间即用户指定的过期时间。 - -TableStore的SDK提供了几个Stream相关的API用于将这部分操作日志读取出来,增量插件也是通过TableStore SDK的接口获取到增量数据的,并将 -增量数据转化为多个6元组的形式(pk, colName, version, colValue, opType, sequenceInfo)导入到ODPS中。 - -### Reader的配置模版: - - "reader": { - "name" : "otsstreamreader", - "parameter" : { - "endpoint" : "", - "accessId" : "", - "accessKey" : "", - "instanceName" : "", - //dataTable即需要导出数据的表。 - "dataTable" : "", - //statusTable是Reader用于保存状态的表,若该表不存在,Reader会自动创建该表。 - //一次离线导出任务完成后,用户不应删除该表,该表中记录的状态可用于下次导出任务中。 - "statusTable" : "TableStoreStreamReaderStatusTable", - //增量数据的时间范围(左闭右开)的左边界。 - "startTimestampMillis" : "", - //增量数据的时间范围(左闭右开)的右边界。 - "endTimestampMillis" : "", - //采云间调度只支持天级别,所以提供该配置,作用与startTimestampMillis和endTimestampMillis类似。 - "date": "", - //是否导出时序信息。 - "isExportSequenceInfo": true, - //从TableStore中读增量数据时,每次请求的最大重试次数,默认为30。 - "maxRetries" : 30 - } - } -### 参数说明 +### 背景信息 + +OTSStream Reader插件主要用于导出Table Store的增量数据。您可以将增量数据看作操作日志,除数据本身外还附有操作信息。 + +与全量导出插件不同,增量导出插件只有多版本模式,且不支持指定列。使用插件前,您必须确保表上已经开启Stream功能。您可以在建表时指定开启,也可以使用SDK的UpdateTable接口开启。 + +开启Stream的方法,如下所示。 +```java +SyncClient client = new SyncClient("", "", "", ""); +#建表的时候开启: +CreateTableRequest createTableRequest = new CreateTableRequest(tableMeta); +createTableRequest.setStreamSpecification(new StreamSpecification(true, 24)); // 24代表增量数据保留24小时。 +client.createTable(createTableRequest); +#如果建表时未开启,您可以通过UpdateTable开启: +UpdateTableRequest updateTableRequest = new UpdateTableRequest("tableName"); +updateTableRequest.setStreamSpecification(new StreamSpecification(true, 24)); +client.updateTable(updateTableRequest); +``` +您使用SDK的UpdateTable功能,指定开启Stream并设置过期时间,即开启了Table Store增量数据导出功能。开启后,Table Store服务端就会将您的操作日志额外保存起来,每个分区有一个有序的操作日志队列,每条操作日志会在一定时间后被垃圾回收,该时间即为您指定的过期时间。 + +Table Store的SDK提供了几个Stream相关的API用于读取这部分的操作日志,增量插件也是通过Table Store SDK的接口获取到增量数据,默认情况下会将增量数据转化为多个6元组的形式(pk、colName、version、colValue、opType和sequenceInfo)导入至MaxCompute中。 + +### 列模式 + +在Table Store多版本模型下,表中的数据组织为行>列>版本三级的模式, 一行可以有任意列,列名并不是固定的,每一列可以含有多个版本,每个版本都有一个特定的时间戳(版本号)。 + +您可以通过Table Store的API进行一系列读写操作,Table Store通过记录您最近对表的一系列写操作(或数据更改操作)来实现记录增量数据的目的,所以您也可以把增量数据看作一批操作记录。 + +Table Store支持**PutRow**、**UpdateRow**和**DeleteRow**操作: +- **PutRow**:写入一行,如果该行已存在即覆盖该行。 +- **UpdateRow**:更新一行,不更改原行的其它数据。更新包括新增或覆盖(如果对应列的对应版本已存在)一些列值、删除某一列的全部版本、删除某一列的某个版本。 +- **DeleteRow**:删除一行。 + +Table Store会根据每种操作生成对应的增量数据记录,Reader插件会读出这些记录,并导出为数据集成的数据格式。 -| 名称 | 说明 | 类型 | 必选 | -| ---- | ---- | ---- | ---- | -| endpoint | TableStoreServer的Endpoint地址。| String | 是 | -| accessId | 用于访问TableStore服务的accessId。| String | 是 | -| accessKey | 用于访问TableStore服务的accessKey。 | String | 是 | -| instanceName | TableStore的实例名称。 | String | 是 | -| dataTable | 需要导出增量数据的表的名称。该表需要开启Stream,可以在建表时开启,或者使用UpdateTable接口开启。 | String | 是 | -| statusTable | Reader插件用于记录状态的表的名称,这些状态可用于减少对非目标范围内的数据的扫描,从而加快导出速度。
1. 用户不需要创建该表,只需要给出一个表名。Reader插件会尝试在用户的instance下创建该表,若该表不存在即创建新表,若该表已存在,会判断该表的Meta是否与期望一致,若不一致会抛出异常。
2. 在一次导出完成之后,用户不应删除该表,该表的状态可用于下次导出任务。
3. 该表会开启TTL,数据自动过期,因此可认为其数据量很小。
4. 针对同一个instance下的多个不同的dataTable的Reader配置,可以使用同一个statusTable,记录的状态信息互不影响。
综上,用户配置一个类似TableStoreStreamReaderStatusTable之类的名称即可,注意不要与业务相关的表重名。| String | 是 | -| startTimestampMillis | 增量数据的时间范围(左闭右开)的左边界,单位毫秒。
1. Reader插件会从statusTable中找对应startTimestampMillis的位点,从该点开始读取开始导出数据。
2. 若statusTable中找不到对应的位点,则从系统保留的增量数据的第一条开始读取,并跳过写入时间小于startTimestampMillis的数据。| Long | 否 | -| endTimestampMillis | 增量数据的时间范围(左闭右开)的右边界,单位毫秒。
1. Reader插件从startTimestampMillis位置开始导出数据后,当遇到第一条时间戳大于等于endTimestampMillis的数据时,结束导出数据,导出完成。
2. 当读取完当前全部的增量数据时,结束读取,即使未达到endTimestampMillis。 | Long | 否 | -| date | 日期格式为yyyyMMdd,如20151111,表示导出该日的数据。
若没有指定date,则必须指定startTimestampMillis和endTimestampMillis,反之也成立。 | String | 否 | -| isExportSequenceInfo | 是否导出时序信息,时序信息包含了数据的写入时间等。默认该值为false,即不导出。 | Boolean | 否 | -| maxRetries | 从TableStore中读增量数据时,每次请求的最大重试次数,默认为30,重试之间有间隔,30次重试总时间约为5分钟,一般无需更改。| Int | 否 | - -### 导出的数据格式 -首先,在TableStore多版本模型下,表中的数据组织为“行-列-版本”三级的模式, -一行可以有任意列,列名也并非固定的,每一列可以含有多个版本,每个版本都有一个特定的时间戳(版本号)。 - -用户可以通过TableStore的API进行一系列读写操作, -TableStore通过记录用户最近对表的一系列写操作(或称为数据更改操作)来实现记录增量数据的目的, -所以也可以把增量数据看作一批操作记录。 - -TableStore有三类数据更改操作:PutRow、UpdateRow、DeleteRow。 - - + PutRow的语义是写入一行,若该行已存在即覆盖该行。 - - + UpdateRow的语义是更新一行,对原行其他数据不做更改, - 更新可能包括新增或覆盖(若对应列的对应版本已存在)一些列值、删除某一列的全部版本、删除某一列的某个版本。 - - + DeleteRow的语义是删除一行。 - -TableStore会根据每种操作生成对应的增量数据记录,Reader插件会读出这些记录,并导出成Datax的数据格式。 - -同时,由于TableStore具有动态列、多版本的特性,所以Reader插件导出的一行不对应TableStore中的一行,而是对应TableStore中的一列的一个版本。 -即TableStore中的一行可能会导出很多行,每行包含主键值、该列的列名、该列下该版本的时间戳(版本号)、该版本的值、操作类型。若设置isExportSequenceInfo为true,还会包括时序信息。 - -转换为Datax的数据格式后,我们定义了四种操作类型,分别为: - - + U(UPDATE): 写入一列的一个版本 - - + DO(DELETE_ONE_VERSION): 删除某一列的某个版本 - - + DA(DELETE_ALL_VERSION): 删除某一列的全部版本,此时需要根据主键和列名,将对应列的全部版本删除 - - + DR(DELETE_ROW): 删除某一行,此时需要根据主键,将该行数据全部删除 - - -举例如下,假设该表有两个主键列,主键列名分别为pkName1, pkName2: - -| pkName1 | pkName2 | columnName | timestamp | columnValue | opType | -| ------- | ------- | ---------- | --------- | ----------- | ------ | +同时,由于Table Store具有动态列、多版本的特性,所以Reader插件导出的一行不对应Table Store中的一行,而是对应Table Store中的一列的一个版本。即Table Store中的一行可能会导出很多行,每行包含主键值、该列的列名、该列下该版本的时间戳(版本号)、该版本的值、操作类型。如果设置isExportSequenceInfo为true,还会包括时序信息。 + +转换为数据集成的数据格式后,定义了以下四种操作类型: +- **U(UPDATE)**:写入一列的一个版本。 +- **DO(DELETE_ONE_VERSION)**:删除某一列的某个版本。 +- **DA(DELETE_ALL_VERSION)**:删除某一列的全部版本,此时需要根据主键和列名,删除对应列的全部版本。 +- **DR(DELETE_ROW)**:删除某一行,此时需要根据主键,删除该行数据。 + +假设该表有两个主键列,主键列名分别为pkName1, pkName2,示例如下。 + +| **pkName1** | **pkName2** | **columnName** | **timestamp** | **columnValue** | **opType** | +| --- | --- | --- | --- | --- | --- | | pk1_V1 | pk2_V1 | col_a | 1441803688001 | col_val1 | U | | pk1_V1 | pk2_V1 | col_a | 1441803688002 | col_val2 | U | -| pk1_V1 | pk2_V1 | col_b | 1441803688003 | col_val3 | U | -| pk1_V2 | pk2_V2 | col_a | 1441803688000 | | DO | -| pk1_V2 | pk2_V2 | col_b | | | DA | -| pk1_V3 | pk2_V3 | | | | DR | +| pk1_V1 | pk2_V1 | col_b | 1441803688003 | col_val3 | U | +| pk1_V2 | pk2_V2 | col_a | 1441803688000 | — | DO | +| pk1_V2 | pk2_V2 | col_b | — | — | DA | +| pk1_V3 | pk2_V3 | — | — | — | DR | | pk1_V3 | pk2_V3 | col_a | 1441803688005 | col_val1 | U | - -假设导出的数据如上,共7行,对应TableStore表内的3行,主键分别是(pk1_V1,pk2_V1), (pk1_V2, pk2_V2), (pk1_V3, pk2_V3)。 -对于主键为(pk1_V1, pk2_V1)的一行,包含三个操作,分别是写入col_a列的两个版本和col_b列的一个版本。 +假设导出的数据如上,共7行,对应Table Store表内的3行,主键分别是(pk1_V1,pk2_V1),(pk1_V2, pk2_V2),(pk1_V3, pk2_V3): +- 对于主键为(pk1_V1,pk2_V1)的一行,包括写入col_a列的两个版本和col_b列的一个版本等操作。 +- 对于主键为(pk1_V2,pk2_V2)的一行,包括删除col_a列的一个版本和删除col_b列的全部版本等操作。 +- 对于主键为(pk1_V3,pk2_V3)的一行,包括删除整行和写入col_a列的一个版本等操作。 + +### 行模式 +#### 宽行表 +您可以通过行模式导出数据,该模式将用户每次更新的记录,抽取成行的形式导出,需要设置mode属性并配置列名。 +```json +"parameter": { + #parameter中配置下面三项配置(例如datasource、table等其它配置项照常配置)。 + "mode": "single_version_and_update_only", # 配置导出模式。 + "column":[ #按照需求添加需要导出TableStore中的列,您可以自定义设置配置个数。 + { + "name": "uid" #列名示例,可以是主键或属性列。 + }, + { + "name": "name" #列名示例,可以是主键或属性列。 + }, + ], + "isExportSequenceInfo": false, #single_version_and_update_only模式下只能是false。 +} +``` +#### 时序表 +`otsstreamreader`支持导出时序表中的增量数据,当表为时序表时,需要配置的信息如下: +```json +"parameter": { + #parameter中配置下面四项配置(例如datasource、table等其它配置项照常配置)。 + "mode": "single_version_and_update_only", # 配置导出模式。 + "isTimeseriesTable":"true", # 配置导出为时序表。 + "column":[ #按照需求添加需要导出TableStore中的列,您可以自定义设置配置个数。 + { + "name": "_m_name" #度量名称字段。 + }, + { + "name": "_data_source" #数据源字段。 + }, + { + "name": "_tags" #标签字段,将tags转换为string类型。 + }, + { + "name": "tag1_1", #标签内部字段键名称。 + "is_timeseries_tag":"true" #表明改字段为tags内部字段。 + }, + { + "name": "time" #时间戳字段。 + }, + { + "name": "name" #属性列名称。 + }, + ], + "isExportSequenceInfo": false, #single_version_and_update_only模式下只能是false。 +} +``` + +行模式导出的数据更接近于原始的行,易于后续处理,但需要注意以下问题: +- 每次导出的行是从用户每次更新的记录中抽取,每一行数据与用户的写入或更新操作一一对应。如果用户存在单独更新某些列的行为,则会出现有一些记录只有被更新的部分列,其它列为空的情况。 +- 行模式不会导出数据的版本号(即每列的时间戳),也无法进行删除操作。 + +### 数据类型转换列表 +目前OTSStream Reader支持所有的Table Store类型,其针对Table Store类型的转换列表,如下所示。 + +| **类型分类** | **OTSStream数据类型** | +| --- | --- | +| 整数类 | INTEGER | +| 浮点类 | DOUBLE | +| 字符串类 | STRING | +| 布尔类 | BOOLEAN | +| 二进制类 | BINARY | + + + +### 参数说明 + +| **参数** | **描述** | **是否必选** | **默认值** | +| --- |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| --- |---------| +| **dataSource** | 数据源名称,脚本模式支持添加数据源,该配置项填写的内容必须与添加的数据源名称保持一致。 | 是 | 无 | +| **dataTable** | 导出增量数据的表的名称。该表需要开启Stream,可以在建表时开启,或者使用UpdateTable接口开启。 | 是 | 无 | +| **statusTable** | Reader插件用于记录状态的表的名称,这些状态可用于减少对非目标范围内的数据的扫描,从而加快导出速度。statusTable是Reader用于保存状态的表,如果该表不存在,Reader会自动创建该表。一次离线导出任务完成后,您无需删除该表,该表中记录的状态可用于下次导出任务中:
  • 您无需创建该表,只需要给出一个表名。Reader插件会尝试在您的instance下创建该表,如果该表不存在即创建新表。如果该表已存在,会判断该表的Meta是否与期望一致,如果不一致会抛出异常。
  • 在一次导出完成之后,您无需删除该表,该表的状态可以用于下次的导出任务。
  • 该表会开启TTL,数据自动过期,会认为其数据量很小。
  • 针对同一个instance下的多个不同的dataTable的Reader配置,可以使用同一个statusTable,记录的状态信息互不影响。您配置一个类似**TableStoreStreamReaderStatusTable**的名称即可,请注意不要与业务相关的表重名。 | 是 | 无 | +| **startTimestampMillis** | 增量数据的时间范围(左闭右开)的左边界,单位为毫秒:
  • Reader插件会从statusTable中找对应**startTimestampMillis**的位点,从该点开始读取开始导出数据。
  • 如果statusTable中找不到对应的位点,则从系统保留的增量数据的第一条开始读取,并跳过写入时间小于**startTimestampMillis**的数据。 | 否 | 无 | +| **endTimestampMillis** | 增量数据的时间范围(左闭右开)的右边界,单位为毫秒:
  • Reader插件从**startTimestampMillis**位置开始导出数据后,当遇到第一条时间戳大于等于**endTimestampMillis**的数据时,结束导出数据,导出完成。
  • 当读取完当前全部的增量数据时,即使未达到**endTimestampMillis**,也会结束读取。 | 否 | 无 | +| **date** | 日期格式为**yyyyMMdd**,例如20151111,表示导出该日的数据。如果没有指定**date**,则需要指定**startTimestampMillis**和**endTimestampMillis**或**startTimeString**和**endTimeString**,反之也成立。例如,采云间调度仅支持天级别,所以提供该配置,作用与**startTimestampMillis**和**endTimestampMillis**或**startTimeString**和**endTimeString**类似。 | 否 | 无 | +| **isExportSequenceInfo** | 是否导出时序信息,时序信息包含了数据的写入时间等。默认该值为false,即不导出。 | 否 | false | +| **maxRetries** | 从TableStore中读增量数据时,每次请求的最大重试次数,默认为30次。重试之间有间隔,重试30次的总时间约为5分钟,通常无需更改。 | 否 | 30 | +| **startTimeString** | 任务的开始时间,即增量数据的时间范围(左闭右开)的左边界,格式为**yyyymmddhh24miss**,单位为秒。 | 否 | 无 | +| **endTimeString** | 任务的结束时间,即增量数据的时间范围(左闭右开)的右边界,格式为**yyyymmddhh24miss**,单位为秒。 | 否 | 无 | +| **enableSeekIterator** | Reader插件需要先确定增量位点,然后再拉取数据,如果是经常运行的任务,插件会根据之前扫描的位点来确定位置。如果之前没运行过这个插件,将会从增量开始位置(默认增量保留7天,即7天前)开始扫描,因此当还没有扫描到设置的开始时间之后的数据时,会存在开始一段时间没有数据导出的情况,您可以在reader的配置参数里增加** "enableSeekIterator": true**的配置,帮助您加快位点定位。 | 否 | false | +| **mode** | 导出模式,设置为**single_version_and_update_only**时为行模式,默认不设置为列模式。 | 否 | 无 | +| **isTimeseriesTable** | 是否为时序表,只有在行模式,即**mode**为**single_version_and_update_only**时配置生效。 | 否 | false | + -对于主键为(pk1_V2, pk2_V2)的一行,包含两个操作,分别是删除col_a列的一个版本、删除col_b列的全部版本。 -对于主键为(pk1_V3, pk2_V3)的一行,包含两个操作,分别是删除整行、写入col_a列的一个版本。 diff --git a/otsstreamreader/pom.xml b/otsstreamreader/pom.xml index 2a12872fb5..db75ba1e09 100644 --- a/otsstreamreader/pom.xml +++ b/otsstreamreader/pom.xml @@ -10,9 +10,21 @@ com.alibaba.datax otsstreamreader - 0.0.1 + 0.0.1-SNAPSHOT + + + com.aliyun.openservices + tablestore-streamclient + 1.0.0 + + + com.aliyun.openservices + tablestore + + + com.alibaba.datax datax-common @@ -22,29 +34,39 @@ slf4j-log4j12 org.slf4j - - logback-classic - ch.qos.logback - + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + com.alibaba + fastjson + 1.2.83_noneautotype + compile + com.aliyun.openservices - tablestore-streamclient - 1.0.0 + tablestore + 5.13.12 + + + log4j-core + org.apache.logging.log4j + + com.google.code.gson gson 2.2.4 - - com.google.guava - guava - 18.0 - test - @@ -85,6 +107,18 @@ + + + org.apache.maven.plugins + maven-surefire-plugin + 2.5 + + + **/unittest/*.java + **/functiontest/*.java + + + diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/LocalStrings.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/LocalStrings.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/LocalStrings_en_US.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/LocalStrings_en_US.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/LocalStrings_ja_JP.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/LocalStrings_zh_CN.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/LocalStrings_zh_HK.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/LocalStrings_zh_TW.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/OTSStreamReader.java b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/OTSStreamReader.java index 6731346701..a41b19d4c4 100644 --- a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/OTSStreamReader.java +++ b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/OTSStreamReader.java @@ -4,17 +4,27 @@ import com.alibaba.datax.common.plugin.RecordSender; import com.alibaba.datax.common.spi.Reader; import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.RetryUtil; import com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.OTSStreamReaderConfig; import com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.OTSStreamReaderConstants; +import com.alibaba.datax.plugin.reader.otsstreamreader.internal.core.CheckpointTimeTracker; +import com.alibaba.datax.plugin.reader.otsstreamreader.internal.model.OTSStreamJobShard; import com.alibaba.datax.plugin.reader.otsstreamreader.internal.model.StreamJob; import com.alibaba.datax.plugin.reader.otsstreamreader.internal.utils.GsonParser; +import com.alibaba.datax.plugin.reader.otsstreamreader.internal.utils.OTSHelper; +import com.alibaba.datax.plugin.reader.otsstreamreader.internal.utils.OTSStreamJobShardUtil; +import com.alicloud.openservices.tablestore.SyncClientInterface; import com.alicloud.openservices.tablestore.TableStoreException; import com.alicloud.openservices.tablestore.model.StreamShard; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; +import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentSkipListSet; +import static com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.OTSStreamReaderConstants.*; + public class OTSStreamReader { public static class Job extends Reader.Job { @@ -46,20 +56,34 @@ public static class Task extends Reader.Task { private OTSStreamReaderSlaveProxy proxy = new OTSStreamReaderSlaveProxy(); @Override - public void startRead(RecordSender recordSender) { - proxy.startRead(recordSender); - } - public void init() { try { OTSStreamReaderConfig config = GsonParser.jsonToConfig( (String) this.getPluginJobConf().get(OTSStreamReaderConstants.CONF)); - StreamJob streamJob = StreamJob.fromJson( - (String) this.getPluginJobConf().get(OTSStreamReaderConstants.STREAM_JOB)); List ownedShards = GsonParser.jsonToList( - (String) this.getPluginJobConf().get(OTSStreamReaderConstants.OWNED_SHARDS)); - List allShards = GsonParser.fromJson( - (String) this.getPluginJobConf().get(OTSStreamReaderConstants.ALL_SHARDS)); + (String) this.getPluginJobConf().get(OTSStreamReaderConstants.OWNED_SHARDS)); + + boolean confSimplifyEnable = this.getPluginJobConf().getBool(CONF_SIMPLIFY_ENABLE, + DEFAULT_CONF_SIMPLIFY_ENABLE_VALUE); + + StreamJob streamJob; + List allShards; + + if (confSimplifyEnable) { + //不要从conf里获取, 避免分布式模式下Job Split切分出来的Config膨胀过大 + String version = this.getPluginJobConf().getString(OTSStreamReaderConstants.VERSION); + OTSStreamJobShard otsStreamJobShard = OTSStreamJobShardUtil.getOTSStreamJobShard(config, version); + + streamJob = otsStreamJobShard.getStreamJob(); + allShards = otsStreamJobShard.getAllShards(); + + } else { + streamJob = StreamJob.fromJson( + (String) this.getPluginJobConf().get(OTSStreamReaderConstants.STREAM_JOB)); + allShards = GsonParser.fromJson( + (String) this.getPluginJobConf().get(OTSStreamReaderConstants.ALL_SHARDS)); + } + proxy.init(config, streamJob, allShards, new HashSet(ownedShards)); } catch (TableStoreException ex) { throw DataXException.asDataXException(new OTSReaderError(ex.getErrorCode(), "OTS ERROR"), ex.toString(), ex); @@ -68,6 +92,11 @@ public void init() { } } + @Override + public void startRead(RecordSender recordSender) { + proxy.startRead(recordSender); + } + public void destroy() { proxy.close(); } diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/OTSStreamReaderMasterProxy.java b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/OTSStreamReaderMasterProxy.java index 473e2c8132..5c6a5b4b30 100644 --- a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/OTSStreamReaderMasterProxy.java +++ b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/OTSStreamReaderMasterProxy.java @@ -15,6 +15,8 @@ import java.util.*; +import static com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.OTSStreamReaderConstants.CONF_SIMPLIFY_ENABLE; + public class OTSStreamReaderMasterProxy { private OTSStreamReaderConfig conf = null; @@ -22,6 +24,7 @@ public class OTSStreamReaderMasterProxy { private StreamJob streamJob; private List allShards; + private String version; private static final Logger LOG = LoggerFactory.getLogger(OTSStreamReaderConfig.class); @@ -41,19 +44,20 @@ public void init(OTSStreamReaderConfig config) throws Exception { checker.checkAndCreateStatusTableIfNotExist(); // 删除StatusTable记录的对应EndTime时刻的Checkpoint信息。防止本次任务受到之前导出任务的影响。 - String streamId = OTSHelper.getStreamDetails(ots, config.getDataTable()).getStreamId(); + String streamId = OTSHelper.getStreamResponse(ots, config.getDataTable(), config.isTimeseriesTable()).getStreamId(); CheckpointTimeTracker checkpointInfoTracker = new CheckpointTimeTracker(ots, config.getStatusTable(), streamId); checkpointInfoTracker.clearAllCheckpoints(config.getEndTimestampMillis()); SyncClientInterface ots = OTSHelper.getOTSInstance(config); - allShards = OTSHelper.getOrderedShardList(ots, streamId); + allShards = OTSHelper.getOrderedShardList(ots, streamId, conf.isTimeseriesTable()); List shardIds = new ArrayList(); for (StreamShard shard : allShards) { shardIds.add(shard.getShardId()); } - String version = "" + System.currentTimeMillis() + "-" + UUID.randomUUID(); + this.version = "" + System.currentTimeMillis() + "-" + UUID.randomUUID(); + LOG.info("version is: {}", this.version); streamJob = new StreamJob(conf.getDataTable(), streamId, version, new HashSet(shardIds), conf.getStartTimestampMillis(), conf.getEndTimestampMillis()); @@ -97,8 +101,16 @@ public List split(int adviceNumber) { Configuration configuration = Configuration.newDefault(); configuration.set(OTSStreamReaderConstants.CONF, GsonParser.configToJson(conf)); - configuration.set(OTSStreamReaderConstants.STREAM_JOB, streamJob.toJson()); - configuration.set(OTSStreamReaderConstants.ALL_SHARDS, GsonParser.toJson(allShards)); + + // Fix #39430646 [离线同步分布式]DataX OTSStreamReader插件分布式模式优化瘦身 + if (conf.isConfSimplifyEnable()) { + configuration.set(OTSStreamReaderConstants.VERSION, this.version); + configuration.set(CONF_SIMPLIFY_ENABLE, true); + } else { + configuration.set(OTSStreamReaderConstants.STREAM_JOB, streamJob.toJson()); + configuration.set(OTSStreamReaderConstants.ALL_SHARDS, GsonParser.toJson(allShards)); + } + configuration.set(OTSStreamReaderConstants.OWNED_SHARDS, GsonParser.listToJson(shardIds.subList(start, end))); configurations.add(configuration); } diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/OTSStreamReaderSlaveProxy.java b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/OTSStreamReaderSlaveProxy.java index 22035851b2..cdfbed2800 100644 --- a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/OTSStreamReaderSlaveProxy.java +++ b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/OTSStreamReaderSlaveProxy.java @@ -36,16 +36,18 @@ public class OTSStreamReaderSlaveProxy { private boolean findCheckpoints; // whether find checkpoint for last job, if so, we should read from checkpoint and skip nothing. private String slaveId = UUID.randomUUID().toString(); private StreamDetails streamDetails; + private boolean enableSeekIteratorByTimestamp; public void init(final OTSStreamReaderConfig otsStreamReaderConfig, StreamJob streamJob, List allShards, Set ownedShardIds) { slaveNumber.getAndIncrement(); this.config = otsStreamReaderConfig; this.ots = OTSHelper.getOTSInstance(config); this.streamJob = streamJob; - this.streamDetails = OTSHelper.getStreamDetails(ots, this.streamJob.getTableName()); + this.streamDetails = OTSHelper.getStreamDetails(ots, this.streamJob.getTableName(),config.isTimeseriesTable()); this.checkpointInfoTracker = new CheckpointTimeTracker(ots, config.getStatusTable(), this.streamJob.getStreamId()); this.checker = new OTSStreamReaderChecker(ots, config); this.allShardsMap = OTSHelper.toShardMap(allShards); + this.enableSeekIteratorByTimestamp = otsStreamReaderConfig.getEnableSeekIteratorByTimestamp(); LOG.info("SlaveId: {}, ShardIds: {}, OwnedShards: {}.", slaveId, allShards, ownedShardIds); this.ownedShards = new HashMap(); @@ -58,12 +60,12 @@ public void init(final OTSStreamReaderConfig otsStreamReaderConfig, StreamJob st } findCheckpoints = checker.checkAndSetCheckpoints(checkpointInfoTracker, allShardsMap, streamJob, shardToCheckpointMap); - if (!findCheckpoints) { - LOG.info("Checkpoint for stream '{}' in timestamp '{}' is not found.", streamJob.getStreamId(), streamJob.getStartTimeInMillis()); + if (!findCheckpoints && !enableSeekIteratorByTimestamp) { + LOG.info("Checkpoint for stream '{}' in timestamp '{}' is not found. EnableSeekIteratorByTimestamp: {}", streamJob.getStreamId(), streamJob.getStartTimeInMillis(), this.enableSeekIteratorByTimestamp); setWithNearestCheckpoint(); } - LOG.info("Find checkpoints: {}.", findCheckpoints); + LOG.info("Find checkpoints: {}, EnableSeekIteratorByTimestamp: {}", findCheckpoints, enableSeekIteratorByTimestamp); for (Map.Entry shard : ownedShards.entrySet()) { LOG.info("Shard to process, ShardInfo: [{}], StartCheckpoint: [{}].", shard.getValue(), shardToCheckpointMap.get(shard.getKey())); } diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/LocalStrings.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/LocalStrings.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/LocalStrings_en_US.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/LocalStrings_en_US.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/LocalStrings_ja_JP.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/LocalStrings_zh_CN.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/LocalStrings_zh_HK.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/LocalStrings_zh_TW.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/OTSStreamReaderConfig.java b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/OTSStreamReaderConfig.java index c89d7a3777..bef910e370 100644 --- a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/OTSStreamReaderConfig.java +++ b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/OTSStreamReaderConfig.java @@ -13,6 +13,9 @@ import java.util.List; import java.util.Map; +import static com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.OTSStreamReaderConstants.CONF_SIMPLIFY_ENABLE; +import static com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.OTSStreamReaderConstants.DEFAULT_CONF_SIMPLIFY_ENABLE_VALUE; + public class OTSStreamReaderConfig { private static final Logger LOG = LoggerFactory.getLogger(OTSStreamReaderConfig.class); @@ -33,6 +36,11 @@ public class OTSStreamReaderConfig { private static final String KEY_MODE = "mode"; private static final String KEY_COLUMN = "column"; private static final String KEY_THREAD_NUM = "threadNum"; + private static final String KEY_ENABLE_TABLE_GROUP_SUPPORT = "enableTableGroupSupport"; + + private static final String ENABLE_SEEK_SHARD_ITERATOR = "enableSeekIterator"; + + private static final String IS_TIMESERIES_TABLE = "isTimeseriesTable"; private static final int DEFAULT_MAX_RETRIES = 30; private static final long DEFAULT_SLAVE_LOOP_INTERVAL = 10 * TimeUtils.SECOND_IN_MILLIS; @@ -51,12 +59,19 @@ public class OTSStreamReaderConfig { private int threadNum = 32; private long slaveLoopInterval = DEFAULT_SLAVE_LOOP_INTERVAL; private long slaveLoggingStatusInterval = DEFAULT_SLAVE_LOGGING_STATUS_INTERVAL; + private boolean enableSeekIteratorByTimestamp; + private boolean enableTableGroupSupport; private Mode mode; private List columns; + private List columnsIsTimeseriesTags; private transient SyncClientInterface otsForTest; + private boolean confSimplifyEnable; + + private boolean isTimeseriesTable; + public String getEndpoint() { return endpoint; } @@ -129,6 +144,22 @@ public void setIsExportSequenceInfo(boolean isExportSequenceInfo) { this.isExportSequenceInfo = isExportSequenceInfo; } + public boolean isEnableTableGroupSupport() { + return enableTableGroupSupport; + } + + public void setEnableTableGroupSupport(boolean enableTableGroupSupport) { + this.enableTableGroupSupport = enableTableGroupSupport; + } + + public boolean getEnableSeekIteratorByTimestamp() { + return enableSeekIteratorByTimestamp; + } + + public void setEnableSeekIteratorByTimestamp(boolean enableSeekIteratorByTimestamp) { + this.enableSeekIteratorByTimestamp = enableSeekIteratorByTimestamp; + } + public Mode getMode() { return mode; } @@ -145,24 +176,62 @@ public void setColumns(List columns) { this.columns = columns; } + public List getColumnsIsTimeseriesTags() { + return columnsIsTimeseriesTags; + } + + public void setColumnsIsTimeseriesTags(List columnsIsTimeseriesTags) { + this.columnsIsTimeseriesTags = columnsIsTimeseriesTags; + } + + public boolean isTimeseriesTable() { + return isTimeseriesTable; + } + + public void setTimeseriesTable(boolean timeseriesTable) { + isTimeseriesTable = timeseriesTable; + } + private static void parseConfigForSingleVersionAndUpdateOnlyMode(OTSStreamReaderConfig config, Configuration param) { + try { + Boolean isTimeseriesTable = param.getBool(IS_TIMESERIES_TABLE); + if (isTimeseriesTable != null) { + config.setTimeseriesTable(isTimeseriesTable); + } else { + config.setTimeseriesTable(false); + } + } catch (RuntimeException ex) { + throw new OTSStreamReaderException("Parse timeseries stream settings fail, please check your config.", ex); + } + try { List values = param.getList(KEY_COLUMN); if (values == null) { config.setColumns(new ArrayList()); + config.setColumnsIsTimeseriesTags(new ArrayList()); return; } List columns = new ArrayList(); + List columnsIsTimeseriesTags = new ArrayList(); + Boolean isTimeseriesTable = config.isTimeseriesTable(); + for (Object item : values) { if (item instanceof Map) { String columnName = (String) ((Map) item).get("name"); columns.add(columnName); + + boolean columnsIsTimeseriesTag = false; + if (isTimeseriesTable && Boolean.parseBoolean((String) ((Map) item).getOrDefault("is_timeseries_tag", "false"))) { + columnsIsTimeseriesTag = true; + } + columnsIsTimeseriesTags.add(columnsIsTimeseriesTag); } else { throw new IllegalArgumentException("The item of column must be map object, please check your input."); } } config.setColumns(columns); + config.setColumnsIsTimeseriesTags(columnsIsTimeseriesTags); } catch (RuntimeException ex) { throw new OTSStreamReaderException("Parse column fail, please check your config.", ex); } @@ -178,56 +247,59 @@ public static OTSStreamReaderConfig load(Configuration param) { config.setDataTable(ParamChecker.checkStringAndGet(param, KEY_DATA_TABLE_NAME, true)); config.setStatusTable(ParamChecker.checkStringAndGet(param, KEY_STATUS_TABLE_NAME, true)); config.setIsExportSequenceInfo(param.getBool(KEY_IS_EXPORT_SEQUENCE_INFO, false)); + config.setEnableSeekIteratorByTimestamp(param.getBool(ENABLE_SEEK_SHARD_ITERATOR, false)); + config.setConfSimplifyEnable(param.getBool(CONF_SIMPLIFY_ENABLE, DEFAULT_CONF_SIMPLIFY_ENABLE_VALUE)); + config.setEnableTableGroupSupport(param.getBool(KEY_ENABLE_TABLE_GROUP_SUPPORT, false)); if (param.getInt(KEY_THREAD_NUM) != null) { config.setThreadNum(param.getInt(KEY_THREAD_NUM)); } if (param.getString(KEY_DATE) == null && - (param.getLong(KEY_START_TIMESTAMP_MILLIS) == null || param.getLong(KEY_END_TIMESTAMP_MILLIS) == null) && + (param.getLong(KEY_START_TIMESTAMP_MILLIS) == null || param.getLong(KEY_END_TIMESTAMP_MILLIS) == null) && (param.getLong(KEY_START_TIME_STRING) == null || param.getLong(KEY_END_TIME_STRING) == null)) { throw new OTSStreamReaderException("Must set date or time range millis or time range string, please check your config."); } - + if (param.get(KEY_DATE) != null && (param.getLong(KEY_START_TIMESTAMP_MILLIS) != null || param.getLong(KEY_END_TIMESTAMP_MILLIS) != null) && (param.getLong(KEY_START_TIME_STRING) != null || param.getLong(KEY_END_TIME_STRING) != null)) { throw new OTSStreamReaderException("Can't set date and time range millis and time range string, please check your config."); } - + if (param.get(KEY_DATE) != null && (param.getLong(KEY_START_TIMESTAMP_MILLIS) != null || param.getLong(KEY_END_TIMESTAMP_MILLIS) != null)) { throw new OTSStreamReaderException("Can't set date and time range both, please check your config."); } - + if (param.get(KEY_DATE) != null && (param.getLong(KEY_START_TIME_STRING) != null || param.getLong(KEY_END_TIME_STRING) != null)) { throw new OTSStreamReaderException("Can't set date and time range string both, please check your config."); } - - if ((param.getLong(KEY_START_TIMESTAMP_MILLIS) != null || param.getLong(KEY_END_TIMESTAMP_MILLIS) != null)&& + + if ((param.getLong(KEY_START_TIMESTAMP_MILLIS) != null || param.getLong(KEY_END_TIMESTAMP_MILLIS) != null) && (param.getLong(KEY_START_TIME_STRING) != null || param.getLong(KEY_END_TIME_STRING) != null)) { - throw new OTSStreamReaderException("Can't set time range millis and time range string both, please check your config."); + throw new OTSStreamReaderException("Can't set time range millis and time range string both, expect timestamp like '1516010400000'."); } if (param.getString(KEY_START_TIME_STRING) != null && param.getString(KEY_END_TIME_STRING) != null) { - String startTime=ParamChecker.checkStringAndGet(param, KEY_START_TIME_STRING, true); - String endTime=ParamChecker.checkStringAndGet(param, KEY_END_TIME_STRING, true); + String startTime = ParamChecker.checkStringAndGet(param, KEY_START_TIME_STRING, true); + String endTime = ParamChecker.checkStringAndGet(param, KEY_END_TIME_STRING, true); try { long startTimestampMillis = TimeUtils.parseTimeStringToTimestampMillis(startTime); config.setStartTimestampMillis(startTimestampMillis); } catch (Exception ex) { - throw new OTSStreamReaderException("Can't parse startTimeString: " + startTime); + throw new OTSStreamReaderException("Can't parse startTimeString: " + startTime + ", expect format date like '201801151612'."); } try { long endTimestampMillis = TimeUtils.parseTimeStringToTimestampMillis(endTime); config.setEndTimestampMillis(endTimestampMillis); } catch (Exception ex) { - throw new OTSStreamReaderException("Can't parse startTimeString: " + startTime); - } - - }else if (param.getString(KEY_DATE) == null) { + throw new OTSStreamReaderException("Can't parse endTimeString: " + endTime + ", expect format date like '201801151612'."); + } + + } else if (param.getString(KEY_DATE) == null) { config.setStartTimestampMillis(param.getLong(KEY_START_TIMESTAMP_MILLIS)); config.setEndTimestampMillis(param.getLong(KEY_END_TIMESTAMP_MILLIS)); } else { @@ -241,8 +313,6 @@ public static OTSStreamReaderConfig load(Configuration param) { } } - - if (config.getStartTimestampMillis() >= config.getEndTimestampMillis()) { throw new OTSStreamReaderException("EndTimestamp must be larger than startTimestamp."); @@ -262,15 +332,21 @@ public static OTSStreamReaderConfig load(Configuration param) { config.setMode(Mode.MULTI_VERSION); List values = param.getList(KEY_COLUMN); if (values != null) { - throw new OTSStreamReaderException("The multi version mode doesn't support setting columns."); + LOG.warn("The multi version mode doesn't support setting columns, column config will ignore."); + } + Boolean isTimeseriesTable = param.getBool(IS_TIMESERIES_TABLE); + if (isTimeseriesTable != null) { + LOG.warn("The multi version mode doesn't support setting Timeseries stream, stream config will ignore."); } } - LOG.info("endpoint: {}, accessId: {}, accessKey: {}, instanceName: {}, dataTableName: {}, statusTableName: {}," + - " isExportSequenceInfo: {}, startTimestampMillis: {}, endTimestampMillis:{}, maxRetries:{}.", config.getEndpoint(), + LOG.info("endpoint: {}, accessKeyId: {}, accessKeySecret: {}, instanceName: {}, dataTableName: {}, statusTableName: {}," + + " isExportSequenceInfo: {}, startTimestampMillis: {}, endTimestampMillis:{}, maxRetries:{}, enableSeekIteratorByTimestamp: {}, " + + "confSimplifyEnable: {}, isTimeseriesTable: {}.", config.getEndpoint(), config.getAccessId(), config.getAccessKey(), config.getInstanceName(), config.getDataTable(), config.getStatusTable(), config.isExportSequenceInfo(), config.getStartTimestampMillis(), - config.getEndTimestampMillis(), config.getMaxRetries()); + config.getEndTimestampMillis(), config.getMaxRetries(), config.getEnableSeekIteratorByTimestamp(), + config.isConfSimplifyEnable(), config.isTimeseriesTable()); return config; } @@ -282,7 +358,6 @@ public static OTSStreamReaderConfig load(Configuration param) { public SyncClientInterface getOtsForTest() { return otsForTest; } - /** * test use * @param otsForTest @@ -290,36 +365,36 @@ public SyncClientInterface getOtsForTest() { public void setOtsForTest(SyncClientInterface otsForTest) { this.otsForTest = otsForTest; } - public int getMaxRetries() { return maxRetries; } - public void setMaxRetries(int maxRetries) { this.maxRetries = maxRetries; } - public int getThreadNum() { return threadNum; } - public void setSlaveLoopInterval(long slaveLoopInterval) { this.slaveLoopInterval = slaveLoopInterval; } - public void setSlaveLoggingStatusInterval(long slaveLoggingStatusInterval) { this.slaveLoggingStatusInterval = slaveLoggingStatusInterval; } - public long getSlaveLoopInterval() { return slaveLoopInterval; } - public long getSlaveLoggingStatusInterval() { return slaveLoggingStatusInterval; } - public void setThreadNum(int threadNum) { this.threadNum = threadNum; } + + public boolean isConfSimplifyEnable() { + return confSimplifyEnable; + } + + public void setConfSimplifyEnable(boolean confSimplifyEnable) { + this.confSimplifyEnable = confSimplifyEnable; + } } diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/OTSStreamReaderConstants.java b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/OTSStreamReaderConstants.java index 19db148a71..c95fdf2c2d 100644 --- a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/OTSStreamReaderConstants.java +++ b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/config/OTSStreamReaderConstants.java @@ -21,7 +21,20 @@ public class OTSStreamReaderConstants { public static final String STREAM_JOB = "STREAM_JOB"; public static final String OWNED_SHARDS = "OWNED_SHARDS"; public static final String ALL_SHARDS = "ALL_SHARDS"; + public static final String VERSION = "STREAM_VERSION"; + /** + * 是否开启OTS分布式模式降低Job Split阶段切分的Task Conf大小启动优化, + * 新增该参数的目的是为了保证DataX灰度过程,避免因为OTS分布式任务运行部分子进程运行在老版本、部分运行在新版本导致任务失败问题, + * 当DataX版本集群粒度已全量升级到新版本以后,再开启该参数为"true",默认值是"false" + */ + public static final String CONF_SIMPLIFY_ENABLE = "confSimplifyEnable"; + + public static final Integer RETRY_TIMES = 3; + + public static final Long DEFAULT_SLEEP_TIME_IN_MILLS = 500l; + + public static final boolean DEFAULT_CONF_SIMPLIFY_ENABLE_VALUE = false; static { String beforeOffsetMillis = System.getProperty("BEFORE_OFFSET_TIME_MILLIS"); diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/LocalStrings.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/LocalStrings.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/LocalStrings_en_US.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/LocalStrings_en_US.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/LocalStrings_ja_JP.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/LocalStrings_zh_CN.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/LocalStrings_zh_HK.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/LocalStrings_zh_TW.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/OTSStreamReaderChecker.java b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/OTSStreamReaderChecker.java index 086d0159a0..560dcb7cb2 100644 --- a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/OTSStreamReaderChecker.java +++ b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/OTSStreamReaderChecker.java @@ -40,11 +40,11 @@ public OTSStreamReaderChecker(SyncClientInterface ots, OTSStreamReaderConfig con * 为了避免时间误差影响,允许导出的范围为: [now - expirationTime + beforeOffset, now - afterOffset] */ public void checkStreamEnabledAndTimeRangeOK() { - boolean exists = OTSHelper.checkTableExists(ots, config.getDataTable()); + boolean exists = OTSHelper.checkTableExists(ots, config.getDataTable(), config.isTimeseriesTable()); if (!exists) { throw new OTSStreamReaderException("The data table is not exist."); } - StreamDetails streamDetails = OTSHelper.getStreamDetails(ots, config.getDataTable()); + StreamDetails streamDetails = OTSHelper.getStreamDetails(ots, config.getDataTable(), config.isTimeseriesTable()); if (streamDetails == null || !streamDetails.isEnableStream()) { throw new OTSStreamReaderException("The stream of data table is not enabled."); } @@ -81,7 +81,7 @@ private void checkTableMetaOfStatusTable(TableMeta tableMeta) { * 检查statusTable是否存在,如果不存在就创建statusTable,并等待表ready。 */ public void checkAndCreateStatusTableIfNotExist() { - boolean tableExist = OTSHelper.checkTableExists(ots, config.getStatusTable()); + boolean tableExist = OTSHelper.checkTableExists(ots, config.getStatusTable(), false); if (tableExist) { DescribeTableResponse describeTableResult = OTSHelper.describeTable(ots, config.getStatusTable()); checkTableMetaOfStatusTable(describeTableResult.getTableMeta()); @@ -135,23 +135,6 @@ public boolean checkAndSetCheckpoints( } } - // 检查是否有丢失的shard - for (Map.Entry entry : allShardsMap.entrySet()) { - StreamShard shard = entry.getValue(); - String parentId = shard.getParentId(); - // shard不在本次任务中,且shard也不在上一次任务中 - if (parentId != null && !allShardsMap.containsKey(parentId) && !allCheckpoints.containsKey(parentId)) { - LOG.error("Shard is lost: {}.", shard); - throw new OTSStreamReaderException("Can't find checkpoint for shard: " + parentId); - } - - parentId = shard.getParentSiblingId(); - if (parentId != null && !allShardsMap.containsKey(parentId) && !allCheckpoints.containsKey(parentId)) { - LOG.error("Shard is lost: {}.", shard); - throw new OTSStreamReaderException("Can't find checkpoint for shard: " + parentId); - } - } - return true; } } diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/RecordProcessor.java b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/RecordProcessor.java index ba17bd9cc1..feb9972275 100644 --- a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/RecordProcessor.java +++ b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/RecordProcessor.java @@ -1,5 +1,6 @@ package com.alibaba.datax.plugin.reader.otsstreamreader.internal.core; +import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.plugin.RecordSender; import com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.Mode; import com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.OTSStreamReaderConfig; @@ -48,6 +49,9 @@ public class RecordProcessor implements Runnable { private AtomicBoolean stop; private AtomicLong sendRecordCount; + //enable seek shardIterator by timestamp + private boolean enableSeekShardIteratorByTimestamp; + public enum State { READY, // initialized but not start RUNNING, // start to read and process records @@ -78,6 +82,7 @@ public RecordProcessor(SyncClientInterface ots, this.recordSender = recordSender; this.isExportSequenceInfo = config.isExportSequenceInfo(); this.lastRecordCheckpointTime = 0; + this.enableSeekShardIteratorByTimestamp = config.getEnableSeekIteratorByTimestamp(); // set init state startTime = 0; @@ -107,22 +112,31 @@ public void initialize() { if (readerConfig.getMode().equals(Mode.MULTI_VERSION)) { this.otsStreamRecordSender = new MultiVerModeRecordSender(recordSender, shard.getShardId(), isExportSequenceInfo); } else if (readerConfig.getMode().equals(Mode.SINGLE_VERSION_AND_UPDATE_ONLY)) { - this.otsStreamRecordSender = new SingleVerAndUpOnlyModeRecordSender(recordSender, shard.getShardId(), isExportSequenceInfo, readerConfig.getColumns()); + this.otsStreamRecordSender = new SingleVerAndUpOnlyModeRecordSender(recordSender, shard.getShardId(), isExportSequenceInfo, readerConfig.getColumns(), readerConfig.getColumnsIsTimeseriesTags()); } else { throw new OTSStreamReaderException("Internal Error. Unhandled Mode: " + readerConfig.getMode()); } if (startCheckpoint.getCheckpoint().equals(CheckpointPosition.TRIM_HORIZON)) { lastShardIterator = null; - nextShardIterator = ots.getShardIterator(new GetShardIteratorRequest(stream.getStreamId(), shard.getShardId())).getShardIterator(); + if (enableSeekShardIteratorByTimestamp) { + long beginTimeStamp = startTimestampMillis - 10 * 60 * 1000; + if (beginTimeStamp > 0) { + nextShardIterator = getShardIteratorWithBeginTime((startTimestampMillis - 10 * 60 * 1000) * 1000); + } else { + nextShardIterator = ots.getShardIterator(new GetShardIteratorRequest(stream.getStreamId(), shard.getShardId())).getShardIterator(); + } + } else { + nextShardIterator = ots.getShardIterator(new GetShardIteratorRequest(stream.getStreamId(), shard.getShardId())).getShardIterator(); + } skipCount = startCheckpoint.getSkipCount(); } else { lastShardIterator = null; nextShardIterator = startCheckpoint.getCheckpoint(); skipCount = startCheckpoint.getSkipCount(); } - LOG.info("Initialize record processor. Mode: {}, StartCheckpoint: [{}], ShardId: {}, ShardIterator: {}, SkipCount: {}.", - readerConfig.getMode(), startCheckpoint, shard.getShardId(), nextShardIterator, skipCount); + LOG.info("Initialize record processor. Mode: {}, StartCheckpoint: [{}], ShardId: {}, ShardIterator: {}, SkipCount: {}, enableSeekShardIteratorByTimestamp: {}, startTimestamp: {}.", + readerConfig.getMode(), startCheckpoint, shard.getShardId(), nextShardIterator, skipCount, enableSeekShardIteratorByTimestamp, startTimestampMillis); } private long getTimestamp(StreamRecord record) { @@ -181,15 +195,32 @@ public void stop() { * * @param records * @param nextShardIterator + * @param mayMoreRecord * @return */ - boolean process(List records, String nextShardIterator) { + boolean process(List records, String nextShardIterator, Boolean mayMoreRecord) { if (records.isEmpty() && nextShardIterator != null) { - LOG.info("ProcessFinished: No more data in shard, shardId: {}.", shard.getShardId()); - ShardCheckpoint checkpoint = new ShardCheckpoint(shard.getShardId(), stream.getVersion(), nextShardIterator, 0); - checkpointTimeTracker.writeCheckpoint(endTimestampMillis, checkpoint, sendRecordCount.get()); - checkpointTimeTracker.setShardTimeCheckpoint(shard.getShardId(), endTimestampMillis, nextShardIterator); - return true; + // 没有读到更多数据 + if (!readerConfig.isEnableTableGroupSupport()) { + LOG.info("ProcessFinished: No more data in shard, shardId: {}.", shard.getShardId()); + ShardCheckpoint checkpoint = new ShardCheckpoint(shard.getShardId(), stream.getVersion(), nextShardIterator, 0); + checkpointTimeTracker.writeCheckpoint(endTimestampMillis, checkpoint, sendRecordCount.get()); + checkpointTimeTracker.setShardTimeCheckpoint(shard.getShardId(), endTimestampMillis, nextShardIterator); + return true; + } else { + if (mayMoreRecord == null) { + LOG.error("mayMoreRecord can not be null when tablegroup is true"); + throw DataXException.asDataXException("mayMoreRecord can not be null when tablegroup is true"); + } else if (mayMoreRecord) { + return false; + } else { + LOG.info("ProcessFinished: No more data in shard, shardId: {}.", shard.getShardId()); + ShardCheckpoint checkpoint = new ShardCheckpoint(shard.getShardId(), stream.getVersion(), nextShardIterator, 0); + checkpointTimeTracker.writeCheckpoint(endTimestampMillis, checkpoint, sendRecordCount.get()); + checkpointTimeTracker.setShardTimeCheckpoint(shard.getShardId(), endTimestampMillis, nextShardIterator); + return true; + } + } } int size = records.size(); @@ -212,17 +243,19 @@ boolean process(List records, String nextShardIterator) { continue; } shouldSkip = false; - if (skipCount > 0) { - LOG.debug("Skip record. Timestamp: {}, SkipCount: {}.", timestamp, skipCount); - skipCount -= 1; - continue; - } LOG.debug("Send record. Timestamp: {}.", timestamp); sendRecord(records.get(i)); } else { LOG.info("ProcessFinished: Record in shard reach boundary of endTime, shardId: {}. Timestamp: {}, EndTime: {}", shard.getShardId(), timestamp, endTimestampMillis); - ShardCheckpoint checkpoint = new ShardCheckpoint(shard.getShardId(), stream.getVersion(), lastShardIterator, i); + + String newIterator = lastShardIterator; + if (i > 0) { + newIterator = GetStreamRecordWithLimitRowCount(lastShardIterator, i); + } + + ShardCheckpoint checkpoint = new ShardCheckpoint(shard.getShardId(), stream.getVersion(), newIterator, 0); + checkpointTimeTracker.writeCheckpoint(endTimestampMillis, checkpoint, sendRecordCount.get()); return true; } @@ -240,14 +273,35 @@ boolean process(List records, String nextShardIterator) { private boolean readAndProcessRecords() { LOG.debug("Read and process records. ShardId: {}, ShardIterator: {}.", shard.getShardId(), nextShardIterator); + if (enableSeekShardIteratorByTimestamp && nextShardIterator == null) { + LOG.info("ProcessFinished: Shard has reach to end, shardId: {}.", shard.getShardId()); + ShardCheckpoint checkpoint = new ShardCheckpoint(shard.getShardId(), stream.getVersion(), CheckpointPosition.SHARD_END, 0); + checkpointTimeTracker.writeCheckpoint(endTimestampMillis, checkpoint, sendRecordCount.get()); + return true; + } + GetStreamRecordRequest request = new GetStreamRecordRequest(nextShardIterator); + if (readerConfig.isEnableTableGroupSupport()) { + request.setTableName(stream.getTableName()); + } + if (readerConfig.isTimeseriesTable()){ + request.setParseInTimeseriesDataFormat(true); + } GetStreamRecordResponse response = ots.getStreamRecord(request); lastShardIterator = nextShardIterator; nextShardIterator = response.getNextShardIterator(); - return processRecords(response.getRecords(), nextShardIterator); + return processRecords(response.getRecords(), nextShardIterator, response.getMayMoreRecord()); + } + + private String GetStreamRecordWithLimitRowCount(String beginIterator, int expectedRowCount) { + LOG.debug("Read and process records. ShardId: {}, ShardIterator: {}, expectedRowCount: {}..", shard.getShardId(), beginIterator, expectedRowCount); + GetStreamRecordRequest request = new GetStreamRecordRequest(beginIterator); + request.setLimit(expectedRowCount); + GetStreamRecordResponse response = ots.getStreamRecord(request); + return response.getNextShardIterator(); } - public boolean processRecords(List records, String nextShardIterator) { + public boolean processRecords(List records, String nextShardIterator, Boolean mayMoreRecord) { long startTime = System.currentTimeMillis(); if (records.isEmpty()) { @@ -256,7 +310,7 @@ public boolean processRecords(List records, String nextShardIterat LOG.debug("StartProcessRecords: size: {}, recordTime: {}.", records.size(), getTimestamp(records.get(0))); } - if (process(records, nextShardIterator)) { + if (process(records, nextShardIterator, mayMoreRecord)) { return true; } @@ -264,4 +318,27 @@ public boolean processRecords(List records, String nextShardIterat shard.getShardId(), System.currentTimeMillis() - startTime, records.size(), nextShardIterator); return false; } -} + + private String getShardIteratorWithBeginTime(long timestamp){ + LOG.info("Begin to seek shard iterator with timestamp, shardId: {}, timestamp: {}.", shard.getShardId(), timestamp); + GetShardIteratorRequest getShardIteratorRequest = new GetShardIteratorRequest(stream.getStreamId(), shard.getShardId()); + getShardIteratorRequest.setTimestamp(timestamp); + + GetShardIteratorResponse response = ots.getShardIterator(getShardIteratorRequest); + String nextToken = response.getNextToken(); + + if (nextToken == null) { + return response.getShardIterator(); + } + + while (nextToken != null) { + getShardIteratorRequest = new GetShardIteratorRequest(stream.getStreamId(), shard.getShardId()); + getShardIteratorRequest.setTimestamp(timestamp); + getShardIteratorRequest.setToken(nextToken); + + response = ots.getShardIterator(getShardIteratorRequest); + nextToken = response.getNextToken(); + } + return response.getShardIterator(); + } +} \ No newline at end of file diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/SingleVerAndUpOnlyModeRecordSender.java b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/SingleVerAndUpOnlyModeRecordSender.java index 1cc32bad08..d962af7619 100644 --- a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/SingleVerAndUpOnlyModeRecordSender.java +++ b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/core/SingleVerAndUpOnlyModeRecordSender.java @@ -5,6 +5,7 @@ import com.alibaba.datax.common.plugin.RecordSender; import com.alibaba.datax.plugin.reader.otsstreamreader.internal.OTSStreamReaderException; import com.alibaba.datax.plugin.reader.otsstreamreader.internal.utils.ColumnValueTransformHelper; +import com.alicloud.openservices.tablestore.core.protocol.timeseries.TimeseriesResponseFactory; import com.alicloud.openservices.tablestore.model.*; import java.util.HashMap; @@ -17,21 +18,23 @@ * | pk1 | pk2 | col1 | col2 | col3 | sequence id | * | --- | --- | ---- | ---- | ---- | ----------- | * | a | b | c1 | null | null | 001 | - * + *

    * 注意:删除整行,删除某列(某个版本或所有),这些增量信息都会被忽略。 */ public class SingleVerAndUpOnlyModeRecordSender implements IStreamRecordSender { private final RecordSender dataxRecordSender; - private String shardId; private final boolean isExportSequenceInfo; + private String shardId; private List columnNames; + private List columnsIsTimeseriesTags; - public SingleVerAndUpOnlyModeRecordSender(RecordSender dataxRecordSender, String shardId, boolean isExportSequenceInfo, List columnNames) { + public SingleVerAndUpOnlyModeRecordSender(RecordSender dataxRecordSender, String shardId, boolean isExportSequenceInfo, List columnNames, List columnsIsTimeseriesTags) { this.dataxRecordSender = dataxRecordSender; this.shardId = shardId; this.isExportSequenceInfo = isExportSequenceInfo; this.columnNames = columnNames; + this.columnsIsTimeseriesTags = columnsIsTimeseriesTags; } @Override @@ -57,25 +60,49 @@ private void sendToDatax(PrimaryKey primaryKey, List columns, Stri map.put(pkCol.getName(), pkCol.getValue()); } + /** + * 将时序数据中tags字段的字符串转化为Map + */ + Map tagsMap = new HashMap<>(); + if (columnsIsTimeseriesTags != null && columnsIsTimeseriesTags.contains(true)) { + try{ + tagsMap = TimeseriesResponseFactory.parseTagsOrAttrs(String.valueOf(map.get("_tags"))); + } + catch (Exception ex){ + throw new OTSStreamReaderException("Parse \"_tags\" fail, please check your config.", ex); + } + + } + for (RecordColumn recordColumn : columns) { if (recordColumn.getColumnType().equals(RecordColumn.ColumnType.PUT)) { map.put(recordColumn.getColumn().getName(), recordColumn.getColumn().getValue()); } } - boolean findColumn = false; + boolean findColumn = false; - for (String colName : columnNames) { - Object value = map.get(colName); - if (value != null) { - findColumn = true; - if (value instanceof ColumnValue) { - line.addColumn(ColumnValueTransformHelper.otsColumnValueToDataxColumn((ColumnValue) value)); + for (int i = 0; i < columnNames.size(); i++) { + if (columnsIsTimeseriesTags != null && columnsIsTimeseriesTags.get(i)) { + String value = tagsMap.get(columnNames.get(i)); + if (value != null) { + findColumn = true; + line.addColumn(new StringColumn(value)); } else { - line.addColumn(ColumnValueTransformHelper.otsPrimaryKeyValueToDataxColumn((PrimaryKeyValue) value)); + line.addColumn(new StringColumn(null)); } } else { - line.addColumn(new StringColumn(null)); + Object value = map.get(columnNames.get(i)); + if (value != null) { + findColumn = true; + if (value instanceof ColumnValue) { + line.addColumn(ColumnValueTransformHelper.otsColumnValueToDataxColumn((ColumnValue) value)); + } else { + line.addColumn(ColumnValueTransformHelper.otsPrimaryKeyValueToDataxColumn((PrimaryKeyValue) value)); + } + } else { + line.addColumn(new StringColumn(null)); + } } } diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/LocalStrings.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/LocalStrings.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/LocalStrings_en_US.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/LocalStrings_en_US.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/LocalStrings_ja_JP.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/LocalStrings_zh_CN.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/LocalStrings_zh_HK.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/LocalStrings_zh_TW.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/OTSStreamJobShard.java b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/OTSStreamJobShard.java new file mode 100644 index 0000000000..d5d5f97190 --- /dev/null +++ b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/model/OTSStreamJobShard.java @@ -0,0 +1,42 @@ +package com.alibaba.datax.plugin.reader.otsstreamreader.internal.model; + +import com.alicloud.openservices.tablestore.model.StreamShard; + +import java.util.List; + +/** + * OTS streamJob & allShards model + * + * @author mingya.wmy (云时) + */ +public class OTSStreamJobShard { + + private StreamJob streamJob; + + private List allShards; + + public OTSStreamJobShard() { + } + + public OTSStreamJobShard(StreamJob streamJob, List allShards) { + this.streamJob = streamJob; + this.allShards = allShards; + } + + public StreamJob getStreamJob() { + return streamJob; + } + + public void setStreamJob(StreamJob streamJob) { + this.streamJob = streamJob; + } + + public List getAllShards() { + return allShards; + } + + public void setAllShards(List allShards) { + this.allShards = allShards; + } + +} diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/LocalStrings.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/LocalStrings.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/LocalStrings_en_US.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/LocalStrings_en_US.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/LocalStrings_ja_JP.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/LocalStrings_zh_CN.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/LocalStrings_zh_HK.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/LocalStrings_zh_TW.properties b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/OTSHelper.java b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/OTSHelper.java index 79b6c1d700..24ea732aa1 100644 --- a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/OTSHelper.java +++ b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/OTSHelper.java @@ -2,11 +2,19 @@ import com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.OTSRetryStrategyForStreamReader; import com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.OTSStreamReaderConfig; +import com.alicloud.openservices.tablestore.ClientConfiguration; +import com.alicloud.openservices.tablestore.SyncClient; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.TableStoreException; import com.alicloud.openservices.tablestore.model.*; -import com.alicloud.openservices.tablestore.*; +import com.alicloud.openservices.tablestore.model.timeseries.DescribeTimeseriesTableRequest; +import com.alicloud.openservices.tablestore.model.timeseries.DescribeTimeseriesTableResponse; import com.aliyun.openservices.ots.internal.streamclient.utils.TimeUtils; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class OTSHelper { @@ -35,14 +43,55 @@ public static SyncClientInterface getOTSInstance(OTSStreamReaderConfig config) { return ots; } + public static DescribeStreamResponse getStreamResponse(SyncClientInterface ots, String tableName, boolean isTimeseriesTable) { + /** + * 对于时序表,需要通过listStream&describeStream两次交互,获取streamID与expirationTime + */ + ListStreamRequest request = new ListStreamRequest(tableName); + ListStreamResponse response = ots.listStream(request); + String streamID = null; + for (Stream stream : response.getStreams()) { + if (stream.getTableName().equals(tableName)) { + streamID = stream.getStreamId(); + break; + } + } + if (streamID == null) { + throw new RuntimeException(String.format("Did not get any stream from table : (\"%s\") .", tableName)); + } + DescribeStreamRequest describeStreamRequest = new DescribeStreamRequest(streamID); + if (isTimeseriesTable) { + describeStreamRequest.setSupportTimeseriesTable(true); + } + DescribeStreamResponse result = ots.describeStream(describeStreamRequest); + if(isTimeseriesTable && !result.isTimeseriesDataTable()){ + throw new RuntimeException(String.format("The table [%s] is not timeseries data table, please remove the config: {isTimeseriesTable : true}.", tableName)); + } + return result; + } + public static StreamDetails getStreamDetails(SyncClientInterface ots, String tableName) { DescribeTableRequest describeTableRequest = new DescribeTableRequest(tableName); DescribeTableResponse result = ots.describeTable(describeTableRequest); return result.getStreamDetails(); } - public static List getOrderedShardList(SyncClientInterface ots, String streamId) { + public static StreamDetails getStreamDetails(SyncClientInterface ots, String tableName, boolean isTimeseriesTable) { + if (!isTimeseriesTable) { + return getStreamDetails(ots, tableName); + } else { + DescribeStreamResponse result = getStreamResponse(ots, tableName, isTimeseriesTable); + //TODO:时序表无法直接获取StreamDetails,需要手动构建。 + // 其中lastEnableTime字段暂时无法获取 + return new StreamDetails(true, result.getStreamId(), result.getExpirationTime(), 0); + } + } + + public static List getOrderedShardList(SyncClientInterface ots, String streamId, boolean isTimeseriesTable) { DescribeStreamRequest describeStreamRequest = new DescribeStreamRequest(streamId); + if (isTimeseriesTable) { + describeStreamRequest.setSupportTimeseriesTable(true); + } DescribeStreamResponse describeStreamResult = ots.describeStream(describeStreamRequest); List shardList = new ArrayList(); shardList.addAll(describeStreamResult.getShards()); @@ -54,10 +103,15 @@ public static List getOrderedShardList(SyncClientInterface ots, Str return shardList; } - public static boolean checkTableExists(SyncClientInterface ots, String tableName) { + public static boolean checkTableExists(SyncClientInterface ots, String tableName, boolean isTimeseriesTable) { boolean exist = false; try { - describeTable(ots, tableName); + if (isTimeseriesTable) { + describeTimeseriesTable(ots, tableName); + } else { + describeTable(ots, tableName); + } + exist = true; } catch (TableStoreException ex) { if (!ex.getErrorCode().equals(OBJECT_NOT_EXIST)) { @@ -71,6 +125,10 @@ public static DescribeTableResponse describeTable(SyncClientInterface ots, Strin return ots.describeTable(new DescribeTableRequest(tableName)); } + public static DescribeTimeseriesTableResponse describeTimeseriesTable(SyncClientInterface ots, String tableName) { + return ((SyncClient) ots).asTimeseriesClient().describeTimeseriesTable(new DescribeTimeseriesTableRequest(tableName)); + } + public static void createTable(SyncClientInterface ots, TableMeta tableMeta, TableOptions tableOptions) { CreateTableRequest request = new CreateTableRequest(tableMeta, tableOptions, new ReservedThroughput(CREATE_TABLE_READ_CU, CREATE_TABLE_WRITE_CU)); @@ -109,11 +167,12 @@ public static boolean waitUntilTableReady(SyncClientInterface ots, String tableN return false; } - public static Map toShardMap(List orderedShardList) { + public static Map toShardMap(List orderedShardList) { Map shardsMap = new HashMap(); for (StreamShard shard : orderedShardList) { shardsMap.put(shard.getShardId(), shard); } return shardsMap; } + } diff --git a/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/OTSStreamJobShardUtil.java b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/OTSStreamJobShardUtil.java new file mode 100644 index 0000000000..a062b44fe0 --- /dev/null +++ b/otsstreamreader/src/main/java/com/alibaba/datax/plugin/reader/otsstreamreader/internal/utils/OTSStreamJobShardUtil.java @@ -0,0 +1,105 @@ +package com.alibaba.datax.plugin.reader.otsstreamreader.internal.utils; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.RetryUtil; +import com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.OTSStreamReaderConfig; +import com.alibaba.datax.plugin.reader.otsstreamreader.internal.core.CheckpointTimeTracker; +import com.alibaba.datax.plugin.reader.otsstreamreader.internal.model.OTSStreamJobShard; +import com.alibaba.datax.plugin.reader.otsstreamreader.internal.model.StreamJob; +import com.alibaba.fastjson.JSON; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.model.StreamShard; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +import static com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.OTSStreamReaderConstants.DEFAULT_SLEEP_TIME_IN_MILLS; +import static com.alibaba.datax.plugin.reader.otsstreamreader.internal.config.OTSStreamReaderConstants.RETRY_TIMES; + +/** + * @author mingya.wmy (云时) + */ +public class OTSStreamJobShardUtil { + + private static OTSStreamJobShard otsStreamJobShard = null; + + /** + * 获取全局OTS StreamJob 和 allShards ,懒汉单例模式,减少对OTS接口交互频次 + * 备注:config 和 version 所有TASK 均一样 + * + * @param config + * @param version + * @return + * @throws Exception + */ + public static OTSStreamJobShard getOTSStreamJobShard(OTSStreamReaderConfig config, String version) throws Exception { + if (otsStreamJobShard == null) { + synchronized (OTSHelper.class) { + if (otsStreamJobShard == null) { + otsStreamJobShard = RetryUtil.executeWithRetry(new Callable() { + @Override + public OTSStreamJobShard call() throws Exception { + return getOTSStreamJobShardByOtsClient(config, version); + } + }, RETRY_TIMES, DEFAULT_SLEEP_TIME_IN_MILLS, true); + } + } + } + + return otsStreamJobShard; + } + + /** + * 获取OTS StreamJob 和 allShards + * + * @param config OTS CONF + * @param version OTS STREAM VERSION + * @return + */ + private static OTSStreamJobShard getOTSStreamJobShardByOtsClient(OTSStreamReaderConfig config, String version) { + // Init ots,Task阶段从OTS中获取 allShards 和 streamJob + SyncClientInterface ots = null; + try { + ots = OTSHelper.getOTSInstance(config); + String streamId = OTSHelper.getStreamResponse(ots, config.getDataTable(), config.isTimeseriesTable()).getStreamId(); + List allShards = OTSHelper.getOrderedShardList(ots, streamId, config.isTimeseriesTable()); + + CheckpointTimeTracker checkpointInfoTracker = new CheckpointTimeTracker(ots, config.getStatusTable(), streamId); + StreamJob streamJobFromCPT = checkpointInfoTracker.readStreamJob(config.getEndTimestampMillis()); + if (!StringUtils.equals(streamJobFromCPT.getVersion(), version)) { + throw new RuntimeException(String.format("streamJob version (\"%s\") is not equal to \"%s\", streamJob: %s", + streamJobFromCPT.getVersion(), version, JSON.toJSONString(streamJobFromCPT))); + } + + Set shardIdSetsFromTracker = streamJobFromCPT.getShardIds(); + + if (shardIdSetsFromTracker == null || shardIdSetsFromTracker.isEmpty()) { + throw new RuntimeException(String.format("StreamJob [statusTable=%s, streamId=%s] shardIds can't be null!", + config.getStatusTable(), streamId)); + } + + Set currentAllStreamShardIdSets = allShards.stream().map(streamShard -> streamShard.getShardId()).collect(Collectors.toSet()); + + for (String shardId: shardIdSetsFromTracker) { + if (!currentAllStreamShardIdSets.contains(shardId)) { + allShards.add(new StreamShard(shardId)); + } + } + + StreamJob streamJob = new StreamJob(config.getDataTable(), streamId, version, shardIdSetsFromTracker, + config.getStartTimestampMillis(), config.getEndTimestampMillis()); + + return new OTSStreamJobShard(streamJob, allShards); + } catch (Throwable e) { + throw new DataXException(String.format("Get ots shards error: %s", e.getMessage())); + } finally { + if (ots != null) { + ots.shutdown(); + } + } + } + +} diff --git a/otsstreamreader/src/main/resources/plugin.json b/otsstreamreader/src/main/resources/plugin.json index 9a70a47a46..57071d6f14 100644 --- a/otsstreamreader/src/main/resources/plugin.json +++ b/otsstreamreader/src/main/resources/plugin.json @@ -2,5 +2,5 @@ "name": "otsstreamreader", "class": "com.alibaba.datax.plugin.reader.otsstreamreader.internal.OTSStreamReader", "description": "", - "developer": "zhaofeng.zhou@alibaba-inc.com" + "developer": "alibaba" } diff --git a/otswriter/doc/otswriter.md b/otswriter/doc/otswriter.md index cbfaf2a865..43697feb9c 100644 --- a/otswriter/doc/otswriter.md +++ b/otswriter/doc/otswriter.md @@ -7,13 +7,8 @@ ___ ## 1 快速介绍 -OTSWriter插件实现了向OTS写入数据,目前支持三种写入方式: +OTSWriter插件实现了向OTS写入数据,目前支持了多版本数据的写入、主键自增列的写入等功能。 -* PutRow,对应于OTS API PutRow,插入数据到指定的行,如果该行不存在,则新增一行;若该行存在,则覆盖原有行。 - -* UpdateRow,对应于OTS API UpdateRow,更新指定行的数据,如果该行不存在,则新增一行;若该行存在,则根据请求的内容在这一行中新增、修改或者删除指定列的值。 - -* DeleteRow,对应于OTS API DeleteRow,删除指定行的数据。 OTS是构建在阿里云飞天分布式系统之上的 NoSQL数据库服务,提供海量结构化数据的存储和实时访问。OTS 以实例和表的形式组织数据,通过数据分片和负载均衡技术,实现规模上的无缝扩展。 @@ -28,6 +23,7 @@ OTS是构建在阿里云飞天分布式系统之上的 NoSQL数据库服务, * 配置一个写入OTS作业: +`normal模式` ``` { "job": { @@ -37,48 +33,53 @@ OTS是构建在阿里云飞天分布式系统之上的 NoSQL数据库服务, { "reader": {}, "writer": { - "name": "otswriter", + "name": "otswriter", "parameter": { "endpoint":"", "accessId":"", "accessKey":"", "instanceName":"", - // 导出数据表的表名 "table":"", - - // Writer支持不同类型之间进行相互转换 - // 如下类型转换不支持: - // ================================ - // int -> binary - // double -> bool, binary - // bool -> binary - // bytes -> int, double, bool - // ================================ - + + // 可选 multiVersion||normal,可选配置,默认normal + "mode":"normal", + + //newVersion定义是否使用新版本插件 可选值:true || false + "newVersion":"true", + + //是否允许向包含主键自增列的ots表中写入数据 + //与mode:multiVersion的多版本模式不兼容 + "enableAutoIncrement":"true", + // 需要导入的PK列名,区分大小写 - // 类型支持:STRING,INT + // 类型支持:STRING,INT,BINARY + // 必选 // 1. 支持类型转换,注意类型转换时的精度丢失 // 2. 顺序不要求和表的Meta一致 - "primaryKey" : [ - {"name":"pk1", "type":"string"}, - {"name":"pk2", "type":"int"} + // 3. name全局唯一 + "primaryKey":[ + "userid", + "groupid" ], - + // 需要导入的列名,区分大小写 // 类型支持STRING,INT,DOUBLE,BOOL和BINARY - "column" : [ - {"name":"col2", "type":"INT"}, - {"name":"col3", "type":"STRING"}, - {"name":"col4", "type":"STRING"}, - {"name":"col5", "type":"BINARY"}, - {"name":"col6", "type":"DOUBLE"} + // 必选 + // 1.name全局唯一 + "column":[ + {"name":"addr", "type":"string"}, + {"name":"height", "type":"int"} ], - + + // 如果用户配置了时间戳,系统将使用配置的时间戳,如果没有配置,使用OTS的系统时间戳 + // 可选 + "defaultTimestampInMillionSecond": 142722431, + // 写入OTS的方式 // PutRow : 等同于OTS API中PutRow操作,检查条件是ignore // UpdateRow : 等同于OTS API中UpdateRow操作,检查条件是ignore - // DeleteRow: 等同于OTS API中DeleteRow操作,检查条件是ignore - "writeMode" : "PutRow" + "writeMode":"PutRow" + } } } @@ -92,94 +93,168 @@ OTS是构建在阿里云飞天分布式系统之上的 NoSQL数据库服务, * **endpoint** - * 描述:OTS Server的EndPoint(服务地址),例如http://bazhen.cn−hangzhou.ots.aliyuncs.com。 + * 描述:OTS Server的EndPoint(服务地址),例如http://bazhen.cn−hangzhou.ots.aliyuncs.com。 - * 必选:是
    + * 必选:是
    - * 默认值:无
    + * 默认值:无
    * **accessId** - * 描述:OTS的accessId
    + * 描述:OTS的accessId
    - * 必选:是
    + * 必选:是
    - * 默认值:无
    + * 默认值:无
    * **accessKey** - * 描述:OTS的accessKey
    + * 描述:OTS的accessKey
    - * 必选:是
    + * 必选:是
    - * 默认值:无
    + * 默认值:无
    * **instanceName** - * 描述:OTS的实例名称,实例是用户使用和管理 OTS 服务的实体,用户在开通 OTS 服务之后,需要通过管理控制台来创建实例,然后在实例内进行表的创建和管理。实例是 OTS 资源管理的基础单元,OTS 对应用程序的访问控制和资源计量都在实例级别完成。
    + * 描述:OTS的实例名称,实例是用户使用和管理 OTS 服务的实体,用户在开通 OTS 服务之后,需要通过管理控制台来创建实例,然后在实例内进行表的创建和管理。实例是 OTS 资源管理的基础单元,OTS 对应用程序的访问控制和资源计量都在实例级别完成。
    - * 必选:是
    + * 必选:是
    - * 默认值:无
    + * 默认值:无
    * **table** - * 描述:所选取的需要抽取的表名称,这里有且只能填写一张表。在OTS不存在多表同步的需求。
    + * 描述:所选取的需要抽取的表名称,这里有且只能填写一张表。在OTS不存在多表同步的需求。
    + + * 必选:是
    + + * 默认值:无
    + +* **newVersion** + + * 描述:version定义了使用的ots SDK版本。
    + * true,新版本插件,使用com.alicloud.openservices.tablestore的依赖(推荐) + * false,旧版本插件,使用com.aliyun.openservices.ots的依赖,**不支持多版本数据的读取** + + * 必选:否
    + + * 默认值:false
    + +* **mode** + + * 描述:是否为多版本数据,目前有两种模式。
    + * normal,对应普通的数据 + * multiVersion,写入数据为多版本格式的数据,多版本模式下,配置参数有所不同,详见3.4节 + + * 必选:否
    - * 必选:是
    + * 默认值:normal
    + + +* **enableAutoIncrement** + + * 描述:是否允许向包含主键自增列的ots表中写入数据。
    + * true,插件会扫描表中的自增列信息,并在写入数据时自动添加自增列 + * false,写入含主键自增列的表时会报错 + + * 必选:否
    + + * 默认值:false
    + + +* **isTimeseriesTable** + + * 描述:写入的对应表是否为时序表,仅在mode=normal模式下生效。
    + * true,写入的数据表为时序数据表 + * false,写入的数据表为普通的宽表 + + * 必选:否
    + + * 默认值:false
    + + * 在写入时序数据表的模式下,不需要配置`primaryKey`字段,只需要配置`column`字段,配置样例: + ```json + "column": [ + { + "name": "_m_name", // 表示度量名称(measurement)字段 + }, + { + "name": "_data_source", // 表示数据源(dataSource)字段 + }, + { + "name": "_tags", // 表示标签字段,会被解析为Map类型 + }, + { + "name": "_time", // 表示时间戳字段,会被解析为long类型的值 + }, + { + "name": "tag_a", + "isTag":"true" // 表示标签内部字段,该字段会被解析到标签的字典内部 + }, + { + "name": "column1", // 属性列名称 + "type": "string" // 属性列类型,支持 bool string int double binary + }, + { + "name": "column2", + "type": "int" + } + ], + ``` + + - * 默认值:无
    * **primaryKey** - * 描述: OTS的主键信息,使用JSON的数组描述字段信息。OTS本身是NoSQL系统,在OTSWriter导入数据过程中,必须指定相应地字段名称。 + * 描述: OTS的主键信息,使用JSON的数组描述字段信息。OTS本身是NoSQL系统,在OTSWriter导入数据过程中,必须指定相应地字段名称。 - OTS的PrimaryKey只能支持STRING,INT两种类型,因此OTSWriter本身也限定填写上述两种类型。 + OTS的PrimaryKey只能支持STRING,INT两种类型,因此OTSWriter本身也限定填写上述两种类型。 - DataX本身支持类型转换的,因此对于源头数据非String/Int,OTSWriter会进行数据类型转换。 + DataX本身支持类型转换的,因此对于源头数据非String/Int,OTSWriter会进行数据类型转换。 - 配置实例: + 配置实例: - ```json - "primaryKey" : [ - {"name":"pk1", "type":"string"}, - {"name":"pk2", "type":"int"} - ], - ``` - * 必选:是
    + ```json + "primaryKey":[ + "userid", + "groupid" + ] + ``` + * 必选:是
    - * 默认值:无
    + * 默认值:无
    * **column** - * 描述:所配置的表中需要同步的列名集合,使用JSON的数组描述字段信息。使用格式为 + * 描述:所配置的表中需要同步的列名集合,使用JSON的数组描述字段信息。使用格式为 - ```json - {"name":"col2", "type":"INT"}, - ``` + ```json + {"name":"col2", "type":"INT"}, + ``` - 其中的name指定写入的OTS列名,type指定写入的类型。OTS类型支持STRING,INT,DOUBLE,BOOL和BINARY几种类型 。 + 其中的name指定写入的OTS列名,type指定写入的类型。OTS类型支持STRING,INT,DOUBLE,BOOL和BINARY几种类型 。 - 写入过程不支持常量、函数或者自定义表达式。 + 写入过程不支持常量、函数或者自定义表达式。 - * 必选:是
    + * 必选:是
    - * 默认值:无
    + * 默认值:无
    * **writeMode** - * 描述:写入模式,目前支持两种模式, + * 描述:写入模式,目前支持两种模式, - * PutRow,对应于OTS API PutRow,插入数据到指定的行,如果该行不存在,则新增一行;若该行存在,则覆盖原有行。 + * PutRow,对应于OTS API PutRow,插入数据到指定的行,如果该行不存在,则新增一行;若该行存在,则覆盖原有行。 - * UpdateRow,对应于OTS API UpdateRow,更新指定行的数据,如果该行不存在,则新增一行;若该行存在,则根据请求的内容在这一行中新增、修改或者删除指定列的值。 + * UpdateRow,对应于OTS API UpdateRow,更新指定行的数据,如果该行不存在,则新增一行;若该行存在,则根据请求的内容在这一行中新增、修改或者删除指定列的值。 - * DeleteRow,对应于OTS API DeleteRow,删除指定行的数据。 - * 必选:是
    +* 必选:是
    - * 默认值:无
    +* 默认值:无
    ### 3.3 类型转换 @@ -197,43 +272,79 @@ OTS是构建在阿里云飞天分布式系统之上的 NoSQL数据库服务, * 注意,OTS本身不支持日期型类型。应用层一般使用Long报错时间的Unix TimeStamp。 -## 4 性能报告 - -### 4.1 环境准备 - -#### 4.1.1 数据特征 - -2列PK(10 + 8),15列String(10 Byte), 2两列Integer(8 Byte),算上Column Name每行大概327Byte,每次BatchWriteRow写入100行数据,所以当个请求的数据大小是32KB。 +### 3.4 multiVersion模式 -#### 4.1.2 机器参数 - -OTS端:3台前端机,5台后端机 - -DataX运行端: 24核CPU, 98GB内存 - -### 4.2 测试报告 - -#### 4.2.1 测试报告 - -|并发数|DataX CPU|DATAX流量 |OTS 流量 | BatchWrite前端QPS| BatchWriteRow前端延时| -|--------|--------| --------|--------|--------|------| -|40| 1027% |Speed 22.13MB/s, 112640 records/s|65.8M/s |42|153ms | -|50| 1218% |Speed 24.11MB/s, 122700 records/s|73.5M/s |47|174ms| -|60| 1355% |Speed 25.31MB/s, 128854 records/s|78.1M/s |50|190ms| -|70| 1578% |Speed 26.35MB/s, 134121 records/s|80.8M/s |52|210ms| -|80| 1771% |Speed 26.55MB/s, 135161 records/s|82.7M/s |53|230ms| +#### 3.4.1 模式介绍 +multiVersion模式解决了ots数据库中多版本数据的导入问题。支持Hbase的全量数据迁移到OTS +* 注意:这种模式的数据格式比较特殊,该writer需要reader也提供版本的输出 +* 当前只有hbase reader 与 ots reader提供这种模式,使用时切记注意 +#### 3.4.2 配置样例 +``` +{ + "job": { + "setting": { + }, + "content": [ + { + "reader": {}, + "writer": { + "name": "otswriter", + "parameter": { + "endpoint":"", + "accessId":"", + "accessKey":"", + "instanceName":"", + "table":"", + + // 多版本模式,插件会按照多版本模式去解析所有配置 + "mode":"multiVersion", + + "newVersion":"true", + + // 配置PK信息 + // 考虑到配置成本,并不需要配置PK在Record(Line)中的位置,要求 + // Record的格式固定,PK一定在行首,PK之后是columnName,格式如下: + // 如:{pk0,pk1,pk2,pk3}, {columnName}, {timestamp}, {value} + "primaryKey":[ + "userid", + "groupid" + ], + + // 列名前缀过滤 + // 描述:hbase导入过来的数据,cf和qulifier共同组成columnName, + // OTS并不支持cf,所以需要将cf过滤掉 + // 注意: + // 1.该参数选填,如果没有填写或者值为空字符串,表示不对列名进行过滤。 + // 2.如果datax传入的数据columnName列不是以前缀开始,则将该Record放入脏数据回收器中 + "columnNamePrefixFilter":"cf:" + } + } + } + ] + } +} +``` -## 5 约束限制 +## 4 约束限制 -### 5.1 写入幂等性 +### 4.1 写入幂等性 OTS写入本身是支持幂等性的,也就是使用OTS SDK同一条数据写入OTS系统,一次和多次请求的结果可以理解为一致的。因此对于OTSWriter多次尝试写入同一条数据与写入一条数据结果是等同的。 -### 5.2 单任务FailOver +### 4.2 单任务FailOver 由于OTS写入本身是幂等性的,因此可以支持单任务FailOver。即一旦写入Fail,DataX会重新启动相关子任务进行重试。 -## 6 FAQ +## 5 FAQ + +* 1.如果使用多版本模式,value为null应该怎么解释? + * : 表示删除指定的版本 +* 2.如果ts列为空怎么办? + * :插件记录为垃圾数据 +* 3.Record的count和期望不符? + * : 插件异常终止 +* 4.在普通模式下,采用UpdateRow的方式写入数据,如果不指定TS,相同行数的数据怎么写入到OTS中? + * : 后面的覆盖前面的数据 diff --git a/otswriter/pom.xml b/otswriter/pom.xml index 8677c8abf4..f393d76c41 100644 --- a/otswriter/pom.xml +++ b/otswriter/pom.xml @@ -33,8 +33,25 @@ com.aliyun.openservices ots-public - 2.2.4 - + 2.2.6 + + + log4j-core + org.apache.logging.log4j + + + + + com.aliyun.openservices + tablestore + 5.13.10 + + + log4j-core + org.apache.logging.log4j + + + com.google.code.gson gson @@ -42,6 +59,14 @@ + + + src/main/java + + **/*.properties + + + diff --git a/otswriter/src/main/assembly/package.xml b/otswriter/src/main/assembly/package.xml index 5ae7a01511..9152302556 100644 --- a/otswriter/src/main/assembly/package.xml +++ b/otswriter/src/main/assembly/package.xml @@ -12,8 +12,8 @@ src/main/resources plugin.json - plugin_job_template.json - + plugin_job_template.json + plugin/writer/otswriter diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/IOtsWriterMasterProxy.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/IOtsWriterMasterProxy.java new file mode 100644 index 0000000000..af364b86f1 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/IOtsWriterMasterProxy.java @@ -0,0 +1,16 @@ +package com.alibaba.datax.plugin.writer.otswriter; + +import com.alibaba.datax.common.util.Configuration; + +import java.util.List; + +public interface IOtsWriterMasterProxy { + + public void init(Configuration param) throws Exception; + + public void close(); + + public List split(int mandatoryNumber); + + +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/IOtsWriterSlaveProxy.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/IOtsWriterSlaveProxy.java new file mode 100644 index 0000000000..1ce78ccb17 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/IOtsWriterSlaveProxy.java @@ -0,0 +1,25 @@ +package com.alibaba.datax.plugin.writer.otswriter; + +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; + +public interface IOtsWriterSlaveProxy { + + /** + * Slave的初始化,创建Slave所使用的资源 + */ + public void init(Configuration configuration); + + /** + * 释放Slave的所有资源 + */ + public void close() throws OTSCriticalException; + + /** + * Slave的执行器,将Datax的数据写入到OTS中 + * @param recordReceiver + * @throws OTSCriticalException + */ + public void write(RecordReceiver recordReceiver, TaskPluginCollector taskPluginCollector) throws OTSCriticalException; +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/Key.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/Key.java index 0724b9cf6f..10dd9cc991 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/Key.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/Key.java @@ -25,7 +25,11 @@ public final class Key { public final static String OTS_ACCESSKEY = "accessKey"; public final static String OTS_INSTANCE_NAME = "instanceName"; - + public final static String ENABLE_AUTO_INCREMENT = "enableAutoIncrement"; + public final static String IS_TIMESERIES_TABLE = "isTimeseriesTable"; + + public final static String TIMEUNIT_FORMAT = "timeunit"; + public final static String TABLE_NAME = "table"; public final static String PRIMARY_KEY = "primaryKey"; @@ -33,4 +37,11 @@ public final class Key { public final static String COLUMN = "column"; public final static String WRITE_MODE = "writeMode"; + + public final static String MODE = "mode"; + public final static String NEW_VERISON = "newVersion"; + + public final static String DEFAULT_TIMESTAMP = "defaultTimestampInMillisecond"; + + public final static String COLUMN_NAME_PREFIX_FILTER = "columnNamePrefixFilter"; } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OTSCriticalException.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OTSCriticalException.java new file mode 100644 index 0000000000..b89df00873 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OTSCriticalException.java @@ -0,0 +1,24 @@ +package com.alibaba.datax.plugin.writer.otswriter; + +/** + * 插件错误异常,该异常主要用于描述插件的异常退出 + * @author redchen + */ +public class OTSCriticalException extends Exception{ + + private static final long serialVersionUID = 5820460098894295722L; + + public OTSCriticalException() {} + + public OTSCriticalException(String message) { + super(message); + } + + public OTSCriticalException(Throwable a) { + super(a); + } + + public OTSCriticalException(String message, Throwable a) { + super(message, a); + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OTSErrorCode.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OTSErrorCode.java new file mode 100644 index 0000000000..86877730c1 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OTSErrorCode.java @@ -0,0 +1,115 @@ +/** + * Copyright (C) Alibaba Cloud Computing + * All rights reserved. + * + * 版权所有 (C)阿里云计算有限公司 + */ + +package com.alibaba.datax.plugin.writer.otswriter; + +/** + * 表示来自开放结构化数据服务(Open Table Service,OTS)的错误代码。 + * + */ +public interface OTSErrorCode { + /** + * 用户身份验证失败。 + */ + static final String AUTHORIZATION_FAILURE = "OTSAuthFailed"; + + /** + * 服务器内部错误。 + */ + static final String INTERNAL_SERVER_ERROR = "OTSInternalServerError"; + + /** + * 参数错误。 + */ + static final String INVALID_PARAMETER = "OTSParameterInvalid"; + + /** + * 整个请求过大。 + */ + static final String REQUEST_TOO_LARGE = "OTSRequestBodyTooLarge"; + + /** + * 客户端请求超时。 + */ + static final String REQUEST_TIMEOUT = "OTSRequestTimeout"; + + /** + * 用户的配额已经用满。 + */ + static final String QUOTA_EXHAUSTED = "OTSQuotaExhausted"; + + /** + * 内部服务器发生failover,导致表的部分分区不可服务。 + */ + static final String PARTITION_UNAVAILABLE = "OTSPartitionUnavailable"; + + /** + * 表刚被创建还无法立马提供服务。 + */ + static final String TABLE_NOT_READY = "OTSTableNotReady"; + + /** + * 请求的表不存在。 + */ + static final String OBJECT_NOT_EXIST = "OTSObjectNotExist"; + + /** + * 请求创建的表已经存在。 + */ + static final String OBJECT_ALREADY_EXIST = "OTSObjectAlreadyExist"; + + /** + * 多个并发的请求写同一行数据,导致冲突。 + */ + static final String ROW_OPEARTION_CONFLICT = "OTSRowOperationConflict"; + + /** + * 主键不匹配。 + */ + static final String INVALID_PK = "OTSInvalidPK"; + + /** + * 读写能力调整过于频繁。 + */ + static final String TOO_FREQUENT_RESERVED_THROUGHPUT_ADJUSTMENT = "OTSTooFrequentReservedThroughputAdjustment"; + + /** + * 该行总列数超出限制。 + */ + static final String OUT_OF_COLUMN_COUNT_LIMIT = "OTSOutOfColumnCountLimit"; + + /** + * 该行所有列数据大小总和超出限制。 + */ + static final String OUT_OF_ROW_SIZE_LIMIT = "OTSOutOfRowSizeLimit"; + + /** + * 剩余预留读写能力不足。 + */ + static final String NOT_ENOUGH_CAPACITY_UNIT = "OTSNotEnoughCapacityUnit"; + + /** + * 预查条件检查失败。 + */ + static final String CONDITION_CHECK_FAIL = "OTSConditionCheckFail"; + + /** + * 在OTS内部操作超时。 + */ + static final String STORAGE_TIMEOUT = "OTSTimeout"; + + /** + * 在OTS内部有服务器不可访问。 + */ + static final String SERVER_UNAVAILABLE = "OTSServerUnavailable"; + + /** + * OTS内部服务器繁忙。 + */ + static final String SERVER_BUSY = "OTSServerBusy"; + +} \ No newline at end of file diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriter.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriter.java index 4d2ed17b3f..4622723897 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriter.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriter.java @@ -1,41 +1,44 @@ package com.alibaba.datax.plugin.writer.otswriter; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.alibaba.datax.common.exception.DataXException; import com.alibaba.datax.common.plugin.RecordReceiver; import com.alibaba.datax.common.spi.Writer; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.datax.plugin.writer.otswriter.utils.Common; -import com.aliyun.openservices.ots.ClientException; -import com.aliyun.openservices.ots.OTSException; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSConf; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSConst; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSMode; +import com.alibaba.datax.plugin.writer.otswriter.utils.GsonParser; +import com.alicloud.openservices.tablestore.ClientException; +import com.alicloud.openservices.tablestore.TableStoreException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; public class OtsWriter { + public static class Job extends Writer.Job { private static final Logger LOG = LoggerFactory.getLogger(Job.class); - private OtsWriterMasterProxy proxy = new OtsWriterMasterProxy(); - + + private IOtsWriterMasterProxy proxy; + @Override public void init() { LOG.info("init() begin ..."); + proxy = new OtsWriterMasterProxy(); try { this.proxy.init(getPluginJobConf()); - } catch (OTSException e) { - LOG.error("OTSException: {}", e.getMessage(), e); - throw DataXException.asDataXException(new OtsWriterError(e.getErrorCode(), "OTS端的错误"), Common.getDetailMessage(e), e); + } catch (TableStoreException e) { + LOG.error("OTSException: {}", e.toString(), e); + throw DataXException.asDataXException(new OtsWriterError(e.getErrorCode(), "OTS Client Error"), e.toString(), e); } catch (ClientException e) { - LOG.error("ClientException: {}", e.getMessage(), e); - throw DataXException.asDataXException(new OtsWriterError(e.getErrorCode(), "OTS端的错误"), Common.getDetailMessage(e), e); - } catch (IllegalArgumentException e) { - LOG.error("IllegalArgumentException. ErrorMsg:{}", e.getMessage(), e); - throw DataXException.asDataXException(OtsWriterError.INVALID_PARAM, Common.getDetailMessage(e), e); + LOG.error("ClientException: {}", e.toString(), e); + throw DataXException.asDataXException(OtsWriterError.ERROR, e.toString(), e); } catch (Exception e) { - LOG.error("Exception. ErrorMsg:{}", e.getMessage(), e); - throw DataXException.asDataXException(OtsWriterError.ERROR, Common.getDetailMessage(e), e); + LOG.error("Exception. ErrorMsg:{}", e.toString(), e); + throw DataXException.asDataXException(OtsWriterError.ERROR, e.toString(), e); } + LOG.info("init() end ..."); } @@ -50,42 +53,67 @@ public List split(int mandatoryNumber) { return this.proxy.split(mandatoryNumber); } catch (Exception e) { LOG.error("Exception. ErrorMsg:{}", e.getMessage(), e); - throw DataXException.asDataXException(OtsWriterError.ERROR, Common.getDetailMessage(e), e); + throw DataXException.asDataXException(OtsWriterError.ERROR, e.toString(), e); } } } - + public static class Task extends Writer.Task { private static final Logger LOG = LoggerFactory.getLogger(Task.class); - private OtsWriterSlaveProxy proxy = new OtsWriterSlaveProxy(); - + private IOtsWriterSlaveProxy proxy = null; + + /** + * 基于配置,构建对应的worker代理 + */ @Override - public void init() {} + public void init() { + OTSConf conf = GsonParser.jsonToConf(this.getPluginJobConf().getString(OTSConst.OTS_CONF)); + // 是否使用新接口 + if(conf.isNewVersion()) { + if (conf.getMode() == OTSMode.MULTI_VERSION) { + LOG.info("init OtsWriterSlaveProxyMultiVersion"); + proxy = new OtsWriterSlaveProxyMultiversion(); + } else { + LOG.info("init OtsWriterSlaveProxyNormal"); + proxy = new OtsWriterSlaveProxyNormal(); + } + + } + else{ + proxy = new OtsWriterSlaveProxyOld(); + } + + proxy.init(this.getPluginJobConf()); + + } @Override public void destroy() { - this.proxy.close(); + try { + proxy.close(); + } catch (OTSCriticalException e) { + LOG.error("OTSCriticalException. ErrorMsg:{}", e.getMessage(), e); + throw DataXException.asDataXException(OtsWriterError.ERROR, e.toString(), e); + } } @Override public void startWrite(RecordReceiver lineReceiver) { LOG.info("startWrite() begin ..."); + try { - this.proxy.init(this.getPluginJobConf()); - this.proxy.write(lineReceiver, this.getTaskPluginCollector()); - } catch (OTSException e) { - LOG.error("OTSException: {}", e.getMessage(), e); - throw DataXException.asDataXException(new OtsWriterError(e.getErrorCode(), "OTS端的错误"), Common.getDetailMessage(e), e); + proxy.write(lineReceiver, this.getTaskPluginCollector()); + } catch (TableStoreException e) { + LOG.error("OTSException: {}", e.toString(), e); + throw DataXException.asDataXException(new OtsWriterError(e.getErrorCode(), "OTS Client Error"), e.toString(), e); } catch (ClientException e) { - LOG.error("ClientException: {}", e.getMessage(), e); - throw DataXException.asDataXException(new OtsWriterError(e.getErrorCode(), "OTS端的错误"), Common.getDetailMessage(e), e); - } catch (IllegalArgumentException e) { - LOG.error("IllegalArgumentException. ErrorMsg:{}", e.getMessage(), e); - throw DataXException.asDataXException(OtsWriterError.INVALID_PARAM, Common.getDetailMessage(e), e); + LOG.error("ClientException: {}", e.toString(), e); + throw DataXException.asDataXException(OtsWriterError.ERROR, e.toString(), e); } catch (Exception e) { - LOG.error("Exception. ErrorMsg:{}", e.getMessage(), e); - throw DataXException.asDataXException(OtsWriterError.ERROR, Common.getDetailMessage(e), e); + LOG.error("Exception. ErrorMsg:{}", e.toString(), e); + throw DataXException.asDataXException(OtsWriterError.ERROR, e.toString(), e); } + LOG.info("startWrite() end ..."); } } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterError.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterError.java index 67d1ee2b77..092a7343a6 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterError.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterError.java @@ -14,10 +14,10 @@ public class OtsWriterError implements ErrorCode { public final static OtsWriterError ERROR = new OtsWriterError( "OtsWriterError", - "该错误表示插件的内部错误,表示系统没有处理到的异常"); + "This error represents an internal error of the ots writer plugin, which indicates that the system is not processed."); public final static OtsWriterError INVALID_PARAM = new OtsWriterError( "OtsWriterInvalidParameter", - "该错误表示参数错误,表示用户输入了错误的参数格式等"); + "This error represents a parameter error, indicating that the user entered the wrong parameter format."); public OtsWriterError (String code) { this.code = code; @@ -41,6 +41,6 @@ public String getDescription() { @Override public String toString() { - return this.code; + return "[ code:" + this.code + ", message:" + this.description + "]"; } } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterMasterProxy.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterMasterProxy.java index 91cf9b120f..774aca1e7f 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterMasterProxy.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterMasterProxy.java @@ -1,110 +1,138 @@ package com.alibaba.datax.plugin.writer.otswriter; -import java.util.ArrayList; -import java.util.List; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import com.alibaba.datax.common.util.Configuration; import com.alibaba.datax.plugin.writer.otswriter.callable.GetTableMetaCallable; import com.alibaba.datax.plugin.writer.otswriter.model.OTSConf; -import com.alibaba.datax.plugin.writer.otswriter.model.OTSConf.RestrictConf; import com.alibaba.datax.plugin.writer.otswriter.model.OTSConst; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSMode; import com.alibaba.datax.plugin.writer.otswriter.model.OTSOpType; -import com.alibaba.datax.plugin.writer.otswriter.utils.GsonParser; -import com.alibaba.datax.plugin.writer.otswriter.utils.ParamChecker; -import com.alibaba.datax.plugin.writer.otswriter.utils.RetryHelper; -import com.alibaba.datax.plugin.writer.otswriter.utils.WriterModelParser; -import com.aliyun.openservices.ots.OTSClient; -import com.aliyun.openservices.ots.model.TableMeta; - -public class OtsWriterMasterProxy { - +import com.alibaba.datax.plugin.writer.otswriter.utils.*; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.TimeseriesClient; +import com.alicloud.openservices.tablestore.model.TableMeta; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class OtsWriterMasterProxy implements IOtsWriterMasterProxy { + + private static final Logger LOG = LoggerFactory.getLogger(OtsWriterMasterProxy.class); private OTSConf conf = new OTSConf(); - - private OTSClient ots = null; - + private SyncClientInterface ots = null; private TableMeta meta = null; - - private static final Logger LOG = LoggerFactory.getLogger(OtsWriterMasterProxy.class); - + /** * @param param * @throws Exception */ + @Override public void init(Configuration param) throws Exception { - + // 默认参数 - conf.setRetry(param.getInt(OTSConst.RETRY, 18)); - conf.setSleepInMillisecond(param.getInt(OTSConst.SLEEP_IN_MILLISECOND, 100)); - conf.setBatchWriteCount(param.getInt(OTSConst.BATCH_WRITE_COUNT, 100)); - conf.setConcurrencyWrite(param.getInt(OTSConst.CONCURRENCY_WRITE, 5)); - conf.setIoThreadCount(param.getInt(OTSConst.IO_THREAD_COUNT, 1)); - conf.setSocketTimeout(param.getInt(OTSConst.SOCKET_TIMEOUT, 20000)); - conf.setConnectTimeout(param.getInt(OTSConst.CONNECT_TIMEOUT, 10000)); - conf.setBufferSize(param.getInt(OTSConst.BUFFER_SIZE, 1024)); - - RestrictConf restrictConf = conf.new RestrictConf(); - restrictConf.setRequestTotalSizeLimition(param.getInt(OTSConst.REQUEST_TOTAL_SIZE_LIMITATION, 1024 * 1024)); - restrictConf.setAttributeColumnSize(param.getInt(OTSConst.ATTRIBUTE_COLUMN_SIZE_LIMITATION, 2 * 1024 * 1024)); - restrictConf.setPrimaryKeyColumnSize(param.getInt(OTSConst.PRIMARY_KEY_COLUMN_SIZE_LIMITATION, 1024)); - restrictConf.setMaxColumnsCount(param.getInt(OTSConst.ATTRIBUTE_COLUMN_MAX_COUNT, 1024)); - conf.setRestrictConf(restrictConf); + setStaticParams(param); + + conf.setTimestamp(param.getInt(Key.DEFAULT_TIMESTAMP, -1)); + conf.setRequestTotalSizeLimitation(param.getInt(OTSConst.REQUEST_TOTAL_SIZE_LIMITATION, 1024 * 1024)); // 必选参数 - conf.setEndpoint(ParamChecker.checkStringAndGet(param, Key.OTS_ENDPOINT)); - conf.setAccessId(ParamChecker.checkStringAndGet(param, Key.OTS_ACCESSID)); - conf.setAccessKey(ParamChecker.checkStringAndGet(param, Key.OTS_ACCESSKEY)); - conf.setInstanceName(ParamChecker.checkStringAndGet(param, Key.OTS_INSTANCE_NAME)); - conf.setTableName(ParamChecker.checkStringAndGet(param, Key.TABLE_NAME)); - - conf.setOperation(WriterModelParser.parseOTSOpType(ParamChecker.checkStringAndGet(param, Key.WRITE_MODE))); - - ots = new OTSClient( - this.conf.getEndpoint(), - this.conf.getAccessId(), - this.conf.getAccessKey(), - this.conf.getInstanceName()); - - meta = getTableMeta(ots, conf.getTableName()); - LOG.info("Table Meta : {}", GsonParser.metaToJson(meta)); - - conf.setPrimaryKeyColumn(WriterModelParser.parseOTSPKColumnList(ParamChecker.checkListAndGet(param, Key.PRIMARY_KEY, true))); - ParamChecker.checkPrimaryKey(meta, conf.getPrimaryKeyColumn()); - - conf.setAttributeColumn(WriterModelParser.parseOTSAttrColumnList(ParamChecker.checkListAndGet(param, Key.COLUMN, conf.getOperation() == OTSOpType.UPDATE_ROW ? true : false))); - ParamChecker.checkAttribute(conf.getAttributeColumn()); + conf.setEndpoint(ParamChecker.checkStringAndGet(param, Key.OTS_ENDPOINT)); + conf.setAccessId(ParamChecker.checkStringAndGet(param, Key.OTS_ACCESSID)); + conf.setAccessKey(ParamChecker.checkStringAndGet(param, Key.OTS_ACCESSKEY)); + conf.setInstanceName(ParamChecker.checkStringAndGet(param, Key.OTS_INSTANCE_NAME)); + conf.setTableName(ParamChecker.checkStringAndGet(param, Key.TABLE_NAME)); + + ots = Common.getOTSInstance(conf); + + conf.setNewVersion(param.getBool(Key.NEW_VERISON, false)); + conf.setMode(WriterModelParser.parseOTSMode(param.getString(Key.MODE, "normal"))); + conf.setEnableAutoIncrement(param.getBool(Key.ENABLE_AUTO_INCREMENT, false)); + conf.setTimeseriesTable(param.getBool(Key.IS_TIMESERIES_TABLE, false)); + ParamChecker.checkVersion(conf); + + if (!conf.isTimeseriesTable()){ + meta = getTableMeta(ots, conf.getTableName()); + LOG.debug("Table Meta : {}", GsonParser.metaToJson(meta)); + conf.setPrimaryKeyColumn(WriterModelParser.parseOTSPKColumnList(meta, ParamChecker.checkListAndGet(param, Key.PRIMARY_KEY, true))); + } + + if (conf.getMode() == OTSMode.MULTI_VERSION) { + conf.setOperation(OTSOpType.UPDATE_ROW);// 多版本只支持Update模式 + conf.setColumnNamePrefixFilter(param.getString(Key.COLUMN_NAME_PREFIX_FILTER, null)); + } else if (!conf.isTimeseriesTable()){ // 普通模式,写入宽表 + conf.setOperation(WriterModelParser.parseOTSOpType(ParamChecker.checkStringAndGet(param, Key.WRITE_MODE), conf.getMode())); + conf.setAttributeColumn(WriterModelParser.parseOTSAttrColumnList(conf.getPrimaryKeyColumn(), ParamChecker.checkListAndGet(param, Key.COLUMN, false), conf.getMode() + ) + ); + ParamChecker.checkAttribute(conf.getAttributeColumn()); + } else { // 普通模式,写入时序表 + conf.setOperation(OTSOpType.PUT_ROW);// 时序表只支持Put模式 + conf.setAttributeColumn(WriterModelParser.parseOTSTimeseriesRowAttrList(ParamChecker.checkListAndGet(param, Key.COLUMN, true))); + conf.setTimeUnit(ParamChecker.checkTimeUnitAndGet(param.getString(Key.TIMEUNIT_FORMAT, "MICROSECONDS"))); + } + + /** + * 如果配置支持主键列自增 + */ + if (conf.getEnableAutoIncrement()) { + ParamChecker.checkPrimaryKeyWithAutoIncrement(meta, conf.getPrimaryKeyColumn()); + conf.setEncodePkColumnMapping(Common.getEncodePkColumnMappingWithAutoIncrement(meta, conf.getPrimaryKeyColumn())); + } + /** + * 如果配置不支持主键列自增 + */ + else if (!conf.isTimeseriesTable()){ + ParamChecker.checkPrimaryKey(meta, conf.getPrimaryKeyColumn()); + conf.setEncodePkColumnMapping(Common.getEncodePkColumnMapping(meta, conf.getPrimaryKeyColumn())); + } + + } - - public List split(int mandatoryNumber){ + + @Override + public List split(int mandatoryNumber) { LOG.info("Begin split and MandatoryNumber : {}", mandatoryNumber); List configurations = new ArrayList(); + String json = GsonParser.confToJson(this.conf); for (int i = 0; i < mandatoryNumber; i++) { Configuration configuration = Configuration.newDefault(); - configuration.set(OTSConst.OTS_CONF, GsonParser.confToJson(this.conf)); + configuration.set(OTSConst.OTS_CONF, json); configurations.add(configuration); } LOG.info("End split."); - assert(mandatoryNumber == configurations.size()); return configurations; } - + + @Override public void close() { ots.shutdown(); } - + public OTSConf getOTSConf() { return conf; } // private function - private TableMeta getTableMeta(OTSClient ots, String tableName) throws Exception { + private TableMeta getTableMeta(SyncClientInterface ots, String tableName) throws Exception { return RetryHelper.executeWithRetry( new GetTableMetaCallable(ots, tableName), conf.getRetry(), conf.getSleepInMillisecond() - ); + ); + } + + public void setStaticParams(Configuration param) { + // 默认参数 + conf.setRetry(param.getInt(OTSConst.RETRY, 18)); + conf.setSleepInMillisecond(param.getInt(OTSConst.SLEEP_IN_MILLISECOND, 100)); + conf.setBatchWriteCount(param.getInt(OTSConst.BATCH_WRITE_COUNT, 100)); + conf.setConcurrencyWrite(param.getInt(OTSConst.CONCURRENCY_WRITE, 5)); + conf.setIoThreadCount(param.getInt(OTSConst.IO_THREAD_COUNT, 1)); + conf.setSocketTimeoutInMillisecond(param.getInt(OTSConst.SOCKET_TIMEOUTIN_MILLISECOND, 10000)); + conf.setConnectTimeoutInMillisecond(param.getInt(OTSConst.CONNECT_TIMEOUT_IN_MILLISECOND, 10000)); + } } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterSlaveProxyMultiversion.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterSlaveProxyMultiversion.java new file mode 100644 index 0000000000..6db7569282 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterSlaveProxyMultiversion.java @@ -0,0 +1,135 @@ +package com.alibaba.datax.plugin.writer.otswriter; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.otswriter.model.*; +import com.alibaba.datax.plugin.writer.otswriter.utils.CollectorUtil; +import com.alibaba.datax.plugin.writer.otswriter.utils.Common; +import com.alibaba.datax.plugin.writer.otswriter.utils.GsonParser; +import com.alibaba.datax.plugin.writer.otswriter.utils.ParseRecord; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.model.PrimaryKey; +import com.alicloud.openservices.tablestore.model.PrimaryKeySchema; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static com.alibaba.datax.plugin.writer.otswriter.utils.Common.getOTSInstance; + +public class OtsWriterSlaveProxyMultiversion implements IOtsWriterSlaveProxy { + + private OTSConf conf = null; + private SyncClientInterface ots = null; + private OTSSendBuffer buffer = null; + private Map pkColumnMapping = null; + private static final Logger LOG = LoggerFactory.getLogger(OtsWriterSlaveProxyMultiversion.class); + + @Override + public void init(Configuration configuration) { + LOG.info("OtsWriterSlaveProxyMultiversion init begin"); + this.conf = GsonParser.jsonToConf(configuration.getString(OTSConst.OTS_CONF)); + this.ots = getOTSInstance(conf); + this.pkColumnMapping = Common.getPkColumnMapping(conf.getEncodePkColumnMapping()); + buffer = new OTSSendBuffer(ots, conf); + LOG.info("init end"); + } + + @Override + public void close() throws OTSCriticalException { + LOG.info("close begin"); + ots.shutdown(); + LOG.info("close end"); + } + + @Override + public void write(RecordReceiver recordReceiver, TaskPluginCollector taskPluginCollector) throws OTSCriticalException { + LOG.info("write begin"); + // 初始化全局垃圾回收器 + CollectorUtil.init(taskPluginCollector); + // Record format : {PK1, PK2, ...} {ColumnName} {TimeStamp} {Value} + int expectColumnCount = conf.getPrimaryKeyColumn().size()+ 3;// 3表示{ColumnName} {TimeStamp} {Value} + Record record = null; + PrimaryKey lastCellPk = null; + List rowBuffer = new ArrayList(); + while ((record = recordReceiver.getFromReader()) != null) { + + LOG.debug("Record Raw: {}", record.toString()); + + int columnCount = record.getColumnNumber(); + if (columnCount != expectColumnCount) { + // 如果Column的个数和预期的个数不一致时,认为是系统故障或者用户配置Column错误,异常退出 + throw new OTSCriticalException(String.format( + OTSErrorMessage.RECORD_AND_COLUMN_SIZE_ERROR, + columnCount, + expectColumnCount, + record.toString() + )); + } + + PrimaryKey curPk = null; + if ((curPk = Common.getPKFromRecord(this.pkColumnMapping, record)) == null) { + continue; + } + + // check same row + if (lastCellPk == null) { + lastCellPk = curPk; + } else if (!lastCellPk.equals(curPk)) { + OTSLine line = ParseRecord.parseMultiVersionRecordToOTSLine( + conf.getTableName(), + conf.getOperation(), + pkColumnMapping, + conf.getColumnNamePrefixFilter(), + lastCellPk, + rowBuffer); + if (line != null) { + buffer.write(line); + } + rowBuffer.clear(); + lastCellPk = curPk; + } + rowBuffer.add(record); + } + // Flush剩余数据 + if (!rowBuffer.isEmpty()) { + OTSLine line = ParseRecord.parseMultiVersionRecordToOTSLine( + conf.getTableName(), + conf.getOperation(), + pkColumnMapping, + conf.getColumnNamePrefixFilter(), + lastCellPk, + rowBuffer); + if (line != null) { + buffer.write(line); + } + } + + buffer.close(); + LOG.info("write end"); + } + + public void setOts(SyncClientInterface ots){ + this.ots = ots; + } + + public OTSConf getConf() { + return conf; + } + + public void setConf(OTSConf conf) { + this.conf = conf; + } + + public void setBuffer(OTSSendBuffer buffer) { + this.buffer = buffer; + } + + public void setPkColumnMapping(Map pkColumnMapping) { + this.pkColumnMapping = pkColumnMapping; + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterSlaveProxyNormal.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterSlaveProxyNormal.java new file mode 100644 index 0000000000..aaa0ef040f --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterSlaveProxyNormal.java @@ -0,0 +1,153 @@ +package com.alibaba.datax.plugin.writer.otswriter; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.otswriter.callable.GetTableMetaCallable; +import com.alibaba.datax.plugin.writer.otswriter.model.*; +import com.alibaba.datax.plugin.writer.otswriter.utils.*; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.model.PrimaryKeySchema; +import com.alicloud.openservices.tablestore.model.TableMeta; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; + +import static com.alibaba.datax.plugin.writer.otswriter.utils.Common.getOTSInstance; + +public class OtsWriterSlaveProxyNormal implements IOtsWriterSlaveProxy { + + private OTSConf conf = null; + private SyncClientInterface ots = null; + private OTSSendBuffer buffer = null; + private Map pkColumnMapping = null; + private static final Logger LOG = LoggerFactory.getLogger(OtsWriterSlaveProxyNormal.class); + private PrimaryKeySchema primaryKeySchema =null; + + + @Override + public void init(Configuration configuration) { + LOG.info("init begin"); + this.conf = GsonParser.jsonToConf(configuration.getString(OTSConst.OTS_CONF)); + this.ots = getOTSInstance(conf); + if (!conf.isTimeseriesTable()){ + this.pkColumnMapping = Common.getPkColumnMapping(conf.getEncodePkColumnMapping()); + } + + buffer = new OTSSendBuffer(ots, conf); + + if(conf.getEnableAutoIncrement()){ + primaryKeySchema = getAutoIncrementKey(); + } + LOG.info("init end"); + } + + @Override + public void close() throws com.alibaba.datax.plugin.writer.otswriter.OTSCriticalException { + LOG.info("close begin"); + ots.shutdown(); + LOG.info("close end"); + } + + @Override + public void write(RecordReceiver recordReceiver, TaskPluginCollector taskPluginCollector) throws com.alibaba.datax.plugin.writer.otswriter.OTSCriticalException { + LOG.info("write begin"); + + // 初始化全局垃圾回收器 + CollectorUtil.init(taskPluginCollector); + int expectColumnCount = conf.getAttributeColumn().size(); + if (!conf.isTimeseriesTable()){ + expectColumnCount += conf.getPrimaryKeyColumn().size(); + } + Record record = null; + + while ((record = recordReceiver.getFromReader()) != null) { + + LOG.debug("Record Raw: {}", record.toString()); + + int columnCount = record.getColumnNumber(); + if (columnCount != expectColumnCount) { + // 如果Column的个数和预期的个数不一致时,认为是系统故障或者用户配置Column错误,异常退出 + throw new OTSCriticalException(String.format( + OTSErrorMessage.RECORD_AND_COLUMN_SIZE_ERROR, + columnCount, + expectColumnCount, + record.toString() + )); + } + OTSLine line; + + if(conf.getEnableAutoIncrement()){ + line = ParseRecord.parseNormalRecordToOTSLineWithAutoIncrement( + conf.getTableName(), + conf.getOperation(), + pkColumnMapping, + conf.getAttributeColumn(), + record, + conf.getTimestamp(), + primaryKeySchema); + } + else if(!conf.isTimeseriesTable()){ + line = ParseRecord.parseNormalRecordToOTSLine( + conf.getTableName(), + conf.getOperation(), + pkColumnMapping, + conf.getAttributeColumn(), + record, + conf.getTimestamp()); + }else{ + line = ParseRecord.parseNormalRecordToOTSLineOfTimeseriesTable(conf.getAttributeColumn(), + record, conf.getTimeUnit()); + } + + + if (line != null) { + buffer.write(line); + } + } + + buffer.close(); + LOG.info("write end"); + } + + private PrimaryKeySchema getAutoIncrementKey() { + TableMeta tableMeta = null; + try { + tableMeta = RetryHelper.executeWithRetry( + new GetTableMetaCallable(ots, conf.getTableName()), + conf.getRetry(), + conf.getSleepInMillisecond() + ); + } catch (Exception e) { + throw new RuntimeException(e); + } + for (PrimaryKeySchema primaryKeySchema : tableMeta.getPrimaryKeyList()) { + if(primaryKeySchema.hasOption()){ + return primaryKeySchema; + } + } + return null; + } + + public void setOts(SyncClientInterface ots){ + this.ots = ots; + } + + public OTSConf getConf() { + return conf; + } + + public void setConf(OTSConf conf) { + this.conf = conf; + } + + public void setBuffer(OTSSendBuffer buffer) { + this.buffer = buffer; + } + + public void setPkColumnMapping(Map pkColumnMapping) { + this.pkColumnMapping = pkColumnMapping; + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterSlaveProxy.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterSlaveProxyOld.java similarity index 77% rename from otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterSlaveProxy.java rename to otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterSlaveProxyOld.java index 762edfb4d6..625925f152 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterSlaveProxy.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/OtsWriterSlaveProxyOld.java @@ -1,7 +1,16 @@ package com.alibaba.datax.plugin.writer.otswriter; -import com.alibaba.datax.plugin.writer.otswriter.model.*; -import com.alibaba.datax.plugin.writer.otswriter.utils.Common; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSConf; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSConst; +import com.alibaba.datax.plugin.writer.otswriter.utils.WriterRetryPolicy; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSErrorMessage; +import com.alibaba.datax.plugin.writer.otswriter.utils.WithRecord; +import com.alibaba.datax.plugin.writer.otswriter.utils.CommonOld; +import com.alibaba.datax.plugin.writer.otswriter.utils.GsonParser; import com.aliyun.openservices.ots.*; import com.aliyun.openservices.ots.internal.OTSCallback; import com.aliyun.openservices.ots.internal.writer.WriterConfig; @@ -10,19 +19,13 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.alibaba.datax.common.element.Record; -import com.alibaba.datax.common.plugin.RecordReceiver; -import com.alibaba.datax.common.plugin.TaskPluginCollector; -import com.alibaba.datax.common.util.Configuration; -import com.alibaba.datax.plugin.writer.otswriter.utils.GsonParser; - import java.util.List; import java.util.concurrent.Executors; -public class OtsWriterSlaveProxy { +public class OtsWriterSlaveProxyOld implements IOtsWriterSlaveProxy { - private static final Logger LOG = LoggerFactory.getLogger(OtsWriterSlaveProxy.class); + private static final Logger LOG = LoggerFactory.getLogger(OtsWriterSlaveProxyOld.class); private OTSConf conf; private OTSAsync otsAsync; private OTSWriter otsWriter; @@ -54,14 +57,16 @@ public void onFailed(OTSContext otsContext, ClientE } } + @Override public void init(Configuration configuration) { conf = GsonParser.jsonToConf(configuration.getString(OTSConst.OTS_CONF)); - + ClientConfiguration clientConfigure = new ClientConfiguration(); clientConfigure.setIoThreadCount(conf.getIoThreadCount()); clientConfigure.setMaxConnections(conf.getConcurrencyWrite()); clientConfigure.setSocketTimeoutInMillisecond(conf.getSocketTimeout()); - clientConfigure.setConnectionTimeoutInMillisecond(conf.getConnectTimeout()); + // TODO + clientConfigure.setConnectionTimeoutInMillisecond(10000); OTSServiceConfiguration otsConfigure = new OTSServiceConfiguration(); otsConfigure.setRetryStrategy(new WriterRetryPolicy(conf)); @@ -75,39 +80,44 @@ public void init(Configuration configuration) { otsConfigure); } + @Override public void close() { otsAsync.shutdown(); } - - public void write(RecordReceiver recordReceiver, TaskPluginCollector collector) throws Exception { + + @Override + public void write(RecordReceiver recordReceiver, TaskPluginCollector collector) throws OTSCriticalException { LOG.info("Writer slave started."); WriterConfig writerConfig = new WriterConfig(); writerConfig.setConcurrency(conf.getConcurrencyWrite()); writerConfig.setMaxBatchRowsCount(conf.getBatchWriteCount()); - writerConfig.setMaxBatchSize(conf.getRestrictConf().getRequestTotalSizeLimition()); - writerConfig.setBufferSize(conf.getBufferSize()); - writerConfig.setMaxAttrColumnSize(conf.getRestrictConf().getAttributeColumnSize()); - writerConfig.setMaxColumnsCount(conf.getRestrictConf().getMaxColumnsCount()); - writerConfig.setMaxPKColumnSize(conf.getRestrictConf().getPrimaryKeyColumnSize()); + // TODO + writerConfig.setMaxBatchSize(1024 * 1024); + writerConfig.setBufferSize(1024); + writerConfig.setMaxAttrColumnSize(2 * 1024 * 1024); + writerConfig.setMaxColumnsCount(1024); + writerConfig.setMaxPKColumnSize(1024); + otsWriter = new DefaultOTSWriter(otsAsync, conf.getTableName(), writerConfig, new WriterCallback(collector), Executors.newFixedThreadPool(3)); int expectColumnCount = conf.getPrimaryKeyColumn().size() + conf.getAttributeColumn().size(); Record record; while ((record = recordReceiver.getFromReader()) != null) { LOG.debug("Record Raw: {}", record.toString()); - + int columnCount = record.getColumnNumber(); if (columnCount != expectColumnCount) { // 如果Column的个数和预期的个数不一致时,认为是系统故障或者用户配置Column错误,异常退出 - throw new IllegalArgumentException(String.format(OTSErrorMessage.RECORD_AND_COLUMN_SIZE_ERROR, columnCount, expectColumnCount)); + throw new IllegalArgumentException(String.format(OTSErrorMessage.RECORD_AND_COLUMN_SIZE_ERROR, columnCount, expectColumnCount, record.toString())); } - + + // 类型转换 try { - RowPrimaryKey primaryKey = Common.getPKFromRecord(conf.getPrimaryKeyColumn(), record); - List> attributes = Common.getAttrFromRecord(conf.getPrimaryKeyColumn().size(), conf.getAttributeColumn(), record); - RowChange rowChange = Common.columnValuesToRowChange(conf.getTableName(), conf.getOperation(), primaryKey, attributes); + RowPrimaryKey primaryKey = CommonOld.getPKFromRecord(conf.getPrimaryKeyColumn(), record); + List> attributes = CommonOld.getAttrFromRecord(conf.getPrimaryKeyColumn().size(), conf.getAttributeColumn(), record); + RowChange rowChange = CommonOld.columnValuesToRowChange(conf.getTableName(), conf.getOperation(), primaryKey, attributes); WithRecord withRecord = (WithRecord)rowChange; withRecord.setRecord(record); otsWriter.addRowChange(rowChange); diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/BatchWriteRowCallable.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/BatchWriteRowCallable.java new file mode 100644 index 0000000000..f7330937c3 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/BatchWriteRowCallable.java @@ -0,0 +1,25 @@ +package com.alibaba.datax.plugin.writer.otswriter.callable; + +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.model.BatchWriteRowRequest; +import com.alicloud.openservices.tablestore.model.BatchWriteRowResponse; + +import java.util.concurrent.Callable; + +public class BatchWriteRowCallable implements Callable{ + + private SyncClientInterface ots = null; + private BatchWriteRowRequest batchWriteRowRequest = null; + + public BatchWriteRowCallable(SyncClientInterface ots, BatchWriteRowRequest batchWriteRowRequest) { + this.ots = ots; + this.batchWriteRowRequest = batchWriteRowRequest; + + } + + @Override + public BatchWriteRowResponse call() throws Exception { + return ots.batchWriteRow(batchWriteRowRequest); + } + +} \ No newline at end of file diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/GetTableMetaCallable.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/GetTableMetaCallable.java index d4128e14ce..b3b26d76e3 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/GetTableMetaCallable.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/GetTableMetaCallable.java @@ -1,29 +1,27 @@ package com.alibaba.datax.plugin.writer.otswriter.callable; -import java.util.concurrent.Callable; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.model.DescribeTableRequest; +import com.alicloud.openservices.tablestore.model.DescribeTableResponse; +import com.alicloud.openservices.tablestore.model.TableMeta; -import com.aliyun.openservices.ots.OTSClient; -import com.aliyun.openservices.ots.model.DescribeTableRequest; -import com.aliyun.openservices.ots.model.DescribeTableResult; -import com.aliyun.openservices.ots.model.TableMeta; +import java.util.concurrent.Callable; public class GetTableMetaCallable implements Callable{ - private OTSClient ots = null; + private SyncClientInterface ots = null; private String tableName = null; - public GetTableMetaCallable(OTSClient ots, String tableName) { + public GetTableMetaCallable(SyncClientInterface ots, String tableName) { this.ots = ots; this.tableName = tableName; } @Override public TableMeta call() throws Exception { - DescribeTableRequest describeTableRequest = new DescribeTableRequest(); - describeTableRequest.setTableName(tableName); - DescribeTableResult result = ots.describeTable(describeTableRequest); - TableMeta tableMeta = result.getTableMeta(); - return tableMeta; + DescribeTableRequest describeTableRequest = new DescribeTableRequest(tableName); + DescribeTableResponse result = ots.describeTable(describeTableRequest); + return result.getTableMeta(); } } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/GetTableMetaCallableOld.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/GetTableMetaCallableOld.java new file mode 100644 index 0000000000..af7d5088a9 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/GetTableMetaCallableOld.java @@ -0,0 +1,29 @@ +package com.alibaba.datax.plugin.writer.otswriter.callable; + +import com.aliyun.openservices.ots.OTSClient; +import com.aliyun.openservices.ots.model.DescribeTableRequest; +import com.aliyun.openservices.ots.model.DescribeTableResult; +import com.aliyun.openservices.ots.model.TableMeta; + +import java.util.concurrent.Callable; + +public class GetTableMetaCallableOld implements Callable{ + + private OTSClient ots = null; + private String tableName = null; + + public GetTableMetaCallableOld(OTSClient ots, String tableName) { + this.ots = ots; + this.tableName = tableName; + } + + @Override + public TableMeta call() throws Exception { + DescribeTableRequest describeTableRequest = new DescribeTableRequest(); + describeTableRequest.setTableName(tableName); + DescribeTableResult result = ots.describeTable(describeTableRequest); + TableMeta tableMeta = result.getTableMeta(); + return tableMeta; + } + +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/PutRowChangeCallable.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/PutRowChangeCallable.java new file mode 100644 index 0000000000..b385709499 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/PutRowChangeCallable.java @@ -0,0 +1,24 @@ +package com.alibaba.datax.plugin.writer.otswriter.callable; + +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.model.PutRowRequest; +import com.alicloud.openservices.tablestore.model.PutRowResponse; + +import java.util.concurrent.Callable; + +public class PutRowChangeCallable implements Callable{ + + private SyncClientInterface ots = null; + private PutRowRequest putRowRequest = null; + + public PutRowChangeCallable(SyncClientInterface ots, PutRowRequest putRowRequest) { + this.ots = ots; + this.putRowRequest = putRowRequest; + } + + @Override + public PutRowResponse call() throws Exception { + return ots.putRow(putRowRequest); + } + +} \ No newline at end of file diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/PutTimeseriesDataCallable.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/PutTimeseriesDataCallable.java new file mode 100644 index 0000000000..664f4b4177 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/PutTimeseriesDataCallable.java @@ -0,0 +1,22 @@ +package com.alibaba.datax.plugin.writer.otswriter.callable; + +import com.alicloud.openservices.tablestore.TimeseriesClient; +import com.alicloud.openservices.tablestore.model.timeseries.PutTimeseriesDataRequest; +import com.alicloud.openservices.tablestore.model.timeseries.PutTimeseriesDataResponse; + +import java.util.concurrent.Callable; + +public class PutTimeseriesDataCallable implements Callable { + private TimeseriesClient client = null; + private PutTimeseriesDataRequest putTimeseriesDataRequest = null; + + public PutTimeseriesDataCallable(TimeseriesClient client, PutTimeseriesDataRequest putTimeseriesDataRequest) { + this.client = client; + this.putTimeseriesDataRequest = putTimeseriesDataRequest; + } + + @Override + public PutTimeseriesDataResponse call() throws Exception { + return client.putTimeseriesData(putTimeseriesDataRequest); + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/UpdateRowChangeCallable.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/UpdateRowChangeCallable.java new file mode 100644 index 0000000000..c302e3a108 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/callable/UpdateRowChangeCallable.java @@ -0,0 +1,24 @@ +package com.alibaba.datax.plugin.writer.otswriter.callable; + +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.model.UpdateRowRequest; +import com.alicloud.openservices.tablestore.model.UpdateRowResponse; + +import java.util.concurrent.Callable; + +public class UpdateRowChangeCallable implements Callable{ + + private SyncClientInterface ots = null; + private UpdateRowRequest updateRowRequest = null; + + public UpdateRowChangeCallable(SyncClientInterface ots, UpdateRowRequest updateRowRequest ) { + this.ots = ots; + this.updateRowRequest = updateRowRequest; + } + + @Override + public UpdateRowResponse call() throws Exception { + return ots.updateRow(updateRowRequest); + } + +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/LogExceptionManager.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/LogExceptionManager.java deleted file mode 100644 index 93175ddb18..0000000000 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/LogExceptionManager.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.alibaba.datax.plugin.writer.otswriter.model; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.aliyun.openservices.ots.OTSErrorCode; -import com.aliyun.openservices.ots.OTSException; - -/** - * 添加这个类的主要目的是为了解决当用户遇到CU不够时,打印大量的日志 - * @author redchen - * - */ -public class LogExceptionManager { - - private long count = 0; - private long updateTimestamp = 0; - - private static final Logger LOG = LoggerFactory.getLogger(LogExceptionManager.class); - - private synchronized void countAndReset() { - count++; - long cur = System.currentTimeMillis(); - long interval = cur - updateTimestamp; - if (interval >= 10000) { - LOG.warn("Call callable fail, OTSNotEnoughCapacityUnit, total times:"+ count +", time range:"+ (interval/1000) +"s, times per second:" + ((float)count / (interval/1000))); - count = 0; - updateTimestamp = cur; - } - } - - public synchronized void addException(Exception exception) { - if (exception instanceof OTSException) { - OTSException e = (OTSException)exception; - if (e.getErrorCode().equals(OTSErrorCode.NOT_ENOUGH_CAPACITY_UNIT)) { - countAndReset(); - } else { - LOG.warn( - "Call callable fail, OTSException:ErrorCode:{}, ErrorMsg:{}, RequestId:{}", - new Object[]{e.getErrorCode(), e.getMessage(), e.getRequestId()} - ); - } - } else { - LOG.warn("Call callable fail, {}", exception.getMessage()); - } - } - - public synchronized void addException(com.aliyun.openservices.ots.model.Error error, String requestId) { - if (error.getCode().equals(OTSErrorCode.NOT_ENOUGH_CAPACITY_UNIT)) { - countAndReset(); - } else { - LOG.warn( - "OTSException:ErrorCode:{}, ErrorMsg:{}, RequestId:{}", - new Object[]{error.getCode(), error.getMessage(), requestId} - ); - } - } -} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSAttrColumn.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSAttrColumn.java index d37960e000..7564130ab0 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSAttrColumn.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSAttrColumn.java @@ -1,16 +1,33 @@ package com.alibaba.datax.plugin.writer.otswriter.model; -import com.aliyun.openservices.ots.model.ColumnType; +import com.alicloud.openservices.tablestore.model.ColumnType; + public class OTSAttrColumn { - private String name; - private ColumnType type; + // 该字段只在多版本中使用,表示多版本中,输入源中columnName的值,由将对应的Cell写入用户配置name的列中 + private String srcName = null; + private String name = null; + private ColumnType type = null; + //该字段只在写入时序表时使用,该字段是否为时序数据的标签内部字段 + private Boolean isTag = false; public OTSAttrColumn(String name, ColumnType type) { this.name = name; this.type = type; } + public OTSAttrColumn(String srcName, String name, ColumnType type) { + this.srcName = srcName; + this.name = name; + this.type = type; + } + + public OTSAttrColumn(String name, ColumnType type, Boolean isTag) { + this.name = name; + this.type = type; + this.isTag = isTag; + } + public String getName() { return name; } @@ -18,4 +35,12 @@ public String getName() { public ColumnType getType() { return type; } + + public String getSrcName() { + return srcName; + } + + public Boolean getTag() { + return isTag; + } } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSBatchWriteRowTaskManager.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSBatchWriteRowTaskManager.java new file mode 100644 index 0000000000..fdeb28251d --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSBatchWriteRowTaskManager.java @@ -0,0 +1,41 @@ +package com.alibaba.datax.plugin.writer.otswriter.model; + +import com.alicloud.openservices.tablestore.SyncClientInterface; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * 控制Task的并发数目 + * + */ +public class OTSBatchWriteRowTaskManager implements OTSTaskManagerInterface { + + private SyncClientInterface ots = null; + private OTSBlockingExecutor executorService = null; + private OTSConf conf = null; + + private static final Logger LOG = LoggerFactory.getLogger(OTSBatchWriteRowTaskManager.class); + + public OTSBatchWriteRowTaskManager( + SyncClientInterface ots, + OTSConf conf) { + this.ots = ots; + this.conf = conf; + + executorService = new OTSBlockingExecutor(conf.getConcurrencyWrite()); + } + + public void execute(List lines) throws Exception { + LOG.debug("Begin execute."); + executorService.execute(new OTSBatchWriterRowTask(ots, conf, lines)); + LOG.debug("End execute."); + } + + public void close() throws Exception { + LOG.debug("Begin close."); + executorService.shutdown(); + LOG.debug("End close."); + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSBatchWriterRowTask.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSBatchWriterRowTask.java new file mode 100644 index 0000000000..416526fdc2 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSBatchWriterRowTask.java @@ -0,0 +1,196 @@ +package com.alibaba.datax.plugin.writer.otswriter.model; + +import com.alibaba.datax.plugin.writer.otswriter.OTSCriticalException; +import com.alibaba.datax.plugin.writer.otswriter.OTSErrorCode; +import com.alibaba.datax.plugin.writer.otswriter.callable.BatchWriteRowCallable; +import com.alibaba.datax.plugin.writer.otswriter.callable.PutRowChangeCallable; +import com.alibaba.datax.plugin.writer.otswriter.callable.UpdateRowChangeCallable; +import com.alibaba.datax.plugin.writer.otswriter.utils.CollectorUtil; +import com.alibaba.datax.plugin.writer.otswriter.utils.Common; +import com.alibaba.datax.plugin.writer.otswriter.utils.LineAndError; +import com.alibaba.datax.plugin.writer.otswriter.utils.RetryHelper; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.TableStoreException; +import com.alicloud.openservices.tablestore.model.*; +import com.alicloud.openservices.tablestore.model.BatchWriteRowResponse.RowResult; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class OTSBatchWriterRowTask implements Runnable { + private SyncClientInterface ots = null; + private OTSConf conf = null; + private List otsLines = new ArrayList(); + + private boolean isDone = false; + private int retryTimes = 0; + + private static final Logger LOG = LoggerFactory.getLogger(OTSBatchWriterRowTask.class); + + public OTSBatchWriterRowTask( + final SyncClientInterface ots, + final OTSConf conf, + final List lines + ) { + this.ots = ots; + this.conf = conf; + + this.otsLines.addAll(lines); + } + + @Override + public void run() { + LOG.debug("Begin run"); + sendAll(otsLines); + LOG.debug("End run"); + } + + public boolean isDone() { + return this.isDone; + } + + private boolean isExceptionForSendOneByOne(TableStoreException ee) { + if (ee.getErrorCode().equals(OTSErrorCode.INVALID_PARAMETER)|| + ee.getErrorCode().equals(OTSErrorCode.REQUEST_TOO_LARGE) + ) { + return true; + } + return false; + } + + private BatchWriteRowRequest createRequest(List lines) { + BatchWriteRowRequest newRequest = new BatchWriteRowRequest(); + switch (conf.getOperation()) { + case PUT_ROW: + case UPDATE_ROW: + for (OTSLine l : lines) { + newRequest.addRowChange(l.getRowChange()); + } + break; + default: + throw new RuntimeException(String.format(OTSErrorMessage.OPERATION_PARSE_ERROR, conf.getOperation())); + } + return newRequest; + } + + /** + * 单行发送数据 + * @param line + */ + public void sendLine(OTSLine line) { + try { + switch (conf.getOperation()) { + case PUT_ROW: + PutRowRequest putRowRequest = new PutRowRequest(); + putRowRequest.setRowChange((RowPutChange) line.getRowChange()); + PutRowResponse putResult = RetryHelper.executeWithRetry( + new PutRowChangeCallable(ots, putRowRequest), + conf.getRetry(), + conf.getSleepInMillisecond()); + LOG.debug("Requst ID : {}", putResult.getRequestId()); + break; + case UPDATE_ROW: + UpdateRowRequest updateRowRequest = new UpdateRowRequest(); + updateRowRequest.setRowChange((RowUpdateChange) line.getRowChange()); + UpdateRowResponse updateResult = RetryHelper.executeWithRetry( + new UpdateRowChangeCallable(ots, updateRowRequest), + conf.getRetry(), + conf.getSleepInMillisecond()); + LOG.debug("Requst ID : {}", updateResult.getRequestId()); + break; + } + } catch (Exception e) { + LOG.warn("sendLine fail. ", e); + CollectorUtil.collect(line.getRecords(), e.getMessage()); + } + } + + private void sendAllOneByOne(List lines) { + for (OTSLine l : lines) { + sendLine(l); + } + } + + /** + * 批量发送数据 + * 如果程序发送失败,BatchWriteRow接口可能整体异常返回或者返回每个子行的操作状态 + * 1.在整体异常的情况下:方法会检查这个异常是否能通过把批量数据拆分成单行发送,如果不行, + * 将会把这一批数据记录到脏数据回收器中,如果可以,方法会调用sendAllOneByOne进行单行数据发送。 + * 2.如果BatchWriteRow成功执行,方法会检查每行的返回状态,如果子行操作失败,方法会收集所有失 + * 败的行,重新调用sendAll,发送失败的数据。 + * @param lines + */ + private void sendAll(List lines) { + try { + Thread.sleep(Common.getDelaySendMillinSeconds(retryTimes, conf.getSleepInMillisecond())); + BatchWriteRowRequest batchWriteRowRequest = createRequest(lines); + BatchWriteRowResponse result = RetryHelper.executeWithRetry( + new BatchWriteRowCallable(ots, batchWriteRowRequest), + conf.getRetry(), + conf.getSleepInMillisecond()); + + LOG.debug("Requst ID : {}", result.getRequestId()); + List errors = getLineAndError(result, lines); + if (!errors.isEmpty()){ + if(retryTimes < conf.getRetry()) { + retryTimes++; + LOG.warn("Retry times : {}", retryTimes); + List newLines = new ArrayList(); + for (LineAndError re : errors) { + LOG.warn("Because: {}", re.getError().getMessage()); + if (RetryHelper.canRetry(re.getError().getCode())) { + newLines.add(re.getLine()); + } else { + LOG.warn("Can not retry, record row to collector. {}", re.getError().getMessage()); + CollectorUtil.collect(re.getLine().getRecords(), re.getError().getMessage()); + } + } + if (!newLines.isEmpty()) { + sendAll(newLines); + } + } else { + LOG.warn("Retry times more than limitation. RetryTime : {}", retryTimes); + CollectorUtil.collect(errors); + } + } + } catch (TableStoreException e) { + LOG.warn("Send data fail. {}", e.getMessage()); + if (isExceptionForSendOneByOne(e)) { + if (lines.size() == 1) { + LOG.warn("Can not retry.", e); + CollectorUtil.collect(e.getMessage(), lines); + } else { + // 进入单行发送的分支 + sendAllOneByOne(lines); + } + } else { + LOG.error("Can not send lines to OTS for RuntimeException.", e); + CollectorUtil.collect(e.getMessage(), lines); + } + } catch (Exception e) { + LOG.error("Can not send lines to OTS for Exception.", e); + CollectorUtil.collect(e.getMessage(), lines); + } + } + + private List getLineAndError(BatchWriteRowResponse result, List lines) throws OTSCriticalException { + List errors = new ArrayList(); + + switch(conf.getOperation()) { + case PUT_ROW: + case UPDATE_ROW: { + List status = result.getFailedRows(); + for (RowResult r : status) { + errors.add(new LineAndError(lines.get(r.getIndex()), r.getError())); + } + } + break; + default: + LOG.error("Bug branch."); + throw new OTSCriticalException(String.format(OTSErrorMessage.OPERATION_PARSE_ERROR, conf.getOperation())); + } + return errors; + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSBlockingExecutor.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSBlockingExecutor.java new file mode 100644 index 0000000000..059ba33832 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSBlockingExecutor.java @@ -0,0 +1,55 @@ +package com.alibaba.datax.plugin.writer.otswriter.model; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.*; + +/** + * 单个Channel会多线程并发的写入数据到OTS中,需要使用一个固定的线程池来执行Runnable对象,同时当 + * 线程池满时,阻塞execute方法。原生的Executor并不能做到阻塞execute方法。只是当queue满时, + * 方法抛出默认RejectedExecutionException,或者我们实现RejectedExecutionHandler, + * 这两种方法都无法满足阻塞用户请求的需求,所以我们用信号量来实现了一个阻塞的Executor + * @author redchen + * + */ +public class OTSBlockingExecutor { + private final ExecutorService exec; + private final Semaphore semaphore; + + private static final Logger LOG = LoggerFactory.getLogger(OTSBlockingExecutor.class); + + public OTSBlockingExecutor(int concurrency) { + this.exec = new ThreadPoolExecutor( + concurrency, concurrency, + 0L, TimeUnit.SECONDS, + new LinkedBlockingQueue()); + this.semaphore = new Semaphore(concurrency); + } + + public void execute(final Runnable task) + throws InterruptedException { + LOG.debug("Begin execute"); + try { + semaphore.acquire(); + exec.execute(new Runnable() { + public void run() { + try { + task.run(); + } finally { + semaphore.release(); + } + } + }); + } catch (RejectedExecutionException e) { + semaphore.release(); + throw new RuntimeException(OTSErrorMessage.INSERT_TASK_ERROR); + } + LOG.debug("End execute"); + } + + public void shutdown() throws InterruptedException { + this.exec.shutdown(); + while (!this.exec.awaitTermination(1, TimeUnit.SECONDS)){} + } +} \ No newline at end of file diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSConf.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSConf.java index bd7eccc5a4..fee9ed551b 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSConf.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSConf.java @@ -1,73 +1,51 @@ package com.alibaba.datax.plugin.writer.otswriter.model; +import com.alicloud.openservices.tablestore.model.PrimaryKeySchema; + import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; public class OTSConf { - private String endpoint; - private String accessId; - private String accessKey; - private String instanceName; - private String tableName; - - private List primaryKeyColumn; - private List attributeColumn; + private String endpoint= null; + private String accessId = null; + private String accessKey = null; + private String instanceName = null; + private String tableName = null; - private int bufferSize = 1024; - private int retry = 18; - private int sleepInMillisecond = 100; - private int batchWriteCount = 10; - private int concurrencyWrite = 5; - private int ioThreadCount = 1; - private int socketTimeout = 20000; - private int connectTimeout = 10000; + + private List primaryKeyColumn = null; + private List attributeColumn = null; + + private int retry = -1; + private int sleepInMillisecond = -1; + private int batchWriteCount = -1; + private int concurrencyWrite = -1; + private int ioThreadCount = -1; + private int socketTimeoutInMillisecond = -1; + private int connectTimeoutInMillisecond = -1; - private OTSOpType operation; - private RestrictConf restrictConf; - - //限制项 - public class RestrictConf { - private int requestTotalSizeLimition = 1024 * 1024; - private int primaryKeyColumnSize = 1024; - private int attributeColumnSize = 2 * 1024 * 1024; - private int maxColumnsCount = 1024; - - public int getRequestTotalSizeLimition() { - return requestTotalSizeLimition; - } - public void setRequestTotalSizeLimition(int requestTotalSizeLimition) { - this.requestTotalSizeLimition = requestTotalSizeLimition; - } - - public void setPrimaryKeyColumnSize(int primaryKeyColumnSize) { - this.primaryKeyColumnSize = primaryKeyColumnSize; - } - - public void setAttributeColumnSize(int attributeColumnSize) { - this.attributeColumnSize = attributeColumnSize; - } + private OTSOpType operation = null; - public void setMaxColumnsCount(int maxColumnsCount) { - this.maxColumnsCount = maxColumnsCount; - } - - public int getAttributeColumnSize() { - return attributeColumnSize; - } - - public int getMaxColumnsCount() { - return maxColumnsCount; - } + private int requestTotalSizeLimitation = -1; + + private OTSMode mode = null; + private boolean enableAutoIncrement = false; + private boolean isNewVersion = false; + private boolean isTimeseriesTable = false; + private TimeUnit timeUnit = TimeUnit.MICROSECONDS; + private long timestamp = -1; + private Map encodePkColumnMapping = null; + private String columnNamePrefixFilter = null; - public int getPrimaryKeyColumnSize() { - return primaryKeyColumnSize; - } + public Map getEncodePkColumnMapping() { + return encodePkColumnMapping; } - - public RestrictConf getRestrictConf() { - return restrictConf; + public void setEncodePkColumnMapping(Map encodePkColumnMapping) { + this.encodePkColumnMapping = encodePkColumnMapping; } - public void setRestrictConf(RestrictConf restrictConf) { - this.restrictConf = restrictConf; + public int getSocketTimeoutInMillisecond() { + return socketTimeoutInMillisecond; } public OTSOpType getOperation() { return operation; @@ -75,10 +53,10 @@ public OTSOpType getOperation() { public void setOperation(OTSOpType operation) { this.operation = operation; } - public List getPrimaryKeyColumn() { + public List getPrimaryKeyColumn() { return primaryKeyColumn; } - public void setPrimaryKeyColumn(List primaryKeyColumn) { + public void setPrimaryKeyColumn(List primaryKeyColumn) { this.primaryKeyColumn = primaryKeyColumn; } @@ -149,24 +127,72 @@ public void setIoThreadCount(int ioThreadCount) { this.ioThreadCount = ioThreadCount; } public int getSocketTimeout() { - return socketTimeout; + return socketTimeoutInMillisecond; + } + public void setSocketTimeoutInMillisecond(int socketTimeoutInMillisecond) { + this.socketTimeoutInMillisecond = socketTimeoutInMillisecond; + } + public int getConnectTimeoutInMillisecond() { + return connectTimeoutInMillisecond; + } + public void setConnectTimeoutInMillisecond(int connectTimeoutInMillisecond) { + this.connectTimeoutInMillisecond = connectTimeoutInMillisecond; + } + public OTSMode getMode() { + return mode; } - public void setSocketTimeout(int socketTimeout) { - this.socketTimeout = socketTimeout; + public void setMode(OTSMode mode) { + this.mode = mode; } - public int getConnectTimeout() { - return connectTimeout; + public long getTimestamp() { + return timestamp; + } + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + public String getColumnNamePrefixFilter() { + return columnNamePrefixFilter; + } + public void setColumnNamePrefixFilter(String columnNamePrefixFilter) { + this.columnNamePrefixFilter = columnNamePrefixFilter; + } + + public boolean getEnableAutoIncrement() { + return enableAutoIncrement; + } + + public void setEnableAutoIncrement(boolean enableAutoIncrement) { + this.enableAutoIncrement = enableAutoIncrement; + } + public boolean isNewVersion() { + return isNewVersion; + } + + public void setNewVersion(boolean newVersion) { + isNewVersion = newVersion; + } + + public boolean isTimeseriesTable() { + return isTimeseriesTable; + } + + public void setTimeseriesTable(boolean timeseriesTable) { + isTimeseriesTable = timeseriesTable; + } + + public TimeUnit getTimeUnit() { + return timeUnit; } - public int getBufferSize() { - return bufferSize; + public void setTimeUnit(TimeUnit timeUnit) { + this.timeUnit = timeUnit; } - public void setBufferSize(int bufferSize) { - this.bufferSize = bufferSize; + public int getRequestTotalSizeLimitation() { + return requestTotalSizeLimitation; } - public void setConnectTimeout(int connectTimeout) { - this.connectTimeout = connectTimeout; + public void setRequestTotalSizeLimitation(int requestTotalSizeLimitation) { + this.requestTotalSizeLimitation = requestTotalSizeLimitation; } } \ No newline at end of file diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSConst.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSConst.java index 1b8f805374..bda736e8e9 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSConst.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSConst.java @@ -2,20 +2,27 @@ public class OTSConst { // Reader support type - public final static String TYPE_STRING = "STRING"; + public final static String TYPE_STRING = "STRING"; public final static String TYPE_INTEGER = "INT"; - public final static String TYPE_DOUBLE = "DOUBLE"; + public final static String TYPE_DOUBLE = "DOUBLE"; public final static String TYPE_BOOLEAN = "BOOL"; - public final static String TYPE_BINARY = "BINARY"; - + public final static String TYPE_BINARY = "BINARY"; + // Column public final static String NAME = "name"; + public final static String SRC_NAME = "srcName"; public final static String TYPE = "type"; - + public final static String IS_TAG = "is_timeseries_tag"; + public final static String OTS_CONF = "OTS_CONF"; - + + public final static String OTS_MODE_NORMAL = "normal"; + public final static String OTS_MODE_MULTI_VERSION = "multiVersion"; + public final static String OTS_MODE_TIME_SERIES = "timeseries"; + public final static String OTS_OP_TYPE_PUT = "PutRow"; public final static String OTS_OP_TYPE_UPDATE = "UpdateRow"; + // only support in old version public final static String OTS_OP_TYPE_DELETE = "DeleteRow"; // options @@ -24,13 +31,13 @@ public class OTSConst { public final static String BATCH_WRITE_COUNT = "batchWriteCount"; public final static String CONCURRENCY_WRITE = "concurrencyWrite"; public final static String IO_THREAD_COUNT = "ioThreadCount"; - public final static String SOCKET_TIMEOUT = "socketTimeoutInMillisecond"; - public final static String CONNECT_TIMEOUT = "connectTimeoutInMillisecond"; - public final static String BUFFER_SIZE = "bufferSize"; - - // 限制项 + public final static String MAX_CONNECT_COUNT = "maxConnectCount"; + public final static String SOCKET_TIMEOUTIN_MILLISECOND = "socketTimeoutInMillisecond"; + public final static String CONNECT_TIMEOUT_IN_MILLISECOND = "connectTimeoutInMillisecond"; public final static String REQUEST_TOTAL_SIZE_LIMITATION = "requestTotalSizeLimitation"; - public final static String ATTRIBUTE_COLUMN_SIZE_LIMITATION = "attributeColumnSizeLimitation"; - public final static String PRIMARY_KEY_COLUMN_SIZE_LIMITATION = "primaryKeyColumnSizeLimitation"; - public final static String ATTRIBUTE_COLUMN_MAX_COUNT = "attributeColumnMaxCount"; -} \ No newline at end of file + + public static final String MEASUREMENT_NAME = "_m_name"; + public static final String DATA_SOURCE = "_data_source"; + public static final String TAGS = "_tags"; + public static final String TIME = "_time"; +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSErrorMessage.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSErrorMessage.java index 9523342fa4..4bde553ad1 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSErrorMessage.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSErrorMessage.java @@ -2,13 +2,19 @@ public class OTSErrorMessage { - public static final String OPERATION_PARSE_ERROR = "The 'writeMode' only support 'PutRow', 'UpdateRow' or 'DeleteRow', not '%s'."; + public static final String MODE_PARSE_ERROR = "The 'mode' only support 'normal' and 'multiVersion' not '%s'."; + + public static final String OPERATION_PARSE_ERROR = "The 'writeMode' only support 'PutRow' and 'UpdateRow' not '%s'."; + + public static final String MUTLI_MODE_OPERATION_PARSE_ERROR = "When configurion set mode='MultiVersion', the 'writeMode' only support 'UpdateRow' not '%s'."; public static final String UNSUPPORT_PARSE = "Unsupport parse '%s' to '%s'."; - public static final String RECORD_AND_COLUMN_SIZE_ERROR = "Size of record not equal size of config column. record size : %d, config column size : %d."; + public static final String UNSUPPORT = "Unsupport : '%s'."; - public static final String PK_TYPE_ERROR = "Primary key type only support 'string' and 'int', not support '%s'."; + public static final String RECORD_AND_COLUMN_SIZE_ERROR = "Size of record not equal size of config column. record size : %d, config column size : %d, record data : %s."; + + public static final String PK_TYPE_ERROR = "Primary key type only support 'string', 'int' and 'binary', not support '%s'."; public static final String ATTR_TYPE_ERROR = "Column type only support 'string','int','double','bool' and 'binary', not support '%s'."; @@ -17,7 +23,9 @@ public class OTSErrorMessage { public static final String INPUT_PK_COUNT_NOT_EQUAL_META_ERROR = "The count of 'primaryKey' not equal meta, input count : %d, primary key count : %d in meta."; public static final String INPUT_PK_TYPE_NOT_MATCH_META_ERROR = "The type of 'primaryKey' not match meta, column name : %s, input type: %s, primary key type : %s in meta."; - + + public static final String INPUT_PK_NAME_NOT_EXIST_IN_META_ERROR = "The input primary column '%s' is not exist in meta."; + public static final String ATTR_REPEAT_COLUMN_ERROR = "Repeat column '%s' in 'column'."; public static final String MISSING_PARAMTER_ERROR = "The param '%s' is not exist."; @@ -36,25 +44,49 @@ public class OTSErrorMessage { public static final String ATTR_MAP_NAME_TYPE_ERROR = "The 'name' and 'type only support string in json map of 'column'."; + public static final String ATTR_MAP_SRCNAME_NAME_TYPE_ERROR = "The 'srcName', 'name' and 'type' only support string in json map of 'column'."; + + public static final String PK_MAP_KEY_TYPE_ERROR = "The '%s' only support string in json map of 'primaryKey'."; + + public static final String ATTR_MAP_KEY_TYPE_ERROR = "The '%s' only support string in json map of 'column'."; + public static final String PK_MAP_INCLUDE_NAME_TYPE_ERROR = "The only support 'name' and 'type' fileds in json map of 'primaryKey'."; public static final String ATTR_MAP_INCLUDE_NAME_TYPE_ERROR = "The only support 'name' and 'type' fileds in json map of 'column'."; - public static final String PK_ITEM_IS_NOT_MAP_ERROR = "The item is not map in 'primaryKey'."; + public static final String PK_MAP_FILED_MISSING_ERROR = "The '%s' fileds is missing in json map of 'primaryKey'."; + + public static final String ATTR_MAP_FILED_MISSING_ERROR = "The '%s' fileds is missing in json map of 'column'."; + + public static final String ATTR_MAP_INCLUDE_SRCNAME_NAME_TYPE_ERROR = "The only support 'srcName', 'name' and 'type' fileds in json map of 'column'."; + + public static final String PK_ITEM_IS_ILLEAGAL_ERROR = "The item is not string or map in 'primaryKey'."; + + public static final String PK_IS_NOT_EXIST_AT_OTS_ERROR = "Can not find the pk('%s') at ots in 'primaryKey'."; public static final String ATTR_ITEM_IS_NOT_MAP_ERROR = "The item is not map in 'column'."; public static final String PK_COLUMN_NAME_IS_EMPTY_ERROR = "The name of item can not be a empty string in 'primaryKey'."; + public static final String PK_COLUMN_TYPE_IS_EMPTY_ERROR = "The type of item can not be a empty string in 'primaryKey'."; + public static final String ATTR_COLUMN_NAME_IS_EMPTY_ERROR = "The name of item can not be a empty string in 'column'."; - public static final String MULTI_ATTR_COLUMN_ERROR = "Multi item in 'column', column name : %s ."; + public static final String ATTR_COLUMN_SRC_NAME_IS_EMPTY_ERROR = "The srcName of item can not be a empty string in 'column'."; + + public static final String ATTR_COLUMN_TYPE_IS_EMPTY_ERROR = "The type of item can not be a empty string in 'column'."; + + public static final String MULTI_PK_ATTR_COLUMN_ERROR = "Duplicate item in 'column' and 'primaryKey', column name : %s ."; + + public static final String MULTI_ATTR_COLUMN_ERROR = "Duplicate item in 'column', column name : %s ."; + + public static final String MULTI_ATTR_SRC_COLUMN_ERROR = "Duplicate src name in 'column', src name : %s ."; public static final String COLUMN_CONVERSION_ERROR = "Column coversion error, src type : %s, src value: %s, expect type: %s ."; public static final String PK_COLUMN_VALUE_IS_NULL_ERROR = "The column of record is NULL, primary key name : %s ."; - public static final String PK_STRONG_LENGTH_ERROR = "The length of pk string value is more than configuration, conf: %d, input: %d ."; + public static final String PK_STRING_LENGTH_ERROR = "The length of pk string value is more than configuration, conf: %d, input: %d ."; public static final String ATTR_STRING_LENGTH_ERROR = "The length of attr string value is more than configuration, conf: %d, input: %d ."; @@ -63,4 +95,31 @@ public class OTSErrorMessage { public static final String LINE_LENGTH_ERROR = "The length of row is more than length of request configuration, conf: %d, row: %d ."; public static final String INSERT_TASK_ERROR = "Can not execute the task, becase the ExecutorService is shutdown."; + + public static final String COLUMN_NOT_DEFINE = "The column name : '%s' not define in column."; + + public static final String INPUT_RECORDS_IS_EMPTY = "The input records can not be empty."; + + public static final String MULTI_VERSION_TIMESTAMP_IS_EMPTY = "The input timestamp can not be empty in the multiVersion mode."; + + public static final String MULTI_VERSION_VALUE_IS_EMPTY = "The input value can not be empty in the multiVersion mode."; + + public static final String INPUT_COLUMN_COUNT_LIMIT = "The input count(%d) of column more than max(%d)."; + + public static final String PUBLIC_SDK_NO_SUPPORT_MULTI_VERSION = "The old version do not support multi version function. Please add config in otswriter: \"newVersion\":\"true\" ."; + + public static final String PUBLIC_SDK_NO_SUPPORT_AUTO_INCREMENT = "The old version do not support auto increment primary key function. Please add config in otswriter: \"newVersion\":\"true\" ."; + + public static final String NOT_SUPPORT_MULTI_VERSION_AUTO_INCREMENT = "The multi version mode do not support auto increment primary key function."; + + public static final String PUBLIC_SDK_NO_SUPPORT_TIMESERIES_TABLE = "The old version do not support write timeseries table. Please add config in otswriter: \"newVersion\":\"true\" ."; + + public static final String NOT_SUPPORT_TIMESERIES_TABLE_AUTO_INCREMENT = "The timeseries table do not support auto increment primary key function."; + + public static final String NO_FOUND_M_NAME_FIELD_ERROR = "The '_m_name' field should be set in columns because 'measurement' is required in timeseries data."; + + public static final String NO_FOUND_TIME_FIELD_ERROR = "The '_time' field should be set in columns because 'time' is required in timeseries data."; + + public static final String TIMEUNIT_FORMAT_ERROR = "The value of param 'timeunit' is '%s', which should be in ['NANOSECONDS', 'MICROSECONDS', 'MILLISECONDS', 'SECONDS', 'MINUTES']."; + } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSLine.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSLine.java new file mode 100644 index 0000000000..7be4a1a8f0 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSLine.java @@ -0,0 +1,85 @@ +package com.alibaba.datax.plugin.writer.otswriter.model; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.plugin.writer.otswriter.OTSCriticalException; +import com.alibaba.datax.plugin.writer.otswriter.utils.CalculateHelper; +import com.alicloud.openservices.tablestore.model.PrimaryKey; +import com.alicloud.openservices.tablestore.model.RowChange; +import com.alicloud.openservices.tablestore.model.RowPutChange; +import com.alicloud.openservices.tablestore.model.RowUpdateChange; +import com.alicloud.openservices.tablestore.model.timeseries.TimeseriesRow; + +import java.util.ArrayList; +import java.util.List; + +public class OTSLine { + private int dataSize = 0; + + private PrimaryKey pk = null; + private RowChange change = null; + private TimeseriesRow timeseriesRow = null; + + private List records = new ArrayList(); + + public OTSLine( + PrimaryKey pk, + List records, + RowChange change) throws OTSCriticalException { + this.pk = pk; + this.change = change; + this.records.addAll(records); + setSize(this.change); + } + + public OTSLine( + PrimaryKey pk, + Record record, + RowChange change) throws OTSCriticalException { + this.pk = pk; + this.change = change; + this.records.add(record); + setSize(this.change); + } + + public OTSLine( + Record record, + TimeseriesRow row) throws OTSCriticalException { + this.timeseriesRow = row; + this.records.add(record); + setSize(this.timeseriesRow); + } + + private void setSize(RowChange change) throws OTSCriticalException { + if (change instanceof RowPutChange) { + this.dataSize = CalculateHelper.getRowPutChangeSize((RowPutChange) change); + } else if (change instanceof RowUpdateChange) { + this.dataSize = CalculateHelper.getRowUpdateChangeSize((RowUpdateChange) change); + } else { + throw new RuntimeException(String.format(OTSErrorMessage.UNSUPPORT_PARSE, change.getClass().toString(), "RowPutChange or RowUpdateChange")); + } + } + + private void setSize(TimeseriesRow row) throws OTSCriticalException { + this.dataSize = CalculateHelper.getTimeseriesRowDataSize(row); + } + + public List getRecords() { + return records; + } + + public PrimaryKey getPk() { + return pk; + } + + public int getDataSize() { + return dataSize; + } + + public RowChange getRowChange() { + return change; + } + + public TimeseriesRow getTimeseriesRow() { + return timeseriesRow; + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSMode.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSMode.java new file mode 100644 index 0000000000..530ad5de54 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSMode.java @@ -0,0 +1,6 @@ +package com.alibaba.datax.plugin.writer.otswriter.model; + +public enum OTSMode { + NORMAL, // 普通模式 + MULTI_VERSION // 多版本模式 +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSOpType.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSOpType.java index 17b650331e..80d70d6dff 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSOpType.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSOpType.java @@ -3,5 +3,6 @@ public enum OTSOpType { PUT_ROW, UPDATE_ROW, + @Deprecated DELETE_ROW } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSPKColumn.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSPKColumn.java deleted file mode 100644 index c873cb9637..0000000000 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSPKColumn.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.alibaba.datax.plugin.writer.otswriter.model; - -import com.aliyun.openservices.ots.model.PrimaryKeyType; - -public class OTSPKColumn { - private String name; - private PrimaryKeyType type; - - public OTSPKColumn(String name, PrimaryKeyType type) { - this.name = name; - this.type = type; - } - - public PrimaryKeyType getType() { - return type; - } - - public String getName() { - return name; - } - -} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSRowPrimaryKey.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSRowPrimaryKey.java deleted file mode 100644 index d89d501779..0000000000 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSRowPrimaryKey.java +++ /dev/null @@ -1,61 +0,0 @@ -package com.alibaba.datax.plugin.writer.otswriter.model; - -import java.util.Map; -import java.util.Map.Entry; - -import com.aliyun.openservices.ots.model.PrimaryKeyValue; - -public class OTSRowPrimaryKey { - - private Map columns; - - public OTSRowPrimaryKey(Map columns) { - if (null == columns) { - throw new IllegalArgumentException("Input columns can not be null."); - } - this.columns = columns; - } - - public Map getColumns() { - return columns; - } - - @Override - public int hashCode() { - int result = 31; - for (Entry entry : columns.entrySet()) { - result = result ^ entry.getKey().hashCode() ^ entry.getValue().hashCode(); - } - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof OTSRowPrimaryKey)) { - return false; - } - OTSRowPrimaryKey other = (OTSRowPrimaryKey) obj; - - if (columns.size() != other.columns.size()) { - return false; - } - - for (Entry entry : columns.entrySet()) { - PrimaryKeyValue otherValue = other.columns.get(entry.getKey()); - - if (otherValue == null) { - return false; - } - if (!otherValue.equals(entry.getValue())) { - return false; - } - } - return true; - } -} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSSendBuffer.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSSendBuffer.java new file mode 100644 index 0000000000..f85b2c1666 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSSendBuffer.java @@ -0,0 +1,82 @@ +package com.alibaba.datax.plugin.writer.otswriter.model; + +import com.alibaba.datax.plugin.writer.otswriter.OTSCriticalException; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class OTSSendBuffer { + + private OTSConf conf = null; + private OTSTaskManagerInterface manager = null; + + private int totalSize = 0; + private List buffer = new ArrayList(); + + + private static final Logger LOG = LoggerFactory.getLogger(OTSSendBuffer.class); + + public OTSSendBuffer( + SyncClientInterface ots, + OTSConf conf) { + this.conf = conf; + if (conf.isTimeseriesTable()){ + this.manager = new OTSTimeseriesRowTaskManager(ots, conf); + } + else { + this.manager = new OTSBatchWriteRowTaskManager(ots, conf); + } + + } + + public void write(OTSLine line) throws OTSCriticalException { + LOG.debug("write begin"); + // 检查是否满足发送条件 + if (buffer.size() >= conf.getBatchWriteCount() || + ((totalSize + line.getDataSize()) > conf.getRequestTotalSizeLimitation() && totalSize > 0) + ) { + try { + manager.execute(new ArrayList(buffer)); + } catch (Exception e) { + LOG.error("OTSBatchWriteRowTaskManager execute fail : {}", e.getMessage(), e); + throw new OTSCriticalException(e); + } + buffer.clear(); + totalSize = 0; + } + buffer.add(line); + totalSize += line.getDataSize(); + LOG.debug("write end"); + } + + public void flush() throws OTSCriticalException { + LOG.debug("flush begin"); + if (!buffer.isEmpty()) { + try { + manager.execute(new ArrayList(buffer)); + } catch (Exception e) { + LOG.error("OTSBatchWriteRowTaskManager flush fail : {}", e.getMessage(), e); + throw new OTSCriticalException(e); + } + } + LOG.debug("flush end"); + } + + public void close() throws OTSCriticalException { + LOG.debug("close begin"); + try { + flush(); + } finally { + try { + manager.close(); + } catch (Exception e) { + LOG.error("OTSBatchWriteRowTaskManager close fail : {}", e.getMessage(), e); + throw new OTSCriticalException(e); + } + } + LOG.debug("close end"); + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSTaskManagerInterface.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSTaskManagerInterface.java new file mode 100644 index 0000000000..5db85d7d4e --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSTaskManagerInterface.java @@ -0,0 +1,9 @@ +package com.alibaba.datax.plugin.writer.otswriter.model; + +import java.util.List; + +public interface OTSTaskManagerInterface { + public void execute(List lines) throws Exception; + + public void close() throws Exception; +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSTimeseriesRowTask.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSTimeseriesRowTask.java new file mode 100644 index 0000000000..7cda8e331b --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSTimeseriesRowTask.java @@ -0,0 +1,167 @@ +package com.alibaba.datax.plugin.writer.otswriter.model; + +import com.alibaba.datax.plugin.writer.otswriter.OTSCriticalException; +import com.alibaba.datax.plugin.writer.otswriter.OTSErrorCode; +import com.alibaba.datax.plugin.writer.otswriter.callable.PutTimeseriesDataCallable; +import com.alibaba.datax.plugin.writer.otswriter.utils.CollectorUtil; +import com.alibaba.datax.plugin.writer.otswriter.utils.Common; +import com.alibaba.datax.plugin.writer.otswriter.utils.LineAndError; +import com.alibaba.datax.plugin.writer.otswriter.utils.RetryHelper; +import com.alicloud.openservices.tablestore.TableStoreException; +import com.alicloud.openservices.tablestore.TimeseriesClient; +import com.alicloud.openservices.tablestore.model.PutRowRequest; +import com.alicloud.openservices.tablestore.model.timeseries.PutTimeseriesDataRequest; +import com.alicloud.openservices.tablestore.model.timeseries.PutTimeseriesDataResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class OTSTimeseriesRowTask implements Runnable { + private static final Logger LOG = LoggerFactory.getLogger(OTSTimeseriesRowTask.class); + private TimeseriesClient client = null; + private OTSConf conf = null; + private List otsLines = new ArrayList(); + private boolean isDone = false; + private int retryTimes = 0; + + public OTSTimeseriesRowTask( + final TimeseriesClient client, + final OTSConf conf, + final List lines + ) { + this.client = client; + this.conf = conf; + + this.otsLines.addAll(lines); + } + + @Override + public void run() { + LOG.debug("Begin run"); + sendAll(otsLines); + LOG.debug("End run"); + } + + public boolean isDone() { + return this.isDone; + } + + private boolean isExceptionForSendOneByOne(TableStoreException ee) { + if (ee.getErrorCode().equals(OTSErrorCode.INVALID_PARAMETER) || + ee.getErrorCode().equals(OTSErrorCode.REQUEST_TOO_LARGE) + ) { + return true; + } + return false; + } + + private PutTimeseriesDataRequest createRequest(List lines) { + PutTimeseriesDataRequest newRequest = new PutTimeseriesDataRequest(conf.getTableName()); + for (OTSLine l : lines) { + newRequest.addRow(l.getTimeseriesRow()); + } + return newRequest; + } + + /** + * 单行发送数据 + * + * @param line + */ + public void sendLine(OTSLine line) { + try { + PutTimeseriesDataRequest putTimeseriesDataRequest = new PutTimeseriesDataRequest(conf.getTableName()); + putTimeseriesDataRequest.addRow(line.getTimeseriesRow()); + PutTimeseriesDataResponse result = RetryHelper.executeWithRetry( + new PutTimeseriesDataCallable(client, putTimeseriesDataRequest), + conf.getRetry(), + conf.getSleepInMillisecond()); + + + if (!result.isAllSuccess()){ + String errMsg = result.getFailedRows().get(0).getError().getMessage(); + LOG.warn("sendLine fail. " + errMsg); + CollectorUtil.collect(line.getRecords(), errMsg); + + }else { + LOG.debug("Request ID : {}", result.getRequestId()); + } + + } catch (Exception e) { + LOG.warn("sendLine fail. ", e); + CollectorUtil.collect(line.getRecords(), e.getMessage()); + } + } + + private void sendAllOneByOne(List lines) { + for (OTSLine l : lines) { + sendLine(l); + } + } + + private void sendAll(List lines) { + try { + Thread.sleep(Common.getDelaySendMillinSeconds(retryTimes, conf.getSleepInMillisecond())); + PutTimeseriesDataRequest putTimeseriesDataRequest = createRequest(lines); + PutTimeseriesDataResponse result = RetryHelper.executeWithRetry( + new PutTimeseriesDataCallable(client, putTimeseriesDataRequest), + conf.getRetry(), + conf.getSleepInMillisecond()); + + LOG.debug("Request ID : {}", result.getRequestId()); + List errors = getLineAndError(result, lines); + if (!errors.isEmpty()) { + if (retryTimes < conf.getRetry()) { + retryTimes++; + LOG.warn("Retry times : {}", retryTimes); + List newLines = new ArrayList(); + for (LineAndError re : errors) { + LOG.warn("Because: {}", re.getError().getMessage()); + if (RetryHelper.canRetry(re.getError().getCode())) { + newLines.add(re.getLine()); + } else { + LOG.warn("Can not retry, record row to collector. {}", re.getError().getMessage()); + CollectorUtil.collect(re.getLine().getRecords(), re.getError().getMessage()); + } + } + if (!newLines.isEmpty()) { + sendAll(newLines); + } + } else { + LOG.warn("Retry times more than limitation. RetryTime : {}", retryTimes); + CollectorUtil.collect(errors); + } + } + } catch (TableStoreException e) { + LOG.warn("Send data fail. {}", e.getMessage()); + if (isExceptionForSendOneByOne(e)) { + if (lines.size() == 1) { + LOG.warn("Can not retry.", e); + CollectorUtil.collect(e.getMessage(), lines); + } else { + // 进入单行发送的分支 + sendAllOneByOne(lines); + } + } else { + LOG.error("Can not send lines to OTS for RuntimeException.", e); + CollectorUtil.collect(e.getMessage(), lines); + } + } catch (Exception e) { + LOG.error("Can not send lines to OTS for Exception.", e); + CollectorUtil.collect(e.getMessage(), lines); + } + } + + private List getLineAndError(PutTimeseriesDataResponse result, List lines) throws OTSCriticalException { + List errors = new ArrayList(); + + List status = result.getFailedRows(); + for (PutTimeseriesDataResponse.FailedRowResult r : status) { + errors.add(new LineAndError(lines.get(r.getIndex()), r.getError())); + } + + return errors; + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSTimeseriesRowTaskManager.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSTimeseriesRowTaskManager.java new file mode 100644 index 0000000000..2816d95512 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/OTSTimeseriesRowTaskManager.java @@ -0,0 +1,41 @@ +package com.alibaba.datax.plugin.writer.otswriter.model; + +import com.alicloud.openservices.tablestore.SyncClient; +import com.alicloud.openservices.tablestore.SyncClientInterface; +import com.alicloud.openservices.tablestore.TimeseriesClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public class OTSTimeseriesRowTaskManager implements OTSTaskManagerInterface{ + + private TimeseriesClient client = null; + private OTSBlockingExecutor executorService = null; + private OTSConf conf = null; + + private static final Logger LOG = LoggerFactory.getLogger(OTSTimeseriesRowTaskManager.class); + + public OTSTimeseriesRowTaskManager( + SyncClientInterface ots, + OTSConf conf) { + this.client = ((SyncClient)ots).asTimeseriesClient(); + this.conf = conf; + + executorService = new OTSBlockingExecutor(conf.getConcurrencyWrite()); + } + + @Override + public void execute(List lines) throws Exception { + LOG.debug("Begin execute."); + executorService.execute(new OTSTimeseriesRowTask(client, conf, lines)); + LOG.debug("End execute."); + } + + @Override + public void close() throws Exception { + LOG.debug("Begin close."); + executorService.shutdown(); + LOG.debug("End close."); + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/RowDeleteChangeWithRecord.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/RowDeleteChangeWithRecord.java index 5d77ad8792..1986100a22 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/RowDeleteChangeWithRecord.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/RowDeleteChangeWithRecord.java @@ -1,6 +1,7 @@ package com.alibaba.datax.plugin.writer.otswriter.model; import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.plugin.writer.otswriter.utils.WithRecord; public class RowDeleteChangeWithRecord extends com.aliyun.openservices.ots.model.RowDeleteChange implements WithRecord { diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/RowPutChangeWithRecord.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/RowPutChangeWithRecord.java index e97a7d63c0..2e19dd77e7 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/RowPutChangeWithRecord.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/RowPutChangeWithRecord.java @@ -1,6 +1,7 @@ package com.alibaba.datax.plugin.writer.otswriter.model; import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.plugin.writer.otswriter.utils.WithRecord; public class RowPutChangeWithRecord extends com.aliyun.openservices.ots.model.RowPutChange implements WithRecord { diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/RowUpdateChangeWithRecord.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/RowUpdateChangeWithRecord.java index f47ca1d294..63f27d65db 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/RowUpdateChangeWithRecord.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/RowUpdateChangeWithRecord.java @@ -1,6 +1,7 @@ package com.alibaba.datax.plugin.writer.otswriter.model; import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.plugin.writer.otswriter.utils.WithRecord; public class RowUpdateChangeWithRecord extends com.aliyun.openservices.ots.model.RowUpdateChange implements WithRecord { diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/CalculateHelper.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/CalculateHelper.java new file mode 100644 index 0000000000..f0d8347de7 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/CalculateHelper.java @@ -0,0 +1,171 @@ +package com.alibaba.datax.plugin.writer.otswriter.utils; + +import com.alibaba.datax.plugin.writer.otswriter.OTSCriticalException; +import com.alicloud.openservices.tablestore.core.utils.Pair; +import com.alicloud.openservices.tablestore.model.*; +import com.alicloud.openservices.tablestore.model.timeseries.TimeseriesKey; +import com.alicloud.openservices.tablestore.model.timeseries.TimeseriesRow; + +import java.util.List; +import java.util.Map; + +import static com.alicloud.openservices.tablestore.model.PrimaryKeyValue.AUTO_INCREMENT; + +public class CalculateHelper { + private static int getPrimaryKeyValueSize(PrimaryKeyValue primaryKeyValue) throws OTSCriticalException { + int primaryKeySize = 0; + if(primaryKeyValue == AUTO_INCREMENT){ + return primaryKeySize; + } + switch (primaryKeyValue.getType()) { + case INTEGER: + primaryKeySize = 8; + break; + case STRING: + primaryKeySize = primaryKeyValue.asStringInBytes().length; + break; + case BINARY: + primaryKeySize = primaryKeyValue.asBinary().length; + break; + default: + throw new OTSCriticalException("Bug: not support the type : " + primaryKeyValue.getType() + " in getPrimaryKeyValueSize"); + } + return primaryKeySize; + } + + private static int getColumnValueSize(ColumnValue columnValue) throws OTSCriticalException { + int columnSize = 0; + switch (columnValue.getType()) { + case INTEGER: + columnSize += 8; + break; + case DOUBLE: + columnSize += 8; + break; + case STRING: + columnSize += columnValue.asStringInBytes().length; + break; + case BINARY: + columnSize += columnValue.asBinary().length; + break; + case BOOLEAN: + columnSize += 1; + break; + default: + throw new OTSCriticalException("Bug: not support the type : " + columnValue.getType() + " in getColumnValueSize"); + } + return columnSize; + } + + public static int getRowPutChangeSize(RowPutChange change) throws OTSCriticalException { + int primaryKeyTotalSize = 0; + int columnTotalSize = 0; + + // PrimaryKeys Total Size + PrimaryKey primaryKey = change.getPrimaryKey(); + PrimaryKeyColumn[] primaryKeyColumnArray = primaryKey.getPrimaryKeyColumns(); + PrimaryKeyColumn primaryKeyColumn; + byte[] primaryKeyName; + PrimaryKeyValue primaryKeyValue; + for (int i = 0; i < primaryKeyColumnArray.length; i++) { + primaryKeyColumn = primaryKeyColumnArray[i]; + primaryKeyName = primaryKeyColumn.getNameRawData(); + primaryKeyValue = primaryKeyColumn.getValue(); + + // += PrimaryKey Name Data + primaryKeyTotalSize += primaryKeyName.length; + + // += PrimaryKey Value Data + primaryKeyTotalSize += getPrimaryKeyValueSize(primaryKeyValue); + } + + // Columns Total Size + List columnList = change.getColumnsToPut(); + for (Column column : columnList) { + // += Column Name + columnTotalSize += column.getNameRawData().length; + + // += Column Value + ColumnValue columnValue = column.getValue(); + + columnTotalSize += getColumnValueSize(columnValue); + + // += Timestamp + if (column.hasSetTimestamp()) { + columnTotalSize += 8; + } + } + + return primaryKeyTotalSize + columnTotalSize; + } + + public static int getRowUpdateChangeSize(RowUpdateChange change) throws OTSCriticalException { + int primaryKeyTotalSize = 0; + int columnPutSize = 0; + int columnDeleteSize = 0; + + // PrimaryKeys Total Size + PrimaryKey primaryKey = change.getPrimaryKey(); + PrimaryKeyColumn[] primaryKeyColumnArray = primaryKey.getPrimaryKeyColumns(); + PrimaryKeyColumn primaryKeyColumn; + byte[] primaryKeyName; + PrimaryKeyValue primaryKeyValue; + for (int i = 0; i < primaryKeyColumnArray.length; i++) { + primaryKeyColumn = primaryKeyColumnArray[i]; + primaryKeyName = primaryKeyColumn.getNameRawData(); + primaryKeyValue = primaryKeyColumn.getValue(); + + // += PrimaryKey Name Data + primaryKeyTotalSize += primaryKeyName.length; + + // += PrimaryKey Value Data + primaryKeyTotalSize += getPrimaryKeyValueSize(primaryKeyValue); + } + + // Column Total Size + List> updatePairList = change.getColumnsToUpdate(); + Column column; + ColumnValue columnValue; + RowUpdateChange.Type type; + for (Pair updatePair : updatePairList) { + column = updatePair.getFirst(); + type = updatePair.getSecond(); + + switch (type) { + case DELETE: + columnDeleteSize += column.getNameRawData().length; + columnDeleteSize += 8;// Timestamp + break; + case DELETE_ALL: + columnDeleteSize += column.getNameRawData().length; + break; + case PUT: + // Name + columnPutSize += column.getNameRawData().length; + + // Value + columnValue = column.getValue(); + columnPutSize += getColumnValueSize(columnValue); + break; + default: + throw new OTSCriticalException("Bug: not support the type : " + type); + } + } + + return primaryKeyTotalSize + columnPutSize + columnDeleteSize; + } + + public static int getTimeseriesRowDataSize(TimeseriesRow row) { + TimeseriesKey timeseriesKey = row.getTimeseriesKey(); + Map fields = row.getFields(); + int totalSize = 0; + totalSize += 8; // time size + totalSize += com.alicloud.openservices.tablestore.core.utils.CalculateHelper.calcStringSizeInBytes(timeseriesKey.getMeasurementName()); + totalSize += com.alicloud.openservices.tablestore.core.utils.CalculateHelper.calcStringSizeInBytes(timeseriesKey.getDataSource()); + totalSize += com.alicloud.openservices.tablestore.core.utils.CalculateHelper.calcStringSizeInBytes(timeseriesKey.buildTagsString()); + for (Map.Entry entry : fields.entrySet()) { + totalSize += entry.getValue().getDataSize() + com.alicloud.openservices.tablestore.core.utils.CalculateHelper.calcStringSizeInBytes(entry.getKey()); + } + return totalSize; + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/CollectorUtil.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/CollectorUtil.java new file mode 100644 index 0000000000..432ac37f40 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/CollectorUtil.java @@ -0,0 +1,40 @@ +package com.alibaba.datax.plugin.writer.otswriter.utils; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSLine; + +import java.util.List; + +public class CollectorUtil { + + private static TaskPluginCollector taskPluginCollector = null; + + public static void init(TaskPluginCollector collector) { + taskPluginCollector = collector; + } + + public static void collect(Record dirtyRecord, String errorMessage) { + if (taskPluginCollector != null) { + taskPluginCollector.collectDirtyRecord(dirtyRecord, errorMessage); + } + } + + public static void collect(List dirtyRecords, String errorMessage) { + for (Record r:dirtyRecords) { + collect(r, errorMessage); + } + } + + public static void collect(List errors) { + for (LineAndError e:errors) { + collect(e.getLine().getRecords(), e.getError().getMessage()); + } + } + + public static void collect(String errorMessage, List lines) { + for (OTSLine l:lines) { + collect(l.getRecords(), errorMessage); + } + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ColumnConversion.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ColumnConversion.java index 51162b8452..5f7c91a551 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ColumnConversion.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ColumnConversion.java @@ -2,11 +2,12 @@ import com.alibaba.datax.common.element.Column; import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.plugin.writer.otswriter.OTSCriticalException; import com.alibaba.datax.plugin.writer.otswriter.model.OTSAttrColumn; import com.alibaba.datax.plugin.writer.otswriter.model.OTSErrorMessage; -import com.alibaba.datax.plugin.writer.otswriter.model.OTSPKColumn; -import com.aliyun.openservices.ots.model.ColumnValue; -import com.aliyun.openservices.ots.model.PrimaryKeyValue; +import com.alicloud.openservices.tablestore.model.ColumnValue; +import com.alicloud.openservices.tablestore.model.PrimaryKeySchema; +import com.alicloud.openservices.tablestore.model.PrimaryKeyValue; /** @@ -17,45 +18,66 @@ * 4. long -> binary */ public class ColumnConversion { - public static PrimaryKeyValue columnToPrimaryKeyValue(Column c, OTSPKColumn col) { + public static PrimaryKeyValue columnToPrimaryKeyValue(Column c, PrimaryKeySchema col) throws OTSCriticalException { try { switch (col.getType()) { - case STRING: - return PrimaryKeyValue.fromString(c.asString()); - case INTEGER: - return PrimaryKeyValue.fromLong(c.asLong()); - default: - throw new IllegalArgumentException(String.format(OTSErrorMessage.UNSUPPORT_PARSE, col.getType(), "PrimaryKeyValue")); + case STRING: + return PrimaryKeyValue.fromString(c.asString()); + case INTEGER: + return PrimaryKeyValue.fromLong(c.asLong()); + case BINARY: + return PrimaryKeyValue.fromBinary(c.asBytes()); + default: + throw new OTSCriticalException(String.format(OTSErrorMessage.UNSUPPORT_PARSE, col.getType(), "PrimaryKeyValue")); } } catch (DataXException e) { throw new IllegalArgumentException(String.format( OTSErrorMessage.COLUMN_CONVERSION_ERROR, c.getType(), c.asString(), col.getType().toString() - )); + ), + e); } } - public static ColumnValue columnToColumnValue(Column c, OTSAttrColumn col) { - try { - switch (col.getType()) { + public static ColumnValue columnToColumnValue(Column c) throws OTSCriticalException { + switch (c.getType()) { case STRING: return ColumnValue.fromString(c.asString()); - case INTEGER: + case LONG: return ColumnValue.fromLong(c.asLong()); - case BOOLEAN: + case BOOL: return ColumnValue.fromBoolean(c.asBoolean()); case DOUBLE: return ColumnValue.fromDouble(c.asDouble()); - case BINARY: + case BYTES: return ColumnValue.fromBinary(c.asBytes()); default: - throw new IllegalArgumentException(String.format(OTSErrorMessage.UNSUPPORT_PARSE, col.getType(), "ColumnValue")); + throw new OTSCriticalException(String.format(OTSErrorMessage.UNSUPPORT_PARSE, c.getType(), "ColumnValue")); + } + } + + public static ColumnValue columnToColumnValue(Column c, OTSAttrColumn col) throws OTSCriticalException { + try { + switch (col.getType()) { + case STRING: + return ColumnValue.fromString(c.asString()); + case INTEGER: + return ColumnValue.fromLong(c.asLong()); + case BOOLEAN: + return ColumnValue.fromBoolean(c.asBoolean()); + case DOUBLE: + return ColumnValue.fromDouble(c.asDouble()); + case BINARY: + return ColumnValue.fromBinary(c.asBytes()); + default: + throw new OTSCriticalException(String.format(OTSErrorMessage.UNSUPPORT_PARSE, col.getType(), "ColumnValue")); } } catch (DataXException e) { throw new IllegalArgumentException(String.format( OTSErrorMessage.COLUMN_CONVERSION_ERROR, c.getType(), c.asString(), col.getType().toString() - )); + ), + e); } } } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ColumnConversionOld.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ColumnConversionOld.java new file mode 100644 index 0000000000..a2920b9170 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ColumnConversionOld.java @@ -0,0 +1,61 @@ +package com.alibaba.datax.plugin.writer.otswriter.utils; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSAttrColumn; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSErrorMessage; +import com.alicloud.openservices.tablestore.model.PrimaryKeySchema; +import com.aliyun.openservices.ots.model.ColumnValue; +import com.aliyun.openservices.ots.model.PrimaryKeyValue; + + +/** + * 备注:datax提供的转换机制有如下限制,如下规则是不能转换的 + * 1. bool -> binary + * 2. binary -> long, double, bool + * 3. double -> bool, binary + * 4. long -> binary + */ +public class ColumnConversionOld { + public static PrimaryKeyValue columnToPrimaryKeyValue(Column c, PrimaryKeySchema col) { + try { + switch (col.getType()) { + case STRING: + return PrimaryKeyValue.fromString(c.asString()); + case INTEGER: + return PrimaryKeyValue.fromLong(c.asLong()); + default: + throw new IllegalArgumentException(String.format(OTSErrorMessage.UNSUPPORT_PARSE, col.getType(), "PrimaryKeyValue")); + } + } catch (DataXException e) { + throw new IllegalArgumentException(String.format( + OTSErrorMessage.COLUMN_CONVERSION_ERROR, + c.getType(), c.asString(), col.getType().toString() + )); + } + } + + public static ColumnValue columnToColumnValue(Column c, OTSAttrColumn col) { + try { + switch (col.getType()) { + case STRING: + return ColumnValue.fromString(c.asString()); + case INTEGER: + return ColumnValue.fromLong(c.asLong()); + case BOOLEAN: + return ColumnValue.fromBoolean(c.asBoolean()); + case DOUBLE: + return ColumnValue.fromDouble(c.asDouble()); + case BINARY: + return ColumnValue.fromBinary(c.asBytes()); + default: + throw new IllegalArgumentException(String.format(OTSErrorMessage.UNSUPPORT_PARSE, col.getType(), "ColumnValue")); + } + } catch (DataXException e) { + throw new IllegalArgumentException(String.format( + OTSErrorMessage.COLUMN_CONVERSION_ERROR, + c.getType(), c.asString(), col.getType().toString() + )); + } + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/Common.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/Common.java index 26eb9329d6..a48efa6911 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/Common.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/Common.java @@ -1,108 +1,124 @@ package com.alibaba.datax.plugin.writer.otswriter.utils; -import java.util.ArrayList; -import java.util.List; - import com.alibaba.datax.common.element.Column; import com.alibaba.datax.common.element.Record; -import com.alibaba.datax.common.plugin.TaskPluginCollector; -import com.alibaba.datax.plugin.writer.otswriter.model.*; -import com.aliyun.openservices.ots.ClientException; -import com.aliyun.openservices.ots.OTSException; -import com.aliyun.openservices.ots.model.ColumnValue; -import com.aliyun.openservices.ots.model.PrimaryKeyValue; -import com.aliyun.openservices.ots.model.RowChange; -import com.aliyun.openservices.ots.model.RowPrimaryKey; -import com.aliyun.openservices.ots.model.RowPutChange; -import com.aliyun.openservices.ots.model.RowUpdateChange; -import org.apache.commons.math3.util.Pair; +import com.alibaba.datax.plugin.writer.otswriter.OTSCriticalException; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSAttrColumn; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSConf; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSErrorMessage; +import com.alicloud.openservices.tablestore.ClientConfiguration; +import com.alicloud.openservices.tablestore.SyncClient; +import com.alicloud.openservices.tablestore.core.utils.Pair; +import com.alicloud.openservices.tablestore.model.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.Map.Entry; public class Common { - - public static String getDetailMessage(Exception exception) { - if (exception instanceof OTSException) { - OTSException e = (OTSException) exception; - return "OTSException[ErrorCode:" + e.getErrorCode() + ", ErrorMessage:" + e.getMessage() + ", RequestId:" + e.getRequestId() + "]"; - } else if (exception instanceof ClientException) { - ClientException e = (ClientException) exception; - return "ClientException[ErrorCode:" + e.getErrorCode() + ", ErrorMessage:" + e.getMessage() + "]"; - } else if (exception instanceof IllegalArgumentException) { - IllegalArgumentException e = (IllegalArgumentException) exception; - return "IllegalArgumentException[ErrorMessage:" + e.getMessage() + "]"; - } else { - return "Exception[ErrorMessage:" + exception.getMessage() + "]"; + + private static final Logger LOG = LoggerFactory.getLogger(Common.class); + + /** + * 从record中分析出PK,如果分析成功,则返回PK,如果分析失败,则返回null,并记录数据到脏数据回收器中 + * @param pkColumns + * @param r + * @return + * @throws OTSCriticalException + */ + public static PrimaryKey getPKFromRecord(Map pkColumns, Record r) throws OTSCriticalException { + if (r.getColumnNumber() < pkColumns.size()) { + throw new OTSCriticalException(String.format("Bug branch, the count(%d) of record < count(%d) of (pk) from config.", r.getColumnNumber(), pkColumns.size())); } - } - - public static RowPrimaryKey getPKFromRecord(List pkColumns, Record r) { - RowPrimaryKey primaryKey = new RowPrimaryKey(); - int pkCount = pkColumns.size(); - for (int i = 0; i < pkCount; i++) { - Column col = r.getColumn(i); - OTSPKColumn expect = pkColumns.get(i); + try { + PrimaryKeyBuilder builder = PrimaryKeyBuilder.createPrimaryKeyBuilder(); + for (Entry en : pkColumns.entrySet()) { + Column col = r.getColumn(en.getValue()); + PrimaryKeySchema expect = en.getKey(); + + if (col.getRawData() == null) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.PK_COLUMN_VALUE_IS_NULL_ERROR, expect.getName())); + } - if (col.getRawData() == null) { - throw new IllegalArgumentException(String.format(OTSErrorMessage.PK_COLUMN_VALUE_IS_NULL_ERROR, expect.getName())); + PrimaryKeyValue pk = ColumnConversion.columnToPrimaryKeyValue(col, expect); + builder.addPrimaryKeyColumn(new PrimaryKeyColumn(expect.getName(), pk)); } - - PrimaryKeyValue pk = ColumnConversion.columnToPrimaryKeyValue(col, expect); - primaryKey.addPrimaryKeyColumn(expect.getName(), pk); + return builder.build(); + } catch (IllegalArgumentException e) { + LOG.warn("getPKFromRecord fail : {}", e.getMessage(), e); + CollectorUtil.collect(r, e.getMessage()); + return null; } - return primaryKey; } - public static List> getAttrFromRecord(int pkCount, List attrColumns, Record r) { - List> attr = new ArrayList>(r.getColumnNumber()); - for (int i = 0; i < attrColumns.size(); i++) { - Column col = r.getColumn(i + pkCount); - OTSAttrColumn expect = attrColumns.get(i); + public static PrimaryKey getPKFromRecordWithAutoIncrement(Map pkColumns, Record r, PrimaryKeySchema autoIncrementPrimaryKey) throws OTSCriticalException { + if (r.getColumnNumber() < pkColumns.size()) { + throw new OTSCriticalException(String.format("Bug branch, the count(%d) of record < count(%d) of (pk) from config.", r.getColumnNumber(), pkColumns.size())); + } + try { + PrimaryKeyBuilder builder = PrimaryKeyBuilder.createPrimaryKeyBuilder(); + for (Entry en : pkColumns.entrySet()) { + Column col = r.getColumn(en.getValue()); + PrimaryKeySchema expect = en.getKey(); + + if (col.getRawData() == null) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.PK_COLUMN_VALUE_IS_NULL_ERROR, expect.getName())); + } - if (col.getRawData() == null) { - attr.add(new Pair(expect.getName(), null)); - continue; + PrimaryKeyValue pk = ColumnConversion.columnToPrimaryKeyValue(col, expect); + builder.addPrimaryKeyColumn(new PrimaryKeyColumn(expect.getName(), pk)); } + if(autoIncrementPrimaryKey != null){ + if(autoIncrementPrimaryKey.getOption()!= PrimaryKeyOption.AUTO_INCREMENT){ + throw new OTSCriticalException(String.format("The auto Increment PrimaryKey [(%s)] option should be PrimaryKeyOption.AUTO_INCREMENT.", autoIncrementPrimaryKey.getName())); + } + builder.addPrimaryKeyColumn(autoIncrementPrimaryKey.getName(),PrimaryKeyValue.AUTO_INCREMENT); - ColumnValue cv = ColumnConversion.columnToColumnValue(col, expect); - attr.add(new Pair(expect.getName(), cv)); + } + return builder.build(); + } catch (IllegalArgumentException e) { + LOG.warn("getPKFromRecord fail : {}", e.getMessage(), e); + CollectorUtil.collect(r, e.getMessage()); + return null; } - return attr; } - public static RowChange columnValuesToRowChange(String tableName, OTSOpType type, RowPrimaryKey pk, List> values) { - switch (type) { - case PUT_ROW: - RowPutChangeWithRecord rowPutChange = new RowPutChangeWithRecord(tableName); - rowPutChange.setPrimaryKey(pk); - - for (Pair en : values) { - if (en.getValue() != null) { - rowPutChange.addAttributeColumn(en.getKey(), en.getValue()); - } + /** + * 从Record中解析ColumnValue,如果Record转换为ColumnValue失败,方法会返回null + * @param pkCount + * @param attrColumns + * @param r + * @return + * @throws OTSCriticalException + */ + public static List> getAttrFromRecord(int pkCount, List attrColumns, Record r) throws OTSCriticalException { + if (pkCount + attrColumns.size() != r.getColumnNumber()) { + throw new OTSCriticalException(String.format("Bug branch, the count(%d) of record != count(%d) of (pk + column) from config.", r.getColumnNumber(), (pkCount + attrColumns.size()))); + } + try { + List> attr = new ArrayList>(r.getColumnNumber()); + for (int i = 0; i < attrColumns.size(); i++) { + Column col = r.getColumn(i + pkCount); + OTSAttrColumn expect = attrColumns.get(i); + + if (col.getRawData() == null) { + attr.add(new Pair(expect.getName(), null)); + continue; } - return rowPutChange; - case UPDATE_ROW: - RowUpdateChangeWithRecord rowUpdateChange = new RowUpdateChangeWithRecord(tableName); - rowUpdateChange.setPrimaryKey(pk); - - for (Pair en : values) { - if (en.getValue() != null) { - rowUpdateChange.addAttributeColumn(en.getKey(), en.getValue()); - } else { - rowUpdateChange.deleteAttributeColumn(en.getKey()); - } - } - return rowUpdateChange; - case DELETE_ROW: - RowDeleteChangeWithRecord rowDeleteChange = new RowDeleteChangeWithRecord(tableName); - rowDeleteChange.setPrimaryKey(pk); - return rowDeleteChange; - default: - throw new IllegalArgumentException(String.format(OTSErrorMessage.UNSUPPORT_PARSE, type, "RowChange")); + ColumnValue cv = ColumnConversion.columnToColumnValue(col, expect); + attr.add(new Pair(expect.getName(), cv)); + } + return attr; + } catch (IllegalArgumentException e) { + LOG.warn("getAttrFromRecord fail : {}", e.getMessage(), e); + CollectorUtil.collect(r, e.getMessage()); + return null; } } - public static long getDelaySendMilliseconds(int hadRetryTimes, int initSleepInMilliSecond) { + public static long getDelaySendMillinSeconds(int hadRetryTimes, int initSleepInMilliSecond) { if (hadRetryTimes <= 0) { return 0; @@ -118,4 +134,83 @@ public static long getDelaySendMilliseconds(int hadRetryTimes, int initSleepInMi } return sleepTime; } + + public static SyncClient getOTSInstance(OTSConf conf) { + ClientConfiguration clientConfigure = new ClientConfiguration(); + clientConfigure.setIoThreadCount(conf.getIoThreadCount()); + clientConfigure.setMaxConnections(conf.getConcurrencyWrite()); + clientConfigure.setSocketTimeoutInMillisecond(conf.getSocketTimeout()); + clientConfigure.setConnectionTimeoutInMillisecond(conf.getConnectTimeoutInMillisecond()); + clientConfigure.setRetryStrategy(new DefaultNoRetry()); + + SyncClient ots = new SyncClient( + conf.getEndpoint(), + conf.getAccessId(), + conf.getAccessKey(), + conf.getInstanceName(), + clientConfigure); + Map extraHeaders = new HashMap(); + extraHeaders.put("x-ots-sdk-type", "public"); + extraHeaders.put("x-ots-request-source", "datax-otswriter"); + ots.setExtraHeaders(extraHeaders); + return ots; + } + + public static LinkedHashMap getEncodePkColumnMapping(TableMeta meta, List attrColumns) throws OTSCriticalException { + LinkedHashMap attrColumnMapping = new LinkedHashMap(); + for (Entry en : meta.getPrimaryKeyMap().entrySet()) { + // don't care performance + int i = 0; + for (; i < attrColumns.size(); i++) { + if (attrColumns.get(i).getName().equals(en.getKey())) { + attrColumnMapping.put(GsonParser.primaryKeySchemaToJson(attrColumns.get(i)), i); + break; + } + } + if (i == attrColumns.size()) { + // exception branch + throw new OTSCriticalException(String.format(OTSErrorMessage.INPUT_PK_NAME_NOT_EXIST_IN_META_ERROR, en.getKey())); + } + } + return attrColumnMapping; + } + + public static LinkedHashMap getEncodePkColumnMappingWithAutoIncrement(TableMeta meta, List attrColumns) throws OTSCriticalException { + LinkedHashMap attrColumnMapping = new LinkedHashMap(); + for (Entry en : meta.getPrimaryKeySchemaMap().entrySet()) { + // don't care performance + if(en.getValue().hasOption()){ + continue; + } + + int i = 0; + for (; i < attrColumns.size(); i++) { + if (attrColumns.get(i).getName().equals(en.getKey())) { + attrColumnMapping.put(GsonParser.primaryKeySchemaToJson(attrColumns.get(i)), i); + break; + } + } + if (i == attrColumns.size()) { + // exception branch + throw new OTSCriticalException(String.format(OTSErrorMessage.INPUT_PK_NAME_NOT_EXIST_IN_META_ERROR, en.getKey())); + } + } + return attrColumnMapping; + } + + public static Map getPkColumnMapping(Map mapping) { + Map target = new LinkedHashMap(); + for (Entry en : mapping.entrySet()) { + target.put(GsonParser.jsonToPrimaryKeySchema(en.getKey()), en.getValue()); + } + return target; + } + + public static Map getAttrColumnMapping(List attrColumns) { + Map attrColumnMapping = new LinkedHashMap(); + for (OTSAttrColumn c : attrColumns) { + attrColumnMapping.put(c.getSrcName(), c); + } + return attrColumnMapping; + } } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/CommonOld.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/CommonOld.java new file mode 100644 index 0000000000..a62711cc80 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/CommonOld.java @@ -0,0 +1,93 @@ +package com.alibaba.datax.plugin.writer.otswriter.utils; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSErrorMessage; +import com.alibaba.datax.plugin.writer.otswriter.model.RowDeleteChangeWithRecord; +import com.alibaba.datax.plugin.writer.otswriter.model.RowPutChangeWithRecord; +import com.alibaba.datax.plugin.writer.otswriter.model.RowUpdateChangeWithRecord; +import com.alicloud.openservices.tablestore.model.PrimaryKeySchema; +import com.aliyun.openservices.ots.ClientException; +import com.aliyun.openservices.ots.OTSException; +import com.aliyun.openservices.ots.model.ColumnValue; +import com.aliyun.openservices.ots.model.PrimaryKeyValue; +import com.aliyun.openservices.ots.model.RowChange; +import com.aliyun.openservices.ots.model.RowPrimaryKey; +import org.apache.commons.math3.util.Pair; + +import java.util.ArrayList; +import java.util.List; + +public class CommonOld { + + public static RowPrimaryKey getPKFromRecord(List pkColumns, Record r) { + RowPrimaryKey primaryKey = new RowPrimaryKey(); + int pkCount = pkColumns.size(); + for (int i = 0; i < pkCount; i++) { + Column col = r.getColumn(i); + PrimaryKeySchema expect = pkColumns.get(i); + + if (col.getRawData() == null) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.PK_COLUMN_VALUE_IS_NULL_ERROR, expect.getName())); + } + + PrimaryKeyValue pk = ColumnConversionOld.columnToPrimaryKeyValue(col, expect); + primaryKey.addPrimaryKeyColumn(expect.getName(), pk); + } + return primaryKey; + } + + public static List> getAttrFromRecord(int pkCount, List attrColumns, Record r) { + List> attr = new ArrayList>(r.getColumnNumber()); + for (int i = 0; i < attrColumns.size(); i++) { + Column col = r.getColumn(i + pkCount); + com.alibaba.datax.plugin.writer.otswriter.model.OTSAttrColumn expect = attrColumns.get(i); + + if (col.getRawData() == null) { + attr.add(new Pair(expect.getName(), null)); + continue; + } + + ColumnValue cv = ColumnConversionOld.columnToColumnValue(col, expect); + attr.add(new Pair(expect.getName(), cv)); + } + return attr; + } + + public static RowChange columnValuesToRowChange(String tableName, + com.alibaba.datax.plugin.writer.otswriter.model.OTSOpType type, + RowPrimaryKey pk, + List> values) { + switch (type) { + case PUT_ROW: + RowPutChangeWithRecord rowPutChange = new RowPutChangeWithRecord(tableName); + rowPutChange.setPrimaryKey(pk); + + for (Pair en : values) { + if (en.getValue() != null) { + rowPutChange.addAttributeColumn(en.getKey(), en.getValue()); + } + } + + return rowPutChange; + case UPDATE_ROW: + RowUpdateChangeWithRecord rowUpdateChange = new RowUpdateChangeWithRecord(tableName); + rowUpdateChange.setPrimaryKey(pk); + + for (Pair en : values) { + if (en.getValue() != null) { + rowUpdateChange.addAttributeColumn(en.getKey(), en.getValue()); + } else { + rowUpdateChange.deleteAttributeColumn(en.getKey()); + } + } + return rowUpdateChange; + case DELETE_ROW: + RowDeleteChangeWithRecord rowDeleteChange = new RowDeleteChangeWithRecord(tableName); + rowDeleteChange.setPrimaryKey(pk); + return rowDeleteChange; + default: + throw new IllegalArgumentException(String.format(OTSErrorMessage.UNSUPPORT_PARSE, type, "RowChange")); + } + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/DefaultNoRetry.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/DefaultNoRetry.java new file mode 100644 index 0000000000..ec00056629 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/DefaultNoRetry.java @@ -0,0 +1,33 @@ +package com.alibaba.datax.plugin.writer.otswriter.utils; + + + +import com.alicloud.openservices.tablestore.model.DefaultRetryStrategy; +import com.alicloud.openservices.tablestore.model.RetryStrategy; + +public class DefaultNoRetry extends DefaultRetryStrategy { + + public DefaultNoRetry() { + super(); + } + + @Override + public RetryStrategy clone() { + return super.clone(); + } + + @Override + public int getRetries() { + return super.getRetries(); + } + + @Override + public boolean shouldRetry(String action, Exception ex) { + return false; + } + + @Override + public long nextPause(String action, Exception ex) { + return super.nextPause(action, ex); + } +} \ No newline at end of file diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/GsonParser.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/GsonParser.java index 0cae91f2b4..4e13a32762 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/GsonParser.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/GsonParser.java @@ -1,9 +1,10 @@ package com.alibaba.datax.plugin.writer.otswriter.utils; import com.alibaba.datax.plugin.writer.otswriter.model.OTSConf; -import com.aliyun.openservices.ots.model.Direction; -import com.aliyun.openservices.ots.model.RowPrimaryKey; -import com.aliyun.openservices.ots.model.TableMeta; +import com.alicloud.openservices.tablestore.model.Direction; +import com.alicloud.openservices.tablestore.model.PrimaryKey; +import com.alicloud.openservices.tablestore.model.PrimaryKeySchema; +import com.alicloud.openservices.tablestore.model.TableMeta; import com.google.gson.Gson; import com.google.gson.GsonBuilder; @@ -39,8 +40,18 @@ public static String metaToJson (TableMeta meta) { return g.toJson(meta); } - public static String rowPrimaryKeyToJson (RowPrimaryKey row) { + public static String primaryKeyToJson (PrimaryKey row) { Gson g = gsonBuilder(); return g.toJson(row); } + + public static String primaryKeySchemaToJson (PrimaryKeySchema schema) { + Gson g = gsonBuilder(); + return g.toJson(schema); + } + + public static PrimaryKeySchema jsonToPrimaryKeySchema (String jsonStr) { + Gson g = gsonBuilder(); + return g.fromJson(jsonStr, PrimaryKeySchema.class); + } } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/LineAndError.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/LineAndError.java new file mode 100644 index 0000000000..f4e8833edb --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/LineAndError.java @@ -0,0 +1,21 @@ +package com.alibaba.datax.plugin.writer.otswriter.utils; + +import com.alibaba.datax.plugin.writer.otswriter.model.OTSLine; + +public class LineAndError { + private OTSLine line; + private com.alicloud.openservices.tablestore.model.Error error; + + public LineAndError(OTSLine record, com.alicloud.openservices.tablestore.model.Error error) { + this.line = record; + this.error = error; + } + + public OTSLine getLine() { + return line; + } + + public com.alicloud.openservices.tablestore.model.Error getError() { + return error; + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ParamChecker.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ParamChecker.java index f9e17af5f1..b04f88786e 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ParamChecker.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ParamChecker.java @@ -1,18 +1,24 @@ package com.alibaba.datax.plugin.writer.otswriter.utils; -import java.util.HashMap; +import com.alibaba.datax.common.exception.CommonErrorCode; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSAttrColumn; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSConf; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSErrorMessage; +import com.alibaba.datax.plugin.writer.otswriter.model.OTSMode; +import com.alicloud.openservices.tablestore.model.PrimaryKeySchema; +import com.alicloud.openservices.tablestore.model.PrimaryKeyType; +import com.alicloud.openservices.tablestore.model.TableMeta; + import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static com.alibaba.datax.plugin.writer.otswriter.model.OTSErrorMessage.*; -import com.alibaba.datax.common.util.Configuration; -import com.alibaba.datax.plugin.writer.otswriter.model.OTSAttrColumn; -import com.alibaba.datax.plugin.writer.otswriter.model.OTSErrorMessage; -import com.alibaba.datax.plugin.writer.otswriter.model.OTSPKColumn; -import com.aliyun.openservices.ots.model.PrimaryKeyType; -import com.aliyun.openservices.ots.model.TableMeta; public class ParamChecker { @@ -28,16 +34,13 @@ private static void throwEmptyListException(String key) { throw new IllegalArgumentException(String.format(OTSErrorMessage.PARAMETER_LIST_IS_EMPTY_ERROR, key)); } - private static void throwNotListException(String key) { - throw new IllegalArgumentException(String.format(OTSErrorMessage.PARAMETER_IS_NOT_ARRAY_ERROR, key)); - } - - private static void throwNotMapException(String key) { - throw new IllegalArgumentException(String.format(OTSErrorMessage.PARAMETER_IS_NOT_MAP_ERROR, key)); + private static void throwNotListException(String key, Throwable t) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.PARAMETER_IS_NOT_ARRAY_ERROR, key), t); } public static String checkStringAndGet(Configuration param, String key) { String value = param.getString(key); + value = value != null ? value.trim() : null; if (null == value) { throwNotExistException(key); } else if (value.length() == 0) { @@ -51,7 +54,7 @@ public static List checkListAndGet(Configuration param, String key, bool try { value = param.getList(key); } catch (ClassCastException e) { - throwNotListException(key); + throwNotListException(key, e); } if (null == value) { throwNotExistException(key); @@ -60,81 +63,76 @@ public static List checkListAndGet(Configuration param, String key, bool } return value; } - - public static List checkListAndGet(Map range, String key) { - Object obj = range.get(key); - if (null == obj) { - return null; - } - return checkListAndGet(range, key, false); - } - - public static List checkListAndGet(Map range, String key, boolean isCheckEmpty) { - Object obj = range.get(key); - if (null == obj) { - throwNotExistException(key); + + public static void checkPrimaryKey(TableMeta meta, List pk) { + Map pkNameAndTypeMapping = meta.getPrimaryKeyMap(); + // 个数是否相等 + if (pkNameAndTypeMapping.size() != pk.size()) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.INPUT_PK_COUNT_NOT_EQUAL_META_ERROR, pk.size(), pkNameAndTypeMapping.size())); } - if (obj instanceof List) { - @SuppressWarnings("unchecked") - List value = (List)obj; - if (isCheckEmpty && value.isEmpty()) { - throwEmptyListException(key); + + // 名字类型是否相等 + for (PrimaryKeySchema col : pk) { + PrimaryKeyType type = pkNameAndTypeMapping.get(col.getName()); + if (type == null) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.PK_COLUMN_MISSING_ERROR, col.getName())); + } + if (type != col.getType()) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.INPUT_PK_TYPE_NOT_MATCH_META_ERROR, col.getName(), type, col.getType())); } - return value; - } else { - throw new IllegalArgumentException(String.format(OTSErrorMessage.PARSE_TO_LIST_ERROR, key)); } } - public static List checkListAndGet(Map range, String key, List defaultList) { - Object obj = range.get(key); - if (null == obj) { - return defaultList; + public static void checkVersion(OTSConf conf) { + /** + * conf检查遵循以下规则 + * 1. 旧版本插件 不支持 主键自增列 + * 2. 旧版本插件 不支持 多版本模式 + * 3. 多版本模式 不支持 主键自增列 + * 4. 旧版本插件 不支持 时序数据表 + * 5. 时序数据表 不支持 主键自增列 + */ + if (!conf.isNewVersion() && conf.getEnableAutoIncrement()) { + throw new IllegalArgumentException(PUBLIC_SDK_NO_SUPPORT_AUTO_INCREMENT); } - if (obj instanceof List) { - @SuppressWarnings("unchecked") - List value = (List)obj; - return value; - } else { - throw new IllegalArgumentException(String.format(OTSErrorMessage.PARSE_TO_LIST_ERROR, key)); + if (!conf.isNewVersion() && conf.getMode() == OTSMode.MULTI_VERSION) { + throw new IllegalArgumentException(PUBLIC_SDK_NO_SUPPORT_MULTI_VERSION); } - } - - public static Map checkMapAndGet(Configuration param, String key, boolean isCheckEmpty) { - Map value = null; - try { - value = param.getMap(key); - } catch (ClassCastException e) { - throwNotMapException(key); + if (conf.getMode() == OTSMode.MULTI_VERSION && conf.getEnableAutoIncrement()) { + throw new IllegalArgumentException(NOT_SUPPORT_MULTI_VERSION_AUTO_INCREMENT); } - if (null == value) { - throwNotExistException(key); - } else if (isCheckEmpty && value.isEmpty()) { - throwEmptyListException(key); + if (!conf.isNewVersion() && conf.isTimeseriesTable()) { + throw new IllegalArgumentException(PUBLIC_SDK_NO_SUPPORT_TIMESERIES_TABLE); + } + if (conf.isTimeseriesTable() && conf.getEnableAutoIncrement()) { + throw new IllegalArgumentException(NOT_SUPPORT_TIMESERIES_TABLE_AUTO_INCREMENT); } - return value; } - - public static void checkPrimaryKey(TableMeta meta, List pk) { - Map types = meta.getPrimaryKey(); + + public static void checkPrimaryKeyWithAutoIncrement(TableMeta meta, List pk) { + Map pkNameAndTypeMapping = meta.getPrimaryKeyMap(); + int autoIncrementKeySize = 0; + for(PrimaryKeySchema p : meta.getPrimaryKeyList()){ + if(p.hasOption()){ + autoIncrementKeySize++; + } + } // 个数是否相等 - if (types.size() != pk.size()) { - throw new IllegalArgumentException(String.format(OTSErrorMessage.INPUT_PK_COUNT_NOT_EQUAL_META_ERROR, pk.size(), types.size())); + if (pkNameAndTypeMapping.size() != pk.size() + autoIncrementKeySize) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.INPUT_PK_COUNT_NOT_EQUAL_META_ERROR, pk.size() + autoIncrementKeySize, pkNameAndTypeMapping.size())); } - + // 名字类型是否相等 - Map inputTypes = new HashMap(); - for (OTSPKColumn col : pk) { - inputTypes.put(col.getName(), col.getType()); - } - - for (Entry e : types.entrySet()) { - if (!inputTypes.containsKey(e.getKey())) { - throw new IllegalArgumentException(String.format(OTSErrorMessage.PK_COLUMN_MISSING_ERROR, e.getKey())); + for (PrimaryKeySchema col : pk) { + if(col.hasOption()){ + continue; + } + PrimaryKeyType type = pkNameAndTypeMapping.get(col.getName()); + if (type == null) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.PK_COLUMN_MISSING_ERROR, col.getName())); } - PrimaryKeyType type = inputTypes.get(e.getKey()); - if (type != e.getValue()) { - throw new IllegalArgumentException(String.format(OTSErrorMessage.INPUT_PK_TYPE_NOT_MATCH_META_ERROR, e.getKey(), type, e.getValue())); + if (type != col.getType()) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.INPUT_PK_TYPE_NOT_MATCH_META_ERROR, col.getName(), type, col.getType())); } } } @@ -150,4 +148,23 @@ public static void checkAttribute(List attr) { } } } + + public static TimeUnit checkTimeUnitAndGet(String str) { + if (null == str) { + return null; + } else if ("NANOSECONDS".equalsIgnoreCase(str)) { + return TimeUnit.NANOSECONDS; + } else if ("MICROSECONDS".equalsIgnoreCase(str)) { + return TimeUnit.MICROSECONDS; + } else if ("MILLISECONDS".equalsIgnoreCase(str)) { + return TimeUnit.MILLISECONDS; + } else if ("SECONDS".equalsIgnoreCase(str)) { + return TimeUnit.SECONDS; + } else if ("MINUTES".equalsIgnoreCase(str)) { + return TimeUnit.MINUTES; + } else { + throw new IllegalArgumentException(String.format(OTSErrorMessage.TIMEUNIT_FORMAT_ERROR, str)); + } + } + } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ParseRecord.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ParseRecord.java new file mode 100644 index 0000000000..1f157131d5 --- /dev/null +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/ParseRecord.java @@ -0,0 +1,326 @@ +package com.alibaba.datax.plugin.writer.otswriter.utils; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.plugin.writer.otswriter.OTSCriticalException; +import com.alibaba.datax.plugin.writer.otswriter.model.*; +import com.alicloud.openservices.tablestore.core.protocol.timeseries.TimeseriesResponseFactory; +import com.alicloud.openservices.tablestore.core.utils.Pair; +import com.alicloud.openservices.tablestore.model.*; +import com.alicloud.openservices.tablestore.model.timeseries.TimeseriesKey; +import com.alicloud.openservices.tablestore.model.timeseries.TimeseriesRow; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + + +public class ParseRecord { + + private static final Logger LOG = LoggerFactory.getLogger(ParseRecord.class); + + private static com.alicloud.openservices.tablestore.model.Column buildColumn(String name, ColumnValue value, long timestamp) { + if (timestamp > 0) { + return new com.alicloud.openservices.tablestore.model.Column( + name, + value, + timestamp + ); + } else { + return new com.alicloud.openservices.tablestore.model.Column( + name, + value + ); + } + } + /** + * 基于普通方式处理Record + * 当PK或者Attr解析失败时,方法会返回null + * @param tableName + * @param type + * @param pkColumns + * @param attrColumns + * @param record + * @param timestamp + * @return + * @throws OTSCriticalException + */ + public static OTSLine parseNormalRecordToOTSLine( + String tableName, + OTSOpType type, + Map pkColumns, + List attrColumns, + Record record, + long timestamp) throws OTSCriticalException { + + PrimaryKey pk = Common.getPKFromRecord(pkColumns, record); + if (pk == null) { + return null; + } + List> values = Common.getAttrFromRecord(pkColumns.size(), attrColumns, record); + if (values == null) { + return null; + } + + switch (type) { + case PUT_ROW: + RowPutChange rowPutChange = new RowPutChange(tableName, pk); + for (Pair en : values) { + if (en.getSecond() != null) { + rowPutChange.addColumn(buildColumn(en.getFirst(), en.getSecond(), timestamp)); + } + } + if (rowPutChange.getColumnsToPut().isEmpty()) { + return null; + } + return new OTSLine(pk, record, rowPutChange); + case UPDATE_ROW: + RowUpdateChange rowUpdateChange = new RowUpdateChange(tableName, pk); + for (Pair en : values) { + if (en.getSecond() != null) { + rowUpdateChange.put(buildColumn(en.getFirst(), en.getSecond(), timestamp)); + } else { + rowUpdateChange.deleteColumns(en.getFirst()); // 删除整列 + } + } + return new OTSLine(pk, record, rowUpdateChange); + default: + LOG.error("Bug branch, can not support : {}(OTSOpType)", type); + throw new OTSCriticalException(String.format(OTSErrorMessage.UNSUPPORT, type)); + } + } + + + public static OTSLine parseNormalRecordToOTSLineWithAutoIncrement( + String tableName, + OTSOpType type, + Map pkColumns, + List attrColumns, + Record record, + long timestamp, + PrimaryKeySchema autoIncrementPrimaryKey) throws OTSCriticalException { + + PrimaryKey pk = Common.getPKFromRecordWithAutoIncrement(pkColumns, record, autoIncrementPrimaryKey); + if (pk == null) { + return null; + } + List> values = Common.getAttrFromRecord(pkColumns.size(), attrColumns, record); + if (values == null) { + return null; + } + + switch (type) { + case PUT_ROW: + RowPutChange rowPutChange = new RowPutChange(tableName, pk); + for (Pair en : values) { + if (en.getSecond() != null) { + rowPutChange.addColumn(buildColumn(en.getFirst(), en.getSecond(), timestamp)); + } + } + if (rowPutChange.getColumnsToPut().isEmpty()) { + return null; + } + return new OTSLine(pk, record, rowPutChange); + case UPDATE_ROW: + RowUpdateChange rowUpdateChange = new RowUpdateChange(tableName, pk); + for (Pair en : values) { + if (en.getSecond() != null) { + rowUpdateChange.put(buildColumn(en.getFirst(), en.getSecond(), timestamp)); + } else { + rowUpdateChange.deleteColumns(en.getFirst()); // 删除整列 + } + } + return new OTSLine(pk, record, rowUpdateChange); + default: + LOG.error("Bug branch, can not support : {}(OTSOpType)", type); + throw new OTSCriticalException(String.format(OTSErrorMessage.UNSUPPORT, type)); + } + } + + public static OTSLine parseNormalRecordToOTSLineOfTimeseriesTable( + List attrColumns, + Record record, + TimeUnit timeUnit + ) throws OTSCriticalException { + + if (attrColumns.size() != record.getColumnNumber()){ + throw new OTSCriticalException(String.format("Bug branch, the count(%d) of record != count(%d) of column from config.", record.getColumnNumber(), (attrColumns.size()))); + } + + Map tags = new HashMap<>(); + String measurementName = null; + String dataSource = null; + Long timeInUs = null; + Map columnsValues = new HashMap<>(); + + try { + for (int i = 0; i < attrColumns.size(); i++) { + // 如果是tags内部字段 + if (attrColumns.get(i).getTag()){ + tags.put(attrColumns.get(i).getName(), record.getColumn(i).asString()); + } + else if (attrColumns.get(i).getName().equals(OTSConst.MEASUREMENT_NAME)){ + measurementName = record.getColumn(i).asString(); + } + else if (attrColumns.get(i).getName().equals(OTSConst.DATA_SOURCE)){ + dataSource = record.getColumn(i).asString(); + } + else if (attrColumns.get(i).getName().equals(OTSConst.TAGS)){ + String tagString = record.getColumn(i).asString(); + tags.putAll(TimeseriesResponseFactory.parseTagsOrAttrs(tagString)); + } + else if (attrColumns.get(i).getName().equals(OTSConst.TIME)){ + timeInUs = record.getColumn(i).asLong(); + } + else{ + switch (attrColumns.get(i).getType()){ + case INTEGER: + columnsValues.put(attrColumns.get(i).getName(), ColumnValue.fromLong(record.getColumn(i).asLong())); + break; + case BOOLEAN: + columnsValues.put(attrColumns.get(i).getName(), ColumnValue.fromBoolean(record.getColumn(i).asBoolean())); + break; + case DOUBLE: + columnsValues.put(attrColumns.get(i).getName(), ColumnValue.fromDouble(record.getColumn(i).asDouble())); + break; + case BINARY: + columnsValues.put(attrColumns.get(i).getName(), ColumnValue.fromBinary(record.getColumn(i).asBytes())); + break; + case STRING: + default: + columnsValues.put(attrColumns.get(i).getName(), ColumnValue.fromString(record.getColumn(i).asString())); + break; + } + } + } + // 度量名称与时间戳字段值不能为空,否则报错 + if (measurementName == null){ + throw new IllegalArgumentException("The value of the '_m_name' (measurement) field cannot be empty. Please check the input of writer"); + } + else if (timeInUs == null){ + throw new IllegalArgumentException("The value of the '_time' field cannot be empty. Please check the input of writer"); + } + } catch (IllegalArgumentException e) { + LOG.warn("getAttrFromRecord fail : {}", e.getMessage(), e); + CollectorUtil.collect(record, e.getMessage()); + return null; + } + TimeseriesKey key = new TimeseriesKey(measurementName, dataSource, tags); + TimeseriesRow row = new TimeseriesRow(key); + switch (timeUnit){ + case NANOSECONDS: + timeInUs = timeInUs / 1000; + break; + case MILLISECONDS: + timeInUs = timeInUs * 1000; + break; + case SECONDS: + timeInUs = timeInUs * 1000 * 1000; + break; + case MINUTES: + timeInUs = timeInUs * 1000 * 1000 * 60; + break; + case MICROSECONDS: + default: + break; + } + row.setTimeInUs(timeInUs); + + for (Map.Entry entry : columnsValues.entrySet()){ + row.addField(entry.getKey(), entry.getValue()); + } + + return new OTSLine(record, row); + } + + public static String getDefineCoumnName(String attrColumnNamePrefixFilter, int columnNameIndex, Record r) { + String columnName = r.getColumn(columnNameIndex).asString(); + if (attrColumnNamePrefixFilter != null) { + if (columnName.startsWith(attrColumnNamePrefixFilter) && columnName.length() > attrColumnNamePrefixFilter.length()) { + columnName = columnName.substring(attrColumnNamePrefixFilter.length()); + } else { + throw new IllegalArgumentException(String.format(OTSErrorMessage.COLUMN_NOT_DEFINE, columnName)); + } + } + return columnName; + } + + private static void appendCellToRowUpdateChange( + Map pkColumns, + String attrColumnNamePrefixFilter, + Record r, + RowUpdateChange updateChange + ) throws OTSCriticalException { + try { + String columnName = getDefineCoumnName(attrColumnNamePrefixFilter, pkColumns.size(), r); + Column timestamp = r.getColumn(pkColumns.size() + 1); + Column value = r.getColumn(pkColumns.size() + 2); + + if (timestamp.getRawData() == null) { + throw new IllegalArgumentException(OTSErrorMessage.MULTI_VERSION_TIMESTAMP_IS_EMPTY); + } + + if (value.getRawData() == null) { + updateChange.deleteColumn(columnName, timestamp.asLong()); + return; + } + + ColumnValue otsValue = ColumnConversion.columnToColumnValue(value); + + com.alicloud.openservices.tablestore.model.Column c = new com.alicloud.openservices.tablestore.model.Column( + columnName, + otsValue, + timestamp.asLong() + ); + updateChange.put(c); + return; + } catch (IllegalArgumentException e) { + LOG.warn("parseToColumn fail : {}", e.getMessage(), e); + CollectorUtil.collect(r, e.getMessage()); + return; + } catch (DataXException e) { + LOG.warn("parseToColumn fail : {}", e.getMessage(), e); + CollectorUtil.collect(r, e.getMessage()); + return; + } + } + + /** + * 基于特殊模式处理Record + * 当所有Record转换为Column失败时,方法会返回null + * @param tableName + * @param type + * @param pkColumns + * @param records + * @return + * @throws Exception + */ + public static OTSLine parseMultiVersionRecordToOTSLine( + String tableName, + OTSOpType type, + Map pkColumns, + String attrColumnNamePrefixFilter, + PrimaryKey pk, + List records) throws OTSCriticalException { + + switch(type) { + case UPDATE_ROW: + RowUpdateChange updateChange = new RowUpdateChange(tableName, pk); + for (Record r : records) { + appendCellToRowUpdateChange(pkColumns, attrColumnNamePrefixFilter, r, updateChange); + } + if (updateChange.getColumnsToUpdate().isEmpty()) { + return null; + } else { + return new OTSLine(pk, records, updateChange); + } + default: + LOG.error("Bug branch, can not support : {}(OTSOpType)", type); + throw new OTSCriticalException(String.format(OTSErrorMessage.UNSUPPORT, type)); + } + } +} diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/RetryHelper.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/RetryHelper.java index a863b908ed..5f353777bc 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/RetryHelper.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/RetryHelper.java @@ -1,34 +1,40 @@ package com.alibaba.datax.plugin.writer.otswriter.utils; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.Callable; - +import com.alibaba.datax.plugin.writer.otswriter.OTSErrorCode; +import com.alicloud.openservices.tablestore.ClientException; +import com.alicloud.openservices.tablestore.TableStoreException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.alibaba.datax.plugin.writer.otswriter.model.LogExceptionManager; -import com.aliyun.openservices.ots.ClientException; -import com.aliyun.openservices.ots.OTSErrorCode; -import com.aliyun.openservices.ots.OTSException; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.Callable; public class RetryHelper { private static final Logger LOG = LoggerFactory.getLogger(RetryHelper.class); private static final Set noRetryErrorCode = prepareNoRetryErrorCode(); - public static LogExceptionManager logManager = new LogExceptionManager(); - + /** + * 对重试的封装,方法需要用户传入最大重试次数,最大的重试时间。 + * 如果方法执行失败,方法会进入重试,每次重试之前,方法会sleep一段时间(sleep机制请参见 + * Common.getDelaySendMillinSeconds方法),直到重试次数达到上限,系统会抛出异常。 + * @param callable + * @param maxRetryTimes + * @param sleepInMilliSecond + * @return + * @throws Exception + */ public static V executeWithRetry(Callable callable, int maxRetryTimes, int sleepInMilliSecond) throws Exception { int retryTimes = 0; while (true){ - Thread.sleep(Common.getDelaySendMilliseconds(retryTimes, sleepInMilliSecond)); + Thread.sleep(Common.getDelaySendMillinSeconds(retryTimes, sleepInMilliSecond)); try { return callable.call(); } catch (Exception e) { - logManager.addException(e); + LOG.warn("Call callable fail.", e); if (!canRetry(e)){ - LOG.error("Can not retry for Exception.", e); + LOG.error("Can not retry for Exception : {}", e.getMessage()); throw e; } else if (retryTimes >= maxRetryTimes) { LOG.error("Retry times more than limition. maxRetryTimes : {}", maxRetryTimes); @@ -41,7 +47,7 @@ public static V executeWithRetry(Callable callable, int maxRetryTimes, in } private static Set prepareNoRetryErrorCode() { - Set pool = new HashSet(); + final Set pool = new HashSet(); pool.add(OTSErrorCode.AUTHORIZATION_FAILURE); pool.add(OTSErrorCode.INVALID_PARAMETER); pool.add(OTSErrorCode.REQUEST_TOO_LARGE); @@ -63,11 +69,21 @@ public static boolean canRetry(String otsErrorCode) { } public static boolean canRetry(Exception exception) { - OTSException e = null; - if (exception instanceof OTSException) { - e = (OTSException) exception; + TableStoreException e = null; + if (exception instanceof TableStoreException) { + e = (TableStoreException) exception; + LOG.warn( + "OTSException:ErrorCode:{}, ErrorMsg:{}, RequestId:{}", + new Object[]{e.getErrorCode(), e.getMessage(), e.getRequestId()} + ); return canRetry(e.getErrorCode()); + } else if (exception instanceof ClientException) { + ClientException ce = (ClientException) exception; + LOG.warn( + "ClientException:ErrorMsg:{}", + ce.getMessage() + ); return true; } else { return false; diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/WithRecord.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/WithRecord.java similarity index 71% rename from otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/WithRecord.java rename to otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/WithRecord.java index 2e1672a7d3..9bb4d4e3a3 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/model/WithRecord.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/WithRecord.java @@ -1,4 +1,4 @@ -package com.alibaba.datax.plugin.writer.otswriter.model; +package com.alibaba.datax.plugin.writer.otswriter.utils; import com.alibaba.datax.common.element.Record; diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/WriterModelParser.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/WriterModelParser.java index c81587b685..76d6c8434e 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/WriterModelParser.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/WriterModelParser.java @@ -1,18 +1,12 @@ package com.alibaba.datax.plugin.writer.otswriter.utils; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import com.alibaba.datax.plugin.writer.otswriter.model.*; +import com.alicloud.openservices.tablestore.model.ColumnType; +import com.alicloud.openservices.tablestore.model.PrimaryKeySchema; +import com.alicloud.openservices.tablestore.model.PrimaryKeyType; +import com.alicloud.openservices.tablestore.model.TableMeta; -import com.alibaba.datax.plugin.writer.otswriter.model.OTSAttrColumn; -import com.alibaba.datax.plugin.writer.otswriter.model.OTSPKColumn; -import com.alibaba.datax.plugin.writer.otswriter.model.OTSConst; -import com.alibaba.datax.plugin.writer.otswriter.model.OTSErrorMessage; -import com.alibaba.datax.plugin.writer.otswriter.model.OTSOpType; -import com.aliyun.openservices.ots.model.ColumnType; -import com.aliyun.openservices.ots.model.PrimaryKeyType; +import java.util.*; /** * 解析配置中参数 @@ -26,39 +20,92 @@ public static PrimaryKeyType parsePrimaryKeyType(String type) { return PrimaryKeyType.STRING; } else if (type.equalsIgnoreCase(OTSConst.TYPE_INTEGER)) { return PrimaryKeyType.INTEGER; + } else if (type.equalsIgnoreCase(OTSConst.TYPE_BINARY)) { + return PrimaryKeyType.BINARY; } else { throw new IllegalArgumentException(String.format(OTSErrorMessage.PK_TYPE_ERROR, type)); } } - public static OTSPKColumn parseOTSPKColumn(Map column) { - if (column.containsKey(OTSConst.NAME) && column.containsKey(OTSConst.TYPE) && column.size() == 2) { - Object type = column.get(OTSConst.TYPE); - Object name = column.get(OTSConst.NAME); - if (type instanceof String && name instanceof String) { - String typeStr = (String) type; - String nameStr = (String) name; - if (nameStr.isEmpty()) { - throw new IllegalArgumentException(OTSErrorMessage.PK_COLUMN_NAME_IS_EMPTY_ERROR); - } - return new OTSPKColumn(nameStr, parsePrimaryKeyType(typeStr)); - } else { - throw new IllegalArgumentException(OTSErrorMessage.PK_MAP_NAME_TYPE_ERROR); - } + private static Object columnGetObject(Map column, String key, String error) { + Object value = column.get(key); + + if (value == null) { + throw new IllegalArgumentException(error); + } + + return value; + } + + private static String checkString(Object value, String error) { + if (!(value instanceof String)) { + throw new IllegalArgumentException(error); + } + return (String)value; + } + + private static void checkStringEmpty(String value, String error) { + if (value.isEmpty()) { + throw new IllegalArgumentException(error); + } + } + + public static PrimaryKeySchema parseOTSPKColumn(Map column) { + String typeStr = checkString( + columnGetObject(column, OTSConst.TYPE, String.format(OTSErrorMessage.PK_MAP_FILED_MISSING_ERROR, OTSConst.TYPE)), + String.format(OTSErrorMessage.PK_MAP_KEY_TYPE_ERROR, OTSConst.TYPE) + ); + String nameStr = checkString( + columnGetObject(column, OTSConst.NAME, String.format(OTSErrorMessage.PK_MAP_FILED_MISSING_ERROR, OTSConst.NAME)), + String.format(OTSErrorMessage.PK_MAP_KEY_TYPE_ERROR, OTSConst.NAME) + ); + + checkStringEmpty(typeStr, OTSErrorMessage.PK_COLUMN_TYPE_IS_EMPTY_ERROR); + checkStringEmpty(nameStr, OTSErrorMessage.PK_COLUMN_NAME_IS_EMPTY_ERROR); + + if (column.size() == 2) { + return new PrimaryKeySchema(nameStr, parsePrimaryKeyType(typeStr)); } else { throw new IllegalArgumentException(OTSErrorMessage.PK_MAP_INCLUDE_NAME_TYPE_ERROR); } } - public static List parseOTSPKColumnList(List values) { - List pks = new ArrayList(); + public static List parseOTSPKColumnList(TableMeta meta, List values) { + + Map pkMapping = meta.getPrimaryKeyMap(); + + List pks = new ArrayList(); for (Object obj : values) { - if (obj instanceof Map) { + /** + * json 中primary key格式为: + * "primaryKey":[ + * "userid", + * "groupid" + *] + */ + if (obj instanceof String) { + String name = (String) obj; + PrimaryKeyType type = pkMapping.get(name); + if (null == type) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.PK_IS_NOT_EXIST_AT_OTS_ERROR, name)); + } else { + pks.add(new PrimaryKeySchema(name, type)); + } + } + /** + * json 中primary key格式为: + * "primaryKey" : [ + * {"name":"pk1", "type":"string"}, + * {"name":"pk2", "type":"int"} + *], + */ + else if (obj instanceof Map) { @SuppressWarnings("unchecked") Map column = (Map) obj; pks.add(parseOTSPKColumn(column)); - } else { - throw new IllegalArgumentException(OTSErrorMessage.PK_ITEM_IS_NOT_MAP_ERROR); + } + else { + throw new IllegalArgumentException(OTSErrorMessage.PK_ITEM_IS_ILLEAGAL_ERROR); } } return pks; @@ -80,60 +127,154 @@ public static ColumnType parseColumnType(String type) { } } - public static OTSAttrColumn parseOTSAttrColumn(Map column) { - if (column.containsKey(OTSConst.NAME) && column.containsKey(OTSConst.TYPE) && column.size() == 2) { - Object type = column.get(OTSConst.TYPE); - Object name = column.get(OTSConst.NAME); - if (type instanceof String && name instanceof String) { - String typeStr = (String) type; - String nameStr = (String) name; - if (nameStr.isEmpty()) { - throw new IllegalArgumentException(OTSErrorMessage.ATTR_COLUMN_NAME_IS_EMPTY_ERROR); - } - return new OTSAttrColumn(nameStr, parseColumnType(typeStr)); + public static OTSAttrColumn parseOTSAttrColumn(Map column, OTSMode mode) { + String typeStr = checkString( + columnGetObject(column, OTSConst.TYPE, String.format(OTSErrorMessage.ATTR_MAP_FILED_MISSING_ERROR, OTSConst.TYPE)), + String.format(OTSErrorMessage.ATTR_MAP_KEY_TYPE_ERROR, OTSConst.TYPE) + ); + String nameStr = checkString( + columnGetObject(column, OTSConst.NAME, String.format(OTSErrorMessage.ATTR_MAP_FILED_MISSING_ERROR, OTSConst.NAME)), + String.format(OTSErrorMessage.ATTR_MAP_KEY_TYPE_ERROR, OTSConst.NAME) + ); + + checkStringEmpty(typeStr, OTSErrorMessage.ATTR_COLUMN_TYPE_IS_EMPTY_ERROR); + checkStringEmpty(nameStr, OTSErrorMessage.ATTR_COLUMN_NAME_IS_EMPTY_ERROR); + + if (mode == OTSMode.MULTI_VERSION) { + String srcNameStr = checkString( + columnGetObject(column, OTSConst.SRC_NAME, String.format(OTSErrorMessage.ATTR_MAP_FILED_MISSING_ERROR, OTSConst.SRC_NAME)), + String.format(OTSErrorMessage.ATTR_MAP_KEY_TYPE_ERROR, OTSConst.SRC_NAME) + ); + checkStringEmpty(srcNameStr, OTSErrorMessage.ATTR_COLUMN_SRC_NAME_IS_EMPTY_ERROR); + if (column.size() == 3) { + return new OTSAttrColumn(srcNameStr, nameStr, parseColumnType(typeStr)); } else { - throw new IllegalArgumentException(OTSErrorMessage.ATTR_MAP_NAME_TYPE_ERROR); + throw new IllegalArgumentException(OTSErrorMessage.ATTR_MAP_INCLUDE_SRCNAME_NAME_TYPE_ERROR); } } else { - throw new IllegalArgumentException(OTSErrorMessage.ATTR_MAP_INCLUDE_NAME_TYPE_ERROR); + if (column.size() == 2) { + return new OTSAttrColumn(nameStr, parseColumnType(typeStr)); + } else { + throw new IllegalArgumentException(OTSErrorMessage.ATTR_MAP_INCLUDE_NAME_TYPE_ERROR); + } } } - - private static void checkMultiAttrColumn(List attrs) { - Set pool = new HashSet(); - for (OTSAttrColumn col : attrs) { - if (pool.contains(col.getName())) { - throw new IllegalArgumentException(String.format(OTSErrorMessage.MULTI_ATTR_COLUMN_ERROR, col.getName())); + + public static List parseOTSTimeseriesRowAttrList(List values) { + List attrs = new ArrayList(); + // columns内部必须配置_m_name与_time字段,否则报错 + boolean getMeasurementField = false; + boolean getTimeField = false; + for (Object obj : values) { + if (obj instanceof Map) { + @SuppressWarnings("unchecked") + Map column = (Map) obj; + + + String nameStr = checkString( + columnGetObject(column, OTSConst.NAME, String.format(OTSErrorMessage.ATTR_MAP_FILED_MISSING_ERROR, OTSConst.NAME)), + String.format(OTSErrorMessage.ATTR_MAP_KEY_TYPE_ERROR, OTSConst.NAME) + ); + boolean isTag = column.get(OTSConst.IS_TAG) != null && Boolean.parseBoolean((String) column.get(OTSConst.IS_TAG)); + String typeStr = "String"; + if (column.get(OTSConst.TYPE) != null){ + typeStr = (String) column.get(OTSConst.TYPE); + } + + checkStringEmpty(nameStr, OTSErrorMessage.ATTR_COLUMN_NAME_IS_EMPTY_ERROR); + + if (nameStr.equals(OTSConst.MEASUREMENT_NAME)){ + getMeasurementField = true; + } else if (nameStr.equals(OTSConst.TIME)) { + getTimeField = true; + } + + attrs.add(new OTSAttrColumn(nameStr, parseColumnType(typeStr), isTag)); } else { - pool.add(col.getName()); + throw new IllegalArgumentException(OTSErrorMessage.ATTR_ITEM_IS_NOT_MAP_ERROR); } } + if (!getMeasurementField){ + throw new IllegalArgumentException(OTSErrorMessage.NO_FOUND_M_NAME_FIELD_ERROR); + } else if (!getTimeField) { + throw new IllegalArgumentException(OTSErrorMessage.NO_FOUND_TIME_FIELD_ERROR); + } + return attrs; } - public static List parseOTSAttrColumnList(List values) { + private static void checkMultiAttrColumn(List pk, List attrs, OTSMode mode) { + // duplicate column name + { + Set pool = new HashSet(); + for (OTSAttrColumn col : attrs) { + if (pool.contains(col.getName())) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.MULTI_ATTR_COLUMN_ERROR, col.getName())); + } else { + pool.add(col.getName()); + } + } + for (PrimaryKeySchema col : pk) { + if (pool.contains(col.getName())) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.MULTI_PK_ATTR_COLUMN_ERROR, col.getName())); + } else { + pool.add(col.getName()); + } + } + } + // duplicate src column name + if (mode == OTSMode.MULTI_VERSION) { + Set pool = new HashSet(); + for (OTSAttrColumn col : attrs) { + if (pool.contains(col.getSrcName())) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.MULTI_ATTR_SRC_COLUMN_ERROR, col.getSrcName())); + } else { + pool.add(col.getSrcName()); + } + } + } + } + + public static List parseOTSAttrColumnList(List pk, List values, OTSMode mode) { List attrs = new ArrayList(); for (Object obj : values) { if (obj instanceof Map) { @SuppressWarnings("unchecked") Map column = (Map) obj; - attrs.add(parseOTSAttrColumn(column)); + attrs.add(parseOTSAttrColumn(column, mode)); } else { throw new IllegalArgumentException(OTSErrorMessage.ATTR_ITEM_IS_NOT_MAP_ERROR); } } - checkMultiAttrColumn(attrs); + checkMultiAttrColumn(pk, attrs, mode); return attrs; } - - public static OTSOpType parseOTSOpType(String value) { + + public static OTSOpType parseOTSOpType(String value, OTSMode mode) { + OTSOpType type = null; if (value.equalsIgnoreCase(OTSConst.OTS_OP_TYPE_PUT)) { - return OTSOpType.PUT_ROW; + type = OTSOpType.PUT_ROW; } else if (value.equalsIgnoreCase(OTSConst.OTS_OP_TYPE_UPDATE)) { - return OTSOpType.UPDATE_ROW; - } else if (value.equalsIgnoreCase(OTSConst.OTS_OP_TYPE_DELETE)) { - return OTSOpType.DELETE_ROW; - } else { + type = OTSOpType.UPDATE_ROW; + }else if (value.equalsIgnoreCase(OTSConst.OTS_OP_TYPE_DELETE)) { + type = OTSOpType.DELETE_ROW; + }else { throw new IllegalArgumentException(String.format(OTSErrorMessage.OPERATION_PARSE_ERROR, value)); } + + if (mode == OTSMode.MULTI_VERSION && type == OTSOpType.PUT_ROW) { + throw new IllegalArgumentException(String.format(OTSErrorMessage.MUTLI_MODE_OPERATION_PARSE_ERROR, value)); + } + return type; } + + public static OTSMode parseOTSMode(String value) { + if (value.equalsIgnoreCase(OTSConst.OTS_MODE_NORMAL)) { + return OTSMode.NORMAL; + } else if (value.equalsIgnoreCase(OTSConst.OTS_MODE_MULTI_VERSION)) { + return OTSMode.MULTI_VERSION; + } else { + throw new IllegalArgumentException(String.format(OTSErrorMessage.MODE_PARSE_ERROR, value)); + } + } + } diff --git a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/WriterRetryPolicy.java b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/WriterRetryPolicy.java similarity index 92% rename from otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/WriterRetryPolicy.java rename to otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/WriterRetryPolicy.java index 3aa61a6834..18d12ddedf 100644 --- a/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/WriterRetryPolicy.java +++ b/otswriter/src/main/java/com/alibaba/datax/plugin/writer/otswriter/utils/WriterRetryPolicy.java @@ -1,4 +1,4 @@ -package com.alibaba.datax.plugin.writer.otswriter; +package com.alibaba.datax.plugin.writer.otswriter.utils; import com.alibaba.datax.plugin.writer.otswriter.model.OTSConf; import com.aliyun.openservices.ots.internal.OTSRetryStrategy; diff --git a/otswriter/src/main/resources/plugin.json b/otswriter/src/main/resources/plugin.json index 315e96cc3c..5151b15d9a 100644 --- a/otswriter/src/main/resources/plugin.json +++ b/otswriter/src/main/resources/plugin.json @@ -3,4 +3,4 @@ "class": "com.alibaba.datax.plugin.writer.otswriter.OtsWriter", "description": "", "developer": "alibaba" -} \ No newline at end of file +} diff --git a/package.xml b/package.xml old mode 100755 new mode 100644 index 2323ce183b..7e122a2705 --- a/package.xml +++ b/package.xml @@ -33,35 +33,35 @@ datax - oceanbasereader/target/datax/ + oceanbasev10reader/target/datax/ **/*.* datax - drdsreader/target/datax/ + obhbasereader/target/datax/ **/*.* datax - oraclereader/target/datax/ + drdsreader/target/datax/ **/*.* datax - sqlserverreader/target/datax/ + oraclereader/target/datax/ **/*.* datax - db2reader/target/datax/ + sqlserverreader/target/datax/ **/*.* @@ -103,13 +103,13 @@ datax - - otsstreamreader/target/datax/ - - **/*.* - - datax - + + otsstreamreader/target/datax/ + + **/*.* + + datax + txtfilereader/target/datax/ @@ -131,6 +131,13 @@ datax + + tdenginereader/target/datax/ + + **/*.* + + datax + streamreader/target/datax/ @@ -145,6 +152,13 @@ datax + + clickhousereader/target/datax/ + + **/*.* + + datax + hdfsreader/target/datax/ @@ -180,6 +194,76 @@ datax + + gdbreader/target/datax/ + + **/*.* + + datax + + + hbase11xsqlreader/target/datax/ + + **/*.* + + datax + + + hbase20xsqlreader/target/datax/ + + **/*.* + + datax + + + tsdbreader/target/datax/ + + **/*.* + + datax + + + datahubreader/target/datax/ + + **/*.* + + datax + + + loghubreader/target/datax/ + + **/*.* + + datax + + + starrocksreader/target/datax/ + + **/*.* + + datax + + + dorisreader/target/datax/ + + **/*.* + + datax + + + sybasereader/target/datax/ + + **/*.* + + datax + + + gaussdbreader/target/datax/ + + **/*.* + + datax + @@ -189,6 +273,20 @@ datax + + tdenginewriter/target/datax/ + + **/*.* + + datax + + + starrockswriter/target/datax/ + + **/*.* + + datax + drdswriter/target/datax/ @@ -203,6 +301,13 @@ datax + + doriswriter/target/datax/ + + **/*.* + + datax + txtfilewriter/target/datax/ @@ -323,56 +428,140 @@ datax - hbase11xsqlreader/target/datax/ + elasticsearchwriter/target/datax/ **/*.* datax - elasticsearchwriter/target/datax/ + hbase20xsqlwriter/target/datax/ **/*.* datax - hbase20xsqlreader/target/datax/ + tsdbwriter/target/datax/ **/*.* datax - hbase20xsqlwriter/target/datax/ + adbpgwriter/target/datax/ **/*.* datax - tsdbwriter/target/datax/ + cassandrawriter/target/datax/ **/*.* datax - adbpgwriter/target/datax/ + clickhousewriter/target/datax/ **/*.* datax - cassandrawriter/target/datax/ + databendwriter/target/datax/ **/*.* datax - clickhousewriter/target/datax/ + oscarwriter/target/datax/ + + **/*.* + + datax + + + oceanbasev10writer/target/datax/ + + **/*.* + + datax + + + obhbasewriter/target/datax/ + + **/*.* + + datax + + + gdbwriter/target/datax/ + + **/*.* + + datax + + + kuduwriter/target/datax/ + + **/*.* + + datax + + + hologresjdbcwriter/target/datax/ + + **/*.* + + datax + + + datahubwriter/target/datax/ + + **/*.* + + datax + + + loghubwriter/target/datax/ + + **/*.* + + datax + + + selectdbwriter/target/datax/ + + **/*.* + + datax + + + neo4jwriter/target/datax/ + + **/*.* + + datax + + + sybasewriter/target/datax/ + + **/*.* + + datax + + + gaussdbwriter/target/datax/ + + **/*.* + + datax + + + milvuswriter/target/datax/ **/*.* diff --git a/plugin-rdbms-util/pom.xml b/plugin-rdbms-util/pom.xml index 1001a37c58..6dc69e06c6 100755 --- a/plugin-rdbms-util/pom.xml +++ b/plugin-rdbms-util/pom.xml @@ -30,9 +30,20 @@ mysql mysql-connector-java - 5.1.34 + ${mysql.driver.version} test + + com.oceanbase + oceanbase-client + 2.4.11 + + + com.google.guava + guava + + + org.slf4j slf4j-api @@ -63,5 +74,5 @@ guava r05 - + diff --git a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/Key.java b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/Key.java index 0e10c7429b..9f2939c491 100755 --- a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/Key.java +++ b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/Key.java @@ -48,4 +48,11 @@ public final class Key { public static String SPLIT_FACTOR = "splitFactor"; -} \ No newline at end of file + public final static String WEAK_READ = "weakRead"; + + public final static String SAVE_POINT = "savePoint"; + + public final static String REUSE_CONN = "reuseConn"; + + public final static String PARTITION_NAME = "partitionName"; +} diff --git a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/util/ObVersion.java b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/util/ObVersion.java new file mode 100644 index 0000000000..da078df924 --- /dev/null +++ b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/util/ObVersion.java @@ -0,0 +1,90 @@ +package com.alibaba.datax.plugin.rdbms.reader.util; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author johnrobbet + */ +public class ObVersion implements Comparable { + + private static final Logger LOG = LoggerFactory.getLogger(ObVersion.class); + + private int majorVersion; + private int minorVersion; + private int releaseNumber; + private int patchNumber; + + public static final ObVersion V2276 = valueOf("2.2.76"); + public static final ObVersion V2252 = valueOf("2.2.52"); + public static final ObVersion V3 = valueOf("3.0.0.0"); + public static final ObVersion V4000 = valueOf("4.0.0.0"); + + private static final ObVersion DEFAULT_VERSION = + valueOf(System.getProperty("defaultObVersion","3.2.3.0")); + + private static final int VERSION_PART_COUNT = 4; + + public ObVersion(String version) { + try { + String[] versionParts = version.split("\\."); + majorVersion = Integer.valueOf(versionParts[0]); + minorVersion = Integer.valueOf(versionParts[1]); + releaseNumber = Integer.valueOf(versionParts[2]); + int tempPatchNum = 0; + if (versionParts.length == VERSION_PART_COUNT) { + try { + tempPatchNum = Integer.valueOf(versionParts[3]); + } catch (Exception e) { + LOG.warn("fail to parse ob version: " + e.getMessage()); + } + } + patchNumber = tempPatchNum; + } catch (Exception ex) { + LOG.warn("fail to get ob version, using default {} {}", + DEFAULT_VERSION, ex.getMessage()); + majorVersion = DEFAULT_VERSION.majorVersion; + minorVersion = DEFAULT_VERSION.minorVersion; + releaseNumber = DEFAULT_VERSION.releaseNumber; + patchNumber = DEFAULT_VERSION.patchNumber; + } + } + + public static ObVersion valueOf(String version) { + return new ObVersion(version); + } + + @Override + public int compareTo(ObVersion o) { + if (this.majorVersion > o.majorVersion) { + return 1; + } else if (this.majorVersion < o.majorVersion) { + return -1; + } + + if (this.minorVersion > o.minorVersion) { + return 1; + } else if (this.minorVersion < o.minorVersion) { + return -1; + } + + if (this.releaseNumber > o.releaseNumber) { + return 1; + } else if (this.releaseNumber < o.releaseNumber) { + return -1; + } + + if (this.patchNumber > o.patchNumber) { + return 1; + } else if (this.patchNumber < o.patchNumber) { + return -1; + } + + return 0; + } + + @Override + public String toString() { + return String.format("%d.%d.%d.%d", majorVersion, minorVersion, releaseNumber, patchNumber); + } +} diff --git a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/util/OriginalConfPretreatmentUtil.java b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/util/OriginalConfPretreatmentUtil.java index 3ac5f2af7d..ef3a876d52 100755 --- a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/util/OriginalConfPretreatmentUtil.java +++ b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/util/OriginalConfPretreatmentUtil.java @@ -261,7 +261,7 @@ private static boolean recognizeTableOrQuerySqlMode( // 混合配制 table 和 querySql if (!ListUtil.checkIfValueSame(tableModeFlags) - || !ListUtil.checkIfValueSame(tableModeFlags)) { + || !ListUtil.checkIfValueSame(querySqlModeFlags)) { throw DataXException.asDataXException(DBUtilErrorCode.TABLE_QUERYSQL_MIXED, "您配置凌乱了. 不能同时既配置table又配置querySql. 请检查您的配置并作出修改."); } diff --git a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/util/SingleTableSplitUtil.java b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/util/SingleTableSplitUtil.java old mode 100755 new mode 100644 index d9846b3939..844b6cfd5f --- a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/util/SingleTableSplitUtil.java +++ b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/reader/util/SingleTableSplitUtil.java @@ -5,8 +5,9 @@ import com.alibaba.datax.plugin.rdbms.reader.Constant; import com.alibaba.datax.plugin.rdbms.reader.Key; import com.alibaba.datax.plugin.rdbms.util.*; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; +import java.text.MessageFormat; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; @@ -20,6 +21,7 @@ import java.sql.Types; import java.util.ArrayList; import java.util.List; +import static org.apache.commons.lang3.StringUtils.EMPTY; public class SingleTableSplitUtil { private static final Logger LOG = LoggerFactory @@ -93,6 +95,7 @@ public static List splitSingleTable( allQuerySql.add(tempQuerySql); tempConfig.set(Key.QUERY_SQL, tempQuerySql); + tempConfig.set(Key.WHERE, (hasWhere ? ("(" + where + ") and") : "") + range); pluginParams.add(tempConfig); } } else { @@ -103,6 +106,7 @@ public static List splitSingleTable( + String.format(" %s IS NOT NULL", splitPkName); allQuerySql.add(tempQuerySql); tempConfig.set(Key.QUERY_SQL, tempQuerySql); + tempConfig.set(Key.WHERE, (hasWhere ? "(" + where + ") and" : "") + String.format(" %s IS NOT NULL", splitPkName)); pluginParams.add(tempConfig); } @@ -118,6 +122,7 @@ public static List splitSingleTable( StringUtils.join(allQuerySql, "\n")); tempConfig.set(Key.QUERY_SQL, tempQuerySql); + tempConfig.set(Key.WHERE, (hasWhere ? "(" + where + ") and" : "") + String.format(" %s IS NULL", splitPkName)); pluginParams.add(tempConfig); return pluginParams; @@ -254,6 +259,7 @@ private static boolean isLongType(int type) { switch (SingleTableSplitUtil.DATABASE_TYPE) { case Oracle: + case OceanBase: isValidLongType |= type == Types.NUMERIC; break; default: @@ -273,7 +279,24 @@ private static String genPKRangeSQL(Configuration configuration) { String splitPK = configuration.getString(Key.SPLIT_PK).trim(); String table = configuration.getString(Key.TABLE).trim(); String where = configuration.getString(Key.WHERE, null); - return genPKSql(splitPK,table,where); + String obMode = configuration.getString("obCompatibilityMode"); + // OceanBase对SELECT MIN(%s),MAX(%s) FROM %s这条sql没有做查询改写,会进行全表扫描,在数据量的时候查询耗时很大甚至超时; + // 所以对于OceanBase数据库,查询模板需要改写为分别查询最大值和最小值。这样可以提升查询数量级的性能。 + if (DATABASE_TYPE == DataBaseType.OceanBase && StringUtils.isNotEmpty(obMode)) { + boolean isOracleMode = "ORACLE".equalsIgnoreCase(obMode); + + String minMaxTemplate = isOracleMode ? "select v2.id as min_a, v1.id as max_a from (" + + "select * from (select %s as id from %s {0} order by id desc) where rownum =1 ) v1," + + "(select * from (select %s as id from %s order by id asc) where rownum =1 ) v2;" : + "select v2.id as min_a, v1.id as max_a from (select %s as id from %s {0} order by id desc limit 1) v1," + + "(select %s as id from %s order by id asc limit 1) v2;"; + + String pkRangeSQL = String.format(minMaxTemplate, splitPK, table, splitPK, table); + String whereString = StringUtils.isNotBlank(where) ? String.format("WHERE (%s AND %s IS NOT NULL)", where, splitPK) : EMPTY; + pkRangeSQL = MessageFormat.format(pkRangeSQL, whereString); + return pkRangeSQL; + } + return genPKSql(splitPK, table, where); } public static String genPKSql(String splitPK, String table, String where){ diff --git a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/DBUtil.java b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/DBUtil.java index 63d1621b34..12a3aa7475 100755 --- a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/DBUtil.java +++ b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/DBUtil.java @@ -358,7 +358,7 @@ private static synchronized Connection connect(DataBaseType dataBaseType, String url, String user, String pass, String socketTimeout) { //ob10的处理 - if (url.startsWith(com.alibaba.datax.plugin.rdbms.writer.Constant.OB10_SPLIT_STRING) && dataBaseType == DataBaseType.MySql) { + if (url.startsWith(com.alibaba.datax.plugin.rdbms.writer.Constant.OB10_SPLIT_STRING)) { String[] ss = url.split(com.alibaba.datax.plugin.rdbms.writer.Constant.OB10_SPLIT_STRING_PATTERN); if (ss.length != 3) { throw DataXException @@ -367,7 +367,7 @@ private static synchronized Connection connect(DataBaseType dataBaseType, } LOG.info("this is ob1_0 jdbc url."); user = ss[1].trim() +":"+user; - url = ss[2]; + url = ss[2].replace("jdbc:mysql:", "jdbc:oceanbase:"); LOG.info("this is ob1_0 jdbc url. user="+user+" :url="+url); } @@ -380,6 +380,9 @@ private static synchronized Connection connect(DataBaseType dataBaseType, // unit ms prop.put("oracle.jdbc.ReadTimeout", socketTimeout); } + if (dataBaseType == DataBaseType.OceanBase) { + url = url.replace("jdbc:mysql:", "jdbc:oceanbase:"); + } return connect(dataBaseType, url, prop); } @@ -717,6 +720,11 @@ public static void dealWithSessionConfig(Connection conn, new ArrayList(), String.class); DBUtil.doDealWithSessionConfig(conn, sessionConfig, message); break; + case SQLServer: + sessionConfig = config.getList(Key.SESSION, + new ArrayList(), String.class); + DBUtil.doDealWithSessionConfig(conn, sessionConfig, message); + break; default: break; } diff --git a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/DataBaseType.java b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/DataBaseType.java index eb82aa448c..dec2353d9e 100755 --- a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/DataBaseType.java +++ b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/DataBaseType.java @@ -18,10 +18,17 @@ public enum DataBaseType { PostgreSQL("postgresql", "org.postgresql.Driver"), RDBMS("rdbms", "com.alibaba.datax.plugin.rdbms.util.DataBaseType"), DB2("db2", "com.ibm.db2.jcc.DB2Driver"), + ADB("adb","com.mysql.jdbc.Driver"), ADS("ads","com.mysql.jdbc.Driver"), ClickHouse("clickhouse", "ru.yandex.clickhouse.ClickHouseDriver"), - KingbaseES("kingbasees", "com.kingbase8.Driver"); - + KingbaseES("kingbasees", "com.kingbase8.Driver"), + Oscar("oscar", "com.oscar.Driver"), + OceanBase("oceanbase", "com.alipay.oceanbase.jdbc.Driver"), + StarRocks("starrocks", "com.mysql.jdbc.Driver"), + Sybase("sybase", "com.sybase.jdbc4.jdbc.SybDriver"), + GaussDB("gaussdb", "org.opengauss.Driver"), + Databend("databend", "com.databend.jdbc.DatabendDriver"), + Doris("doris","com.mysql.jdbc.Driver"); private String typeName; private String driverClassName; @@ -41,6 +48,7 @@ public String appendJDBCSuffixForReader(String jdbc) { switch (this) { case MySql: case DRDS: + case OceanBase: suffix = "yearIsDateType=false&zeroDateTimeBehavior=convertToNull&tinyInt1isBit=false&rewriteBatchedStatements=true"; if (jdbc.contains("?")) { result = jdbc + "&" + suffix; @@ -55,13 +63,21 @@ public String appendJDBCSuffixForReader(String jdbc) { case DB2: break; case PostgreSQL: - break; + break; case ClickHouse: break; case RDBMS: break; case KingbaseES: break; + case Oscar: + break; + case StarRocks: + break; + case GaussDB: + break; + case Doris: + break; default: throw DataXException.asDataXException(DBUtilErrorCode.UNSUPPORTED_TYPE, "unsupported database type."); } @@ -81,6 +97,14 @@ public String appendJDBCSuffixForWriter(String jdbc) { result = jdbc + "?" + suffix; } break; + case ADB: + suffix = "yearIsDateType=false&zeroDateTimeBehavior=convertToNull&rewriteBatchedStatements=true&tinyInt1isBit=false"; + if (jdbc.contains("?")) { + result = jdbc + "&" + suffix; + } else { + result = jdbc + "?" + suffix; + } + break; case DRDS: suffix = "yearIsDateType=false&zeroDateTimeBehavior=convertToNull"; if (jdbc.contains("?")) { @@ -96,13 +120,29 @@ public String appendJDBCSuffixForWriter(String jdbc) { case DB2: break; case PostgreSQL: - break; + break; case ClickHouse: break; case RDBMS: break; + case Databend: + break; case KingbaseES: break; + case Oscar: + break; + case OceanBase: + suffix = "yearIsDateType=false&zeroDateTimeBehavior=convertToNull&tinyInt1isBit=false&rewriteBatchedStatements=true"; + if (jdbc.contains("?")) { + result = jdbc + "&" + suffix; + } else { + result = jdbc + "?" + suffix; + } + break; + case Sybase: + break; + case GaussDB: + break; default: throw DataXException.asDataXException(DBUtilErrorCode.UNSUPPORTED_TYPE, "unsupported database type."); } @@ -128,7 +168,10 @@ public String formatPk(String splitPk) { case DB2: case PostgreSQL: case KingbaseES: - break; + case Oscar: + break; + case GaussDB: + break; default: throw DataXException.asDataXException(DBUtilErrorCode.UNSUPPORTED_TYPE, "unsupported database type."); } @@ -152,6 +195,9 @@ public String quoteColumnName(String columnName) { case DB2: case PostgreSQL: case KingbaseES: + case Oscar: + break; + case GaussDB: break; default: throw DataXException.asDataXException(DBUtilErrorCode.UNSUPPORTED_TYPE, "unsupported database type"); @@ -177,6 +223,10 @@ public String quoteTableName(String tableName) { break; case KingbaseES: break; + case Oscar: + break; + case GaussDB: + break; default: throw DataXException.asDataXException(DBUtilErrorCode.UNSUPPORTED_TYPE, "unsupported database type"); } diff --git a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/RdbmsException.java b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/RdbmsException.java index 4b6601adb9..7091bd5a24 100644 --- a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/RdbmsException.java +++ b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/RdbmsException.java @@ -69,32 +69,32 @@ public static DBUtilErrorCode oracleConnectionErrorAna(String e){ } public static DataXException asQueryException(DataBaseType dataBaseType, Exception e,String querySql,String table,String userName){ - if (dataBaseType.equals(DataBaseType.MySql)){ + if (dataBaseType.equals(DataBaseType.MySql)) { DBUtilErrorCode dbUtilErrorCode = mySqlQueryErrorAna(e.getMessage()); - if (dbUtilErrorCode == DBUtilErrorCode.MYSQL_QUERY_TABLE_NAME_ERROR && table != null){ - return DataXException.asDataXException(dbUtilErrorCode,"表名为:"+table+" 执行的SQL为:"+querySql+" 具体错误信息为:"+e); + if (dbUtilErrorCode == DBUtilErrorCode.MYSQL_QUERY_TABLE_NAME_ERROR && table != null) { + return DataXException.asDataXException(dbUtilErrorCode, "表名为:" + table + " 执行的SQL为:" + querySql + " 具体错误信息为:" + e, e); } - if (dbUtilErrorCode == DBUtilErrorCode.MYSQL_QUERY_SELECT_PRI_ERROR && userName != null){ - return DataXException.asDataXException(dbUtilErrorCode,"用户名为:"+userName+" 具体错误信息为:"+e); + if (dbUtilErrorCode == DBUtilErrorCode.MYSQL_QUERY_SELECT_PRI_ERROR && userName != null) { + return DataXException.asDataXException(dbUtilErrorCode, "用户名为:" + userName + " 具体错误信息为:" + e, e); } - return DataXException.asDataXException(dbUtilErrorCode,"执行的SQL为: "+querySql+" 具体错误信息为:"+e); + return DataXException.asDataXException(dbUtilErrorCode, "执行的SQL为: " + querySql + " 具体错误信息为:" + e, e); } - if (dataBaseType.equals(DataBaseType.Oracle)){ + if (dataBaseType.equals(DataBaseType.Oracle)) { DBUtilErrorCode dbUtilErrorCode = oracleQueryErrorAna(e.getMessage()); - if (dbUtilErrorCode == DBUtilErrorCode.ORACLE_QUERY_TABLE_NAME_ERROR && table != null){ - return DataXException.asDataXException(dbUtilErrorCode,"表名为:"+table+" 执行的SQL为:"+querySql+" 具体错误信息为:"+e); + if (dbUtilErrorCode == DBUtilErrorCode.ORACLE_QUERY_TABLE_NAME_ERROR && table != null) { + return DataXException.asDataXException(dbUtilErrorCode, "表名为:" + table + " 执行的SQL为:" + querySql + " 具体错误信息为:" + e, e); } - if (dbUtilErrorCode == DBUtilErrorCode.ORACLE_QUERY_SELECT_PRI_ERROR){ - return DataXException.asDataXException(dbUtilErrorCode,"用户名为:"+userName+" 具体错误信息为:"+e); + if (dbUtilErrorCode == DBUtilErrorCode.ORACLE_QUERY_SELECT_PRI_ERROR) { + return DataXException.asDataXException(dbUtilErrorCode, "用户名为:" + userName + " 具体错误信息为:" + e, e); } - return DataXException.asDataXException(dbUtilErrorCode,"执行的SQL为: "+querySql+" 具体错误信息为:"+e); + return DataXException.asDataXException(dbUtilErrorCode, "执行的SQL为: " + querySql + " 具体错误信息为:" + e, e); } - return DataXException.asDataXException(DBUtilErrorCode.SQL_EXECUTE_FAIL, "执行的SQL为: "+querySql+" 具体错误信息为:"+e); + return DataXException.asDataXException(DBUtilErrorCode.SQL_EXECUTE_FAIL, "执行的SQL为: " + querySql + " 具体错误信息为:" + e, e); } public static DBUtilErrorCode mySqlQueryErrorAna(String e){ diff --git a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/SplitedSlice.java b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/SplitedSlice.java new file mode 100644 index 0000000000..d8de129490 --- /dev/null +++ b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/util/SplitedSlice.java @@ -0,0 +1,37 @@ +package com.alibaba.datax.plugin.rdbms.util; + +public class SplitedSlice { + private String begin; + private String end; + private String range; + + public SplitedSlice(String begin, String end, String range) { + this.begin = begin; + this.end = end; + this.range = range; + } + + public String getBegin() { + return begin; + } + + public void setBegin(String begin) { + this.begin = begin; + } + + public String getEnd() { + return end; + } + + public void setEnd(String end) { + this.end = end; + } + + public String getRange() { + return range; + } + + public void setRange(String range) { + this.range = range; + } +} diff --git a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/CommonRdbmsWriter.java b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/CommonRdbmsWriter.java index 440aac2ad8..7b84c32088 100755 --- a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/CommonRdbmsWriter.java +++ b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/CommonRdbmsWriter.java @@ -12,6 +12,7 @@ import com.alibaba.datax.plugin.rdbms.util.RdbmsException; import com.alibaba.datax.plugin.rdbms.writer.util.OriginalConfPretreatmentUtil; import com.alibaba.datax.plugin.rdbms.writer.util.WriterUtil; +import java.util.concurrent.atomic.AtomicLong; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Triple; import org.slf4j.Logger; @@ -199,6 +200,9 @@ public static class Task { protected boolean emptyAsNull; protected Triple, List, List> resultSetMetaData; + private int dumpRecordLimit = Constant.DEFAULT_DUMP_RECORD_LIMIT; + private AtomicLong dumpRecordCount = new AtomicLong(0); + public Task(DataBaseType dataBaseType) { this.dataBaseType = dataBaseType; } @@ -209,7 +213,7 @@ public void init(Configuration writerSliceConfig) { this.jdbcUrl = writerSliceConfig.getString(Key.JDBC_URL); //ob10的处理 - if (this.jdbcUrl.startsWith(Constant.OB10_SPLIT_STRING) && this.dataBaseType == DataBaseType.MySql) { + if (this.jdbcUrl.startsWith(Constant.OB10_SPLIT_STRING)) { String[] ss = this.jdbcUrl.split(Constant.OB10_SPLIT_STRING_PATTERN); if (ss.length != 3) { throw DataXException @@ -368,7 +372,11 @@ protected void doBatchInsert(Connection connection, List buffer) } } - protected void doOneInsert(Connection connection, List buffer) { + public boolean needToDumpRecord() { + return dumpRecordCount.incrementAndGet() <= dumpRecordLimit; + } + + public void doOneInsert(Connection connection, List buffer) { PreparedStatement preparedStatement = null; try { connection.setAutoCommit(true); @@ -381,7 +389,10 @@ protected void doOneInsert(Connection connection, List buffer) { preparedStatement, record); preparedStatement.execute(); } catch (SQLException e) { - LOG.debug(e.toString()); + if (needToDumpRecord()) { + LOG.warn("ERROR : record {}", record); + LOG.warn("Insert fatal error SqlState ={}, errorCode = {}, {}", e.getSQLState(), e.getErrorCode(), e); + } this.taskPluginCollector.collectDirtyRecord(record, e); } finally { @@ -402,13 +413,15 @@ protected PreparedStatement fillPreparedStatement(PreparedStatement preparedStat throws SQLException { for (int i = 0; i < this.columnNumber; i++) { int columnSqltype = this.resultSetMetaData.getMiddle().get(i); - preparedStatement = fillPreparedStatementColumnType(preparedStatement, i, columnSqltype, record.getColumn(i)); + String typeName = this.resultSetMetaData.getRight().get(i); + preparedStatement = fillPreparedStatementColumnType(preparedStatement, i, columnSqltype, typeName, record.getColumn(i)); } return preparedStatement; } - protected PreparedStatement fillPreparedStatementColumnType(PreparedStatement preparedStatement, int columnIndex, int columnSqltype, Column column) throws SQLException { + protected PreparedStatement fillPreparedStatementColumnType(PreparedStatement preparedStatement, int columnIndex, + int columnSqltype, String typeName, Column column) throws SQLException { java.util.Date utilDate; switch (columnSqltype) { case Types.CHAR: @@ -451,8 +464,11 @@ protected PreparedStatement fillPreparedStatementColumnType(PreparedStatement pr // for mysql bug, see http://bugs.mysql.com/bug.php?id=35115 case Types.DATE: - if (this.resultSetMetaData.getRight().get(columnIndex) - .equalsIgnoreCase("year")) { + if (typeName == null) { + typeName = this.resultSetMetaData.getRight().get(columnIndex); + } + + if (typeName.equalsIgnoreCase("year")) { if (column.asBigInteger() == null) { preparedStatement.setString(columnIndex + 1, null); } else { @@ -514,7 +530,7 @@ protected PreparedStatement fillPreparedStatementColumnType(PreparedStatement pr break; case Types.BOOLEAN: - preparedStatement.setString(columnIndex + 1, column.asString()); + preparedStatement.setBoolean(columnIndex + 1, column.asBoolean()); break; // warn: bit(1) -> Types.BIT 可使用setBoolean diff --git a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/Constant.java b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/Constant.java index 0e4692e2c8..9510fd14ef 100755 --- a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/Constant.java +++ b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/Constant.java @@ -19,4 +19,5 @@ public final class Constant { public static final String OB10_SPLIT_STRING = "||_dsc_ob10_dsc_||"; public static final String OB10_SPLIT_STRING_PATTERN = "\\|\\|_dsc_ob10_dsc_\\|\\|"; + public static final int DEFAULT_DUMP_RECORD_LIMIT = 10; } diff --git a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/Key.java b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/Key.java index 25a2ab52f8..3c282d5d7d 100755 --- a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/Key.java +++ b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/Key.java @@ -11,6 +11,8 @@ public final class Key { public final static String COLUMN = "column"; + public final static String ONCONFLICT_COLUMN = "onConflictColumn"; + //可选值为:insert,replace,默认为 insert (mysql 支持,oracle 没用 replace 机制,只能 insert,oracle 可以不暴露这个参数) public final static String WRITE_MODE = "writeMode"; diff --git a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/util/OriginalConfPretreatmentUtil.java b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/util/OriginalConfPretreatmentUtil.java index c42dd3eac4..556e50ac66 100755 --- a/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/util/OriginalConfPretreatmentUtil.java +++ b/plugin-rdbms-util/src/main/java/com/alibaba/datax/plugin/rdbms/writer/util/OriginalConfPretreatmentUtil.java @@ -10,6 +10,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.sql.Connection; import java.util.ArrayList; import java.util.List; @@ -62,7 +63,7 @@ public static void simplifyConf(Configuration originalConfig) { throw DataXException.asDataXException(DBUtilErrorCode.REQUIRED_VALUE, "您未配置的写入数据库表的 jdbcUrl."); } - jdbcUrl = DATABASE_TYPE.appendJDBCSuffixForReader(jdbcUrl); + jdbcUrl = DATABASE_TYPE.appendJDBCSuffixForWriter(jdbcUrl); originalConfig.set(String.format("%s[%d].%s", Constant.CONN_MARK, i, Key.JDBC_URL), jdbcUrl); @@ -120,9 +121,15 @@ public static void dealColumnConf(Configuration originalConfig, ConnectionFactor } else { // 确保用户配置的 column 不重复 ListUtil.makeSureNoValueDuplicate(userConfiguredColumns, false); + Connection connection = null; + try { + connection = connectionFactory.getConnecttion(); + // 检查列是否都为数据库表中正确的列(通过执行一次 select column from table 进行判断) + DBUtil.getColumnMetaData(connection, oneTable,StringUtils.join(userConfiguredColumns, ",")); + } finally { + DBUtil.closeDBResources(null, connection); + } - // 检查列是否都为数据库表中正确的列(通过执行一次 select column from table 进行判断) - DBUtil.getColumnMetaData(connectionFactory.getConnecttion(), oneTable,StringUtils.join(userConfiguredColumns, ",")); } } } diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/FileFormat.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/FileFormat.java new file mode 100644 index 0000000000..b9368e676c --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/FileFormat.java @@ -0,0 +1,97 @@ +package com.alibaba.datax.plugin.unstructuredstorage; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.unstructuredstorage.reader.Constant; +import com.alibaba.datax.plugin.unstructuredstorage.reader.Key; +import com.alibaba.datax.plugin.unstructuredstorage.reader.UnstructuredStorageReaderErrorCode; +import org.apache.commons.lang3.StringUtils; + +import java.util.Arrays; + +/** + * @Author: guxuan + * @Date 2022-05-17 16:04 + */ +public enum FileFormat { + TEXT("text"), + CSV("csv"), + EXCEL("excel"), + BINARY("binary"); + + private String fileFormat; + + private boolean isText; + private boolean isCsv; + private boolean isExcel; + private boolean isBinary; + + FileFormat(String fileFormat) { + this.fileFormat = fileFormat.toLowerCase(); + } + + /** + * 获取文件类型: 目前支持text,csv,excel,binary + * @param configuration + * @return + */ + public static FileFormat getFileFormatByConfiguration(Configuration configuration) { + String fileFormat = configuration.getString(Key.FILE_FORMAT, Constant.DEFAULT_FILE_FORMAT); + return FileFormat.getByTypeName(fileFormat); + } + + public String getFileFormat() { + return this.fileFormat; + } + + public static FileFormat getByTypeName(String fileFormat) { + for (FileFormat fFormat : values()) { + if (fFormat.fileFormat.equalsIgnoreCase(fileFormat)) { + return fFormat; + } + } + throw DataXException.asDataXException(UnstructuredStorageReaderErrorCode.ILLEGAL_VALUE, + String.format("DataX 不支持该 fileFormat 类型:%s, 目前支持的 fileFormat 类型是:%s", fileFormat, Arrays.asList(values()))); + } + + public boolean equalsIgnoreCase(String fileFormat){ + return StringUtils.equalsIgnoreCase(fileFormat, this.fileFormat); + } + + public boolean isText() { + return this.equalsIgnoreCase(Constant.FILE_FORMAT_TEXT); + } + + public void setText(boolean text) { + isText = text; + } + + public boolean isCsv() { + return this.equalsIgnoreCase(Constant.FILE_FORMAT_CSV); + } + + public void setCsv(boolean csv) { + isCsv = csv; + } + + public boolean isExcel() { + return this.equalsIgnoreCase(Constant.FILE_FORMAT_EXCEL); + } + + public void setExcel(boolean excel) { + isExcel = excel; + } + + public boolean isBinary() { + return this.equalsIgnoreCase(Constant.FILE_FORMAT_BINARY); + } + + public void setBinary(boolean binary) { + isBinary = binary; + } + + @Override + public String toString(){ + return this.fileFormat; + } +} diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings.properties b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings.properties new file mode 100644 index 0000000000..d53d474963 --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings.properties @@ -0,0 +1 @@ +fileformaterror.1=DataX \u4E0D\u652F\u6301\u8BE5 fileFormat \u7C7B\u578B:{0}, \u76EE\u524D\u652F\u6301\u7684 fileFormat \u7C7B\u578B\u662F:{1} \ No newline at end of file diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_en_US.properties b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_en_US.properties new file mode 100644 index 0000000000..d53d474963 --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_en_US.properties @@ -0,0 +1 @@ +fileformaterror.1=DataX \u4E0D\u652F\u6301\u8BE5 fileFormat \u7C7B\u578B:{0}, \u76EE\u524D\u652F\u6301\u7684 fileFormat \u7C7B\u578B\u662F:{1} \ No newline at end of file diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_ja_JP.properties b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_ja_JP.properties new file mode 100644 index 0000000000..d53d474963 --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_ja_JP.properties @@ -0,0 +1 @@ +fileformaterror.1=DataX \u4E0D\u652F\u6301\u8BE5 fileFormat \u7C7B\u578B:{0}, \u76EE\u524D\u652F\u6301\u7684 fileFormat \u7C7B\u578B\u662F:{1} \ No newline at end of file diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_zh_CN.properties b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_zh_CN.properties new file mode 100644 index 0000000000..d53d474963 --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_zh_CN.properties @@ -0,0 +1 @@ +fileformaterror.1=DataX \u4E0D\u652F\u6301\u8BE5 fileFormat \u7C7B\u578B:{0}, \u76EE\u524D\u652F\u6301\u7684 fileFormat \u7C7B\u578B\u662F:{1} \ No newline at end of file diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_zh_HK.properties b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_zh_HK.properties new file mode 100644 index 0000000000..b92a73ecd7 --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_zh_HK.properties @@ -0,0 +1 @@ +fileformaterror.1=DataX \u4E0D\u652F\u6301\u8BE5 fileFormat \u7C7B\u578B:{0}, \u76EE\u524D\u652F\u6301\u7684 fileFormat \u7C7B\u578B\u662F:{1}fileformaterror.1=DataX不支持該fileFormat類型:{0},現時支持的fileFormat類型是:{1} diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_zh_TW.properties b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_zh_TW.properties new file mode 100644 index 0000000000..b92a73ecd7 --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/LocalStrings_zh_TW.properties @@ -0,0 +1 @@ +fileformaterror.1=DataX \u4E0D\u652F\u6301\u8BE5 fileFormat \u7C7B\u578B:{0}, \u76EE\u524D\u652F\u6301\u7684 fileFormat \u7C7B\u578B\u662F:{1}fileformaterror.1=DataX不支持該fileFormat類型:{0},現時支持的fileFormat類型是:{1} diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/ColumnEntry.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/ColumnEntry.java index ee3af81601..6bfc1bb9e2 100644 --- a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/ColumnEntry.java +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/ColumnEntry.java @@ -1,11 +1,11 @@ package com.alibaba.datax.plugin.unstructuredstorage.reader; +import com.alibaba.fastjson2.JSON; +import org.apache.commons.lang3.StringUtils; + import java.text.DateFormat; import java.text.SimpleDateFormat; -import org.apache.commons.lang3.StringUtils; - -import com.alibaba.fastjson.JSON; public class ColumnEntry { private Integer index; @@ -13,6 +13,15 @@ public class ColumnEntry { private String value; private String format; private DateFormat dateParse; + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } public Integer getIndex() { return index; diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/Constant.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/Constant.java index 7c6bc13956..6be46cb20c 100755 --- a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/Constant.java +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/Constant.java @@ -10,4 +10,47 @@ public class Constant { public static final String DEFAULT_NULL_FORMAT = "\\N"; public static final Integer DEFAULT_BUFFER_SIZE = 8192; + + public static final String FILE_FORMAT_CSV = "csv"; + + public static final String FILE_FORMAT_TEXT = "text"; + + public static final String FILE_FORMAT_EXCEL = "excel"; + + public static final String FILE_FORMAT_BINARY = "binary"; + + public static final String DEFAULT_FILE_FORMAT = "csv"; + + public static final Boolean DEFAULE_SKIP_TEXT_EMPTY_RECORDS = true; + + public static final String EXCEL_VERSION_03_OR_EARLIER = "03_OR_EARLIER"; + + public static final String EXCEL_VERSION_07_OR_LATER = "07_OR_LATER"; + + /** + * 文件全限定名 + * */ + public static final String SOURCE_FILE = "sourceFile"; + + /** + * 单纯的文件名 + * */ + public static final String SOURCE_FILE_NAME = "sourceFileName"; + + public static final boolean DEFAULT_OUTPUT_SHEET_NAME = false; + + /** + * TODO 暂时先不考虑整个文件夹同步 + * 在同步音视频等二进制文件的情况下: + * 半结构读插件(txtfilreader, ftpreader, hdfsreader, ossreader)需要将相对文件路径注入 RELATIVE_SOURCE_FILE 属性 + * 目的是半结构化写插件可以统一使用 RELATIVE_SOURCE_FILE 获取到读端插件的所有二进制文件名及其相对路径。 + * 举个栗子: + * 读端插件PATH配置了/home/admin/myapp/ + */ + public static final String RELATIVE_SOURCE_FILE = "relativeSourceFile"; + + /** + * 默认读取二进制文件一次性读取的Byte数目: 1048576 Byte [1MB] + */ + public static final int DEFAULT_BLOCK_SIZE_IN_BYTE = 1048576; } diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/Key.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/Key.java index bb5bf59fee..0945779b17 100755 --- a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/Key.java +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/Key.java @@ -28,4 +28,66 @@ public class Key { public static final String CSV_READER_CONFIG = "csvReaderConfig"; + public static final String MARK_DONE_FILE_NAME = "markDoneFileName"; + + public static final String MARK_DOING_FILE_NAME = "markDoingFileName"; + + // public static final String RETRY_TIME = "retryTime"; + public final static String MAX_RETRY_TIME = "maxRetryTime"; + + public final static String RETRY_INTERVAL = "retryInterval"; + + public static final String TEXT_READER_CONFIG = "textReaderConfig"; + + public static final String SKIP_EMPTY_RECORDS = "skipEmptyRecords"; + + public static final String EXCEL_READER_CONFIG = "excelReaderConfig"; + + public static final String EXCEL_SHEET_NAME = "excelSheetName"; + + public static final String VERSION = "version"; + + public static final String OUTPUT_SHEET_NAME = "outputSheetName"; + + /** + * csv or text or excel + */ + public static final String FILE_FORMAT = "fileFormat"; + + /** + * 是否把一个file当做一个column + */ + public static final String FILE_AS_COLUMN = "fileAsColumn"; + + /** + * 读取二进制文件一次性读取的Byte数目 + */ + public static final String BLOCK_SIZE_IN_BYTE = "blockSizeInByte"; + + /** + * 半结构化标示一个Record来源的绝对文件路径名,可以是ftp文件,oss的object等 + * */ + public static final String META_KEY_FILE_PATH = "filePath"; + + /** + * 多文件切分的工作项,Task通过此配置项表示工作内容, 文件内部切分相关key + */ + public static final String SPLIT_SLICE_CONFIG = "__splitSliceConfig"; + public static final String SPLIT_SLICE_FILE_PATH = "filePath"; + public static final String SPLIT_SLICE_START_POINT = "startPoint"; + public static final String SPLIT_SLICE_END_POINT = "endPoint"; + + /** + * tar.gz压缩包,支持配置 tarFileFilterPattern 参数,来过滤要同步的文件 + * For Example: + * "tarFileFilterPattern" : "*.dat" + * + * 同步的时候,只同步 tar.gz 里面文件名后缀为 .dat 的文件 + */ + public static final String TAR_FILE_FILTER_PATTERN = "tarFileFilterPattern"; + public static final String ENABLE_INNER_SPLIT = "enableInnerSplit"; + + public static final String HIVE_PARTION_COLUMN = "hivePartitionColumn"; + + } diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/UnstructuredStorageReaderUtil.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/UnstructuredStorageReaderUtil.java index 423f66db99..27f4c48ac4 100755 --- a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/UnstructuredStorageReaderUtil.java +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/UnstructuredStorageReaderUtil.java @@ -5,9 +5,9 @@ import com.alibaba.datax.common.plugin.RecordSender; import com.alibaba.datax.common.plugin.TaskPluginCollector; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; -import com.alibaba.fastjson.TypeReference; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.alibaba.fastjson2.TypeReference; import com.csvreader.CsvReader; import org.apache.commons.beanutils.BeanUtils; import io.airlift.compress.snappy.SnappyCodec; @@ -26,10 +26,7 @@ import java.io.*; import java.nio.charset.UnsupportedCharsetException; import java.text.DateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; +import java.util.*; public class UnstructuredStorageReaderUtil { private static final Logger LOG = LoggerFactory @@ -695,4 +692,93 @@ public static void setCsvReaderConfig(CsvReader csvReader){ LOG.info(String.format("CsvReader使用默认值[%s],csvReaderConfig值为[%s]",JSON.toJSONString(csvReader),JSON.toJSONString(UnstructuredStorageReaderUtil.csvReaderConfigMap))); } } + + public static Map buildRecordMeta(String filePath) { + Map meta = new HashMap(); + // 上下文filePath元数据注入, 目前传递的是纯文件名 + // File file = new File(filePath); + // meta.put(Key.META_KEY_FILE_PATH, file.getName()); + meta.put(Key.META_KEY_FILE_PATH, filePath); + return meta; + } + + public static void setSourceFileName(Configuration configuration, List sourceFiles){ + List sourceFilesName = new ArrayList(); + File file; + for (String sourceFile: sourceFiles){ + file = new File(sourceFile); + sourceFilesName.add(file.getName()); + } + configuration.set(Constant.SOURCE_FILE_NAME, sourceFilesName); + } + + public static void setSourceFile(Configuration configuration, List sourceFiles){ + configuration.set(Constant.SOURCE_FILE, sourceFiles); + } + + public static ArrayList getHivePartitionColumns(String filePath, List hivePartitionColumnEntrys) { + ArrayList hivePartitionColumns = new ArrayList<>(); + + if (null == hivePartitionColumnEntrys) { + return hivePartitionColumns; + } + + // 对于分区列pt,则从path中找/pt=xxx/,xxx即分区列的值,另外确认在path中只有一次出现 + + for (ColumnEntry columnEntry : hivePartitionColumnEntrys) { + String parColName = columnEntry.getValue(); + String patten = String.format("/%s=", parColName); + int index = filePath.indexOf(patten); + if (index != filePath.lastIndexOf(patten)) { + throw new DataXException(String.format("Found multiple partition folder in filePath %s, partition: %s", filePath, parColName)); + } + + String subPath = filePath.substring(index + 1); + int firstSeparatorIndex = subPath.indexOf(File.separator); + if (firstSeparatorIndex > 0) { + subPath = subPath.substring(0, firstSeparatorIndex); + } + + if (subPath.split("=").length != 2) { + throw new DataXException(String.format("Found partition column value in filePath %s failed, partition: %s", filePath, parColName)); + } + String parColVal = subPath.split("=")[1]; + + String colType = columnEntry.getType().toUpperCase(); + Type type = Type.valueOf(colType); + + Column generateColumn; + switch (type) { + case STRING: + generateColumn = new StringColumn(parColVal); + break; + + case DOUBLE: + generateColumn = new DoubleColumn(parColVal); + break; + + case LONG: + generateColumn = new LongColumn(parColVal); + break; + + case BOOLEAN: + generateColumn = new BoolColumn(parColVal); + break; + + case DATE: + generateColumn = new DateColumn(new StringColumn(parColVal.toString()).asDate()); + break; + + default: + String errorMessage = String.format("The column type you configured is not currently supported: %s", parColVal); + LOG.error(errorMessage); + throw DataXException.asDataXException(UnstructuredStorageReaderErrorCode.NOT_SUPPORT_TYPE, errorMessage); + } + + hivePartitionColumns.add(generateColumn); + } + + return hivePartitionColumns; + } + } diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/binaryFileUtil/BinaryFileReaderUtil.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/binaryFileUtil/BinaryFileReaderUtil.java new file mode 100644 index 0000000000..a7d846b324 --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/binaryFileUtil/BinaryFileReaderUtil.java @@ -0,0 +1,62 @@ +package com.alibaba.datax.plugin.unstructuredstorage.reader.binaryFileUtil; + +import com.alibaba.datax.common.element.BytesColumn; +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.plugin.unstructuredstorage.reader.Key; +import com.alibaba.datax.plugin.unstructuredstorage.reader.UnstructuredStorageReaderErrorCode; +import com.alibaba.datax.plugin.unstructuredstorage.reader.UnstructuredStorageReaderUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * @Author: guxuan + * @Date 2022-05-17 15:59 + */ +public class BinaryFileReaderUtil { + private static final Logger LOG = LoggerFactory.getLogger(BinaryFileReaderUtil.class); + + public static void readFromStream(InputStream inputStream, String filePath, RecordSender recordSender, int blockSizeInByte) { + try { + Map meta = UnstructuredStorageReaderUtil.buildRecordMeta(filePath); + byte[] tmp = new byte[blockSizeInByte]; + int len; + ByteUtils byteUtils = new ByteUtils(); + while ((len = inputStream.read(tmp)) != -1) { + /**如果len小于blockSizeInByte,说明已经读到了最后一个byte数组 + * 此时需要将byte数组长度调整为实际读到的字节数, + * 否则会导致写入目的文件字节数大于实际文件字节数, 有可能会导致文件损坏(比如pptx, docx等文件) + */ + // warn: 这里可以优化掉,没必要做一次数组拷贝,直接复用byte[] tmp即可 + byte[] readBytesArray = Arrays.copyOf(tmp, len); + byteUtils.append(readBytesArray); + if (byteUtils.getSize() >= blockSizeInByte) { + recordSenderBytesColumn(recordSender, byteUtils.getBuffer(), meta); + byteUtils.clear(); + } + } + recordSenderBytesColumn(recordSender, byteUtils.getBuffer(), meta); + LOG.info("End read!!!"); + } catch (IOException e) { + throw DataXException.asDataXException(UnstructuredStorageReaderErrorCode.READ_FILE_IO_ERROR, e); + } + } + + private static void recordSenderBytesColumn(RecordSender recordSender, byte[] tmp, Map meta){ + Record record = recordSender.createRecord(); + Column column = new BytesColumn(tmp); + record.addColumn(column); + record.setMeta(meta); + recordSender.sendToWriter(record); + } + + +} diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/binaryFileUtil/ByteUtils.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/binaryFileUtil/ByteUtils.java new file mode 100644 index 0000000000..14ba9c47ea --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/binaryFileUtil/ByteUtils.java @@ -0,0 +1,48 @@ +package com.alibaba.datax.plugin.unstructuredstorage.reader.binaryFileUtil; + +import java.util.Arrays; + +/** + * @Author: guxuan + * @Date 2022-05-17 16:00 + */ +public class ByteUtils { + + private int size; + private int kDefaultBufferSize = 0; + private byte[] buffer; + + public byte[] getBuffer() { + return buffer; + } + + public ByteUtils() { + buffer = new byte[0]; + size = 0; + } + + public long getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + public ByteUtils append(byte[] buf) { + + if (buf == null){ + return this; + } + buffer = Arrays.copyOf(buffer, buffer.length + buf.length); + System.arraycopy(buf, 0, buffer, size, buf.length); + size += buf.length; + return this; + } + + public void clear() + { + buffer = new byte[kDefaultBufferSize]; + size = 0; + } +} diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/split/StartEndPair.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/split/StartEndPair.java new file mode 100644 index 0000000000..aa021c9969 --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/split/StartEndPair.java @@ -0,0 +1,49 @@ +package com.alibaba.datax.plugin.unstructuredstorage.reader.split; + +/** + * @Author: guxuan + * @Date 2022-05-17 15:50 + */ +public class StartEndPair { + private Long start; + private Long end; + private String filePath; + + public StartEndPair() { + } + + public StartEndPair(Long start, Long end, String filePath) { + this.start = start; + this.end = end; + this.filePath = filePath; + } + + public Long getEnd() { + return end; + } + + public void setEnd(Long end) { + this.end = end; + } + + public Long getStart() { + return start; + } + + public void setStart(Long start) { + this.start = start; + } + + public String getFilePath() { + return filePath; + } + + public void setFilePath(String filePath) { + this.filePath = filePath; + } + + @Override + public String toString() { + return "StartEndPair [start=" + start + ", end=" + end + ", filePath=" + filePath + "]"; + } +} diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/split/UnstructuredSplitUtil.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/split/UnstructuredSplitUtil.java new file mode 100644 index 0000000000..4e42583dfe --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/reader/split/UnstructuredSplitUtil.java @@ -0,0 +1,191 @@ +package com.alibaba.datax.plugin.unstructuredstorage.reader.split; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.RangeSplitUtil; +import com.alibaba.datax.plugin.unstructuredstorage.reader.Key; +import com.alibaba.datax.plugin.unstructuredstorage.reader.UnstructuredStorageReaderErrorCode; +import com.alibaba.fastjson2.JSON; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.tuple.ImmutableTriple; +import org.apache.commons.lang3.tuple.Triple; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** + * @Author: guxuan + * @Date 2022-05-17 15:49 + */ +public abstract class UnstructuredSplitUtil { + private static final Logger LOG = LoggerFactory.getLogger(UnstructuredSplitUtil.class); + private boolean needInnerSplit; + // 对每个文件进行切分的块大小是 64MB + // warn: 这个最好弄成可配置的, 用户配置channel为2但是有10个文件,不一定需要文件内部切分; + // 弄成可配置的有些情况下可以避免文件内部切分切分的task太碎 + private static final Long BLOCK_BYTE_CAPACITY = 64 * FileUtils.ONE_MB; + + public UnstructuredSplitUtil(boolean needInnerSplit) { + this.needInnerSplit = needInnerSplit; + } + + public List getSplitConfiguration(Configuration originConfiguration, List sourceObjectList, + int adviceNumber) { + + List splitConfiguration = new ArrayList(); + List regulateSplitStartEndPairList = new ArrayList(); + + for (String object : sourceObjectList) { + boolean realNeedInnerSplit = false; + Long contentTotalLength = -1L; + if (this.needInnerSplit) { + // 减少不必要的oss接口调用 + contentTotalLength = this.getFileTotalLength(object); + if (isNeedSplit(contentTotalLength)) { + realNeedInnerSplit = true; + } + } + // warn: 数据读模式允许文件内部切分,并且文件大小满足 + if (realNeedInnerSplit) { + List startEndPairList = getSplitStartEndPairList(contentTotalLength, object); + List> startEndInputStreamTripleList = new ArrayList>(); + for (int i = 0; i < startEndPairList.size(); i++) { + StartEndPair startEndPair = startEndPairList.get(i); + InputStream inputStream = this.getFileInputStream(startEndPair); + Triple startEndInputStreamTriple = new ImmutableTriple( + startEndPair.getStart(), startEndPair.getEnd(), inputStream); + startEndInputStreamTripleList.add(startEndInputStreamTriple); + } + regulateSplitStartEndPairList.addAll(regulateSplitStartEndPair(startEndInputStreamTripleList, object)); + } else { + // 如果指定的Range无效(比如开始位置、结束位置为负数,大于文件大小),则会下载整个文件; + StartEndPair startEndPair = new StartEndPair(0L, -1L, object); + regulateSplitStartEndPairList.add(startEndPair); + } + } + + // merge task 将多个文件merge到一个task中执行 + List> splitResult = RangeSplitUtil.doListSplit(regulateSplitStartEndPairList, adviceNumber); + // at here this.objects is not null and not empty + for (List eachSlice : splitResult) { + Configuration splitedConfig = originConfiguration.clone(); + splitedConfig.set(Key.SPLIT_SLICE_CONFIG, eachSlice); + splitConfiguration.add(splitedConfig); + LOG.info(String.format("File to be read:%s", JSON.toJSONString(eachSlice))); + } + return splitConfiguration; + } + + /** + * 对原始的切分点位进行调节校准, 将点位落在每一行数据的换行符处 + * + * @param startEndInputStreamTripleList + * 原始的切分点位及inputstream (start, end, inputStream) + * @return + */ + private List regulateSplitStartEndPair( + List> startEndInputStreamTripleList, String filePath) { + List regulatedStartEndPairList = new ArrayList(); + + for (int i = 0; i < startEndInputStreamTripleList.size(); i++) { + if (i == 0) { + Triple firstBlock = startEndInputStreamTripleList.get(i); + StartEndPair startEndPair = new StartEndPair(firstBlock.getLeft(), null, filePath); + regulatedStartEndPairList.add(startEndPair); + continue; + } + Triple block = startEndInputStreamTripleList.get(i); + long start = block.getLeft(); + long offset = 0; + // 对切分点位进行调节,将切分起始点移动到行尾(即'\n'上) + if (i < startEndInputStreamTripleList.size()) { + offset = getLFIndex(block.getRight()); + } + // 调节正确的切分点位 + long regulatedPoint = start + offset; + // 将上一个block的末尾点位调节成行尾 + regulatedStartEndPairList.get(i - 1).setEnd(regulatedPoint); + if (i < startEndInputStreamTripleList.size() - 1) { + // 将本block起始点位进行调节, 结束点位暂不调节 + regulatedStartEndPairList.add(new StartEndPair(regulatedPoint + 1, null, filePath)); + } else { + // 调节最后一个block, 调节起始点位, 结束点位就用文件的字节总长度 + regulatedStartEndPairList.add(new StartEndPair(regulatedPoint + 1, block.getMiddle(), filePath)); + } + } + return regulatedStartEndPairList; + } + + /** + * 获取到输入流开始的第一个'\n'偏移量, 如果向后偏移了ByteCapacity个字节,还是没有找到'\n'的话,则抛出异常 注: + * 对文件切分的最后一个分块不会调用该方法 + * + * @param inputStream + * 输入流 + * @return + */ + private Long getLFIndex(InputStream inputStream) { + Long hasReadByteIndex = -1L; + int ch = 0; + while (ch != -1) { + try { + ch = inputStream.read(); + } catch (IOException e) { + throw DataXException.asDataXException(UnstructuredStorageReaderErrorCode.READ_FILE_IO_ERROR, + String.format("inputstream read Byte has exception: %s", e.getMessage()), e); + } + hasReadByteIndex++; + if (ch == '\n') { + return hasReadByteIndex; + } + } + return hasReadByteIndex; + } + + /** + * 得到一个文件最多能拆分成的份数 + * + * @param fileTotalLength + * @return + */ + private List getSplitStartEndPairList(Long fileTotalLength, String filePath) { + long splitNum = (long) Math.ceil(fileTotalLength * 1.0 / BLOCK_BYTE_CAPACITY); + List startEndPairList = new ArrayList(); + long start, end; + for (int i = 1; i <= splitNum; i++) { + if (i == 1) { + start = (i - 1) * BLOCK_BYTE_CAPACITY; + end = i * BLOCK_BYTE_CAPACITY; + } else if (i < splitNum) { + start = (i - 1) * BLOCK_BYTE_CAPACITY + 1; + end = i * BLOCK_BYTE_CAPACITY; + } else { + start = (i - 1) * BLOCK_BYTE_CAPACITY + 1; + end = fileTotalLength - 1; + } + StartEndPair startEndPair = new StartEndPair(start, end, filePath); + startEndPairList.add(startEndPair); + } + return startEndPairList; + } + + /** + * 判断文件是否需要切分, 切分的条件是必须要大于 transport.channel.byteCapacity + * + * @param fileTotalLength: + * 文件总字节数 + * @return + */ + private boolean isNeedSplit(Long fileTotalLength) { + boolean fileSizeCouldSplit = fileTotalLength > BLOCK_BYTE_CAPACITY ? true : false; + return fileSizeCouldSplit && this.needInnerSplit; + } + + public abstract Long getFileTotalLength(String filePath); + + public abstract InputStream getFileInputStream(StartEndPair startEndPair); +} diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/util/ColumnTypeUtil.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/util/ColumnTypeUtil.java new file mode 100644 index 0000000000..a03bf07e58 --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/util/ColumnTypeUtil.java @@ -0,0 +1,90 @@ +package com.alibaba.datax.plugin.unstructuredstorage.util; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.unstructuredstorage.reader.ColumnEntry; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * @Author: guxuan + * @Date 2022-05-17 16:40 + */ +public class ColumnTypeUtil { + + private static final String TYPE_NAME = "decimal"; + private static final String LEFT_BRACKETS = "("; + private static final String RIGHT_BRACKETS = ")"; + private static final String DELIM = ","; + + public static boolean isDecimalType(String typeName){ + return typeName.toLowerCase().startsWith(TYPE_NAME); + } + + public static DecimalInfo getDecimalInfo(String typeName, DecimalInfo defaultInfo){ + if(!isDecimalType(typeName)){ + throw new IllegalArgumentException("Unsupported column type:" + typeName); + } + + if (typeName.contains(LEFT_BRACKETS) && typeName.contains(RIGHT_BRACKETS)){ + int precision = Integer.parseInt(typeName.substring(typeName.indexOf(LEFT_BRACKETS) + 1,typeName.indexOf(DELIM)).trim()); + int scale = Integer.parseInt(typeName.substring(typeName.indexOf(DELIM) + 1,typeName.indexOf(RIGHT_BRACKETS)).trim()); + return new DecimalInfo(precision, scale); + } else { + return defaultInfo; + } + } + + public static class DecimalInfo { + private int precision; + private int scale; + + public DecimalInfo(int precision, int scale) { + this.precision = precision; + this.scale = scale; + } + + public int getPrecision() { + return precision; + } + + public int getScale() { + return scale; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()){ + return false; + + } + DecimalInfo that = (DecimalInfo) o; + return precision == that.precision && scale == that.scale; + } + + @Override + public int hashCode() { + return Objects.hash(precision, scale); + } + } + + public static List getListColumnEntry( + Configuration configuration, final String path) { + List lists = configuration.getList(path, JSONObject.class); + if (lists == null) { + return null; + } + List result = new ArrayList<>(); + for (final JSONObject object : lists) { + result.add(JSON.parseObject(object.toJSONString(), ColumnEntry.class)); + } + return result; + } +} diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/util/HdfsUtil.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/util/HdfsUtil.java new file mode 100644 index 0000000000..4098ff1d8c --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/util/HdfsUtil.java @@ -0,0 +1,16 @@ +package com.alibaba.datax.plugin.unstructuredstorage.util; + + +public class HdfsUtil { + private static final double SCALE_TWO = 2.0; + private static final double SCALE_TEN = 10.0; + private static final int BIT_SIZE = 8; + public static int computeMinBytesForPrecision(int precision){ + + int numBytes = 1; + while (Math.pow(SCALE_TWO, BIT_SIZE * numBytes - 1.0) < Math.pow(SCALE_TEN, precision)) { + numBytes += 1; + } + return numBytes; + } +} diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/Constant.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/Constant.java index 93b4baa978..a485c1249b 100755 --- a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/Constant.java +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/Constant.java @@ -11,9 +11,35 @@ public class Constant { public static final String FILE_FORMAT_CSV = "csv"; public static final String FILE_FORMAT_TEXT = "text"; - - //每个分块10MB,最大10000个分块 - public static final Long MAX_FILE_SIZE = 1024 * 1024 * 10 * 10000L; - + + public static final String FILE_FORMAT_SQL = "sql"; + + //每个分块10MB,最大10000个分块, MAX_FILE_SIZE 单位: MB + public static final Long MAX_FILE_SIZE = 10 * 10000L; + + public static final int DEFAULT_COMMIT_SIZE = 2000; + public static final String DEFAULT_SUFFIX = ""; + + public static final String TRUNCATE = "truncate"; + public static final String APPEND = "append"; + public static final String NOCONFLICT = "nonConflict"; + + /** + * 在同步音视频等二进制文件的情况下: + * 半结构化写插件可以统一使用 SOURCE_FILE 获取到读端插件的split file路径 + */ + public static final String SOURCE_FILE = "sourceFile"; + + public static final String SOURCE_FILE_NAME = "sourceFileName"; + + /** + * 是否是音视频等无结构化文件 + */ + public static final String BINARY = "binary"; + + /** + * 文件同步模式, 如果是copy表示纯文件拷贝 + * */ + public static final String SYNC_MODE_VALUE_COPY = "copy"; } diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/DataXCsvWriter.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/DataXCsvWriter.java new file mode 100644 index 0000000000..85deea2cd9 --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/DataXCsvWriter.java @@ -0,0 +1,296 @@ +package com.alibaba.datax.plugin.unstructuredstorage.writer; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.Writer; + +/** + * @Author: guxuan + * @Date 2022-05-19 10:44 + */ +public class DataXCsvWriter { + private Writer writer; + @SuppressWarnings("unused") + private String fileName; + private boolean firstColumn; + private boolean useCustomRecordDelimiter; + private UserSettings userSettings; + private boolean initialized; + private boolean closed; + public static final int ESCAPE_MODE_DOUBLED = 1; + public static final int ESCAPE_MODE_BACKSLASH = 2; + + public DataXCsvWriter(Writer writer, char delimiter) { + this.writer = null; + this.fileName = null; + this.firstColumn = true; + this.useCustomRecordDelimiter = false; + this.userSettings = new UserSettings(); + this.initialized = false; + this.closed = false; + if(writer == null) { + throw new IllegalArgumentException("Parameter writer can not be null."); + } else { + this.writer = writer; + this.userSettings.Delimiter = delimiter; + this.initialized = true; + } + } + + public char getDelimiter() { + return this.userSettings.Delimiter; + } + + public void setDelimiter(char var1) { + this.userSettings.Delimiter = var1; + } + + public char getRecordDelimiter() { + return this.userSettings.RecordDelimiter; + } + + public void setRecordDelimiter(char var1) { + this.useCustomRecordDelimiter = true; + this.userSettings.RecordDelimiter = var1; + } + + public char getTextQualifier() { + return this.userSettings.TextQualifier; + } + + public void setTextQualifier(char var1) { + this.userSettings.TextQualifier = var1; + } + + public boolean getUseTextQualifier() { + return this.userSettings.UseTextQualifier; + } + + public void setUseTextQualifier(boolean var1) { + this.userSettings.UseTextQualifier = var1; + } + + public int getEscapeMode() { + return this.userSettings.EscapeMode; + } + + public void setEscapeMode(int var1) { + this.userSettings.EscapeMode = var1; + } + + public void setComment(char var1) { + this.userSettings.Comment = var1; + } + + public char getComment() { + return this.userSettings.Comment; + } + + public boolean getForceQualifier() { + return this.userSettings.ForceQualifier; + } + + public void setForceQualifier(boolean var1) { + this.userSettings.ForceQualifier = var1; + } + + public void write(String var1, boolean var2) throws IOException { + this.checkClosed(); + if(var1 == null) { + var1 = ""; + } + + if(!this.firstColumn) { + this.writer.write(this.userSettings.Delimiter); + } + + boolean var3 = this.userSettings.ForceQualifier; + if(!var2 && var1.length() > 0) { + var1 = var1.trim(); + } + + if(!var3 && this.userSettings.UseTextQualifier && (var1.indexOf(this.userSettings.TextQualifier) > -1 || var1.indexOf(this.userSettings.Delimiter) > -1 || !this.useCustomRecordDelimiter && (var1.indexOf(10) > -1 || var1.indexOf(13) > -1) || this.useCustomRecordDelimiter && var1.indexOf(this.userSettings.RecordDelimiter) > -1 || this.firstColumn && var1.length() > 0 && var1.charAt(0) == this.userSettings.Comment || this.firstColumn && var1.length() == 0)) { + var3 = true; + } + + if(this.userSettings.UseTextQualifier && !var3 && var1.length() > 0 && var2) { + char var4 = var1.charAt(0); + if(var4 == 32 || var4 == 9) { + var3 = true; + } + + if(!var3 && var1.length() > 1) { + char var5 = var1.charAt(var1.length() - 1); + if(var5 == 32 || var5 == 9) { + var3 = true; + } + } + } + + if(var3) { + this.writer.write(this.userSettings.TextQualifier); + if(this.userSettings.EscapeMode == 2) { + var1 = replace(var1, "\\", "\\\\"); + var1 = replace(var1, "" + this.userSettings.TextQualifier, "\\" + this.userSettings.TextQualifier); + } else { + var1 = replace(var1, "" + this.userSettings.TextQualifier, "" + this.userSettings.TextQualifier + this.userSettings.TextQualifier); + } + } else if(this.userSettings.EscapeMode == 2) { + var1 = replace(var1, "\\", "\\\\"); + var1 = replace(var1, "" + this.userSettings.Delimiter, "\\" + this.userSettings.Delimiter); + if(this.useCustomRecordDelimiter) { + var1 = replace(var1, "" + this.userSettings.RecordDelimiter, "\\" + this.userSettings.RecordDelimiter); + } else { + var1 = replace(var1, "\r", "\\\r"); + var1 = replace(var1, "\n", "\\\n"); + } + + if(this.firstColumn && var1.length() > 0 && var1.charAt(0) == this.userSettings.Comment) { + if(var1.length() > 1) { + var1 = "\\" + this.userSettings.Comment + var1.substring(1); + } else { + var1 = "\\" + this.userSettings.Comment; + } + } + } + + this.writer.write(var1); + if(var3) { + this.writer.write(this.userSettings.TextQualifier); + } + + this.firstColumn = false; + } + + public void write(String var1) throws IOException { + this.write(var1, false); + } + + public void writeComment(String var1) throws IOException { + this.checkClosed(); + this.writer.write(this.userSettings.Comment); + this.writer.write(var1); + if(this.useCustomRecordDelimiter) { + this.writer.write(this.userSettings.RecordDelimiter); + } else { + this.writer.write(IOUtils.LINE_SEPARATOR); + } + + this.firstColumn = true; + } + + public void writeRecord(String[] var1, boolean var2) throws IOException { + if(var1 != null && var1.length > 0) { + for(int var3 = 0; var3 < var1.length; ++var3) { + this.write(var1[var3], var2); + } + + this.endRecord(); + } + + } + + public void writeRecord(String[] var1) throws IOException { + this.writeRecord(var1, false); + } + + public void endRecord() throws IOException { + this.checkClosed(); + if(this.useCustomRecordDelimiter) { + this.writer.write(this.userSettings.RecordDelimiter); + } else { + this.writer.write(IOUtils.LINE_SEPARATOR); + } + + this.firstColumn = true; + } + + public void flush() throws IOException { + this.writer.flush(); + } + + public void close() { + if(!this.closed) { + this.close(true); + this.closed = true; + } + + } + + private void close(boolean var1) { + if(!this.closed) { + try { + if(this.initialized) { + this.writer.close(); + } + } catch (Exception var3) { + ; + } + + this.writer = null; + this.closed = true; + } + + } + + private void checkClosed() throws IOException { + if(this.closed) { + throw new IOException("This instance of the CsvWriter class has already been closed."); + } + } + + @Override + protected void finalize() { + this.close(false); + } + + public static String replace(String var0, String var1, String var2) { + int var3 = var1.length(); + int var4 = var0.indexOf(var1); + if(var4 <= -1) { + return var0; + } else { + StringBuffer var5 = new StringBuffer(); + + int var6; + for(var6 = 0; var4 != -1; var4 = var0.indexOf(var1, var6)) { + var5.append(var0.substring(var6, var4)); + var5.append(var2); + var6 = var4 + var3; + } + + var5.append(var0.substring(var6)); + return var5.toString(); + } + } + + private class UserSettings { + public char TextQualifier = 34; + public boolean UseTextQualifier = true; + public char Delimiter = 44; + public char RecordDelimiter = 0; + public char Comment = 35; + public int EscapeMode = 1; + public boolean ForceQualifier = false; + + public UserSettings() { + } + } + + @SuppressWarnings("unused") + private class Letters { + public static final char LF = '\n'; + public static final char CR = '\r'; + public static final char QUOTE = '\"'; + public static final char COMMA = ','; + public static final char SPACE = ' '; + public static final char TAB = '\t'; + public static final char POUND = '#'; + public static final char BACKSLASH = '\\'; + public static final char NULL = '\u0000'; + + private Letters() { + } + } +} diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/Key.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/Key.java index 2e7fe079f3..ee97abd86d 100755 --- a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/Key.java +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/Key.java @@ -1,15 +1,25 @@ package com.alibaba.datax.plugin.unstructuredstorage.writer; public class Key { + public static final String PATH = "path"; // must have public static final String FILE_NAME = "fileName"; + public static final String TABLE_NAME = "table"; + // must have public static final String WRITE_MODE = "writeMode"; // not must , not default , public static final String FIELD_DELIMITER = "fieldDelimiter"; + public static final String QUOTE_CHARACTER = "quoteChar"; + + // not must , default os's line delimiter + public static final String LINE_DELIMITER = "lineDelimiter"; + + public static final String CSV_WRITER_CONFIG = "csvWriterConfig"; + // not must, default UTF-8 public static final String ENCODING = "encoding"; @@ -32,7 +42,37 @@ public class Key { // writer maxFileSize public static final String MAX_FILE_SIZE = "maxFileSize"; + + public static final String COMMIT_SIZE = "commitSize"; // writer file type suffix, like .txt .csv public static final String SUFFIX = "suffix"; + + public static final String MARK_DONE_FILE_NAME = "markDoneFileName"; + + public static final String MARK_DOING_FILE_NAME = "markDoingFileName"; + + // public static final String RETRY_TIME = "retryTime"; + + public final static String MAX_RETRY_TIME = "maxRetryTime"; + + /** + * 半结构化标示一个Record来源的绝对文件路径名,可以是ftp文件,oss的object等 + * */ + public static final String META_KEY_FILE_PATH = "filePath"; + + /** + * 多文件切分的工作项,Task通过此配置项表示工作内容, 文件内部切分相关key + */ + public static final String SPLIT_SLICE_CONFIG = "__splitSliceConfig"; + public static final String SPLIT_SLICE_FILE_PATH = "filePath"; + public static final String SPLIT_SLICE_START_POINT = "startPoint"; + public static final String SPLIT_SLICE_END_POINT = "endPoint"; + + /** + * 文件同步模式, 如果是copy表示纯文件拷贝 + * */ + public static final String SYNC_MODE = "syncMode"; + + public static final String BYTE_ENCODING = "byteEncoding"; } diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/SqlWriter.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/SqlWriter.java new file mode 100644 index 0000000000..18a9c1be6f --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/SqlWriter.java @@ -0,0 +1,76 @@ +package com.alibaba.datax.plugin.unstructuredstorage.writer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.Writer; +import java.util.List; +import java.util.stream.Collectors; + +public class SqlWriter implements UnstructuredWriter { + private static final Logger LOG = LoggerFactory.getLogger(SqlWriter.class); + + private Writer sqlWriter; + private String quoteChar; + private String lineSeparator; + private String tableName; + private String nullFormat; + private StringBuilder insertPrefix; + + public SqlWriter(Writer writer, String quoteChar, String tableName, String lineSeparator, List columnNames, String nullFormat) { + this.sqlWriter = writer; + this.quoteChar = quoteChar; + this.lineSeparator = lineSeparator; + this.tableName = quoteChar + tableName + quoteChar; + this.nullFormat = nullFormat; + buildInsertPrefix(columnNames); + } + + @Override + public void writeOneRecord(List splitedRows) throws IOException { + if (splitedRows.isEmpty()) { + LOG.info("Found one record line which is empty."); + return; + } + + StringBuilder sqlPatten = new StringBuilder(4096).append(insertPrefix); + sqlPatten.append(splitedRows.stream().map(e -> { + if (nullFormat.equals(e)) { + return "NULL"; + } + return "'" + DataXCsvWriter.replace(e, "'", "''") + "'"; + }).collect(Collectors.joining(","))); + sqlPatten.append(");").append(lineSeparator); + this.sqlWriter.write(sqlPatten.toString()); + } + + private void buildInsertPrefix(List columnNames) { + StringBuilder sb = new StringBuilder(columnNames.size() * 32); + + for (String columnName : columnNames) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(quoteChar).append(columnName).append(quoteChar); + } + + int capacity = 16 + tableName.length() + sb.length(); + this.insertPrefix = new StringBuilder(capacity); + this.insertPrefix.append("INSERT INTO ").append(tableName).append(" (").append(sb).append(")").append(" VALUES("); + } + + public void appendCommit() throws IOException { + this.sqlWriter.write("commit;" + lineSeparator); + } + + @Override + public void flush() throws IOException { + this.sqlWriter.flush(); + } + + @Override + public void close() throws IOException { + this.sqlWriter.close(); + } +} diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/TextCsvWriterManager.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/TextCsvWriterManager.java index 1ea8275963..4a9b919774 100644 --- a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/TextCsvWriterManager.java +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/TextCsvWriterManager.java @@ -2,8 +2,13 @@ import java.io.IOException; import java.io.Writer; +import java.util.HashMap; import java.util.List; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.TypeReference; +import org.apache.commons.beanutils.BeanUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -11,15 +16,15 @@ import com.csvreader.CsvWriter; + public class TextCsvWriterManager { - public static UnstructuredWriter produceUnstructuredWriter( - String fileFormat, char fieldDelimiter, Writer writer) { - // warn: false means plain text(old way), true means strict csv format - if (Constant.FILE_FORMAT_TEXT.equals(fileFormat)) { - return new TextWriterImpl(writer, fieldDelimiter); - } else { - return new CsvWriterImpl(writer, fieldDelimiter); - } + + public static UnstructuredWriter produceTextWriter( Writer writer, String fieldDelimiter, Configuration config) { + return new TextWriterImpl(writer, fieldDelimiter, config); + } + + public static UnstructuredWriter produceCsvWriter( Writer writer, char fieldDelimiter, Configuration config) { + return new CsvWriterImpl(writer, fieldDelimiter, config); } } @@ -28,15 +33,40 @@ class CsvWriterImpl implements UnstructuredWriter { .getLogger(CsvWriterImpl.class); // csv 严格符合csv语法, 有标准的转义等处理 private char fieldDelimiter; - private CsvWriter csvWriter; + private String lineDelimiter; + private DataXCsvWriter csvWriter; - public CsvWriterImpl(Writer writer, char fieldDelimiter) { + public CsvWriterImpl(Writer writer, char fieldDelimiter, Configuration config) { this.fieldDelimiter = fieldDelimiter; - this.csvWriter = new CsvWriter(writer, this.fieldDelimiter); + this.lineDelimiter = config.getString(Key.LINE_DELIMITER, IOUtils.LINE_SEPARATOR); + this.csvWriter = new DataXCsvWriter(writer, this.fieldDelimiter); this.csvWriter.setTextQualifier('"'); this.csvWriter.setUseTextQualifier(true); // warn: in linux is \n , in windows is \r\n - this.csvWriter.setRecordDelimiter(IOUtils.LINE_SEPARATOR.charAt(0)); + this.csvWriter.setRecordDelimiter(this.lineDelimiter.charAt(0)); + + String csvWriterConfig = config.getString(Key.CSV_WRITER_CONFIG); + if (StringUtils.isNotBlank(csvWriterConfig)) { + try { + HashMap csvWriterConfigMap = JSON.parseObject(csvWriterConfig, + new TypeReference>() { + }); + if (!csvWriterConfigMap.isEmpty()) { + // this.csvWriter.setComment(var1); + // this.csvWriter.setDelimiter(var1); + // this.csvWriter.setEscapeMode(var1); + // this.csvWriter.setForceQualifier(var1); + // this.csvWriter.setRecordDelimiter(var1); + // this.csvWriter.setTextQualifier(var1); + // this.csvWriter.setUseTextQualifier(var1); + BeanUtils.populate(this.csvWriter, csvWriterConfigMap); + LOG.info(String.format("csvwriterConfig is set successfully. After setting, csvwriter:%s", JSON.toJSONString(this.csvWriter))); + } + } catch (Exception e) { + LOG.warn(String.format("invalid csvWriterConfig config: %s, DataX will ignore it.", csvWriterConfig), + e); + } + } } @Override @@ -44,8 +74,7 @@ public void writeOneRecord(List splitedRows) throws IOException { if (splitedRows.isEmpty()) { LOG.info("Found one record line which is empty."); } - this.csvWriter.writeRecord((String[]) splitedRows - .toArray(new String[0])); + this.csvWriter.writeRecord(splitedRows.toArray(new String[0])); } @Override @@ -64,12 +93,14 @@ class TextWriterImpl implements UnstructuredWriter { private static final Logger LOG = LoggerFactory .getLogger(TextWriterImpl.class); // text StringUtils的join方式, 简单的字符串拼接 - private char fieldDelimiter; + private String fieldDelimiter; private Writer textWriter; + private String lineDelimiter; - public TextWriterImpl(Writer writer, char fieldDelimiter) { + public TextWriterImpl(Writer writer, String fieldDelimiter, Configuration config) { this.fieldDelimiter = fieldDelimiter; this.textWriter = writer; + this.lineDelimiter = config.getString(Key.LINE_DELIMITER, IOUtils.LINE_SEPARATOR); } @Override @@ -79,7 +110,7 @@ public void writeOneRecord(List splitedRows) throws IOException { } this.textWriter.write(String.format("%s%s", StringUtils.join(splitedRows, this.fieldDelimiter), - IOUtils.LINE_SEPARATOR)); + this.lineDelimiter)); } @Override diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/UnstructuredStorageWriterErrorCode.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/UnstructuredStorageWriterErrorCode.java index 0f780ebdd1..b83cfa1c74 100755 --- a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/UnstructuredStorageWriterErrorCode.java +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/UnstructuredStorageWriterErrorCode.java @@ -8,7 +8,8 @@ public enum UnstructuredStorageWriterErrorCode implements ErrorCode { Write_FILE_WITH_CHARSET_ERROR("UnstructuredStorageWriter-01", "您配置的编码未能正常写入."), Write_FILE_IO_ERROR("UnstructuredStorageWriter-02", "您配置的文件在写入时出现IO异常."), RUNTIME_EXCEPTION("UnstructuredStorageWriter-03", "出现运行时异常, 请联系我们"), - REQUIRED_VALUE("UnstructuredStorageWriter-04", "您缺失了必须填写的参数值."),; + REQUIRED_VALUE("UnstructuredStorageWriter-04", "您缺失了必须填写的参数值."), + Write_ERROR("UnstructuredStorageWriter-05", "errorcode.write_error"),; private final String code; private final String description; diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/UnstructuredStorageWriterUtil.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/UnstructuredStorageWriterUtil.java index b1927ce79b..e74e5698ed 100755 --- a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/UnstructuredStorageWriterUtil.java +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/UnstructuredStorageWriterUtil.java @@ -1,10 +1,6 @@ package com.alibaba.datax.plugin.unstructuredstorage.writer; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; +import java.io.*; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -13,6 +9,11 @@ import java.util.Set; import java.util.UUID; +import com.alibaba.datax.common.element.BytesColumn; + +import com.google.common.base.Preconditions; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.compress.compressors.CompressorOutputStream; import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; @@ -53,10 +54,7 @@ public static void validateParameter(Configuration writerConfiguration) { if (!supportedWriteModes.contains(writeMode)) { throw DataXException .asDataXException( - UnstructuredStorageWriterErrorCode.ILLEGAL_VALUE, - String.format( - "仅支持 truncate, append, nonConflict 三种模式, 不支持您配置的 writeMode 模式 : [%s]", - writeMode)); + UnstructuredStorageWriterErrorCode.ILLEGAL_VALUE, writeMode); } writerConfiguration.set(Key.WRITE_MODE, writeMode); @@ -64,8 +62,6 @@ public static void validateParameter(Configuration writerConfiguration) { String encoding = writerConfiguration.getString(Key.ENCODING); if (StringUtils.isBlank(encoding)) { // like " ", null - LOG.warn(String.format("您的encoding配置为空, 将使用默认值[%s]", - Constant.DEFAULT_ENCODING)); writerConfiguration.set(Key.ENCODING, Constant.DEFAULT_ENCODING); } else { try { @@ -74,8 +70,7 @@ public static void validateParameter(Configuration writerConfiguration) { Charsets.toCharset(encoding); } catch (Exception e) { throw DataXException.asDataXException( - UnstructuredStorageWriterErrorCode.ILLEGAL_VALUE, - String.format("不支持您配置的编码格式:[%s]", encoding), e); + UnstructuredStorageWriterErrorCode.ILLEGAL_VALUE, e); } } @@ -86,45 +81,41 @@ public static void validateParameter(Configuration writerConfiguration) { } else { Set supportedCompress = Sets.newHashSet("gzip", "bzip2"); if (!supportedCompress.contains(compress.toLowerCase().trim())) { - String message = String.format( - "仅支持 [%s] 文件压缩格式 , 不支持您配置的文件压缩格式: [%s]", - StringUtils.join(supportedCompress, ","), compress); throw DataXException.asDataXException( - UnstructuredStorageWriterErrorCode.ILLEGAL_VALUE, - String.format(message, compress)); + UnstructuredStorageWriterErrorCode.ILLEGAL_VALUE, String.format("unsupported commpress format %s ", compress)); } } + // fileFormat check + String fileFormat = writerConfiguration.getString(Key.FILE_FORMAT); + if (StringUtils.isBlank(fileFormat)) { + fileFormat = Constant.FILE_FORMAT_TEXT; + writerConfiguration.set(Key.FILE_FORMAT, fileFormat); + } + if (!Constant.FILE_FORMAT_CSV.equals(fileFormat) + && !Constant.FILE_FORMAT_TEXT.equals(fileFormat) + && !Constant.FILE_FORMAT_SQL.equals(fileFormat)) { + throw DataXException.asDataXException( + UnstructuredStorageWriterErrorCode.ILLEGAL_VALUE, String.format("unsupported fileFormat %s ", fileFormat)); + } + // fieldDelimiter check - String delimiterInStr = writerConfiguration - .getString(Key.FIELD_DELIMITER); - // warn: if have, length must be one - if (null != delimiterInStr && 1 != delimiterInStr.length()) { + String delimiterInStr = writerConfiguration.getString(Key.FIELD_DELIMITER); + + if (StringUtils.equalsIgnoreCase(fileFormat, Constant.FILE_FORMAT_CSV) && + null != delimiterInStr && 1 != delimiterInStr.length()) { throw DataXException.asDataXException( UnstructuredStorageWriterErrorCode.ILLEGAL_VALUE, - String.format("仅仅支持单字符切分, 您配置的切分为 : [%s]", delimiterInStr)); + String.format("unsupported delimiterInStr %s ", delimiterInStr)); } if (null == delimiterInStr) { - LOG.warn(String.format("您没有配置列分隔符, 使用默认值[%s]", - Constant.DEFAULT_FIELD_DELIMITER)); - writerConfiguration.set(Key.FIELD_DELIMITER, - Constant.DEFAULT_FIELD_DELIMITER); - } - - // fileFormat check - String fileFormat = writerConfiguration.getString(Key.FILE_FORMAT, - Constant.FILE_FORMAT_TEXT); - if (!Constant.FILE_FORMAT_CSV.equals(fileFormat) - && !Constant.FILE_FORMAT_TEXT.equals(fileFormat)) { - throw DataXException.asDataXException( - UnstructuredStorageWriterErrorCode.ILLEGAL_VALUE, String - .format("您配置的fileFormat [%s]错误, 支持csv, text两种.", - fileFormat)); + delimiterInStr = String.valueOf(Constant.DEFAULT_FIELD_DELIMITER); + writerConfiguration.set(Key.FIELD_DELIMITER, delimiterInStr); } } public static List split(Configuration writerSliceConfig, - Set originAllFileExists, int mandatoryNumber) { + Set originAllFileExists, int mandatoryNumber) { LOG.info("begin do split..."); Set allFileExists = new HashSet(); allFileExists.addAll(originAllFileExists); @@ -153,19 +144,19 @@ public static List split(Configuration writerSliceConfig, } public static String buildFilePath(String path, String fileName, - String suffix) { + String suffix) { boolean isEndWithSeparator = false; switch (IOUtils.DIR_SEPARATOR) { - case IOUtils.DIR_SEPARATOR_UNIX: - isEndWithSeparator = path.endsWith(String - .valueOf(IOUtils.DIR_SEPARATOR)); - break; - case IOUtils.DIR_SEPARATOR_WINDOWS: - isEndWithSeparator = path.endsWith(String - .valueOf(IOUtils.DIR_SEPARATOR_WINDOWS)); - break; - default: - break; + case IOUtils.DIR_SEPARATOR_UNIX: + isEndWithSeparator = path.endsWith(String + .valueOf(IOUtils.DIR_SEPARATOR)); + break; + case IOUtils.DIR_SEPARATOR_WINDOWS: + isEndWithSeparator = path.endsWith(String + .valueOf(IOUtils.DIR_SEPARATOR_WINDOWS)); + break; + default: + break; } if (!isEndWithSeparator) { path = path + IOUtils.DIR_SEPARATOR; @@ -179,14 +170,12 @@ public static String buildFilePath(String path, String fileName, } public static void writeToStream(RecordReceiver lineReceiver, - OutputStream outputStream, Configuration config, String context, - TaskPluginCollector taskPluginCollector) { + OutputStream outputStream, Configuration config, String context, + TaskPluginCollector taskPluginCollector) { String encoding = config.getString(Key.ENCODING, Constant.DEFAULT_ENCODING); // handle blank encoding if (StringUtils.isBlank(encoding)) { - LOG.warn(String.format("您配置的encoding为[%s], 使用默认值[%s]", encoding, - Constant.DEFAULT_ENCODING)); encoding = Constant.DEFAULT_ENCODING; } String compress = config.getString(Key.COMPRESS); @@ -212,10 +201,7 @@ public static void writeToStream(RecordReceiver lineReceiver, } else { throw DataXException .asDataXException( - UnstructuredStorageWriterErrorCode.ILLEGAL_VALUE, - String.format( - "仅支持 gzip, bzip2 文件压缩格式 , 不支持您配置的文件压缩格式: [%s]", - compress)); + UnstructuredStorageWriterErrorCode.ILLEGAL_VALUE, compress); } } UnstructuredStorageWriterUtil.doWriteToStream(lineReceiver, writer, @@ -223,24 +209,21 @@ public static void writeToStream(RecordReceiver lineReceiver, } catch (UnsupportedEncodingException uee) { throw DataXException .asDataXException( - UnstructuredStorageWriterErrorCode.Write_FILE_WITH_CHARSET_ERROR, - String.format("不支持的编码格式 : [%s]", encoding), uee); + UnstructuredStorageWriterErrorCode.Write_FILE_WITH_CHARSET_ERROR, uee); } catch (NullPointerException e) { throw DataXException.asDataXException( - UnstructuredStorageWriterErrorCode.RUNTIME_EXCEPTION, - "运行时错误, 请联系我们", e); + UnstructuredStorageWriterErrorCode.RUNTIME_EXCEPTION,e); } catch (IOException e) { throw DataXException.asDataXException( - UnstructuredStorageWriterErrorCode.Write_FILE_IO_ERROR, - String.format("流写入错误 : [%s]", context), e); + UnstructuredStorageWriterErrorCode.Write_FILE_IO_ERROR, e); } finally { IOUtils.closeQuietly(writer); } } private static void doWriteToStream(RecordReceiver lineReceiver, - BufferedWriter writer, String contex, Configuration config, - TaskPluginCollector taskPluginCollector) throws IOException { + BufferedWriter writer, String contex, Configuration config, + TaskPluginCollector taskPluginCollector) throws IOException { String nullFormat = config.getString(Key.NULL_FORMAT); @@ -252,49 +235,67 @@ private static void doWriteToStream(RecordReceiver lineReceiver, } // warn: default false - String fileFormat = config.getString(Key.FILE_FORMAT, - Constant.FILE_FORMAT_TEXT); - - String delimiterInStr = config.getString(Key.FIELD_DELIMITER); - if (null != delimiterInStr && 1 != delimiterInStr.length()) { - throw DataXException.asDataXException( - UnstructuredStorageWriterErrorCode.ILLEGAL_VALUE, - String.format("仅仅支持单字符切分, 您配置的切分为 : [%s]", delimiterInStr)); - } - if (null == delimiterInStr) { - LOG.warn(String.format("您没有配置列分隔符, 使用默认值[%s]", - Constant.DEFAULT_FIELD_DELIMITER)); - } - - // warn: fieldDelimiter could not be '' for no fieldDelimiter - char fieldDelimiter = config.getChar(Key.FIELD_DELIMITER, - Constant.DEFAULT_FIELD_DELIMITER); - - UnstructuredWriter unstructuredWriter = TextCsvWriterManager - .produceUnstructuredWriter(fileFormat, fieldDelimiter, writer); + String fileFormat = config.getString(Key.FILE_FORMAT, Constant.FILE_FORMAT_TEXT); + boolean isSqlFormat = Constant.FILE_FORMAT_SQL.equalsIgnoreCase(fileFormat); + int commitSize = config.getInt(Key.COMMIT_SIZE, Constant.DEFAULT_COMMIT_SIZE); + UnstructuredWriter unstructuredWriter = produceUnstructuredWriter(fileFormat, config, writer); List headers = config.getList(Key.HEADER, String.class); - if (null != headers && !headers.isEmpty()) { + if (null != headers && !headers.isEmpty() && !isSqlFormat) { unstructuredWriter.writeOneRecord(headers); } Record record = null; + int receivedCount = 0; + String byteEncoding = config.getString(Key.BYTE_ENCODING); while ((record = lineReceiver.getFromReader()) != null) { UnstructuredStorageWriterUtil.transportOneRecord(record, nullFormat, dateParse, taskPluginCollector, - unstructuredWriter); + unstructuredWriter, byteEncoding); + receivedCount++; + if (isSqlFormat && receivedCount % commitSize == 0) { + ((SqlWriter) unstructuredWriter).appendCommit(); + } } + if (isSqlFormat) { + ((SqlWriter)unstructuredWriter).appendCommit(); + } // warn:由调用方控制流的关闭 // IOUtils.closeQuietly(unstructuredWriter); } + public static UnstructuredWriter produceUnstructuredWriter(String fileFormat, Configuration config, Writer writer){ + UnstructuredWriter unstructuredWriter = null; + if (StringUtils.equalsIgnoreCase(fileFormat, Constant.FILE_FORMAT_CSV)) { + + Character fieldDelimiter = config.getChar(Key.FIELD_DELIMITER, Constant.DEFAULT_FIELD_DELIMITER); + unstructuredWriter = TextCsvWriterManager.produceCsvWriter(writer, fieldDelimiter, config); + } else if (StringUtils.equalsIgnoreCase(fileFormat, Constant.FILE_FORMAT_TEXT)) { + + String fieldDelimiter = config.getString(Key.FIELD_DELIMITER, String.valueOf(Constant.DEFAULT_FIELD_DELIMITER)); + unstructuredWriter = TextCsvWriterManager.produceTextWriter(writer, fieldDelimiter, config); + } else if (StringUtils.equalsIgnoreCase(fileFormat, Constant.FILE_FORMAT_SQL)) { + String tableName = config.getString(Key.TABLE_NAME); + Preconditions.checkArgument(StringUtils.isNotEmpty(tableName), "table name is empty"); + String quoteChar = config.getString(Key.QUOTE_CHARACTER); + Preconditions.checkArgument(StringUtils.isNotEmpty(quoteChar), "quote character is empty"); + String lineSeparator = config.getString(Key.LINE_DELIMITER, IOUtils.LINE_SEPARATOR); + List headers = config.getList(Key.HEADER, String.class); + Preconditions.checkArgument(CollectionUtils.isNotEmpty(headers), "column names are empty"); + String nullFormat = config.getString(Key.NULL_FORMAT, Constant.DEFAULT_NULL_FORMAT); + unstructuredWriter = new SqlWriter(writer, quoteChar, tableName, lineSeparator, headers, nullFormat); + } + + return unstructuredWriter; + } + /** * 异常表示脏数据 * */ public static void transportOneRecord(Record record, String nullFormat, - DateFormat dateParse, TaskPluginCollector taskPluginCollector, - UnstructuredWriter unstructuredWriter) { + DateFormat dateParse, TaskPluginCollector taskPluginCollector, + UnstructuredWriter unstructuredWriter, String byteEncoding) { // warn: default is null if (null == nullFormat) { nullFormat = "null"; @@ -309,7 +310,15 @@ public static void transportOneRecord(Record record, String nullFormat, if (null != column.getRawData()) { boolean isDateColumn = column instanceof DateColumn; if (!isDateColumn) { - splitedRows.add(column.asString()); + if (column instanceof BytesColumn) { + if ("base64".equalsIgnoreCase(byteEncoding)) { + splitedRows.add(Base64.encodeBase64String(column.asBytes())); + } else { + splitedRows.add(column.asString()); + } + } else { + splitedRows.add(column.asString()); + } } else { if (null != dateParse) { splitedRows.add(dateParse.format(column @@ -325,9 +334,18 @@ public static void transportOneRecord(Record record, String nullFormat, } } unstructuredWriter.writeOneRecord(splitedRows); - } catch (Exception e) { + } catch (IllegalArgumentException e){ // warn: dirty data taskPluginCollector.collectDirtyRecord(record, e); + } catch (DataXException e){ + // warn: dirty data + taskPluginCollector.collectDirtyRecord(record, e); + } catch (Exception e) { + // throw exception, it is not dirty data, + // may be network unreachable and the other problem + throw DataXException.asDataXException( + UnstructuredStorageWriterErrorCode.Write_ERROR, e.getMessage(),e); } } + } diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/binaryFileUtil/BinaryFileWriterErrorCode.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/binaryFileUtil/BinaryFileWriterErrorCode.java new file mode 100755 index 0000000000..77e51026cc --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/binaryFileUtil/BinaryFileWriterErrorCode.java @@ -0,0 +1,33 @@ +package com.alibaba.datax.plugin.unstructuredstorage.writer.binaryFileUtil; + +import com.alibaba.datax.common.spi.ErrorCode; + +public enum BinaryFileWriterErrorCode implements ErrorCode { + ILLEGAL_VALUE("UnstructuredStorageWriter-00", "errorcode.illegal_value"), + REPEATED_FILE_NAME("UnstructuredStorageWriter-01", "errorcode.repeated_file_name"), + REQUIRED_VALUE("UnstructuredStorageWriter-02","errorcode.required_value"),; + + private final String code; + private final String description; + + private BinaryFileWriterErrorCode(String code, String description) { + this.code = code; + this.description = description; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + return String.format("Code:[%s], Description:[%s].", this.code, + this.description); + } +} diff --git a/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/binaryFileUtil/BinaryFileWriterUtil.java b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/binaryFileUtil/BinaryFileWriterUtil.java new file mode 100644 index 0000000000..e2c9ad164f --- /dev/null +++ b/plugin-unstructured-storage-util/src/main/java/com/alibaba/datax/plugin/unstructuredstorage/writer/binaryFileUtil/BinaryFileWriterUtil.java @@ -0,0 +1,126 @@ +package com.alibaba.datax.plugin.unstructuredstorage.writer.binaryFileUtil; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.unstructuredstorage.reader.UnstructuredStorageReaderErrorCode; +import com.alibaba.datax.plugin.unstructuredstorage.writer.Key; +import com.alibaba.datax.plugin.unstructuredstorage.writer.UnstructuredStorageWriterErrorCode; +import com.google.common.collect.Sets; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.*; + +/** + * @Author: guxuan + * @Date 2022-05-17 17:01 + */ +public class BinaryFileWriterUtil { + + private static final Logger LOG = LoggerFactory.getLogger(BinaryFileWriterUtil.class); + + + /** + * 从RecordReceiver获取源文件Bytes数组, 写到目的端 + * + * @param outputStream: 写文件流 + * @param recordReceiver: RecordReceiver + */ + public static void writeFileFromRecordReceiver(OutputStream outputStream, RecordReceiver recordReceiver) { + try { + Record record; + while ((record = recordReceiver.getFromReader()) != null) { + Column column = record.getColumn(0); + outputStream.write(column.asBytes()); + } + outputStream.flush(); + LOG.info("End write!!!"); + } catch (IOException e) { + throw DataXException.asDataXException(UnstructuredStorageReaderErrorCode.READ_FILE_IO_ERROR, e); + } + } + + /** + * 校验同步二进制文件的参数 + * + * @param writerConfiguration: writer的配置 + */ + public static void validateParameter(Configuration writerConfiguration) { + // writeMode check + String writeMode = writerConfiguration.getNecessaryValue( + Key.WRITE_MODE, + UnstructuredStorageWriterErrorCode.REQUIRED_VALUE); + writeMode = writeMode.trim(); + Set supportedWriteModes = Sets.newHashSet(TRUNCATE, NOCONFLICT); + if (!supportedWriteModes.contains(writeMode)) { + throw DataXException + .asDataXException( + BinaryFileWriterErrorCode.ILLEGAL_VALUE, + String.format("Synchronous binary format file, only supports truncate and nonConflict modes, does not support the writeMode mode you configured: %s", writeMode)); + } + writerConfiguration.set(Key.WRITE_MODE, writeMode); + } + + /** + * 校验文件名是否有重复的,如果有重复的文件名则抛出异常 + * @param fileNameList + */ + public static void checkFileNameIfRepeatedThrowException(List fileNameList) { + Set sourceFileNameSet = new HashSet(); + for (String fileName : fileNameList) { + if (!sourceFileNameSet.contains(fileName)) { + sourceFileNameSet.add(fileName); + } else { + throw DataXException.asDataXException(BinaryFileWriterErrorCode.REPEATED_FILE_NAME, + String.format("Source File Name [%s] is repeated!", fileName)); + } + } + } + + /** + * + * @param readerSplitConfigs + * @param writerSliceConfig + * @return 切分后的结果 + */ + public static List split(List readerSplitConfigs, Configuration writerSliceConfig) { + List writerSplitConfigs = new ArrayList(); + + for (Configuration readerSliceConfig : readerSplitConfigs) { + Configuration splitedTaskConfig = writerSliceConfig.clone(); + String fileName = getFileName(readerSliceConfig.getString(SOURCE_FILE)); + splitedTaskConfig + .set(com.alibaba.datax.plugin.unstructuredstorage.writer.Key.FILE_NAME, fileName); + splitedTaskConfig. + set(com.alibaba.datax.plugin.unstructuredstorage.writer.Constant.BINARY, true); + writerSplitConfigs.add(splitedTaskConfig); + } + LOG.info("end do split."); + return writerSplitConfigs; + } + + /** + * 根据文件路径获取到文件名, filePath必定包含了文件名 + * + * @param filePath: 文件路径 + */ + public static String getFileName(String filePath) { + if (StringUtils.isBlank(filePath)) { + return null; + } + File file = new File(filePath); + return file.getName(); + } +} diff --git a/pom.xml b/pom.xml old mode 100755 new mode 100644 index 5b7f8e487d..1b364a7546 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ 3.3.2 1.10 1.2 - 1.1.46.sec10 + 2.0.23 16.0.1 3.7.2.1-SNAPSHOT @@ -30,7 +30,7 @@ 1.7.10 1.0.13 2.4 - 4.11 + 4.13.1 5.1.22-1 1.0.0 @@ -38,6 +38,7 @@ UTF-8 UTF-8 UTF-8 + 5.1.47 @@ -52,57 +53,90 @@ postgresqlreader kingbaseesreader oraclereader + cassandrareader + oceanbasev10reader + obhbasereader + rdbmsreader + odpsreader otsreader otsstreamreader - txtfilereader - hdfsreader - streamreader + hbase11xreader + hbase094xreader + hbase11xsqlreader + hbase20xsqlreader + ossreader + hdfsreader ftpreader + txtfilereader + streamreader + clickhousereader + mongodbreader - rdbmsreader - hbase11xreader - hbase094xreader + tdenginereader + gdbreader tsdbreader opentsdbreader - cassandrareader - gdbreader - + loghubreader + datahubreader + starrocksreader + sybasereader + dorisreader mysqlwriter + starrockswriter drdswriter - odpswriter - txtfilewriter - ftpwriter - hdfswriter - streamwriter - otswriter + databendwriter oraclewriter sqlserverwriter postgresqlwriter kingbaseeswriter - osswriter - mongodbwriter adswriter - ocswriter + oceanbasev10writer + obhbasewriter + adbpgwriter + hologresjdbcwriter rdbmswriter + + + odpswriter + osswriter + otswriter hbase11xwriter hbase094xwriter hbase11xsqlwriter - hbase11xsqlreader + hbase20xsqlwriter + kuduwriter + ftpwriter + hdfswriter + txtfilewriter + streamwriter + elasticsearchwriter + mongodbwriter + tdenginewriter + ocswriter tsdbwriter - adbpgwriter gdbwriter + oscarwriter + loghubwriter + datahubwriter cassandrawriter clickhousewriter + doriswriter + selectdbwriter + adbmysqlwriter + sybasewriter + neo4jwriter + milvuswriter plugin-rdbms-util plugin-unstructured-storage-util - hbase20xsqlreader - hbase20xsqlwriter - kuduwriter + gaussdbreader + gaussdbwriter + datax-example + @@ -113,8 +147,8 @@ ${commons-lang3-version} - com.alibaba - fastjson + com.alibaba.fastjson2 + fastjson2 ${fastjson-version} - com.dm - dm - system - ${basedir}/src/main/libs/Dm7JdbcDriver16.jar + com.dameng + Dm7JdbcDriver17 + 7.6.0.142 + com.sybase jconn3 @@ -38,13 +39,20 @@ system ${basedir}/src/main/libs/jconn3-1.0.0-SNAPSHOT.jar - + + ppas ppas 16 system ${basedir}/src/main/libs/edb-jdbc16.jar + + + com.ibm.db2.jcc + db2jcc + db2jcc4 + org.slf4j @@ -97,13 +105,4 @@ - - - - com.dm - dm - 16 - - - diff --git a/rdbmsreader/src/main/libs/Dm7JdbcDriver16.jar b/rdbmsreader/src/main/libs/Dm7JdbcDriver16.jar deleted file mode 100755 index 30740dcd2c..0000000000 Binary files a/rdbmsreader/src/main/libs/Dm7JdbcDriver16.jar and /dev/null differ diff --git a/rdbmsreader/src/main/libs/db2jcc4.jar b/rdbmsreader/src/main/libs/db2jcc4.jar deleted file mode 100755 index fc53cfd94b..0000000000 Binary files a/rdbmsreader/src/main/libs/db2jcc4.jar and /dev/null differ diff --git a/rdbmsreader/src/main/resources/plugin.json b/rdbmsreader/src/main/resources/plugin.json index d344dd8602..f79a6ace8f 100755 --- a/rdbmsreader/src/main/resources/plugin.json +++ b/rdbmsreader/src/main/resources/plugin.json @@ -3,5 +3,5 @@ "class": "com.alibaba.datax.plugin.reader.rdbmsreader.RdbmsReader", "description": "useScene: prod. mechanism: Jdbc connection using the database, execute select sql, retrieve data from the ResultSet. warn: The more you know about the database, the less problems you encounter.", "developer": "alibaba", - "drivers":["dm.jdbc.driver.DmDriver", "com.sybase.jdbc3.jdbc.SybDriver", "com.edb.Driver"] + "drivers":["dm.jdbc.driver.DmDriver", "com.sybase.jdbc3.jdbc.SybDriver", "com.edb.Driver", "com.ibm.db2.jcc.DB2Driver"] } diff --git a/rdbmswriter/doc/rdbmswriter.md b/rdbmswriter/doc/rdbmswriter.md index 4135d93bcf..50beb3d355 100644 --- a/rdbmswriter/doc/rdbmswriter.md +++ b/rdbmswriter/doc/rdbmswriter.md @@ -197,4 +197,4 @@ RDBMSWriter 通过 DataX 框架获取 Reader 生成的协议数据,RDBMSWriter ### 3.3 类型转换 -目前RDBMSReader支持大部分通用得关系数据库类型如数字、字符等,但也存在部分个别类型没有支持的情况,请注意检查你的类型,根据具体的数据库做选择。 +目前RDBMSWriter支持大部分通用得关系数据库类型如数字、字符等,但也存在部分个别类型没有支持的情况,请注意检查你的类型,根据具体的数据库做选择。 diff --git a/rdbmswriter/pom.xml b/rdbmswriter/pom.xml index 194619609e..a74838b750 100755 --- a/rdbmswriter/pom.xml +++ b/rdbmswriter/pom.xml @@ -25,27 +25,34 @@ + - com.dm - dm + com.dameng + Dm7JdbcDriver17 + 7.6.0.142 + + + + com.sybase + jconn3 + 1.0.0-SNAPSHOT + system + ${basedir}/src/main/libs/jconn3-1.0.0-SNAPSHOT.jar + + + + ppas + ppas 16 system - ${basedir}/src/main/libs/Dm7JdbcDriver16.jar + ${basedir}/src/main/libs/edb-jdbc16.jar + - com.sybase - jconn3 - 1.0.0-SNAPSHOT - system - ${basedir}/src/main/libs/jconn3-1.0.0-SNAPSHOT.jar - - - ppas - ppas - 16 - system - ${basedir}/src/main/libs/edb-jdbc16.jar - + com.ibm.db2.jcc + db2jcc + db2jcc4 + org.slf4j diff --git a/rdbmswriter/src/main/java/com/alibaba/datax/plugin/reader/rdbmswriter/SubCommonRdbmsWriter.java b/rdbmswriter/src/main/java/com/alibaba/datax/plugin/reader/rdbmswriter/SubCommonRdbmsWriter.java index f1fbc552ef..88e50f1124 100755 --- a/rdbmswriter/src/main/java/com/alibaba/datax/plugin/reader/rdbmswriter/SubCommonRdbmsWriter.java +++ b/rdbmswriter/src/main/java/com/alibaba/datax/plugin/reader/rdbmswriter/SubCommonRdbmsWriter.java @@ -29,7 +29,7 @@ public Task(DataBaseType dataBaseType) { @Override protected PreparedStatement fillPreparedStatementColumnType( PreparedStatement preparedStatement, int columnIndex, - int columnSqltype, Column column) throws SQLException { + int columnSqltype, String typeName, Column column) throws SQLException { java.util.Date utilDate; try { switch (columnSqltype) { diff --git a/rdbmswriter/src/main/libs/Dm7JdbcDriver16.jar b/rdbmswriter/src/main/libs/Dm7JdbcDriver16.jar deleted file mode 100755 index 30740dcd2c..0000000000 Binary files a/rdbmswriter/src/main/libs/Dm7JdbcDriver16.jar and /dev/null differ diff --git a/rdbmswriter/src/main/libs/db2jcc4.jar b/rdbmswriter/src/main/libs/db2jcc4.jar deleted file mode 100755 index fc53cfd94b..0000000000 Binary files a/rdbmswriter/src/main/libs/db2jcc4.jar and /dev/null differ diff --git a/rdbmswriter/src/main/resources/plugin.json b/rdbmswriter/src/main/resources/plugin.json index fa771af294..bf32140a9c 100755 --- a/rdbmswriter/src/main/resources/plugin.json +++ b/rdbmswriter/src/main/resources/plugin.json @@ -3,5 +3,5 @@ "class": "com.alibaba.datax.plugin.reader.rdbmswriter.RdbmsWriter", "description": "useScene: prod. mechanism: Jdbc connection using the database, execute select sql, retrieve data from the ResultSet. warn: The more you know about the database, the less problems you encounter.", "developer": "alibaba", - "drivers":["dm.jdbc.driver.DmDriver", "com.sybase.jdbc3.jdbc.SybDriver", "com.edb.Driver"] + "drivers":["dm.jdbc.driver.DmDriver", "com.sybase.jdbc3.jdbc.SybDriver", "com.edb.Driver", "com.ibm.db2.jcc.DB2Driver"] } diff --git a/selectdbwriter/doc/selectdbwriter.md b/selectdbwriter/doc/selectdbwriter.md new file mode 100644 index 0000000000..cdf3926306 --- /dev/null +++ b/selectdbwriter/doc/selectdbwriter.md @@ -0,0 +1,428 @@ +# SelectdbWriter 插件文档 + +## 1 快速介绍 +SelectdbWriter支持将大批量数据写入SELECTDB中。 + +## 2 实现原理 +SelectdbWriter 通过调用selectdb api (/copy/upload),返回一个重定向的S3地址,使用Http向S3地址发送字节流,设置参数达到要求时执行copy into + +## 3 编译 + +1. 运行 init-env.sh + +2. 编译 selectdbwriter: + +i. 单独编译 selectdbwriter 插件: + + ```text + mvn clean install -pl plugin-rdbms-util,selectdbwriter -DskipTests + ``` + + +ii.编译整个 DataX 项目: + + ```text + mvn package assembly:assembly -Dmaven.test.skip=true + ``` +产出在 target/datax/datax/. +hdfsreader, hdfswriter and oscarwriter 这三个插件需要额外的jar包。如果你并不需要这些插件,可以在 DataX/pom.xml 中删除这些插件的模块。 + + +iii.编译错误 + +如遇到如下编译错误: + ```text + Could not find artifact com.alibaba.datax:datax-all:pom:0.0.1-SNAPSHOT + ``` + +可尝试以下方式解决: + +a.下载 alibaba-datax-maven-m2-20210928.tar.gz + +b.解压后,将得到的 alibaba/datax/ 目录,拷贝到所使用的 maven 对应的 .m2/repository/com/alibaba/ 下。 + +c.再次尝试编译。 + +## 3 功能说明 + +### 3.1 配置样例 + +这里是一份从Stream读取数据后导入至selectdb的配置文件。 + +``` +{ + "job":{ + "content":[ + { + "reader":{ + "name":"streamreader", + "parameter":{ + "column":[ + { + "type":"string", + "random":"0,31" + }, + { + "type":"string", + "random":"0,31" + }, + { + "type":"string", + "random":"0,31" + }, + { + "type":"string", + "random":"0,31" + }, + { + "type":"long", + "random":"0,5" + }, + { + "type":"string", + "random":"0,10" + }, + { + "type":"string", + "random":"0,5" + }, + { + "type":"string", + "random":"0,31" + }, + { + "type":"string", + "random":"0,31" + }, + { + "type":"string", + "random":"0,21" + }, + { + "type":"string", + "random":"0,31" + }, + { + "type":"long", + "random":"0,10" + }, + { + "type":"long", + "random":"0,20" + }, + { + "type":"date", + "random":"2022-01-01 12:00:00,2023-01-01 12:00:00" + }, + { + "type":"long", + "random":"0,10" + }, + { + "type":"date", + "random":"2022-01-01 12:00:00,2023-01-01 12:00:00" + }, + { + "type":"string", + "random":"0,10" + }, + { + "type":"long", + "random":"0,10" + }, + { + "type":"date", + "random":"2022-01-01 12:00:00,2023-01-01 12:00:00" + }, + { + "type":"long", + "random":"0,10" + }, + { + "type":"date", + "random":"2022-01-01 12:00:00,2023-01-01 12:00:00" + }, + { + "type":"long", + "random":"0,10" + }, + { + "type":"date", + "random":"2022-01-01 12:00:00,2023-01-01 12:00:00" + }, + { + "type":"long", + "random":"0,10" + }, + { + "type":"date", + "random":"2022-01-01 12:00:00,2023-01-01 12:00:00" + }, + { + "type":"string", + "random":"0,100" + }, + { + "type":"string", + "random":"0,1" + }, + { + "type":"long", + "random":"0,1" + }, + { + "type":"string", + "random":"0,64" + }, + { + "type":"string", + "random":"0,20" + }, + { + "type":"string", + "random":"0,31" + }, + { + "type":"long", + "random":"0,3" + }, + { + "type":"long", + "random":"0,3" + }, + { + "type":"long", + "random":"0,19" + }, + { + "type":"date", + "random":"2022-01-01 12:00:00,2023-01-01 12:00:00" + }, + { + "type":"string", + "random":"0,1" + } + ], + "sliceRecordCount":10 + } + }, + "writer":{ + "name":"selectdbwriter", + "parameter":{ + "loadUrl":[ + "xxx:47150" + ], + "loadProps":{ + "file.type":"json", + "file.strip_outer_array":"true" + }, + "column":[ + "id", + "table_id", + "table_no", + "table_name", + "table_status", + "no_disturb", + "dinner_type", + "member_id", + "reserve_bill_no", + "pre_order_no", + "queue_num", + "person_num", + "open_time", + "open_time_format", + "order_time", + "order_time_format", + "table_bill_id", + "offer_time", + "offer_time_format", + "confirm_bill_time", + "confirm_bill_time_format", + "bill_time", + "bill_time_format", + "clear_time", + "clear_time_format", + "table_message", + "bill_close", + "table_type", + "pad_mac", + "company_id", + "shop_id", + "is_sync", + "table_split_no", + "ts", + "ts_format", + "dr" + ], + "username":"admin", + "password":"SelectDB2022", + "postSql":[ + + ], + "preSql":[ + + ], + "connection":[ + { + "jdbcUrl":"jdbc:mysql://xxx:34142/cl_test", + "table":[ + "ods_pos_pro_table_dynamic_delta_v4" + ], + "selectedDatabase":"cl_test" + } + ], + "maxBatchRows":1000000, + "maxBatchByteSize":536870912000 + } + } + } + ], + "setting":{ + "errorLimit":{ + "percentage":0.02, + "record":0 + }, + "speed":{ + "channel":5 + } + } + } +} + +``` + +### 3.2 参数说明 + +```text + **jdbcUrl** + + - 描述:selectdb 的 JDBC 连接串,用户执行 preSql 或 postSQL。 + - 必选:是 + - 默认值:无 + +* **loadUrl** + + - 描述:作为 selecdb 的连接目标。格式为 "ip:port"。其中 IP 是 selectdb的private-link,port 是selectdb 集群的 http_port + - 必选:是 + - 默认值:无 + +* **username** + + - 描述:访问selectdb数据库的用户名 + - 必选:是 + - 默认值:无 + +* **password** + + - 描述:访问selectdb数据库的密码 + - 必选:否 + - 默认值:空 + +* **connection.selectedDatabase** + - 描述:需要写入的selectdb数据库名称。 + - 必选:是 + - 默认值:无 + +* **connection.table** + - 描述:需要写入的selectdb表名称。 + - 必选:是 + - 默认值:无 + +* **column** + + - 描述:目的表**需要写入数据**的字段,这些字段将作为生成的 Json 数据的字段名。字段之间用英文逗号分隔。例如: "column": ["id","name","age"]。 + - 必选:是 + - 默认值:否 + +* **preSql** + + - 描述:写入数据到目的表前,会先执行这里的标准语句。 + - 必选:否 + - 默认值:无 + +* **postSql** + + - 描述:写入数据到目的表后,会执行这里的标准语句。 + - 必选:否 + - 默认值:无 + + +* **maxBatchRows** + + - 描述:每批次导入数据的最大行数。和 **batchSize** 共同控制每批次的导入数量。每批次数据达到两个阈值之一,即开始导入这一批次的数据。 + - 必选:否 + - 默认值:500000 + +* **batchSize** + + - 描述:每批次导入数据的最大数据量。和 **maxBatchRows** 共同控制每批次的导入数量。每批次数据达到两个阈值之一,即开始导入这一批次的数据。 + - 必选:否 + - 默认值:90M + +* **maxRetries** + + - 描述:每批次导入数据失败后的重试次数。 + - 必选:否 + - 默认值:3 + +* **labelPrefix** + + - 描述:每批次上传文件的 label 前缀。最终的 label 将有 `labelPrefix + UUID` 组成全局唯一的 label,确保数据不会重复导入 + - 必选:否 + - 默认值:`datax_selectdb_writer_` + +* **loadProps** + + - 描述:COPY INOT 的请求参数 + + 这里包括导入的数据格式:file.type等,导入数据格式默认我们使用csv,支持JSON,具体可以参照下面类型转换部分 + + - 必选:否 + + - 默认值:无 + +* **clusterName** + + - 描述:selectdb could 集群名称 + + - 必选:否 + + - 默认值:无 + +* **flushQueueLength** + + - 描述:队列长度 + + - 必选:否 + + - 默认值:1 + +* **flushInterval** + + - 描述:数据写入批次的时间间隔,如果maxBatchRows 和 batchSize 参数设置的有很大,那么很可能达不到你这设置的数据量大小,会执行导入。 + + - 必选:否 + + - 默认值:30000ms +``` + +### 类型转换 + +默认传入的数据均会被转为字符串,并以`\t`作为列分隔符,`\n`作为行分隔符,组成`csv`文件进行Selectdb导入操作。 + +默认是csv格式导入,如需更改列分隔符, 则正确配置 `loadProps` 即可: + +```json +"loadProps": { + "file.column_separator": "\\x01", + "file.line_delimiter": "\\x02" +} +``` + +如需更改导入格式为`json`, 则正确配置 `loadProps` 即可: +```json +"loadProps": { + "file.type": "json", + "file.strip_outer_array": true +} +``` \ No newline at end of file diff --git a/selectdbwriter/doc/stream2selectdb.json b/selectdbwriter/doc/stream2selectdb.json new file mode 100644 index 0000000000..0c5be6dc0a --- /dev/null +++ b/selectdbwriter/doc/stream2selectdb.json @@ -0,0 +1,62 @@ +{ + "core": { + "transport": { + "channel": { + "speed": { + "byte": 10485760 + } + } + } + }, + "job": { + "content": [ + { + "reader": {}, + "writer": { + "name": "selectdbwriter", + "parameter": { + "loadUrl": [ + "xxx:35871" + ], + "loadProps": { + "file.type": "json", + "file.strip_outer_array": "true" + }, + "database": "db1", + "column": [ + "k1", + "k2", + "k3", + "k4", + "k5" + ], + "username": "admin", + "password": "SelectDB2022", + "postSql": [], + "preSql": [], + "connection": [ + { + "jdbcUrl": "jdbc:mysql://xxx:32386/cl_test", + "table": [ + "test_selectdb" + ], + "selectedDatabase": "cl_test" + } + ], + "maxBatchRows": 200000, + "batchSize": 53687091200 + } + } + } + ], + "setting": { + "errorLimit": { + "percentage": 0.02, + "record": 0 + }, + "speed": { + "byte": 10485760 + } + } + } +} \ No newline at end of file diff --git a/selectdbwriter/pom.xml b/selectdbwriter/pom.xml new file mode 100644 index 0000000000..fd2a19f7d6 --- /dev/null +++ b/selectdbwriter/pom.xml @@ -0,0 +1,96 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + selectdbwriter + selectdbwriter + jar + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + mysql + mysql-connector-java + ${mysql.driver.version} + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + com.fasterxml.jackson.core + jackson-annotations + 2.13.3 + + + com.fasterxml.jackson.core + jackson-core + 2.13.3 + + + com.fasterxml.jackson.core + jackson-databind + 2.13.3 + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + diff --git a/selectdbwriter/src/main/assembly/package.xml b/selectdbwriter/src/main/assembly/package.xml new file mode 100644 index 0000000000..1ea0009e5a --- /dev/null +++ b/selectdbwriter/src/main/assembly/package.xml @@ -0,0 +1,34 @@ + + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/writer/selectdbwriter + + + target/ + + selectdbwriter-0.0.1-SNAPSHOT.jar + + plugin/writer/selectdbwriter + + + + + false + plugin/writer/selectdbwriter/libs + runtime + + + diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/BaseResponse.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/BaseResponse.java new file mode 100644 index 0000000000..c02f725f69 --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/BaseResponse.java @@ -0,0 +1,23 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class BaseResponse { + private int code; + private String msg; + private T data; + private int count; + + public int getCode() { + return code; + } + + public String getMsg() { + return msg; + } + + public T getData(){ + return data; + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/CopyIntoResp.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/CopyIntoResp.java new file mode 100644 index 0000000000..4da002acea --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/CopyIntoResp.java @@ -0,0 +1,26 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import java.util.Map; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class CopyIntoResp extends BaseResponse{ + private String code; + private String exception; + + private Map result; + + public String getDataCode() { + return code; + } + + public String getException() { + return exception; + } + + public Map getResult() { + return result; + } + +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/CopySQLBuilder.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/CopySQLBuilder.java new file mode 100644 index 0000000000..62910d5d2c --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/CopySQLBuilder.java @@ -0,0 +1,40 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + + +import java.util.Map; +import java.util.StringJoiner; + +public class CopySQLBuilder { + private final static String COPY_SYNC = "copy.async"; + private final String fileName; + private final Keys options; + private Map properties; + + + + public CopySQLBuilder(Keys options, String fileName) { + this.options=options; + this.fileName=fileName; + this.properties=options.getLoadProps(); + } + + public String buildCopySQL(){ + StringBuilder sb = new StringBuilder(); + sb.append("COPY INTO ") + .append(options.getDatabase() + "." + options.getTable()) + .append(" FROM @~('").append(fileName).append("') ") + .append("PROPERTIES ("); + + //copy into must be sync + properties.put(COPY_SYNC,false); + StringJoiner props = new StringJoiner(","); + for(Map.Entry entry : properties.entrySet()){ + String key = String.valueOf(entry.getKey()); + String value = String.valueOf(entry.getValue()); + String prop = String.format("'%s'='%s'",key,value); + props.add(prop); + } + sb.append(props).append(" )"); + return sb.toString(); + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/DelimiterParser.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/DelimiterParser.java new file mode 100644 index 0000000000..fa6b397c6c --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/DelimiterParser.java @@ -0,0 +1,54 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import com.google.common.base.Strings; + +import java.io.StringWriter; + +public class DelimiterParser { + + private static final String HEX_STRING = "0123456789ABCDEF"; + + public static String parse(String sp, String dSp) throws RuntimeException { + if ( Strings.isNullOrEmpty(sp)) { + return dSp; + } + if (!sp.toUpperCase().startsWith("\\X")) { + return sp; + } + String hexStr = sp.substring(2); + // check hex str + if (hexStr.isEmpty()) { + throw new RuntimeException("Failed to parse delimiter: Hex str is empty"); + } + if (hexStr.length() % 2 != 0) { + throw new RuntimeException("Failed to parse delimiter: Hex str length error"); + } + for (char hexChar : hexStr.toUpperCase().toCharArray()) { + if (HEX_STRING.indexOf(hexChar) == -1) { + throw new RuntimeException("Failed to parse delimiter: Hex str format error"); + } + } + // transform to separator + StringWriter writer = new StringWriter(); + for (byte b : hexStrToBytes(hexStr)) { + writer.append((char) b); + } + return writer.toString(); + } + + private static byte[] hexStrToBytes(String hexStr) { + String upperHexStr = hexStr.toUpperCase(); + int length = upperHexStr.length() / 2; + char[] hexChars = upperHexStr.toCharArray(); + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) { + int pos = i * 2; + bytes[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1])); + } + return bytes; + } + + private static byte charToByte(char c) { + return (byte) HEX_STRING.indexOf(c); + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/HttpPostBuilder.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/HttpPostBuilder.java new file mode 100644 index 0000000000..9471debb97 --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/HttpPostBuilder.java @@ -0,0 +1,51 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHeaders; +import org.apache.http.client.methods.HttpPost; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + + +public class HttpPostBuilder { + String url; + Map header; + HttpEntity httpEntity; + public HttpPostBuilder() { + header = new HashMap<>(); + } + + public HttpPostBuilder setUrl(String url) { + this.url = url; + return this; + } + + public HttpPostBuilder addCommonHeader() { + header.put(HttpHeaders.EXPECT, "100-continue"); + return this; + } + + public HttpPostBuilder baseAuth(String user, String password) { + final String authInfo = user + ":" + password; + byte[] encoded = Base64.encodeBase64(authInfo.getBytes(StandardCharsets.UTF_8)); + header.put(HttpHeaders.AUTHORIZATION, "Basic " + new String(encoded)); + return this; + } + + public HttpPostBuilder setEntity(HttpEntity httpEntity) { + this.httpEntity = httpEntity; + return this; + } + + public HttpPost build() { + SelectdbUtil.checkNotNull(url); + SelectdbUtil.checkNotNull(httpEntity); + HttpPost put = new HttpPost(url); + header.forEach(put::setHeader); + put.setEntity(httpEntity); + return put; + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/HttpPutBuilder.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/HttpPutBuilder.java new file mode 100644 index 0000000000..59d7dbca06 --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/HttpPutBuilder.java @@ -0,0 +1,65 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHeaders; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.entity.StringEntity; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +public class HttpPutBuilder { + String url; + Map header; + HttpEntity httpEntity; + public HttpPutBuilder() { + header = new HashMap<>(); + } + + public HttpPutBuilder setUrl(String url) { + this.url = url; + return this; + } + + public HttpPutBuilder addFileName(String fileName){ + header.put("fileName", fileName); + return this; + } + + public HttpPutBuilder setEmptyEntity() { + try { + this.httpEntity = new StringEntity(""); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + return this; + } + + public HttpPutBuilder addCommonHeader() { + header.put(HttpHeaders.EXPECT, "100-continue"); + return this; + } + + public HttpPutBuilder baseAuth(String user, String password) { + final String authInfo = user + ":" + password; + byte[] encoded = Base64.encodeBase64(authInfo.getBytes(StandardCharsets.UTF_8)); + header.put(HttpHeaders.AUTHORIZATION, "Basic " + new String(encoded)); + return this; + } + + public HttpPutBuilder setEntity(HttpEntity httpEntity) { + this.httpEntity = httpEntity; + return this; + } + + public HttpPut build() { + SelectdbUtil.checkNotNull(url); + SelectdbUtil.checkNotNull(httpEntity); + HttpPut put = new HttpPut(url); + header.forEach(put::setHeader); + put.setEntity(httpEntity); + return put; + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/Keys.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/Keys.java new file mode 100644 index 0000000000..6c767d9321 --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/Keys.java @@ -0,0 +1,186 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class Keys implements Serializable { + + private static final long serialVersionUID = 1l; + private static final int DEFAULT_MAX_RETRIES = 3; + private static final int BATCH_ROWS = 500000; + private static final long DEFAULT_FLUSH_INTERVAL = 30000; + + private static final String LOAD_PROPS_FORMAT = "file.type"; + public enum StreamLoadFormat { + CSV, JSON; + } + + private static final String USERNAME = "username"; + private static final String PASSWORD = "password"; + private static final String DATABASE = "connection[0].selectedDatabase"; + private static final String TABLE = "connection[0].table[0]"; + private static final String COLUMN = "column"; + private static final String PRE_SQL = "preSql"; + private static final String POST_SQL = "postSql"; + private static final String JDBC_URL = "connection[0].jdbcUrl"; + private static final String LABEL_PREFIX = "labelPrefix"; + private static final String MAX_BATCH_ROWS = "maxBatchRows"; + private static final String MAX_BATCH_SIZE = "batchSize"; + private static final String FLUSH_INTERVAL = "flushInterval"; + private static final String LOAD_URL = "loadUrl"; + private static final String FLUSH_QUEUE_LENGTH = "flushQueueLength"; + private static final String LOAD_PROPS = "loadProps"; + + private static final String DEFAULT_LABEL_PREFIX = "datax_selectdb_writer_"; + + private static final long DEFAULT_MAX_BATCH_SIZE = 90 * 1024 * 1024; //default 90M + + private static final String CLUSTER_NAME = "clusterName"; + + private static final String MAX_RETRIES = "maxRetries"; + private final Configuration options; + + private List infoSchemaColumns; + private List userSetColumns; + private boolean isWildcardColumn; + + public Keys ( Configuration options) { + this.options = options; + this.userSetColumns = options.getList(COLUMN, String.class).stream().map(str -> str.replace("`", "")).collect(Collectors.toList()); + if (1 == options.getList(COLUMN, String.class).size() && "*".trim().equals(options.getList(COLUMN, String.class).get(0))) { + this.isWildcardColumn = true; + } + } + + public void doPretreatment() { + validateRequired(); + validateStreamLoadUrl(); + } + + public String getJdbcUrl() { + return options.getString(JDBC_URL); + } + + public String getDatabase() { + return options.getString(DATABASE); + } + + public String getTable() { + return options.getString(TABLE); + } + + public String getUsername() { + return options.getString(USERNAME); + } + + public String getPassword() { + return options.getString(PASSWORD); + } + + public String getClusterName(){ + return options.getString(CLUSTER_NAME); + } + + public String getLabelPrefix() { + String label = options.getString(LABEL_PREFIX); + return null == label ? DEFAULT_LABEL_PREFIX : label; + } + + public List getLoadUrlList() { + return options.getList(LOAD_URL, String.class); + } + + public List getColumns() { + if (isWildcardColumn) { + return this.infoSchemaColumns; + } + return this.userSetColumns; + } + + public boolean isWildcardColumn() { + return this.isWildcardColumn; + } + + public void setInfoCchemaColumns(List cols) { + this.infoSchemaColumns = cols; + } + + public List getPreSqlList() { + return options.getList(PRE_SQL, String.class); + } + + public List getPostSqlList() { + return options.getList(POST_SQL, String.class); + } + + public Map getLoadProps() { + return options.getMap(LOAD_PROPS); + } + + public int getMaxRetries() { + Integer retries = options.getInt(MAX_RETRIES); + return null == retries ? DEFAULT_MAX_RETRIES : retries; + } + + public int getBatchRows() { + Integer rows = options.getInt(MAX_BATCH_ROWS); + return null == rows ? BATCH_ROWS : rows; + } + + public long getBatchSize() { + Long size = options.getLong(MAX_BATCH_SIZE); + return null == size ? DEFAULT_MAX_BATCH_SIZE : size; + } + + public long getFlushInterval() { + Long interval = options.getLong(FLUSH_INTERVAL); + return null == interval ? DEFAULT_FLUSH_INTERVAL : interval; + } + + public int getFlushQueueLength() { + Integer len = options.getInt(FLUSH_QUEUE_LENGTH); + return null == len ? 1 : len; + } + + + public StreamLoadFormat getStreamLoadFormat() { + Map loadProps = getLoadProps(); + if (null == loadProps) { + return StreamLoadFormat.CSV; + } + if (loadProps.containsKey(LOAD_PROPS_FORMAT) + && StreamLoadFormat.JSON.name().equalsIgnoreCase(String.valueOf(loadProps.get(LOAD_PROPS_FORMAT)))) { + return StreamLoadFormat.JSON; + } + return StreamLoadFormat.CSV; + } + + private void validateStreamLoadUrl() { + List urlList = getLoadUrlList(); + for (String host : urlList) { + if (host.split(":").length < 2) { + throw DataXException.asDataXException(DBUtilErrorCode.CONF_ERROR, + "The format of loadUrl is not correct, please enter:[`fe_ip:fe_http_ip;fe_ip:fe_http_ip`]."); + } + } + } + + private void validateRequired() { + final String[] requiredOptionKeys = new String[]{ + USERNAME, + DATABASE, + TABLE, + COLUMN, + LOAD_URL + }; + for (String optionKey : requiredOptionKeys) { + options.getNecessaryValue(optionKey, DBUtilErrorCode.REQUIRED_VALUE); + } + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbBaseCodec.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbBaseCodec.java new file mode 100644 index 0000000000..d2fc122437 --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbBaseCodec.java @@ -0,0 +1,23 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import com.alibaba.datax.common.element.Column; + +public class SelectdbBaseCodec { + protected String convertionField( Column col) { + if (null == col.getRawData() || Column.Type.NULL == col.getType()) { + return null; + } + if ( Column.Type.BOOL == col.getType()) { + return String.valueOf(col.asLong()); + } + if ( Column.Type.BYTES == col.getType()) { + byte[] bts = (byte[])col.getRawData(); + long value = 0; + for (int i = 0; i < bts.length; i++) { + value += (bts[bts.length - i - 1] & 0xffL) << (8 * i); + } + return String.valueOf(value); + } + return col.asString(); + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbCodec.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbCodec.java new file mode 100644 index 0000000000..b7e9d6aeb1 --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbCodec.java @@ -0,0 +1,10 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import com.alibaba.datax.common.element.Record; + +import java.io.Serializable; + +public interface SelectdbCodec extends Serializable { + + String codec( Record row); +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbCodecFactory.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbCodecFactory.java new file mode 100644 index 0000000000..567f4c0b98 --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbCodecFactory.java @@ -0,0 +1,19 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import java.util.Map; + +public class SelectdbCodecFactory { + public SelectdbCodecFactory (){ + + } + public static SelectdbCodec createCodec( Keys writerOptions) { + if ( Keys.StreamLoadFormat.CSV.equals(writerOptions.getStreamLoadFormat())) { + Map props = writerOptions.getLoadProps(); + return new SelectdbCsvCodec (null == props || !props.containsKey("file.column_separator") ? null : String.valueOf(props.get("file.column_separator"))); + } + if ( Keys.StreamLoadFormat.JSON.equals(writerOptions.getStreamLoadFormat())) { + return new SelectdbJsonCodec (writerOptions.getColumns()); + } + throw new RuntimeException("Failed to create row serializer, unsupported `format` from stream load properties."); + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbCopyIntoObserver.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbCopyIntoObserver.java new file mode 100644 index 0000000000..c9228b2215 --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbCopyIntoObserver.java @@ -0,0 +1,233 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +public class SelectdbCopyIntoObserver { + private static final Logger LOG = LoggerFactory.getLogger(SelectdbCopyIntoObserver.class); + + private Keys options; + private long pos; + public static final int SUCCESS = 0; + public static final String FAIL = "1"; + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private final HttpClientBuilder httpClientBuilder = HttpClients + .custom() + .disableRedirectHandling(); + private CloseableHttpClient httpClient; + private static final String UPLOAD_URL_PATTERN = "%s/copy/upload"; + private static final String COMMIT_PATTERN = "%s/copy/query"; + private static final Pattern COMMITTED_PATTERN = Pattern.compile("errCode = 2, detailMessage = No files can be copied, matched (\\d+) files, " + "filtered (\\d+) files because files may be loading or loaded"); + + + public SelectdbCopyIntoObserver(Keys options) { + this.options = options; + this.httpClient = httpClientBuilder.build(); + + } + + public void streamLoad(WriterTuple data) throws Exception { + String host = getLoadHost(); + if (host == null) { + throw new RuntimeException("load_url cannot be empty, or the host cannot connect.Please check your configuration."); + } + String loadUrl = String.format(UPLOAD_URL_PATTERN, host); + String uploadAddress = getUploadAddress(loadUrl, data.getLabel()); + put(uploadAddress, data.getLabel(), addRows(data.getRows(), data.getBytes().intValue())); + executeCopy(host,data.getLabel()); + + } + + private String getUploadAddress(String loadUrl, String fileName) throws IOException { + HttpPutBuilder putBuilder = new HttpPutBuilder(); + putBuilder.setUrl(loadUrl) + .addFileName(fileName) + .addCommonHeader() + .setEmptyEntity() + .baseAuth(options.getUsername(), options.getPassword()); + CloseableHttpResponse execute = httpClientBuilder.build().execute(putBuilder.build()); + int statusCode = execute.getStatusLine().getStatusCode(); + String reason = execute.getStatusLine().getReasonPhrase(); + if (statusCode == 307) { + Header location = execute.getFirstHeader("location"); + String uploadAddress = location.getValue(); + LOG.info("redirect to s3:{}", uploadAddress); + return uploadAddress; + } else { + HttpEntity entity = execute.getEntity(); + String result = entity == null ? null : EntityUtils.toString(entity); + LOG.error("Failed get the redirected address, status {}, reason {}, response {}", statusCode, reason, result); + throw new RuntimeException("Could not get the redirected address."); + } + + } + + private byte[] addRows(List rows, int totalBytes) { + if (Keys.StreamLoadFormat.CSV.equals(options.getStreamLoadFormat())) { + Map props = (options.getLoadProps() == null ? new HashMap<>() : options.getLoadProps()); + byte[] lineDelimiter = DelimiterParser.parse((String) props.get("file.line_delimiter"), "\n").getBytes(StandardCharsets.UTF_8); + ByteBuffer bos = ByteBuffer.allocate(totalBytes + rows.size() * lineDelimiter.length); + for (byte[] row : rows) { + bos.put(row); + bos.put(lineDelimiter); + } + return bos.array(); + } + + if (Keys.StreamLoadFormat.JSON.equals(options.getStreamLoadFormat())) { + ByteBuffer bos = ByteBuffer.allocate(totalBytes + (rows.isEmpty() ? 2 : rows.size() + 1)); + bos.put("[".getBytes(StandardCharsets.UTF_8)); + byte[] jsonDelimiter = ",".getBytes(StandardCharsets.UTF_8); + boolean isFirstElement = true; + for (byte[] row : rows) { + if (!isFirstElement) { + bos.put(jsonDelimiter); + } + bos.put(row); + isFirstElement = false; + } + bos.put("]".getBytes(StandardCharsets.UTF_8)); + return bos.array(); + } + throw new RuntimeException("Failed to join rows data, unsupported `file.type` from copy into properties:"); + } + + public void put(String loadUrl, String fileName, byte[] data) throws IOException { + LOG.info(String.format("Executing upload file to: '%s', size: '%s'", loadUrl, data.length)); + HttpPutBuilder putBuilder = new HttpPutBuilder(); + putBuilder.setUrl(loadUrl) + .addCommonHeader() + .setEntity(new InputStreamEntity(new ByteArrayInputStream(data))); + CloseableHttpResponse response = httpClient.execute(putBuilder.build()); + final int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode != 200) { + String result = response.getEntity() == null ? null : EntityUtils.toString(response.getEntity()); + LOG.error("upload file {} error, response {}", fileName, result); + throw new SelectdbWriterException("upload file error: " + fileName,true); + } + } + + private String getLoadHost() { + List hostList = options.getLoadUrlList(); + long tmp = pos + hostList.size(); + for (; pos < tmp; pos++) { + String host = new StringBuilder("http://").append(hostList.get((int) (pos % hostList.size()))).toString(); + if (checkConnection(host)) { + return host; + } + } + return null; + } + + private boolean checkConnection(String host) { + try { + URL url = new URL(host); + HttpURLConnection co = (HttpURLConnection) url.openConnection(); + co.setConnectTimeout(5000); + co.connect(); + co.disconnect(); + return true; + } catch (Exception e1) { + e1.printStackTrace(); + return false; + } + } + + + /** + * execute copy into + */ + public void executeCopy(String hostPort, String fileName) throws IOException{ + long start = System.currentTimeMillis(); + CopySQLBuilder copySQLBuilder = new CopySQLBuilder(options, fileName); + String copySQL = copySQLBuilder.buildCopySQL(); + LOG.info("build copy SQL is {}", copySQL); + Map params = new HashMap<>(); + params.put("sql", copySQL); + if(StringUtils.isNotBlank(options.getClusterName())){ + params.put("cluster",options.getClusterName()); + } + HttpPostBuilder postBuilder = new HttpPostBuilder(); + postBuilder.setUrl(String.format(COMMIT_PATTERN, hostPort)) + .baseAuth(options.getUsername(), options.getPassword()) + .setEntity(new StringEntity(OBJECT_MAPPER.writeValueAsString(params))); + + CloseableHttpResponse response = httpClient.execute(postBuilder.build()); + final int statusCode = response.getStatusLine().getStatusCode(); + final String reasonPhrase = response.getStatusLine().getReasonPhrase(); + String loadResult = ""; + if (statusCode != 200) { + LOG.warn("commit failed with status {} {}, reason {}", statusCode, hostPort, reasonPhrase); + throw new SelectdbWriterException("commit error with file: " + fileName,true); + } else if (response.getEntity() != null){ + loadResult = EntityUtils.toString(response.getEntity()); + boolean success = handleCommitResponse(loadResult); + if(success){ + LOG.info("commit success cost {}ms, response is {}", System.currentTimeMillis() - start, loadResult); + }else{ + throw new SelectdbWriterException("commit fail",true); + } + } + } + + public boolean handleCommitResponse(String loadResult) throws IOException { + BaseResponse baseResponse = OBJECT_MAPPER.readValue(loadResult, new TypeReference>(){}); + if(baseResponse.getCode() == SUCCESS){ + CopyIntoResp dataResp = baseResponse.getData(); + if(FAIL.equals(dataResp.getDataCode())){ + LOG.error("copy into execute failed, reason:{}", loadResult); + return false; + }else{ + Map result = dataResp.getResult(); + if(!result.get("state").equals("FINISHED") && !isCommitted(result.get("msg"))){ + LOG.error("copy into load failed, reason:{}", loadResult); + return false; + }else{ + return true; + } + } + }else{ + LOG.error("commit failed, reason:{}", loadResult); + return false; + } + } + + public static boolean isCommitted(String msg) { + return COMMITTED_PATTERN.matcher(msg).matches(); + } + + + public void close() throws IOException { + if (null != httpClient) { + try { + httpClient.close(); + } catch (IOException e) { + LOG.error("Closing httpClient failed.", e); + throw new RuntimeException("Closing httpClient failed.", e); + } + } + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbCsvCodec.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbCsvCodec.java new file mode 100644 index 0000000000..57cad84d23 --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbCsvCodec.java @@ -0,0 +1,27 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import com.alibaba.datax.common.element.Record; + +public class SelectdbCsvCodec extends SelectdbBaseCodec implements SelectdbCodec { + + private static final long serialVersionUID = 1L; + + private final String columnSeparator; + + public SelectdbCsvCodec ( String sp) { + this.columnSeparator = DelimiterParser.parse(sp, "\t"); + } + + @Override + public String codec( Record row) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < row.getColumnNumber(); i++) { + String value = convertionField(row.getColumn(i)); + sb.append(null == value ? "\\N" : value); + if (i < row.getColumnNumber() - 1) { + sb.append(columnSeparator); + } + } + return sb.toString(); + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbJsonCodec.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbJsonCodec.java new file mode 100644 index 0000000000..8b1a376009 --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbJsonCodec.java @@ -0,0 +1,33 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.fastjson2.JSON; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class SelectdbJsonCodec extends SelectdbBaseCodec implements SelectdbCodec { + + private static final long serialVersionUID = 1L; + + private final List fieldNames; + + public SelectdbJsonCodec ( List fieldNames) { + this.fieldNames = fieldNames; + } + + @Override + public String codec( Record row) { + if (null == fieldNames) { + return ""; + } + Map rowMap = new HashMap<> (fieldNames.size()); + int idx = 0; + for (String fieldName : fieldNames) { + rowMap.put(fieldName, convertionField(row.getColumn(idx))); + idx++; + } + return JSON.toJSONString(rowMap); + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbUtil.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbUtil.java new file mode 100644 index 0000000000..6cfcc8bf6f --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbUtil.java @@ -0,0 +1,113 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.util.RdbmsException; +import com.alibaba.datax.plugin.rdbms.writer.Constant; +import com.alibaba.druid.sql.parser.ParserException; +import com.google.common.base.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * jdbc util + */ +public class SelectdbUtil { + private static final Logger LOG = LoggerFactory.getLogger(SelectdbUtil.class); + + private SelectdbUtil() {} + + public static List getDorisTableColumns( Connection conn, String databaseName, String tableName) { + String currentSql = String.format("SELECT COLUMN_NAME FROM `information_schema`.`COLUMNS` WHERE `TABLE_SCHEMA` = '%s' AND `TABLE_NAME` = '%s' ORDER BY `ORDINAL_POSITION` ASC;", databaseName, tableName); + List columns = new ArrayList<> (); + ResultSet rs = null; + try { + rs = DBUtil.query(conn, currentSql); + while (DBUtil.asyncResultSetNext(rs)) { + String colName = rs.getString("COLUMN_NAME"); + columns.add(colName); + } + return columns; + } catch (Exception e) { + throw RdbmsException.asQueryException(DataBaseType.MySql, e, currentSql, null, null); + } finally { + DBUtil.closeDBResources(rs, null, null); + } + } + + public static List renderPreOrPostSqls(List preOrPostSqls, String tableName) { + if (null == preOrPostSqls) { + return Collections.emptyList(); + } + List renderedSqls = new ArrayList<>(); + for (String sql : preOrPostSqls) { + if (! Strings.isNullOrEmpty(sql)) { + renderedSqls.add(sql.replace(Constant.TABLE_NAME_PLACEHOLDER, tableName)); + } + } + return renderedSqls; + } + + public static void executeSqls(Connection conn, List sqls) { + Statement stmt = null; + String currentSql = null; + try { + stmt = conn.createStatement(); + for (String sql : sqls) { + currentSql = sql; + DBUtil.executeSqlWithoutResultSet(stmt, sql); + } + } catch (Exception e) { + throw RdbmsException.asQueryException(DataBaseType.MySql, e, currentSql, null, null); + } finally { + DBUtil.closeDBResources(null, stmt, null); + } + } + + public static void preCheckPrePareSQL( Keys options) { + String table = options.getTable(); + List preSqls = options.getPreSqlList(); + List renderedPreSqls = SelectdbUtil.renderPreOrPostSqls(preSqls, table); + if (null != renderedPreSqls && !renderedPreSqls.isEmpty()) { + LOG.info("Begin to preCheck preSqls:[{}].", String.join(";", renderedPreSqls)); + for (String sql : renderedPreSqls) { + try { + DBUtil.sqlValid(sql, DataBaseType.MySql); + } catch ( ParserException e) { + throw RdbmsException.asPreSQLParserException(DataBaseType.MySql,e,sql); + } + } + } + } + + public static void preCheckPostSQL( Keys options) { + String table = options.getTable(); + List postSqls = options.getPostSqlList(); + List renderedPostSqls = SelectdbUtil.renderPreOrPostSqls(postSqls, table); + if (null != renderedPostSqls && !renderedPostSqls.isEmpty()) { + LOG.info("Begin to preCheck postSqls:[{}].", String.join(";", renderedPostSqls)); + for(String sql : renderedPostSqls) { + try { + DBUtil.sqlValid(sql, DataBaseType.MySql); + } catch (ParserException e){ + throw RdbmsException.asPostSQLParserException(DataBaseType.MySql,e,sql); + } + } + } + } + + public static T checkNotNull(T reference) { + if (reference == null) { + throw new NullPointerException(); + } else { + return reference; + } + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbWriter.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbWriter.java new file mode 100644 index 0000000000..2b91f122da --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbWriter.java @@ -0,0 +1,149 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.List; + +/** + * doris data writer + */ +public class SelectdbWriter extends Writer { + + public static class Job extends Writer.Job { + + private static final Logger LOG = LoggerFactory.getLogger(Job.class); + private Configuration originalConfig = null; + private Keys options; + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + options = new Keys (super.getPluginJobConf()); + options.doPretreatment(); + } + + @Override + public void preCheck(){ + this.init(); + SelectdbUtil.preCheckPrePareSQL(options); + SelectdbUtil.preCheckPostSQL(options); + } + + @Override + public void prepare() { + String username = options.getUsername(); + String password = options.getPassword(); + String jdbcUrl = options.getJdbcUrl(); + List renderedPreSqls = SelectdbUtil.renderPreOrPostSqls(options.getPreSqlList(), options.getTable()); + if (null != renderedPreSqls && !renderedPreSqls.isEmpty()) { + Connection conn = DBUtil.getConnection(DataBaseType.MySql, jdbcUrl, username, password); + LOG.info("Begin to execute preSqls:[{}]. context info:{}.", String.join(";", renderedPreSqls), jdbcUrl); + SelectdbUtil.executeSqls(conn, renderedPreSqls); + DBUtil.closeDBResources(null, null, conn); + } + } + + @Override + public List split(int mandatoryNumber) { + List configurations = new ArrayList<>(mandatoryNumber); + for (int i = 0; i < mandatoryNumber; i++) { + configurations.add(originalConfig); + } + return configurations; + } + + @Override + public void post() { + String username = options.getUsername(); + String password = options.getPassword(); + String jdbcUrl = options.getJdbcUrl(); + List renderedPostSqls = SelectdbUtil.renderPreOrPostSqls(options.getPostSqlList(), options.getTable()); + if (null != renderedPostSqls && !renderedPostSqls.isEmpty()) { + Connection conn = DBUtil.getConnection(DataBaseType.MySql, jdbcUrl, username, password); + LOG.info("Start to execute preSqls:[{}]. context info:{}.", String.join(";", renderedPostSqls), jdbcUrl); + SelectdbUtil.executeSqls(conn, renderedPostSqls); + DBUtil.closeDBResources(null, null, conn); + } + } + + @Override + public void destroy() { + } + + } + + public static class Task extends Writer.Task { + private SelectdbWriterManager writerManager; + private Keys options; + private SelectdbCodec rowCodec; + + @Override + public void init() { + options = new Keys (super.getPluginJobConf()); + if (options.isWildcardColumn()) { + Connection conn = DBUtil.getConnection(DataBaseType.MySql, options.getJdbcUrl(), options.getUsername(), options.getPassword()); + List columns = SelectdbUtil.getDorisTableColumns(conn, options.getDatabase(), options.getTable()); + options.setInfoCchemaColumns(columns); + } + writerManager = new SelectdbWriterManager(options); + rowCodec = SelectdbCodecFactory.createCodec(options); + } + + @Override + public void prepare() { + } + + public void startWrite(RecordReceiver recordReceiver) { + try { + Record record; + while ((record = recordReceiver.getFromReader()) != null) { + if (record.getColumnNumber() != options.getColumns().size()) { + throw DataXException + .asDataXException( + DBUtilErrorCode.CONF_ERROR, + String.format( + "There is an error in the column configuration information. " + + "This is because you have configured a task where the number of fields to be read from the source:%s " + + "is not equal to the number of fields to be written to the destination table:%s. " + + "Please check your configuration and make changes.", + record.getColumnNumber(), + options.getColumns().size())); + } + writerManager.writeRecord(rowCodec.codec(record)); + } + } catch (Exception e) { + throw DataXException.asDataXException(DBUtilErrorCode.WRITE_DATA_ERROR, e); + } + } + + @Override + public void post() { + try { + writerManager.close(); + } catch (Exception e) { + throw DataXException.asDataXException(DBUtilErrorCode.WRITE_DATA_ERROR, e); + } + } + + @Override + public void destroy() {} + + @Override + public boolean supportFailOver(){ + return false; + } + } + + +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbWriterException.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbWriterException.java new file mode 100644 index 0000000000..f85a06d180 --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbWriterException.java @@ -0,0 +1,39 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + + +public class SelectdbWriterException extends RuntimeException { + + private boolean reCreateLabel; + + + public SelectdbWriterException() { + super(); + } + + public SelectdbWriterException(String message) { + super(message); + } + + public SelectdbWriterException(String message, boolean reCreateLabel) { + super(message); + this.reCreateLabel = reCreateLabel; + } + + public SelectdbWriterException(String message, Throwable cause) { + super(message, cause); + } + + public SelectdbWriterException(Throwable cause) { + super(cause); + } + + protected SelectdbWriterException(String message, Throwable cause, + boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public boolean needReCreateLabel() { + return reCreateLabel; + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbWriterManager.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbWriterManager.java new file mode 100644 index 0000000000..e8b22b7f78 --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/SelectdbWriterManager.java @@ -0,0 +1,196 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import com.google.common.base.Strings; +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public class SelectdbWriterManager { + + private static final Logger LOG = LoggerFactory.getLogger(SelectdbWriterManager.class); + + private final SelectdbCopyIntoObserver visitor; + private final Keys options; + private final List buffer = new ArrayList<>(); + private int batchCount = 0; + private long batchSize = 0; + private volatile boolean closed = false; + private volatile Exception flushException; + private final LinkedBlockingDeque flushQueue; + private ScheduledExecutorService scheduler; + private ScheduledFuture scheduledFuture; + + public SelectdbWriterManager(Keys options) { + this.options = options; + this.visitor = new SelectdbCopyIntoObserver(options); + flushQueue = new LinkedBlockingDeque<>(options.getFlushQueueLength()); + this.startScheduler(); + this.startAsyncFlushing(); + } + + public void startScheduler() { + stopScheduler(); + this.scheduler = Executors.newScheduledThreadPool(1, new BasicThreadFactory.Builder().namingPattern("Doris-interval-flush").daemon(true).build()); + this.scheduledFuture = this.scheduler.schedule(() -> { + synchronized (SelectdbWriterManager.this) { + if (!closed) { + try { + String label = createBatchLabel(); + LOG.info(String.format("Selectdb interval Sinking triggered: label[%s].", label)); + if (batchCount == 0) { + startScheduler(); + } + flush(label, false); + } catch (Exception e) { + flushException = e; + } + } + } + }, options.getFlushInterval(), TimeUnit.MILLISECONDS); + } + + public void stopScheduler() { + if (this.scheduledFuture != null) { + scheduledFuture.cancel(false); + this.scheduler.shutdown(); + } + } + + public final synchronized void writeRecord(String record) throws IOException { + checkFlushException(); + try { + byte[] bts = record.getBytes(StandardCharsets.UTF_8); + buffer.add(bts); + batchCount++; + batchSize += bts.length; + if (batchCount >= options.getBatchRows() || batchSize >= options.getBatchSize()) { + String label = createBatchLabel(); + if(LOG.isDebugEnabled()){ + LOG.debug(String.format("buffer Sinking triggered: rows[%d] label [%s].", batchCount, label)); + } + flush(label, false); + } + } catch (Exception e) { + throw new SelectdbWriterException("Writing records to selectdb failed.", e); + } + } + + public synchronized void flush(String label, boolean waitUtilDone) throws Exception { + checkFlushException(); + if (batchCount == 0) { + if (waitUtilDone) { + waitAsyncFlushingDone(); + } + return; + } + flushQueue.put(new WriterTuple(label, batchSize, new ArrayList<>(buffer))); + if (waitUtilDone) { + // wait the last flush + waitAsyncFlushingDone(); + } + buffer.clear(); + batchCount = 0; + batchSize = 0; + } + + public synchronized void close() throws IOException { + if (!closed) { + closed = true; + try { + String label = createBatchLabel(); + if (batchCount > 0) { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Selectdb Sink is about to close: label[%s].", label)); + } + } + flush(label, true); + } catch (Exception e) { + throw new RuntimeException("Writing records to Selectdb failed.", e); + } + } + checkFlushException(); + } + + public String createBatchLabel() { + StringBuilder sb = new StringBuilder(); + if (!Strings.isNullOrEmpty(options.getLabelPrefix())) { + sb.append(options.getLabelPrefix()); + } + return sb.append(UUID.randomUUID().toString()) + .toString(); + } + + private void startAsyncFlushing() { + // start flush thread + Thread flushThread = new Thread(new Runnable() { + public void run() { + while (true) { + try { + asyncFlush(); + } catch (Exception e) { + flushException = e; + } + } + } + }); + flushThread.setDaemon(true); + flushThread.start(); + } + + private void waitAsyncFlushingDone() throws InterruptedException { + // wait previous flushings + for (int i = 0; i <= options.getFlushQueueLength(); i++) { + flushQueue.put(new WriterTuple("", 0l, null)); + } + checkFlushException(); + } + + private void asyncFlush() throws Exception { + WriterTuple flushData = flushQueue.take(); + if (Strings.isNullOrEmpty(flushData.getLabel())) { + return; + } + stopScheduler(); + for (int i = 0; i <= options.getMaxRetries(); i++) { + try { + // copy into + visitor.streamLoad(flushData); + startScheduler(); + break; + } catch (Exception e) { + LOG.warn("Failed to flush batch data to selectdb, retry times = {}", i, e); + if (i >= options.getMaxRetries()) { + throw new RuntimeException(e); + } + if (e instanceof SelectdbWriterException && ((SelectdbWriterException)e).needReCreateLabel()) { + String newLabel = createBatchLabel(); + LOG.warn(String.format("Batch label changed from [%s] to [%s]", flushData.getLabel(), newLabel)); + flushData.setLabel(newLabel); + } + try { + Thread.sleep(1000l * Math.min(i + 1, 100)); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new RuntimeException("Unable to flush, interrupted while doing another attempt", e); + } + } + } + } + + private void checkFlushException() { + if (flushException != null) { + throw new RuntimeException("Writing records to selectdb failed.", flushException); + } + } +} diff --git a/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/WriterTuple.java b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/WriterTuple.java new file mode 100644 index 0000000000..483ade05fb --- /dev/null +++ b/selectdbwriter/src/main/java/com/alibaba/datax/plugin/writer/selectdbwriter/WriterTuple.java @@ -0,0 +1,22 @@ +package com.alibaba.datax.plugin.writer.selectdbwriter; + +import java.util.List; + +public class WriterTuple { + private String label; + private Long bytes; + private List rows; + + + public WriterTuple ( String label, Long bytes, List rows){ + this.label = label; + this.rows = rows; + this.bytes = bytes; + } + + public String getLabel() { return label; } + public void setLabel(String label) { this.label = label; } + public Long getBytes() { return bytes; } + public List getRows() { return rows; } + +} diff --git a/selectdbwriter/src/main/resources/plugin.json b/selectdbwriter/src/main/resources/plugin.json new file mode 100644 index 0000000000..4b84a945cf --- /dev/null +++ b/selectdbwriter/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "selectdbwriter", + "class": "com.alibaba.datax.plugin.writer.selectdbwriter.SelectdbWriter", + "description": "selectdb writer plugin", + "developer": "selectdb" +} \ No newline at end of file diff --git a/selectdbwriter/src/main/resources/plugin_job_template.json b/selectdbwriter/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..c603b7e02e --- /dev/null +++ b/selectdbwriter/src/main/resources/plugin_job_template.json @@ -0,0 +1,19 @@ +{ + "name": "selectdbwriter", + "parameter": { + "username": "", + "password": "", + "column": [], + "preSql": [], + "postSql": [], + "loadUrl": [], + "loadProps": {}, + "connection": [ + { + "jdbcUrl": "", + "selectedDatabase": "", + "table": [] + } + ] + } +} \ No newline at end of file diff --git a/sqlserverreader/pom.xml b/sqlserverreader/pom.xml index 5372a0577b..326f1ce51d 100755 --- a/sqlserverreader/pom.xml +++ b/sqlserverreader/pom.xml @@ -31,10 +31,7 @@ com.microsoft.sqlserver sqljdbc4 4.0 - system - ${basedir}/src/main/lib/sqljdbc4-4.0.jar - com.alibaba.datax plugin-rdbms-util diff --git a/sqlserverreader/src/main/assembly/package.xml b/sqlserverreader/src/main/assembly/package.xml index 55fbdc0b9c..6180fbc0a9 100755 --- a/sqlserverreader/src/main/assembly/package.xml +++ b/sqlserverreader/src/main/assembly/package.xml @@ -16,13 +16,6 @@ plugin/reader/sqlserverreader - - src/main/lib - - sqljdbc4-4.0.jar - - plugin/reader/sqlserverreader/libs - target/ diff --git a/sqlserverreader/src/main/lib/sqljdbc4-4.0.jar b/sqlserverreader/src/main/lib/sqljdbc4-4.0.jar deleted file mode 100644 index d6b7f6daf4..0000000000 Binary files a/sqlserverreader/src/main/lib/sqljdbc4-4.0.jar and /dev/null differ diff --git a/sqlserverwriter/doc/sqlserverwriter.md b/sqlserverwriter/doc/sqlserverwriter.md index 255834c65b..7d786292a4 100644 --- a/sqlserverwriter/doc/sqlserverwriter.md +++ b/sqlserverwriter/doc/sqlserverwriter.md @@ -69,6 +69,7 @@ SqlServerWriter 通过 DataX 框架获取 Reader 生成的协议数据,根据 "jdbcUrl": "jdbc:sqlserver://[HOST_NAME]:PORT;DatabaseName=[DATABASE_NAME]" } ], + "session": ["SET IDENTITY_INSERT TABLE_NAME ON"], "preSql": [ "delete from @table where db_id = -1;" ], @@ -127,7 +128,7 @@ SqlServerWriter 通过 DataX 框架获取 Reader 生成的协议数据,根据 * **column** - * 描述:目的表需要写入数据的字段,字段之间用英文逗号分隔。例如: "column": ["id","name","age"]。如果要依次写入全部列,使用*表示, 例如: "column": ["*"] + * 描述:目的表需要写入数据的字段,字段之间用英文逗号分隔。例如: "column": ["id","name","age"]。如果要依次写入全部列,使用*表示, 例如: "column": ["\*"] **column配置项必须指定,不能留空!** @@ -139,6 +140,14 @@ SqlServerWriter 通过 DataX 框架获取 Reader 生成的协议数据,根据 * 默认值:否
    +* **session** + + * 描述:DataX在获取 seqlserver 连接时,执行session指定的SQL语句,修改当前connection session属性
    + + * 必选:否
    + + * 默认值:无
    + * **preSql** * 描述:写入数据到目的表前,会先执行这里的标准语句。如果 Sql 中有你需要操作到的表名称,请使用 `@table` 表示,这样在实际执行 Sql 语句时,会对变量按照实际表名称进行替换。
    diff --git a/sqlserverwriter/pom.xml b/sqlserverwriter/pom.xml index d2b1eea1f8..6f52c14c96 100644 --- a/sqlserverwriter/pom.xml +++ b/sqlserverwriter/pom.xml @@ -35,8 +35,6 @@ com.microsoft.sqlserver sqljdbc4 4.0 - system - ${basedir}/src/main/lib/sqljdbc4-4.0.jar com.alibaba.datax diff --git a/sqlserverwriter/src/main/assembly/package.xml b/sqlserverwriter/src/main/assembly/package.xml index 761dffcd12..f8f262987a 100755 --- a/sqlserverwriter/src/main/assembly/package.xml +++ b/sqlserverwriter/src/main/assembly/package.xml @@ -16,13 +16,6 @@
    plugin/writer/sqlserverwriter
    - - src/main/lib - - sqljdbc4-4.0.jar - - plugin/writer/sqlserverwriter/libs - target/ diff --git a/sqlserverwriter/src/main/lib/sqljdbc4-4.0.jar b/sqlserverwriter/src/main/lib/sqljdbc4-4.0.jar deleted file mode 100644 index d6b7f6daf4..0000000000 Binary files a/sqlserverwriter/src/main/lib/sqljdbc4-4.0.jar and /dev/null differ diff --git a/starrocksreader/pom.xml b/starrocksreader/pom.xml new file mode 100644 index 0000000000..a8b049ea9c --- /dev/null +++ b/starrocksreader/pom.xml @@ -0,0 +1,95 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + starrocksreader + starrocksreader + jar + + + 8 + 8 + + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + + mysql + mysql-connector-java + 5.1.46 + + + + + + + + src/main/java + + **/*.properties + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + \ No newline at end of file diff --git a/starrocksreader/src/main/assembly/package.xml b/starrocksreader/src/main/assembly/package.xml new file mode 100644 index 0000000000..c126c107a8 --- /dev/null +++ b/starrocksreader/src/main/assembly/package.xml @@ -0,0 +1,35 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/reader/starrocksreader + + + target/ + + starrocksreader-0.0.1-SNAPSHOT.jar + + plugin/reader/starrocksreader + + + + + + false + plugin/reader/starrocksreader/libs + runtime + + + diff --git a/starrocksreader/src/main/java/com/alibaba/datax/plugin/reader/starrocksreader/StarRocksReader.java b/starrocksreader/src/main/java/com/alibaba/datax/plugin/reader/starrocksreader/StarRocksReader.java new file mode 100644 index 0000000000..d4bf343714 --- /dev/null +++ b/starrocksreader/src/main/java/com/alibaba/datax/plugin/reader/starrocksreader/StarRocksReader.java @@ -0,0 +1,116 @@ +package com.alibaba.datax.plugin.reader.starrocksreader; + +import java.util.List; + +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.spi.Reader; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.reader.CommonRdbmsReader; +import com.alibaba.datax.plugin.rdbms.reader.Constant; +import com.alibaba.datax.plugin.rdbms.reader.Key; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +public class StarRocksReader extends Reader { + + private static final DataBaseType DATABASE_TYPE = DataBaseType.StarRocks; + + public static class Job extends Reader.Job { + private static final Logger LOG = LoggerFactory + .getLogger(Job.class); + + private Configuration originalConfig = null; + private CommonRdbmsReader.Job commonRdbmsReaderJob; + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + int fetchSize = this.originalConfig.getInt(Constant.FETCH_SIZE, + Integer.MIN_VALUE); + this.originalConfig.set(Constant.FETCH_SIZE, fetchSize); + + this.commonRdbmsReaderJob = new CommonRdbmsReader.Job(DATABASE_TYPE); + this.commonRdbmsReaderJob.init(this.originalConfig); + } + + @Override + public void preCheck(){ + init(); + this.commonRdbmsReaderJob.preCheck(this.originalConfig,DATABASE_TYPE); + + } + + @Override + public void prepare() { + } + + @Override + public List split(int adviceNumber) { + LOG.info("split() begin..."); + List splitResult = this.commonRdbmsReaderJob.split(this.originalConfig, adviceNumber); + /** + * 在日志中告知用户,为什么实际datax切分跑的channel数会小于用户配置的channel数 + */ + if(splitResult.size() < adviceNumber){ + // 如果用户没有配置切分主键splitPk + if(StringUtils.isBlank(this.originalConfig.getString(Key.SPLIT_PK, null))){ + LOG.info("User has not configured splitPk."); + }else{ + // 用户配置了切分主键,但是切分主键可能重复太多,或者要同步的表的记录太少,无法切分成adviceNumber个task + LOG.info("User has configured splitPk. But the number of task finally split is smaller than that user has configured. " + + "The possible reasons are: 1) too many repeated splitPk values, 2) too few records."); + } + } + LOG.info("split() ok and end..."); + return splitResult; + } + + @Override + public void post() { + this.commonRdbmsReaderJob.post(this.originalConfig); + } + + @Override + public void destroy() { + this.commonRdbmsReaderJob.destroy(this.originalConfig); + } + + } + + public static class Task extends Reader.Task { + + private Configuration readerSliceConfig; + private CommonRdbmsReader.Task commonRdbmsReaderTask; + + @Override + public void init() { + this.readerSliceConfig = super.getPluginJobConf(); + this.commonRdbmsReaderTask = new CommonRdbmsReader.Task(DATABASE_TYPE, super.getTaskGroupId(), super.getTaskId()); + this.commonRdbmsReaderTask.init(this.readerSliceConfig); + + } + + @Override + public void startRead(RecordSender recordSender) { + int fetchSize = this.readerSliceConfig.getInt(Constant.FETCH_SIZE); + + this.commonRdbmsReaderTask.startRead(this.readerSliceConfig, recordSender, + super.getTaskPluginCollector(), fetchSize); + } + + @Override + public void post() { + this.commonRdbmsReaderTask.post(this.readerSliceConfig); + } + + @Override + public void destroy() { + this.commonRdbmsReaderTask.destroy(this.readerSliceConfig); + } + + } +} diff --git a/starrocksreader/src/main/resources/plugin.json b/starrocksreader/src/main/resources/plugin.json new file mode 100644 index 0000000000..b0d6e0395e --- /dev/null +++ b/starrocksreader/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "starrocksreader", + "class": "com.alibaba.datax.plugin.reader.starrocksreader.StarRocksReader", + "description": "useScene: prod. mechanism: Jdbc connection using the database, execute select sql, retrieve data from the ResultSet. warn: The more you know about the database, the less problems you encounter.", + "developer": "alibaba" +} \ No newline at end of file diff --git a/starrockswriter/doc/starrockswriter.md b/starrockswriter/doc/starrockswriter.md new file mode 100644 index 0000000000..6ebe36816e --- /dev/null +++ b/starrockswriter/doc/starrockswriter.md @@ -0,0 +1,222 @@ +# DataX StarRocksWriter + + +--- + + +## 1 快速介绍 + +StarRocksWriter 插件实现了写入数据到 StarRocks 主库的目的表的功能。在底层实现上, StarRocksWriter 通过Streamload以csv格式导入数据至StarRocks。 + + +## 2 实现原理 + + StarRocksWriter 通过Streamload以csv格式导入数据至StarRocks, 内部将`reader`读取的数据进行缓存后批量导入至StarRocks,以提高写入性能。 + + +## 3 功能说明 + +### 3.1 配置样例 + +* 这里使用一份从内存Mysql读取数据后导入至StarRocks。 + +```json +{ + "job": { + "setting": { + "speed": { + "channel": 1 + }, + "errorLimit": { + "record": 0, + "percentage": 0 + } + }, + "content": [ + { + "reader": { + "name": "mysqlreader", + "parameter": { + "username": "xxxx", + "password": "xxxx", + "column": [ "k1", "k2", "v1", "v2" ], + "connection": [ + { + "table": [ "table1", "table2" ], + "jdbcUrl": [ + "jdbc:mysql://127.0.0.1:3306/datax_test1" + ] + }, + { + "table": [ "table3", "table4" ], + "jdbcUrl": [ + "jdbc:mysql://127.0.0.1:3306/datax_test2" + ] + } + ] + } + }, + "writer": { + "name": "starrockswriter", + "parameter": { + "username": "xxxx", + "password": "xxxx", + "column": ["k1", "k2", "v1", "v2"], + "preSql": [], + "postSql": [], + "connection": [ + { + "table": ["xxx"], + "jdbcUrl": "jdbc:mysql://172.28.17.100:9030/", + "selectedDatabase": "xxxx" + } + ], + "loadUrl": ["172.28.17.100:8030", "172.28.17.100:8030"], + "loadProps": {} + } + } + } + ] + } +} + +``` + + +### 3.2 参数说明 + +* **username** + + * 描述:StarRocks数据库的用户名
    + + * 必选:是
    + + * 默认值:无
    + +* **password** + + * 描述:StarRocks数据库的密码
    + + * 必选:是
    + + * 默认值:无
    + +* **selectedDatabase** + + * 描述:StarRocks表的数据库名称。 + + * 必选:是
    + + * 默认值:无
    + +* **table** + + * 描述:StarRocks表的表名称。 + + * 必选:是
    + + * 默认值:无
    + +* **loadUrl** + + * 描述:StarRocks FE的地址用于Streamload,可以为多个fe地址,`fe_ip:fe_http_port`。 + + * 必选:是
    + + * 默认值:无
    + +* **column** + + * 描述:目的表需要写入数据的字段,字段之间用英文逗号分隔。例如: "column": ["id","name","age"]。 + + **column配置项必须指定,不能留空!** + + 注意:我们强烈不推荐你这样配置,因为当你目的表字段个数、类型等有改动时,你的任务可能运行不正确或者失败 + + * 必选:是
    + + * 默认值:否
    + +* **preSql** + + * 描述:写入数据到目的表前,会先执行这里的标准语句。
    + + * 必选:否
    + + * 默认值:无
    + +* **postSql** + + * 描述:写入数据到目的表后,会执行这里的标准语句。
    + + * 必选:否
    + + * 默认值:无
    + +* **jdbcUrl** + + * 描述:目的数据库的 JDBC 连接信息,用于执行`preSql`及`postSql`。
    + + * 必选:否
    + + * 默认值:无
    + +* **maxBatchRows** + + * 描述:单次StreamLoad导入的最大行数
    + + * 必选:否
    + + * 默认值:500000 (50W)
    + +* **maxBatchSize** + + * 描述:单次StreamLoad导入的最大字节数。
    + + * 必选:否
    + + * 默认值:104857600 (100M) + +* **flushInterval** + + * 描述:上一次StreamLoad结束至下一次开始的时间间隔(单位:ms)。
    + + * 必选:否
    + + * 默认值:300000 (ms) + +* **loadProps** + + * 描述:StreamLoad 的请求参数,详情参照StreamLoad介绍页面。
    + + * 必选:否
    + + * 默认值:无
    + + +### 3.3 类型转换 + +默认传入的数据均会被转为字符串,并以`\t`作为列分隔符,`\n`作为行分隔符,组成`csv`文件进行StreamLoad导入操作。 +如需更改列分隔符, 则正确配置 `loadProps` 即可: +```json +"loadProps": { + "column_separator": "\\x01", + "row_delimiter": "\\x02" +} +``` + +如需更改导入格式为`json`, 则正确配置 `loadProps` 即可: +```json +"loadProps": { + "format": "json", + "strip_outer_array": true +} +``` + +## 4 性能报告 + + +## 5 约束限制 + + +## FAQ diff --git a/starrockswriter/pom.xml b/starrockswriter/pom.xml new file mode 100755 index 0000000000..73a5142240 --- /dev/null +++ b/starrockswriter/pom.xml @@ -0,0 +1,155 @@ + + 4.0.0 + + com.alibaba.datax + datax-all + 0.0.1-SNAPSHOT + + starrockswriter + starrockswriter + 1.1.0 + jar + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + commons-codec + commons-codec + 1.9 + + + org.apache.commons + commons-lang3 + 3.12.0 + + + commons-logging + commons-logging + 1.1.1 + + + org.apache.httpcomponents + httpcore + 4.4.6 + + + org.apache.httpcomponents + httpclient + 4.5.3 + + + com.alibaba.fastjson2 + fastjson2 + + + mysql + mysql-connector-java + 5.1.46 + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + org.apache.maven.plugins + maven-shade-plugin + 3.0.0 + + + + package + + shade + + + true + + + org.apache.http + com.starrocks.shade.org.apache.http + + + org.apache.commons + com.starrocks.shade.org.apache.commons + + + + + org.apache.commons:commons-lang3 + commons-codec:commons-codec + commons-logging:* + org.apache.httpcomponents:httpclient + org.apache.httpcomponents:httpcore + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + diff --git a/starrockswriter/src/main/assembly/package.xml b/starrockswriter/src/main/assembly/package.xml new file mode 100755 index 0000000000..c63845b471 --- /dev/null +++ b/starrockswriter/src/main/assembly/package.xml @@ -0,0 +1,35 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/writer/starrockswriter + + + target/ + + starrockswriter-1.1.0.jar + + plugin/writer/starrockswriter + + + + + + false + plugin/writer/starrockswriter/libs + runtime + + + diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/StarRocksWriter.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/StarRocksWriter.java new file mode 100755 index 0000000000..d5f2887a49 --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/StarRocksWriter.java @@ -0,0 +1,151 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.starrocks.connector.datax.plugin.writer.starrockswriter.manager.StarRocksWriterManager; +import com.starrocks.connector.datax.plugin.writer.starrockswriter.row.StarRocksISerializer; +import com.starrocks.connector.datax.plugin.writer.starrockswriter.row.StarRocksSerializerFactory; +import com.starrocks.connector.datax.plugin.writer.starrockswriter.util.StarRocksWriterUtil; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.List; + +public class StarRocksWriter extends Writer { + + public static class Job extends Writer.Job { + + private static final Logger LOG = LoggerFactory.getLogger(Job.class); + private Configuration originalConfig = null; + private StarRocksWriterOptions options; + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + String selectedDatabase = super.getPluginJobConf().getString(StarRocksWriterOptions.KEY_SELECTED_DATABASE); + if(StringUtils.isBlank(this.originalConfig.getString(StarRocksWriterOptions.KEY_DATABASE)) && StringUtils.isNotBlank(selectedDatabase)){ + this.originalConfig.set(StarRocksWriterOptions.KEY_DATABASE, selectedDatabase); + } + options = new StarRocksWriterOptions(super.getPluginJobConf()); + options.doPretreatment(); + } + + @Override + public void preCheck(){ + this.init(); + StarRocksWriterUtil.preCheckPrePareSQL(options); + StarRocksWriterUtil.preCheckPostSQL(options); + } + + @Override + public void prepare() { + String username = options.getUsername(); + String password = options.getPassword(); + String jdbcUrl = options.getJdbcUrl(); + List renderedPreSqls = StarRocksWriterUtil.renderPreOrPostSqls(options.getPreSqlList(), options.getTable()); + if (null != renderedPreSqls && !renderedPreSqls.isEmpty()) { + Connection conn = DBUtil.getConnection(DataBaseType.MySql, jdbcUrl, username, password); + LOG.info("Begin to execute preSqls:[{}]. context info:{}.", String.join(";", renderedPreSqls), jdbcUrl); + StarRocksWriterUtil.executeSqls(conn, renderedPreSqls); + DBUtil.closeDBResources(null, null, conn); + } + } + + @Override + public List split(int mandatoryNumber) { + List configurations = new ArrayList<>(mandatoryNumber); + for (int i = 0; i < mandatoryNumber; i++) { + configurations.add(originalConfig); + } + return configurations; + } + + @Override + public void post() { + String username = options.getUsername(); + String password = options.getPassword(); + String jdbcUrl = options.getJdbcUrl(); + List renderedPostSqls = StarRocksWriterUtil.renderPreOrPostSqls(options.getPostSqlList(), options.getTable()); + if (null != renderedPostSqls && !renderedPostSqls.isEmpty()) { + Connection conn = DBUtil.getConnection(DataBaseType.MySql, jdbcUrl, username, password); + LOG.info("Begin to execute postSqls:[{}]. context info:{}.", String.join(";", renderedPostSqls), jdbcUrl); + StarRocksWriterUtil.executeSqls(conn, renderedPostSqls); + DBUtil.closeDBResources(null, null, conn); + } + } + + @Override + public void destroy() { + } + + } + + public static class Task extends Writer.Task { + private StarRocksWriterManager writerManager; + private StarRocksWriterOptions options; + private StarRocksISerializer rowSerializer; + + @Override + public void init() { + options = new StarRocksWriterOptions(super.getPluginJobConf()); + if (options.isWildcardColumn()) { + Connection conn = DBUtil.getConnection(DataBaseType.MySql, options.getJdbcUrl(), options.getUsername(), options.getPassword()); + List columns = StarRocksWriterUtil.getStarRocksColumns(conn, options.getDatabase(), options.getTable()); + options.setInfoCchemaColumns(columns); + } + writerManager = new StarRocksWriterManager(options); + rowSerializer = StarRocksSerializerFactory.createSerializer(options); + } + + @Override + public void prepare() { + } + + public void startWrite(RecordReceiver recordReceiver) { + try { + Record record; + while ((record = recordReceiver.getFromReader()) != null) { + if (record.getColumnNumber() != options.getColumns().size()) { + throw DataXException + .asDataXException( + DBUtilErrorCode.CONF_ERROR, + String.format( + "Column configuration error. The number of reader columns %d and the number of writer columns %d are not equal.", + record.getColumnNumber(), + options.getColumns().size())); + } + writerManager.writeRecord(rowSerializer.serialize(record)); + } + } catch (Exception e) { + throw DataXException.asDataXException(DBUtilErrorCode.WRITE_DATA_ERROR, e); + } + } + + @Override + public void post() { + try { + writerManager.close(); + } catch (Exception e) { + throw DataXException.asDataXException(DBUtilErrorCode.WRITE_DATA_ERROR, e); + } + } + + @Override + public void destroy() {} + + @Override + public boolean supportFailOver(){ + return false; + } + } +} diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/StarRocksWriterOptions.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/StarRocksWriterOptions.java new file mode 100644 index 0000000000..5c6ddacde5 --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/StarRocksWriterOptions.java @@ -0,0 +1,199 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter; + +import java.io.Serializable; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DBUtilErrorCode; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class StarRocksWriterOptions implements Serializable { + + private static final long serialVersionUID = 1l; + private static final long KILO_BYTES_SCALE = 1024l; + private static final long MEGA_BYTES_SCALE = KILO_BYTES_SCALE * KILO_BYTES_SCALE; + private static final int MAX_RETRIES = 1; + private static final int BATCH_ROWS = 500000; + private static final long BATCH_BYTES = 5 * MEGA_BYTES_SCALE; + private static final long FLUSH_INTERVAL = 300000; + + private static final String KEY_LOAD_PROPS_FORMAT = "format"; + public enum StreamLoadFormat { + CSV, JSON; + } + + public static final String KEY_USERNAME = "username"; + public static final String KEY_PASSWORD = "password"; + public static final String KEY_DATABASE = "database"; + public static final String KEY_SELECTED_DATABASE = "selectedDatabase"; + public static final String KEY_TABLE = "table"; + public static final String KEY_COLUMN = "column"; + public static final String KEY_PRE_SQL = "preSql"; + public static final String KEY_POST_SQL = "postSql"; + public static final String KEY_JDBC_URL = "jdbcUrl"; + public static final String KEY_LABEL_PREFIX = "labelPrefix"; + public static final String KEY_MAX_BATCH_ROWS = "maxBatchRows"; + public static final String KEY_MAX_BATCH_SIZE = "maxBatchSize"; + public static final String KEY_FLUSH_INTERVAL = "flushInterval"; + public static final String KEY_LOAD_URL = "loadUrl"; + public static final String KEY_FLUSH_QUEUE_LENGTH = "flushQueueLength"; + public static final String KEY_LOAD_PROPS = "loadProps"; + public static final String CONNECTION_JDBC_URL = "connection[0].jdbcUrl"; + public static final String CONNECTION_TABLE_NAME = "connection[0].table[0]"; + public static final String CONNECTION_SELECTED_DATABASE = "connection[0].selectedDatabase"; + + private final Configuration options; + private List infoCchemaColumns; + private List userSetColumns; + private boolean isWildcardColumn; + + public StarRocksWriterOptions(Configuration options) { + this.options = options; + // database + String database = this.options.getString(CONNECTION_SELECTED_DATABASE); + if (StringUtils.isBlank(database)) { + database = this.options.getString(KEY_SELECTED_DATABASE); + } + if (StringUtils.isNotBlank(database)) { + this.options.set(KEY_DATABASE, database); + } + // jdbcUrl + String jdbcUrl = this.options.getString(CONNECTION_JDBC_URL); + if (StringUtils.isNotBlank(jdbcUrl)) { + this.options.set(KEY_JDBC_URL, jdbcUrl); + } + // table + String table = this.options.getString(CONNECTION_TABLE_NAME); + if (StringUtils.isNotBlank(table)) { + this.options.set(KEY_TABLE, table); + } + // column + this.userSetColumns = options.getList(KEY_COLUMN, String.class).stream().map(str -> str.replace("`", "")).collect(Collectors.toList()); + if (1 == options.getList(KEY_COLUMN, String.class).size() && "*".trim().equals(options.getList(KEY_COLUMN, String.class).get(0))) { + this.isWildcardColumn = true; + } + } + + public void doPretreatment() { + validateRequired(); + validateStreamLoadUrl(); + } + + public String getJdbcUrl() { + return options.getString(KEY_JDBC_URL); + } + + public String getDatabase() { + return options.getString(KEY_DATABASE); + } + + public String getTable() { + return options.getString(KEY_TABLE); + } + + public String getUsername() { + return options.getString(KEY_USERNAME); + } + + public String getPassword() { + return options.getString(KEY_PASSWORD); + } + + public String getLabelPrefix() { + return options.getString(KEY_LABEL_PREFIX); + } + + public List getLoadUrlList() { + return options.getList(KEY_LOAD_URL, String.class); + } + + public List getColumns() { + if (isWildcardColumn) { + return this.infoCchemaColumns; + } + return this.userSetColumns; + } + + public boolean isWildcardColumn() { + return this.isWildcardColumn; + } + + public void setInfoCchemaColumns(List cols) { + this.infoCchemaColumns = cols; + } + + public List getPreSqlList() { + return options.getList(KEY_PRE_SQL, String.class); + } + + public List getPostSqlList() { + return options.getList(KEY_POST_SQL, String.class); + } + + public Map getLoadProps() { + return options.getMap(KEY_LOAD_PROPS); + } + + public int getMaxRetries() { + return MAX_RETRIES; + } + + public int getBatchRows() { + Integer rows = options.getInt(KEY_MAX_BATCH_ROWS); + return null == rows ? BATCH_ROWS : rows; + } + + public long getBatchSize() { + Long size = options.getLong(KEY_MAX_BATCH_SIZE); + return null == size ? BATCH_BYTES : size; + } + + public long getFlushInterval() { + Long interval = options.getLong(KEY_FLUSH_INTERVAL); + return null == interval ? FLUSH_INTERVAL : interval; + } + + public int getFlushQueueLength() { + Integer len = options.getInt(KEY_FLUSH_QUEUE_LENGTH); + return null == len ? 1 : len; + } + + public StreamLoadFormat getStreamLoadFormat() { + Map loadProps = getLoadProps(); + if (null == loadProps) { + return StreamLoadFormat.CSV; + } + if (loadProps.containsKey(KEY_LOAD_PROPS_FORMAT) + && StreamLoadFormat.JSON.name().equalsIgnoreCase(String.valueOf(loadProps.get(KEY_LOAD_PROPS_FORMAT)))) { + return StreamLoadFormat.JSON; + } + return StreamLoadFormat.CSV; + } + + private void validateStreamLoadUrl() { + List urlList = getLoadUrlList(); + for (String host : urlList) { + if (host.split(":").length < 2) { + throw DataXException.asDataXException(DBUtilErrorCode.CONF_ERROR, + "The format of loadUrl is illegal, please input `fe_ip:fe_http_ip;fe_ip:fe_http_ip`."); + } + } + } + + private void validateRequired() { + final String[] requiredOptionKeys = new String[]{ + KEY_USERNAME, + KEY_DATABASE, + KEY_TABLE, + KEY_COLUMN, + KEY_LOAD_URL + }; + for (String optionKey : requiredOptionKeys) { + options.getNecessaryValue(optionKey, DBUtilErrorCode.REQUIRED_VALUE); + } + } +} diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/manager/StarRocksFlushTuple.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/manager/StarRocksFlushTuple.java new file mode 100644 index 0000000000..5c939f9b08 --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/manager/StarRocksFlushTuple.java @@ -0,0 +1,21 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter.manager; + +import java.util.List; + +public class StarRocksFlushTuple { + + private String label; + private Long bytes; + private List rows; + + public StarRocksFlushTuple(String label, Long bytes, List rows) { + this.label = label; + this.bytes = bytes; + this.rows = rows; + } + + public String getLabel() { return label; } + public void setLabel(String label) { this.label = label; } + public Long getBytes() { return bytes; } + public List getRows() { return rows; } +} \ No newline at end of file diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/manager/StarRocksStreamLoadFailedException.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/manager/StarRocksStreamLoadFailedException.java new file mode 100644 index 0000000000..4eb4704859 --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/manager/StarRocksStreamLoadFailedException.java @@ -0,0 +1,33 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter.manager; + +import java.io.IOException; +import java.util.Map; + + +public class StarRocksStreamLoadFailedException extends IOException { + + static final long serialVersionUID = 1L; + + private final Map response; + private boolean reCreateLabel; + + public StarRocksStreamLoadFailedException(String message, Map response) { + super(message); + this.response = response; + } + + public StarRocksStreamLoadFailedException(String message, Map response, boolean reCreateLabel) { + super(message); + this.response = response; + this.reCreateLabel = reCreateLabel; + } + + public Map getFailedResponse() { + return response; + } + + public boolean needReCreateLabel() { + return reCreateLabel; + } + +} diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/manager/StarRocksStreamLoadVisitor.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/manager/StarRocksStreamLoadVisitor.java new file mode 100644 index 0000000000..b3671556a2 --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/manager/StarRocksStreamLoadVisitor.java @@ -0,0 +1,304 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter.manager; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import com.alibaba.fastjson2.JSON; +import com.starrocks.connector.datax.plugin.writer.starrockswriter.StarRocksWriterOptions; +import com.starrocks.connector.datax.plugin.writer.starrockswriter.row.StarRocksDelimiterParser; + +import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpEntity; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultRedirectStrategy; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + + +public class StarRocksStreamLoadVisitor { + + private static final Logger LOG = LoggerFactory.getLogger(StarRocksStreamLoadVisitor.class); + + private final StarRocksWriterOptions writerOptions; + private long pos; + private static final String RESULT_FAILED = "Fail"; + private static final String RESULT_LABEL_EXISTED = "Label Already Exists"; + private static final String LAEBL_STATE_VISIBLE = "VISIBLE"; + private static final String LAEBL_STATE_COMMITTED = "COMMITTED"; + private static final String RESULT_LABEL_PREPARE = "PREPARE"; + private static final String RESULT_LABEL_ABORTED = "ABORTED"; + private static final String RESULT_LABEL_UNKNOWN = "UNKNOWN"; + + public StarRocksStreamLoadVisitor(StarRocksWriterOptions writerOptions) { + this.writerOptions = writerOptions; + } + + public void doStreamLoad(StarRocksFlushTuple flushData) throws IOException { + String host = getAvailableHost(); + if (null == host) { + throw new IOException("None of the host in `load_url` could be connected."); + } + String loadUrl = new StringBuilder(host) + .append("/api/") + .append(writerOptions.getDatabase()) + .append("/") + .append(writerOptions.getTable()) + .append("/_stream_load") + .toString(); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Start to join batch data: rows[%d] bytes[%d] label[%s].", flushData.getRows().size(), flushData.getBytes(), flushData.getLabel())); + } + Map loadResult = doHttpPut(loadUrl, flushData.getLabel(), joinRows(flushData.getRows(), flushData.getBytes().intValue())); + final String keyStatus = "Status"; + if (null == loadResult || !loadResult.containsKey(keyStatus)) { + LOG.error("unknown result status. {}", loadResult); + throw new IOException("Unable to flush data to StarRocks: unknown result status. " + loadResult); + } + if (LOG.isDebugEnabled()) { + LOG.debug(new StringBuilder("StreamLoad response:\n").append(JSON.toJSONString(loadResult)).toString()); + } + if (RESULT_FAILED.equals(loadResult.get(keyStatus))) { + StringBuilder errorBuilder = new StringBuilder("Failed to flush data to StarRocks.\n"); + if (loadResult.containsKey("Message")) { + errorBuilder.append(loadResult.get("Message")); + errorBuilder.append('\n'); + } + if (loadResult.containsKey("ErrorURL")) { + LOG.error("StreamLoad response: {}", loadResult); + try { + errorBuilder.append(doHttpGet(loadResult.get("ErrorURL").toString())); + errorBuilder.append('\n'); + } catch (IOException e) { + LOG.warn("Get Error URL failed. {} ", loadResult.get("ErrorURL"), e); + } + } else { + errorBuilder.append(JSON.toJSONString(loadResult)); + errorBuilder.append('\n'); + } + throw new IOException(errorBuilder.toString()); + } else if (RESULT_LABEL_EXISTED.equals(loadResult.get(keyStatus))) { + LOG.debug(new StringBuilder("StreamLoad response:\n").append(JSON.toJSONString(loadResult)).toString()); + // has to block-checking the state to get the final result + checkLabelState(host, flushData.getLabel()); + } + } + + private String getAvailableHost() { + List hostList = writerOptions.getLoadUrlList(); + long tmp = pos + hostList.size(); + for (; pos < tmp; pos++) { + String host = new StringBuilder("http://").append(hostList.get((int) (pos % hostList.size()))).toString(); + if (tryHttpConnection(host)) { + return host; + } + } + return null; + } + + private boolean tryHttpConnection(String host) { + try { + URL url = new URL(host); + HttpURLConnection co = (HttpURLConnection) url.openConnection(); + co.setConnectTimeout(1000); + co.connect(); + co.disconnect(); + return true; + } catch (Exception e1) { + LOG.warn("Failed to connect to address:{}", host, e1); + return false; + } + } + + private byte[] joinRows(List rows, int totalBytes) { + if (StarRocksWriterOptions.StreamLoadFormat.CSV.equals(writerOptions.getStreamLoadFormat())) { + Map props = (writerOptions.getLoadProps() == null ? new HashMap<>() : writerOptions.getLoadProps()); + byte[] lineDelimiter = StarRocksDelimiterParser.parse((String)props.get("row_delimiter"), "\n").getBytes(StandardCharsets.UTF_8); + ByteBuffer bos = ByteBuffer.allocate(totalBytes + rows.size() * lineDelimiter.length); + for (byte[] row : rows) { + bos.put(row); + bos.put(lineDelimiter); + } + return bos.array(); + } + + if (StarRocksWriterOptions.StreamLoadFormat.JSON.equals(writerOptions.getStreamLoadFormat())) { + ByteBuffer bos = ByteBuffer.allocate(totalBytes + (rows.isEmpty() ? 2 : rows.size() + 1)); + bos.put("[".getBytes(StandardCharsets.UTF_8)); + byte[] jsonDelimiter = ",".getBytes(StandardCharsets.UTF_8); + boolean isFirstElement = true; + for (byte[] row : rows) { + if (!isFirstElement) { + bos.put(jsonDelimiter); + } + bos.put(row); + isFirstElement = false; + } + bos.put("]".getBytes(StandardCharsets.UTF_8)); + return bos.array(); + } + throw new RuntimeException("Failed to join rows data, unsupported `format` from stream load properties:"); + } + + @SuppressWarnings("unchecked") + private void checkLabelState(String host, String label) throws IOException { + int idx = 0; + while(true) { + try { + TimeUnit.SECONDS.sleep(Math.min(++idx, 5)); + } catch (InterruptedException ex) { + break; + } + try (CloseableHttpClient httpclient = HttpClients.createDefault()) { + HttpGet httpGet = new HttpGet(new StringBuilder(host).append("/api/").append(writerOptions.getDatabase()).append("/get_load_state?label=").append(label).toString()); + httpGet.setHeader("Authorization", getBasicAuthHeader(writerOptions.getUsername(), writerOptions.getPassword())); + httpGet.setHeader("Connection", "close"); + + try (CloseableHttpResponse resp = httpclient.execute(httpGet)) { + HttpEntity respEntity = getHttpEntity(resp); + if (respEntity == null) { + throw new IOException(String.format("Failed to flush data to StarRocks, Error " + + "could not get the final state of label[%s].\n", label), null); + } + Map result = (Map)JSON.parse(EntityUtils.toString(respEntity)); + String labelState = (String)result.get("state"); + if (null == labelState) { + throw new IOException(String.format("Failed to flush data to StarRocks, Error " + + "could not get the final state of label[%s]. response[%s]\n", label, EntityUtils.toString(respEntity)), null); + } + LOG.info(String.format("Checking label[%s] state[%s]\n", label, labelState)); + switch(labelState) { + case LAEBL_STATE_VISIBLE: + case LAEBL_STATE_COMMITTED: + return; + case RESULT_LABEL_PREPARE: + continue; + case RESULT_LABEL_ABORTED: + throw new StarRocksStreamLoadFailedException(String.format("Failed to flush data to StarRocks, Error " + + "label[%s] state[%s]\n", label, labelState), null, true); + case RESULT_LABEL_UNKNOWN: + default: + throw new IOException(String.format("Failed to flush data to StarRocks, Error " + + "label[%s] state[%s]\n", label, labelState), null); + } + } + } + } + } + + @SuppressWarnings("unchecked") + private Map doHttpPut(String loadUrl, String label, byte[] data) throws IOException { + LOG.info(String.format("Executing stream load to: '%s', size: '%s'", loadUrl, data.length)); + final HttpClientBuilder httpClientBuilder = HttpClients.custom() + .setRedirectStrategy(new DefaultRedirectStrategy() { + @Override + protected boolean isRedirectable(String method) { + return true; + } + }); + try (CloseableHttpClient httpclient = httpClientBuilder.build()) { + HttpPut httpPut = new HttpPut(loadUrl); + List cols = writerOptions.getColumns(); + if (null != cols && !cols.isEmpty() && StarRocksWriterOptions.StreamLoadFormat.CSV.equals(writerOptions.getStreamLoadFormat())) { + httpPut.setHeader("columns", String.join(",", cols.stream().map(f -> String.format("`%s`", f)).collect(Collectors.toList()))); + } + if (null != writerOptions.getLoadProps()) { + for (Map.Entry entry : writerOptions.getLoadProps().entrySet()) { + httpPut.setHeader(entry.getKey(), String.valueOf(entry.getValue())); + } + } + httpPut.setHeader("Expect", "100-continue"); + httpPut.setHeader("label", label); + httpPut.setHeader("Content-Type", "application/x-www-form-urlencoded"); + httpPut.setHeader("Authorization", getBasicAuthHeader(writerOptions.getUsername(), writerOptions.getPassword())); + httpPut.setEntity(new ByteArrayEntity(data)); + httpPut.setConfig(RequestConfig.custom().setRedirectsEnabled(true).build()); + try (CloseableHttpResponse resp = httpclient.execute(httpPut)) { + int code = resp.getStatusLine().getStatusCode(); + if (200 != code) { + String errorText; + try { + HttpEntity respEntity = resp.getEntity(); + errorText = EntityUtils.toString(respEntity); + } catch (Exception err) { + errorText = "find errorText failed: " + err.getMessage(); + } + LOG.warn("Request failed with code:{}, err:{}", code, errorText); + Map errorMap = new HashMap<>(); + errorMap.put("Status", "Fail"); + errorMap.put("Message", errorText); + return errorMap; + } + HttpEntity respEntity = resp.getEntity(); + if (null == respEntity) { + LOG.warn("Request failed with empty response."); + return null; + } + return (Map)JSON.parse(EntityUtils.toString(respEntity)); + } + } + } + + private String getBasicAuthHeader(String username, String password) { + String auth = username + ":" + password; + byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.UTF_8)); + return new StringBuilder("Basic ").append(new String(encodedAuth)).toString(); + } + + private HttpEntity getHttpEntity(CloseableHttpResponse resp) { + int code = resp.getStatusLine().getStatusCode(); + if (200 != code) { + LOG.warn("Request failed with code:{}", code); + return null; + } + HttpEntity respEntity = resp.getEntity(); + if (null == respEntity) { + LOG.warn("Request failed with empty response."); + return null; + } + return respEntity; + } + + private String doHttpGet(String getUrl) throws IOException { + LOG.info("Executing GET from {}.", getUrl); + try (CloseableHttpClient httpclient = buildHttpClient()) { + HttpGet httpGet = new HttpGet(getUrl); + try (CloseableHttpResponse resp = httpclient.execute(httpGet)) { + HttpEntity respEntity = resp.getEntity(); + if (null == respEntity) { + LOG.warn("Request failed with empty response."); + return null; + } + return EntityUtils.toString(respEntity); + } + } + } + + private CloseableHttpClient buildHttpClient(){ + final HttpClientBuilder httpClientBuilder = HttpClients.custom() + .setRedirectStrategy(new DefaultRedirectStrategy() { + @Override + protected boolean isRedirectable(String method) { + return true; + } + }); + return httpClientBuilder.build(); + } + +} diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/manager/StarRocksWriterManager.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/manager/StarRocksWriterManager.java new file mode 100644 index 0000000000..a0cb1f8bd3 --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/manager/StarRocksWriterManager.java @@ -0,0 +1,203 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter.manager; + +import org.apache.commons.lang3.concurrent.BasicThreadFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import com.google.common.base.Strings; +import com.starrocks.connector.datax.plugin.writer.starrockswriter.StarRocksWriterOptions; + +public class StarRocksWriterManager { + + private static final Logger LOG = LoggerFactory.getLogger(StarRocksWriterManager.class); + + private final StarRocksStreamLoadVisitor starrocksStreamLoadVisitor; + private final StarRocksWriterOptions writerOptions; + + private final List buffer = new ArrayList<>(); + private int batchCount = 0; + private long batchSize = 0; + private volatile boolean closed = false; + private volatile Exception flushException; + private final LinkedBlockingDeque flushQueue; + private ScheduledExecutorService scheduler; + private ScheduledFuture scheduledFuture; + + public StarRocksWriterManager(StarRocksWriterOptions writerOptions) { + this.writerOptions = writerOptions; + this.starrocksStreamLoadVisitor = new StarRocksStreamLoadVisitor(writerOptions); + flushQueue = new LinkedBlockingDeque<>(writerOptions.getFlushQueueLength()); + this.startScheduler(); + this.startAsyncFlushing(); + } + + public void startScheduler() { + stopScheduler(); + this.scheduler = Executors.newScheduledThreadPool(1, new BasicThreadFactory.Builder().namingPattern("starrocks-interval-flush").daemon(true).build()); + this.scheduledFuture = this.scheduler.schedule(() -> { + synchronized (StarRocksWriterManager.this) { + if (!closed) { + try { + String label = createBatchLabel(); + LOG.info(String.format("StarRocks interval Sinking triggered: label[%s].", label)); + if (batchCount == 0) { + startScheduler(); + } + flush(label, false); + } catch (Exception e) { + flushException = e; + } + } + } + }, writerOptions.getFlushInterval(), TimeUnit.MILLISECONDS); + } + + public void stopScheduler() { + if (this.scheduledFuture != null) { + scheduledFuture.cancel(false); + this.scheduler.shutdown(); + } + } + + public final synchronized void writeRecord(String record) throws IOException { + checkFlushException(); + try { + byte[] bts = record.getBytes(StandardCharsets.UTF_8); + buffer.add(bts); + batchCount++; + batchSize += bts.length; + if (batchCount >= writerOptions.getBatchRows() || batchSize >= writerOptions.getBatchSize()) { + String label = createBatchLabel(); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("StarRocks buffer Sinking triggered: rows[%d] label[%s].", batchCount, label)); + } + flush(label, false); + } + } catch (Exception e) { + throw new IOException("Writing records to StarRocks failed.", e); + } + } + + public synchronized void flush(String label, boolean waitUtilDone) throws Exception { + checkFlushException(); + if (batchCount == 0) { + if (waitUtilDone) { + waitAsyncFlushingDone(); + } + return; + } + flushQueue.put(new StarRocksFlushTuple(label, batchSize, new ArrayList<>(buffer))); + if (waitUtilDone) { + // wait the last flush + waitAsyncFlushingDone(); + } + buffer.clear(); + batchCount = 0; + batchSize = 0; + } + + public synchronized void close() { + if (!closed) { + closed = true; + try { + String label = createBatchLabel(); + if (batchCount > 0) { + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("StarRocks Sink is about to close: label[%s].", label)); + } + } + flush(label, true); + } catch (Exception e) { + throw new RuntimeException("Writing records to StarRocks failed.", e); + } + } + checkFlushException(); + } + + public String createBatchLabel() { + StringBuilder sb = new StringBuilder(); + if (!Strings.isNullOrEmpty(writerOptions.getLabelPrefix())) { + sb.append(writerOptions.getLabelPrefix()); + } + return sb.append(UUID.randomUUID().toString()) + .toString(); + } + + private void startAsyncFlushing() { + // start flush thread + Thread flushThread = new Thread(new Runnable(){ + public void run() { + while(true) { + try { + asyncFlush(); + } catch (Exception e) { + flushException = e; + } + } + } + }); + flushThread.setDaemon(true); + flushThread.start(); + } + + private void waitAsyncFlushingDone() throws InterruptedException { + // wait previous flushings + for (int i = 0; i <= writerOptions.getFlushQueueLength(); i++) { + flushQueue.put(new StarRocksFlushTuple("", 0l, null)); + } + checkFlushException(); + } + + private void asyncFlush() throws Exception { + StarRocksFlushTuple flushData = flushQueue.take(); + if (Strings.isNullOrEmpty(flushData.getLabel())) { + return; + } + stopScheduler(); + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Async stream load: rows[%d] bytes[%d] label[%s].", flushData.getRows().size(), flushData.getBytes(), flushData.getLabel())); + } + for (int i = 0; i <= writerOptions.getMaxRetries(); i++) { + try { + // flush to StarRocks with stream load + starrocksStreamLoadVisitor.doStreamLoad(flushData); + LOG.info(String.format("Async stream load finished: label[%s].", flushData.getLabel())); + startScheduler(); + break; + } catch (Exception e) { + LOG.warn("Failed to flush batch data to StarRocks, retry times = {}", i, e); + if (i >= writerOptions.getMaxRetries()) { + throw new IOException(e); + } + if (e instanceof StarRocksStreamLoadFailedException && ((StarRocksStreamLoadFailedException)e).needReCreateLabel()) { + String newLabel = createBatchLabel(); + LOG.warn(String.format("Batch label changed from [%s] to [%s]", flushData.getLabel(), newLabel)); + flushData.setLabel(newLabel); + } + try { + Thread.sleep(1000l * Math.min(i + 1, 10)); + } catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + throw new IOException("Unable to flush, interrupted while doing another attempt", e); + } + } + } + } + + private void checkFlushException() { + if (flushException != null) { + throw new RuntimeException("Writing records to StarRocks failed.", flushException); + } + } +} diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksBaseSerializer.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksBaseSerializer.java new file mode 100644 index 0000000000..a7ad499d55 --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksBaseSerializer.java @@ -0,0 +1,26 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter.row; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Column.Type; + +public class StarRocksBaseSerializer { + + protected String fieldConvertion(Column col) { + if (null == col.getRawData() || Type.NULL == col.getType()) { + return null; + } + if (Type.BOOL == col.getType()) { + return String.valueOf(col.asLong()); + } + if (Type.BYTES == col.getType()) { + byte[] bts = (byte[])col.getRawData(); + long value = 0; + for (int i = 0; i < bts.length; i++) { + value += (bts[bts.length - i - 1] & 0xffL) << (8 * i); + } + return String.valueOf(value); + } + return col.asString(); + } + +} \ No newline at end of file diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksCsvSerializer.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksCsvSerializer.java new file mode 100644 index 0000000000..1366d57097 --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksCsvSerializer.java @@ -0,0 +1,32 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter.row; + +import java.io.StringWriter; + +import com.alibaba.datax.common.element.Record; + +import com.google.common.base.Strings; + +public class StarRocksCsvSerializer extends StarRocksBaseSerializer implements StarRocksISerializer { + + private static final long serialVersionUID = 1L; + + private final String columnSeparator; + + public StarRocksCsvSerializer(String sp) { + this.columnSeparator = StarRocksDelimiterParser.parse(sp, "\t"); + } + + @Override + public String serialize(Record row) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < row.getColumnNumber(); i++) { + String value = fieldConvertion(row.getColumn(i)); + sb.append(null == value ? "\\N" : value); + if (i < row.getColumnNumber() - 1) { + sb.append(columnSeparator); + } + } + return sb.toString(); + } + +} diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksDelimiterParser.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksDelimiterParser.java new file mode 100644 index 0000000000..04301e0f13 --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksDelimiterParser.java @@ -0,0 +1,55 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter.row; + +import java.io.StringWriter; + +import com.google.common.base.Strings; + +public class StarRocksDelimiterParser { + + private static final String HEX_STRING = "0123456789ABCDEF"; + + public static String parse(String sp, String dSp) throws RuntimeException { + if (Strings.isNullOrEmpty(sp)) { + return dSp; + } + if (!sp.toUpperCase().startsWith("\\X")) { + return sp; + } + String hexStr = sp.substring(2); + // check hex str + if (hexStr.isEmpty()) { + throw new RuntimeException("Failed to parse delimiter: `Hex str is empty`"); + } + if (hexStr.length() % 2 != 0) { + throw new RuntimeException("Failed to parse delimiter: `Hex str length error`"); + } + for (char hexChar : hexStr.toUpperCase().toCharArray()) { + if (HEX_STRING.indexOf(hexChar) == -1) { + throw new RuntimeException("Failed to parse delimiter: `Hex str format error`"); + } + } + // transform to separator + StringWriter writer = new StringWriter(); + for (byte b : hexStrToBytes(hexStr)) { + writer.append((char) b); + } + return writer.toString(); + } + + private static byte[] hexStrToBytes(String hexStr) { + String upperHexStr = hexStr.toUpperCase(); + int length = upperHexStr.length() / 2; + char[] hexChars = upperHexStr.toCharArray(); + byte[] bytes = new byte[length]; + for (int i = 0; i < length; i++) { + int pos = i * 2; + bytes[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1])); + } + return bytes; + } + + private static byte charToByte(char c) { + return (byte) HEX_STRING.indexOf(c); + } + +} diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksISerializer.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksISerializer.java new file mode 100644 index 0000000000..7bcb89739c --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksISerializer.java @@ -0,0 +1,11 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter.row; + +import java.io.Serializable; + +import com.alibaba.datax.common.element.Record; + +public interface StarRocksISerializer extends Serializable { + + String serialize(Record row); + +} diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksJsonSerializer.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksJsonSerializer.java new file mode 100644 index 0000000000..f235a08dc1 --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksJsonSerializer.java @@ -0,0 +1,34 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter.row; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.alibaba.datax.common.element.Record; +import com.alibaba.fastjson2.JSON; + +public class StarRocksJsonSerializer extends StarRocksBaseSerializer implements StarRocksISerializer { + + private static final long serialVersionUID = 1L; + + private final List fieldNames; + + public StarRocksJsonSerializer(List fieldNames) { + this.fieldNames = fieldNames; + } + + @Override + public String serialize(Record row) { + if (null == fieldNames) { + return ""; + } + Map rowMap = new HashMap<>(fieldNames.size()); + int idx = 0; + for (String fieldName : fieldNames) { + rowMap.put(fieldName, fieldConvertion(row.getColumn(idx))); + idx++; + } + return JSON.toJSONString(rowMap); + } + +} diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksSerializerFactory.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksSerializerFactory.java new file mode 100644 index 0000000000..85f446cd93 --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/row/StarRocksSerializerFactory.java @@ -0,0 +1,22 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter.row; + +import java.util.Map; + +import com.starrocks.connector.datax.plugin.writer.starrockswriter.StarRocksWriterOptions; + +public class StarRocksSerializerFactory { + + private StarRocksSerializerFactory() {} + + public static StarRocksISerializer createSerializer(StarRocksWriterOptions writerOptions) { + if (StarRocksWriterOptions.StreamLoadFormat.CSV.equals(writerOptions.getStreamLoadFormat())) { + Map props = writerOptions.getLoadProps(); + return new StarRocksCsvSerializer(null == props || !props.containsKey("column_separator") ? null : String.valueOf(props.get("column_separator"))); + } + if (StarRocksWriterOptions.StreamLoadFormat.JSON.equals(writerOptions.getStreamLoadFormat())) { + return new StarRocksJsonSerializer(writerOptions.getColumns()); + } + throw new RuntimeException("Failed to create row serializer, unsupported `format` from stream load properties."); + } + +} diff --git a/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/util/StarRocksWriterUtil.java b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/util/StarRocksWriterUtil.java new file mode 100755 index 0000000000..8de4ad60be --- /dev/null +++ b/starrockswriter/src/main/java/com/starrocks/connector/datax/plugin/writer/starrockswriter/util/StarRocksWriterUtil.java @@ -0,0 +1,102 @@ +package com.starrocks.connector.datax.plugin.writer.starrockswriter.util; + +import com.alibaba.datax.plugin.rdbms.util.DBUtil; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.util.RdbmsException; +import com.alibaba.datax.plugin.rdbms.writer.Constant; +import com.alibaba.druid.sql.parser.ParserException; +import com.starrocks.connector.datax.plugin.writer.starrockswriter.StarRocksWriterOptions; +import com.google.common.base.Strings; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.Statement; +import java.util.*; + +public final class StarRocksWriterUtil { + private static final Logger LOG = LoggerFactory.getLogger(StarRocksWriterUtil.class); + + private StarRocksWriterUtil() {} + + public static List getStarRocksColumns(Connection conn, String databaseName, String tableName) { + String currentSql = String.format("SELECT COLUMN_NAME FROM `information_schema`.`COLUMNS` WHERE `TABLE_SCHEMA` = '%s' AND `TABLE_NAME` = '%s' ORDER BY `ORDINAL_POSITION` ASC;", databaseName, tableName); + List columns = new ArrayList<>(); + ResultSet rs = null; + try { + rs = DBUtil.query(conn, currentSql); + while (DBUtil.asyncResultSetNext(rs)) { + String colName = rs.getString("COLUMN_NAME"); + columns.add(colName); + } + return columns; + } catch (Exception e) { + throw RdbmsException.asQueryException(DataBaseType.MySql, e, currentSql, null, null); + } finally { + DBUtil.closeDBResources(rs, null, null); + } + } + + public static List renderPreOrPostSqls(List preOrPostSqls, String tableName) { + if (null == preOrPostSqls) { + return Collections.emptyList(); + } + List renderedSqls = new ArrayList<>(); + for (String sql : preOrPostSqls) { + if (!Strings.isNullOrEmpty(sql)) { + renderedSqls.add(sql.replace(Constant.TABLE_NAME_PLACEHOLDER, tableName)); + } + } + return renderedSqls; + } + + public static void executeSqls(Connection conn, List sqls) { + Statement stmt = null; + String currentSql = null; + try { + stmt = conn.createStatement(); + for (String sql : sqls) { + currentSql = sql; + DBUtil.executeSqlWithoutResultSet(stmt, sql); + } + } catch (Exception e) { + throw RdbmsException.asQueryException(DataBaseType.MySql, e, currentSql, null, null); + } finally { + DBUtil.closeDBResources(null, stmt, null); + } + } + + public static void preCheckPrePareSQL(StarRocksWriterOptions options) { + String table = options.getTable(); + List preSqls = options.getPreSqlList(); + List renderedPreSqls = StarRocksWriterUtil.renderPreOrPostSqls(preSqls, table); + if (null != renderedPreSqls && !renderedPreSqls.isEmpty()) { + LOG.info("Begin to preCheck preSqls:[{}].", String.join(";", renderedPreSqls)); + for (String sql : renderedPreSqls) { + try { + DBUtil.sqlValid(sql, DataBaseType.MySql); + } catch (ParserException e) { + throw RdbmsException.asPreSQLParserException(DataBaseType.MySql,e,sql); + } + } + } + } + + public static void preCheckPostSQL(StarRocksWriterOptions options) { + String table = options.getTable(); + List postSqls = options.getPostSqlList(); + List renderedPostSqls = StarRocksWriterUtil.renderPreOrPostSqls(postSqls, table); + if (null != renderedPostSqls && !renderedPostSqls.isEmpty()) { + LOG.info("Begin to preCheck postSqls:[{}].", String.join(";", renderedPostSqls)); + for(String sql : renderedPostSqls) { + try { + DBUtil.sqlValid(sql, DataBaseType.MySql); + } catch (ParserException e){ + throw RdbmsException.asPostSQLParserException(DataBaseType.MySql,e,sql); + } + } + } + } +} diff --git a/starrockswriter/src/main/resources/plugin.json b/starrockswriter/src/main/resources/plugin.json new file mode 100755 index 0000000000..8edec1e01e --- /dev/null +++ b/starrockswriter/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "starrockswriter", + "class": "com.starrocks.connector.datax.plugin.writer.starrockswriter.StarRocksWriter", + "description": "useScene: prod. mechanism: StarRocksStreamLoad. warn: The more you know about the database, the less problems you encounter.", + "developer": "starrocks" +} \ No newline at end of file diff --git a/starrockswriter/src/main/resources/plugin_job_template.json b/starrockswriter/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..06c075bcb2 --- /dev/null +++ b/starrockswriter/src/main/resources/plugin_job_template.json @@ -0,0 +1,18 @@ +{ + "name": "starrockswriter", + "parameter": { + "username": "", + "password": "", + "column": [], + "preSql": [], + "postSql": [], + "loadUrl": [], + "connection": [ + { + "jdbcUrl": "", + "selectedDatabase": "", + "table": [] + } + ] + } +} \ No newline at end of file diff --git a/streamreader/pom.xml b/streamreader/pom.xml index dc754d9a48..7d186076d2 100755 --- a/streamreader/pom.xml +++ b/streamreader/pom.xml @@ -39,6 +39,16 @@ + + + + src/main/resources + + **/*.* + + true + + diff --git a/streamreader/src/main/java/com/alibaba/datax/plugin/reader/streamreader/StreamReader.java b/streamreader/src/main/java/com/alibaba/datax/plugin/reader/streamreader/StreamReader.java index e3b866596a..6b8c55bcec 100755 --- a/streamreader/src/main/java/com/alibaba/datax/plugin/reader/streamreader/StreamReader.java +++ b/streamreader/src/main/java/com/alibaba/datax/plugin/reader/streamreader/StreamReader.java @@ -5,7 +5,7 @@ import com.alibaba.datax.common.plugin.RecordSender; import com.alibaba.datax.common.spi.Reader; import com.alibaba.datax.common.util.Configuration; -import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson2.JSONObject; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.RandomUtils; diff --git a/streamwriter/pom.xml b/streamwriter/pom.xml index 4a987fac4c..2fa95d7bb3 100755 --- a/streamwriter/pom.xml +++ b/streamwriter/pom.xml @@ -34,6 +34,16 @@ + + + + src/main/resources + + **/*.* + + true + + diff --git a/sybasereader/doc/sybasereader.md b/sybasereader/doc/sybasereader.md new file mode 100644 index 0000000000..abde7cb1ef --- /dev/null +++ b/sybasereader/doc/sybasereader.md @@ -0,0 +1,327 @@ + +# SybaseReader 插件文档 + + +___ + + +## 1 快速介绍 + +SybaseReader插件实现了从Sybase读取数据。在底层实现上,SybaseReader通过JDBC连接远程Sybase数据库,并执行相应的sql语句将数据从Sybase库中SELECT出来。 + +## 2 实现原理 + +简而言之,SybaseReader通过JDBC连接器连接到远程的Sybase数据库,并根据用户配置的信息生成查询SELECT SQL语句并发送到远程Sybase数据库,并将该SQL执行返回结果使用DataX自定义的数据类型拼装为抽象的数据集,并传递给下游Writer处理。 + +对于用户配置Table、Column、Where的信息,SybaseReader将其拼接为SQL语句发送到Sybase数据库;对于用户配置querySql信息,Sybase直接将其发送到Sybase数据库。 + + +## 3 功能说明 + +### 3.1 配置样例 + +* 配置一个从Sybase数据库同步抽取数据到本地的作业: + +``` +{ + "job": { + "setting": { + "speed": { + //设置传输速度 byte/s 尽量逼近这个速度但是不高于它. + // channel 表示通道数量,byte表示通道速度,如果单通道速度1MB,配置byte为1048576表示一个channel + "byte": 1048576 + }, + //出错限制 + "errorLimit": { + //先选择record + "record": 0, + //百分比 1表示100% + "percentage": 0.02 + } + }, + "content": [ + { + "reader": { + "name": "SybaseReader", + "parameter": { + // 数据库连接用户名 + "username": "root", + // 数据库连接密码 + "password": "root", + "column": [ + "id","name" + ], + //切分主键 + "splitPk": "db_id", + "connection": [ + { + "table": [ + "table" + ], + "jdbcUrl": [ + "jdbc:sybase:Tds:192.168.1.92:5000/tempdb?charset=cp936" + ] + } + ] + } + }, + "writer": { + //writer类型 + "name": "streamwriter", + // 是否打印内容 + "parameter": { + "print": true + } + } + } + ] + } +} + +``` + +* 配置一个自定义SQL的数据库同步任务到本地内容的作业: + +``` +{ + "job": { + "setting": { + "speed": { + "channel": 5 + } + }, + "content": [ + { + "reader": { + "name": "SybaseReader", + "parameter": { + "username": "root", + "password": "root", + "where": "", + "connection": [ + { + "querySql": [ + "select db_id,on_line_flag from db_info where db_id < 10" + ], + "jdbcUrl": [ + "jdbc:sybase:Tds:192.168.1.92:5000/tempdb?charset=cp936" + ] + } + ] + } + }, + "writer": { + "name": "streamwriter", + "parameter": { + "visible": false, + "encoding": "UTF-8" + } + } + } + ] + } +} +``` + + +### 3.2 参数说明 + +* **jdbcUrl** + + * 描述:描述的是到对端数据库的JDBC连接信息,使用JSON的数组描述,并支持一个库填写多个连接地址。之所以使用JSON数组描述连接信息,是因为阿里集团内部支持多个IP探测,如果配置了多个,SybaseReader可以依次探测ip的可连接性,直到选择一个合法的IP。如果全部连接失败,SybaseReader报错。 注意,jdbcUrl必须包含在connection配置单元中。对于阿里集团外部使用情况,JSON数组填写一个JDBC连接即可。 + + jdbcUrl按照Sybase官方规范,并可以填写连接附件控制信息。具体请参看[Sybase官方文档](http://www.Sybase.com/technetwork/database/enterprise-edition/documentation/index.html)。 + + * 必选:是
    + + * 默认值:无
    + +* **username** + + * 描述:数据源的用户名
    + + * 必选:是
    + + * 默认值:无
    + +* **password** + + * 描述:数据源指定用户名的密码
    + + * 必选:是
    + + * 默认值:无
    + +* **table** + + * 描述:所选取的需要同步的表。使用JSON的数组描述,因此支持多张表同时抽取。当配置为多张表时,用户自己需保证多张表是同一schema结构,SybaseReader不予检查表是否同一逻辑表。注意,table必须包含在connection配置单元中。
    + + * 必选:是
    + + * 默认值:无
    + +* **column** + + * 描述:所配置的表中需要同步的列名集合,使用JSON的数组描述字段信息。用户使用\*代表默认使用所有列配置,例如['\*']。 + + 支持列裁剪,即列可以挑选部分列进行导出。 + + 支持列换序,即列可以不按照表schema信息进行导出。 + + 支持常量配置,用户需要按照JSON格式: + ["id", "`table`", "1", "'bazhen.csy'", "null", "to_char(a + 1)", "2.3" , "true"] + id为普通列名,\`table\`为包含保留在的列名,1为整形数字常量,'bazhen.csy'为字符串常量,null为空指针,to_char(a + 1)为表达式,2.3为浮点数,true为布尔值。 + + Column必须显示填写,不允许为空! + + * 必选:是
    + + * 默认值:无
    + +* **splitPk** + + * 描述:SybaseReader进行数据抽取时,如果指定splitPk,表示用户希望使用splitPk代表的字段进行数据分片,DataX因此会启动并发任务进行数据同步,这样可以大大提供数据同步的效能。 + + 推荐splitPk用户使用表主键,因为表主键通常情况下比较均匀,因此切分出来的分片也不容易出现数据热点。 + + 目前splitPk仅支持整形、字符串型数据切分,`不支持浮点、日期等其他类型`。如果用户指定其他非支持类型,SybaseReader将报错! + + splitPk如果不填写,将视作用户不对单表进行切分,SybaseReader使用单通道同步全量数据。 + + * 必选:否
    + + * 默认值:无
    + +* **where** + + * 描述:筛选条件,MysqlReader根据指定的column、table、where条件拼接SQL,并根据这个SQL进行数据抽取。在实际业务场景中,往往会选择当天的数据进行同步,可以将where条件指定为gmt_create > $bizdate 。注意:不可以将where条件指定为limit 10,limit不是SQL的合法where子句。
    + + where条件可以有效地进行业务增量同步。 + + * 必选:否
    + + * 默认值:无
    + +* **querySql** + + * 描述:在有些业务场景下,where这一配置项不足以描述所筛选的条件,用户可以通过该配置型来自定义筛选SQL。当用户配置了这一项之后,DataX系统就会忽略table,column这些配置型,直接使用这个配置项的内容对数据进行筛选,例如需要进行多表join后同步数据,使用select a,b from table_a join table_b on table_a.id = table_b.id
    + + `当用户配置querySql时,SybaseReader直接忽略table、column、where条件的配置`。 + + * 必选:否
    + + * 默认值:无
    + +* **fetchSize** + + * 描述:该配置项定义了插件和数据库服务器端每次批量数据获取条数,该值决定了DataX和服务器端的网络交互次数,能够较大的提升数据抽取性能。
    + + `注意,该值过大(>2048)可能造成DataX进程OOM。`。 + + * 必选:否
    + + * 默认值:1024
    + + + +### 3.3 类型转换 + +目前SybaseReader支持大部分Sybase类型,但也存在部分个别类型没有支持的情况,请注意检查你的类型。 + +下面列出SybaseReader针对Sybase类型转换列表: + + +| DataX 内部类型| Sybase 数据类型 | +| -------- | ----- | +| Long |Tinyint,Smallint,Int,Money,Smallmoney| +| Double |Float,Real,Numeric,Decimal| +| String |Char,Varchar,Nchar,Nvarchar,Text| +| Date |Timestamp,Datetime,Smalldatetime| +| Boolean |bit, bool| +| Bytes |Binary,Varbinary,Image| + + + +请注意: + +* `除上述罗列字段类型外,其他类型均不支持`。 + + +## 4 性能报告 + +### 4.1 环境准备 + +#### 4.1.1 数据特征 + +为了模拟线上真实数据,我们设计两个Sybase数据表,分别为: + +#### 4.1.2 机器参数 + +* 执行DataX的机器参数为: + +* Sybase数据库机器参数为: + +### 4.2 测试报告 + +#### 4.2.1 表1测试报告 + + +| 并发任务数| DataX速度(Rec/s)|DataX流量|网卡流量|DataX运行负载|DB运行负载| +|--------| --------|--------|--------|--------|--------| +|1| DataX 统计速度(Rec/s)|DataX统计流量|网卡流量|DataX运行负载|DB运行负载| + +## 5 约束限制 + + +### 5.1 一致性约束 + +Sybase在数据存储划分中属于RDBMS系统,对外可以提供强一致性数据查询接口。例如当一次同步任务启动运行过程中,当该库存在其他数据写入方写入数据时,SybaseReader完全不会获取到写入更新数据,这是由于数据库本身的快照特性决定的。关于数据库快照特性,请参看[MVCC Wikipedia](https://en.wikipedia.org/wiki/Multiversion_concurrency_control) + +上述是在SybaseReader单线程模型下数据同步一致性的特性,由于SybaseReader可以根据用户配置信息使用了并发数据抽取,因此不能严格保证数据一致性:当SybaseReader根据splitPk进行数据切分后,会先后启动多个并发任务完成数据同步。由于多个并发任务相互之间不属于同一个读事务,同时多个并发任务存在时间间隔。因此这份数据并不是`完整的`、`一致的`数据快照信息。 + +针对多线程的一致性快照需求,在技术上目前无法实现,只能从工程角度解决,工程化的方式存在取舍,我们提供几个解决思路给用户,用户可以自行选择: + +1. 使用单线程同步,即不再进行数据切片。缺点是速度比较慢,但是能够很好保证一致性。 + +2. 关闭其他数据写入方,保证当前数据为静态数据,例如,锁表、关闭备库同步等等。缺点是可能影响在线业务。 + +### 5.2 数据库编码问题 + + +SybaseReader底层使用JDBC进行数据抽取,JDBC天然适配各类编码,并在底层进行了编码转换。因此SybaseReader不需用户指定编码,可以自动获取编码并转码。 + +对于Sybase底层写入编码和其设定的编码不一致的混乱情况,SybaseReader对此无法识别,对此也无法提供解决方案,对于这类情况,`导出有可能为乱码`。 + +### 5.3 增量数据同步 + +SybaseReader使用JDBC SELECT语句完成数据抽取工作,因此可以使用SELECT...WHERE...进行增量数据抽取,方式有多种: + +* 数据库在线应用写入数据库时,填充modify字段为更改时间戳,包括新增、更新、删除(逻辑删)。对于这类应用,SybaseReader只需要WHERE条件跟上一同步阶段时间戳即可。 +* 对于新增流水型数据,SybaseReader可以WHERE条件后跟上一阶段最大自增ID即可。 + +对于业务上无字段区分新增、修改数据情况,SybaseReader也无法进行增量数据同步,只能同步全量数据。 + +### 5.4 Sql安全性 + +SybaseReader提供querySql语句交给用户自己实现SELECT抽取语句,SybaseReader本身对querySql不做任何安全性校验。这块交由DataX用户方自己保证。 + +## 6 FAQ + +*** + +**Q: 目前已验证支持sybase的版本?** + + A: Sybase ASE 16/15.7 + +**Q: SybaseReader同步报错,报错信息为XXX** + + A: 网络或者权限问题,请使用Sybase命令行或者可视化工具进行测试: + 如果上述命令也报错,那可以证实是环境问题,请联系你的DBA。 + + +**Q: SybaseReader抽取速度很慢怎么办?** + + A: 影响抽取时间的原因大概有如下几个: + 1. 由于SQL的plan异常,导致的抽取时间长; 在抽取时,尽可能使用全表扫描代替索引扫描; + 2. 合理sql的并发度,减少抽取时间;根据表的大小, + 3. 设置合理fetchsize,减少网络IO; diff --git a/sybasereader/pom.xml b/sybasereader/pom.xml new file mode 100644 index 0000000000..9ff7ce6695 --- /dev/null +++ b/sybasereader/pom.xml @@ -0,0 +1,111 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + + sybasereader + sybasereader + jar + + + 8 + 8 + + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + + com.oracle + ojdbc6 + 11.2.0.3 + + + com.alibaba.datax + datax-common + 0.0.1-SNAPSHOT + compile + + + + com.sybase.jconnect + jconn4 + 16.0 + system + ${basedir}/src/main/libs/jconn4-16.0.jar + + + + junit + junit + 4.13.2 + test + + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + + + \ No newline at end of file diff --git a/sybasereader/src/main/assembly/package.xml b/sybasereader/src/main/assembly/package.xml new file mode 100755 index 0000000000..a726b29796 --- /dev/null +++ b/sybasereader/src/main/assembly/package.xml @@ -0,0 +1,42 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/reader/sybasereader + + + src/main/libs + + *.* + + plugin/reader/sybasereader/libs + + + target/ + + sybasereader-0.0.1-SNAPSHOT.jar + + plugin/reader/sybasereader + + + + + + false + plugin/reader/sybasereader/libs + runtime + + + diff --git a/sybasereader/src/main/java/com/alibaba/datax/plugin/reader/sybasereader/Constants.java b/sybasereader/src/main/java/com/alibaba/datax/plugin/reader/sybasereader/Constants.java new file mode 100755 index 0000000000..2de9764466 --- /dev/null +++ b/sybasereader/src/main/java/com/alibaba/datax/plugin/reader/sybasereader/Constants.java @@ -0,0 +1,7 @@ +package com.alibaba.datax.plugin.reader.sybasereader; + +public class Constants { + + public static final int DEFAULT_FETCH_SIZE = 1024; + +} diff --git a/sybasereader/src/main/java/com/alibaba/datax/plugin/reader/sybasereader/SybaseReader.java b/sybasereader/src/main/java/com/alibaba/datax/plugin/reader/sybasereader/SybaseReader.java new file mode 100755 index 0000000000..f0a0ac1a80 --- /dev/null +++ b/sybasereader/src/main/java/com/alibaba/datax/plugin/reader/sybasereader/SybaseReader.java @@ -0,0 +1,108 @@ +package com.alibaba.datax.plugin.reader.sybasereader; + +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.spi.Reader; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import com.alibaba.datax.plugin.rdbms.reader.CommonRdbmsReader; +import com.alibaba.datax.plugin.rdbms.reader.Constant; + + +public class SybaseReader extends Reader { + + private static final DataBaseType DATABASE_TYPE = DataBaseType.Sybase; + + public static class Job extends Reader.Job { + private static final Logger LOG = LoggerFactory + .getLogger(SybaseReader.Job.class); + + private Configuration originalConfig = null; + private CommonRdbmsReader.Job commonRdbmsReaderJob; + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + + dealFetchSize(this.originalConfig); + + this.commonRdbmsReaderJob = new CommonRdbmsReader.Job( + DATABASE_TYPE); + this.commonRdbmsReaderJob.init(this.originalConfig); + + } + + @Override + public void preCheck(){ + init(); + this.commonRdbmsReaderJob.preCheck(this.originalConfig,DATABASE_TYPE); + } + + @Override + public List split(int adviceNumber) { + return this.commonRdbmsReaderJob.split(this.originalConfig, + adviceNumber); + } + + @Override + public void post() { + this.commonRdbmsReaderJob.post(this.originalConfig); + } + + @Override + public void destroy() { + this.commonRdbmsReaderJob.destroy(this.originalConfig); + } + + private void dealFetchSize(Configuration originalConfig) { + int fetchSize = originalConfig.getInt( + com.alibaba.datax.plugin.rdbms.reader.Constant.FETCH_SIZE, + Constants.DEFAULT_FETCH_SIZE); + if (fetchSize < 1) { + LOG.warn("对 sybasereader 需要配置 fetchSize, 对性能提升有较大影响 请配置fetchSize."); + } + originalConfig.set( + com.alibaba.datax.plugin.rdbms.reader.Constant.FETCH_SIZE, + fetchSize); + } + } + + public static class Task extends Reader.Task { + + private Configuration readerSliceConfig; + private CommonRdbmsReader.Task commonRdbmsReaderTask; + + @Override + public void init() { + this.readerSliceConfig = super.getPluginJobConf(); + this.commonRdbmsReaderTask = new CommonRdbmsReader.Task( + DATABASE_TYPE ,super.getTaskGroupId(), super.getTaskId()); + this.commonRdbmsReaderTask.init(this.readerSliceConfig); + } + + @Override + public void startRead(RecordSender recordSender) { + int fetchSize = this.readerSliceConfig + .getInt(com.alibaba.datax.plugin.rdbms.reader.Constant.FETCH_SIZE); + + this.commonRdbmsReaderTask.startRead(this.readerSliceConfig, + recordSender, super.getTaskPluginCollector(), fetchSize); + } + + @Override + public void post() { + this.commonRdbmsReaderTask.post(this.readerSliceConfig); + } + + @Override + public void destroy() { + this.commonRdbmsReaderTask.destroy(this.readerSliceConfig); + } + + } + +} diff --git a/sybasereader/src/main/libs/jconn4-16.0.jar b/sybasereader/src/main/libs/jconn4-16.0.jar new file mode 100644 index 0000000000..e166b44d46 Binary files /dev/null and b/sybasereader/src/main/libs/jconn4-16.0.jar differ diff --git a/sybasereader/src/main/resources/plugin.json b/sybasereader/src/main/resources/plugin.json new file mode 100755 index 0000000000..39dd61d76d --- /dev/null +++ b/sybasereader/src/main/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "sybasereader", + "class": "com.alibaba.datax.plugin.reader.sybasereader.SybaseReader", + "description": "useScene: prod. mechanism: Jdbc connection using the database, execute select sql, retrieve data from the ResultSet. warn: The more you know about the database, the less problems you encounter.", + "developer": "alibaba" +} \ No newline at end of file diff --git a/sybasereader/src/main/resources/plugin_job_template.json b/sybasereader/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..5d5a1f45b2 --- /dev/null +++ b/sybasereader/src/main/resources/plugin_job_template.json @@ -0,0 +1,14 @@ +{ + "name": "sybasereader", + "parameter": { + "username": "", + "password": "", + "column": [], + "connection": [ + { + "table": [], + "jdbcUrl": [] + } + ] + } +} \ No newline at end of file diff --git a/sybasereader/src/test/java/com/alibaba/datax/plugin/reader/sybasereader/SybaseDatabaseUnitTest.java b/sybasereader/src/test/java/com/alibaba/datax/plugin/reader/sybasereader/SybaseDatabaseUnitTest.java new file mode 100644 index 0000000000..f77caccd42 --- /dev/null +++ b/sybasereader/src/test/java/com/alibaba/datax/plugin/reader/sybasereader/SybaseDatabaseUnitTest.java @@ -0,0 +1,55 @@ +package com.alibaba.datax.plugin.reader.sybasereader; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.Assert.assertEquals; + +public class SybaseDatabaseUnitTest { + private Connection connection; + + @Before + public void setUp() { + // 连接到 Sybase 数据库 + String jdbcUrl = "jdbc:sybase:Tds:192.172.172.80:1680/database"; + String username = "admin"; + String password = "admin123"; + + try { + connection = DriverManager.getConnection(jdbcUrl, username, password); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + @After + public void tearDown() { + if (connection != null) { + try { + connection.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + @Test + public void testDatabaseQuery() throws SQLException { + String query = "SELECT COUNT(*) FROM your_table"; + int expectedRowCount = 10; // 假设期望返回的行数是 10 + + Statement statement = connection.createStatement(); + ResultSet resultSet = statement.executeQuery(query); + resultSet.next(); + int rowCount = resultSet.getInt(1); + + assertEquals(expectedRowCount, rowCount); + } +} diff --git a/sybasewriter/doc/sybasewriter.md b/sybasewriter/doc/sybasewriter.md new file mode 100644 index 0000000000..cccc62d6cd --- /dev/null +++ b/sybasewriter/doc/sybasewriter.md @@ -0,0 +1,228 @@ +# DataX SybaseWriter + + +--- + + +## 1 快速介绍 + +SybaseWriter 插件实现了写入数据到 Sybase 主库的目的表的功能。在底层实现上, SybaseWriter 通过 JDBC 连接远程 Sybase 数据库,并执行相应的 insert into ... 或者 ( replace into ...) 的 sql 语句将数据写入 Sybase,内部会分批次提交入库,需要数据库本身采用 innodb 引擎。 + +SybaseWriter 面向ETL开发工程师,他们使用 SybaseWriter 从数仓导入数据到 Sybase。同时 SybaseWriter 亦可以作为数据迁移工具为DBA等用户提供服务。 + + +## 2 实现原理 + +SybaseWriter 通过 DataX 框架获取 Reader 生成的协议数据,根据你配置的 `writeMode` 生成 + + +* `insert into...`(当主键/唯一性索引冲突时会写不进去冲突的行) + +##### 或者 + +* `replace into...`(没有遇到主键/唯一性索引冲突时,与 insert into 行为一致,冲突时会用新行替换原有行所有字段) 的语句写入数据到 Sybase。出于性能考虑,采用了 `PreparedStatement + Batch`,并且设置了:`rewriteBatchedStatements=true`,将数据缓冲到线程上下文 Buffer 中,当 Buffer 累计到预定阈值时,才发起写入请求。 + +
    + + 注意:目的表所在数据库必须是主库才能写入数据;整个任务至少需要具备 insert/replace into...的权限,是否需要其他权限,取决于你任务配置中在 preSql 和 postSql 中指定的语句。 + + +## 3 功能说明 + +### 3.1 配置样例 + +* 这里使用一份从内存产生到 Sybase 导入的数据。 + +```json +{ + "job": { + "setting": { + "speed": { + "channel": 1 + } + }, + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "column" : [ + { + "value": "DataX", + "type": "string" + }, + { + "value": 19880808, + "type": "long" + }, + { + "value": "1988-08-08 08:08:08", + "type": "date" + }, + { + "value": true, + "type": "bool" + }, + { + "value": "test", + "type": "bytes" + } + ], + "sliceRecordCount": 1000 + } + }, + "writer": { + "name": "Sybasewriter", + "parameter": { + "writeMode": "insert", + "username": "root", + "password": "root", + "column": [ + "id", + "name" + ], + "preSql": [ + "delete from test" + ], + "connection": [ + { + "jdbcUrl":"jdbc:sybase:Tds:192.168.1.92:5000/tempdb?charset=cp936", + "table": [ + "test" + ] + } + ] + } + } + } + ] + } +} + +``` + + +### 3.2 参数说明 + +* **jdbcUrl** + + * 描述:目的数据库的 JDBC 连接信息。作业运行时,DataX 会在你提供的 jdbcUrl 后面追加如下属性:yearIsDateType=false&zeroDateTimeBehavior=convertToNull&rewriteBatchedStatements=true + + 注意:1、在一个数据库上只能配置一个 jdbcUrl 值。这与 SybaseReader 支持多个备库探测不同,因为此处不支持同一个数据库存在多个主库的情况(双主导入数据情况) + 2、jdbcUrl按照Sybase官方规范,并可以填写连接附加控制信息,比如想指定连接编码为 gbk ,则在 jdbcUrl 后面追加属性 useUnicode=true&characterEncoding=gbk。具体请参看 Sybase官方文档或者咨询对应 DBA。 + + + * 必选:是
    + + * 默认值:无
    + +* **username** + + * 描述:目的数据库的用户名
    + + * 必选:是
    + + * 默认值:无
    + +* **password** + + * 描述:目的数据库的密码
    + + * 必选:是
    + + * 默认值:无
    + +* **table** + + * 描述:目的表的表名称。支持写入一个或者多个表。当配置为多张表时,必须确保所有表结构保持一致。 + + 注意:table 和 jdbcUrl 必须包含在 connection 配置单元中 + + * 必选:是
    + + * 默认值:无
    + +* **column** + + * 描述:目的表需要写入数据的字段,字段之间用英文逗号分隔。例如: "column": ["id","name","age"]。如果要依次写入全部列,使用`*`表示, 例如: `"column": ["*"]`。 + + **column配置项必须指定,不能留空!** + + 注意:1、我们强烈不推荐你这样配置,因为当你目的表字段个数、类型等有改动时,你的任务可能运行不正确或者失败 + 2、 column 不能配置任何常量值 + + * 必选:是
    + + * 默认值:否
    + +* **preSql** + + * 描述:写入数据到目的表前,会先执行这里的标准语句。如果 Sql 中有你需要操作到的表名称,请使用 `@table` 表示,这样在实际执行 Sql 语句时,会对变量按照实际表名称进行替换。比如你的任务是要写入到目的端的100个同构分表(表名称为:datax_00,datax01, ... datax_98,datax_99),并且你希望导入数据前,先对表中数据进行删除操作,那么你可以这样配置:`"preSql":["delete from 表名"]`,效果是:在执行到每个表写入数据前,会先执行对应的 delete from 对应表名称
    + + * 必选:否
    + + * 默认值:无
    + +* **postSql** + + * 描述:写入数据到目的表后,会执行这里的标准语句。(原理同 preSql )
    + + * 必选:否
    + + * 默认值:无
    + +* **writeMode** + + * 描述:控制写入数据到目标表采用 `insert into` 或者 `replace into` 或者 `ON DUPLICATE KEY UPDATE` 语句
    + + * 必选:是
    + + * 所有选项:insert/replace/update
    + + * 默认值:insert
    + +* **batchSize** + + * 描述:一次性批量提交的记录数大小,该值可以极大减少DataX与Sybase的网络交互次数,并提升整体吞吐量。但是该值设置过大可能会造成DataX运行进程OOM情况。
    + + * 必选:否
    + + * 默认值:1024
    + + +### 3.3 类型转换 + +类似 SybaseReader ,目前 SybaseWriter 支持大部分 Sybase 类型,但也存在部分个别类型没有支持的情况,请注意检查你的类型。 + +下面列出 SybaseWriter 针对 Sybase 类型转换列表: + + +| DataX 内部类型| Sybase 数据类型 | +| -------- | ----- | +| Long |Tinyint,Smallint,Int,Money,Smallmoney| +| Double |Float,Real,Numeric,Decimal| +| String |Char,Varchar,Nchar,Nvarchar,Text| +| Date |Timestamp,Datetime,Smalldatetime| +| Boolean |bit, bool| +| Bytes |Binary,Varbinary,Image| + +## 4 性能报告 + + +## 5 约束限制 + + + + +## FAQ + +*** + +**Q: 目前已验证支持sybase的版本?** + +A: Sybase ASE 16/15.7 + +**Q: SybaseReader同步报错,报错信息为XXX** + +A: 网络或者权限问题,请使用Sybase命令行或者可视化工具进行测试: +如果上述命令也报错,那可以证实是环境问题,请联系你的DBA。 diff --git a/sybasewriter/pom.xml b/sybasewriter/pom.xml new file mode 100644 index 0000000000..821969fc50 --- /dev/null +++ b/sybasewriter/pom.xml @@ -0,0 +1,100 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + + sybasewriter + + + 8 + 8 + + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + org.slf4j + slf4j-api + + + ch.qos.logback + logback-classic + + + + com.alibaba.datax + plugin-rdbms-util + ${datax-project-version} + + + + com.oracle + ojdbc6 + 11.2.0.3 + + + com.alibaba.datax + datax-common + 0.0.1-SNAPSHOT + compile + + + + com.sybase.jconnect + jconn4 + 16.0 + system + ${basedir}/src/main/libs/jconn4-16.0.jar + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + + \ No newline at end of file diff --git a/sybasewriter/src/main/assembly/package.xml b/sybasewriter/src/main/assembly/package.xml new file mode 100755 index 0000000000..1c7b8cebec --- /dev/null +++ b/sybasewriter/src/main/assembly/package.xml @@ -0,0 +1,43 @@ + + + + dir + + false + + + src/main/java/resources + + plugin.json + plugin_job_template.json + + plugin/writer/sybasewriter + + + src/main/libs + + *.* + + plugin/writer/sybasewriter/libs + + + target/ + + sybasewriter-0.0.1-SNAPSHOT.jar + + plugin/writer/sybasewriter + + + + + + false + plugin/writer/sybasewriter/libs + runtime + + + + diff --git a/sybasewriter/src/main/java/com/alibaba/datax/plugin/writer/sybasewriter/SybaseWriter.java b/sybasewriter/src/main/java/com/alibaba/datax/plugin/writer/sybasewriter/SybaseWriter.java new file mode 100755 index 0000000000..51b90d66eb --- /dev/null +++ b/sybasewriter/src/main/java/com/alibaba/datax/plugin/writer/sybasewriter/SybaseWriter.java @@ -0,0 +1,100 @@ +package com.alibaba.datax.plugin.writer.sybasewriter; + +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; +import com.alibaba.datax.plugin.rdbms.writer.CommonRdbmsWriter; +import com.alibaba.datax.plugin.rdbms.writer.Key; +import com.alibaba.datax.plugin.rdbms.util.DataBaseType; + + +import java.util.List; + + +public class SybaseWriter extends Writer { + private static final DataBaseType DATABASE_TYPE = DataBaseType.Sybase; + public static class Job extends Writer.Job { + private Configuration originalConfig = null; + private CommonRdbmsWriter.Job commonRdbmsWriterJob; + + @Override + public void preCheck(){ + this.init(); + this.commonRdbmsWriterJob.writerPreCheck(this.originalConfig, DATABASE_TYPE); + } + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + this.commonRdbmsWriterJob = new CommonRdbmsWriter.Job(DATABASE_TYPE); + this.commonRdbmsWriterJob.init(this.originalConfig); + } + + // 一般来说,是需要推迟到 task 中进行pre 的执行(单表情况例外) + @Override + public void prepare() { + //实跑先不支持 权限 检验 + //this.commonRdbmsWriterJob.privilegeValid(this.originalConfig, DATABASE_TYPE); + this.commonRdbmsWriterJob.prepare(this.originalConfig); + } + + @Override + public List split(int mandatoryNumber) { + return this.commonRdbmsWriterJob.split(this.originalConfig, mandatoryNumber); + } + + // 一般来说,是需要推迟到 task 中进行post 的执行(单表情况例外) + @Override + public void post() { + this.commonRdbmsWriterJob.post(this.originalConfig); + } + + @Override + public void destroy() { + this.commonRdbmsWriterJob.destroy(this.originalConfig); + } + + } + + public static class Task extends Writer.Task { + private Configuration writerSliceConfig; + private CommonRdbmsWriter.Task commonRdbmsWriterTask; + + @Override + public void init() { + this.writerSliceConfig = super.getPluginJobConf(); + this.commonRdbmsWriterTask = new CommonRdbmsWriter.Task(DATABASE_TYPE); + this.commonRdbmsWriterTask.init(this.writerSliceConfig); + } + + @Override + public void prepare() { + this.commonRdbmsWriterTask.prepare(this.writerSliceConfig); + } + + public void startWrite(RecordReceiver recordReceiver) { + this.commonRdbmsWriterTask.startWrite(recordReceiver, this.writerSliceConfig, + super.getTaskPluginCollector()); + } + + @Override + public void post() { + this.commonRdbmsWriterTask.post(this.writerSliceConfig); + } + + @Override + public void destroy() { + this.commonRdbmsWriterTask.destroy(this.writerSliceConfig); + } + + @Override + public boolean supportFailOver(){ + String writeMode = writerSliceConfig.getString(Key.WRITE_MODE); + return "replace".equalsIgnoreCase(writeMode); + } + + } + + +} diff --git a/sybasewriter/src/main/java/resources/plugin.json b/sybasewriter/src/main/java/resources/plugin.json new file mode 100755 index 0000000000..c66e06ae5f --- /dev/null +++ b/sybasewriter/src/main/java/resources/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "sybasewriter", + "class": "com.alibaba.datax.plugin.writer.sybasewriter.SybaseWriter", + "description": "useScene: prod. mechanism: Jdbc connection using the database, execute select sql, retrieve data from the ResultSet. warn: The more you know about the database, the less problems you encounter.", + "developer": "alibaba" +} \ No newline at end of file diff --git a/sybasewriter/src/main/java/resources/plugin_job_template.json b/sybasewriter/src/main/java/resources/plugin_job_template.json new file mode 100644 index 0000000000..212f76b9a8 --- /dev/null +++ b/sybasewriter/src/main/java/resources/plugin_job_template.json @@ -0,0 +1,14 @@ +{ + "name": "sybasewriter", + "parameter": { + "username": "", + "password": "", + "column": [], + "connection": [ + { + "table": [], + "jdbcUrl": [] + } + ] + } +} \ No newline at end of file diff --git a/sybasewriter/src/main/libs/jconn4-16.0.jar b/sybasewriter/src/main/libs/jconn4-16.0.jar new file mode 100644 index 0000000000..e166b44d46 Binary files /dev/null and b/sybasewriter/src/main/libs/jconn4-16.0.jar differ diff --git a/tdenginereader/doc/tdenginereader-CN.md b/tdenginereader/doc/tdenginereader-CN.md new file mode 100644 index 0000000000..e950275640 --- /dev/null +++ b/tdenginereader/doc/tdenginereader-CN.md @@ -0,0 +1,195 @@ +# DataX TDengineReader + +## 1 快速介绍 + +TDengineReader 插件实现了 TDengine 读取数据的功能。 + +## 2 实现原理 + +TDengineReader 通过 TDengine 的 JDBC driver 查询获取数据。 + +## 3 功能说明 + +### 3.1 配置样例 + +* 配置一个从 TDengine 抽取数据作业: + +```json +{ + "job": { + "content": [ + { + "reader": { + "name": "tdenginereader", + "parameter": { + "username": "root", + "password": "taosdata", + "connection": [ + { + "table": [ + "meters" + ], + "jdbcUrl": [ + "jdbc:TAOS-RS://192.168.56.105:6041/test?timestampFormat=TIMESTAMP" + ] + } + ], + "column": [ + "ts", + "current", + "voltage", + "phase" + ], + "where": "ts>=0", + "beginDateTime": "2017-07-14 10:40:00", + "endDateTime": "2017-08-14 10:40:00" + } + }, + "writer": { + "name": "streamwriter", + "parameter": { + "encoding": "UTF-8", + "print": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} +``` + +* 配置一个自定义 SQL 的数据抽取作业: + +```json +{ + "job": { + "content": [ + { + "reader": { + "name": "tdenginereader", + "parameter": { + "user": "root", + "password": "taosdata", + "connection": [ + { + "querySql": [ + "select * from test.meters" + ], + "jdbcUrl": [ + "jdbc:TAOS-RS://192.168.56.105:6041/test?timestampFormat=TIMESTAMP" + ] + } + ] + } + }, + "writer": { + "name": "streamwriter", + "parameter": { + "encoding": "UTF-8", + "print": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} +``` + +### 3.2 参数说明 + +* **username** + * 描述:TDengine 实例的用户名
    + * 必选:是
    + * 默认值:无
    +* **password** + * 描述:TDengine 实例的密码
    + * 必选:是
    + * 默认值:无
    +* **jdbcUrl** + * 描述:TDengine 数据库的JDBC连接信息。注意,jdbcUrl必须包含在connection配置单元中。JdbcUrl具体请参看TDengine官方文档。 + * 必选:是
    + * 默认值:无
    +* **querySql** + * 描述:在有些业务场景下,where 这一配置项不足以描述所筛选的条件,用户可以通过该配置型来自定义筛选SQL。当用户配置了 querySql 后, TDengineReader 就会忽略 table, column, + where, beginDateTime, endDateTime这些配置型,直接使用这个配置项的内容对数据进行筛选。例如需要 进行多表join后同步数据,使用 select a,b from table_a join + table_b on table_a.id = table_b.id
    + * 必选:否
    + * 默认值:无
    +* **table** + * 描述:所选取的需要同步的表。使用 JSON 的数组描述,因此支持多张表同时抽取。当配置为多张表时,用户自己需保证多张表是同一 schema 结构, TDengineReader不予检查表是否同一逻辑表。注意,table必须包含在 + connection 配置单元中。
    + * 必选:是
    + * 默认值:无
    +* **where** + * 描述:筛选条件中的 where 子句,TDengineReader 根据指定的column, table, where, begingDateTime, endDateTime 条件拼接 SQL,并根据这个 SQL + 进行数据抽取。
    + * 必选:否
    + * 默认值:无
    +* **beginDateTime** + * 描述:数据的开始时间,Job 迁移从 begineDateTime 到 endDateTime 的数据,格式为 yyyy-MM-dd HH:mm:ss
    + * 必选:否
    + * 默认值:无
    +* **endDateTime** + * 描述:数据的结束时间,Job 迁移从 begineDateTime 到 endDateTime 的数据,格式为 yyyy-MM-dd HH:mm:ss
    + * 必选:否
    + * 默认值:无
    + +### 3.3 类型转换 + +| TDengine 数据类型 | DataX 内部类型 | +| --------------- | ------------- | +| TINYINT | Long | +| SMALLINT | Long | +| INTEGER | Long | +| BIGINT | Long | +| FLOAT | Double | +| DOUBLE | Double | +| BOOLEAN | Bool | +| TIMESTAMP | Date | +| BINARY | Bytes | +| NCHAR | String | + +## 4 性能报告 + +### 4.1 环境准备 + +#### 4.1.1 数据特征 + +#### 4.1.2 机器参数 + +#### 4.1.3 DataX jvm 参数 + + -Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError + +### 4.2 测试报告 + +#### 4.2.1 单表测试报告 + +| 通道数| DataX速度(Rec/s)|DataX流量(MB/s)| DataX机器网卡流出流量(MB/s)|DataX机器运行负载|DB网卡进入流量(MB/s)|DB运行负载|DB TPS| +|--------| --------|--------|--------|--------|--------|--------|--------| +|1| | | | | | | | +|4| | | | | | | | +|8| | | | | | | | +|16| | | | | | | | +|32| | | | | | | | + +说明: + +#### 4.2.4 性能测试小结 + +1. +2. + +## 5 约束限制 + +## FAQ \ No newline at end of file diff --git a/tdenginereader/pom.xml b/tdenginereader/pom.xml new file mode 100644 index 0000000000..075a2789c0 --- /dev/null +++ b/tdenginereader/pom.xml @@ -0,0 +1,123 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + + tdenginereader + + + 8 + 8 + + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + + com.alibaba.datax.tdenginewriter + tdenginewriter + 0.0.1-SNAPSHOT + compile + + + + com.taosdata.jdbc + taos-jdbcdriver + 2.0.39 + + + + junit + junit + ${junit-version} + test + + + com.alibaba.datax + plugin-rdbms-util + 0.0.1-SNAPSHOT + compile + + + + com.alibaba.datax + datax-core + 0.0.1-SNAPSHOT + test + + + + + + + + + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + + + **/*Test.java + + + + + true + + + + + + + \ No newline at end of file diff --git a/tdenginereader/src/main/assembly/package.xml b/tdenginereader/src/main/assembly/package.xml new file mode 100755 index 0000000000..b52f20fb81 --- /dev/null +++ b/tdenginereader/src/main/assembly/package.xml @@ -0,0 +1,34 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/reader/tdenginereader + + + target/ + + tdenginereader-0.0.1-SNAPSHOT.jar + + plugin/reader/tdenginereader + + + + + + false + plugin/reader/tdenginereader/libs + runtime + + + diff --git a/tdenginereader/src/main/java/com/alibaba/datax/plugin/reader/TDengineReader.java b/tdenginereader/src/main/java/com/alibaba/datax/plugin/reader/TDengineReader.java new file mode 100644 index 0000000000..4ec42d9e39 --- /dev/null +++ b/tdenginereader/src/main/java/com/alibaba/datax/plugin/reader/TDengineReader.java @@ -0,0 +1,266 @@ +package com.alibaba.datax.plugin.reader; + +import com.alibaba.datax.common.element.*; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordSender; +import com.alibaba.datax.common.spi.Reader; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.tdenginewriter.Key; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.UnsupportedEncodingException; +import java.sql.*; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class TDengineReader extends Reader { + + private static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + public static class Job extends Reader.Job { + private static final Logger LOG = LoggerFactory.getLogger(Job.class); + private Configuration originalConfig; + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + // check username + String username = this.originalConfig.getString(Key.USERNAME); + if (StringUtils.isBlank(username)) + throw DataXException.asDataXException(TDengineReaderErrorCode.REQUIRED_VALUE, + "The parameter [" + Key.USERNAME + "] is not set."); + + // check password + String password = this.originalConfig.getString(Key.PASSWORD); + if (StringUtils.isBlank(password)) + throw DataXException.asDataXException(TDengineReaderErrorCode.REQUIRED_VALUE, + "The parameter [" + Key.PASSWORD + "] is not set."); + + // check connection + List connectionList = this.originalConfig.getListConfiguration(Key.CONNECTION); + if (connectionList == null || connectionList.isEmpty()) + throw DataXException.asDataXException(TDengineReaderErrorCode.REQUIRED_VALUE, + "The parameter [" + Key.CONNECTION + "] is not set."); + for (int i = 0; i < connectionList.size(); i++) { + Configuration conn = connectionList.get(i); + // check jdbcUrl + List jdbcUrlList = conn.getList(Key.JDBC_URL); + if (jdbcUrlList == null || jdbcUrlList.isEmpty()) { + throw DataXException.asDataXException(TDengineReaderErrorCode.REQUIRED_VALUE, + "The parameter [" + Key.JDBC_URL + "] of connection[" + (i + 1) + "] is not set."); + } + // check table/querySql + List querySqlList = conn.getList(Key.QUERY_SQL); + if (querySqlList == null || querySqlList.isEmpty()) { + String querySql = conn.getString(Key.QUERY_SQL); + if (StringUtils.isBlank(querySql)) { + List table = conn.getList(Key.TABLE); + if (table == null || table.isEmpty()) + throw DataXException.asDataXException(TDengineReaderErrorCode.REQUIRED_VALUE, + "The parameter [" + Key.TABLE + "] of connection[" + (i + 1) + "] is not set."); + } + } + } + + SimpleDateFormat format = new SimpleDateFormat(DATETIME_FORMAT); + // check beginDateTime + String beginDatetime = this.originalConfig.getString(Key.BEGIN_DATETIME); + long start = Long.MIN_VALUE; + if (!StringUtils.isBlank(beginDatetime)) { + try { + start = format.parse(beginDatetime).getTime(); + } catch (ParseException e) { + throw DataXException.asDataXException(TDengineReaderErrorCode.ILLEGAL_VALUE, + "The parameter [" + Key.BEGIN_DATETIME + "] needs to conform to the [" + DATETIME_FORMAT + "] format."); + } + } + // check endDateTime + String endDatetime = this.originalConfig.getString(Key.END_DATETIME); + long end = Long.MAX_VALUE; + if (!StringUtils.isBlank(endDatetime)) { + try { + end = format.parse(endDatetime).getTime(); + } catch (ParseException e) { + throw DataXException.asDataXException(TDengineReaderErrorCode.ILLEGAL_VALUE, + "The parameter [" + Key.END_DATETIME + "] needs to conform to the [" + DATETIME_FORMAT + "] format."); + } + } + if (start >= end) + throw DataXException.asDataXException(TDengineReaderErrorCode.ILLEGAL_VALUE, + "The parameter [" + Key.BEGIN_DATETIME + "] should be less than the parameter [" + Key.END_DATETIME + "]."); + + } + + @Override + public void destroy() { + + } + + @Override + public List split(int adviceNumber) { + List configurations = new ArrayList<>(); + + List connectionList = this.originalConfig.getListConfiguration(Key.CONNECTION); + for (Configuration conn : connectionList) { + List jdbcUrlList = conn.getList(Key.JDBC_URL, String.class); + for (String jdbcUrl : jdbcUrlList) { + Configuration clone = this.originalConfig.clone(); + clone.set(Key.JDBC_URL, jdbcUrl); + clone.set(Key.TABLE, conn.getList(Key.TABLE)); + clone.set(Key.QUERY_SQL, conn.getList(Key.QUERY_SQL)); + clone.remove(Key.CONNECTION); + configurations.add(clone); + } + } + + LOG.info("Configuration: {}", configurations); + return configurations; + } + } + + public static class Task extends Reader.Task { + private static final Logger LOG = LoggerFactory.getLogger(Task.class); + + private Configuration readerSliceConfig; + private String mandatoryEncoding; + private Connection conn; + + private List tables; + private List columns; + private String startTime; + private String endTime; + private String where; + private List querySql; + + static { + try { + Class.forName("com.taosdata.jdbc.TSDBDriver"); + Class.forName("com.taosdata.jdbc.rs.RestfulDriver"); + } catch (ClassNotFoundException ignored) { + LOG.warn(ignored.getMessage(), ignored); + } + } + + @Override + public void init() { + this.readerSliceConfig = super.getPluginJobConf(); + + String user = readerSliceConfig.getString(Key.USERNAME); + String password = readerSliceConfig.getString(Key.PASSWORD); + + String url = readerSliceConfig.getString(Key.JDBC_URL); + try { + this.conn = DriverManager.getConnection(url, user, password); + } catch (SQLException e) { + throw DataXException.asDataXException(TDengineReaderErrorCode.CONNECTION_FAILED, + "The parameter [" + Key.JDBC_URL + "] : " + url + " failed to connect since: " + e.getMessage(), e); + } + + this.tables = readerSliceConfig.getList(Key.TABLE, String.class); + this.columns = readerSliceConfig.getList(Key.COLUMN, String.class); + this.startTime = readerSliceConfig.getString(Key.BEGIN_DATETIME); + this.endTime = readerSliceConfig.getString(Key.END_DATETIME); + this.where = readerSliceConfig.getString(Key.WHERE, "_c0 > " + Long.MIN_VALUE); + this.querySql = readerSliceConfig.getList(Key.QUERY_SQL, String.class); + this.mandatoryEncoding = readerSliceConfig.getString(Key.MANDATORY_ENCODING, "UTF-8"); + } + + @Override + public void destroy() { + try { + if (conn != null) + conn.close(); + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + } + + @Override + public void startRead(RecordSender recordSender) { + List sqlList = new ArrayList<>(); + + if (querySql == null || querySql.isEmpty()) { + for (String table : tables) { + StringBuilder sb = new StringBuilder(); + sb.append("select ").append(StringUtils.join(columns, ",")).append(" from ").append(table).append(" "); + sb.append("where ").append(where); + if (!StringUtils.isBlank(startTime)) { + sb.append(" and _c0 >= '").append(startTime).append("'"); + } + if (!StringUtils.isBlank(endTime)) { + sb.append(" and _c0 < '").append(endTime).append("'"); + } + String sql = sb.toString().trim(); + sqlList.add(sql); + } + } else { + sqlList.addAll(querySql); + } + + for (String sql : sqlList) { + try (Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery(sql); + while (rs.next()) { + Record record = buildRecord(recordSender, rs, mandatoryEncoding); + recordSender.sendToWriter(record); + } + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + } + } + + private Record buildRecord(RecordSender recordSender, ResultSet rs, String mandatoryEncoding) { + Record record = recordSender.createRecord(); + try { + ResultSetMetaData metaData = rs.getMetaData(); + for (int i = 1; i <= metaData.getColumnCount(); i++) { + int columnType = metaData.getColumnType(i); + switch (columnType) { + case Types.SMALLINT: + case Types.TINYINT: + case Types.INTEGER: + case Types.BIGINT: + record.addColumn(new LongColumn(rs.getString(i))); + break; + case Types.FLOAT: + case Types.DOUBLE: + record.addColumn(new DoubleColumn(rs.getString(i))); + break; + case Types.BOOLEAN: + record.addColumn(new BoolColumn(rs.getBoolean(i))); + break; + case Types.TIMESTAMP: + record.addColumn(new DateColumn(rs.getTimestamp(i))); + break; + case Types.BINARY: + record.addColumn(new BytesColumn(rs.getBytes(i))); + break; + case Types.NCHAR: + String rawData; + if (StringUtils.isBlank(mandatoryEncoding)) { + rawData = rs.getString(i); + } else { + rawData = new String((rs.getBytes(i) == null ? new byte[0] : rs.getBytes(i)), mandatoryEncoding); + } + record.addColumn(new StringColumn(rawData)); + break; + } + } + } catch (SQLException e) { + throw DataXException.asDataXException(TDengineReaderErrorCode.ILLEGAL_VALUE, "database query error!", e); + } catch (UnsupportedEncodingException e) { + throw DataXException.asDataXException(TDengineReaderErrorCode.ILLEGAL_VALUE, "illegal mandatoryEncoding", e); + } + return record; + } + } + + +} diff --git a/tdenginereader/src/main/java/com/alibaba/datax/plugin/reader/TDengineReaderErrorCode.java b/tdenginereader/src/main/java/com/alibaba/datax/plugin/reader/TDengineReaderErrorCode.java new file mode 100644 index 0000000000..b784ab064d --- /dev/null +++ b/tdenginereader/src/main/java/com/alibaba/datax/plugin/reader/TDengineReaderErrorCode.java @@ -0,0 +1,34 @@ +package com.alibaba.datax.plugin.reader; + +import com.alibaba.datax.common.spi.ErrorCode; + +public enum TDengineReaderErrorCode implements ErrorCode { + + REQUIRED_VALUE("TDengineReader-00", "parameter value is missing"), + ILLEGAL_VALUE("TDengineReader-01", "invalid parameter value"), + CONNECTION_FAILED("TDengineReader-02", "connection error"), + RUNTIME_EXCEPTION("TDengineWriter-03", "runtime exception"); + + private final String code; + private final String description; + + TDengineReaderErrorCode(String code, String description) { + this.code = code; + this.description = description; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + return String.format("Code:[%s], Description:[%s]. ", this.code, this.description); + } +} diff --git a/tdenginereader/src/main/resources/plugin.json b/tdenginereader/src/main/resources/plugin.json new file mode 100755 index 0000000000..7ccdbe639a --- /dev/null +++ b/tdenginereader/src/main/resources/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "tdenginereader", + "class": "com.alibaba.datax.plugin.reader.TDengineReader", + "description": { + "useScene": "data migration from tdengine", + "mechanism": "use JDBC to read data from tdengine." + }, + "developer": "zyyang-taosdata" +} \ No newline at end of file diff --git a/tdenginereader/src/main/resources/plugin_job_template.json b/tdenginereader/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..934fe96a4c --- /dev/null +++ b/tdenginereader/src/main/resources/plugin_job_template.json @@ -0,0 +1,23 @@ +{ + "name": "tdenginereader", + "parameter": { + "user": "", + "password": "", + "connection": [ + { + "table": [ + "" + ], + "jdbcUrl": [ + "" + ] + } + ], + "column": [ + "" + ], + "beginDateTime": "", + "endDateTime": "", + "where": "" + } +} \ No newline at end of file diff --git a/tdenginereader/src/test/java/com/alibaba/datax/plugin/reader/TDengine2DMTest.java b/tdenginereader/src/test/java/com/alibaba/datax/plugin/reader/TDengine2DMTest.java new file mode 100644 index 0000000000..e106471700 --- /dev/null +++ b/tdenginereader/src/test/java/com/alibaba/datax/plugin/reader/TDengine2DMTest.java @@ -0,0 +1,86 @@ +package com.alibaba.datax.plugin.reader; + +import com.alibaba.datax.core.Engine; +import org.junit.Ignore; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Random; + +@Ignore +public class TDengine2DMTest { + private static final String host1 = "192.168.56.105"; + private static final String host2 = "192.168.0.72"; + + private final Random random = new Random(System.currentTimeMillis()); + + @Test + public void t2dm_case01() throws Throwable { + // given + createSupTable("ms"); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/t2dm.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + @Test + public void t2dm_case02() throws Throwable { + // given + createSupTable("us"); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/t2dm.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + @Test + public void t2dm_case03() throws Throwable { + // given + createSupTable("ns"); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/t2dm.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + private void createSupTable(String precision) throws SQLException { + final String url = "jdbc:TAOS-RS://" + host1 + ":6041/"; + try (Connection conn = DriverManager.getConnection(url, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + + stmt.execute("drop database if exists db1"); + stmt.execute("create database if not exists db1 precision '" + precision + "'"); + stmt.execute("create table db1.stb1(ts timestamp, f1 tinyint, f2 smallint, f3 int, f4 bigint, f5 float, " + + "f6 double, f7 bool, f8 binary(100), f9 nchar(100)) tags(t1 timestamp, t2 tinyint, t3 smallint, " + + "t4 int, t5 bigint, t6 float, t7 double, t8 bool, t9 binary(100), t10 nchar(100))"); + + for (int i = 1; i <= 10; i++) { + stmt.execute("insert into db1.tb" + i + " using db1.stb1 tags(now, " + random.nextInt(10) + "," + + random.nextInt(10) + "," + random.nextInt(10) + "," + random.nextInt(10) + "," + + random.nextFloat() + "," + random.nextDouble() + "," + random.nextBoolean() + ",'abcABC123'," + + "'北京朝阳望京') values(now+" + i + "s, " + random.nextInt(10) + "," + random.nextInt(10) + "," + + +random.nextInt(10) + "," + random.nextInt(10) + "," + random.nextFloat() + "," + + random.nextDouble() + "," + random.nextBoolean() + ",'abcABC123','北京朝阳望京')"); + } + stmt.close(); + } + + final String url2 = "jdbc:dm://" + host2 + ":5236"; + try (Connection conn = DriverManager.getConnection(url2, "TESTUSER", "test123456")) { + conn.setAutoCommit(true); + Statement stmt = conn.createStatement(); + stmt.execute("drop table if exists stb2"); + stmt.execute("create table stb2(ts timestamp, f1 tinyint, f2 smallint, f3 int, f4 bigint, f5 float, " + + "f6 double, f7 BIT, f8 VARCHAR(100), f9 VARCHAR2(200), t1 timestamp, t2 tinyint, t3 smallint, " + + "t4 int, t5 bigint, t6 float, t7 double, t8 BIT, t9 VARCHAR(100), t10 VARCHAR2(200))"); + } + } + +} diff --git a/tdenginereader/src/test/java/com/alibaba/datax/plugin/reader/TDengine2StreamTest.java b/tdenginereader/src/test/java/com/alibaba/datax/plugin/reader/TDengine2StreamTest.java new file mode 100644 index 0000000000..f628a648cd --- /dev/null +++ b/tdenginereader/src/test/java/com/alibaba/datax/plugin/reader/TDengine2StreamTest.java @@ -0,0 +1,66 @@ +package com.alibaba.datax.plugin.reader; + +import com.alibaba.datax.core.Engine; +import org.junit.Ignore; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Random; + +@Ignore +public class TDengine2StreamTest { + + private static final String host = "192.168.56.105"; + private static final Random random = new Random(System.currentTimeMillis()); + + @Test + public void case01() throws Throwable { + // given + prepare("ms"); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/t2stream-1.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + @Test + public void case02() throws Throwable { + // given + prepare("ms"); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/t2stream-2.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + + private void prepare(String precision) throws SQLException { + final String url = "jdbc:TAOS-RS://" + host + ":6041/"; + try (Connection conn = DriverManager.getConnection(url, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + + stmt.execute("drop database if exists db1"); + stmt.execute("create database if not exists db1 precision '" + precision + "'"); + stmt.execute("create table db1.stb1(ts timestamp, f1 tinyint, f2 smallint, f3 int, f4 bigint, f5 float, " + + "f6 double, f7 bool, f8 binary(100), f9 nchar(100)) tags(t1 timestamp, t2 tinyint, t3 smallint, " + + "t4 int, t5 bigint, t6 float, t7 double, t8 bool, t9 binary(100), t10 nchar(100))"); + + for (int i = 1; i <= 10; i++) { + stmt.execute("insert into db1.tb" + i + " using db1.stb1 tags(now, " + random.nextInt(10) + "," + + random.nextInt(10) + "," + random.nextInt(10) + "," + random.nextInt(10) + "," + + random.nextFloat() + "," + random.nextDouble() + "," + random.nextBoolean() + ",'abcABC123'," + + "'北京朝阳望京') values(now+" + i + "s, " + random.nextInt(10) + "," + random.nextInt(10) + "," + + +random.nextInt(10) + "," + random.nextInt(10) + "," + random.nextFloat() + "," + + random.nextDouble() + "," + random.nextBoolean() + ",'abcABC123','北京朝阳望京')"); + } + stmt.close(); + } + } + + +} diff --git a/tdenginereader/src/test/java/com/alibaba/datax/plugin/reader/TDengineReaderTest.java b/tdenginereader/src/test/java/com/alibaba/datax/plugin/reader/TDengineReaderTest.java new file mode 100644 index 0000000000..491ddbaff2 --- /dev/null +++ b/tdenginereader/src/test/java/com/alibaba/datax/plugin/reader/TDengineReaderTest.java @@ -0,0 +1,153 @@ +package com.alibaba.datax.plugin.reader; + +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.plugin.writer.tdenginewriter.Key; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class TDengineReaderTest { + + @Test + public void jobInit_case01() { + // given + TDengineReader.Job job = new TDengineReader.Job(); + Configuration configuration = Configuration.from("{" + + "\"username\": \"root\"," + + "\"password\": \"taosdata\"," + + "\"connection\": [{\"table\":[\"weather\"],\"jdbcUrl\":[\"jdbc:TAOS-RS://master:6041/test\"]}]," + + "\"column\": [\"ts\",\"current\",\"voltage\",\"phase\"]," + + "\"where\":\"_c0 > 0\"," + + "\"beginDateTime\": \"2021-01-01 00:00:00\"," + + "\"endDateTime\": \"2021-01-01 12:00:00\"" + + "}"); + job.setPluginJobConf(configuration); + + // when + job.init(); + + // assert + Configuration conf = job.getPluginJobConf(); + + Assert.assertEquals("root", conf.getString(Key.USERNAME)); + Assert.assertEquals("taosdata", conf.getString("password")); + Assert.assertEquals("weather", conf.getString("connection[0].table[0]")); + Assert.assertEquals("jdbc:TAOS-RS://master:6041/test", conf.getString("connection[0].jdbcUrl[0]")); + Assert.assertEquals("2021-01-01 00:00:00", conf.getString("beginDateTime")); + Assert.assertEquals("2021-01-01 12:00:00", conf.getString("endDateTime")); + Assert.assertEquals("_c0 > 0", conf.getString("where")); + } + + + @Test + public void jobInit_case02() { + // given + TDengineReader.Job job = new TDengineReader.Job(); + Configuration configuration = Configuration.from("{" + + "\"username\": \"root\"," + + "\"password\": \"taosdata\"," + + "\"connection\": [{\"querySql\":[\"select * from weather\"],\"jdbcUrl\":[\"jdbc:TAOS-RS://master:6041/test\"]}]," + + "}"); + job.setPluginJobConf(configuration); + + // when + job.init(); + + // assert + Configuration conf = job.getPluginJobConf(); + + Assert.assertEquals("root", conf.getString(Key.USERNAME)); + Assert.assertEquals("taosdata", conf.getString("password")); + Assert.assertEquals("jdbc:TAOS-RS://master:6041/test", conf.getString("connection[0].jdbcUrl[0]")); + Assert.assertEquals("select * from weather", conf.getString("connection[0].querySql[0]")); + } + + @Test + public void jobSplit_case01() { + // given + TDengineReader.Job job = new TDengineReader.Job(); + Configuration configuration = Configuration.from("{" + + "\"username\": \"root\"," + + "\"password\": \"taosdata\"," + + "\"connection\": [{\"table\":[\"weather\"],\"jdbcUrl\":[\"jdbc:TAOS-RS://master:6041/test\"]}]," + + "\"column\": [\"ts\",\"current\",\"voltage\",\"phase\"]," + + "\"where\":\"_c0 > 0\"," + + "\"beginDateTime\": \"2021-01-01 00:00:00\"," + + "\"endDateTime\": \"2021-01-01 12:00:00\"" + + "}"); + job.setPluginJobConf(configuration); + + // when + job.init(); + List configurationList = job.split(1); + + // assert + Assert.assertEquals(1, configurationList.size()); + Configuration conf = configurationList.get(0); + Assert.assertEquals("root", conf.getString("username")); + Assert.assertEquals("taosdata", conf.getString("password")); + Assert.assertEquals("_c0 > 0", conf.getString("where")); + Assert.assertEquals("weather", conf.getString("table[0]")); + Assert.assertEquals("jdbc:TAOS-RS://master:6041/test", conf.getString("jdbcUrl")); + + } + + @Test + public void jobSplit_case02() { + // given + TDengineReader.Job job = new TDengineReader.Job(); + Configuration configuration = Configuration.from("{" + + "\"username\": \"root\"," + + "\"password\": \"taosdata\"," + + "\"connection\": [{\"querySql\":[\"select * from weather\"],\"jdbcUrl\":[\"jdbc:TAOS-RS://master:6041/test\"]}]," + + "\"column\": [\"ts\",\"current\",\"voltage\",\"phase\"]," + + "}"); + job.setPluginJobConf(configuration); + + // when + job.init(); + List configurationList = job.split(1); + + // assert + Assert.assertEquals(1, configurationList.size()); + Configuration conf = configurationList.get(0); + Assert.assertEquals("root", conf.getString("username")); + Assert.assertEquals("taosdata", conf.getString("password")); + Assert.assertEquals("select * from weather", conf.getString("querySql[0]")); + Assert.assertEquals("jdbc:TAOS-RS://master:6041/test", conf.getString("jdbcUrl")); + } + + @Test + public void jobSplit_case03() { + // given + TDengineReader.Job job = new TDengineReader.Job(); + Configuration configuration = Configuration.from("{" + + "\"username\": \"root\"," + + "\"password\": \"taosdata\"," + + "\"connection\": [{\"querySql\":[\"select * from weather\",\"select * from test.meters\"],\"jdbcUrl\":[\"jdbc:TAOS-RS://master:6041/test\", \"jdbc:TAOS://master:6030/test\"]}]," + + "\"column\": [\"ts\",\"current\",\"voltage\",\"phase\"]," + + "}"); + job.setPluginJobConf(configuration); + + // when + job.init(); + List configurationList = job.split(1); + + // assert + Assert.assertEquals(2, configurationList.size()); + Configuration conf = configurationList.get(0); + Assert.assertEquals("root", conf.getString("username")); + Assert.assertEquals("taosdata", conf.getString("password")); + Assert.assertEquals("select * from weather", conf.getString("querySql[0]")); + Assert.assertEquals("jdbc:TAOS-RS://master:6041/test", conf.getString("jdbcUrl")); + + Configuration conf1 = configurationList.get(1); + Assert.assertEquals("root", conf1.getString("username")); + Assert.assertEquals("taosdata", conf1.getString("password")); + Assert.assertEquals("select * from weather", conf1.getString("querySql[0]")); + Assert.assertEquals("select * from test.meters", conf1.getString("querySql[1]")); + Assert.assertEquals("jdbc:TAOS://master:6030/test", conf1.getString("jdbcUrl")); + } + +} \ No newline at end of file diff --git a/tdenginereader/src/test/resources/t2dm.json b/tdenginereader/src/test/resources/t2dm.json new file mode 100644 index 0000000000..d87ade0c2c --- /dev/null +++ b/tdenginereader/src/test/resources/t2dm.json @@ -0,0 +1,52 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "tdenginereader", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "*" + ], + "connection": [ + { + "table": [ + "stb1" + ], + "jdbcUrl": [ + "jdbc:TAOS-RS://192.168.56.105:6041/db1" + ] + } + ] + } + }, + "writer": { + "name": "rdbmswriter", + "parameter": { + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:dm://192.168.0.72:5236" + } + ], + "username": "TESTUSER", + "password": "test123456", + "table": "stb2", + "column": [ + "*" + ] + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginereader/src/test/resources/t2stream-1.json b/tdenginereader/src/test/resources/t2stream-1.json new file mode 100644 index 0000000000..183ab7e267 --- /dev/null +++ b/tdenginereader/src/test/resources/t2stream-1.json @@ -0,0 +1,47 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "tdenginereader", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "ts", + "f1", + "f2", + "t1", + "t2" + ], + "connection": [ + { + "table": [ + "stb1" + ], + "jdbcUrl": [ + "jdbc:TAOS-RS://192.168.56.105:6041/db1" + ] + } + ], + "where": "t10 = '北京朝阳望京'", + "beginDateTime": "2022-03-07 12:00:00", + "endDateTime": "2022-03-07 19:00:00" + } + }, + "writer": { + "name": "streamwriter", + "parameter": { + "encoding": "UTF-8", + "print": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginereader/src/test/resources/t2stream-2.json b/tdenginereader/src/test/resources/t2stream-2.json new file mode 100644 index 0000000000..15bfe9be51 --- /dev/null +++ b/tdenginereader/src/test/resources/t2stream-2.json @@ -0,0 +1,37 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "tdenginereader", + "parameter": { + "username": "root", + "password": "taosdata", + "connection": [ + { + "querySql": [ + "select * from stb1 where t10 = '北京朝阳望京' and _c0 >= '2022-03-07 12:00:00' and _c0 < '2022-03-07 19:00:00'" + ], + "jdbcUrl": [ + "jdbc:TAOS-RS://192.168.56.105:6041/db1" + ] + } + ] + } + }, + "writer": { + "name": "streamwriter", + "parameter": { + "encoding": "UTF-8", + "print": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/doc/tdenginewriter-CN.md b/tdenginewriter/doc/tdenginewriter-CN.md new file mode 100644 index 0000000000..3d115fb79a --- /dev/null +++ b/tdenginewriter/doc/tdenginewriter-CN.md @@ -0,0 +1,245 @@ +# DataX TDengineWriter + +简体中文| [English](./tdenginewriter.md) + +## 1 快速介绍 + +TDengineWriter插件实现了写入数据到TDengine数据库目标表的功能。底层实现上,TDengineWriter通过JDBC连接TDengine,按照TDengine的SQL语法,执行insert语句/schemaless语句,将数据写入TDengine。 + +TDengineWriter可以作为数据迁移工具供DBA将其它数据库的数据导入到TDengine。 + + + +## 2 实现原理 + +TDengineWriter 通过 DataX 框架获取 Reader生成的协议数据,通过JDBC Driver连接TDengine,执行insert语句/schemaless语句,将数据写入TDengine。 + +在TDengine中,table可以分成超级表、子表、普通表三种类型,超级表和子表包括colum和tag,子表的tag列的值为固定值,普通表与关系型数据库中表的概念一致。(详细请参考:[数据模型](https://www.taosdata.com/docs/cn/v2.0/architecture#model) ) + +TDengineWriter支持向超级表、子表、普通表中写入数据,按照table的类型和column参数中是否包含tbname,使用以下方法进行写入: + +1. table为超级表,column中指定tbname:使用自动建表的insert语句,使用tbname作为子表的名称。 +2. table为超级表,column中未指定tbname:使用schemaless写入,TDengine会根据超级表名、tag值计算一个子表名称。 +3. table为子表:使用insert语句写入,ignoreTagUnmatched参数为true时,忽略record中tag值与table的tag值不一致的数据。 +4. table为普通表:使用insert语句写入。 + + + +## 3 功能说明 +### 3.1 配置样例 + +配置一个写入TDengine的作业 +先在TDengine上创建超级表: + +```sql +create database if not exists test; +create table test.weather (ts timestamp, temperature int, humidity double) tags(is_normal bool, device_id binary(100), address nchar(100)); +``` + +使用下面的Job配置,将数据写入TDengine: + +```json +{ + "job": { + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "column": [ + { + "type": "string", + "value": "tb1" + }, + { + "type": "date", + "value": "2022-02-20 12:00:01" + }, + { + "type": "long", + "random": "0, 10" + }, + { + "type": "double", + "random": "0, 10" + }, + { + "type": "bool", + "random": "0, 50" + }, + { + "type": "bytes", + "value": "abcABC123" + }, + { + "type": "string", + "value": "北京朝阳望京" + } + ], + "sliceRecordCount": 1 + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "tbname", + "ts", + "temperature", + "humidity", + "is_normal", + "device_id", + "address" + ], + "connection": [ + { + "table": [ + "weather" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.56.105:6041/test" + } + ], + "batchSize": 100, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} +``` + +### 3.2 参数说明 + +* jdbcUrl + * 描述:数据源的JDBC连接信息,TDengine的JDBC信息请参考:[Java连接器的使用](https://www.taosdata.com/docs/cn/v2.0/connector/java#url) + * 必选:是 + * 默认值:无 +* username + * 描述:用户名 + * 必选:是 + * 默认值:无 +* password + * 描述:用户名的密码 + * 必选:是 + * 默认值:无 +* table + * 描述:表名的集合,table应该包含column参数中的所有列(tbname除外)。注意,column中的tbname会被当作TDengine中子表名使用。 + * 必选:是 + * 默认值:无 +* column + * 描述:字段名的集合,字段的顺序应该与record中column的 + * 必选:是 + * 默认值:无 +* batchSize + * 描述:每batchSize条record为一个batch进行写入 + * 必选:否 + * 默认值:1 +* ignoreTagsUnmatched + * 描述:当table为TDengine中的一张子表,table具有tag值。如果数据的tag值与table的tag值不想等,数据不写入到table中。 + * 必选:否 + * 默认值:false + + +### 3.3 类型转换 + +datax中的数据类型,可以映射到TDengine的数据类型 + +| DataX 内部类型 | TDengine 数据类型 | +| -------------- | ----------------------------------------- | +| INT | TINYINT, SMALLINT, INT | +| LONG | TIMESTAMP, TINYINT, SMALLINT, INT, BIGINT | +| DOUBLE | FLOAT, DOUBLE | +| STRING | TIMESTAMP, BINARY, NCHAR | +| BOOL | BOOL | +| DATE | TIMESTAMP | +| BYTES | BINARY | + + + +### 3.4 各数据源到TDengine的参考示例 + +下面是一些数据源到TDengine进行数据迁移的示例 + +| 数据迁移示例 | 配置的示例 | +| ------------------ | ------------------------------------------------------------ | +| TDengine到TDengine | [超级表到超级表,指定tbname](../src/test/resources/t2t-1.json) | +| TDengine到TDengine | [超级表到超级表,不指定tbname](../src/test/resources/t2t-2.json) | +| TDengine到TDengine | [超级表到子表](../src/test/resources/t2t-3.json) | +| TDengine到TDengine | [普通表到普通表](../src/test/resources/t2t-4.json) | +| RDBMS到TDengine | [普通表到超级表,指定tbname](../src/test/resources/dm2t-1.json) | +| RDBMS到TDengine | [普通表到超级表,不指定tbname](../src/test/resources/dm2t-3.json) | +| RDBMS到TDengine | [普通表到子表](../src/test/resources/dm2t-2.json) | +| RDBMS到TDengine | [普通表到普通表](../src/test/resources/dm2t-4.json) | +| OpenTSDB到TDengine | [metric到普通表](../src/test/resources/o2t-1.json) | + + + + +## 4 性能报告 + +### 4.1 环境准备 + +#### 4.1.1 数据特征 + +建表语句: + +单行记录类似于: + +#### 4.1.2 机器参数 + +* 执行DataX的机器参数为: + 1. cpu: + 2. mem: + 3. net: 千兆双网卡 + 4. disc: DataX 数据不落磁盘,不统计此项 + +* TDengine数据库机器参数为: + 1. cpu: + 2. mem: + 3. net: 千兆双网卡 + 4. disc: + +#### 4.1.3 DataX jvm 参数 + + -Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError + +### 4.2 测试报告 + +#### 4.2.1 单表测试报告 + +| 通道数 | DataX速度(Rec/s) | DataX流量(MB/s) | DataX机器网卡流出流量(MB/s) | DataX机器运行负载 | DB网卡进入流量(MB/s) | DB运行负载 | DB TPS | +| ------ | ---------------- | --------------- | --------------------------- | ----------------- | -------------------- | ---------- | ------ | +| 1 | | | | | | | | +| 4 | | | | | | | | +| 8 | | | | | | | | +| 16 | | | | | | | | +| 32 | | | | | | | | + +说明: + +1. + +#### 4.2.4 性能测试小结 + + + + +## 5 约束限制 + +1. + + + +## FAQ + +### 源表和目标表的字段顺序一致吗? + +是的,TDengineWriter按照column中字段的顺序解析来自datax的数据。 diff --git a/tdenginewriter/doc/tdenginewriter.md b/tdenginewriter/doc/tdenginewriter.md new file mode 100644 index 0000000000..d23a2b4f6e --- /dev/null +++ b/tdenginewriter/doc/tdenginewriter.md @@ -0,0 +1,196 @@ +# DataX TDengineWriter + +[简体中文](./tdenginewriter-CN.md) | English + +## 1 Quick Introduction + +The TDengineWriter plugin enables writing data to the target table of the TDengine database. At the bottom level, TDengineWriter connects TDengine through JDBC, executes insert statement /schemaless statement according to TDengine SQL syntax, and writes data to TDengine. + +TDengineWriter can be used as a data migration tool for DBAs to import data from other databases into TDengine. + + + +## 2 Implementation + +TDengineWriter obtains the protocol data generated by Reader through DataX framework, connects to TDengine through JDBC Driver, executes insert statement /schemaless statement, and writes the data to TDengine. + +In TDengine, table can be divided into super table, sub-table and ordinary table. Super table and sub-table include Column and Tag. The value of tag column of sub-table is fixed value. (details please refer to: [data model](https://www.taosdata.com/docs/cn/v2.0/architecture#model)) + +The TDengineWriter can write data to super tables, sub-tables, and ordinary tables using the following methods based on the type of the table and whether the column parameter contains TBName: + +1. Table is a super table and column specifies tbname: use the automatic insert statement to create the table and use tbname as the name of the sub-table. + +2. Table is a super table and column does not contain tbname: use schemaless to write the table. TDengine will auto-create a tbname based on the super table name and tag value. + +3. Table is a sub-table: Use insert statement to write, ignoreTagUnmatched parameter is true, ignore data in record whose tag value is inconsistent with that of table. + +4. Table is a common table: use insert statement to write data. + + +## 3 Features Introduction +### 3.1 Sample +Configure a job to write to TDengine + +Create a supertable on TDengine: + +```sql +create database if not exists test; +create table test.weather (ts timestamp, temperature int, humidity double) tags(is_normal bool, device_id binary(100), address nchar(100)); +``` + +Write data to TDengine using the following Job configuration: + +```json +{ + "job": { + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "column": [ + { + "type": "string", + "value": "tb1" + }, + { + "type": "date", + "value": "2022-02-20 12:00:01" + }, + { + "type": "long", + "random": "0, 10" + }, + { + "type": "double", + "random": "0, 10" + }, + { + "type": "bool", + "random": "0, 50" + }, + { + "type": "bytes", + "value": "abcABC123" + }, + { + "type": "string", + "value": "北京朝阳望京" + } + ], + "sliceRecordCount": 1 + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "tbname", + "ts", + "temperature", + "humidity", + "is_normal", + "device_id", + "address" + ], + "connection": [ + { + "table": [ + "weather" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.56.105:6041/test" + } + ], + "batchSize": 100, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} +``` + +### 3.2 Configuration + +* jdbcUrl + * Descrption: Data source JDBC connection information, TDengine JDBC information please refer to: [Java connector](https://www.taosdata.com/docs/cn/v2.0/connector/java#url) + * Required: yes + * Default: none +* username + * Descrption: username + * Required: yes + * Default: none + +* password + * Descrption: password of username + * Required: yes + * Default: none + +* table + * Descrption: A list of table names that should contain all of the columns in the column parameter (except tbname). Note that tbname in column is used as the TDengine sub-table name. + * Required: yes + * Default: none + +* column + * Descrption: A list of field names, the order of the fields should be the column in the record + * Required: yes + * Default: none + +* batchSize + * Descrption: Each batchSize record is written to a batch + * Required: no + * Default: 1 + +* ignoreTagsUnmatched + * Descrption: When table is a sub-table in TDengine, table has a tag value. If the tag value of the data and the tag value of the table are not equal, the data is not written to the table. + * Required: no + * Default: false + + +#### 3.3 Type Convert + +Data types in datax that can be mapped to data types in TDengine + +| DataX Type | TDengine Type | +| ---------- | ----------------------------------------- | +| INT | TINYINT, SMALLINT, INT | +| LONG | TIMESTAMP, TINYINT, SMALLINT, INT, BIGINT | +| DOUBLE | FLOAT, DOUBLE | +| STRING | TIMESTAMP, BINARY, NCHAR | +| BOOL | BOOL | +| DATE | TIMESTAMP | +| BYTES | BINARY | + +### 3.2 From MongoDB to TDengine + +Here are some examples of data sources migrating to TDengine + +| Sample | Configuration | +| -------------------- | ------------------------------------------------------------ | +| TDengine to TDengine | [super table to super table with tbname](../src/test/resources/t2t-1.json) | +| TDengine to TDengine | [super table to super table without tbname](../src/test/resources/t2t-2.json) | +| TDengine to TDengine | [super table to sub-table](../src/test/resources/t2t-3.json) | +| TDengine to TDengine | [table to table](../src/test/resources/t2t-4.json) | +| RDBMS to TDengine | [table to super table with tbname](../src/test/resources/dm2t-1.json) | +| RDBMS to TDengine | [table to super table without tbname](../src/test/resources/dm2t-2.json) | +| RDBMS to TDengine | [table to sub-table](../src/test/resources/dm2t-3.json) | +| RDBMS to TDengine | [table to table](../src/test/resources/dm2t-4.json) | +| OpenTSDB to TDengine | [metric to table](../src/test/resources/o2t-1.json) | + +## 4 Restriction + + + +## FAQ + +### Do columns in source table and columns in target table must be in the same order? + +Yes, TDengineWriter parses the data from the Datax in the order of the fields in the column. diff --git a/tdenginewriter/pom.xml b/tdenginewriter/pom.xml new file mode 100644 index 0000000000..a7564e6bad --- /dev/null +++ b/tdenginewriter/pom.xml @@ -0,0 +1,127 @@ + + + + datax-all + com.alibaba.datax + 0.0.1-SNAPSHOT + + 4.0.0 + + com.alibaba.datax.tdenginewriter + tdenginewriter + 0.0.1-SNAPSHOT + + + 8 + 8 + + + + + + com.taosdata.jdbc + taos-jdbcdriver + 2.0.39 + + + + org.apache.commons + commons-lang3 + ${commons-lang3-version} + + + + com.alibaba.datax + datax-common + ${datax-project-version} + + + slf4j-log4j12 + org.slf4j + + + + + + junit + junit + ${junit-version} + test + + + com.alibaba.datax + datax-core + 0.0.1-SNAPSHOT + test + + + mysql + mysql-connector-java + 5.1.49 + test + + + + + + + + + + + + + + + + + + + maven-compiler-plugin + + ${jdk-version} + ${jdk-version} + ${project-sourceEncoding} + + + + maven-assembly-plugin + + + src/main/assembly/package.xml + + datax + + + + dwzip + package + + single + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12.4 + + + + **/*Test.java + + + + + true + + + + + + + \ No newline at end of file diff --git a/tdenginewriter/src/main/assembly/package.xml b/tdenginewriter/src/main/assembly/package.xml new file mode 100644 index 0000000000..d3b75ea238 --- /dev/null +++ b/tdenginewriter/src/main/assembly/package.xml @@ -0,0 +1,34 @@ + + + + dir + + false + + + src/main/resources + + plugin.json + plugin_job_template.json + + plugin/writer/tdenginewriter + + + target/ + + tdenginewriter-0.0.1-SNAPSHOT.jar + + plugin/writer/tdenginewriter + + + + + + false + plugin/writer/tdenginewriter/libs + runtime + + + diff --git a/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/ColumnMeta.java b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/ColumnMeta.java new file mode 100644 index 0000000000..5c77eccdc6 --- /dev/null +++ b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/ColumnMeta.java @@ -0,0 +1,24 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +public class ColumnMeta { + String field; + String type; + int length; + String note; + boolean isTag; + boolean isPrimaryKey; + Object value; + + @Override + public String toString() { + return "ColumnMeta{" + + "field='" + field + '\'' + + ", type='" + type + '\'' + + ", length=" + length + + ", note='" + note + '\'' + + ", isTag=" + isTag + + ", isPrimaryKey=" + isPrimaryKey + + ", value=" + value + + '}'; + } +} diff --git a/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/Constants.java b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/Constants.java new file mode 100644 index 0000000000..d62c8f3273 --- /dev/null +++ b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/Constants.java @@ -0,0 +1,8 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +public class Constants { + public static final String DEFAULT_USERNAME = "root"; + public static final String DEFAULT_PASSWORD = "taosdata"; + public static final int DEFAULT_BATCH_SIZE = 1; + public static final boolean DEFAULT_IGNORE_TAGS_UNMATCHED = false; +} \ No newline at end of file diff --git a/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/DataHandler.java b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/DataHandler.java new file mode 100644 index 0000000000..f22d4d6cf8 --- /dev/null +++ b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/DataHandler.java @@ -0,0 +1,8 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.plugin.TaskPluginCollector; + +public interface DataHandler { + int handle(RecordReceiver lineReceiver, TaskPluginCollector collector); +} diff --git a/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/DefaultDataHandler.java b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/DefaultDataHandler.java new file mode 100644 index 0000000000..27ade38278 --- /dev/null +++ b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/DefaultDataHandler.java @@ -0,0 +1,569 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; +import com.taosdata.jdbc.SchemalessWriter; +import com.taosdata.jdbc.enums.SchemalessProtocolType; +import com.taosdata.jdbc.enums.SchemalessTimestampType; +import com.taosdata.jdbc.utils.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.*; +import java.util.*; +import java.util.Date; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class DefaultDataHandler implements DataHandler { + private static final Logger LOG = LoggerFactory.getLogger(DefaultDataHandler.class); + + static { + try { + Class.forName("com.taosdata.jdbc.TSDBDriver"); + Class.forName("com.taosdata.jdbc.rs.RestfulDriver"); + } catch (ClassNotFoundException e) { + LOG.error(e.getMessage(), e); + } + } + + private final TaskPluginCollector taskPluginCollector; + private String username; + private String password; + private String jdbcUrl; + private int batchSize; + private boolean ignoreTagsUnmatched; + + private List tables; + private List columns; + + private Map tableMetas; + private SchemaManager schemaManager; + + public void setTableMetas(Map tableMetas) { + this.tableMetas = tableMetas; + } + + public void setTbnameColumnMetasMap(Map> tbnameColumnMetasMap) { + this.tbnameColumnMetasMap = tbnameColumnMetasMap; + } + + public void setSchemaManager(SchemaManager schemaManager) { + this.schemaManager = schemaManager; + } + + private Map> tbnameColumnMetasMap; + + public DefaultDataHandler(Configuration configuration, TaskPluginCollector taskPluginCollector) { + this.username = configuration.getString(Key.USERNAME, Constants.DEFAULT_USERNAME); + this.password = configuration.getString(Key.PASSWORD, Constants.DEFAULT_PASSWORD); + this.jdbcUrl = configuration.getString(Key.JDBC_URL); + this.batchSize = configuration.getInt(Key.BATCH_SIZE, Constants.DEFAULT_BATCH_SIZE); + this.tables = configuration.getList(Key.TABLE, String.class); + this.columns = configuration.getList(Key.COLUMN, String.class); + this.ignoreTagsUnmatched = configuration.getBool(Key.IGNORE_TAGS_UNMATCHED, Constants.DEFAULT_IGNORE_TAGS_UNMATCHED); + this.taskPluginCollector = taskPluginCollector; + } + + @Override + public int handle(RecordReceiver lineReceiver, TaskPluginCollector collector) { + int count = 0; + int affectedRows = 0; + + try (Connection conn = DriverManager.getConnection(jdbcUrl, username, password)) { + LOG.info("connection[ jdbcUrl: " + jdbcUrl + ", username: " + username + "] established."); + // prepare table_name -> table_meta + this.schemaManager = new SchemaManager(conn); + this.tableMetas = schemaManager.loadTableMeta(tables); + // prepare table_name -> column_meta + this.tbnameColumnMetasMap = schemaManager.loadColumnMetas(tables); + + List recordBatch = new ArrayList<>(); + Record record; + for (int i = 1; (record = lineReceiver.getFromReader()) != null; i++) { + if (i % batchSize != 0) { + recordBatch.add(record); + } else { + try { + recordBatch.add(record); + affectedRows += writeBatch(conn, recordBatch); + } catch (SQLException e) { + LOG.warn("use one row insert. because:" + e.getMessage()); + affectedRows += writeEachRow(conn, recordBatch); + } + recordBatch.clear(); + } + count++; + } + + if (!recordBatch.isEmpty()) { + try { + affectedRows += writeBatch(conn, recordBatch); + } catch (SQLException e) { + LOG.warn("use one row insert. because:" + e.getMessage()); + affectedRows += writeEachRow(conn, recordBatch); + } + recordBatch.clear(); + } + } catch (SQLException e) { + throw DataXException.asDataXException(TDengineWriterErrorCode.RUNTIME_EXCEPTION, e.getMessage()); + } + + if (affectedRows != count) { + LOG.error("write record missing or incorrect happened, affectedRows: " + affectedRows + ", total: " + count); + } + + return affectedRows; + } + + private int writeEachRow(Connection conn, List recordBatch) { + int affectedRows = 0; + for (Record record : recordBatch) { + List recordList = new ArrayList<>(); + recordList.add(record); + try { + affectedRows += writeBatch(conn, recordList); + } catch (SQLException e) { + LOG.error(e.getMessage()); + this.taskPluginCollector.collectDirtyRecord(record, e); + } + } + return affectedRows; + } + + /** + * table: [ "stb1", "stb2", "tb1", "tb2", "t1" ] + * stb1[ts,f1,f2] tags:[t1] + * stb2[ts,f1,f2,f3] tags:[t1,t2] + * 1. tables 表的的类型分成:stb(super table)/tb(sub table)/t(original table) + * 2. 对于stb,自动建表/schemaless + * 2.1: data中有tbname字段, 例如:data: [ts, f1, f2, f3, t1, t2, tbname] tbColumn: [ts, f1, f2, t1] => insert into tbname using stb1 tags(t1) values(ts, f1, f2) + * 2.2: data中没有tbname字段,例如:data: [ts, f1, f2, f3, t1, t2] tbColumn: [ts, f1, f2, t1] => schemaless: stb1,t1=t1 f1=f1,f2=f2 ts, 没有批量写 + * 3. 对于tb,拼sql,例如:data: [ts, f1, f2, f3, t1, t2] tbColumn: [ts, f1, f2, t1] => insert into tb(ts, f1, f2) values(ts, f1, f2) + * 4. 对于t,拼sql,例如:data: [ts, f1, f2, f3, t1, t2] tbColumn: [ts, f1, f2, f3, t1, t2] insert into t(ts, f1, f2, f3, t1, t2) values(ts, f1, f2, f3, t1, t2) + */ + public int writeBatch(Connection conn, List recordBatch) throws SQLException { + int affectedRows = 0; + for (String table : tables) { + TableMeta tableMeta = tableMetas.get(table); + switch (tableMeta.tableType) { + case SUP_TABLE: { + if (columns.contains("tbname")) { + affectedRows += writeBatchToSupTableBySQL(conn, table, recordBatch); + } else { + Map tag2Tbname = schemaManager.loadTagTableNameMap(table); + affectedRows += writeBatchToSupTableWithoutTbname(conn, table, recordBatch, tag2Tbname); + } + } + break; + case SUB_TABLE: + affectedRows += writeBatchToSubTable(conn, table, recordBatch); + break; + case NML_TABLE: + default: + affectedRows += writeBatchToNormalTable(conn, table, recordBatch); + } + } + return affectedRows; + } + + private int writeBatchToSupTableWithoutTbname(Connection conn, String table, List recordBatch, Map tag2Tbname) throws SQLException { + List columnMetas = tbnameColumnMetasMap.get(table); + List subTableExist = filterSubTableExistRecords(recordBatch, columnMetas, tag2Tbname); + List subTableNotExist = filterSubTableNotExistRecords(recordBatch, columnMetas, tag2Tbname); + + int affectedRows = 0; + Map> subTableRecordsMap = splitRecords(subTableExist, columnMetas, tag2Tbname); + + List subTables = new ArrayList<>(subTableRecordsMap.keySet()); + this.tbnameColumnMetasMap.putAll(schemaManager.loadColumnMetas(subTables)); + + for (String subTable : subTableRecordsMap.keySet()) { + List subTableRecords = subTableRecordsMap.get(subTable); + affectedRows += writeBatchToNormalTable(conn, subTable, subTableRecords); + } + if (!subTableNotExist.isEmpty()) + affectedRows += writeBatchToSupTableBySchemaless(conn, table, subTableNotExist); + return affectedRows; + } + + private List filterSubTableExistRecords(List recordBatch, List columnMetas, Map tag2Tbname) { + return recordBatch.stream().filter(record -> { + String tagStr = getTagString(columnMetas, record); + return tag2Tbname.containsKey(tagStr); + }).collect(Collectors.toList()); + } + + private List filterSubTableNotExistRecords(List recordBatch, List columnMetas, Map tag2Tbname) { + return recordBatch.stream().filter(record -> { + String tagStr = getTagString(columnMetas, record); + return !tag2Tbname.containsKey(tagStr); + }).collect(Collectors.toList()); + } + + private Map> splitRecords(List subTableExist, List columnMetas, Map tag2Tbname) { + Map> ret = new HashMap<>(); + for (Record record : subTableExist) { + String tagstr = getTagString(columnMetas, record); + String tbname = tag2Tbname.get(tagstr); + if (ret.containsKey(tbname)) { + ret.get(tbname).add(record); + } else { + List list = new ArrayList<>(); + list.add(record); + ret.put(tbname, list); + } + } + return ret; + } + + private String getTagString(List columnMetas, Record record) { + return IntStream.range(0, columnMetas.size()).mapToObj(colIndex -> { + ColumnMeta columnMeta = columnMetas.get(colIndex); + if (columnMeta.isTag) { + Column column = record.getColumn(colIndex); + switch (columnMeta.type) { + case "TINYINT": + case "SMALLINT": + case "INT": + case "BIGINT": + return column.asLong().toString(); + default: + return column.asString(); + } + } + return ""; + }).collect(Collectors.joining()); + } + + /** + * insert into record[idx(tbname)] using table tags(record[idx(t1)]) (ts, f1, f2, f3) values(record[idx(ts)], record[idx(f1)], ) + * record[idx(tbname)] using table tags(record[idx(t1)]) (ts, f1, f2, f3) values(record[idx(ts)], record[idx(f1)], ) + * record[idx(tbname)] using table tags(record[idx(t1)]) (ts, f1, f2, f3) values(record[idx(ts)], record[idx(f1)], ) + */ + private int writeBatchToSupTableBySQL(Connection conn, String table, List recordBatch) throws SQLException { + List columnMetas = this.tbnameColumnMetasMap.get(table); + + StringBuilder sb = new StringBuilder("insert into"); + for (Record record : recordBatch) { + sb.append(" ").append(record.getColumn(indexOf("tbname")).asString()) + .append(" using ").append(table) + .append(" tags") + .append(columnMetas.stream().filter(colMeta -> columns.contains(colMeta.field)).filter(colMeta -> { + return colMeta.isTag; + }).map(colMeta -> { + return buildColumnValue(colMeta, record); + }).collect(Collectors.joining(",", "(", ")"))) + .append(" ") + .append(columnMetas.stream().filter(colMeta -> columns.contains(colMeta.field)).filter(colMeta -> { + return !colMeta.isTag; + }).map(colMeta -> { + return colMeta.field; + }).collect(Collectors.joining(",", "(", ")"))) + .append(" values") + .append(columnMetas.stream().filter(colMeta -> columns.contains(colMeta.field)).filter(colMeta -> { + return !colMeta.isTag; + }).map(colMeta -> { + return buildColumnValue(colMeta, record); + }).collect(Collectors.joining(",", "(", ")"))); + } + String sql = sb.toString(); + + return executeUpdate(conn, sql); + } + + private int executeUpdate(Connection conn, String sql) throws SQLException { + int count; + try (Statement stmt = conn.createStatement()) { + LOG.debug(">>> " + sql); + count = stmt.executeUpdate(sql); + } + return count; + } + + private String buildColumnValue(ColumnMeta colMeta, Record record) { + Column column = record.getColumn(indexOf(colMeta.field)); + TimestampPrecision timestampPrecision = schemaManager.loadDatabasePrecision(); + switch (column.getType()) { + case DATE: { + Date value = column.asDate(); + switch (timestampPrecision) { + case MILLISEC: + return "" + (value.getTime()); + case MICROSEC: + return "" + (value.getTime() * 1000); + case NANOSEC: + return "" + (value.getTime() * 1000_000); + default: + return "'" + column.asString() + "'"; + } + } + case BYTES: + case STRING: + if (colMeta.type.equals("TIMESTAMP")) + return "\"" + column.asString() + "\""; + String value = column.asString(); + if (value == null) + return "NULL"; + return "\'" + Utils.escapeSingleQuota(value) + "\'"; + case NULL: + case BAD: + return "NULL"; + case BOOL: + case DOUBLE: + case INT: + case LONG: + default: + return column.asString(); + } + } + + /** + * table: ["stb1"], column: ["ts", "f1", "f2", "t1"] + * data: [ts, f1, f2, f3, t1, t2] tbColumn: [ts, f1, f2, t1] => schemaless: stb1,t1=t1 f1=f1,f2=f2 ts + */ + private int writeBatchToSupTableBySchemaless(Connection conn, String table, List recordBatch) throws SQLException { + int count = 0; + TimestampPrecision timestampPrecision = schemaManager.loadDatabasePrecision(); + + List columnMetaList = this.tbnameColumnMetasMap.get(table); + ColumnMeta ts = columnMetaList.stream().filter(colMeta -> colMeta.isPrimaryKey).findFirst().get(); + + List lines = new ArrayList<>(); + for (Record record : recordBatch) { + StringBuilder sb = new StringBuilder(); + sb.append(table).append(",") + .append(columnMetaList.stream().filter(colMeta -> columns.contains(colMeta.field)).filter(colMeta -> { + return colMeta.isTag; + }).map(colMeta -> { + String value = record.getColumn(indexOf(colMeta.field)).asString(); + if (value.contains(" ")) + value = value.replace(" ", "\\ "); + return colMeta.field + "=" + value; + }).collect(Collectors.joining(","))) + .append(" ") + .append(columnMetaList.stream().filter(colMeta -> columns.contains(colMeta.field)).filter(colMeta -> { + return !colMeta.isTag && !colMeta.isPrimaryKey; + }).map(colMeta -> { + return colMeta.field + "=" + buildSchemalessColumnValue(colMeta, record); +// return colMeta.field + "=" + record.getColumn(indexOf(colMeta.field)).asString(); + }).collect(Collectors.joining(","))) + .append(" "); + // timestamp + Column column = record.getColumn(indexOf(ts.field)); + Object tsValue = column.getRawData(); + if (column.getType() == Column.Type.DATE && tsValue instanceof Date) { + long time = column.asDate().getTime(); + switch (timestampPrecision) { + case NANOSEC: + sb.append(time * 1000000); + break; + case MICROSEC: + sb.append(time * 1000); + break; + case MILLISEC: + default: + sb.append(time); + } + } else if (column.getType() == Column.Type.STRING) { + sb.append(Utils.parseTimestamp(column.asString())); + } else { + sb.append(column.asLong()); + } + String line = sb.toString(); + LOG.debug(">>> " + line); + lines.add(line); + count++; + } + + SchemalessWriter writer = new SchemalessWriter(conn); + SchemalessTimestampType timestampType; + switch (timestampPrecision) { + case NANOSEC: + timestampType = SchemalessTimestampType.NANO_SECONDS; + break; + case MICROSEC: + timestampType = SchemalessTimestampType.MICRO_SECONDS; + break; + case MILLISEC: + timestampType = SchemalessTimestampType.MILLI_SECONDS; + break; + default: + timestampType = SchemalessTimestampType.NOT_CONFIGURED; + } + + writer.write(lines, SchemalessProtocolType.LINE, timestampType); + + LOG.warn("schemalessWriter does not return affected rows!"); + return count; + } + + private long dateAsLong(Column column) { + TimestampPrecision timestampPrecision = schemaManager.loadDatabasePrecision(); + long time = column.asDate().getTime(); + switch (timestampPrecision) { + case NANOSEC: + return time * 1000000; + case MICROSEC: + return time * 1000; + case MILLISEC: + default: + return time; + } + } + + private String buildSchemalessColumnValue(ColumnMeta colMeta, Record record) { + Column column = record.getColumn(indexOf(colMeta.field)); + switch (column.getType()) { + case DATE: + if (colMeta.type.equals("TIMESTAMP")) + return dateAsLong(column) + "i64"; + return "L'" + column.asString() + "'"; + case NULL: + case BAD: + return "NULL"; + case DOUBLE: { + if (colMeta.type.equals("FLOAT")) + return column.asString() + "f32"; + if (colMeta.type.equals("DOUBLE")) + return column.asString() + "f64"; + } + case INT: + case LONG: { + if (colMeta.type.equals("TINYINT")) + return column.asString() + "i8"; + if (colMeta.type.equals("SMALLINT")) + return column.asString() + "i16"; + if (colMeta.type.equals("INT")) + return column.asString() + "i32"; + if (colMeta.type.equals("BIGINT")) + return column.asString() + "i64"; + } + case BYTES: + case STRING: + if (colMeta.type.equals("TIMESTAMP")) + return column.asString() + "i64"; + String value = column.asString(); + value = value.replace("\"", "\\\""); + if (colMeta.type.startsWith("BINARY")) + return "\"" + value + "\""; + if (colMeta.type.startsWith("NCHAR")) + return "L\"" + value + "\""; + case BOOL: + default: + return column.asString(); + } + } + + /** + * table: ["tb1"], column: [tbname, ts, f1, f2, t1] + * if contains("tbname") and tbname != tb1 continue; + * else if t1 != record[idx(t1)] or t2 != record[idx(t2)]... continue; + * else + * insert into tb1 (ts, f1, f2) values( record[idx(ts)], record[idx(f1)], record[idx(f2)]) + */ + private int writeBatchToSubTable(Connection conn, String table, List recordBatch) throws SQLException { + List columnMetas = this.tbnameColumnMetasMap.get(table); + + StringBuilder sb = new StringBuilder(); + sb.append("insert into ").append(table).append(" ") + .append(columnMetas.stream().filter(colMeta -> columns.contains(colMeta.field)).filter(colMeta -> { + return !colMeta.isTag; + }).map(colMeta -> { + return colMeta.field; + }).collect(Collectors.joining(",", "(", ")"))) + .append(" values"); + int validRecords = 0; + for (Record record : recordBatch) { + if (columns.contains("tbname") && !table.equals(record.getColumn(indexOf("tbname")).asString())) + continue; + + boolean tagsAllMatch = columnMetas.stream().filter(colMeta -> columns.contains(colMeta.field)).filter(colMeta -> { + return colMeta.isTag; + }).allMatch(colMeta -> { + Column column = record.getColumn(indexOf(colMeta.field)); + boolean equals = equals(column, colMeta); + return equals; + }); + + if (ignoreTagsUnmatched && !tagsAllMatch) + continue; + + sb.append(columnMetas.stream().filter(colMeta -> columns.contains(colMeta.field)).filter(colMeta -> { + return !colMeta.isTag; + }).map(colMeta -> { + return buildColumnValue(colMeta, record); + }).collect(Collectors.joining(", ", "(", ") "))); + validRecords++; + } + + if (validRecords == 0) { + LOG.warn("no valid records in this batch"); + return 0; + } + + String sql = sb.toString(); + return executeUpdate(conn, sql); + } + + private boolean equals(Column column, ColumnMeta colMeta) { + switch (column.getType()) { + case BOOL: + return column.asBoolean().equals(Boolean.valueOf(colMeta.value.toString())); + case INT: + case LONG: + return column.asLong().equals(Long.valueOf(colMeta.value.toString())); + case DOUBLE: + return column.asDouble().equals(Double.valueOf(colMeta.value.toString())); + case NULL: + return colMeta.value == null; + case DATE: + return column.asDate().getTime() == ((Timestamp) colMeta.value).getTime(); + case BAD: + case BYTES: + return Arrays.equals(column.asBytes(), (byte[]) colMeta.value); + case STRING: + default: + return column.asString().equals(colMeta.value.toString()); + } + } + + /** + * table: ["weather"], column: ["ts, f1, f2, f3, t1, t2"] + * sql: insert into weather (ts, f1, f2, f3, t1, t2) values( record[idx(ts), record[idx(f1)], ...) + */ + private int writeBatchToNormalTable(Connection conn, String table, List recordBatch) throws SQLException { + List columnMetas = this.tbnameColumnMetasMap.get(table); + + StringBuilder sb = new StringBuilder(); + sb.append("insert into ").append(table) + .append(" ") + .append(columnMetas.stream().filter(colMeta -> !colMeta.isTag).filter(colMeta -> columns.contains(colMeta.field)).map(colMeta -> { + return colMeta.field; + }).collect(Collectors.joining(",", "(", ")"))) + .append(" values "); + + for (Record record : recordBatch) { + sb.append(columnMetas.stream().filter(colMeta -> !colMeta.isTag).filter(colMeta -> columns.contains(colMeta.field)).map(colMeta -> { + return buildColumnValue(colMeta, record); + }).collect(Collectors.joining(",", "(", ")"))); + } + + String sql = sb.toString(); + return executeUpdate(conn, sql); + } + + private int indexOf(String colName) throws DataXException { + for (int i = 0; i < columns.size(); i++) { + if (columns.get(i).equals(colName)) + return i; + } + throw DataXException.asDataXException(TDengineWriterErrorCode.RUNTIME_EXCEPTION, + "cannot find col: " + colName + " in columns: " + columns); + } + +} \ No newline at end of file diff --git a/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/Key.java b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/Key.java new file mode 100644 index 0000000000..1a9358db0a --- /dev/null +++ b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/Key.java @@ -0,0 +1,18 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +public class Key { + public static final String USERNAME = "username"; + public static final String PASSWORD = "password"; + public static final String CONNECTION = "connection"; + public static final String BATCH_SIZE = "batchSize"; + public static final String TABLE = "table"; + public static final String JDBC_URL = "jdbcUrl"; + public static final String COLUMN = "column"; + public static final String IGNORE_TAGS_UNMATCHED = "ignoreTagsUnmatched"; + + public static final String BEGIN_DATETIME = "beginDateTime"; + public static final String END_DATETIME = "endDateTime"; + public static final String WHERE = "where"; + public static final String QUERY_SQL = "querySql"; + public static final String MANDATORY_ENCODING = "mandatoryEncoding"; +} \ No newline at end of file diff --git a/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/OpentsdbDataHandler.java b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/OpentsdbDataHandler.java new file mode 100644 index 0000000000..2fb5a98f51 --- /dev/null +++ b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/OpentsdbDataHandler.java @@ -0,0 +1,99 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; +import com.taosdata.jdbc.SchemalessWriter; +import com.taosdata.jdbc.enums.SchemalessProtocolType; +import com.taosdata.jdbc.enums.SchemalessTimestampType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +public class OpentsdbDataHandler implements DataHandler { + private static final Logger LOG = LoggerFactory.getLogger(OpentsdbDataHandler.class); + private SchemalessWriter writer; + + private String jdbcUrl; + private String user; + private String password; + int batchSize; + + public OpentsdbDataHandler(Configuration config) { + // opentsdb json protocol use JNI and schemaless API to write + this.jdbcUrl = config.getString(Key.JDBC_URL); + this.user = config.getString(Key.USERNAME, "root"); + this.password = config.getString(Key.PASSWORD, "taosdata"); + this.batchSize = config.getInt(Key.BATCH_SIZE, Constants.DEFAULT_BATCH_SIZE); + } + + @Override + public int handle(RecordReceiver lineReceiver, TaskPluginCollector collector) { + int count = 0; + try (Connection conn = DriverManager.getConnection(jdbcUrl, user, password);) { + LOG.info("connection[ jdbcUrl: " + jdbcUrl + ", username: " + user + "] established."); + writer = new SchemalessWriter(conn); + count = write(lineReceiver, batchSize); + } catch (Exception e) { + throw DataXException.asDataXException(TDengineWriterErrorCode.RUNTIME_EXCEPTION, e); + } + + return count; + } + + private int write(RecordReceiver lineReceiver, int batchSize) throws DataXException { + int recordIndex = 1; + try { + Record record; + StringBuilder sb = new StringBuilder(); + while ((record = lineReceiver.getFromReader()) != null) { + if (batchSize == 1) { + String jsonData = recordToString(record); + LOG.debug(">>> " + jsonData); + writer.write(jsonData, SchemalessProtocolType.JSON, SchemalessTimestampType.NOT_CONFIGURED); + } else if (recordIndex % batchSize == 1) { + sb.append("[").append(recordToString(record)).append(","); + } else if (recordIndex % batchSize == 0) { + sb.append(recordToString(record)).append("]"); + String jsonData = sb.toString(); + LOG.debug(">>> " + jsonData); + writer.write(jsonData, SchemalessProtocolType.JSON, SchemalessTimestampType.NOT_CONFIGURED); + sb.delete(0, sb.length()); + } else { + sb.append(recordToString(record)).append(","); + } + recordIndex++; + } + if (sb.length() != 0 && sb.charAt(0) == '[') { + String jsonData = sb.deleteCharAt(sb.length() - 1).append("]").toString(); + System.err.println(jsonData); + LOG.debug(">>> " + jsonData); + writer.write(jsonData, SchemalessProtocolType.JSON, SchemalessTimestampType.NOT_CONFIGURED); + } + } catch (Exception e) { + throw DataXException.asDataXException(TDengineWriterErrorCode.RUNTIME_EXCEPTION, e); + } + return recordIndex - 1; + } + + private String recordToString(Record record) { + int recordLength = record.getColumnNumber(); + if (0 == recordLength) { + return ""; + } + Column column; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < recordLength; i++) { + column = record.getColumn(i); + sb.append(column.asString()).append("\t"); + } + sb.setLength(sb.length() - 1); + return sb.toString(); + } +} diff --git a/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/SchemaManager.java b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/SchemaManager.java new file mode 100644 index 0000000000..fc0c002d91 --- /dev/null +++ b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/SchemaManager.java @@ -0,0 +1,206 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.common.exception.DataXException; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.*; +import java.util.*; +import java.util.stream.Collectors; + +public class SchemaManager { + private static final Logger LOG = LoggerFactory.getLogger(SchemaManager.class); +// private static final String TAG_TABLE_NAME_MAP_KEY_SPLITTER = "_"; + private static final String TAG_TABLE_NAME_MAP_KEY_SPLITTER = ""; + + private final Connection conn; + private TimestampPrecision precision; + private Map> tags2tbnameMaps = new HashMap<>(); + + public SchemaManager(Connection conn) { + this.conn = conn; + } + + public TimestampPrecision loadDatabasePrecision() throws DataXException { + if (this.precision != null) + return this.precision; + + try (Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery("select database()"); + String dbname = null; + while (rs.next()) { + dbname = rs.getString("database()"); + } + if (dbname == null) + throw DataXException.asDataXException(TDengineWriterErrorCode.RUNTIME_EXCEPTION, + "Database not specified or available"); + + rs = stmt.executeQuery("show databases"); + while (rs.next()) { + String name = rs.getString("name"); + if (!name.equalsIgnoreCase(dbname)) + continue; + String precision = rs.getString("precision"); + switch (precision) { + case "ns": + this.precision = TimestampPrecision.NANOSEC; + break; + case "us": + this.precision = TimestampPrecision.MICROSEC; + break; + case "ms": + default: + this.precision = TimestampPrecision.MILLISEC; + } + } + } catch (SQLException e) { + throw DataXException.asDataXException(TDengineWriterErrorCode.RUNTIME_EXCEPTION, e.getMessage()); + } + return this.precision; + } + + public Map loadTableMeta(List tables) throws DataXException { + Map tableMetas = new HashMap(); + + try (Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery("show stables"); + while (rs.next()) { + TableMeta tableMeta = buildSupTableMeta(rs); + if (!tables.contains(tableMeta.tbname)) + continue; + tableMetas.put(tableMeta.tbname, tableMeta); + } + + rs = stmt.executeQuery("show tables"); + while (rs.next()) { + TableMeta tableMeta = buildSubTableMeta(rs); + if (!tables.contains(tableMeta.tbname)) + continue; + tableMetas.put(tableMeta.tbname, tableMeta); + } + + for (String tbname : tables) { + if (!tableMetas.containsKey(tbname)) { + throw DataXException.asDataXException(TDengineWriterErrorCode.RUNTIME_EXCEPTION, "table metadata of " + tbname + " is empty!"); + } + } + } catch (SQLException e) { + throw DataXException.asDataXException(TDengineWriterErrorCode.RUNTIME_EXCEPTION, e.getMessage()); + } + return tableMetas; + } + + public Map> loadColumnMetas(List tables) throws DataXException { + Map> ret = new HashMap<>(); + + for (String table : tables) { + List columnMetaList = new ArrayList<>(); + try (Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery("describe " + table); + for (int i = 0; rs.next(); i++) { + ColumnMeta columnMeta = buildColumnMeta(rs, i == 0); + columnMetaList.add(columnMeta); + } + } catch (SQLException e) { + throw DataXException.asDataXException(TDengineWriterErrorCode.RUNTIME_EXCEPTION, e.getMessage()); + } + + if (columnMetaList.isEmpty()) { + LOG.error("column metadata of " + table + " is empty!"); + continue; + } + + columnMetaList.stream().filter(colMeta -> colMeta.isTag).forEach(colMeta -> { + String sql = "select " + colMeta.field + " from " + table; + Object value = null; + try (Statement stmt = conn.createStatement()) { + ResultSet rs = stmt.executeQuery(sql); + for (int i = 0; rs.next(); i++) { + value = rs.getObject(colMeta.field); + if (i > 0) { + value = null; + break; + } + } + } catch (SQLException e) { + e.printStackTrace(); + } + colMeta.value = value; + }); + + LOG.debug("load column metadata of " + table + ": " + Arrays.toString(columnMetaList.toArray())); + ret.put(table, columnMetaList); + } + return ret; + } + + private TableMeta buildSupTableMeta(ResultSet rs) throws SQLException { + TableMeta tableMeta = new TableMeta(); + tableMeta.tableType = TableType.SUP_TABLE; + tableMeta.tbname = rs.getString("name"); + tableMeta.columns = rs.getInt("columns"); + tableMeta.tags = rs.getInt("tags"); + tableMeta.tables = rs.getInt("tables"); + + LOG.debug("load table metadata of " + tableMeta.tbname + ": " + tableMeta); + return tableMeta; + } + + private TableMeta buildSubTableMeta(ResultSet rs) throws SQLException { + TableMeta tableMeta = new TableMeta(); + String stable_name = rs.getString("stable_name"); + tableMeta.tableType = StringUtils.isBlank(stable_name) ? TableType.NML_TABLE : TableType.SUB_TABLE; + tableMeta.tbname = rs.getString("table_name"); + tableMeta.columns = rs.getInt("columns"); + tableMeta.stable_name = StringUtils.isBlank(stable_name) ? null : stable_name; + + LOG.debug("load table metadata of " + tableMeta.tbname + ": " + tableMeta); + return tableMeta; + } + + private ColumnMeta buildColumnMeta(ResultSet rs, boolean isPrimaryKey) throws SQLException { + ColumnMeta columnMeta = new ColumnMeta(); + columnMeta.field = rs.getString("Field"); + columnMeta.type = rs.getString("Type"); + columnMeta.length = rs.getInt("Length"); + columnMeta.note = rs.getString("Note"); + columnMeta.isTag = columnMeta.note != null && columnMeta.note.equals("TAG"); + columnMeta.isPrimaryKey = isPrimaryKey; + return columnMeta; + } + + public Map loadTagTableNameMap(String table) throws SQLException { + if (tags2tbnameMaps.containsKey(table)) + return tags2tbnameMaps.get(table); + Map tags2tbname = new HashMap<>(); + try (Statement stmt = conn.createStatement()) { + // describe table + List tags = new ArrayList<>(); + ResultSet rs = stmt.executeQuery("describe " + table); + while (rs.next()) { + String note = rs.getString("Note"); + if ("TAG".equals(note)) { + tags.add(rs.getString("Field")); + } + } + // select distinct tbname, t1, t2 from stb + rs = stmt.executeQuery("select distinct " + String.join(",", tags) + ",tbname from " + table); + while (rs.next()) { + ResultSet finalRs = rs; + String tagStr = tags.stream().map(t -> { + try { + return finalRs.getString(t); + } catch (SQLException e) { + LOG.error(e.getMessage(), e); + } + return "NULL"; + }).collect(Collectors.joining(TAG_TABLE_NAME_MAP_KEY_SPLITTER)); + String tbname = rs.getString("tbname"); + tags2tbname.put(tagStr, tbname); + } + } + tags2tbnameMaps.put(table, tags2tbname); + return tags2tbname; + } +} diff --git a/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TDengineWriter.java b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TDengineWriter.java new file mode 100644 index 0000000000..7398274496 --- /dev/null +++ b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TDengineWriter.java @@ -0,0 +1,114 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.common.exception.DataXException; +import com.alibaba.datax.common.plugin.RecordReceiver; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.spi.Writer; +import com.alibaba.datax.common.util.Configuration; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class TDengineWriter extends Writer { + + private static final String PEER_PLUGIN_NAME = "peerPluginName"; + + public static class Job extends Writer.Job { + + private Configuration originalConfig; + private static final Logger LOG = LoggerFactory.getLogger(Job.class); + + @Override + public void init() { + this.originalConfig = super.getPluginJobConf(); + this.originalConfig.set(PEER_PLUGIN_NAME, getPeerPluginName()); + + // check username + String user = this.originalConfig.getString(Key.USERNAME); + if (StringUtils.isBlank(user)) + throw DataXException.asDataXException(TDengineWriterErrorCode.REQUIRED_VALUE, "The parameter [" + + Key.USERNAME + "] is not set."); + + // check password + String password = this.originalConfig.getString(Key.PASSWORD); + if (StringUtils.isBlank(password)) + throw DataXException.asDataXException(TDengineWriterErrorCode.REQUIRED_VALUE, "The parameter [" + + Key.PASSWORD + "] is not set."); + + // check connection + List connection = this.originalConfig.getList(Key.CONNECTION); + if (connection == null || connection.isEmpty()) + throw DataXException.asDataXException(TDengineWriterErrorCode.REQUIRED_VALUE, "The parameter [" + + Key.CONNECTION + "] is not set."); + if (connection.size() > 1) + LOG.warn("connection.size is " + connection.size() + " and only connection[0] will be used."); + Configuration conn = Configuration.from(connection.get(0).toString()); + String jdbcUrl = conn.getString(Key.JDBC_URL); + if (StringUtils.isBlank(jdbcUrl)) + throw DataXException.asDataXException(TDengineWriterErrorCode.REQUIRED_VALUE, "The parameter [" + + Key.JDBC_URL + "] of connection is not set."); + + // check column + } + + @Override + public void destroy() { + + } + + @Override + public List split(int mandatoryNumber) { + List writerSplitConfigs = new ArrayList<>(); + + List conns = this.originalConfig.getList(Key.CONNECTION); + for (int i = 0; i < mandatoryNumber; i++) { + Configuration clone = this.originalConfig.clone(); + Configuration conf = Configuration.from(conns.get(0).toString()); + String jdbcUrl = conf.getString(Key.JDBC_URL); + clone.set(Key.JDBC_URL, jdbcUrl); + clone.set(Key.TABLE, conf.getList(Key.TABLE)); + clone.remove(Key.CONNECTION); + writerSplitConfigs.add(clone); + } + + return writerSplitConfigs; + } + } + + public static class Task extends Writer.Task { + private static final Logger LOG = LoggerFactory.getLogger(Task.class); + + private Configuration writerSliceConfig; + private TaskPluginCollector taskPluginCollector; + + @Override + public void init() { + this.writerSliceConfig = getPluginJobConf(); + this.taskPluginCollector = super.getTaskPluginCollector(); + } + + @Override + public void destroy() { + + } + + @Override + public void startWrite(RecordReceiver lineReceiver) { + String peerPluginName = this.writerSliceConfig.getString(PEER_PLUGIN_NAME); + LOG.debug("start to handle record from: " + peerPluginName); + + DataHandler handler; + if (peerPluginName.equals("opentsdbreader")) + handler = new OpentsdbDataHandler(this.writerSliceConfig); + else + handler = new DefaultDataHandler(this.writerSliceConfig, this.taskPluginCollector); + + long records = handler.handle(lineReceiver, getTaskPluginCollector()); + LOG.debug("handle data finished, records: " + records); + } + + } +} diff --git a/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TDengineWriterErrorCode.java b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TDengineWriterErrorCode.java new file mode 100644 index 0000000000..469449e63a --- /dev/null +++ b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TDengineWriterErrorCode.java @@ -0,0 +1,34 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.common.spi.ErrorCode; + +public enum TDengineWriterErrorCode implements ErrorCode { + + REQUIRED_VALUE("TDengineWriter-00", "缺失必要的值"), + ILLEGAL_VALUE("TDengineWriter-01", "值非法"), + RUNTIME_EXCEPTION("TDengineWriter-02", "运行时异常"), + TYPE_ERROR("TDengineWriter-03", "Datax类型无法正确映射到TDengine类型"); + + private final String code; + private final String description; + + TDengineWriterErrorCode(String code, String description) { + this.code = code; + this.description = description; + } + + @Override + public String getCode() { + return this.code; + } + + @Override + public String getDescription() { + return this.description; + } + + @Override + public String toString() { + return String.format("Code:[%s], Description:[%s]. ", this.code, this.description); + } +} diff --git a/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TableMeta.java b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TableMeta.java new file mode 100644 index 0000000000..2a6b053697 --- /dev/null +++ b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TableMeta.java @@ -0,0 +1,22 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +public class TableMeta { + TableType tableType; + String tbname; + int columns; + int tags; + int tables; + String stable_name; + + @Override + public String toString() { + return "TableMeta{" + + "tableType=" + tableType + + ", tbname='" + tbname + '\'' + + ", columns=" + columns + + ", tags=" + tags + + ", tables=" + tables + + ", stable_name='" + stable_name + '\'' + + '}'; + } +} diff --git a/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TableType.java b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TableType.java new file mode 100644 index 0000000000..6c97ac3fe0 --- /dev/null +++ b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TableType.java @@ -0,0 +1,5 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +public enum TableType { + SUP_TABLE, SUB_TABLE, NML_TABLE +} diff --git a/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TimestampPrecision.java b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TimestampPrecision.java new file mode 100644 index 0000000000..46564d6a6a --- /dev/null +++ b/tdenginewriter/src/main/java/com/alibaba/datax/plugin/writer/tdenginewriter/TimestampPrecision.java @@ -0,0 +1,5 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +public enum TimestampPrecision { + MILLISEC, MICROSEC, NANOSEC +} diff --git a/tdenginewriter/src/main/resources/plugin.json b/tdenginewriter/src/main/resources/plugin.json new file mode 100644 index 0000000000..10d8e2cf1f --- /dev/null +++ b/tdenginewriter/src/main/resources/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "tdenginewriter", + "class": "com.alibaba.datax.plugin.writer.tdenginewriter.TDengineWriter", + "description": { + "useScene": "data migration to tdengine", + "mechanism": "use taos-jdbcdriver to write data." + }, + "developer": "support@taosdata.com" +} \ No newline at end of file diff --git a/tdenginewriter/src/main/resources/plugin_job_template.json b/tdenginewriter/src/main/resources/plugin_job_template.json new file mode 100644 index 0000000000..3d30330548 --- /dev/null +++ b/tdenginewriter/src/main/resources/plugin_job_template.json @@ -0,0 +1,20 @@ +{ + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "" + ], + "connection": [ + { + "table": [ + "" + ], + "jdbcUrl": "" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Csv2TDengineTest.java b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Csv2TDengineTest.java new file mode 100644 index 0000000000..7352c3cab9 --- /dev/null +++ b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Csv2TDengineTest.java @@ -0,0 +1,41 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.core.Engine; +import org.junit.Ignore; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +@Ignore +public class Csv2TDengineTest { + + private static final String host = "192.168.56.105"; + + @Test + public void case01() throws Throwable { + // given + prepareTable(); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/csv2t.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + public void prepareTable() throws SQLException { + final String url = "jdbc:TAOS-RS://" + host + ":6041"; + try (Connection conn = DriverManager.getConnection(url, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + + stmt.execute("drop database if exists test"); + stmt.execute("create database if not exists test"); + stmt.execute("create table test.weather (ts timestamp, temperature bigint, humidity double, is_normal bool) " + + "tags(device_id binary(10),address nchar(10))"); + } + } + + +} diff --git a/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/DM2TDengineTest.java b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/DM2TDengineTest.java new file mode 100644 index 0000000000..15f6b1bcd0 --- /dev/null +++ b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/DM2TDengineTest.java @@ -0,0 +1,122 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.core.Engine; +import org.junit.Before; +import org.junit.Test; + +import java.sql.*; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Random; + +public class DM2TDengineTest { + + private String host1 = "192.168.0.72"; + private String host2 = "192.168.1.93"; + private final Random random = new Random(System.currentTimeMillis()); + + @Test + public void dm2t_case01() throws Throwable { + // given + createSupTable(); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/dm2t-1.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + @Test + public void dm2t_case02() throws Throwable { + // given + createSupAndSubTable(); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/dm2t-2.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + @Test + public void dm2t_case03() throws Throwable { + // given + createTable(); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/dm2t-3.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + @Test + public void dm2t_case04() throws Throwable { + // given + createSupTable(); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/dm2t-4.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + private void createSupTable() throws SQLException { + final String url2 = "jdbc:TAOS-RS://" + host2 + ":6041"; + try (Connection conn = DriverManager.getConnection(url2, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + stmt.execute("drop database if exists db2"); + stmt.execute("create database if not exists db2"); + stmt.execute("create table db2.stb2(ts timestamp, f2 smallint, f4 bigint,f5 float, " + + "f6 double, f7 double, f8 bool, f9 nchar(100), f10 nchar(200)) tags(f1 tinyint,f3 int)"); + stmt.close(); + } + } + + private void createSupAndSubTable() throws SQLException { + final String url2 = "jdbc:TAOS-RS://" + host2 + ":6041"; + try (Connection conn = DriverManager.getConnection(url2, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + stmt.execute("drop database if exists db2"); + stmt.execute("create database if not exists db2"); + stmt.execute("create table db2.stb2(ts timestamp, f2 smallint, f4 bigint,f5 float, " + + "f6 double, f7 double, f8 bool, f9 nchar(100), f10 nchar(200)) tags(f1 tinyint,f3 int)"); + for (int i = 0; i < 10; i++) { + stmt.execute("create table db2.t" + (i + 1) + "_" + i + " using db2.stb2 tags(" + (i + 1) + "," + i + ")"); + } + stmt.close(); + } + } + + private void createTable() throws SQLException { + final String url2 = "jdbc:TAOS-RS://" + host2 + ":6041"; + try (Connection conn = DriverManager.getConnection(url2, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + stmt.execute("drop database if exists db2"); + stmt.execute("create database if not exists db2"); + stmt.execute("create table db2.stb2(ts timestamp, f1 tinyint, f2 smallint, f3 int, f4 bigint,f5 float, " + + "f6 double, f7 double, f8 bool, f9 nchar(100), f10 nchar(200))"); + stmt.close(); + } + } + + @Before + public void before() throws SQLException { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + long ts = System.currentTimeMillis(); + + final String url = "jdbc:dm://" + host1 + ":5236"; + try (Connection conn = DriverManager.getConnection(url, "TESTUSER", "test123456")) { + conn.setAutoCommit(true); + Statement stmt = conn.createStatement(); + stmt.execute("drop table if exists stb1"); + stmt.execute("create table stb1(ts timestamp, f1 tinyint, f2 smallint, f3 int, f4 bigint, f5 float, " + + "f6 double, f7 NUMERIC(10,2), f8 BIT, f9 VARCHAR(100), f10 VARCHAR2(200))"); + for (int i = 0; i < 10; i++) { + String sql = "insert into stb1 values('" + sdf.format(new Date(ts + i * 1000)) + "'," + (i + 1) + "," + + random.nextInt(100) + "," + i + ",4,5.55,6.666,7.77," + (random.nextBoolean() ? 1 : 0) + + ",'abcABC123','北京朝阳望京DM')"; + stmt.execute(sql); + } + } + } + +} diff --git a/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/DefaultDataHandlerTest.java b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/DefaultDataHandlerTest.java new file mode 100644 index 0000000000..46e601ad4a --- /dev/null +++ b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/DefaultDataHandlerTest.java @@ -0,0 +1,301 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.common.element.DateColumn; +import com.alibaba.datax.common.element.LongColumn; +import com.alibaba.datax.common.element.Record; +import com.alibaba.datax.common.element.StringColumn; +import com.alibaba.datax.common.plugin.TaskPluginCollector; +import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.core.transport.record.DefaultRecord; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class DefaultDataHandlerTest { + + private static final String host = "192.168.1.93"; + private static Connection conn; + + private final TaskPluginCollector taskPluginCollector = new TDengineWriter.Task().getTaskPluginCollector(); + + @Test + public void writeSupTableBySQL() throws SQLException { + // given + createSupAndSubTable(); + Configuration configuration = Configuration.from("{" + + "\"username\": \"root\"," + + "\"password\": \"taosdata\"," + + "\"column\": [\"tbname\", \"ts\", \"f1\", \"f2\", \"t1\"]," + + "\"table\":[\"stb1\"]," + + "\"jdbcUrl\":\"jdbc:TAOS-RS://" + host + ":6041/test\"," + + "\"batchSize\": \"1000\"" + + "}"); + long current = System.currentTimeMillis(); + List recordList = IntStream.range(1, 11).mapToObj(i -> { + Record record = new DefaultRecord(); + record.addColumn(new StringColumn("tb" + (i + 10))); + record.addColumn(new DateColumn(current + 1000 * i)); + record.addColumn(new LongColumn(1)); + record.addColumn(new LongColumn(2)); + record.addColumn(new LongColumn(i)); + return record; + }).collect(Collectors.toList()); + + + // when + DefaultDataHandler handler = new DefaultDataHandler(configuration, taskPluginCollector); + List tables = configuration.getList("table", String.class); + SchemaManager schemaManager = new SchemaManager(conn); + Map tableMetas = schemaManager.loadTableMeta(tables); + Map> columnMetas = schemaManager.loadColumnMetas(tables); + handler.setTableMetas(tableMetas); + handler.setTbnameColumnMetasMap(columnMetas); + handler.setSchemaManager(schemaManager); + + int count = handler.writeBatch(conn, recordList); + + // then + Assert.assertEquals(10, count); + } + + @Test + public void writeSupTableBySQL_2() throws SQLException { + // given + createSupAndSubTable(); + Configuration configuration = Configuration.from("{" + + "\"username\": \"root\"," + + "\"password\": \"taosdata\"," + + "\"column\": [\"tbname\", \"ts\", \"f1\", \"t1\"]," + + "\"table\":[\"stb1\"]," + + "\"jdbcUrl\":\"jdbc:TAOS-RS://" + host + ":6041/test\"," + + "\"batchSize\": \"1000\"" + + "}"); + long current = System.currentTimeMillis(); + List recordList = IntStream.range(1, 11).mapToObj(i -> { + Record record = new DefaultRecord(); + record.addColumn(new StringColumn("tb" + (i + 10))); + record.addColumn(new DateColumn(current + 1000 * i)); + record.addColumn(new LongColumn(1)); + record.addColumn(new LongColumn(i)); + return record; + }).collect(Collectors.toList()); + + // when + DefaultDataHandler handler = new DefaultDataHandler(configuration, taskPluginCollector); + List tables = configuration.getList("table", String.class); + SchemaManager schemaManager = new SchemaManager(conn); + Map tableMetas = schemaManager.loadTableMeta(tables); + Map> columnMetas = schemaManager.loadColumnMetas(tables); + handler.setTableMetas(tableMetas); + handler.setTbnameColumnMetasMap(columnMetas); + handler.setSchemaManager(schemaManager); + + int count = handler.writeBatch(conn, recordList); + + // then + Assert.assertEquals(10, count); + } + + @Test + public void writeSupTableBySchemaless() throws SQLException { + // given + createSupTable(); + Configuration configuration = Configuration.from("{" + + "\"username\": \"root\"," + + "\"password\": \"taosdata\"," + + "\"column\": [\"ts\", \"f1\", \"f2\", \"t1\"]," + + "\"table\":[\"stb1\"]," + + "\"jdbcUrl\":\"jdbc:TAOS://" + host + ":6030/scm_test\"," + + "\"batchSize\": \"1000\"" + + "}"); + String jdbcUrl = configuration.getString("jdbcUrl"); + Connection connection = DriverManager.getConnection(jdbcUrl, "root", "taosdata"); + long current = System.currentTimeMillis(); + List recordList = IntStream.range(1, 11).mapToObj(i -> { + Record record = new DefaultRecord(); + record.addColumn(new DateColumn(current + 1000 * i)); + record.addColumn(new LongColumn(1)); + record.addColumn(new LongColumn(2)); + record.addColumn(new StringColumn("t" + i + " 22")); + return record; + }).collect(Collectors.toList()); + + // when + DefaultDataHandler handler = new DefaultDataHandler(configuration, taskPluginCollector); + List tables = configuration.getList("table", String.class); + SchemaManager schemaManager = new SchemaManager(connection); + Map tableMetas = schemaManager.loadTableMeta(tables); + Map> columnMetas = schemaManager.loadColumnMetas(tables); + handler.setTableMetas(tableMetas); + handler.setTbnameColumnMetasMap(columnMetas); + handler.setSchemaManager(schemaManager); + + int count = handler.writeBatch(connection, recordList); + + // then + Assert.assertEquals(10, count); + } + + @Test + public void writeSubTableWithTableName() throws SQLException { + // given + createSupAndSubTable(); + Configuration configuration = Configuration.from("{" + + "\"username\": \"root\"," + + "\"password\": \"taosdata\"," + + "\"column\": [\"tbname\", \"ts\", \"f1\", \"f2\", \"t1\"]," + + "\"table\":[\"tb1\"]," + + "\"jdbcUrl\":\"jdbc:TAOS-RS://" + host + ":6041/test\"," + + "\"batchSize\": \"1000\"" + + "}"); + long current = System.currentTimeMillis(); + List recordList = IntStream.range(1, 11).mapToObj(i -> { + Record record = new DefaultRecord(); + record.addColumn(new StringColumn("tb" + i)); + record.addColumn(new DateColumn(current + 1000 * i)); + record.addColumn(new LongColumn(1)); + record.addColumn(new LongColumn(2)); + record.addColumn(new LongColumn(i)); + return record; + }).collect(Collectors.toList()); + + // when + DefaultDataHandler handler = new DefaultDataHandler(configuration, taskPluginCollector); + List tables = configuration.getList("table", String.class); + SchemaManager schemaManager = new SchemaManager(conn); + Map tableMetas = schemaManager.loadTableMeta(tables); + Map> columnMetas = schemaManager.loadColumnMetas(tables); + handler.setTableMetas(tableMetas); + handler.setTbnameColumnMetasMap(columnMetas); + handler.setSchemaManager(schemaManager); + + int count = handler.writeBatch(conn, recordList); + + // then + Assert.assertEquals(1, count); + } + + @Test + public void writeSubTableWithoutTableName() throws SQLException { + // given + createSupAndSubTable(); + Configuration configuration = Configuration.from("{" + + "\"username\": \"root\"," + + "\"password\": \"taosdata\"," + + "\"column\": [\"ts\", \"f1\", \"f2\", \"t1\"]," + + "\"table\":[\"tb1\"]," + + "\"jdbcUrl\":\"jdbc:TAOS-RS://" + host + ":6041/test\"," + + "\"batchSize\": \"1000\"," + + "\"ignoreTagsUnmatched\": \"true\"" + + "}"); + long current = System.currentTimeMillis(); + List recordList = IntStream.range(1, 11).mapToObj(i -> { + Record record = new DefaultRecord(); + record.addColumn(new DateColumn(current + 1000 * i)); + record.addColumn(new LongColumn(1)); + record.addColumn(new LongColumn(2)); + record.addColumn(new LongColumn(i)); + return record; + }).collect(Collectors.toList()); + + // when + DefaultDataHandler handler = new DefaultDataHandler(configuration, taskPluginCollector); + List tables = configuration.getList("table", String.class); + SchemaManager schemaManager = new SchemaManager(conn); + Map tableMetas = schemaManager.loadTableMeta(tables); + Map> columnMetas = schemaManager.loadColumnMetas(tables); + handler.setTableMetas(tableMetas); + handler.setTbnameColumnMetasMap(columnMetas); + handler.setSchemaManager(schemaManager); + + int count = handler.writeBatch(conn, recordList); + + // then + Assert.assertEquals(1, count); + } + + @Test + public void writeNormalTable() throws SQLException { + // given + createSupAndSubTable(); + Configuration configuration = Configuration.from("{" + + "\"username\": \"root\"," + + "\"password\": \"taosdata\"," + + "\"column\": [\"ts\", \"f1\", \"f2\", \"t1\"]," + + "\"table\":[\"weather\"]," + + "\"jdbcUrl\":\"jdbc:TAOS-RS://" + host + ":6041/test\"," + + "\"batchSize\": \"1000\"," + + "\"ignoreTagsUnmatched\": \"true\"" + + "}"); + long current = System.currentTimeMillis(); + List recordList = IntStream.range(1, 11).mapToObj(i -> { + Record record = new DefaultRecord(); + record.addColumn(new DateColumn(current + 1000 * i)); + record.addColumn(new LongColumn(1)); + record.addColumn(new LongColumn(2)); + record.addColumn(new LongColumn(i)); + return record; + }).collect(Collectors.toList()); + + // when + DefaultDataHandler handler = new DefaultDataHandler(configuration, taskPluginCollector); + List tables = configuration.getList("table", String.class); + SchemaManager schemaManager = new SchemaManager(conn); + Map tableMetas = schemaManager.loadTableMeta(tables); + Map> columnMetas = schemaManager.loadColumnMetas(tables); + handler.setTableMetas(tableMetas); + handler.setTbnameColumnMetasMap(columnMetas); + handler.setSchemaManager(schemaManager); + + int count = handler.writeBatch(conn, recordList); + + // then + Assert.assertEquals(10, count); + } + + private void createSupAndSubTable() throws SQLException { + try (Statement stmt = conn.createStatement()) { + stmt.execute("drop database if exists scm_test"); + stmt.execute("create database if not exists scm_test"); + stmt.execute("use scm_test"); + stmt.execute("create table stb1(ts timestamp, f1 int, f2 int) tags(t1 nchar(32))"); + stmt.execute("create table stb2(ts timestamp, f1 int, f2 int, f3 int) tags(t1 int, t2 int)"); + stmt.execute("create table tb1 using stb1 tags(1)"); + stmt.execute("create table tb2 using stb1 tags(2)"); + stmt.execute("create table tb3 using stb2 tags(1,1)"); + stmt.execute("create table tb4 using stb2 tags(2,2)"); + stmt.execute("create table weather(ts timestamp, f1 int, f2 int, f3 int, t1 int, t2 int)"); + } + } + + private void createSupTable() throws SQLException { + try (Statement stmt = conn.createStatement()) { + stmt.execute("drop database if exists scm_test"); + stmt.execute("create database if not exists scm_test"); + stmt.execute("use scm_test"); + stmt.execute("create table stb1(ts timestamp, f1 int, f2 int) tags(t1 nchar(32))"); + } + } + + @BeforeClass + public static void beforeClass() throws SQLException { + conn = DriverManager.getConnection("jdbc:TAOS-RS://" + host + ":6041", "root", "taosdata"); + } + + @AfterClass + public static void afterClass() throws SQLException { + if (conn != null) { + conn.close(); + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Mongo2TDengineTest.java b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Mongo2TDengineTest.java new file mode 100644 index 0000000000..2356b6f808 --- /dev/null +++ b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Mongo2TDengineTest.java @@ -0,0 +1,16 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.core.Engine; +import org.junit.Test; + +public class Mongo2TDengineTest { + + @Test + public void case01() throws Throwable { + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/mongo2t.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Mysql2TDengineTest.java b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Mysql2TDengineTest.java new file mode 100644 index 0000000000..4a662711f3 --- /dev/null +++ b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Mysql2TDengineTest.java @@ -0,0 +1,70 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.core.Engine; +import org.junit.Before; +import org.junit.Test; + +import java.sql.*; +import java.text.SimpleDateFormat; +import java.util.Random; + +public class Mysql2TDengineTest { + + private static final String host1 = "192.168.56.105"; + private static final String host2 = "192.168.1.93"; + private static final Random random = new Random(System.currentTimeMillis()); + + @Test + public void mysql2tdengine() throws Throwable { + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/m2t-1.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + @Before + public void before() throws SQLException { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + String ts = sdf.format(new Date(System.currentTimeMillis())); + + final String url = "jdbc:mysql://" + host1 + ":3306/?useSSL=false&useUnicode=true&charset=UTF-8&generateSimpleParameterMetadata=true"; + try (Connection conn = DriverManager.getConnection(url, "root", "123456")) { + Statement stmt = conn.createStatement(); + + stmt.execute("drop database if exists db1"); + stmt.execute("create database if not exists db1"); + stmt.execute("use db1"); + stmt.execute("create table stb1(id int primary key AUTO_INCREMENT, " + + "f1 tinyint, f2 smallint, f3 int, f4 bigint, " + + "f5 float, f6 double, " + + "ts timestamp, dt datetime," + + "f7 nchar(100), f8 varchar(100))"); + for (int i = 1; i <= 10; i++) { + String sql = "insert into stb1(f1, f2, f3, f4, f5, f6, ts, dt, f7, f8) values(" + + i + "," + random.nextInt(100) + "," + random.nextInt(100) + "," + random.nextInt(100) + "," + + random.nextFloat() + "," + random.nextDouble() + ", " + + "'" + ts + "', '" + ts + "', " + + "'中国北京朝阳望京abc', '中国北京朝阳望京adc')"; + stmt.execute(sql); + } + + stmt.close(); + } + + final String url2 = "jdbc:TAOS-RS://" + host2 + ":6041/"; + try (Connection conn = DriverManager.getConnection(url2, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + + stmt.execute("drop database if exists db2"); + stmt.execute("create database if not exists db2"); + stmt.execute("create table db2.stb2(" + + "ts timestamp, dt timestamp, " + + "f1 tinyint, f2 smallint, f3 int, f4 bigint, " + + "f5 float, f6 double, " + + "f7 nchar(100), f8 nchar(100))"); + + stmt.close(); + } + + } + +} diff --git a/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Opentsdb2TDengineTest.java b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Opentsdb2TDengineTest.java new file mode 100644 index 0000000000..ad326f7e33 --- /dev/null +++ b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Opentsdb2TDengineTest.java @@ -0,0 +1,36 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.core.Engine; +import org.junit.Assert; +import org.junit.Test; + +import java.sql.*; + +public class Opentsdb2TDengineTest { + + @Test + public void opentsdb2tdengine() throws SQLException { + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/o2t-1.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + try { + Engine.entry(params); + } catch (Throwable e) { + e.printStackTrace(); + } + + // assert + String jdbcUrl = "jdbc:TAOS://192.168.56.105:6030/test?timestampFormat=TIMESTAMP"; + try (Connection conn = DriverManager.getConnection(jdbcUrl, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("select count(*) from weather_temperature"); + int rows = 0; + while (rs.next()) { + rows = rs.getInt("count(*)"); + } + Assert.assertEquals(5, rows); + stmt.close(); + } + } + +} diff --git a/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/SchemaManagerTest.java b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/SchemaManagerTest.java new file mode 100644 index 0000000000..3708e6f967 --- /dev/null +++ b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/SchemaManagerTest.java @@ -0,0 +1,107 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +public class SchemaManagerTest { + + private static Connection conn; + + @Test + public void loadTableMeta() throws SQLException { + // given + SchemaManager schemaManager = new SchemaManager(conn); + List tables = Arrays.asList("stb1", "stb2", "tb1", "tb3", "weather"); + + // when + Map tableMetaMap = schemaManager.loadTableMeta(tables); + + // then + TableMeta stb1 = tableMetaMap.get("stb1"); + Assert.assertEquals(TableType.SUP_TABLE, stb1.tableType); + Assert.assertEquals("stb1", stb1.tbname); + Assert.assertEquals(3, stb1.columns); + Assert.assertEquals(1, stb1.tags); + Assert.assertEquals(2, stb1.tables); + + TableMeta tb3 = tableMetaMap.get("tb3"); + Assert.assertEquals(TableType.SUB_TABLE, tb3.tableType); + Assert.assertEquals("tb3", tb3.tbname); + Assert.assertEquals(4, tb3.columns); + Assert.assertEquals("stb2", tb3.stable_name); + + TableMeta weather = tableMetaMap.get("weather"); + Assert.assertEquals(TableType.NML_TABLE, weather.tableType); + Assert.assertEquals("weather", weather.tbname); + Assert.assertEquals(6, weather.columns); + Assert.assertNull(weather.stable_name); + } + + @Test + public void loadColumnMetas() { + // given + SchemaManager schemaManager = new SchemaManager(conn); + List tables = Arrays.asList("stb1", "stb2", "tb1", "tb3", "weather"); + + // when + Map> columnMetaMap = schemaManager.loadColumnMetas(tables); + + // then + List stb1 = columnMetaMap.get("stb1"); + Assert.assertEquals(4, stb1.size()); + } + + @Test + public void loadTagTableNameMap() throws SQLException { + // given + SchemaManager schemaManager = new SchemaManager(conn); + String table = "stb3"; + + // when + Map tagTableMap = schemaManager.loadTagTableNameMap(table); + + // then + Assert.assertEquals(2, tagTableMap.keySet().size()); + Assert.assertTrue(tagTableMap.containsKey("11.1abc")); + Assert.assertTrue(tagTableMap.containsKey("22.2defg")); + Assert.assertEquals("tb5", tagTableMap.get("11.1abc")); + Assert.assertEquals("tb6", tagTableMap.get("22.2defg")); + } + + @BeforeClass + public static void beforeClass() throws SQLException { + conn = DriverManager.getConnection("jdbc:TAOS-RS://192.168.56.105:6041", "root", "taosdata"); + try (Statement stmt = conn.createStatement()) { + stmt.execute("drop database if exists scm_test"); + stmt.execute("create database if not exists scm_test"); + stmt.execute("use scm_test"); + stmt.execute("create table stb1(ts timestamp, f1 int, f2 int) tags(t1 int)"); + stmt.execute("create table stb2(ts timestamp, f1 int, f2 int, f3 int) tags(t1 int, t2 int)"); + stmt.execute("insert into tb1 using stb1 tags(1) values(now, 1, 2)"); + stmt.execute("insert into tb2 using stb1 tags(2) values(now, 1, 2)"); + stmt.execute("insert into tb3 using stb2 tags(1,1) values(now, 1, 2, 3)"); + stmt.execute("insert into tb4 using stb2 tags(2,2) values(now, 1, 2, 3)"); + stmt.execute("create table weather(ts timestamp, f1 int, f2 int, f3 int, t1 int, t2 int)"); + stmt.execute("create table stb3(ts timestamp, f1 int) tags(t1 int, t2 float, t3 nchar(32))"); + stmt.execute("insert into tb5 using stb3 tags(1,1.1,'abc') values(now, 1)"); + stmt.execute("insert into tb6 using stb3 tags(2,2.2,'defg') values(now, 2)"); + } + } + + @AfterClass + public static void afterClass() throws SQLException { + if (conn != null) { + conn.close(); + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Stream2TDengineTest.java b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Stream2TDengineTest.java new file mode 100644 index 0000000000..e54bcbde02 --- /dev/null +++ b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/Stream2TDengineTest.java @@ -0,0 +1,69 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.core.Engine; +import org.junit.Before; +import org.junit.Test; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; + +public class Stream2TDengineTest { + + private String host2 = "192.168.56.105"; + + @Test + public void s2t_case1() throws Throwable { + // given + createSupTable("ms"); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/defaultJob.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + @Test + public void s2t_case2() throws Throwable { + // given + createSupTable("us"); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/defaultJob.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + @Test + public void s2t_case3() throws Throwable { + // given + createSupTable("ns"); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/defaultJob.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + void createSupTable(String precision) throws SQLException { + + final String url = "jdbc:TAOS-RS://" + host2 + ":6041/"; + try (Connection conn = DriverManager.getConnection(url, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + + stmt.execute("drop database if exists db2"); + stmt.execute("create database if not exists db2 precision '" + precision + "'"); + stmt.execute("create table db2.stb2(ts1 timestamp, ts2 timestamp,ts3 timestamp,ts4 timestamp,ts5 timestamp," + + "ts6 timestamp,ts7 timestamp, ts8 timestamp, ts9 timestamp, ts10 timestamp, f1 tinyint, f2 smallint," + + "f3 int, f4 bigint, f5 float, f6 double," + + "f7 bool, f8 binary(100), f9 nchar(100)) tags(t1 timestamp,t2 timestamp,t3 timestamp,t4 timestamp," + + "t5 timestamp,t6 timestamp,t7 timestamp, t8 tinyint, t9 smallint, t10 int, t11 bigint, t12 float," + + "t13 double, t14 bool, t15 binary(100), t16 nchar(100))"); + + stmt.close(); + } + + } + +} diff --git a/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/TDengine2TDengineTest.java b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/TDengine2TDengineTest.java new file mode 100644 index 0000000000..9e9546332a --- /dev/null +++ b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/TDengine2TDengineTest.java @@ -0,0 +1,127 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.core.Engine; +import org.junit.Before; +import org.junit.Test; + +import java.sql.*; +import java.text.SimpleDateFormat; +import java.util.Random; + +public class TDengine2TDengineTest { + + private static final String host1 = "192.168.56.105"; + private static final String host2 = "192.168.1.93"; + private static final Random random = new Random(System.currentTimeMillis()); + + @Test + public void case_01() throws Throwable { + // given + createSupTable(); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/t2t-1.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + @Test + public void case_02() throws Throwable { + // given + createSupTable(); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/t2t-2.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + @Test + public void case_03() throws Throwable { + // given + createSupAndSubTable(); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/t2t-3.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + @Test + public void case_04() throws Throwable { + // given + createTable(); + + // when + String[] params = {"-mode", "standalone", "-jobid", "-1", "-job", "src/test/resources/t2t-4.json"}; + System.setProperty("datax.home", "../target/datax/datax"); + Engine.entry(params); + } + + private void createTable() throws SQLException { + final String url2 = "jdbc:TAOS-RS://" + host2 + ":6041"; + try (Connection conn = DriverManager.getConnection(url2, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + stmt.execute("drop database if exists db2"); + stmt.execute("create database if not exists db2"); + stmt.execute("create table db2.weather (ts timestamp, f1 tinyint, f2 smallint, f3 int, f4 bigint, " + + "f5 float, f6 double, f7 bool, f8 binary(100), f9 nchar(100))"); + stmt.close(); + } + } + + private void createSupTable() throws SQLException { + final String url2 = "jdbc:TAOS-RS://" + host2 + ":6041"; + try (Connection conn = DriverManager.getConnection(url2, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + stmt.execute("drop database if exists db2"); + stmt.execute("create database if not exists db2"); + stmt.execute("create table db2.stb2 (ts timestamp, f1 tinyint, f2 smallint, f3 int, f4 bigint," + + " f5 float, f6 double, f7 bool, f8 binary(100), f9 nchar(100)) tags(t1 timestamp, t2 tinyint, " + + "t3 smallint, t4 int, t5 bigint, t6 float, t7 double, t8 bool, t9 binary(100), t10 nchar(1000))"); + stmt.close(); + } + } + + private void createSupAndSubTable() throws SQLException { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); + final String ts = sdf.format(new Date(System.currentTimeMillis())); + + final String url2 = "jdbc:TAOS-RS://" + host2 + ":6041"; + try (Connection conn = DriverManager.getConnection(url2, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + stmt.execute("drop database if exists db2"); + stmt.execute("create database if not exists db2"); + stmt.execute("create table db2.stb2 (ts timestamp, f1 tinyint, f2 smallint, f3 int, f4 bigint," + + " f5 float, f6 double, f7 bool, f8 binary(100), f9 nchar(100)) tags(t1 timestamp, t2 tinyint, " + + "t3 smallint, t4 int, t5 bigint, t6 float, t7 double, t8 bool, t9 binary(100), t10 nchar(1000))"); + + stmt.execute("create table db2.t1 using db2.stb2 tags('" + ts + "',1,2,3,4,5.0,6.0,true,'abc123ABC','北京朝阳望京')"); + stmt.close(); + } + } + + @Before + public void before() throws SQLException { + final String url = "jdbc:TAOS-RS://" + host1 + ":6041"; + try (Connection conn = DriverManager.getConnection(url, "root", "taosdata")) { + Statement stmt = conn.createStatement(); + + stmt.execute("drop database if exists db1"); + stmt.execute("create database if not exists db1"); + stmt.execute("create table db1.stb1 (ts timestamp, f1 tinyint, f2 smallint, f3 int, f4 bigint," + + " f5 float, f6 double, f7 bool, f8 binary(100), f9 nchar(100)) tags(t1 timestamp, t2 tinyint, " + + "t3 smallint, t4 int, t5 bigint, t6 float, t7 double, t8 bool, t9 binary(100), t10 nchar(1000))"); + for (int i = 0; i < 10; i++) { + String sql = "insert into db1.t" + (i + 1) + " using db1.stb1 tags(now+" + i + "s," + + random.nextInt(100) + "," + random.nextInt(100) + "," + random.nextInt(100) + "," + + random.nextInt(100) + "," + random.nextFloat() + "," + random.nextDouble() + "," + + random.nextBoolean() + ",'abc123ABC','北京朝阳望京') values(now+" + i + "s, " + + random.nextInt(100) + "," + random.nextInt(100) + "," + random.nextInt(100) + "," + + random.nextInt(100) + "," + random.nextFloat() + "," + random.nextDouble() + "," + + random.nextBoolean() + ",'abc123ABC','北京朝阳望京')"; + stmt.execute(sql); + } + } + } +} diff --git a/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/TDengineWriterTest.java b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/TDengineWriterTest.java new file mode 100644 index 0000000000..813f61317d --- /dev/null +++ b/tdenginewriter/src/test/java/com/alibaba/datax/plugin/writer/tdenginewriter/TDengineWriterTest.java @@ -0,0 +1,62 @@ +package com.alibaba.datax.plugin.writer.tdenginewriter; + +import com.alibaba.datax.common.util.Configuration; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.List; + +public class TDengineWriterTest { + + TDengineWriter.Job job; + + @Before + public void before() { + job = new TDengineWriter.Job(); + Configuration configuration = Configuration.from("{" + + "\"username\": \"root\"," + + "\"password\": \"taosdata\"," + + "\"column\": [\"ts\", \"f1\", \"f2\", \"t1\"]," + + "\"connection\": [{\"table\":[\"weather\"],\"jdbcUrl\":\"jdbc:TAOS-RS://master:6041/test\"}]," + + "\"batchSize\": \"1000\"" + + "}"); + job.setPluginJobConf(configuration); + } + + @Test + public void jobInit() { + // when + job.init(); + + // assert + Configuration conf = job.getPluginJobConf(); + + Assert.assertEquals("root", conf.getString("username")); + Assert.assertEquals("taosdata", conf.getString("password")); + Assert.assertEquals("jdbc:TAOS-RS://master:6041/test", conf.getString("connection[0].jdbcUrl")); + Assert.assertEquals(new Integer(1000), conf.getInt("batchSize")); + Assert.assertEquals("ts", conf.getString("column[0]")); + Assert.assertEquals("f2", conf.getString("column[2]")); + } + + @Test + public void jobSplit() { + // when + job.init(); + List configurationList = job.split(10); + + // assert + Assert.assertEquals(10, configurationList.size()); + for (Configuration conf : configurationList) { + Assert.assertEquals("root", conf.getString("username")); + Assert.assertEquals("taosdata", conf.getString("password")); + Assert.assertEquals("jdbc:TAOS-RS://master:6041/test", conf.getString("jdbcUrl")); + Assert.assertEquals(new Integer(1000), conf.getInt("batchSize")); + Assert.assertEquals("ts", conf.getString("column[0]")); + Assert.assertEquals("f2", conf.getString("column[2]")); + + } + } + +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/csv2t.json b/tdenginewriter/src/test/resources/csv2t.json new file mode 100644 index 0000000000..ef5c4d04c0 --- /dev/null +++ b/tdenginewriter/src/test/resources/csv2t.json @@ -0,0 +1,80 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "txtfilereader", + "parameter": { + "path": [ + "/Users/yangzy/IdeaProjects/DataX/tdenginewriter/src/test/resources/weather.csv" + ], + "encoding": "UTF-8", + "column": [ + { + "index": 0, + "type": "string" + }, + { + "index": 1, + "type": "date", + "format": "yyy-MM-dd HH:mm:ss.SSS" + }, + { + "index": 2, + "type": "long" + }, + { + "index": 3, + "type": "double" + }, + { + "index": 4, + "type": "long" + }, + { + "index": 5, + "type": "string" + }, + { + "index": 6, + "type": "String" + } + ], + "fieldDelimiter": "," + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "tbname", + "ts", + "temperature", + "humidity", + "is_normal", + "device_id", + "address" + ], + "connection": [ + { + "table": [ + "weather" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.56.105:6041/test" + } + ], + "batchSize": 100, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/defaultJob.json b/tdenginewriter/src/test/resources/defaultJob.json new file mode 100644 index 0000000000..2a36dfd08c --- /dev/null +++ b/tdenginewriter/src/test/resources/defaultJob.json @@ -0,0 +1,226 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "streamreader", + "parameter": { + "column": [ + { + "type": "string", + "value": "tb1" + }, + { + "type": "date", + "value": "2022-02-20 12:00:01" + }, + { + "type": "date", + "value": "2022-02-20 12:00:02.123", + "dateFormat": "yyyy-MM-dd HH:mm:ss.SSS" + }, + { + "type": "date", + "value": "2022-02-20 12:00:03.123456", + "dateFormat": "yyyy-MM-dd HH:mm:ss.SSSSSS" + }, + { + "type": "date", + "value": "2022-02-20 12:00:04.123456789", + "dateFormat": "yyyy-MM-dd HH:mm:ss.SSSSSSSSS" + }, + { + "type": "string", + "value": "2022-02-20 12:00:05.123" + }, + { + "type": "string", + "value": "2022-02-20 12:00:06.123456" + }, + { + "type": "string", + "value": "2022-02-20 12:00:07.123456789" + }, + { + "type": "long", + "value": 1645329608000 + }, + { + "type": "long", + "value": 1645329609000000 + }, + { + "type": "long", + "value": 1645329610000000000 + }, + { + "type": "long", + "random": "0, 10" + }, + { + "type": "long", + "random": "0, 100" + }, + { + "type": "long", + "random": "0, 1000" + }, + { + "type": "long", + "random": "0, 10000" + }, + { + "type": "double", + "random": "0, 10" + }, + { + "type": "double", + "random": "10, 20" + }, + { + "type": "bool", + "random": "0, 50" + }, + { + "type": "bytes", + "random": "0, 10" + }, + { + "type": "string", + "random": "10, 50" + }, + { + "type": "date", + "value": "2022-02-20 12:00:01" + }, + { + "type": "date", + "value": "2022-02-20 12:00:02.123", + "dateFormat": "yyyy-MM-dd HH:mm:ss.SSS" + }, + { + "type": "date", + "value": "2022-02-20 12:00:03.123456", + "dateFormat": "yyyy-MM-dd HH:mm:ss.SSSSSS" + }, + { + "type": "date", + "value": "2022-02-20 12:00:04.123456789", + "dateFormat": "yyyy-MM-dd HH:mm:ss.SSSSSSSSS" + }, + { + "type": "string", + "value": "2022-02-20 12:00:05.123" + }, + { + "type": "string", + "value": "2022-02-20 12:00:06.123456" + }, + { + "type": "string", + "value": "2022-02-20 12:00:07.123456789" + }, + { + "type": "long", + "value": 1 + }, + { + "type": "long", + "value": 2 + }, + { + "type": "long", + "value": 3 + }, + { + "type": "long", + "value": 4 + }, + { + "type": "double", + "value": 5.55 + }, + { + "type": "double", + "value": 6.666666 + }, + { + "type": "bool", + "value": true + }, + { + "type": "bytes", + "value": "abcABC123" + }, + { + "type": "string", + "value": "北京朝阳望京" + } + ], + "sliceRecordCount": 10 + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "tbname", + "ts1", + "ts2", + "ts3", + "ts4", + "ts5", + "ts6", + "ts7", + "ts8", + "ts9", + "ts10", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "t1", + "t2", + "t3", + "t4", + "t5", + "t6", + "t7", + "t8", + "t9", + "t10", + "t11", + "t12", + "t13", + "t14", + "t15", + "t16" + ], + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.56.105:6041/db2" + } + ], + "batchSize": 100, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/dm-schema.sql b/tdenginewriter/src/test/resources/dm-schema.sql new file mode 100644 index 0000000000..d9985b1b32 --- /dev/null +++ b/tdenginewriter/src/test/resources/dm-schema.sql @@ -0,0 +1,30 @@ +select tablespace_name +from dba_data_files; + +create +tablespace test datafile '/home/dmdba/dmdbms/data/DAMENG/test.dbf' size 32 autoextend on next 1 maxsize 1024; + +create +user TESTUSER identified by test123456 default tablespace test; + +grant dba to TESTUSER; + +select * +from user_tables; + +drop table if exists stb1; + +create table stb1 +( + ts timestamp, + f1 tinyint, + f2 smallint, + f3 int, + f4 bigint, + f5 float, + f6 double, + f7 NUMERIC(10, 2), + f8 BIT, + f9 VARCHAR(100), + f10 VARCHAR2(200) +); diff --git a/tdenginewriter/src/test/resources/dm2t-1.json b/tdenginewriter/src/test/resources/dm2t-1.json new file mode 100644 index 0000000000..183786bf82 --- /dev/null +++ b/tdenginewriter/src/test/resources/dm2t-1.json @@ -0,0 +1,62 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "rdbmsreader", + "parameter": { + "username": "TESTUSER", + "password": "test123456", + "connection": [ + { + "querySql": [ + "select concat(concat(concat('t', f1), '_'),f3) as tbname,* from stb1;" + ], + "jdbcUrl": [ + "jdbc:dm://192.168.0.72:5236" + ] + } + ], + "fetchSize": 1024 + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "tbname", + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "f10" + ], + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.1.93:6041/db2" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/dm2t-2.json b/tdenginewriter/src/test/resources/dm2t-2.json new file mode 100644 index 0000000000..dfea82bf3d --- /dev/null +++ b/tdenginewriter/src/test/resources/dm2t-2.json @@ -0,0 +1,62 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "rdbmsreader", + "parameter": { + "username": "TESTUSER", + "password": "test123456", + "connection": [ + { + "querySql": [ + "select concat(concat(concat('t', f1), '_'),f3) as tbname,* from stb1;" + ], + "jdbcUrl": [ + "jdbc:dm://192.168.0.72:5236" + ] + } + ], + "fetchSize": 1024, + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "tbname", + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "f10" + ], + "connection": [ + { + "table": [ + "t1_0" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.1.93:6041/db2" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/dm2t-3.json b/tdenginewriter/src/test/resources/dm2t-3.json new file mode 100644 index 0000000000..cd96a5365b --- /dev/null +++ b/tdenginewriter/src/test/resources/dm2t-3.json @@ -0,0 +1,76 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "rdbmsreader", + "parameter": { + "username": "TESTUSER", + "password": "test123456", + "column": [ + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "f10" + ], + "splitPk": "f1", + "connection": [ + { + "table": [ + "stb1" + ], + "jdbcUrl": [ + "jdbc:dm://192.168.0.72:5236" + ] + } + ], + "fetchSize": 1024, + "where": "1 = 1" + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "f10" + ], + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.1.93:6041/db2" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/dm2t-4.json b/tdenginewriter/src/test/resources/dm2t-4.json new file mode 100644 index 0000000000..5f169d5bd9 --- /dev/null +++ b/tdenginewriter/src/test/resources/dm2t-4.json @@ -0,0 +1,61 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "rdbmsreader", + "parameter": { + "username": "TESTUSER", + "password": "test123456", + "connection": [ + { + "querySql": [ + "select * from stb1" + ], + "jdbcUrl": [ + "jdbc:dm://192.168.0.72:5236" + ] + } + ], + "fetchSize": 1024 + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "f10" + ], + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:TAOS://192.168.1.93:6030/db2" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/incremental_sync/clean_env.sh b/tdenginewriter/src/test/resources/incremental_sync/clean_env.sh new file mode 100755 index 0000000000..f3dca7c13f --- /dev/null +++ b/tdenginewriter/src/test/resources/incremental_sync/clean_env.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +datax_home_dir=$(dirname $(readlink -f "$0")) + +curl -H 'Authorization: Basic cm9vdDp0YW9zZGF0YQ==' -d 'drop table if exists db2.stb2;' 192.168.1.93:6041/rest/sql +curl -H 'Authorization: Basic cm9vdDp0YW9zZGF0YQ==' -d 'create table if not exists db2.stb2 (`ts` TIMESTAMP,`f2` SMALLINT,`f4` BIGINT,`f5` FLOAT,`f6` DOUBLE,`f7` DOUBLE,`f8` BOOL,`f9` NCHAR(100),`f10` NCHAR(200)) TAGS (`f1` TINYINT,`f3` INT);' 192.168.1.93:6041/rest/sql + +rm -f ${datax_home_dir}/log/* +rm -f ${datax_home_dir}/job/*.csv \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/incremental_sync/csv2t-jni.json b/tdenginewriter/src/test/resources/incremental_sync/csv2t-jni.json new file mode 100644 index 0000000000..625c38010c --- /dev/null +++ b/tdenginewriter/src/test/resources/incremental_sync/csv2t-jni.json @@ -0,0 +1,106 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "txtfilereader", + "parameter": { + "path": [ + "/root/workspace/tmp/a.txt" + ], + "encoding": "UTF-8", + "column": [ + { + "index": 0, + "type": "date", + "format": "yyyy-MM-dd HH:mm:ss.SSS" + }, + { + "index": 1, + "type": "long" + }, + { + "index": 2, + "type": "long" + }, + { + "index": 3, + "type": "long" + }, + { + "index": 4, + "type": "long" + }, + { + "index": 5, + "type": "double" + }, + { + "index": 6, + "type": "double" + }, + { + "index": 7, + "type": "boolean" + }, + { + "index": 8, + "type": "string" + }, + { + "index": 9, + "type": "string" + }, + { + "index": 10, + "type": "date", + "format": "yyyy-MM-dd HH:mm:ss.SSS" + }, + { + "index": 11, + "type": "string" + } + ], + "fieldDelimiter": "," + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "t1", + "tbname" + ], + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:TAOS://192.168.1.93:6030/db2" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/incremental_sync/csv2t-restful.json b/tdenginewriter/src/test/resources/incremental_sync/csv2t-restful.json new file mode 100644 index 0000000000..d852e2e2c1 --- /dev/null +++ b/tdenginewriter/src/test/resources/incremental_sync/csv2t-restful.json @@ -0,0 +1,57 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "txtfilereader", + "parameter": { + "path": [ + "/root/workspace/tmp/a.txt" + ], + "encoding": "UTF-8", + "column": [ + "*" + ], + "fieldDelimiter": "," + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "t1", + "tbname" + ], + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.1.93:6041/db2" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/incremental_sync/dm2t-jni.json b/tdenginewriter/src/test/resources/incremental_sync/dm2t-jni.json new file mode 100644 index 0000000000..3e86bb8deb --- /dev/null +++ b/tdenginewriter/src/test/resources/incremental_sync/dm2t-jni.json @@ -0,0 +1,62 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "rdbmsreader", + "parameter": { + "username": "TESTUSER", + "password": "test123456", + "connection": [ + { + "querySql": [ + "select concat(concat(concat('t', f1), '_'),f3) as tbname,* from stb1;" + ], + "jdbcUrl": [ + "jdbc:dm://192.168.0.72:5236" + ] + } + ], + "fetchSize": 1024 + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "tbname", + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "f10" + ], + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:TAOS://192.168.1.93:6030/db2" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/incremental_sync/dm2t-restful.json b/tdenginewriter/src/test/resources/incremental_sync/dm2t-restful.json new file mode 100644 index 0000000000..183786bf82 --- /dev/null +++ b/tdenginewriter/src/test/resources/incremental_sync/dm2t-restful.json @@ -0,0 +1,62 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "rdbmsreader", + "parameter": { + "username": "TESTUSER", + "password": "test123456", + "connection": [ + { + "querySql": [ + "select concat(concat(concat('t', f1), '_'),f3) as tbname,* from stb1;" + ], + "jdbcUrl": [ + "jdbc:dm://192.168.0.72:5236" + ] + } + ], + "fetchSize": 1024 + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "tbname", + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "f10" + ], + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.1.93:6041/db2" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/incremental_sync/dm2t-update.json b/tdenginewriter/src/test/resources/incremental_sync/dm2t-update.json new file mode 100644 index 0000000000..d9285b23cd --- /dev/null +++ b/tdenginewriter/src/test/resources/incremental_sync/dm2t-update.json @@ -0,0 +1,63 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "rdbmsreader", + "parameter": { + "username": "TESTUSER", + "password": "test123456", + "connection": [ + { + "querySql": [ + "select concat(concat(concat('t', f1), '_'),f3) as tbname,* from stb1" + ], + "jdbcUrl": [ + "jdbc:dm://192.168.0.72:5236" + ] + } + ], + "where": "1=1", + "fetchSize": 1024 + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "tbname", + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "f10" + ], + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.1.93:6041/db2" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/incremental_sync/dm2t_sync.sh b/tdenginewriter/src/test/resources/incremental_sync/dm2t_sync.sh new file mode 100755 index 0000000000..426c623338 --- /dev/null +++ b/tdenginewriter/src/test/resources/incremental_sync/dm2t_sync.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +set -e +#set -x + +datax_home_dir=$(dirname $(readlink -f "$0")) +table_name="stb1" +update_key="ts" + +while getopts "hd:t:" arg; do + case $arg in + d) + datax_home_dir=$(echo $OPTARG) + ;; + v) + table_name=$(echo $OPTARG) + ;; + h) + echo "Usage: $(basename $0) -d [datax_home_dir] -t [table_name] -k [update_key]" + echo " -h help" + exit 0 + ;; + ?) #unknow option + echo "unkonw argument" + exit 1 + ;; + esac +done + +if [[ -e ${datax_home_dir}/job/${table_name}.csv ]]; then + MAX_TIME=$(cat ${datax_home_dir}/job/${table_name}.csv) +else + MAX_TIME="null" +fi +current_datetime=$(date +"%Y-%m-%d %H:%M:%S") +current_timestamp=$(date +%s) + +if [ "$MAX_TIME" != "null" ]; then + WHERE="${update_key} >= '$MAX_TIME' and ${update_key} < '$current_datetime'" + sed "s/1=1/$WHERE/g" ${datax_home_dir}/job/dm2t-update.json >${datax_home_dir}/job/dm2t_${current_timestamp}.json + echo "incremental data synchronization, from '${MAX_TIME}' to '${current_datetime}'" + python ${datax_home_dir}/bin/datax.py ${datax_home_dir}/job/dm2t_${current_timestamp}.json 1> /dev/null 2>&1 +else + echo "full data synchronization, to '${current_datetime}'" + python ${datax_home_dir}/bin/datax.py ${datax_home_dir}/job/dm2t-update.json 1> /dev/null 2>&1 +fi + +if [[ $? -ne 0 ]]; then + echo "datax migration job falied" +else + echo ${current_datetime} >$datax_home_dir/job/${table_name}.csv + echo "datax migration job success" +fi + +rm -rf ${datax_home_dir}/job/dm2t_${current_timestamp}.json + +#while true; do ./dm2t_sync.sh; sleep 5s; done \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/incremental_sync/t2dm-jni.json b/tdenginewriter/src/test/resources/incremental_sync/t2dm-jni.json new file mode 100644 index 0000000000..341f6293ed --- /dev/null +++ b/tdenginewriter/src/test/resources/incremental_sync/t2dm-jni.json @@ -0,0 +1,50 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "tdenginereader", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "*" + ], + "connection": [ + { + "table": [ + "stb1" + ], + "jdbcUrl": "jdbc:TAOS://192.168.56.105:6030/db1" + } + ] + } + }, + "writer": { + "name": "rdbmswriter", + "parameter": { + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:dm://192.168.0.72:5236" + } + ], + "username": "TESTUSER", + "password": "test123456", + "table": "stb2", + "column": [ + "*" + ] + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/incremental_sync/t2dm-restful.json b/tdenginewriter/src/test/resources/incremental_sync/t2dm-restful.json new file mode 100644 index 0000000000..b2cf91e2ee --- /dev/null +++ b/tdenginewriter/src/test/resources/incremental_sync/t2dm-restful.json @@ -0,0 +1,50 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "tdenginereader", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "*" + ], + "connection": [ + { + "table": [ + "stb1" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.56.105:6041/db1" + } + ] + } + }, + "writer": { + "name": "rdbmswriter", + "parameter": { + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:dm://192.168.0.72:5236" + } + ], + "username": "TESTUSER", + "password": "test123456", + "table": "stb2", + "column": [ + "*" + ] + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/incremental_sync/upload.sh b/tdenginewriter/src/test/resources/incremental_sync/upload.sh new file mode 100755 index 0000000000..388d275b03 --- /dev/null +++ b/tdenginewriter/src/test/resources/incremental_sync/upload.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +scp t2dm-restful.json root@192.168.56.105:/root/workspace/tmp/datax/job +scp t2dm-jni.json root@192.168.56.105:/root/workspace/tmp/datax/job +scp dm2t-restful.json root@192.168.56.105:/root/workspace/tmp/datax/job +scp dm2t-jni.json root@192.168.56.105:/root/workspace/tmp/datax/job +scp dm2t-update.json root@192.168.56.105:/root/workspace/tmp/datax/job +scp csv2t-restful.json root@192.168.56.105:/root/workspace/tmp/datax/job +scp csv2t-jni.json root@192.168.56.105:/root/workspace/tmp/datax/job + + +scp dm2t_sync.sh root@192.168.56.105:/root/workspace/tmp/datax +scp clean_env.sh root@192.168.56.105:/root/workspace/tmp/datax \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/m2t-1.json b/tdenginewriter/src/test/resources/m2t-1.json new file mode 100644 index 0000000000..dcacb4b2ed --- /dev/null +++ b/tdenginewriter/src/test/resources/m2t-1.json @@ -0,0 +1,72 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "mysqlreader", + "parameter": { + "username": "root", + "password": "123456", + "column": [ + "ts", + "dt", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8" + ], + "splitPk": "id", + "connection": [ + { + "table": [ + "stb1" + ], + "jdbcUrl": [ + "jdbc:mysql://192.168.56.105:3306/db1?useSSL=false&useUnicode=true&characterEncoding=utf8" + ] + } + ] + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "ts", + "dt", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8" + ], + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.1.93:6041/db2" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/mongo2t.json b/tdenginewriter/src/test/resources/mongo2t.json new file mode 100644 index 0000000000..902e6f7c74 --- /dev/null +++ b/tdenginewriter/src/test/resources/mongo2t.json @@ -0,0 +1,66 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "mongodbreader", + "parameter": { + "address": [ + "192.168.1.213:27017" + ], + "userName": "", + "userPassword": "", + "dbName": "testdb", + "collectionName": "monitor_data", + "column": [ + { + "name": "ct", + "type": "date" + }, + { + "name": "pv", + "type": "float" + }, + { + "name": "tv", + "type": "float" + }, + { + "name": "pid", + "type": "float" + } + ] + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "hmdata", + "column": [ + "ts", + "pressure", + "temperature", + "position_id" + ], + "connection": [ + { + "table": [ + "pipeline_data" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.1.213:6041/mongo3040" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/o2t-1.json b/tdenginewriter/src/test/resources/o2t-1.json new file mode 100644 index 0000000000..11264c9868 --- /dev/null +++ b/tdenginewriter/src/test/resources/o2t-1.json @@ -0,0 +1,36 @@ +{ + "job":{ + "content":[{ + "reader": { + "name": "opentsdbreader", + "parameter": { + "endpoint": "http://192.168.56.105:4242", + "column": ["weather_temperature"], + "beginDateTime": "2021-01-01 00:00:00", + "endDateTime": "2021-01-01 01:00:00" + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "connection": [ + { + "table": [ + "meters" + ], + "jdbcUrl": "jdbc:TAOS://192.168.56.105:6030/test?timestampFormat=TIMESTAMP" + } + ], + "batchSize": 1000 + } + } + }], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/t2t-1.json b/tdenginewriter/src/test/resources/t2t-1.json new file mode 100644 index 0000000000..5ca04d9a98 --- /dev/null +++ b/tdenginewriter/src/test/resources/t2t-1.json @@ -0,0 +1,94 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "tdenginereader", + "parameter": { + "username": "root", + "password": "taosdata", + "connection": [ + { + "table": [ + "stb1" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.56.105:6041/db1?timestampFormat=TIMESTAMP" + } + ], + "column": [ + "tbname", + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "t1", + "t2", + "t3", + "t4", + "t5", + "t6", + "t7", + "t8", + "t9", + "t10" + ], + "beginDateTime": "2022-02-15 00:00:00", + "endDateTime": "2022-02-16 00:00:00", + "splitInterval": "1d" + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "tbname", + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "t1", + "t2", + "t3", + "t4", + "t5", + "t6", + "t7", + "t8", + "t9", + "t10" + ], + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.1.93:6041/db2?timestampFormat=TIMESTAMP" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/t2t-2.json b/tdenginewriter/src/test/resources/t2t-2.json new file mode 100644 index 0000000000..18130b6e76 --- /dev/null +++ b/tdenginewriter/src/test/resources/t2t-2.json @@ -0,0 +1,92 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "tdenginereader", + "parameter": { + "username": "root", + "password": "taosdata", + "connection": [ + { + "table": [ + "stb1" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.56.105:6041/db1?timestampFormat=TIMESTAMP" + } + ], + "column": [ + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "t1", + "t2", + "t3", + "t4", + "t5", + "t6", + "t7", + "t8", + "t9", + "t10" + ], + "beginDateTime": "2022-02-15 00:00:00", + "endDateTime": "2022-02-16 00:00:00", + "splitInterval": "1d" + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "t1", + "t2", + "t3", + "t4", + "t5", + "t6", + "t7", + "t8", + "t9", + "t10" + ], + "connection": [ + { + "table": [ + "stb2" + ], + "jdbcUrl": "jdbc:TAOS://192.168.1.93:6030/db2" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/t2t-3.json b/tdenginewriter/src/test/resources/t2t-3.json new file mode 100644 index 0000000000..e0a2295973 --- /dev/null +++ b/tdenginewriter/src/test/resources/t2t-3.json @@ -0,0 +1,92 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "tdenginereader", + "parameter": { + "username": "root", + "password": "taosdata", + "connection": [ + { + "table": [ + "stb1" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.56.105:6041/db1?timestampFormat=TIMESTAMP" + } + ], + "column": [ + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "t1", + "t2", + "t3", + "t4", + "t5", + "t6", + "t7", + "t8", + "t9", + "t10" + ], + "beginDateTime": "2022-02-15 00:00:00", + "endDateTime": "2022-02-16 00:00:00", + "splitInterval": "1d" + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9", + "t1", + "t2", + "t3", + "t4", + "t5", + "t6", + "t7", + "t8", + "t9", + "t10" + ], + "connection": [ + { + "table": [ + "t1" + ], + "jdbcUrl": "jdbc:TAOS://192.168.1.93:6030/db2?timestampFormat=TIMESTAMP" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/t2t-4.json b/tdenginewriter/src/test/resources/t2t-4.json new file mode 100644 index 0000000000..b77163636e --- /dev/null +++ b/tdenginewriter/src/test/resources/t2t-4.json @@ -0,0 +1,72 @@ +{ + "job": { + "content": [ + { + "reader": { + "name": "tdenginereader", + "parameter": { + "username": "root", + "password": "taosdata", + "connection": [ + { + "table": [ + "stb1" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.56.105:6041/db1?timestampFormat=TIMESTAMP" + } + ], + "column": [ + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9" + ], + "beginDateTime": "2022-02-15 00:00:00", + "endDateTime": "2022-02-16 00:00:00", + "splitInterval": "1d" + } + }, + "writer": { + "name": "tdenginewriter", + "parameter": { + "username": "root", + "password": "taosdata", + "column": [ + "ts", + "f1", + "f2", + "f3", + "f4", + "f5", + "f6", + "f7", + "f8", + "f9" + ], + "connection": [ + { + "table": [ + "weather" + ], + "jdbcUrl": "jdbc:TAOS-RS://192.168.1.93:6041/db2" + } + ], + "batchSize": 1000, + "ignoreTagsUnmatched": true + } + } + } + ], + "setting": { + "speed": { + "channel": 1 + } + } + } +} \ No newline at end of file diff --git a/tdenginewriter/src/test/resources/weather.csv b/tdenginewriter/src/test/resources/weather.csv new file mode 100644 index 0000000000..21c4a1aacb --- /dev/null +++ b/tdenginewriter/src/test/resources/weather.csv @@ -0,0 +1,10 @@ +tb1,2022-02-20 04:05:59.255,5,8.591868744,1,abcABC123,北京朝阳望京 +tb1,2022-02-20 04:58:47.068,3,1.489693641,1,abcABC123,北京朝阳望京 +tb1,2022-02-20 06:31:09.408,1,4.026500719,1,abcABC123,北京朝阳望京 +tb1,2022-02-20 08:08:00.336,1,9.606400360,1,abcABC123,北京朝阳望京 +tb1,2022-02-20 08:28:58.053,9,7.872178184,1,abcABC123123,北京朝阳望京 +tb1,2022-02-20 10:23:20.836,9,2.699478524,1,abcABC123,北京朝阳望京 +tb1,2022-02-20 11:09:59.739,7,7.906723716,1,abcABC123,北京朝阳望京 +tb1,2022-02-20 19:08:29.315,1,5.852338895,1,abcABC123,北京朝阳望京 +tb1,2022-02-20 22:10:06.243,10,5.535007901,1,abcABC123,北京朝阳望京 +tb1,2022-02-20 23:52:43.683,10,10.642013185,1,abcABC123,北京朝阳望京 diff --git a/transformer/doc/transformer.md b/transformer/doc/transformer.md index 247ab39b4d..a9da83a4ed 100644 --- a/transformer/doc/transformer.md +++ b/transformer/doc/transformer.md @@ -42,12 +42,12 @@ dx_substr(1,"5","10") column 1的value为“dataxTest”=>"Test" * 举例: ``` dx_replace(1,"2","4","****") column 1的value为“dataxTest”=>"da****est" -dx_replace(1,"5","10","****") column 1的value为“dataxTest”=>"data****" +dx_replace(1,"5","10","****") column 1的value为“dataxTest”=>"datax****" ``` 4. dx_filter (关联filter暂不支持,即多个字段的联合判断,函参太过复杂,用户难以使用。) * 参数: * 第一个参数:字段编号,对应record中第几个字段。 - * 第二个参数:运算符,支持一下运算符:like, not like, >, =, <, >=, !=, <= + * 第二个参数:运算符,支持以下运算符:like, not like, >, =, <, >=, !=, <= * 第三个参数:正则表达式(java正则表达式)、值。 * 返回: * 如果匹配正则表达式,返回Null,表示过滤该行。不匹配表达式时,表示保留该行。(注意是该行)。对于>=<都是对字段直接compare的结果. @@ -59,7 +59,17 @@ dx_replace(1,"5","10","****") column 1的value为“dataxTest”=>"data****" dx_filter(1,"like","dataTest") dx_filter(1,">=","10") ``` -5. dx_groovy +5. dx_digest +* 参数:3个 + * 第一个参数:字段编号,对应record中第几个字段。 + * 第二个参数:hash类型,md5、sha1 + * 第三个参数:hash值大小写 toUpperCase(大写)、toLowerCase(小写) +* 返回: 返回指定类型的hashHex,如果字段为空,则转为空字符串,再返回对应hashHex +* 举例: +``` +dx_digest(1,"md5","toUpperCase"), column 1的值为 xyzzzzz => 9CDFFC4FA4E45A99DB8BBCD762ACFFA2 +``` +6. dx_groovy * 参数。 * 第一个参数: groovy code * 第二个参数(列表或者为空):extraPackage @@ -67,7 +77,9 @@ dx_filter(1,">=","10") * dx_groovy只能调用一次。不能多次调用。 * groovy code中支持java.lang, java.util的包,可直接引用的对象有record,以及element下的各种column(BoolColumn.class,BytesColumn.class,DateColumn.class,DoubleColumn.class,LongColumn.class,StringColumn.class)。不支持其他包,如果用户有需要用到其他包,可设置extraPackage,注意extraPackage不支持第三方jar包。 * groovy code中,返回更新过的Record(比如record.setColumn(columnIndex, new StringColumn(newValue));),或者null。返回null表示过滤此行。 - * 用户可以直接调用静态的Util方式(GroovyTransformerStaticUtil),目前GroovyTransformerStaticUtil的方法列表 (按需补充): + * 用户可以直接调用静态的Util方式(GroovyTransformerStaticUtil),目前GroovyTransformerStaticUtil的方法列表: + * md5(String):String + * sha1(String):String * 举例: ``` groovy 实现的subStr: @@ -109,7 +121,7 @@ String code3 = "Column column = record.getColumn(1);\n" + ``` ## Job定义 -* 本例中,配置3个UDF。 +* 本例中,配置4个UDF。 ``` { @@ -133,11 +145,11 @@ String code3 = "Column column = record.getColumn(1);\n" + "type": "string" }, { - "value": 19890604, + "value": 1724154616370, "type": "long" }, { - "value": "1989-06-04 00:00:00", + "value": "2024-01-01 00:00:00", "type": "date" }, { @@ -145,11 +157,11 @@ String code3 = "Column column = record.getColumn(1);\n" + "type": "bool" }, { - "value": "test", + "value": "TestRawData", "type": "bytes" } ], - "sliceRecordCount": 100000 + "sliceRecordCount": 100 } }, "writer": { @@ -162,30 +174,44 @@ String code3 = "Column column = record.getColumn(1);\n" + "transformer": [ { "name": "dx_substr", - "parameter": - { - "columnIndex":5, - "paras":["1","3"] - } + "parameter": { + "columnIndex": 5, + "paras": [ + "1", + "3" + ] + } }, { "name": "dx_replace", - "parameter": - { - "columnIndex":4, - "paras":["3","4","****"] - } + "parameter": { + "columnIndex": 4, + "paras": [ + "3", + "4", + "****" + ] + } + }, + { + "name": "dx_digest", + "parameter": { + "columnIndex": 3, + "paras": [ + "md5", + "toLowerCase" + ] + } }, { "name": "dx_groovy", - "parameter": - { - "code": "//groovy code//", - "extraPackage":[ - "import somePackage1;", - "import somePackage2;" - ] - } + "parameter": { + "code": "//groovy code//", + "extraPackage": [ + "import somePackage1;", + "import somePackage2;" + ] + } } ] } diff --git a/tsdbreader/pom.xml b/tsdbreader/pom.xml index b9a4598593..4b3f58c633 100644 --- a/tsdbreader/pom.xml +++ b/tsdbreader/pom.xml @@ -21,14 +21,11 @@ 3.3.2 - 4.4 + 4.5 2.4 - - 1.2.28 - - 4.12 + 4.13.1 2.9.9 @@ -44,10 +41,6 @@ slf4j-log4j12 org.slf4j - - fastjson - com.alibaba - commons-math3 org.apache.commons @@ -89,9 +82,8 @@ - com.alibaba - fastjson - ${fastjson.version} + com.alibaba.fastjson2 + fastjson2 diff --git a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/Constant.java b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/Constant.java index e42dedc04a..f5069dc9a1 100644 --- a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/Constant.java +++ b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/Constant.java @@ -16,6 +16,8 @@ public final class Constant { static final String DEFAULT_DATA_FORMAT = "yyyy-MM-dd HH:mm:ss"; public static final String METRIC_SPECIFY_KEY = "__metric__"; + public static final String METRIC_SPECIFY_KEY_PREFIX = METRIC_SPECIFY_KEY + "."; + public static final int METRIC_SPECIFY_KEY_PREFIX_LENGTH = METRIC_SPECIFY_KEY_PREFIX.length(); public static final String TS_SPECIFY_KEY = "__ts__"; public static final String VALUE_SPECIFY_KEY = "__value__"; diff --git a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/Key.java b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/Key.java index 14ee7e416e..c8a3d7aea9 100644 --- a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/Key.java +++ b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/Key.java @@ -17,14 +17,19 @@ public class Key { // RDB for MySQL / ADB etc. static final String SINK_DB_TYPE = "sinkDbType"; static final String ENDPOINT = "endpoint"; + static final String USERNAME = "username"; + static final String PASSWORD = "password"; static final String COLUMN = "column"; static final String METRIC = "metric"; static final String FIELD = "field"; static final String TAG = "tag"; + static final String COMBINE = "combine"; static final String INTERVAL_DATE_TIME = "splitIntervalMs"; static final String BEGIN_DATE_TIME = "beginDateTime"; static final String END_DATE_TIME = "endDateTime"; + static final String HINT = "hint"; + static final Boolean COMBINE_DEFAULT_VALUE = false; static final Integer INTERVAL_DATE_TIME_DEFAULT_VALUE = 60; static final String TYPE_DEFAULT_VALUE = "TSDB"; static final Set TYPE_SET = new HashSet<>(); diff --git a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/TSDBReader.java b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/TSDBReader.java index 04b931c7b1..1f8c3d18e0 100755 --- a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/TSDBReader.java +++ b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/TSDBReader.java @@ -6,7 +6,7 @@ import com.alibaba.datax.common.util.Configuration; import com.alibaba.datax.plugin.reader.tsdbreader.conn.TSDBConnection; import com.alibaba.datax.plugin.reader.tsdbreader.util.TimeUtils; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import org.apache.commons.lang3.StringUtils; import org.joda.time.DateTime; import org.slf4j.Logger; @@ -60,6 +60,15 @@ public void init() { "The parameter [" + Key.ENDPOINT + "] is not set."); } + String username = originalConfig.getString(Key.USERNAME, null); + if (StringUtils.isBlank(username)) { + LOG.warn("The parameter [" + Key.USERNAME + "] is blank."); + } + String password = originalConfig.getString(Key.PASSWORD, null); + if (StringUtils.isBlank(password)) { + LOG.warn("The parameter [" + Key.PASSWORD + "] is blank."); + } + // tagK / field could be empty if ("TSDB".equals(type)) { List columns = originalConfig.getList(Key.COLUMN, String.class); @@ -76,7 +85,14 @@ public void init() { "The parameter [" + Key.COLUMN + "] is not set."); } for (String specifyKey : Constant.MUST_CONTAINED_SPECIFY_KEYS) { - if (!columns.contains(specifyKey)) { + boolean containSpecifyKey = false; + for (String column : columns) { + if (column.startsWith(specifyKey)) { + containSpecifyKey = true; + break; + } + } + if (!containSpecifyKey) { throw DataXException.asDataXException( TSDBReaderErrorCode.ILLEGAL_VALUE, "The parameter [" + Key.COLUMN + "] should contain " @@ -99,6 +115,8 @@ public void init() { "The parameter [" + Key.INTERVAL_DATE_TIME + "] should be great than zero."); } + Boolean isCombine = originalConfig.getBool(Key.COMBINE, Key.COMBINE_DEFAULT_VALUE); + SimpleDateFormat format = new SimpleDateFormat(Constant.DEFAULT_DATA_FORMAT); String startTime = originalConfig.getString(Key.BEGIN_DATE_TIME); Long startDate; @@ -168,14 +186,14 @@ public List split(int adviceNumber) { startTime = format.parse(originalConfig.getString(Key.BEGIN_DATE_TIME)).getTime(); } catch (ParseException e) { throw DataXException.asDataXException( - TSDBReaderErrorCode.ILLEGAL_VALUE, "解析[" + Key.BEGIN_DATE_TIME + "]失败.", e); + TSDBReaderErrorCode.ILLEGAL_VALUE, "Analysis [" + Key.BEGIN_DATE_TIME + "] failed.", e); } long endTime; try { endTime = format.parse(originalConfig.getString(Key.END_DATE_TIME)).getTime(); } catch (ParseException e) { throw DataXException.asDataXException( - TSDBReaderErrorCode.ILLEGAL_VALUE, "解析[" + Key.END_DATE_TIME + "]失败.", e); + TSDBReaderErrorCode.ILLEGAL_VALUE, "Analysis [" + Key.END_DATE_TIME + "] failed.", e); } if (TimeUtils.isSecond(startTime)) { startTime *= 1000; @@ -186,13 +204,14 @@ public List split(int adviceNumber) { DateTime startDateTime = new DateTime(TimeUtils.getTimeInHour(startTime)); DateTime endDateTime = new DateTime(TimeUtils.getTimeInHour(endTime)); + final Boolean isCombine = originalConfig.getBool(Key.COMBINE, Key.COMBINE_DEFAULT_VALUE); + if ("TSDB".equals(type)) { - // split by metric - for (String column : columns4TSDB) { + if (isCombine) { // split by time in hour while (startDateTime.isBefore(endDateTime)) { Configuration clone = this.originalConfig.clone(); - clone.set(Key.COLUMN, Collections.singletonList(column)); + clone.set(Key.COLUMN, columns4TSDB); clone.set(Key.BEGIN_DATE_TIME, startDateTime.getMillis()); startDateTime = startDateTime.plusMillis(splitIntervalMs); @@ -202,15 +221,30 @@ public List split(int adviceNumber) { LOG.info("Configuration: {}", JSON.toJSONString(clone)); } + } else { + // split by time in hour + while (startDateTime.isBefore(endDateTime)) { + // split by metric + for (String column : columns4TSDB) { + Configuration clone = this.originalConfig.clone(); + clone.set(Key.COLUMN, Collections.singletonList(column)); + + clone.set(Key.BEGIN_DATE_TIME, startDateTime.getMillis()); + startDateTime = startDateTime.plusMillis(splitIntervalMs); + // Make sure the time interval is [start, end). + clone.set(Key.END_DATE_TIME, startDateTime.getMillis() - 1); + configurations.add(clone); + + LOG.info("Configuration: {}", JSON.toJSONString(clone)); + } + } } } else { - // split by metric - for (String metric : metrics) { - // split by time in hour + if (isCombine) { while (startDateTime.isBefore(endDateTime)) { Configuration clone = this.originalConfig.clone(); clone.set(Key.COLUMN, columns4RDB); - clone.set(Key.METRIC, Collections.singletonList(metric)); + clone.set(Key.METRIC, metrics); clone.set(Key.BEGIN_DATE_TIME, startDateTime.getMillis()); startDateTime = startDateTime.plusMillis(splitIntervalMs); @@ -220,6 +254,24 @@ public List split(int adviceNumber) { LOG.info("Configuration: {}", JSON.toJSONString(clone)); } + } else { + // split by time in hour + while (startDateTime.isBefore(endDateTime)) { + // split by metric + for (String metric : metrics) { + Configuration clone = this.originalConfig.clone(); + clone.set(Key.COLUMN, columns4RDB); + clone.set(Key.METRIC, Collections.singletonList(metric)); + + clone.set(Key.BEGIN_DATE_TIME, startDateTime.getMillis()); + startDateTime = startDateTime.plusMillis(splitIntervalMs); + // Make sure the time interval is [start, end). + clone.set(Key.END_DATE_TIME, startDateTime.getMillis() - 1); + configurations.add(clone); + + LOG.info("Configuration: {}", JSON.toJSONString(clone)); + } + } } } return configurations; @@ -247,6 +299,8 @@ public static class Task extends Reader.Task { private TSDBConnection conn; private Long startTime; private Long endTime; + private Boolean isCombine; + private Map hint; @Override public void init() { @@ -265,11 +319,16 @@ public void init() { this.tags = readerSliceConfig.getMap(Key.TAG); String address = readerSliceConfig.getString(Key.ENDPOINT); + String username = readerSliceConfig.getString(Key.USERNAME); + String password = readerSliceConfig.getString(Key.PASSWORD); - conn = new TSDBConnection(address); + conn = new TSDBConnection(address, username, password); this.startTime = readerSliceConfig.getLong(Key.BEGIN_DATE_TIME); this.endTime = readerSliceConfig.getLong(Key.END_DATE_TIME); + + this.isCombine = readerSliceConfig.getBool(Key.COMBINE, Key.COMBINE_DEFAULT_VALUE); + this.hint = readerSliceConfig.getMap(Key.HINT); } @Override @@ -283,29 +342,35 @@ public void startRead(RecordSender recordSender) { if ("TSDB".equals(type)) { for (String metric : columns4TSDB) { final Map tags = this.tags == null ? - null : (Map) this.tags.get(metric); + null : (Map) this.tags.get(metric); if (fields == null || !fields.containsKey(metric)) { - conn.sendDPs(metric, tags, this.startTime, this.endTime, recordSender); + conn.sendDPs(metric, tags, this.startTime, this.endTime, recordSender, hint); } else { conn.sendDPs(metric, (List) fields.get(metric), - tags, this.startTime, this.endTime, recordSender); + tags, this.startTime, this.endTime, recordSender, hint); } } } else { - for (String metric : metrics) { + if (isCombine) { final Map tags = this.tags == null ? - null : (Map) this.tags.get(metric); - if (fields == null || !fields.containsKey(metric)) { - conn.sendRecords(metric, tags, startTime, endTime, columns4RDB, recordSender); - } else { - conn.sendRecords(metric, (List) fields.get(metric), - tags, startTime, endTime, columns4RDB, recordSender); + null : (Map) this.tags.get(metrics.get(0)); + conn.sendRecords(metrics, tags, startTime, endTime, columns4RDB, recordSender, hint); + } else { + for (String metric : metrics) { + final Map tags = this.tags == null ? + null : (Map) this.tags.get(metric); + if (fields == null || !fields.containsKey(metric)) { + conn.sendRecords(metric, tags, startTime, endTime, columns4RDB, isCombine, recordSender, hint); + } else { + conn.sendRecords(metric, (List) fields.get(metric), + tags, startTime, endTime, columns4RDB, recordSender, hint); + } } } } } catch (Exception e) { throw DataXException.asDataXException( - TSDBReaderErrorCode.ILLEGAL_VALUE, "获取或发送数据点的过程中出错!", e); + TSDBReaderErrorCode.ILLEGAL_VALUE, "Error in getting or sending data point!", e); } } diff --git a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/Connection4TSDB.java b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/Connection4TSDB.java index 500894bbf1..96cb7f9d72 100644 --- a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/Connection4TSDB.java +++ b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/Connection4TSDB.java @@ -22,6 +22,20 @@ public interface Connection4TSDB { */ String address(); + /** + * Get the address of Database. + * + * @return host+ip + */ + String username(); + + /** + * Get the address of Database. + * + * @return host+ip + */ + String password(); + /** * Get the version of Database. * @@ -46,22 +60,27 @@ public interface Connection4TSDB { /** * Send data points for TSDB with single field. */ - void sendDPs(String metric, Map tags, Long start, Long end, RecordSender recordSender) throws Exception; + void sendDPs(String metric, Map tags, Long start, Long end, RecordSender recordSender, Map hint) throws Exception; /** * Send data points for TSDB with multi fields. */ - void sendDPs(String metric, List fields, Map tags, Long start, Long end, RecordSender recordSender) throws Exception; + void sendDPs(String metric, List fields, Map tags, Long start, Long end, RecordSender recordSender, Map hint) throws Exception; /** * Send data points for RDB with single field. */ - void sendRecords(String metric, Map tags, Long start, Long end, List columns4RDB, RecordSender recordSender) throws Exception; + void sendRecords(String metric, Map tags, Long start, Long end, List columns4RDB, Boolean isCombine, RecordSender recordSender, Map hint) throws Exception; /** * Send data points for RDB with multi fields. */ - void sendRecords(String metric, List fields, Map tags, Long start, Long end, List columns4RDB, RecordSender recordSender) throws Exception; + void sendRecords(String metric, List fields, Map tags, Long start, Long end, List columns4RDB, RecordSender recordSender, Map hint) throws Exception; + + /** + * Send data points for RDB with single fields on combine mode. + */ + void sendRecords(List metrics, Map tags, Long start, Long end, List columns4RDB, RecordSender recordSender, Map hint) throws Exception; /** * Put data point. diff --git a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/DataPoint4MultiFieldsTSDB.java b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/DataPoint4MultiFieldsTSDB.java index 5b380c73e8..3e8d43d4bd 100644 --- a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/DataPoint4MultiFieldsTSDB.java +++ b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/DataPoint4MultiFieldsTSDB.java @@ -1,6 +1,6 @@ package com.alibaba.datax.plugin.reader.tsdbreader.conn; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import java.util.Map; diff --git a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/DataPoint4TSDB.java b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/DataPoint4TSDB.java index 5c5c13494b..8724bfbb98 100644 --- a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/DataPoint4TSDB.java +++ b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/DataPoint4TSDB.java @@ -1,6 +1,6 @@ package com.alibaba.datax.plugin.reader.tsdbreader.conn; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import java.util.Map; diff --git a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/TSDBConnection.java b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/TSDBConnection.java index 5426ab4961..479c16c106 100644 --- a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/TSDBConnection.java +++ b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/TSDBConnection.java @@ -2,7 +2,7 @@ import com.alibaba.datax.common.plugin.RecordSender; import com.alibaba.datax.plugin.reader.tsdbreader.util.TSDBUtils; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import org.apache.commons.lang3.StringUtils; import java.util.List; @@ -19,9 +19,13 @@ public class TSDBConnection implements Connection4TSDB { private String address; + private String username; + private String password; - public TSDBConnection(String address) { + public TSDBConnection(String address, String username, String password) { this.address = address; + this.username = username; + this.password = password; } @Override @@ -29,14 +33,24 @@ public String address() { return address; } + @Override + public String username() { + return username; + } + + @Override + public String password() { + return password; + } + @Override public String version() { - return TSDBUtils.version(address); + return TSDBUtils.version(address, username, password); } @Override public String config() { - return TSDBUtils.config(address); + return TSDBUtils.config(address, username, password); } @Override @@ -45,23 +59,28 @@ public String[] getSupportVersionPrefix() { } @Override - public void sendDPs(String metric, Map tags, Long start, Long end, RecordSender recordSender) throws Exception { - TSDBDump.dump4TSDB(this, metric, tags, start, end, recordSender); + public void sendDPs(String metric, Map tags, Long start, Long end, RecordSender recordSender, Map hint) throws Exception { + TSDBDump.dump4TSDB(this, metric, tags, start, end, recordSender, hint); + } + + @Override + public void sendDPs(String metric, List fields, Map tags, Long start, Long end, RecordSender recordSender, Map hint) throws Exception { + TSDBDump.dump4TSDB(this, metric, fields, tags, start, end, recordSender, hint); } @Override - public void sendDPs(String metric, List fields, Map tags, Long start, Long end, RecordSender recordSender) throws Exception { - TSDBDump.dump4TSDB(this, metric, fields, tags, start, end, recordSender); + public void sendRecords(String metric, Map tags, Long start, Long end, List columns4RDB, Boolean isCombine, RecordSender recordSender, Map hint) throws Exception { + TSDBDump.dump4RDB(this, metric, tags, start, end, columns4RDB, recordSender, hint); } @Override - public void sendRecords(String metric, Map tags, Long start, Long end, List columns4RDB, RecordSender recordSender) throws Exception { - TSDBDump.dump4RDB(this, metric, tags, start, end, columns4RDB, recordSender); + public void sendRecords(List metrics, Map tags, Long start, Long end, List columns4RDB, RecordSender recordSender, Map hint) throws Exception { + TSDBDump.dump4RDB(this, metrics, tags, start, end, columns4RDB, recordSender, hint); } @Override - public void sendRecords(String metric, List fields, Map tags, Long start, Long end, List columns4RDB, RecordSender recordSender) throws Exception { - TSDBDump.dump4RDB(this, metric, fields, tags, start, end, columns4RDB, recordSender); + public void sendRecords(String metric, List fields, Map tags, Long start, Long end, List columns4RDB, RecordSender recordSender, Map hint) throws Exception { + TSDBDump.dump4RDB(this, metric, fields, tags, start, end, columns4RDB, recordSender, hint); } @Override diff --git a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/TSDBDump.java b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/TSDBDump.java index 8bae3a70c6..05b9c5c220 100644 --- a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/TSDBDump.java +++ b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/TSDBDump.java @@ -4,15 +4,16 @@ import com.alibaba.datax.common.plugin.RecordSender; import com.alibaba.datax.plugin.reader.tsdbreader.Constant; import com.alibaba.datax.plugin.reader.tsdbreader.util.HttpUtils; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.parser.Feature; +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONReader.Feature; +import com.alibaba.fastjson2.JSONWriter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; + +import static com.alibaba.datax.plugin.reader.tsdbreader.Constant.METRIC_SPECIFY_KEY_PREFIX_LENGTH; /** * Copyright @ 2019 alibaba.com @@ -30,17 +31,17 @@ final class TSDBDump { private static final String QUERY_MULTI_FIELD = "/api/mquery"; static { - JSON.DEFAULT_PARSER_FEATURE &= ~Feature.UseBigDecimal.getMask(); + JSON.config(Feature.UseBigDecimalForDoubles); } private TSDBDump() { } static void dump4TSDB(TSDBConnection conn, String metric, Map tags, - Long start, Long end, RecordSender sender) throws Exception { + Long start, Long end, RecordSender sender, Map hint) throws Exception { LOG.info("conn address: {}, metric: {}, start: {}, end: {}", conn.address(), metric, start, end); - String res = queryRange4SingleField(conn, metric, tags, start, end); + String res = queryRange4SingleField(conn, metric, tags, start, end, hint); List dps = getDps4TSDB(metric, res); if (dps == null || dps.isEmpty()) { return; @@ -49,10 +50,10 @@ static void dump4TSDB(TSDBConnection conn, String metric, Map ta } static void dump4TSDB(TSDBConnection conn, String metric, List fields, Map tags, - Long start, Long end, RecordSender sender) throws Exception { + Long start, Long end, RecordSender sender, Map hint) throws Exception { LOG.info("conn address: {}, metric: {}, start: {}, end: {}", conn.address(), metric, start, end); - String res = queryRange4MultiFields(conn, metric, fields, tags, start, end); + String res = queryRange4MultiFields(conn, metric, fields, tags, start, end, hint); List dps = getDps4TSDB(metric, fields, res); if (dps == null || dps.isEmpty()) { return; @@ -61,10 +62,10 @@ static void dump4TSDB(TSDBConnection conn, String metric, List fields, M } static void dump4RDB(TSDBConnection conn, String metric, Map tags, - Long start, Long end, List columns4RDB, RecordSender sender) throws Exception { + Long start, Long end, List columns4RDB, RecordSender sender, Map hint) throws Exception { LOG.info("conn address: {}, metric: {}, start: {}, end: {}", conn.address(), metric, start, end); - String res = queryRange4SingleField(conn, metric, tags, start, end); + String res = queryRange4SingleField(conn, metric, tags, start, end, hint); List dps = getDps4RDB(metric, res); if (dps == null || dps.isEmpty()) { return; @@ -92,12 +93,71 @@ static void dump4RDB(TSDBConnection conn, String metric, Map tag } } + public static void dump4RDB(TSDBConnection conn, List metrics, Map tags, Long start, Long end, List columns4RDB, RecordSender sender, Map hint) throws Exception { + LOG.info("conn address: {}, metric: {}, start: {}, end: {}", conn.address(), metrics, start, end); + + List dps = new LinkedList<>(); + for (String metric : metrics) { + String res = queryRange4SingleField(conn, metric, tags, start, end, hint); + final List dpList = getDps4RDB(metric, res); + if (dpList == null || dpList.isEmpty()) { + continue; + } + dps.addAll(dpList); + } + if (dps.isEmpty()) { + return; + } + Map> dpsCombinedByTs = new LinkedHashMap<>(); + for (DataPoint4TSDB dp : dps) { + final long ts = dp.getTimestamp(); + final Map dpsWithSameTs = dpsCombinedByTs.computeIfAbsent(ts, k -> new LinkedHashMap<>()); + dpsWithSameTs.put(dp.getMetric(), dp); + } + + for (Map.Entry> entry : dpsCombinedByTs.entrySet()) { + final Long ts = entry.getKey(); + final Map metricAndDps = entry.getValue(); + final Record record = sender.createRecord(); + DataPoint4TSDB tmpDp = null; + + for (final String column : columns4RDB) { + if (column.startsWith(Constant.METRIC_SPECIFY_KEY)) { + final String m = column.substring(METRIC_SPECIFY_KEY_PREFIX_LENGTH); + tmpDp = metricAndDps.get(m); + if (tmpDp == null) { + continue; + } + record.addColumn(getColumn(tmpDp.getValue())); + } else if (Constant.TS_SPECIFY_KEY.equals(column)) { + record.addColumn(new LongColumn(ts)); + } else if (Constant.VALUE_SPECIFY_KEY.equals(column)) { + // combine 模式下,不应该定义 __value__ 字段,因为 __metric__.xxx 字段会输出对应的 value 值 + throw new RuntimeException("The " + Constant.VALUE_SPECIFY_KEY + + " column should not be specified in combine mode!"); + } else { + // combine 模式下,应该确保 __metric__.xxx 字段的定义,放在 column 数组的最前面,以保证获取到 metric + if (tmpDp == null) { + throw new RuntimeException("These " + Constant.METRIC_SPECIFY_KEY_PREFIX + + " column should be placed first in the column array in combine mode!"); + } + final Object tagv = tmpDp.getTags().get(column); + if (tagv == null) { + continue; + } + record.addColumn(getColumn(tagv)); + } + } + sender.sendToWriter(record); + } + } + static void dump4RDB(TSDBConnection conn, String metric, List fields, Map tags, Long start, Long end, - List columns4RDB, RecordSender sender) throws Exception { + List columns4RDB, RecordSender sender, Map hint) throws Exception { LOG.info("conn address: {}, metric: {}, start: {}, end: {}", conn.address(), metric, start, end); - String res = queryRange4MultiFields(conn, metric, fields, tags, start, end); + String res = queryRange4MultiFields(conn, metric, fields, tags, start, end, hint); List dps = getDps4RDB(metric, fields, res); if (dps == null || dps.isEmpty()) { return; @@ -131,14 +191,16 @@ private static Column getColumn(Object value) throws Exception { valueColumn = new LongColumn((Long) value); } else if (value instanceof String) { valueColumn = new StringColumn((String) value); + } else if (value instanceof Integer) { + valueColumn = new LongColumn(((Integer)value).longValue()); } else { - throw new Exception(String.format("value 不支持类型: [%s]", value.getClass().getSimpleName())); + throw new Exception(String.format("value not supported type: [%s]", value.getClass().getSimpleName())); } return valueColumn; } private static String queryRange4SingleField(TSDBConnection conn, String metric, Map tags, - Long start, Long end) throws Exception { + Long start, Long end, Map hint) throws Exception { String tagKV = getFilterByTags(tags); String body = "{\n" + " \"start\": " + start + ",\n" + @@ -148,14 +210,15 @@ private static String queryRange4SingleField(TSDBConnection conn, String metric, " \"aggregator\": \"none\",\n" + " \"metric\": \"" + metric + "\"\n" + (tagKV == null ? "" : tagKV) + + (hint == null ? "" : (", \"hint\": " + JSON.toJSONString(hint))) + " }\n" + " ]\n" + "}"; - return HttpUtils.post(conn.address() + QUERY, body); + return HttpUtils.post(conn.address() + QUERY, conn.username(), conn.password(), body); } private static String queryRange4MultiFields(TSDBConnection conn, String metric, List fields, - Map tags, Long start, Long end) throws Exception { + Map tags, Long start, Long end, Map hint) throws Exception { // fields StringBuilder fieldBuilder = new StringBuilder(); fieldBuilder.append("\"fields\":["); @@ -177,10 +240,11 @@ private static String queryRange4MultiFields(TSDBConnection conn, String metric, " \"metric\": \"" + metric + "\",\n" + fieldBuilder.toString() + (tagKV == null ? "" : tagKV) + + (hint == null ? "" : (", \"hint\": " + JSON.toJSONString(hint))) + " }\n" + " ]\n" + "}"; - return HttpUtils.post(conn.address() + QUERY_MULTI_FIELD, body); + return HttpUtils.post(conn.address() + QUERY_MULTI_FIELD, conn.username(), conn.password(), body); } private static String getFilterByTags(Map tags) { diff --git a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/util/HttpUtils.java b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/util/HttpUtils.java index 3e0be85429..af81988c67 100644 --- a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/util/HttpUtils.java +++ b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/util/HttpUtils.java @@ -1,11 +1,13 @@ package com.alibaba.datax.plugin.reader.tsdbreader.util; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; +import org.apache.commons.lang3.StringUtils; import org.apache.http.client.fluent.Content; import org.apache.http.client.fluent.Request; import org.apache.http.entity.ContentType; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -22,13 +24,18 @@ public final class HttpUtils { public final static int CONNECT_TIMEOUT_DEFAULT_IN_MILL = (int) TimeUnit.SECONDS.toMillis(60); public final static int SOCKET_TIMEOUT_DEFAULT_IN_MILL = (int) TimeUnit.SECONDS.toMillis(60); + private static final String CREDENTIALS_FORMAT = "%s:%s"; + private static final String BASIC_AUTHENTICATION_FORMAT = "Basic %s"; + private HttpUtils() { } - public static String get(String url) throws Exception { - Content content = Request.Get(url) + public static String get(String url, String username, String password) throws Exception { + final Request request = Request.Get(url) .connectTimeout(CONNECT_TIMEOUT_DEFAULT_IN_MILL) - .socketTimeout(SOCKET_TIMEOUT_DEFAULT_IN_MILL) + .socketTimeout(SOCKET_TIMEOUT_DEFAULT_IN_MILL); + addAuth(request, username, password); + Content content = request .execute() .returnContent(); if (content == null) { @@ -37,24 +44,21 @@ public static String get(String url) throws Exception { return content.asString(StandardCharsets.UTF_8); } - public static String post(String url, Map params) throws Exception { - return post(url, JSON.toJSONString(params), CONNECT_TIMEOUT_DEFAULT_IN_MILL, SOCKET_TIMEOUT_DEFAULT_IN_MILL); - } - - public static String post(String url, String params) throws Exception { - return post(url, params, CONNECT_TIMEOUT_DEFAULT_IN_MILL, SOCKET_TIMEOUT_DEFAULT_IN_MILL); + public static String post(String url, String username, String password, Map params) throws Exception { + return post(url, username, password, JSON.toJSONString(params), CONNECT_TIMEOUT_DEFAULT_IN_MILL, SOCKET_TIMEOUT_DEFAULT_IN_MILL); } - public static String post(String url, Map params, - int connectTimeoutInMill, int socketTimeoutInMill) throws Exception { - return post(url, JSON.toJSONString(params), connectTimeoutInMill, socketTimeoutInMill); + public static String post(String url, String username, String password, String params) throws Exception { + return post(url, username, password, params, CONNECT_TIMEOUT_DEFAULT_IN_MILL, SOCKET_TIMEOUT_DEFAULT_IN_MILL); } - public static String post(String url, String params, + public static String post(String url, String username, String password, String params, int connectTimeoutInMill, int socketTimeoutInMill) throws Exception { - Content content = Request.Post(url) + Request request = Request.Post(url) .connectTimeout(connectTimeoutInMill) - .socketTimeout(socketTimeoutInMill) + .socketTimeout(socketTimeoutInMill); + addAuth(request, username, password); + Content content = request .addHeader("Content-Type", "application/json") .bodyString(params, ContentType.APPLICATION_JSON) .execute() @@ -64,4 +68,20 @@ public static String post(String url, String params, } return content.asString(StandardCharsets.UTF_8); } + + private static void addAuth(Request request, String username, String password) { + String authorization = generateHttpAuthorization(username, password); + if (authorization != null) { + request.setHeader("Authorization", authorization); + } + } + + private static String generateHttpAuthorization(String username, String password) { + if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { + return null; + } + String credentials = String.format(CREDENTIALS_FORMAT, username, password); + credentials = Base64.getEncoder().encodeToString(credentials.getBytes()); + return String.format(BASIC_AUTHENTICATION_FORMAT, credentials); + } } diff --git a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/util/TSDBUtils.java b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/util/TSDBUtils.java index bb7b4b87c3..d91c355780 100644 --- a/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/util/TSDBUtils.java +++ b/tsdbreader/src/main/java/com/alibaba/datax/plugin/reader/tsdbreader/util/TSDBUtils.java @@ -1,11 +1,5 @@ package com.alibaba.datax.plugin.reader.tsdbreader.util; -import com.alibaba.datax.plugin.reader.tsdbreader.conn.DataPoint4TSDB; -import com.alibaba.fastjson.JSON; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; /** * Copyright @ 2019 alibaba.com @@ -17,52 +11,28 @@ */ public final class TSDBUtils { - private static final Logger LOGGER = LoggerFactory.getLogger(TSDBUtils.class); - private TSDBUtils() { } - public static String version(String address) { + public static String version(String address, String username, String password) { String url = String.format("%s/api/version", address); String rsp; try { - rsp = HttpUtils.get(url); + rsp = HttpUtils.get(url, username, password); } catch (Exception e) { throw new RuntimeException(e); } return rsp; } - public static String config(String address) { + public static String config(String address, String username, String password) { String url = String.format("%s/api/config", address); String rsp; try { - rsp = HttpUtils.get(url); + rsp = HttpUtils.get(url, username, password); } catch (Exception e) { throw new RuntimeException(e); } return rsp; } - - public static boolean put(String address, List dps) { - return put(address, JSON.toJSON(dps)); - } - - public static boolean put(String address, DataPoint4TSDB dp) { - return put(address, JSON.toJSON(dp)); - } - - private static boolean put(String address, Object o) { - String url = String.format("%s/api/put", address); - String rsp; - try { - rsp = HttpUtils.post(url, o.toString()); - // If successful, the returned content should be null. - assert rsp == null; - } catch (Exception e) { - LOGGER.error("Address: {}, DataPoints: {}", url, o); - throw new RuntimeException(e); - } - return true; - } } diff --git a/tsdbreader/src/main/resources/plugin.json b/tsdbreader/src/main/resources/plugin.json index f2dbb1f075..3b10d228b1 100755 --- a/tsdbreader/src/main/resources/plugin.json +++ b/tsdbreader/src/main/resources/plugin.json @@ -6,5 +6,5 @@ "mechanism": "通过 /api/query 接口查询出符合条件的数据点", "warn": "指定起止时间会自动忽略分钟和秒,转为整点时刻,例如 2019-4-18 的 [3:35, 4:55) 会被转为 [3:00, 4:00)" }, - "developer": "Benedict Jin" + "developer": "alibaba" } diff --git a/tsdbreader/src/test/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/TSDBConnectionTest.java b/tsdbreader/src/test/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/TSDBConnectionTest.java index e45440886d..6be291e8cd 100644 --- a/tsdbreader/src/test/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/TSDBConnectionTest.java +++ b/tsdbreader/src/test/java/com/alibaba/datax/plugin/reader/tsdbreader/conn/TSDBConnectionTest.java @@ -19,12 +19,12 @@ public class TSDBConnectionTest { @Test public void testVersion() { - String version = new TSDBConnection(TSDB_ADDRESS).version(); + String version = new TSDBConnection(TSDB_ADDRESS,null,null).version(); Assert.assertNotNull(version); } @Test public void testIsSupported() { - Assert.assertTrue(new TSDBConnection(TSDB_ADDRESS).isSupported()); + Assert.assertTrue(new TSDBConnection(TSDB_ADDRESS,null,null).isSupported()); } } diff --git a/tsdbreader/src/test/java/com/alibaba/datax/plugin/reader/tsdbreader/util/HttpUtilsTest.java b/tsdbreader/src/test/java/com/alibaba/datax/plugin/reader/tsdbreader/util/HttpUtilsTest.java deleted file mode 100644 index 12a2660a9c..0000000000 --- a/tsdbreader/src/test/java/com/alibaba/datax/plugin/reader/tsdbreader/util/HttpUtilsTest.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.alibaba.datax.plugin.reader.tsdbreader.util; - -import org.junit.Assert; -import org.junit.Ignore; -import org.junit.Test; - -import java.util.HashMap; -import java.util.Map; - -/** - * Copyright @ 2019 alibaba.com - * All right reserved. - * Function:HttpUtils Test - * - * @author Benedict Jin - * @since 2019-10-21 - */ -@Ignore -public class HttpUtilsTest { - - @Test - public void testSimpleCase() throws Exception { - String url = "https://httpbin.org/post"; - Map params = new HashMap<>(); - params.put("foo", "bar"); - - String rsp = HttpUtils.post(url, params); - System.out.println(rsp); - Assert.assertNotNull(rsp); - } - - @Test - public void testGet() throws Exception { - String url = String.format("%s/api/version", Const.TSDB_ADDRESS); - String rsp = HttpUtils.get(url); - System.out.println(rsp); - Assert.assertNotNull(rsp); - } -} diff --git a/tsdbwriter/doc/tsdbhttpwriter.md b/tsdbwriter/doc/tsdbhttpwriter.md index c723a360c6..c0b136542a 100644 --- a/tsdbwriter/doc/tsdbhttpwriter.md +++ b/tsdbwriter/doc/tsdbhttpwriter.md @@ -6,24 +6,57 @@ ___ ## 1 快速介绍 -TSDBWriter 插件实现了将数据点写入到阿里巴巴自研 TSDB 数据库中(后续简称 TSDB)。 +TSDBWriter 插件实现了将数据点写入到阿里巴巴云原生多模数据库Lindorm TSDB数据库中(后续简称 TSDB)。 -时间序列数据库(Time Series Database , 简称 TSDB)是一种高性能,低成本,稳定可靠的在线时序数据库服务;提供高效读写,高压缩比存储、时序数据插值及聚合计算,广泛应用于物联网(IoT)设备监控系统 ,企业能源管理系统(EMS),生产安全监控系统,电力检测系统等行业场景。 TSDB 提供百万级时序数据秒级写入,高压缩比低成本存储、预降采样、插值、多维聚合计算,查询结果可视化功能;解决由于设备采集点数量巨大,数据采集频率高,造成的存储成本高,写入和查询分析效率低的问题。更多关于 TSDB 的介绍,详见[阿里云 TSDB 官网](https://help.aliyun.com/product/54825.html)。 +时间序列数据库(Time Series Database , 简称 TSDB)是一种高性能,低成本,稳定可靠的在线时序数据库服务;提供高效读写,高压缩比存储、时序数据插值及聚合计算,广泛应用于物联网(IoT)设备监控系统 ,企业能源管理系统(EMS),生产安全监控系统,电力检测系统等行业场景。 TSDB 提供千万级时序数据秒级写入,高压缩比低成本存储、预降采样、插值、多维聚合计算,查询结果可视化功能;解决由于设备采集点数量巨大,数据采集频率高,造成的存储成本高,写入和查询分析效率低的问题。更多关于 TSDB 的介绍,详见[阿里云 Lindorm TSDB 官网](https://help.aliyun.com/document_detail/174600.html)。 +注意:阿里巴巴自研HiTSDB已全新升级为云原生多模数据库Lindorm TSDB。Lindorm TSDB兼容大部分HiTSDB的HTTP API并提供原生SQL能力,TSDBWriter插件使用HTTP API方式写入,要使用原生SQL能力需要提前在Lindorm TSDB进行建表。详细参见[与旧版TSDB的比较](https://help.aliyun.com/document_detail/387477.html) ## 2 实现原理 -通过 HTTP 连接 TSDB 实例,并通过 `/api/put` 接口将数据点写入。关于写入接口详见 TSDB 的[接口说明文档](https://help.aliyun.com/document_detail/59939.html)。 - - +通过TSDB客户端 hitsdb-client 连接 TSDB 实例,并将数据点通过HTTP API方式写入。关于写入接口详见TSDB 的[SDK 参考](https://help.aliyun.com/document_detail/61587.html)。 ## 3 功能说明 ### 3.1 配置样例 -* 配置一个从 OpenTSDB 数据库同步抽取数据到 TSDB: +* 配置TSDB Writer: +```json +{ + "name": "tsdbwriter", + "parameter": { + "endpoint": "http://localhost:8242", + "sourceDbType": "RDB", + "batchSize": 256, + "columnType": [ + "tag", + "tag", + "field_string", + "field_double", + "timestamp", + "field_bool" + ], + "column": [ + "tag1", + "tag2", + "field1", + "field2", + "timestamp", + "field3" + ], + "multiField":"true", + "table":"testmetric", + "username":"xxx", + "password":"xxx", + "ignoreWriteError":"false", + "database":"default" + } +} +``` + +* 配置一个从 支持 OpenTSDB 协议的数据库同步抽取数据到 TSDB: ```json { @@ -42,7 +75,7 @@ TSDBWriter 插件实现了将数据点写入到阿里巴巴自研 TSDB 数据库 } }, "writer": { - "name": "tsdbhttpwriter", + "name": "tsdbwriter", "parameter": { "endpoint": "http://localhost:8242" } @@ -58,7 +91,37 @@ TSDBWriter 插件实现了将数据点写入到阿里巴巴自研 TSDB 数据库 } ``` - +* 使用 OpenTSDB (单值)协议写入TSDB(不推荐): +```json +{ + "name": "tsdbwriter", + "endpoint": "http://localhost:8242", + "sourceDbType": "RDB", + "parameter": { + "batchSize": 256, + "columnType": [ + "tag", + "tag", + "field_string", + "field_double", + "timestamp", + "field_boolean" + ], + "column": [ + "tag1", + "tag2", + "field_metric_1", + "field_metric_2", + "timestamp", + "field_metric_3" + ], + "username":"tsdb", + "password":"enxU^", + "ignoreWriteError":"false" + } +} +``` +转换到的TSDB 表名(metric)由column中field对应的列名决定:对于上诉配置一行关系型数据将会写入三个metric(field_metric_1,field_metric_2,field_metric_3) ### 3.2 参数说明 @@ -74,23 +137,85 @@ TSDBWriter 插件实现了将数据点写入到阿里巴巴自研 TSDB 数据库 * 格式:http://IP:Port * 默认值:无 +* **sourceDbType** + * 描述:源端数据类型 + * 必选:否 + * 格式:string [RDB或者TSDB] + * 默认值:TSDB + +* **multiField** + * 描述:使用HTTP API多值(多个field)方式写入,目前TSDB版本使用多值写入,需要指定为true + * 必选:是 + * 格式:bool + * 默认值:false (单值) + * 说明: 如果使用Lindorm TSDB原生SQL能力访问HTTP API方式写入的数据,需要在TSDB进行预建表,否则只能使用HiTSDB HTTP API方式[查询数据](https://help.aliyun.com/document_detail/107576.html)。 + +* **column** + * 描述:关系型数据库中表的字段名 + * 必选:当sourceDbType为RDB时必选 + * 格式:string + * 默认值:无 + * 说明: 此处的字段顺序,需要和Reader插件中配置的column字段的顺序保持一致。 + +* **columnType** + * 描述:关系型数据库中表字段,映射到TSDB中的类型。支持的类型如下所示: + * timestamp:该字段为时间戳 + * tag:该字段为tag + * field_string: 该Field的value是字符串类型 + * field_double: 该Field的value是数值类型 + * field_boolean: 该Field的value是布尔类型 + * 必选:当sourceDbType为RDB时必选 + * 格式:string [RDB或者TSDB] + * 默认值: 无 + * 说明: 此处的字段顺序,需要和column配置中的字段顺序保持一致 + +* **table** + * 描述:TSDB对应表名(metric) + * 必选:当sourceDbType为RDB时且multiField为true时必选 + * 格式:string + * 默认值:无 + * 说明: 要导入的TSDB表名,如果multiField为falase,不需要填写,对应的metric需要写到column字段 + * **batchSize** * 描述:每次批量数据的条数 * 必选:否 * 格式:int,需要保证大于 0 * 默认值:100 +* **ignoreWriteError** + * 描述:如果设置为 true,则忽略写入错误,继续写入;否则,多次重试后仍写入失败的话,则会终止写入任务 + * 必选:否 + * 格式:bool + * 默认值:false + +* **username** + * 描述:数据库用户名 + * 必选:否 + * 格式:string + * 默认值:无 + * 说明: TSDB配置了鉴权需要填写 + + +* **password** + * 描述:数据库密码 + * 必选:否 + * 格式:string + * 默认值:无 + * 说明: TSDB配置了鉴权需要填写 + +* **database** + * 描述:导入的数据库 + * 必选:否 + * 格式:string + * 默认值:default + * 说明: TSDB需要提前创建数据库 + * **maxRetryTime** * 描述:失败后重试的次数 * 必选:否 * 格式:int,需要保证大于 1 * 默认值:3 -* **ignoreWriteError** - * 描述:如果设置为 true,则忽略写入错误,继续写入;否则,多次重试后仍写入失败的话,则会终止写入任务 - * 必选:否 - * 格式:bool - * 默认值:false @@ -171,7 +296,7 @@ HBase 机型: 8C16G * 5 ## 5 约束限制 -### 5.1 目前只支持兼容 TSDB 2.4.x 及以上版本 +### 5.1 目前支持Lindorm TSDB全部版本 以及 HiTSDB 2.4.x 及以上版本 其他版本暂不保证兼容 diff --git a/tsdbwriter/pom.xml b/tsdbwriter/pom.xml index 497264c0ec..9f99712340 100644 --- a/tsdbwriter/pom.xml +++ b/tsdbwriter/pom.xml @@ -21,14 +21,11 @@ 3.3.2 - 4.4 + 4.5 2.4 - - 1.2.28 - - 4.12 + 4.13.1 @@ -41,10 +38,6 @@ slf4j-log4j12 org.slf4j - - fastjson - com.alibaba - commons-math3 org.apache.commons @@ -86,9 +79,15 @@ - com.alibaba - fastjson - ${fastjson.version} + com.alibaba.fastjson2 + fastjson2 + + + + + com.aliyun + hitsdb-client + 0.3.7 diff --git a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/conn/Connection4TSDB.java b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/conn/Connection4TSDB.java index 8119348d1b..ecb3005553 100644 --- a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/conn/Connection4TSDB.java +++ b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/conn/Connection4TSDB.java @@ -21,6 +21,28 @@ public interface Connection4TSDB { */ String address(); + /** + * Get the setted database name. + * + * @return database + */ + String database(); + + + /** + * Get the username of Database. + * + * @return username + */ + String username(); + + /** + * Get the password of Database. + * + * @return password + */ + String password(); + /** * Get the version of Database. * @@ -69,17 +91,25 @@ public interface Connection4TSDB { boolean put(List dps); /** - * Put data points. + * Put data points with single field. * * @param dps data points * @return whether the data point is written successfully */ boolean put(String dps); + /** + * Put data points with multi fields. + * + * @param dps data points + * @return whether the data point is written successfully + */ + boolean mput(String dps); + /** * Whether current version is supported. * * @return true: supported; false: not yet! */ boolean isSupported(); -} +} \ No newline at end of file diff --git a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/conn/DataPoint4TSDB.java b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/conn/DataPoint4TSDB.java index fee012dfd7..b6e2d309d1 100644 --- a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/conn/DataPoint4TSDB.java +++ b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/conn/DataPoint4TSDB.java @@ -1,6 +1,6 @@ package com.alibaba.datax.plugin.writer.conn; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import java.util.Map; diff --git a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/conn/TSDBConnection.java b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/conn/TSDBConnection.java index e4ebad7d72..5266f5d9c4 100644 --- a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/conn/TSDBConnection.java +++ b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/conn/TSDBConnection.java @@ -2,7 +2,7 @@ import com.alibaba.datax.common.plugin.RecordSender; import com.alibaba.datax.plugin.writer.util.TSDBUtils; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; import org.apache.commons.lang3.StringUtils; import java.util.List; @@ -18,12 +18,18 @@ public class TSDBConnection implements Connection4TSDB { private String address; + private String username; + private String password; + private String database; - public TSDBConnection(String address) { + public TSDBConnection(String address, String database, String username, String password) { if (StringUtils.isBlank(address)) { throw new RuntimeException("TSDBConnection init failed because address is blank!"); } this.address = address; + this.database = database; + this.username = username; + this.password = password; } @Override @@ -31,14 +37,29 @@ public String address() { return address; } + @Override + public String username() { + return username; + } + + @Override + public String database() { + return database; + } + + @Override + public String password() { + return password; + } + @Override public String version() { - return TSDBUtils.version(address); + return TSDBUtils.version(address, username, password); } @Override public String config() { - return TSDBUtils.config(address); + return TSDBUtils.config(address, username, password); } @Override @@ -53,17 +74,22 @@ public void sendDPs(String metric, Long start, Long end, RecordSender recordSend @Override public boolean put(DataPoint4TSDB dp) { - return TSDBUtils.put(address, dp); + return TSDBUtils.put(address, database, username, password, dp); } @Override public boolean put(List dps) { - return TSDBUtils.put(address, dps); + return TSDBUtils.put(address, database, username, password, dps); } @Override public boolean put(String dps) { - return TSDBUtils.put(address, dps); + return TSDBUtils.put(address, database, username, password, dps); + } + + @Override + public boolean mput(String dps) { + return TSDBUtils.mput(address, database, username, password, dps); } @Override diff --git a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/Key.java b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/Key.java index 2cc3f67124..6cb239ece9 100755 --- a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/Key.java +++ b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/Key.java @@ -10,8 +10,22 @@ */ public class Key { + static final String SOURCE_DB_TYPE = "sourceDbType"; + static final String MULTI_FIELD = "multiField"; + + // common static final String ENDPOINT = "endpoint"; + static final String USERNAME = "username"; + static final String PASSWORD = "password"; + static final String IGNORE_WRITE_ERROR = "ignoreWriteError"; + static final String DATABASE = "database"; + + // for tsdb static final String BATCH_SIZE = "batchSize"; static final String MAX_RETRY_TIME = "maxRetryTime"; - static final String IGNORE_WRITE_ERROR = "ignoreWriteError"; + + // for rdb + static final String COLUMN = "column"; + static final String COLUMN_TYPE = "columnType"; + static final String TABLE = "table"; } diff --git a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/SourceDBType.java b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/SourceDBType.java new file mode 100644 index 0000000000..792806a651 --- /dev/null +++ b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/SourceDBType.java @@ -0,0 +1,5 @@ +package com.alibaba.datax.plugin.writer.tsdbwriter; + +public enum SourceDBType { + TSDB, RDB +} diff --git a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBConverter.java b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBConverter.java new file mode 100644 index 0000000000..9bde0c9e06 --- /dev/null +++ b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBConverter.java @@ -0,0 +1,96 @@ +package com.alibaba.datax.plugin.writer.tsdbwriter; + +import com.alibaba.datax.common.element.Column; +import com.alibaba.datax.common.element.Record; +import com.alibaba.fastjson2.JSON; +import com.aliyun.hitsdb.client.value.request.MultiFieldPoint; +import com.aliyun.hitsdb.client.value.request.Point; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class TSDBConverter { + + private static final Logger LOG = LoggerFactory.getLogger(TSDBConverter.class); + + private List columnName; + private List columnType; + + TSDBConverter(List columnName, List columnType) { + this.columnName = columnName; + this.columnType = columnType; + LOG.info("columnName: {}, columnType: {}", JSON.toJSONString(columnName), JSON.toJSONString(columnType)); + } + + List transRecord2Point(List records) { + List dps = new ArrayList(); + for (Record record : records) { + List metricBuilders = new ArrayList(); + Map tags = new HashMap(); + Long time = 0L; + + for (int i = 0; i < columnType.size(); i++) { + String type = columnType.get(i); + String name = columnName.get(i); + Column column = record.getColumn(i); + if (TSDBModel.TSDB_TAG.equals(type)) { + tags.put(name, column.asString()); + } else if (TSDBModel.TSDB_FIELD_DOUBLE.equals(type)) { + metricBuilders.add(new Point.MetricBuilder(name).value(column.asDouble())); + } else if (TSDBModel.TSDB_FIELD_STRING.equals(type)) { + metricBuilders.add(new Point.MetricBuilder(name).value(column.asString())); + } else if (TSDBModel.TSDB_FIELD_BOOL.equals(type)) { + metricBuilders.add(new Point.MetricBuilder(name).value(column.asBoolean())); + } else if (TSDBModel.TSDB_TIMESTAMP.equals(type)) { + time = column.asLong(); + } else if (TSDBModel.TSDB_METRIC_NUM.equals(type)) { + // compatible with previous usage of TSDB_METRIC_NUM + metricBuilders.add(new Point.MetricBuilder(name).value(column.asDouble())); + } else if (TSDBModel.TSDB_METRIC_STRING.equals(type)) { + // compatible with previous usage of TSDB_METRIC_STRING + metricBuilders.add(new Point.MetricBuilder(name).value(column.asString())); + } + } + for (Point.MetricBuilder metricBuilder : metricBuilders) { + dps.add(metricBuilder.tag(tags).timestamp(time).build(false)); + } + } + return dps; + } + + List transRecord2MultiFieldPoint(List records, String tableName) { + List dps = new ArrayList(); + for (Record record : records) { + MultiFieldPoint.MetricBuilder builder = MultiFieldPoint.metric(tableName); + for (int i = 0; i < columnType.size(); i++) { + String type = columnType.get(i); + String name = columnName.get(i); + Column column = record.getColumn(i); + if (TSDBModel.TSDB_TAG.equals(type)) { + builder.tag(name, column.asString()); + } else if (TSDBModel.TSDB_FIELD_DOUBLE.equals(type)) { + builder.field(name, column.asDouble()); + } else if (TSDBModel.TSDB_FIELD_STRING.equals(type)) { + builder.field(name, column.asString()); + } else if (TSDBModel.TSDB_FIELD_BOOL.equals(type)) { + builder.field(name, column.asBoolean()); + } else if (TSDBModel.TSDB_TIMESTAMP.equals(type)) { + builder.timestamp(column.asLong()); + } else if (TSDBModel.TSDB_METRIC_NUM.equals(type)) { + // compatible with previous usage of TSDB_METRIC_NUM + builder.field(name, column.asDouble()); + } else if (TSDBModel.TSDB_METRIC_STRING.equals(type)) { + // compatible with previous usage of TSDB_METRIC_STRING + builder.field(name, column.asString()); + } + } + MultiFieldPoint point = builder.build(false); + dps.add(point); + } + return dps; + } +} diff --git a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBModel.java b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBModel.java new file mode 100644 index 0000000000..ead0e2cc65 --- /dev/null +++ b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBModel.java @@ -0,0 +1,11 @@ +package com.alibaba.datax.plugin.writer.tsdbwriter; + +class TSDBModel { + static final String TSDB_METRIC_NUM = "metric_num"; + static final String TSDB_METRIC_STRING = "metric_string"; + static final String TSDB_TAG = "tag"; + static final String TSDB_TIMESTAMP = "timestamp"; + static final String TSDB_FIELD_DOUBLE = "field_double"; + static final String TSDB_FIELD_STRING = "field_string"; + static final String TSDB_FIELD_BOOL = "field_bool"; +} \ No newline at end of file diff --git a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBWriter.java b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBWriter.java index e410b2ba06..433527daba 100755 --- a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBWriter.java +++ b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBWriter.java @@ -5,14 +5,23 @@ import com.alibaba.datax.common.plugin.RecordReceiver; import com.alibaba.datax.common.spi.Writer; import com.alibaba.datax.common.util.Configuration; +import com.alibaba.datax.common.util.ConfigurationUtil; import com.alibaba.datax.common.util.RetryUtil; import com.alibaba.datax.plugin.writer.conn.TSDBConnection; +import com.aliyun.hitsdb.client.TSDB; +import com.aliyun.hitsdb.client.TSDBClientFactory; +import com.aliyun.hitsdb.client.TSDBConfig; +import com.aliyun.hitsdb.client.value.request.MultiFieldPoint; +import com.aliyun.hitsdb.client.value.request.Point; +import com.aliyun.hitsdb.client.value.response.batch.IgnoreErrorsResult; +import com.aliyun.hitsdb.client.value.response.batch.MultiFieldIgnoreErrorsResult; +import com.aliyun.hitsdb.client.value.response.batch.SummaryResult; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; -import java.util.List; +import java.io.IOException; +import java.util.*; import java.util.concurrent.Callable; /** @@ -26,6 +35,9 @@ @SuppressWarnings("unused") public class TSDBWriter extends Writer { + private static SourceDBType DB_TYPE; + private static TSDB tsdb = null; + public static class Job extends Writer.Job { private static final Logger LOG = LoggerFactory.getLogger(Job.class); @@ -34,33 +46,100 @@ public static class Job extends Writer.Job { @Override public void init() { - this.originalConfig = super.getPluginJobConf(); + originalConfig = super.getPluginJobConf(); - String address = this.originalConfig.getString(Key.ENDPOINT); - if (StringUtils.isBlank(address)) { + // check source db type + String sourceDbType = originalConfig.getString(Key.SOURCE_DB_TYPE); + if (StringUtils.isBlank(sourceDbType)) { + sourceDbType = SourceDBType.TSDB.name(); + originalConfig.set(Key.SOURCE_DB_TYPE, sourceDbType); + LOG.info("The parameter [" + Key.SOURCE_DB_TYPE + "] will be default value: " + SourceDBType.TSDB); + } + try { + DB_TYPE = SourceDBType.valueOf(sourceDbType); + } catch (Exception e) { throw DataXException.asDataXException(TSDBWriterErrorCode.REQUIRED_VALUE, - "The parameter [" + Key.ENDPOINT + "] is not set."); + "The parameter [" + Key.SOURCE_DB_TYPE + + "] is invalid, which should be one of [" + Arrays.toString(SourceDBType.values()) + "]."); } - Integer batchSize = this.originalConfig.getInt(Key.BATCH_SIZE); - if (batchSize == null || batchSize < 1) { - originalConfig.set(Key.BATCH_SIZE, Constant.DEFAULT_BATCH_SIZE); - LOG.info("The parameter [" + Key.BATCH_SIZE + - "] will be default value: " + Constant.DEFAULT_BATCH_SIZE); - } + // for tsdb + if (DB_TYPE == SourceDBType.TSDB) { + String address = originalConfig.getString(Key.ENDPOINT); + if (StringUtils.isBlank(address)) { + throw DataXException.asDataXException(TSDBWriterErrorCode.REQUIRED_VALUE, + "The parameter [" + Key.ENDPOINT + "] is not set."); + } - Integer retrySize = this.originalConfig.getInt(Key.MAX_RETRY_TIME); - if (retrySize == null || retrySize < 0) { - originalConfig.set(Key.MAX_RETRY_TIME, Constant.DEFAULT_TRY_SIZE); - LOG.info("The parameter [" + Key.MAX_RETRY_TIME + - "] will be default value: " + Constant.DEFAULT_TRY_SIZE); - } + String username = originalConfig.getString(Key.USERNAME, null); + if (StringUtils.isBlank(username)) { + LOG.warn("The parameter [" + Key.USERNAME + "] is blank."); + } + String password = originalConfig.getString(Key.PASSWORD, null); + if (StringUtils.isBlank(password)) { + LOG.warn("The parameter [" + Key.PASSWORD + "] is blank."); + } + + Integer batchSize = originalConfig.getInt(Key.BATCH_SIZE); + if (batchSize == null || batchSize < 1) { + originalConfig.set(Key.BATCH_SIZE, Constant.DEFAULT_BATCH_SIZE); + LOG.info("The parameter [" + Key.BATCH_SIZE + + "] will be default value: " + Constant.DEFAULT_BATCH_SIZE); + } + + Integer retrySize = originalConfig.getInt(Key.MAX_RETRY_TIME); + if (retrySize == null || retrySize < 0) { + originalConfig.set(Key.MAX_RETRY_TIME, Constant.DEFAULT_TRY_SIZE); + LOG.info("The parameter [" + Key.MAX_RETRY_TIME + + "] will be default value: " + Constant.DEFAULT_TRY_SIZE); + } + + Boolean ignoreWriteError = originalConfig.getBool(Key.IGNORE_WRITE_ERROR); + if (ignoreWriteError == null) { + originalConfig.set(Key.IGNORE_WRITE_ERROR, Constant.DEFAULT_IGNORE_WRITE_ERROR); + LOG.info("The parameter [" + Key.IGNORE_WRITE_ERROR + + "] will be default value: " + Constant.DEFAULT_IGNORE_WRITE_ERROR); + } + } else if (DB_TYPE == SourceDBType.RDB) { + // for rdb + originalConfig.getNecessaryValue(Key.ENDPOINT, TSDBWriterErrorCode.REQUIRED_VALUE); + originalConfig.getNecessaryValue(Key.COLUMN_TYPE, TSDBWriterErrorCode.REQUIRED_VALUE); + originalConfig.getNecessaryValue(Key.COLUMN, TSDBWriterErrorCode.REQUIRED_VALUE); + String endpoint = originalConfig.getString(Key.ENDPOINT); + String[] split = endpoint.split(":"); + if (split.length != 3) { + throw DataXException.asDataXException(TSDBWriterErrorCode.REQUIRED_VALUE, + "The parameter [" + Key.ENDPOINT + "] is invalid, which should be [http://IP:Port]."); + } + String ip = split[1].substring(2); + int port = Integer.parseInt(split[2]); + + String username = originalConfig.getString(Key.USERNAME, null); + if (StringUtils.isBlank(username)) { + LOG.warn("The parameter [" + Key.USERNAME + "] is blank."); + } + + String password = originalConfig.getString(Key.PASSWORD, null); + if (StringUtils.isBlank(password)) { + LOG.warn("The parameter [" + Key.PASSWORD + "] is blank."); + } + + if (!StringUtils.isBlank(password) && !StringUtils.isBlank(username)) { + tsdb = TSDBClientFactory.connect(TSDBConfig.address(ip, port).basicAuth(username, password).config()); + } else { + tsdb = TSDBClientFactory.connect(TSDBConfig.address(ip, port).config()); + } + + String database = originalConfig.getString(Key.DATABASE, null); + if (StringUtils.isBlank(database)) { + LOG.info("The parameter [" + Key.DATABASE + "] is blank."); + } else { + LOG.warn("The parameter [" + Key.DATABASE + "] : {} is ignored."); + // tsdb.useDatabase(database); + } + + LOG.info("Tsdb config: {}", ConfigurationUtil.filterSensitive(originalConfig).toJSON()); - Boolean ignoreWriteError = this.originalConfig.getBool(Key.IGNORE_WRITE_ERROR); - if (ignoreWriteError == null) { - originalConfig.set(Key.IGNORE_WRITE_ERROR, Constant.DEFAULT_IGNORE_WRITE_ERROR); - LOG.info("The parameter [" + Key.IGNORE_WRITE_ERROR + - "] will be default value: " + Constant.DEFAULT_IGNORE_WRITE_ERROR); } } @@ -72,7 +151,7 @@ public void prepare() { public List split(int mandatoryNumber) { ArrayList configurations = new ArrayList(mandatoryNumber); for (int i = 0; i < mandatoryNumber; i++) { - configurations.add(this.originalConfig.clone()); + configurations.add(originalConfig.clone()); } return configurations; } @@ -83,6 +162,14 @@ public void post() { @Override public void destroy() { + if (DB_TYPE == SourceDBType.RDB) { + if (tsdb != null) { + try { + tsdb.close(); + } catch (IOException ignored) { + } + } + } } } @@ -91,18 +178,87 @@ public static class Task extends Writer.Task { private static final Logger LOG = LoggerFactory.getLogger(Task.class); private TSDBConnection conn; + private boolean multiField; private int batchSize; private int retrySize; private boolean ignoreWriteError; + private String tableName; + private TSDBConverter tsdbConverter; @Override public void init() { Configuration writerSliceConfig = getPluginJobConf(); - String address = writerSliceConfig.getString(Key.ENDPOINT); - this.conn = new TSDBConnection(address); - this.batchSize = writerSliceConfig.getInt(Key.BATCH_SIZE); - this.retrySize = writerSliceConfig.getInt(Key.MAX_RETRY_TIME); + + // single field | multi fields + this.multiField = writerSliceConfig.getBool(Key.MULTI_FIELD, false); this.ignoreWriteError = writerSliceConfig.getBool(Key.IGNORE_WRITE_ERROR); + + // for tsdb + if (DB_TYPE == SourceDBType.TSDB) { + String address = writerSliceConfig.getString(Key.ENDPOINT); + String database = writerSliceConfig.getString(Key.DATABASE); + String username = writerSliceConfig.getString(Key.USERNAME); + String password = writerSliceConfig.getString(Key.PASSWORD); + this.conn = new TSDBConnection(address, database, username, password); + this.batchSize = writerSliceConfig.getInt(Key.BATCH_SIZE); + this.retrySize = writerSliceConfig.getInt(Key.MAX_RETRY_TIME); + + } else if (DB_TYPE == SourceDBType.RDB) { + // for rdb + int timeSize = 0; + int fieldSize = 0; + int tagSize = 0; + batchSize = writerSliceConfig.getInt(Key.BATCH_SIZE, 100); + List columnName = writerSliceConfig.getList(Key.COLUMN, String.class); + List columnType = writerSliceConfig.getList(Key.COLUMN_TYPE, String.class); + Set typeSet = new HashSet(columnType); + if (columnName.size() != columnType.size()) { + throw DataXException.asDataXException(TSDBWriterErrorCode.ILLEGAL_VALUE, + "The parameter [" + Key.COLUMN_TYPE + "] should has same length with [" + Key.COLUMN + "]."); + } + + for (String type : columnType) { + if (TSDBModel.TSDB_TAG.equals(type)) { + tagSize ++; + } else if (TSDBModel.TSDB_FIELD_DOUBLE.equals(type) || TSDBModel.TSDB_FIELD_STRING.equals(type) + || TSDBModel.TSDB_FIELD_BOOL.equals(type)) { + fieldSize++; + } else if (TSDBModel.TSDB_TIMESTAMP.equals(type)) { + timeSize++; + } + } + + if (fieldSize == 0) { + // compatible with previous usage of TSDB_METRIC_NUM and TSDB_METRIC_STRING + if (!typeSet.contains(TSDBModel.TSDB_METRIC_NUM) && !typeSet.contains(TSDBModel.TSDB_METRIC_STRING)) { + throw DataXException.asDataXException(TSDBWriterErrorCode.ILLEGAL_VALUE, + "The parameter [" + Key.COLUMN_TYPE + "] is invalid, must set at least one of " + + TSDBModel.TSDB_FIELD_DOUBLE + ", " + TSDBModel.TSDB_FIELD_STRING + " or " + TSDBModel.TSDB_FIELD_BOOL + "."); + } + } + + if (tagSize == 0) { + throw DataXException.asDataXException(TSDBWriterErrorCode.ILLEGAL_VALUE, + "The parameter [" + Key.COLUMN_TYPE + "] is invalid, must set " + TSDBModel.TSDB_TAG + ". "); + } + + if (timeSize != 1) { + throw DataXException.asDataXException(TSDBWriterErrorCode.ILLEGAL_VALUE, + "The parameter [" + Key.COLUMN_TYPE + "] is invalid, must set one and only one " + + TSDBModel.TSDB_TIMESTAMP + "."); + } + + if (multiField) { + // check source db type + tableName = writerSliceConfig.getString(Key.TABLE); + if (StringUtils.isBlank(tableName)) { + throw DataXException.asDataXException(TSDBWriterErrorCode.ILLEGAL_VALUE, + "The parameter [" + Key.TABLE + "] h must set when use multi field input."); + } + } + tsdbConverter = new TSDBConverter(columnName, columnType); + + } } @Override @@ -111,30 +267,52 @@ public void prepare() { @Override public void startWrite(RecordReceiver recordReceiver) { - try { - Record lastRecord = null; + // for tsdb + if (DB_TYPE == SourceDBType.TSDB) { + try { + Record lastRecord = null; + Record record; + int count = 0; + StringBuilder dps = new StringBuilder(); + while ((record = recordReceiver.getFromReader()) != null) { + final int recordLength = record.getColumnNumber(); + for (int i = 0; i < recordLength; i++) { + dps.append(record.getColumn(i).asString()); + dps.append(","); + count++; + if (count == batchSize) { + count = 0; + batchPut(record, "[" + dps.substring(0, dps.length() - 1) + "]"); + dps = new StringBuilder(); + } + } + lastRecord = record; + } + if (StringUtils.isNotBlank(dps.toString())) { + batchPut(lastRecord, "[" + dps.substring(0, dps.length() - 1) + "]"); + } + } catch (Exception e) { + throw DataXException.asDataXException(TSDBWriterErrorCode.RUNTIME_EXCEPTION, e); + } + } else if (DB_TYPE == SourceDBType.RDB) { + // for rdb + List writerBuffer = new ArrayList(this.batchSize); Record record; - int count = 0; - StringBuilder dps = new StringBuilder(); + long total = 0; while ((record = recordReceiver.getFromReader()) != null) { - final int recordLength = record.getColumnNumber(); - for (int i = 0; i < recordLength; i++) { - dps.append(record.getColumn(i).asString()); - dps.append(","); - count++; - if (count == batchSize) { - count = 0; - batchPut(record, "[" + dps.substring(0, dps.length() - 1) + "]"); - dps = new StringBuilder(); - } + writerBuffer.add(record); + if (writerBuffer.size() >= this.batchSize) { + total += doBatchInsert(writerBuffer); + writerBuffer.clear(); } - lastRecord = record; } - if (StringUtils.isNotBlank(dps.toString())) { - batchPut(lastRecord, "[" + dps.substring(0, dps.length() - 1) + "]"); + if (!writerBuffer.isEmpty()) { + total += doBatchInsert(writerBuffer); + writerBuffer.clear(); } - } catch (Exception e) { - throw DataXException.asDataXException(TSDBWriterErrorCode.RUNTIME_EXCEPTION, e); + getTaskPluginCollector().collectMessage("write size", total + ""); + LOG.info("Task finished, write size: {}", total); + } } @@ -143,12 +321,13 @@ private void batchPut(final Record record, final String dps) { RetryUtil.executeWithRetry(new Callable() { @Override public Integer call() { - if (!conn.put(dps)) { - getTaskPluginCollector().collectDirtyRecord(record, "Put data points failed!"); - throw DataXException.asDataXException(TSDBWriterErrorCode.RUNTIME_EXCEPTION, - "Put data points failed!"); + final boolean success = multiField ? conn.mput(dps) : conn.put(dps); + if (success) { + return 0; } - return 0; + getTaskPluginCollector().collectDirtyRecord(record, "Put data points failed!"); + throw DataXException.asDataXException(TSDBWriterErrorCode.RUNTIME_EXCEPTION, + "Put data points failed!"); } }, retrySize, 60000L, true); } catch (Exception e) { @@ -160,6 +339,47 @@ public Integer call() { } } + private long doBatchInsert(final List writerBuffer) { + int size; + if (ignoreWriteError) { + if (multiField) { + List points = tsdbConverter.transRecord2MultiFieldPoint(writerBuffer, tableName); + size = points.size(); + MultiFieldIgnoreErrorsResult ignoreErrorsResult = tsdb.multiFieldPutSync(points, MultiFieldIgnoreErrorsResult.class); + if (ignoreErrorsResult == null) { + LOG.error("Unexpected inner error for insert"); + } else if (ignoreErrorsResult.getFailed() > 0) { + LOG.error("write TSDB failed num:" + ignoreErrorsResult.getFailed()); + } + } else { + List points = tsdbConverter.transRecord2Point(writerBuffer); + size = points.size(); + IgnoreErrorsResult ignoreErrorsResult = tsdb.putSync(points, IgnoreErrorsResult.class); + if (ignoreErrorsResult == null) { + LOG.error("Unexpected inner error for insert"); + } else if (ignoreErrorsResult.getFailed() > 0) { + LOG.error("write TSDB failed num:" + ignoreErrorsResult.getFailed()); + } + } + } else { + SummaryResult summaryResult; + if (multiField) { + List points = tsdbConverter.transRecord2MultiFieldPoint(writerBuffer, tableName); + size = points.size(); + summaryResult = tsdb.multiFieldPutSync(points, SummaryResult.class); + } else { + List points = tsdbConverter.transRecord2Point(writerBuffer); + size = points.size(); + summaryResult = tsdb.putSync(points, SummaryResult.class); + } + if (summaryResult.getFailed() > 0) { + LOG.error("write TSDB failed num:" + summaryResult.getFailed()); + throw DataXException.asDataXException(TSDBWriterErrorCode.RUNTIME_EXCEPTION, "Write TSDB failed", new Exception()); + } + } + return size; + } + @Override public void post() { } diff --git a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBWriterErrorCode.java b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBWriterErrorCode.java index f907fb67eb..ab4c389436 100755 --- a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBWriterErrorCode.java +++ b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/tsdbwriter/TSDBWriterErrorCode.java @@ -13,6 +13,7 @@ public enum TSDBWriterErrorCode implements ErrorCode { REQUIRED_VALUE("TSDBWriter-00", "Missing the necessary value"), + ILLEGAL_VALUE("TSDBWriter-01", "Illegal value"), RUNTIME_EXCEPTION("TSDBWriter-01", "Runtime exception"), RETRY_WRITER_EXCEPTION("TSDBWriter-02", "After repeated attempts, the write still fails"); diff --git a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/util/HttpUtils.java b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/util/HttpUtils.java index b81512f728..97055adc95 100644 --- a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/util/HttpUtils.java +++ b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/util/HttpUtils.java @@ -1,11 +1,14 @@ package com.alibaba.datax.plugin.writer.util; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; +import org.apache.commons.lang3.StringUtils; import org.apache.http.client.fluent.Content; import org.apache.http.client.fluent.Request; import org.apache.http.entity.ContentType; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.Map; import java.util.concurrent.TimeUnit; @@ -19,43 +22,44 @@ */ public final class HttpUtils { - public final static Charset UTF_8 = Charset.forName("UTF-8"); public final static int CONNECT_TIMEOUT_DEFAULT_IN_MILL = (int) TimeUnit.SECONDS.toMillis(60); public final static int SOCKET_TIMEOUT_DEFAULT_IN_MILL = (int) TimeUnit.SECONDS.toMillis(60); + private static final String CREDENTIALS_FORMAT = "%s:%s"; + private static final String BASIC_AUTHENTICATION_FORMAT = "Basic %s"; + private HttpUtils() { } - public static String get(String url) throws Exception { - Content content = Request.Get(url) + public static String get(String url, String username, String password) throws Exception { + final Request request = Request.Get(url) .connectTimeout(CONNECT_TIMEOUT_DEFAULT_IN_MILL) - .socketTimeout(SOCKET_TIMEOUT_DEFAULT_IN_MILL) + .socketTimeout(SOCKET_TIMEOUT_DEFAULT_IN_MILL); + addAuth(request, username, password); + Content content = request .execute() .returnContent(); if (content == null) { return null; } - return content.asString(UTF_8); - } - - public static String post(String url, Map params) throws Exception { - return post(url, JSON.toJSONString(params), CONNECT_TIMEOUT_DEFAULT_IN_MILL, SOCKET_TIMEOUT_DEFAULT_IN_MILL); + return content.asString(StandardCharsets.UTF_8); } - public static String post(String url, String params) throws Exception { - return post(url, params, CONNECT_TIMEOUT_DEFAULT_IN_MILL, SOCKET_TIMEOUT_DEFAULT_IN_MILL); + public static String post(String url, String username, String password, Map params) throws Exception { + return post(url, username, password, JSON.toJSONString(params), CONNECT_TIMEOUT_DEFAULT_IN_MILL, SOCKET_TIMEOUT_DEFAULT_IN_MILL); } - public static String post(String url, Map params, - int connectTimeoutInMill, int socketTimeoutInMill) throws Exception { - return post(url, JSON.toJSONString(params), connectTimeoutInMill, socketTimeoutInMill); + public static String post(String url, String username, String password, String params) throws Exception { + return post(url, username, password, params, CONNECT_TIMEOUT_DEFAULT_IN_MILL, SOCKET_TIMEOUT_DEFAULT_IN_MILL); } - public static String post(String url, String params, + public static String post(String url, String username, String password, String params, int connectTimeoutInMill, int socketTimeoutInMill) throws Exception { - Content content = Request.Post(url) + Request request = Request.Post(url) .connectTimeout(connectTimeoutInMill) - .socketTimeout(socketTimeoutInMill) + .socketTimeout(socketTimeoutInMill); + addAuth(request, username, password); + Content content = request .addHeader("Content-Type", "application/json") .bodyString(params, ContentType.APPLICATION_JSON) .execute() @@ -63,6 +67,22 @@ public static String post(String url, String params, if (content == null) { return null; } - return content.asString(UTF_8); + return content.asString(StandardCharsets.UTF_8); + } + + private static void addAuth(Request request, String username, String password) { + String authorization = generateHttpAuthorization(username, password); + if (authorization != null) { + request.setHeader("Authorization", authorization); + } + } + + private static String generateHttpAuthorization(String username, String password) { + if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) { + return null; + } + String credentials = String.format(CREDENTIALS_FORMAT, username, password); + credentials = Base64.getEncoder().encodeToString(credentials.getBytes()); + return String.format(BASIC_AUTHENTICATION_FORMAT, credentials); } } diff --git a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/util/TSDBUtils.java b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/util/TSDBUtils.java index ed01d877f2..83250b3289 100644 --- a/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/util/TSDBUtils.java +++ b/tsdbwriter/src/main/java/com/alibaba/datax/plugin/writer/util/TSDBUtils.java @@ -1,7 +1,8 @@ package com.alibaba.datax.plugin.writer.util; import com.alibaba.datax.plugin.writer.conn.DataPoint4TSDB; -import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson2.JSON; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,45 +23,56 @@ public final class TSDBUtils { private TSDBUtils() { } - public static String version(String address) { + public static String version(String address, String username, String password) { String url = String.format("%s/api/version", address); String rsp; try { - rsp = HttpUtils.get(url); + rsp = HttpUtils.get(url, username, password); } catch (Exception e) { throw new RuntimeException(e); } return rsp; } - public static String config(String address) { + public static String config(String address, String username, String password) { String url = String.format("%s/api/config", address); String rsp; try { - rsp = HttpUtils.get(url); + rsp = HttpUtils.get(url, username, password); } catch (Exception e) { throw new RuntimeException(e); } return rsp; } - public static boolean put(String address, List dps) { - return put(address, JSON.toJSON(dps)); + public static boolean put(String address, String database, String username, String password, List dps) { + return put(address, database, username, password, JSON.toJSON(dps)); } - public static boolean put(String address, DataPoint4TSDB dp) { - return put(address, JSON.toJSON(dp)); + public static boolean put(String address, String database, String username, String password, DataPoint4TSDB dp) { + return put(address, database, username, password, JSON.toJSON(dp)); } - private static boolean put(String address, Object o) { - return put(address, o.toString()); + private static boolean put(String address, String database, String username, String password, Object o) { + return put(address, database, username, password, o.toString()); } - public static boolean put(String address, String s) { - String url = String.format("%s/api/put", address); + public static boolean put(String address, String database, String username, String password, String s) { + return put(address, database, username, password, s, false); + } + + public static boolean mput(String address, String database, String username, String password, String s) { + return put(address, database, username, password, s, true); + } + + public static boolean put(String address, String database, String username, String password, String s, boolean multiField) { + String url = address + (multiField ? "/api/mput" : "/api/put"); + if (!StringUtils.isBlank(database)) { + url = url.concat("?db=" + database); + } String rsp; try { - rsp = HttpUtils.post(url, s); + rsp = HttpUtils.post(url, username, password, s); // If successful, the returned content should be null. assert rsp == null; } catch (Exception e) { diff --git a/tsdbwriter/src/main/resources/plugin.json b/tsdbwriter/src/main/resources/plugin.json index 78c8273f6e..26f927c2bd 100755 --- a/tsdbwriter/src/main/resources/plugin.json +++ b/tsdbwriter/src/main/resources/plugin.json @@ -6,5 +6,5 @@ "mechanism": "调用 TSDB 的 /api/put 接口,实现数据点的写入", "warn": "" }, - "developer": "Benedict Jin" + "developer": "alibaba" } diff --git a/tsdbwriter/src/test/java/com/alibaba/datax/plugin/writer/conn/TSDBConnectionTest.java b/tsdbwriter/src/test/java/com/alibaba/datax/plugin/writer/conn/TSDBConnectionTest.java index 455f4ce648..fada706eaa 100644 --- a/tsdbwriter/src/test/java/com/alibaba/datax/plugin/writer/conn/TSDBConnectionTest.java +++ b/tsdbwriter/src/test/java/com/alibaba/datax/plugin/writer/conn/TSDBConnectionTest.java @@ -19,12 +19,12 @@ public class TSDBConnectionTest { @Test public void testVersion() { - String version = new TSDBConnection(TSDB_ADDRESS).version(); + String version = new TSDBConnection(TSDB_ADDRESS,null,null,null).version(); Assert.assertNotNull(version); } @Test public void testIsSupported() { - Assert.assertTrue(new TSDBConnection(TSDB_ADDRESS).isSupported()); + Assert.assertTrue(new TSDBConnection(TSDB_ADDRESS,null,null,null).isSupported()); } } diff --git a/tsdbwriter/src/test/java/com/alibaba/datax/plugin/writer/util/HttpUtilsTest.java b/tsdbwriter/src/test/java/com/alibaba/datax/plugin/writer/util/HttpUtilsTest.java index 69f26b800e..1f8fb87007 100644 --- a/tsdbwriter/src/test/java/com/alibaba/datax/plugin/writer/util/HttpUtilsTest.java +++ b/tsdbwriter/src/test/java/com/alibaba/datax/plugin/writer/util/HttpUtilsTest.java @@ -24,7 +24,7 @@ public void testSimpleCase() throws Exception { Map params = new HashMap(); params.put("foo", "bar"); - String rsp = HttpUtils.post(url, params); + String rsp = HttpUtils.post(url, null,null,params); System.out.println(rsp); Assert.assertNotNull(rsp); } @@ -32,7 +32,7 @@ public void testSimpleCase() throws Exception { @Test public void testGet() throws Exception { String url = String.format("%s/api/version", Const.OPENTSDB_ADDRESS); - String rsp = HttpUtils.get(url); + String rsp = HttpUtils.get(url,null,null); System.out.println(rsp); Assert.assertNotNull(rsp); } diff --git a/tsdbwriter/src/test/java/com/alibaba/datax/plugin/writer/util/TSDBTest.java b/tsdbwriter/src/test/java/com/alibaba/datax/plugin/writer/util/TSDBTest.java index 7d22bb7263..8debf4062f 100644 --- a/tsdbwriter/src/test/java/com/alibaba/datax/plugin/writer/util/TSDBTest.java +++ b/tsdbwriter/src/test/java/com/alibaba/datax/plugin/writer/util/TSDBTest.java @@ -17,11 +17,11 @@ public class TSDBTest { @Test public void testVersion() { - String version = TSDBUtils.version(Const.TSDB_ADDRESS); + String version = TSDBUtils.version(Const.TSDB_ADDRESS,null,null); Assert.assertNotNull(version); System.out.println(version); - version = TSDBUtils.version(Const.OPENTSDB_ADDRESS); + version = TSDBUtils.version(Const.OPENTSDB_ADDRESS,null,null); Assert.assertNotNull(version); System.out.println(version); } diff --git a/txtfilereader/src/main/java/com/alibaba/datax/plugin/reader/txtfilereader/TxtFileReader.java b/txtfilereader/src/main/java/com/alibaba/datax/plugin/reader/txtfilereader/TxtFileReader.java index 914305c69c..a74ef8fcd0 100755 --- a/txtfilereader/src/main/java/com/alibaba/datax/plugin/reader/txtfilereader/TxtFileReader.java +++ b/txtfilereader/src/main/java/com/alibaba/datax/plugin/reader/txtfilereader/TxtFileReader.java @@ -182,6 +182,7 @@ private void validateParameter() { delimiterInStr)); } + UnstructuredStorageReaderUtil.validateCsvReaderConfig(this.originConfig); } @Override diff --git a/userGuid.md b/userGuid.md index 153c8111dd..badb1b4e75 100644 --- a/userGuid.md +++ b/userGuid.md @@ -10,14 +10,14 @@ DataX本身作为数据同步框架,将不同数据源的同步抽象为从源 - Linux - [JDK(1.8以上,推荐1.8) ](http://www.oracle.com/technetwork/cn/java/javase/downloads/index.html) -- [Python(推荐Python2.6.X) ](https://www.python.org/downloads/) +- [Python(2或3都可以) ](https://www.python.org/downloads/) - [Apache Maven 3.x](https://maven.apache.org/download.cgi) (Compile DataX) # Quick Start * 工具部署 - * 方法一、直接下载DataX工具包:[DataX下载地址](http://datax-opensource.oss-cn-hangzhou.aliyuncs.com/datax.tar.gz) + * 方法一、直接下载DataX工具包:[DataX下载地址](https://datax-opensource.oss-cn-hangzhou.aliyuncs.com/202309/datax.tar.gz) 下载后解压至本地某个目录,进入bin目录,即可运行同步作业: